@lpm-registry/cli 0.2.1 → 0.2.2

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 CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.2] - 2026-03-16
6
+
7
+ ### Added
8
+
9
+ - `publish.sh` script for automated linting, testing, and publishing to npm
10
+ - Local project scanning for test files during quality checks — authors no longer need to ship test files in the tarball to earn quality points
11
+ - Registry-scoped token storage — dev and production tokens no longer overwrite each other
12
+
13
+ ### Changed
14
+
15
+ - Token storage is now scoped per registry URL with automatic migration from legacy un-scoped keys
16
+ - Quality check "Has test files" now scans the local project directory instead of only the tarball contents
17
+
5
18
  ## [0.2.1] - 2026-03-12
6
19
 
7
20
  ### Added
@@ -308,6 +308,48 @@ async function packWithTar(ecosystem, name, version) {
308
308
  }
309
309
  }
310
310
 
311
+ /**
312
+ * Scan local project directory for test files.
313
+ * This avoids requiring test files in the tarball to earn quality points.
314
+ * Uses a limited-depth scan, skipping known non-source directories.
315
+ */
316
+ function scanProjectForTestFiles() {
317
+ const skipDirs = new Set([
318
+ "node_modules",
319
+ ".git",
320
+ "dist",
321
+ "build",
322
+ ".next",
323
+ ".nuxt",
324
+ "coverage",
325
+ ".lpm",
326
+ ])
327
+
328
+ const result = []
329
+
330
+ function walk(dir, depth = 0) {
331
+ if (depth > 4) return
332
+ let entries
333
+ try {
334
+ entries = fs.readdirSync(dir, { withFileTypes: true })
335
+ } catch {
336
+ return
337
+ }
338
+ for (const entry of entries) {
339
+ if (entry.isDirectory()) {
340
+ if (!skipDirs.has(entry.name)) {
341
+ walk(path.join(dir, entry.name), depth + 1)
342
+ }
343
+ } else if (entry.isFile()) {
344
+ result.push({ path: path.join(dir, entry.name) })
345
+ }
346
+ }
347
+ }
348
+
349
+ walk(process.cwd())
350
+ return result
351
+ }
352
+
311
353
  export async function publish(options = {}) {
312
354
  const checkOnly = !!options.check
313
355
  const dryRun = !!options.dryRun
@@ -468,11 +510,13 @@ export async function publish(options = {}) {
468
510
 
469
511
  // 7. Run quality checks and display report
470
512
  spinner.text = "Running quality checks..."
513
+ const projectFiles = scanProjectForTestFiles()
471
514
  const qualityResult = runQualityChecks({
472
515
  packageJson: pkg,
473
516
  readme,
474
517
  lpmConfig,
475
518
  files: npmPackMeta.files || [],
519
+ projectFiles,
476
520
  unpackedSize: npmPackMeta.unpackedSize,
477
521
  ecosystem,
478
522
  swiftManifest,
package/lib/config.js CHANGED
@@ -64,6 +64,7 @@ export function clearTransientToken() {
64
64
  /**
65
65
  * Get the stored auth token from secure storage.
66
66
  * Priority: transient (OIDC) > LPM_TOKEN env var > secure storage > legacy storage
67
+ * Tokens are scoped per registry URL so dev and production don't collide.
67
68
  * @returns {Promise<string | null>}
68
69
  */
69
70
  export async function getToken() {
@@ -74,15 +75,17 @@ export async function getToken() {
74
75
  const envToken = process.env.LPM_TOKEN
75
76
  if (envToken) return envToken
76
77
 
77
- // Try secure store
78
- const secureToken = await secureGetToken()
78
+ const registryUrl = getRegistryUrl()
79
+
80
+ // Try secure store (registry-scoped)
81
+ const secureToken = await secureGetToken(registryUrl)
79
82
  if (secureToken) return secureToken
80
83
 
81
- // Migration: check if token exists in old storage
84
+ // Migration: check if token exists in old Conf storage
82
85
  const legacyToken = config.get("token")
83
86
  if (legacyToken) {
84
- // Migrate to secure storage
85
- await secureSetToken(legacyToken)
87
+ // Migrate to secure storage (scoped to current registry)
88
+ await secureSetToken(legacyToken, registryUrl)
86
89
  config.delete("token")
87
90
  return legacyToken
88
91
  }
@@ -91,26 +94,27 @@ export async function getToken() {
91
94
  }
92
95
 
93
96
  /**
94
- * Set the auth token in secure storage.
97
+ * Set the auth token in secure storage (scoped to current registry).
95
98
  * @param {string | null} token
96
99
  * @returns {Promise<void>}
97
100
  */
98
101
  export async function setToken(token) {
102
+ const registryUrl = getRegistryUrl()
99
103
  if (token === null) {
100
- await secureClearToken()
104
+ await secureClearToken(registryUrl)
101
105
  } else {
102
- await secureSetToken(token)
106
+ await secureSetToken(token, registryUrl)
103
107
  }
104
108
  // Ensure legacy token is cleared
105
109
  config.delete("token")
106
110
  }
107
111
 
108
112
  /**
109
- * Clear the auth token from secure storage.
113
+ * Clear the auth token from secure storage (scoped to current registry).
110
114
  * @returns {Promise<void>}
111
115
  */
112
116
  export async function clearToken() {
113
- await secureClearToken()
117
+ await secureClearToken(getRegistryUrl())
114
118
  config.delete("token")
115
119
  }
116
120
 
@@ -336,8 +336,11 @@ const hasTestFiles = {
336
336
  category: "testing",
337
337
  label: "Has test files",
338
338
  maxPoints: 7,
339
- run: ({ files }) => {
340
- const testFiles = files.filter(f => {
339
+ run: ({ projectFiles, files }) => {
340
+ // Prefer projectFiles (local project scan) over files (tarball)
341
+ // so authors don't need to ship test files in the package
342
+ const fileList = projectFiles || files
343
+ const testFiles = fileList.filter(f => {
341
344
  const name = (f.path || f).toLowerCase()
342
345
  return (
343
346
  name.includes(".test.") ||
@@ -76,6 +76,14 @@ export function displayQualityReport({ score, checks, meta }) {
76
76
  }
77
77
  }
78
78
 
79
+ if (failed.length > 0 && score < 90) {
80
+ console.log(
81
+ chalk.dim(
82
+ " Use the lpm-guide skill in your AI editor to improve quality and prepare for publish",
83
+ ),
84
+ )
85
+ }
86
+
79
87
  // Score summary at the end
80
88
  console.log("")
81
89
  console.log(
@@ -37,6 +37,7 @@ function getChecksForEcosystem(ecosystem) {
37
37
  * @param {string|null} params.readme - README content
38
38
  * @param {object|null} params.lpmConfig - Parsed lpm.config.json
39
39
  * @param {Array} params.files - File list (each has .path)
40
+ * @param {Array} [params.projectFiles] - Local project file list for test detection
40
41
  * @param {number} [params.unpackedSize] - Unpacked size in bytes
41
42
  * @param {string} [params.ecosystem] - Ecosystem identifier (default: "js")
42
43
  * @param {object} [params.swiftManifest] - Parsed Swift manifest metadata (for Swift packages)
@@ -47,6 +48,7 @@ export function runQualityChecks({
47
48
  readme,
48
49
  lpmConfig,
49
50
  files,
51
+ projectFiles,
50
52
  unpackedSize,
51
53
  ecosystem = "js",
52
54
  swiftManifest,
@@ -57,6 +59,7 @@ export function runQualityChecks({
57
59
  readme,
58
60
  lpmConfig,
59
61
  files,
62
+ projectFiles,
60
63
  unpackedSize,
61
64
  swiftManifest,
62
65
  xcframeworkMeta,
@@ -259,30 +259,63 @@ export async function isUsingKeychain() {
259
259
  }
260
260
 
261
261
  // ============================================================================
262
- // Token-Specific Helpers
262
+ // Token-Specific Helpers (Registry-Aware)
263
263
  // ============================================================================
264
264
 
265
265
  /**
266
- * Get the stored auth token.
266
+ * Build a registry-scoped key for token storage.
267
+ * Each registry URL gets its own token slot so dev and production
268
+ * tokens don't overwrite each other.
269
+ * @param {string} [registryUrl]
270
+ * @returns {string}
271
+ */
272
+ function tokenKey(registryUrl) {
273
+ if (!registryUrl) return KEYTAR_ACCOUNT_NAME
274
+ return `${KEYTAR_ACCOUNT_NAME}:${registryUrl}`
275
+ }
276
+
277
+ /**
278
+ * Get the stored auth token for a specific registry.
279
+ * Falls back to the legacy un-scoped key and migrates it forward.
280
+ * @param {string} [registryUrl] - Registry URL to scope the token to
267
281
  * @returns {Promise<string | null>}
268
282
  */
269
- export async function getToken() {
270
- return getSecret(KEYTAR_ACCOUNT_NAME)
283
+ export async function getToken(registryUrl) {
284
+ const key = tokenKey(registryUrl)
285
+
286
+ // Try registry-scoped key first
287
+ const scoped = await getSecret(key)
288
+ if (scoped) return scoped
289
+
290
+ // Migration: if legacy un-scoped token exists, migrate it to the
291
+ // default registry key (production) so existing logins keep working
292
+ if (registryUrl) {
293
+ const legacy = await getSecret(KEYTAR_ACCOUNT_NAME)
294
+ if (legacy) {
295
+ await setSecret(key, legacy)
296
+ await deleteSecret(KEYTAR_ACCOUNT_NAME)
297
+ return legacy
298
+ }
299
+ }
300
+
301
+ return null
271
302
  }
272
303
 
273
304
  /**
274
- * Set the auth token.
305
+ * Set the auth token for a specific registry.
275
306
  * @param {string} token
307
+ * @param {string} [registryUrl] - Registry URL to scope the token to
276
308
  * @returns {Promise<void>}
277
309
  */
278
- export async function setToken(token) {
279
- return setSecret(KEYTAR_ACCOUNT_NAME, token)
310
+ export async function setToken(token, registryUrl) {
311
+ return setSecret(tokenKey(registryUrl), token)
280
312
  }
281
313
 
282
314
  /**
283
- * Clear the auth token.
315
+ * Clear the auth token for a specific registry.
316
+ * @param {string} [registryUrl] - Registry URL to scope the token to
284
317
  * @returns {Promise<boolean>}
285
318
  */
286
- export async function clearToken() {
287
- return deleteSecret(KEYTAR_ACCOUNT_NAME)
319
+ export async function clearToken(registryUrl) {
320
+ return deleteSecret(tokenKey(registryUrl))
288
321
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lpm-registry/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "CLI for Licensed Package Manager",
5
5
  "author": "LPM <hello@lpm.dev> (https://lpm.dev)",
6
6
  "repository": {