@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 +13 -0
- package/lib/commands/publish.js +44 -0
- package/lib/config.js +14 -10
- package/lib/quality/checks.js +5 -2
- package/lib/quality/display.js +8 -0
- package/lib/quality/score.js +3 -0
- package/lib/secure-store.js +43 -10
- package/package.json +1 -1
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
|
package/lib/commands/publish.js
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
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
|
|
package/lib/quality/checks.js
CHANGED
|
@@ -336,8 +336,11 @@ const hasTestFiles = {
|
|
|
336
336
|
category: "testing",
|
|
337
337
|
label: "Has test files",
|
|
338
338
|
maxPoints: 7,
|
|
339
|
-
run: ({ files }) => {
|
|
340
|
-
|
|
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.") ||
|
package/lib/quality/display.js
CHANGED
|
@@ -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(
|
package/lib/quality/score.js
CHANGED
|
@@ -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,
|
package/lib/secure-store.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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(
|
|
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(
|
|
319
|
+
export async function clearToken(registryUrl) {
|
|
320
|
+
return deleteSecret(tokenKey(registryUrl))
|
|
288
321
|
}
|