@lpm-registry/cli 0.58.0 → 0.59.1

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 (62) hide show
  1. package/README.md +25 -44
  2. package/bin/lpm.js +2 -354
  3. package/bin/lpx.js +4 -0
  4. package/bin/native.js +154 -0
  5. package/package.json +42 -82
  6. package/scripts/install-binary.js +219 -550
  7. package/CHANGELOG.md +0 -197
  8. package/LICENSE +0 -15
  9. package/index.d.ts +0 -131
  10. package/index.js +0 -31
  11. package/lib/api.js +0 -398
  12. package/lib/commands/add.js +0 -1217
  13. package/lib/commands/audit.js +0 -283
  14. package/lib/commands/cache.js +0 -209
  15. package/lib/commands/check-name.js +0 -112
  16. package/lib/commands/config.js +0 -186
  17. package/lib/commands/doctor.js +0 -140
  18. package/lib/commands/info.js +0 -215
  19. package/lib/commands/init.js +0 -146
  20. package/lib/commands/install.js +0 -264
  21. package/lib/commands/login.js +0 -595
  22. package/lib/commands/logout.js +0 -94
  23. package/lib/commands/marketplace-compare.js +0 -164
  24. package/lib/commands/marketplace-earnings.js +0 -89
  25. package/lib/commands/mcp-setup.js +0 -363
  26. package/lib/commands/npmrc.js +0 -178
  27. package/lib/commands/open.js +0 -82
  28. package/lib/commands/outdated.js +0 -291
  29. package/lib/commands/pool-stats.js +0 -100
  30. package/lib/commands/publish.js +0 -949
  31. package/lib/commands/quality.js +0 -211
  32. package/lib/commands/remove.js +0 -82
  33. package/lib/commands/run.js +0 -14
  34. package/lib/commands/search.js +0 -143
  35. package/lib/commands/setup.js +0 -221
  36. package/lib/commands/skills.js +0 -898
  37. package/lib/commands/token-rotate.js +0 -25
  38. package/lib/commands/uninstall.js +0 -107
  39. package/lib/commands/whoami.js +0 -129
  40. package/lib/config.js +0 -328
  41. package/lib/constants.js +0 -190
  42. package/lib/ecosystem.js +0 -501
  43. package/lib/editor-skills.js +0 -321
  44. package/lib/editors.js +0 -215
  45. package/lib/import-rewriter.js +0 -364
  46. package/lib/install-targets/mcp-server.js +0 -245
  47. package/lib/install-targets/vscode-extension.js +0 -178
  48. package/lib/install-targets.js +0 -82
  49. package/lib/integrity.js +0 -179
  50. package/lib/lpm-config-prompts.js +0 -102
  51. package/lib/lpm-config.js +0 -408
  52. package/lib/oidc.js +0 -166
  53. package/lib/project-utils.js +0 -152
  54. package/lib/quality/checks.js +0 -687
  55. package/lib/quality/display.js +0 -155
  56. package/lib/quality/score.js +0 -118
  57. package/lib/quality/swift-checks.js +0 -447
  58. package/lib/safe-path.js +0 -180
  59. package/lib/secure-store.js +0 -321
  60. package/lib/swift-project.js +0 -637
  61. package/lib/ui.js +0 -40
  62. package/scripts/__tests__/install-binary.test.js +0 -346
@@ -1,898 +0,0 @@
1
- /**
2
- * `lpm skills` - Manage Agent Skills for LPM packages.
3
- *
4
- * Subcommands:
5
- * validate - Validate .lpm/skills/ files in current directory
6
- * install - Fetch and install skills from registry
7
- * list - List available skills for installed packages
8
- * clean - Remove locally installed skills
9
- */
10
-
11
- import fs from "node:fs"
12
- import path from "node:path"
13
- import chalk from "chalk"
14
- import { get } from "../api.js"
15
- import { autoIntegrateSkills } from "../editor-skills.js"
16
- import { createSpinner, log, printHeader } from "../ui.js"
17
-
18
- // ============================================================================
19
- // Shared constants and utilities
20
- // ============================================================================
21
-
22
- const SKILLS_DIR = ".lpm/skills"
23
- const SKILLS_PATTERN = /\.md$/
24
-
25
- // Same limits as server-side validation
26
- const MAX_SKILL_SIZE = 15 * 1024
27
- const MAX_TOTAL_SIZE = 100 * 1024
28
- const MAX_SKILLS_COUNT = 10
29
- const MIN_CONTENT_LENGTH = 100
30
-
31
- const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/
32
- const NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/
33
-
34
- const BLOCKED_PATTERNS = [
35
- { pattern: /curl\s+.*\|\s*(ba)?sh/i, category: "shell-injection" },
36
- { pattern: /wget\s+.*\|\s*(ba)?sh/i, category: "shell-injection" },
37
- { pattern: /\beval\s*\(/i, category: "shell-injection" },
38
- { pattern: /child_process/i, category: "shell-injection" },
39
- {
40
- pattern: /process\.env\.[A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)/i,
41
- category: "env-exfiltration",
42
- },
43
- {
44
- pattern: /ignore\s+(?:all\s+)?previous\s+instructions/i,
45
- category: "prompt-injection",
46
- },
47
- { pattern: /you\s+are\s+now\s+/i, category: "prompt-injection" },
48
- { pattern: /\[INST\]/i, category: "prompt-injection" },
49
- { pattern: /<<SYS>>/i, category: "prompt-injection" },
50
- {
51
- pattern: /forget\s+(?:all\s+)?(?:your\s+)?(?:previous\s+)?instructions/i,
52
- category: "prompt-injection",
53
- },
54
- { pattern: /fs\.(unlink|rmdir|rm)Sync?\s*\(/i, category: "fs-attack" },
55
- { pattern: /\brimraf\b/i, category: "fs-attack" },
56
- { pattern: /rm\s+-rf\s+\//i, category: "fs-attack" },
57
- ]
58
-
59
- /**
60
- * Parse simple YAML frontmatter (no external dependency).
61
- */
62
- function parseSimpleYaml(yamlStr) {
63
- const result = {}
64
- const lines = yamlStr.split("\n")
65
- let currentKey = null
66
- let inArray = false
67
-
68
- for (const line of lines) {
69
- const trimmed = line.trim()
70
- if (!trimmed || trimmed.startsWith("#")) continue
71
-
72
- if (inArray && trimmed.startsWith("- ")) {
73
- const value = trimmed
74
- .slice(2)
75
- .trim()
76
- .replace(/^["']|["']$/g, "")
77
- if (currentKey && Array.isArray(result[currentKey])) {
78
- result[currentKey].push(value)
79
- }
80
- continue
81
- }
82
-
83
- const colonIndex = trimmed.indexOf(":")
84
- if (colonIndex > 0) {
85
- const key = trimmed.slice(0, colonIndex).trim()
86
- const value = trimmed.slice(colonIndex + 1).trim()
87
-
88
- currentKey = key
89
- inArray = false
90
-
91
- if (!value) {
92
- result[key] = []
93
- inArray = true
94
- } else if (value.startsWith("[") && value.endsWith("]")) {
95
- result[key] = value
96
- .slice(1, -1)
97
- .split(",")
98
- .map(s => s.trim().replace(/^["']|["']$/g, ""))
99
- .filter(Boolean)
100
- } else {
101
- result[key] = value.replace(/^["']|["']$/g, "")
102
- inArray = false
103
- }
104
- }
105
- }
106
-
107
- return result
108
- }
109
-
110
- /**
111
- * Parse a skill file into structured data.
112
- */
113
- function parseSkill(rawContent, filePath) {
114
- const sizeBytes = Buffer.byteLength(rawContent, "utf-8")
115
-
116
- const match = rawContent.match(FRONTMATTER_REGEX)
117
- if (!match) {
118
- return {
119
- skill: null,
120
- error: `${filePath}: Missing or invalid YAML frontmatter (must start with --- and end with ---)`,
121
- }
122
- }
123
-
124
- const [, yamlStr, markdownBody] = match
125
-
126
- let frontmatter
127
- try {
128
- frontmatter = parseSimpleYaml(yamlStr)
129
- } catch {
130
- return {
131
- skill: null,
132
- error: `${filePath}: Failed to parse YAML frontmatter`,
133
- }
134
- }
135
-
136
- if (!frontmatter.name || typeof frontmatter.name !== "string") {
137
- return {
138
- skill: null,
139
- error: `${filePath}: Missing required "name" field in frontmatter`,
140
- }
141
- }
142
-
143
- if (!frontmatter.description || typeof frontmatter.description !== "string") {
144
- return {
145
- skill: null,
146
- error: `${filePath}: Missing required "description" field in frontmatter`,
147
- }
148
- }
149
-
150
- if (!NAME_PATTERN.test(frontmatter.name)) {
151
- return {
152
- skill: null,
153
- error: `${filePath}: Skill name "${frontmatter.name}" must be lowercase letters, numbers, and hyphens only`,
154
- }
155
- }
156
-
157
- const globs = frontmatter.globs
158
- if (globs && !Array.isArray(globs)) {
159
- return {
160
- skill: null,
161
- error: `${filePath}: "globs" field must be an array of strings`,
162
- }
163
- }
164
-
165
- // Optional version field (package version when skill was authored)
166
- const version = frontmatter.version
167
- if (version && typeof version !== "string") {
168
- return {
169
- skill: null,
170
- error: `${filePath}: "version" field must be a string`,
171
- }
172
- }
173
-
174
- const content = markdownBody.trim()
175
-
176
- return {
177
- skill: {
178
- name: frontmatter.name,
179
- description: frontmatter.description,
180
- globs: globs && globs.length > 0 ? globs : null,
181
- version: version || null,
182
- content,
183
- rawContent,
184
- sizeBytes,
185
- },
186
- error: null,
187
- }
188
- }
189
-
190
- /**
191
- * Validate a parsed skill against size and security constraints.
192
- */
193
- function validateSkill(skill, filePath) {
194
- const errors = []
195
-
196
- if (skill.sizeBytes > MAX_SKILL_SIZE) {
197
- errors.push(
198
- `${filePath}: File size ${(skill.sizeBytes / 1024).toFixed(1)}KB exceeds maximum ${MAX_SKILL_SIZE / 1024}KB`,
199
- )
200
- }
201
-
202
- if (skill.content.length < MIN_CONTENT_LENGTH) {
203
- errors.push(
204
- `${filePath}: Content is too short (${skill.content.length} chars, minimum ${MIN_CONTENT_LENGTH})`,
205
- )
206
- }
207
-
208
- if (skill.description.length < 10) {
209
- errors.push(`${filePath}: Description is too short (minimum 10 characters)`)
210
- }
211
-
212
- if (skill.description.length > 500) {
213
- errors.push(`${filePath}: Description is too long (maximum 500 characters)`)
214
- }
215
-
216
- for (const { pattern, category } of BLOCKED_PATTERNS) {
217
- if (pattern.test(skill.rawContent)) {
218
- errors.push(
219
- `${filePath}: Blocked pattern detected (${category}): ${pattern.source}`,
220
- )
221
- }
222
- }
223
-
224
- return { valid: errors.length === 0, errors }
225
- }
226
-
227
- /**
228
- * Validate a batch of skills (count, total size, duplicates).
229
- */
230
- function validateBatch(skills) {
231
- const errors = []
232
-
233
- if (skills.length > MAX_SKILLS_COUNT) {
234
- errors.push(
235
- `Too many skills (${skills.length}), maximum is ${MAX_SKILLS_COUNT}`,
236
- )
237
- }
238
-
239
- const totalSize = skills.reduce((sum, s) => sum + s.sizeBytes, 0)
240
- if (totalSize > MAX_TOTAL_SIZE) {
241
- errors.push(
242
- `Total skills size ${(totalSize / 1024).toFixed(1)}KB exceeds maximum ${MAX_TOTAL_SIZE / 1024}KB`,
243
- )
244
- }
245
-
246
- const names = new Set()
247
- for (const skill of skills) {
248
- if (names.has(skill.name)) {
249
- errors.push(`Duplicate skill name: "${skill.name}"`)
250
- }
251
- names.add(skill.name)
252
- }
253
-
254
- return { valid: errors.length === 0, errors }
255
- }
256
-
257
- /**
258
- * Read and find all .md files in the .lpm/skills/ directory.
259
- */
260
- function findSkillFiles(baseDir) {
261
- const skillsDir = path.join(baseDir, SKILLS_DIR)
262
- if (!fs.existsSync(skillsDir)) {
263
- return { dir: skillsDir, files: [] }
264
- }
265
-
266
- const entries = fs.readdirSync(skillsDir)
267
- const files = entries
268
- .filter(f => SKILLS_PATTERN.test(f))
269
- .map(f => path.join(skillsDir, f))
270
-
271
- return { dir: skillsDir, files }
272
- }
273
-
274
- /**
275
- * Ensure ".lpm" is in package.json "files" array.
276
- * Returns "added" if it was added, "already-present" if it was already there,
277
- * "missing-files-field" if there's no "files" field (npm includes everything).
278
- */
279
- function ensureLpmInFiles() {
280
- const packageJsonPath = path.resolve(process.cwd(), "package.json")
281
- if (!fs.existsSync(packageJsonPath)) return "missing-files-field"
282
-
283
- const raw = fs.readFileSync(packageJsonPath, "utf-8")
284
- let pkg
285
- try {
286
- pkg = JSON.parse(raw)
287
- } catch {
288
- return "missing-files-field"
289
- }
290
-
291
- // No "files" field means npm includes everything by default
292
- if (!Array.isArray(pkg.files)) return "missing-files-field"
293
-
294
- // Check if .lpm is already included (exact or glob)
295
- const hasLpm = pkg.files.some(
296
- f => f === ".lpm" || f === ".lpm/" || f === ".lpm/**",
297
- )
298
- if (hasLpm) return "already-present"
299
-
300
- // Add ".lpm" to the files array
301
- pkg.files.push(".lpm")
302
-
303
- // Preserve formatting: detect indent from the original file
304
- const indentMatch = raw.match(/^(\s+)"/)
305
- const indent = indentMatch ? indentMatch[1] : "\t"
306
- fs.writeFileSync(
307
- packageJsonPath,
308
- `${JSON.stringify(pkg, null, indent)}\n`,
309
- "utf-8",
310
- )
311
-
312
- return "added"
313
- }
314
-
315
- /**
316
- * Read LPM packages from the nearest package.json.
317
- */
318
- function getLpmDependencies() {
319
- const packageJsonPath = path.resolve(process.cwd(), "package.json")
320
- if (!fs.existsSync(packageJsonPath)) return []
321
-
322
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
323
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
324
-
325
- return Object.entries(allDeps)
326
- .filter(([name, version]) => {
327
- if (!name.startsWith("@lpm.dev/")) return false
328
- // Skip local file/link references — they have their own .lpm/skills/
329
- if (
330
- version.startsWith("file:") ||
331
- version.startsWith("link:") ||
332
- version.startsWith("workspace:")
333
- ) {
334
- return false
335
- }
336
- return true
337
- })
338
- .map(([name, version]) => ({
339
- fullName: name,
340
- shortName: name.replace("@lpm.dev/", ""),
341
- version: version.replace(/^[\^~>=<]+/, ""),
342
- }))
343
- }
344
-
345
- /**
346
- * Parse a package name input (accepts "owner.pkg" or "@lpm.dev/owner.pkg").
347
- */
348
- function parsePkgName(input) {
349
- let cleaned = input
350
- if (cleaned.startsWith("@lpm.dev/")) {
351
- cleaned = cleaned.replace("@lpm.dev/", "")
352
- }
353
- const dotIndex = cleaned.indexOf(".")
354
- if (dotIndex === -1 || dotIndex === 0 || dotIndex === cleaned.length - 1) {
355
- return null
356
- }
357
- return cleaned
358
- }
359
-
360
- // ============================================================================
361
- // Subcommand: validate
362
- // ============================================================================
363
-
364
- /**
365
- * lpm skills validate
366
- * Validates .lpm/skills/ files in the current directory.
367
- */
368
- export async function skillsValidate(options = {}) {
369
- const isJson = options.json
370
-
371
- if (!isJson) printHeader()
372
-
373
- const { dir, files } = findSkillFiles(process.cwd())
374
-
375
- if (files.length === 0) {
376
- if (isJson) {
377
- console.log(
378
- JSON.stringify({
379
- valid: false,
380
- skills: [],
381
- errors: [`No skill files found in ${SKILLS_DIR}/`],
382
- }),
383
- )
384
- } else {
385
- log.warn(`No skill files found in ${SKILLS_DIR}/`)
386
- console.log(
387
- chalk.dim(
388
- ` Create .lpm/skills/*.md files to add Agent Skills to your package.`,
389
- ),
390
- )
391
- console.log(chalk.dim(` See: https://lpm.dev/docs/packages/skills`))
392
- }
393
- return
394
- }
395
-
396
- if (!isJson) {
397
- console.log(
398
- ` Validating ${files.length} skill file${files.length !== 1 ? "s" : ""} in ${dir}\n`,
399
- )
400
- }
401
-
402
- const parsed = []
403
- const allErrors = []
404
-
405
- for (const filePath of files) {
406
- const rawContent = fs.readFileSync(filePath, "utf-8")
407
- const relativePath = path.relative(process.cwd(), filePath)
408
- const { skill, error } = parseSkill(rawContent, relativePath)
409
-
410
- if (error) {
411
- allErrors.push(error)
412
- if (!isJson) {
413
- console.log(` ${chalk.red("✗")} ${relativePath}`)
414
- console.log(` ${chalk.red(error)}`)
415
- }
416
- continue
417
- }
418
-
419
- const validation = validateSkill(skill, relativePath)
420
- if (!validation.valid) {
421
- allErrors.push(...validation.errors)
422
- if (!isJson) {
423
- console.log(` ${chalk.red("✗")} ${relativePath}`)
424
- for (const err of validation.errors) {
425
- console.log(` ${chalk.red(err)}`)
426
- }
427
- }
428
- } else {
429
- parsed.push(skill)
430
- if (!isJson) {
431
- const globInfo = skill.globs
432
- ? chalk.dim(
433
- ` (${skill.globs.length} glob${skill.globs.length !== 1 ? "s" : ""})`,
434
- )
435
- : ""
436
- console.log(` ${chalk.green("✓")} ${relativePath}${globInfo}`)
437
- console.log(` ${chalk.dim(skill.description)}`)
438
- }
439
- }
440
- }
441
-
442
- // Batch validation
443
- if (parsed.length > 0) {
444
- const batchResult = validateBatch(parsed)
445
- if (!batchResult.valid) {
446
- allErrors.push(...batchResult.errors)
447
- if (!isJson) {
448
- console.log("")
449
- for (const err of batchResult.errors) {
450
- console.log(` ${chalk.red("✗")} ${err}`)
451
- }
452
- }
453
- }
454
- }
455
-
456
- const isValid = allErrors.length === 0 && parsed.length > 0
457
-
458
- if (isJson) {
459
- console.log(
460
- JSON.stringify(
461
- {
462
- valid: isValid,
463
- skills: parsed.map(s => ({
464
- name: s.name,
465
- description: s.description,
466
- globs: s.globs,
467
- sizeBytes: s.sizeBytes,
468
- })),
469
- errors: allErrors,
470
- },
471
- null,
472
- 2,
473
- ),
474
- )
475
- return
476
- }
477
-
478
- console.log("")
479
- if (isValid) {
480
- const totalSize = parsed.reduce((sum, s) => sum + s.sizeBytes, 0)
481
- log.success(
482
- `${parsed.length} skill${parsed.length !== 1 ? "s" : ""} valid (${(totalSize / 1024).toFixed(1)}KB total)`,
483
- )
484
- console.log(
485
- chalk.dim(
486
- ` Quality impact: +7 pts (has-skills)${parsed.length >= 3 ? " +3 pts (comprehensive)" : ""}`,
487
- ),
488
- )
489
-
490
- // Check if .lpm is in package.json files field
491
- const fixed = ensureLpmInFiles()
492
- if (fixed === "added") {
493
- console.log("")
494
- log.success(
495
- `Added ".lpm" to package.json "files" field so skills are included in the tarball.`,
496
- )
497
- } else if (fixed === "missing-files-field") {
498
- // No "files" field means npm includes everything - skills are included
499
- } else if (fixed === "already-present") {
500
- // Already configured correctly
501
- }
502
- } else {
503
- log.error(
504
- `Validation failed with ${allErrors.length} error${allErrors.length !== 1 ? "s" : ""}.`,
505
- )
506
- }
507
- console.log("")
508
- }
509
-
510
- // ============================================================================
511
- // Subcommand: install
512
- // ============================================================================
513
-
514
- /**
515
- * lpm skills install [package]
516
- * Fetches and installs skills from the registry.
517
- */
518
- export async function skillsInstall(packageInput, options = {}) {
519
- const isJson = options.json
520
-
521
- if (!isJson) printHeader()
522
-
523
- let packages = []
524
-
525
- if (packageInput) {
526
- const cleaned = parsePkgName(packageInput)
527
- if (!cleaned) {
528
- if (isJson) {
529
- console.log(
530
- JSON.stringify({
531
- error: "Invalid package name format. Use: owner.package-name",
532
- }),
533
- )
534
- } else {
535
- log.error("Invalid package name format. Use: owner.package-name")
536
- }
537
- process.exit(1)
538
- }
539
- packages = [{ shortName: cleaned, fullName: `@lpm.dev/${cleaned}` }]
540
- } else {
541
- // Read from package.json
542
- packages = getLpmDependencies()
543
- if (packages.length === 0) {
544
- if (isJson) {
545
- console.log(
546
- JSON.stringify({
547
- installed: 0,
548
- packages: [],
549
- errors: ["No @lpm.dev/* packages found in package.json."],
550
- }),
551
- )
552
- } else {
553
- log.info("No @lpm.dev/* packages found in package.json.")
554
- }
555
- return
556
- }
557
- }
558
-
559
- const spinner = isJson
560
- ? null
561
- : createSpinner(
562
- `Fetching skills for ${packages.length} package${packages.length !== 1 ? "s" : ""}...`,
563
- ).start()
564
-
565
- const results = []
566
- const errors = []
567
-
568
- for (const pkg of packages) {
569
- try {
570
- const response = await get(
571
- `/skills?name=${encodeURIComponent(pkg.shortName)}${pkg.version ? `&version=${encodeURIComponent(pkg.version)}` : ""}`,
572
- )
573
-
574
- if (!response.ok) {
575
- if (response.status === 404) {
576
- results.push({ name: pkg.fullName, skills: [], status: "not-found" })
577
- continue
578
- }
579
- const data = await response.json().catch(() => ({}))
580
- errors.push(
581
- `${pkg.fullName}: ${data.error || `HTTP ${response.status}`}`,
582
- )
583
- continue
584
- }
585
-
586
- const data = await response.json()
587
-
588
- if (!data.skills || data.skills.length === 0) {
589
- results.push({ name: pkg.fullName, skills: [], status: "no-skills" })
590
- continue
591
- }
592
-
593
- // Write skills to .lpm/skills/{owner}.{package-name}/
594
- const targetDir = path.join(process.cwd(), SKILLS_DIR, pkg.shortName)
595
- fs.mkdirSync(targetDir, { recursive: true })
596
-
597
- for (const skill of data.skills) {
598
- const fileName = `${skill.name}.md`
599
- const content = skill.rawContent || skill.content
600
- fs.writeFileSync(path.join(targetDir, fileName), content, "utf-8")
601
- }
602
-
603
- results.push({
604
- name: pkg.fullName,
605
- skills: data.skills.map(s => s.name),
606
- status: "installed",
607
- path: targetDir,
608
- })
609
- } catch (err) {
610
- errors.push(`${pkg.fullName}: ${err.message}`)
611
- }
612
- }
613
-
614
- // Ensure .lpm/skills/ is in .gitignore
615
- ensureGitignore()
616
-
617
- if (spinner) spinner.stop()
618
-
619
- if (isJson) {
620
- const hasInstalled = results.some(r => r.status === "installed")
621
- let editorSetup = null
622
- if (hasInstalled && options.editorSetup !== false) {
623
- const result = autoIntegrateSkills({ json: true })
624
- editorSetup = result
625
- }
626
- console.log(
627
- JSON.stringify(
628
- {
629
- installed: results.filter(r => r.status === "installed").length,
630
- packages: results,
631
- errors,
632
- ...(editorSetup ? { editorSetup } : {}),
633
- },
634
- null,
635
- 2,
636
- ),
637
- )
638
- return
639
- }
640
-
641
- // Print results
642
- const installed = results.filter(r => r.status === "installed")
643
- const noSkills = results.filter(
644
- r => r.status === "no-skills" || r.status === "not-found",
645
- )
646
-
647
- if (installed.length > 0) {
648
- console.log("")
649
- for (const r of installed) {
650
- log.success(
651
- `${r.name}: ${r.skills.length} skill${r.skills.length !== 1 ? "s" : ""} installed`,
652
- )
653
- for (const name of r.skills) {
654
- console.log(chalk.dim(` ${name}.md`))
655
- }
656
- }
657
- }
658
-
659
- if (noSkills.length > 0) {
660
- console.log("")
661
- for (const r of noSkills) {
662
- log.dim(` ${r.name}: no skills available`)
663
- }
664
- }
665
-
666
- if (errors.length > 0) {
667
- console.log("")
668
- for (const err of errors) {
669
- log.error(err)
670
- }
671
- }
672
-
673
- if (installed.length > 0) {
674
- console.log("")
675
- console.log(chalk.dim(` Skills saved to ${SKILLS_DIR}/`))
676
-
677
- // Auto-integrate with detected AI editors (unless --no-editor-setup)
678
- if (options.editorSetup !== false) {
679
- autoIntegrateSkills({ json: false })
680
- } else {
681
- printWiringInstructions()
682
- }
683
- }
684
-
685
- console.log("")
686
- }
687
-
688
- /**
689
- * Print instructions for wiring skills into AI tools.
690
- */
691
- function printWiringInstructions() {
692
- const skillsPath = path.join(process.cwd(), SKILLS_DIR)
693
- if (!fs.existsSync(skillsPath)) return
694
-
695
- console.log("")
696
- console.log(chalk.dim(" To use these skills with your AI coding tool:"))
697
- console.log("")
698
- console.log(chalk.dim(" Claude Code:"))
699
- console.log(
700
- chalk.dim(
701
- ` Add to CLAUDE.md: "See ${SKILLS_DIR}/ for package-specific guidelines"`,
702
- ),
703
- )
704
- console.log("")
705
- console.log(chalk.dim(" Cursor:"))
706
- console.log(
707
- chalk.dim(` Copy files to .cursor/rules/ or reference in .cursorrules`),
708
- )
709
- }
710
-
711
- /**
712
- * Ensure .lpm/skills/ is listed in .gitignore.
713
- */
714
- function ensureGitignore() {
715
- const gitignorePath = path.join(process.cwd(), ".gitignore")
716
-
717
- if (!fs.existsSync(gitignorePath)) return
718
-
719
- const content = fs.readFileSync(gitignorePath, "utf-8")
720
- if (content.includes(".lpm/skills")) return
721
-
722
- fs.appendFileSync(
723
- gitignorePath,
724
- "\n# LPM Agent Skills (fetched from registry)\n.lpm/skills/\n",
725
- )
726
- }
727
-
728
- // ============================================================================
729
- // Subcommand: list
730
- // ============================================================================
731
-
732
- /**
733
- * lpm skills list
734
- * Lists available skills for installed @lpm.dev/* packages.
735
- */
736
- export async function skillsList(options = {}) {
737
- const isJson = options.json
738
-
739
- if (!isJson) printHeader()
740
-
741
- const packages = getLpmDependencies()
742
-
743
- if (packages.length === 0) {
744
- if (isJson) {
745
- console.log(JSON.stringify({ packages: [] }))
746
- } else {
747
- log.info("No @lpm.dev/* packages found in package.json.")
748
- }
749
- return
750
- }
751
-
752
- const spinner = isJson
753
- ? null
754
- : createSpinner(
755
- `Checking skills for ${packages.length} package${packages.length !== 1 ? "s" : ""}...`,
756
- ).start()
757
-
758
- const results = []
759
-
760
- for (const pkg of packages) {
761
- try {
762
- const response = await get(
763
- `/skills?name=${encodeURIComponent(pkg.shortName)}${pkg.version ? `&version=${encodeURIComponent(pkg.version)}` : ""}`,
764
- )
765
-
766
- if (!response.ok) {
767
- results.push({
768
- name: pkg.fullName,
769
- version: pkg.version,
770
- skillsCount: 0,
771
- installed: false,
772
- })
773
- continue
774
- }
775
-
776
- const data = await response.json()
777
-
778
- // Check if installed locally
779
- const localDir = path.join(process.cwd(), SKILLS_DIR, pkg.shortName)
780
- const installed = fs.existsSync(localDir)
781
-
782
- results.push({
783
- name: pkg.fullName,
784
- version: data.version || pkg.version,
785
- skillsCount: data.skillsCount || 0,
786
- skills: (data.skills || []).map(s => s.name),
787
- installed,
788
- })
789
- } catch {
790
- results.push({
791
- name: pkg.fullName,
792
- version: pkg.version,
793
- skillsCount: 0,
794
- installed: false,
795
- })
796
- }
797
- }
798
-
799
- if (spinner) spinner.stop()
800
-
801
- if (isJson) {
802
- console.log(JSON.stringify({ packages: results }, null, 2))
803
- return
804
- }
805
-
806
- console.log("")
807
-
808
- const withSkills = results.filter(r => r.skillsCount > 0)
809
- const withoutSkills = results.filter(r => r.skillsCount === 0)
810
-
811
- if (withSkills.length > 0) {
812
- console.log(chalk.bold(" Packages with Agent Skills:\n"))
813
- for (const r of withSkills) {
814
- const installedBadge = r.installed
815
- ? chalk.green(" [installed]")
816
- : chalk.dim(" [not installed]")
817
- console.log(
818
- ` ${r.name}@${r.version} ${chalk.cyan(`${r.skillsCount} skill${r.skillsCount !== 1 ? "s" : ""}`)}${installedBadge}`,
819
- )
820
- }
821
- }
822
-
823
- if (withoutSkills.length > 0) {
824
- if (withSkills.length > 0) console.log("")
825
- console.log(chalk.dim(" No skills available:"))
826
- for (const r of withoutSkills) {
827
- console.log(chalk.dim(` ${r.name}@${r.version}`))
828
- }
829
- }
830
-
831
- if (withSkills.length > 0) {
832
- const notInstalled = withSkills.filter(r => !r.installed)
833
- if (notInstalled.length > 0) {
834
- console.log("")
835
- console.log(
836
- chalk.dim(
837
- ` Run ${chalk.cyan("lpm skills install")} to download all available skills.`,
838
- ),
839
- )
840
- }
841
- }
842
-
843
- console.log("")
844
- }
845
-
846
- // ============================================================================
847
- // Subcommand: clean
848
- // ============================================================================
849
-
850
- /**
851
- * lpm skills clean
852
- * Removes locally installed skills (.lpm/skills/ directory).
853
- */
854
- export async function skillsClean(options = {}) {
855
- const isJson = options.json
856
-
857
- if (!isJson) printHeader()
858
-
859
- const skillsPath = path.join(process.cwd(), SKILLS_DIR)
860
-
861
- if (!fs.existsSync(skillsPath)) {
862
- if (isJson) {
863
- console.log(
864
- JSON.stringify({
865
- cleaned: false,
866
- message: "No .lpm/skills/ directory found.",
867
- }),
868
- )
869
- } else {
870
- log.info("No .lpm/skills/ directory found. Nothing to clean.")
871
- }
872
- return
873
- }
874
-
875
- // Count files before removing
876
- let fileCount = 0
877
- const countFiles = dir => {
878
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
879
- if (entry.isDirectory()) {
880
- countFiles(path.join(dir, entry.name))
881
- } else {
882
- fileCount++
883
- }
884
- }
885
- }
886
- countFiles(skillsPath)
887
-
888
- fs.rmSync(skillsPath, { recursive: true, force: true })
889
-
890
- if (isJson) {
891
- console.log(JSON.stringify({ cleaned: true, filesRemoved: fileCount }))
892
- } else {
893
- log.success(
894
- `Removed ${SKILLS_DIR}/ (${fileCount} file${fileCount !== 1 ? "s" : ""})`,
895
- )
896
- console.log("")
897
- }
898
- }