@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,547 @@
1
+ import http from "node:http"
2
+ import open from "open"
3
+ import { request } from "../api.js"
4
+ import { getRegistryUrl, setToken } from "../config.js"
5
+ import { WARNING_MESSAGES } from "../constants.js"
6
+ import { createSpinner, log, printHeader } from "../ui.js"
7
+
8
+ function getErrorHtml(title, message) {
9
+ return `
10
+ <!DOCTYPE html>
11
+ <html>
12
+ <head>
13
+ <title>LPM - ${title}</title>
14
+ <link rel="preconnect" href="https://fonts.googleapis.com">
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet">
17
+ <style>
18
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
19
+
20
+ :root {
21
+ --bg-primary: #0a0a0b;
22
+ --bg-card: #111113;
23
+ --bg-subtle: #18181b;
24
+ --border: #27272a;
25
+ --text-primary: #fafafa;
26
+ --text-secondary: #a1a1aa;
27
+ --text-muted: #71717a;
28
+ --error: #ef4444;
29
+ --error-glow: rgba(239, 68, 68, 0.15);
30
+ }
31
+
32
+ body {
33
+ font-family: 'Outfit', system-ui, sans-serif;
34
+ min-height: 100vh;
35
+ display: flex;
36
+ justify-content: center;
37
+ align-items: center;
38
+ background: var(--bg-primary);
39
+ background-image:
40
+ radial-gradient(ellipse 80% 50% at 50% -20%, var(--error-glow), transparent),
41
+ radial-gradient(circle at 50% 50%, var(--bg-primary), var(--bg-primary));
42
+ padding: 1.5rem;
43
+ }
44
+
45
+ .container {
46
+ width: 100%;
47
+ max-width: 420px;
48
+ animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
49
+ }
50
+
51
+ @keyframes fadeInUp {
52
+ from { opacity: 0; transform: translateY(20px); }
53
+ to { opacity: 1; transform: translateY(0); }
54
+ }
55
+
56
+ .card {
57
+ background: var(--bg-card);
58
+ border: 1px solid var(--border);
59
+ border-radius: 16px;
60
+ padding: 2.5rem 2rem;
61
+ text-align: center;
62
+ position: relative;
63
+ overflow: hidden;
64
+ }
65
+
66
+ .card::before {
67
+ content: '';
68
+ position: absolute;
69
+ top: 0;
70
+ left: 50%;
71
+ transform: translateX(-50%);
72
+ width: 200px;
73
+ height: 1px;
74
+ background: linear-gradient(90deg, transparent, var(--error), transparent);
75
+ opacity: 0.6;
76
+ }
77
+
78
+ .icon-wrapper {
79
+ width: 80px;
80
+ height: 80px;
81
+ margin: 0 auto 1.5rem;
82
+ position: relative;
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ }
87
+
88
+ .icon-circle {
89
+ width: 64px;
90
+ height: 64px;
91
+ background: var(--error);
92
+ border-radius: 50%;
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ box-shadow: 0 0 40px var(--error-glow), 0 0 80px var(--error-glow);
97
+ animation: scaleIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s both;
98
+ }
99
+
100
+ @keyframes scaleIn {
101
+ from { transform: scale(0); }
102
+ to { transform: scale(1); }
103
+ }
104
+
105
+ .x-icon {
106
+ width: 28px;
107
+ height: 28px;
108
+ stroke: var(--bg-primary);
109
+ stroke-width: 3;
110
+ stroke-linecap: round;
111
+ fill: none;
112
+ }
113
+
114
+ .title {
115
+ font-size: 1.5rem;
116
+ font-weight: 600;
117
+ color: var(--text-primary);
118
+ margin-bottom: 0.75rem;
119
+ letter-spacing: -0.02em;
120
+ animation: fadeIn 0.5s ease-out 0.3s both;
121
+ }
122
+
123
+ @keyframes fadeIn {
124
+ from { opacity: 0; }
125
+ to { opacity: 1; }
126
+ }
127
+
128
+ .message {
129
+ font-size: 0.9375rem;
130
+ color: var(--text-secondary);
131
+ line-height: 1.6;
132
+ margin-bottom: 1.5rem;
133
+ animation: fadeIn 0.5s ease-out 0.4s both;
134
+ }
135
+
136
+ .terminal-hint {
137
+ padding: 0.75rem 1rem;
138
+ background: var(--bg-subtle);
139
+ border-radius: 8px;
140
+ font-family: 'JetBrains Mono', monospace;
141
+ font-size: 0.75rem;
142
+ color: var(--text-muted);
143
+ animation: fadeIn 0.5s ease-out 0.5s both;
144
+ }
145
+
146
+ .terminal-hint code {
147
+ color: var(--error);
148
+ }
149
+ </style>
150
+ </head>
151
+ <body>
152
+ <div class="container">
153
+ <div class="card">
154
+ <div class="icon-wrapper">
155
+ <div class="icon-circle">
156
+ <svg class="x-icon" viewBox="0 0 24 24">
157
+ <path d="M18 6L6 18M6 6l12 12"/>
158
+ </svg>
159
+ </div>
160
+ </div>
161
+
162
+ <h1 class="title">${title}</h1>
163
+ <p class="message">${message}</p>
164
+
165
+ <div class="terminal-hint">
166
+ Run <code>lpm login</code> in your terminal to try again
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </body>
171
+ </html>`
172
+ }
173
+
174
+ export async function login() {
175
+ printHeader()
176
+ const registryUrl = getRegistryUrl()
177
+ const spinner = createSpinner(`Logging in to ${registryUrl}...`).start()
178
+
179
+ function closeAndExit(res) {
180
+ res.on("finish", () => {
181
+ server.close(() => process.exit(0))
182
+ })
183
+ }
184
+
185
+ const server = http.createServer(async (req, res) => {
186
+ const url = new URL(req.url, `http://${req.headers.host}`)
187
+
188
+ res.setHeader("Access-Control-Allow-Origin", "*")
189
+
190
+ if (url.pathname === "/callback") {
191
+ const token = url.searchParams.get("token")
192
+
193
+ if (token) {
194
+ // Use async setToken
195
+ await setToken(token)
196
+
197
+ try {
198
+ const response = await request("/-/whoami")
199
+ if (response.ok) {
200
+ const data = await response.json()
201
+ spinner.succeed(`Successfully logged in as: ${data.username}`)
202
+
203
+ // Show warning if personal username is not set
204
+ if (!data.profile_username) {
205
+ console.log("")
206
+ log.warn(WARNING_MESSAGES.usernameNotSet)
207
+ log.warn(WARNING_MESSAGES.usernameNotSetHint(registryUrl))
208
+ }
209
+
210
+ const html = `
211
+ <!DOCTYPE html>
212
+ <html>
213
+ <head>
214
+ <title>LPM - Access Granted</title>
215
+ <link rel="preconnect" href="https://fonts.googleapis.com">
216
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
217
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet">
218
+ <style>
219
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
220
+
221
+ :root {
222
+ --bg-primary: #0a0a0b;
223
+ --bg-card: #111113;
224
+ --bg-subtle: #18181b;
225
+ --border: #27272a;
226
+ --text-primary: #fafafa;
227
+ --text-secondary: #a1a1aa;
228
+ --text-muted: #71717a;
229
+ --accent: #22c55e;
230
+ --accent-glow: rgba(34, 197, 94, 0.15);
231
+ }
232
+
233
+ body {
234
+ font-family: 'Outfit', system-ui, sans-serif;
235
+ min-height: 100vh;
236
+ display: flex;
237
+ justify-content: center;
238
+ align-items: center;
239
+ background: var(--bg-primary);
240
+ background-image:
241
+ radial-gradient(ellipse 80% 50% at 50% -20%, var(--accent-glow), transparent),
242
+ radial-gradient(circle at 50% 50%, var(--bg-primary), var(--bg-primary));
243
+ padding: 1.5rem;
244
+ }
245
+
246
+ .container {
247
+ width: 100%;
248
+ max-width: 420px;
249
+ animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
250
+ }
251
+
252
+ @keyframes fadeInUp {
253
+ from { opacity: 0; transform: translateY(20px); }
254
+ to { opacity: 1; transform: translateY(0); }
255
+ }
256
+
257
+ .card {
258
+ background: var(--bg-card);
259
+ border: 1px solid var(--border);
260
+ border-radius: 16px;
261
+ padding: 2.5rem 2rem;
262
+ text-align: center;
263
+ position: relative;
264
+ overflow: hidden;
265
+ }
266
+
267
+ .card::before {
268
+ content: '';
269
+ position: absolute;
270
+ top: 0;
271
+ left: 50%;
272
+ transform: translateX(-50%);
273
+ width: 200px;
274
+ height: 1px;
275
+ background: linear-gradient(90deg, transparent, var(--accent), transparent);
276
+ opacity: 0.6;
277
+ }
278
+
279
+ .icon-wrapper {
280
+ width: 80px;
281
+ height: 80px;
282
+ margin: 0 auto 1.5rem;
283
+ position: relative;
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: center;
287
+ }
288
+
289
+ .icon-ring {
290
+ position: absolute;
291
+ inset: 0;
292
+ border-radius: 50%;
293
+ border: 2px solid var(--accent);
294
+ opacity: 0;
295
+ animation: ringPulse 2s ease-out 0.3s infinite;
296
+ }
297
+
298
+ .icon-ring:nth-child(2) { animation-delay: 0.6s; }
299
+
300
+ @keyframes ringPulse {
301
+ 0% { transform: scale(1); opacity: 0.6; }
302
+ 100% { transform: scale(1.8); opacity: 0; }
303
+ }
304
+
305
+ .icon-circle {
306
+ width: 64px;
307
+ height: 64px;
308
+ background: var(--accent);
309
+ border-radius: 50%;
310
+ display: flex;
311
+ align-items: center;
312
+ justify-content: center;
313
+ box-shadow: 0 0 40px var(--accent-glow), 0 0 80px var(--accent-glow);
314
+ animation: scaleIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s both;
315
+ }
316
+
317
+ @keyframes scaleIn {
318
+ from { transform: scale(0); }
319
+ to { transform: scale(1); }
320
+ }
321
+
322
+ .checkmark {
323
+ width: 32px;
324
+ height: 32px;
325
+ stroke: var(--bg-primary);
326
+ stroke-width: 3;
327
+ stroke-linecap: round;
328
+ stroke-linejoin: round;
329
+ fill: none;
330
+ }
331
+
332
+ .checkmark-path {
333
+ stroke-dasharray: 50;
334
+ stroke-dashoffset: 50;
335
+ animation: drawCheck 0.4s ease-out 0.5s forwards;
336
+ }
337
+
338
+ @keyframes drawCheck {
339
+ to { stroke-dashoffset: 0; }
340
+ }
341
+
342
+ .title {
343
+ font-size: 1.5rem;
344
+ font-weight: 600;
345
+ color: var(--text-primary);
346
+ margin-bottom: 0.5rem;
347
+ letter-spacing: -0.02em;
348
+ animation: fadeIn 0.5s ease-out 0.3s both;
349
+ }
350
+
351
+ @keyframes fadeIn {
352
+ from { opacity: 0; }
353
+ to { opacity: 1; }
354
+ }
355
+
356
+ .subtitle {
357
+ font-size: 0.9375rem;
358
+ color: var(--text-secondary);
359
+ margin-bottom: 1.5rem;
360
+ animation: fadeIn 0.5s ease-out 0.4s both;
361
+ }
362
+
363
+ .user-badge {
364
+ display: inline-flex;
365
+ align-items: center;
366
+ gap: 0.5rem;
367
+ background: var(--bg-subtle);
368
+ border: 1px solid var(--border);
369
+ border-radius: 8px;
370
+ padding: 0.625rem 1rem;
371
+ margin-bottom: 1.5rem;
372
+ animation: fadeIn 0.5s ease-out 0.5s both;
373
+ }
374
+
375
+ .user-icon {
376
+ width: 18px;
377
+ height: 18px;
378
+ stroke: var(--text-muted);
379
+ stroke-width: 2;
380
+ fill: none;
381
+ }
382
+
383
+ .username {
384
+ font-family: 'JetBrains Mono', monospace;
385
+ font-size: 0.875rem;
386
+ font-weight: 500;
387
+ color: var(--text-primary);
388
+ }
389
+
390
+ .divider {
391
+ height: 1px;
392
+ background: var(--border);
393
+ margin: 1.25rem 0;
394
+ animation: fadeIn 0.5s ease-out 0.6s both;
395
+ }
396
+
397
+ .footer {
398
+ display: flex;
399
+ align-items: center;
400
+ justify-content: center;
401
+ gap: 0.5rem;
402
+ animation: fadeIn 0.5s ease-out 0.7s both;
403
+ }
404
+
405
+ .footer-text {
406
+ font-size: 0.8125rem;
407
+ color: var(--text-muted);
408
+ }
409
+
410
+ .countdown {
411
+ font-family: 'JetBrains Mono', monospace;
412
+ font-size: 0.75rem;
413
+ font-weight: 500;
414
+ color: var(--accent);
415
+ background: var(--accent-glow);
416
+ padding: 0.25rem 0.5rem;
417
+ border-radius: 4px;
418
+ }
419
+
420
+ .terminal-hint {
421
+ margin-top: 1.5rem;
422
+ padding: 0.75rem 1rem;
423
+ background: var(--bg-subtle);
424
+ border-radius: 8px;
425
+ font-family: 'JetBrains Mono', monospace;
426
+ font-size: 0.75rem;
427
+ color: var(--text-muted);
428
+ animation: fadeIn 0.5s ease-out 0.8s both;
429
+ }
430
+
431
+ .terminal-hint code {
432
+ color: var(--accent);
433
+ }
434
+ </style>
435
+ </head>
436
+ <body>
437
+ <div class="container">
438
+ <div class="card">
439
+ <div class="icon-wrapper">
440
+ <div class="icon-ring"></div>
441
+ <div class="icon-ring"></div>
442
+ <div class="icon-circle">
443
+ <svg class="checkmark" viewBox="0 0 24 24">
444
+ <path class="checkmark-path" d="M5 12l5 5L19 7"/>
445
+ </svg>
446
+ </div>
447
+ </div>
448
+
449
+ <h1 class="title">Access Granted</h1>
450
+ <p class="subtitle">CLI authentication successful</p>
451
+
452
+ <div class="user-badge">
453
+ <svg class="user-icon" viewBox="0 0 24 24">
454
+ <circle cx="12" cy="8" r="4"/>
455
+ <path d="M4 21v-2a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v2"/>
456
+ </svg>
457
+ <span class="username">${data.username}</span>
458
+ </div>
459
+
460
+ <div class="divider"></div>
461
+
462
+ <div class="footer" id="footer">
463
+ <span class="footer-text">Closing in</span>
464
+ <span class="countdown" id="countdown">5s</span>
465
+ </div>
466
+
467
+ <div class="terminal-hint" id="hint">
468
+ Return to your terminal to continue using <code>lpm</code>
469
+ </div>
470
+ </div>
471
+ </div>
472
+
473
+ <script>
474
+ let seconds = 5;
475
+ const countdown = document.getElementById('countdown');
476
+ const footer = document.getElementById('footer');
477
+ const hint = document.getElementById('hint');
478
+
479
+ const interval = setInterval(() => {
480
+ seconds--;
481
+ countdown.textContent = seconds + 's';
482
+ if (seconds <= 0) {
483
+ clearInterval(interval);
484
+ window.close();
485
+ // If window didn't close (browser security), show message
486
+ setTimeout(() => {
487
+ footer.innerHTML = '<span class="footer-text" style="color: var(--accent);">You can close this tab now</span>';
488
+ hint.innerHTML = 'Return to your terminal to continue using <code>lpm</code>';
489
+ }, 100);
490
+ }
491
+ }, 1000);
492
+ </script>
493
+ </body>
494
+ </html>`
495
+ res.setHeader("Content-Type", "text/html")
496
+ closeAndExit(res)
497
+ res.end(html)
498
+ } else {
499
+ console.error("\nToken verification failed.")
500
+ spinner.fail("Token verification failed")
501
+ res.setHeader("Content-Type", "text/html")
502
+ closeAndExit(res)
503
+ res.end(
504
+ getErrorHtml(
505
+ "Invalid Token",
506
+ "The authentication token could not be verified. Please try logging in again.",
507
+ ),
508
+ )
509
+ }
510
+ } catch (err) {
511
+ console.error("\nError verifying token:", err.message)
512
+ spinner.fail("Error verifying token")
513
+ res.setHeader("Content-Type", "text/html")
514
+ closeAndExit(res)
515
+ res.end(
516
+ getErrorHtml(
517
+ "Verification Error",
518
+ "An error occurred while verifying your token. Please try again.",
519
+ ),
520
+ )
521
+ }
522
+ } else {
523
+ spinner.fail("No token received")
524
+ res.setHeader("Content-Type", "text/html")
525
+ closeAndExit(res)
526
+ res.end(
527
+ getErrorHtml(
528
+ "No Token",
529
+ "No authentication token was received. Please try logging in again.",
530
+ ),
531
+ )
532
+ }
533
+ } else {
534
+ res.end("LPM CLI Login Server")
535
+ }
536
+ })
537
+
538
+ server.listen(0, async () => {
539
+ const port = server.address().port
540
+ // Assuming registryUrl is the base URL of the Next.js app
541
+ const loginUrl = `${registryUrl}/cli/login?port=${port}`
542
+
543
+ console.log(`Opening browser to: ${loginUrl}`)
544
+ await open(loginUrl)
545
+ console.log("Waiting for authentication...")
546
+ })
547
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Logout Command
3
+ *
4
+ * Clears stored authentication token and optionally revokes it on the server.
5
+ *
6
+ * Usage:
7
+ * lpm logout [options]
8
+ *
9
+ * Options:
10
+ * --revoke Also revoke the token on the server
11
+ * --clear-cache Clear local package cache
12
+ *
13
+ * @module cli/lib/commands/logout
14
+ */
15
+
16
+ import chalk from "chalk"
17
+ import ora from "ora"
18
+ import { request } from "../api.js"
19
+ import { clearToken, getToken } from "../config.js"
20
+
21
+ /**
22
+ * Execute the logout command.
23
+ *
24
+ * @param {Object} options - Command options
25
+ * @param {boolean} [options.revoke] - Whether to revoke the token on the server
26
+ * @param {boolean} [options.clearCache] - Whether to clear the local cache
27
+ */
28
+ export async function logout(options = {}) {
29
+ const spinner = ora("Logging out...").start()
30
+
31
+ try {
32
+ const token = await getToken()
33
+
34
+ if (!token) {
35
+ spinner.info("Not currently logged in.")
36
+ return
37
+ }
38
+
39
+ // Optionally revoke the token on the server
40
+ if (options.revoke) {
41
+ spinner.text = "Revoking token on server..."
42
+
43
+ try {
44
+ const response = await request("/tokens/revoke", {
45
+ method: "POST",
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ },
49
+ body: JSON.stringify({ token }),
50
+ skipRetry: true,
51
+ })
52
+
53
+ if (response.ok) {
54
+ spinner.text = "Token revoked on server."
55
+ } else {
56
+ // Don't fail logout if revoke fails - just warn
57
+ spinner.text =
58
+ "Could not revoke token on server (continuing with local logout)."
59
+ }
60
+ } catch {
61
+ // Don't fail logout if revoke fails
62
+ spinner.text =
63
+ "Could not reach server to revoke token (continuing with local logout)."
64
+ }
65
+ }
66
+
67
+ // Clear local token
68
+ await clearToken()
69
+
70
+ // Optionally clear cache
71
+ if (options.clearCache) {
72
+ spinner.text = "Clearing local cache..."
73
+ // Import dynamically to avoid circular dependency
74
+ const { clearCache } = await import("./cache.js")
75
+ await clearCache()
76
+ }
77
+
78
+ spinner.succeed(chalk.green("Successfully logged out."))
79
+
80
+ if (options.revoke) {
81
+ console.log(chalk.dim(" Token has been revoked on the server."))
82
+ }
83
+
84
+ if (options.clearCache) {
85
+ console.log(chalk.dim(" Local cache has been cleared."))
86
+ }
87
+ } catch (error) {
88
+ spinner.fail(chalk.red("Logout failed."))
89
+ console.error(chalk.red(` ${error.message}`))
90
+ process.exit(1)
91
+ }
92
+ }
93
+
94
+ export default logout