@lpm-registry/cli 0.2.5 → 0.3.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.
- package/CHANGELOG.md +36 -0
- package/README.md +32 -9
- package/bin/lpm.js +18 -1
- package/lib/commands/doctor.js +7 -9
- package/lib/commands/install.js +66 -22
- package/lib/commands/npmrc.js +41 -4
- package/lib/commands/publish.js +65 -5
- package/lib/commands/setup.js +78 -8
- package/lib/commands/skills.js +12 -1
- package/lib/commands/uninstall.js +107 -0
- package/lib/config.js +37 -0
- package/lib/quality/display.js +10 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-03-22
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Upstream npm proxy mode (default)** — `lpm npmrc` and `lpm setup` now configure LPM as the default registry. All packages (LPM + npm) are fetched through LPM for faster, edge-cached installs via Cloudflare R2.
|
|
10
|
+
- **`--scoped` flag** — Use `lpm npmrc --scoped` or `lpm setup --scoped` to only route `@lpm.dev` packages through LPM (old behavior).
|
|
11
|
+
- **Custom registry detection** — If `.npmrc` already has a custom default registry (e.g., Artifactory), the CLI warns and automatically falls back to `--scoped` mode to avoid overriding it.
|
|
12
|
+
- **Pre-publish skills staleness warning** — When Agent Skills haven't changed since the previous version, the warning now appears before the publish confirmation prompt (instead of after), so you can cancel and update skills before publishing.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `lpm npmrc` default output changed from `@lpm.dev:registry=...` (scoped) to `registry=...` (proxy mode)
|
|
17
|
+
- `lpm setup` and `lpm setup --oidc` default output changed to proxy mode
|
|
18
|
+
- Updated CLI output messaging to indicate proxy vs scoped mode
|
|
19
|
+
- Updated README with new default behavior and `--scoped` flag
|
|
20
|
+
|
|
21
|
+
## [0.2.6] - 2026-03-21
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`lpm uninstall` command** — Mirrors `npm uninstall` with automatic registry auth. Aliases: `lpm un`, `lpm unlink`.
|
|
26
|
+
- **`--pm` flag for install/uninstall** — Choose package manager: `lpm install --pm pnpm`. Supports npm (default), pnpm, yarn, bun.
|
|
27
|
+
- **`lpm config set packageManager`** — Set default package manager globally. Also available via `LPM_PACKAGE_MANAGER` env var.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `lpm install` now shows package list before install: "Found 15 LPM packages in package.json"
|
|
32
|
+
- Skills install now scoped to explicit packages when specified (`lpm install @lpm.dev/pkg` only fetches skills for that package)
|
|
33
|
+
- Skills install skips `file:`, `link:`, `workspace:` dependencies (local packages)
|
|
34
|
+
- `lpm doctor` registry check fixed — uses `/-/whoami` endpoint instead of non-existent `/health`
|
|
35
|
+
- Publish script updated with version bumping, git commit/tag/push, and `--dry-run` support
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- pnpm/yarn/bun compatibility — no longer passes npm-specific `--userconfig` flag. Uses project `.npmrc` with backup/restore for non-npm package managers.
|
|
40
|
+
|
|
5
41
|
## [0.2.5] - 2026-03-19
|
|
6
42
|
|
|
7
43
|
### Added
|
package/README.md
CHANGED
|
@@ -65,23 +65,32 @@ lpm whoami
|
|
|
65
65
|
|
|
66
66
|
### Project Setup
|
|
67
67
|
|
|
68
|
-
####
|
|
68
|
+
#### Generate .npmrc Token (Local Dev)
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
Generate a 30-day read-only token for your project. Makes `npm install` work with LPM packages.
|
|
71
|
+
|
|
72
|
+
By default, LPM is configured as your default registry — all packages (LPM + npm) are fetched through LPM for faster, edge-cached installs.
|
|
71
73
|
|
|
72
74
|
```bash
|
|
73
|
-
lpm
|
|
75
|
+
lpm npmrc # 30-day token, proxy mode (default)
|
|
76
|
+
lpm npmrc --days 7 # 7-day token
|
|
77
|
+
lpm npmrc --days 90 # 90-day token
|
|
78
|
+
lpm npmrc --scoped # Only route @lpm.dev packages through LPM
|
|
74
79
|
```
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
Automatically adds `.npmrc` to `.gitignore` to prevent token leaks.
|
|
82
|
+
|
|
83
|
+
#### Setup .npmrc (CI/CD)
|
|
84
|
+
|
|
85
|
+
Configure `.npmrc` for CI/CD environments.
|
|
77
86
|
|
|
78
|
-
```
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
```bash
|
|
88
|
+
lpm setup # Writes ${LPM_TOKEN} placeholder (proxy mode)
|
|
89
|
+
lpm setup --oidc # OIDC — no secrets needed (GitHub Actions, GitLab CI)
|
|
90
|
+
lpm setup --scoped # Only route @lpm.dev packages through LPM
|
|
82
91
|
```
|
|
83
92
|
|
|
84
|
-
For deployment, set the `LPM_TOKEN` environment variable
|
|
93
|
+
For deployment platforms (Vercel, Netlify), set the `LPM_TOKEN` environment variable.
|
|
85
94
|
|
|
86
95
|
### Package Management
|
|
87
96
|
|
|
@@ -120,6 +129,7 @@ lpm install @lpm.dev/owner.package-name
|
|
|
120
129
|
lpm install @lpm.dev/tolgaergin.utils @lpm.dev/acme.helpers
|
|
121
130
|
lpm install # Install all @lpm.dev packages from package.json
|
|
122
131
|
lpm install --no-skills # Skip fetching Agent Skills
|
|
132
|
+
lpm install --pm pnpm # Use pnpm instead of npm
|
|
123
133
|
lpm i # Shortcut
|
|
124
134
|
```
|
|
125
135
|
|
|
@@ -128,6 +138,19 @@ Agent Skills are fetched by default for packages that include them.
|
|
|
128
138
|
| Option | Description |
|
|
129
139
|
| ------------- | ---------------------------------------- |
|
|
130
140
|
| `--no-skills` | Skip fetching Agent Skills after install |
|
|
141
|
+
| `--pm <name>` | Package manager: npm (default), pnpm, yarn, bun |
|
|
142
|
+
|
|
143
|
+
Set a default package manager: `lpm config set packageManager pnpm`
|
|
144
|
+
|
|
145
|
+
#### Uninstall
|
|
146
|
+
|
|
147
|
+
Uninstall packages with automatic registry authentication.
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
lpm uninstall @lpm.dev/owner.package-name
|
|
151
|
+
lpm uninstall --pm pnpm # Use pnpm
|
|
152
|
+
lpm un # Shortcut
|
|
153
|
+
```
|
|
131
154
|
|
|
132
155
|
#### Add (Source Code)
|
|
133
156
|
|
package/bin/lpm.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
skillsValidate,
|
|
37
37
|
} from "../lib/commands/skills.js"
|
|
38
38
|
import { rotateToken } from "../lib/commands/token-rotate.js"
|
|
39
|
+
import { uninstall } from "../lib/commands/uninstall.js"
|
|
39
40
|
import { whoami } from "../lib/commands/whoami.js"
|
|
40
41
|
|
|
41
42
|
// Load package.json
|
|
@@ -93,8 +94,16 @@ program
|
|
|
93
94
|
.description("Install packages with automatic registry authentication")
|
|
94
95
|
.option("--json", "Machine-readable JSON output")
|
|
95
96
|
.option("--no-skills", "Skip fetching Agent Skills after install")
|
|
97
|
+
.option("--pm <manager>", "Package manager to use (npm, pnpm, yarn, bun)")
|
|
96
98
|
.action(install)
|
|
97
99
|
|
|
100
|
+
program
|
|
101
|
+
.command("uninstall <packages...>")
|
|
102
|
+
.aliases(["un", "unlink"])
|
|
103
|
+
.description("Uninstall packages via package manager")
|
|
104
|
+
.option("--pm <manager>", "Package manager to use (npm, pnpm, yarn, bun)")
|
|
105
|
+
.action(uninstall)
|
|
106
|
+
|
|
98
107
|
program
|
|
99
108
|
.command("publish")
|
|
100
109
|
.alias("p")
|
|
@@ -201,18 +210,26 @@ program
|
|
|
201
210
|
|
|
202
211
|
program
|
|
203
212
|
.command("setup")
|
|
204
|
-
.description("Configure .npmrc for LPM
|
|
213
|
+
.description("Configure .npmrc for LPM registry")
|
|
205
214
|
.option("-r, --registry <url>", "Custom registry URL")
|
|
206
215
|
.option(
|
|
207
216
|
"--oidc",
|
|
208
217
|
"Exchange CI OIDC token for install access (no secrets needed)",
|
|
209
218
|
)
|
|
219
|
+
.option(
|
|
220
|
+
"--scoped",
|
|
221
|
+
"Only route @lpm.dev packages through LPM (don't proxy npm)",
|
|
222
|
+
)
|
|
210
223
|
.action(setup)
|
|
211
224
|
|
|
212
225
|
program
|
|
213
226
|
.command("npmrc")
|
|
214
227
|
.description("Generate a read-only .npmrc token for local development")
|
|
215
228
|
.option("-d, --days <number>", "Token expiry in days (default: 30)")
|
|
229
|
+
.option(
|
|
230
|
+
"--scoped",
|
|
231
|
+
"Only route @lpm.dev packages through LPM (don't proxy npm)",
|
|
232
|
+
)
|
|
216
233
|
.action(npmrc)
|
|
217
234
|
|
|
218
235
|
program
|
package/lib/commands/doctor.js
CHANGED
|
@@ -24,16 +24,14 @@ export async function doctor() {
|
|
|
24
24
|
// 2. Check Registry Reachability
|
|
25
25
|
const spinnerApi = createSpinner("Checking registry connection...").start()
|
|
26
26
|
try {
|
|
27
|
-
// Assuming we have a health endpoint or similar. Using the registry URL from config or default.
|
|
28
27
|
const registryUrl = getRegistryUrl()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
28
|
+
// Use /-/whoami without a token — a 401 "Missing token" proves the registry is reachable.
|
|
29
|
+
// Only network errors (ECONNREFUSED, timeout) indicate the registry is down.
|
|
30
|
+
await fetch(`${registryUrl}/api/registry/-/whoami`, {
|
|
31
|
+
signal: AbortSignal.timeout(10000),
|
|
32
|
+
})
|
|
33
|
+
spinnerApi.succeed(`Registry reachable at ${registryUrl}`)
|
|
34
|
+
checks.push({ name: "Registry API", status: "ok" })
|
|
37
35
|
} catch (err) {
|
|
38
36
|
spinnerApi.fail(`Could not connect to registry: ${err.message}`)
|
|
39
37
|
checks.push({ name: "Registry API", status: "fail" })
|
package/lib/commands/install.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process"
|
|
2
2
|
import fs from "node:fs"
|
|
3
3
|
import path from "node:path"
|
|
4
|
-
import { getRegistryUrl, getToken } from "../config.js"
|
|
4
|
+
import { getPackageManager, getRegistryUrl, getToken } from "../config.js"
|
|
5
5
|
import { createSpinner, log, printHeader } from "../ui.js"
|
|
6
6
|
import { skillsInstall } from "./skills.js"
|
|
7
7
|
|
|
@@ -44,6 +44,9 @@ export async function install(packages, options) {
|
|
|
44
44
|
process.exit(1)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Track if user specified packages explicitly (for scoped skills fetch)
|
|
48
|
+
const explicitPackages = packages && packages.length > 0
|
|
49
|
+
|
|
47
50
|
if (!packages || packages.length === 0) {
|
|
48
51
|
// No packages specified - read from package.json
|
|
49
52
|
const packageJsonPath = path.resolve(process.cwd(), "package.json")
|
|
@@ -99,17 +102,16 @@ export async function install(packages, options) {
|
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
if (!isJson) {
|
|
102
|
-
log.info(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
log.info(`Found ${packages.length} LPM packages in package.json:`)
|
|
106
|
+
for (const pkg of packages) {
|
|
107
|
+
log.info(` ${pkg}`)
|
|
108
|
+
}
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
let spinner
|
|
109
113
|
if (!isJson) {
|
|
110
|
-
spinner = createSpinner(
|
|
111
|
-
`Preparing to install ${packages.join(", ")}...`,
|
|
112
|
-
).start()
|
|
114
|
+
spinner = createSpinner("Configuring registry authentication...").start()
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
const baseRegistryUrl = getRegistryUrl()
|
|
@@ -126,26 +128,54 @@ export async function install(packages, options) {
|
|
|
126
128
|
@lpm.dev:registry=${registryUrl}
|
|
127
129
|
`
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
const pm = getPackageManager(options?.pm)
|
|
132
|
+
|
|
133
|
+
// For npm we can use --userconfig with a temp file.
|
|
134
|
+
// For pnpm/yarn/bun we write a temporary project .npmrc (all PMs read it).
|
|
135
|
+
const projectNpmrcPath = path.resolve(process.cwd(), ".npmrc")
|
|
136
|
+
let existingNpmrc = null
|
|
137
|
+
let tempNpmrcPath = null
|
|
138
|
+
|
|
139
|
+
if (pm === "npm") {
|
|
140
|
+
// npm supports --userconfig — use a separate temp file (doesn't touch project .npmrc)
|
|
141
|
+
tempNpmrcPath = path.resolve(process.cwd(), `.npmrc.lpm-${Date.now()}`)
|
|
142
|
+
fs.writeFileSync(tempNpmrcPath, npmrcContent)
|
|
143
|
+
} else {
|
|
144
|
+
// pnpm/yarn/bun read project .npmrc — back up existing, write temp, restore after
|
|
145
|
+
if (fs.existsSync(projectNpmrcPath)) {
|
|
146
|
+
existingNpmrc = fs.readFileSync(projectNpmrcPath, "utf8")
|
|
147
|
+
}
|
|
148
|
+
fs.writeFileSync(projectNpmrcPath, npmrcContent)
|
|
149
|
+
}
|
|
132
150
|
|
|
133
151
|
if (!isJson) {
|
|
134
|
-
spinner.succeed(
|
|
152
|
+
spinner.succeed(`Configuration generated. Running ${pm} install...`)
|
|
135
153
|
}
|
|
136
154
|
|
|
137
|
-
|
|
138
|
-
|
|
155
|
+
const pmArgs =
|
|
156
|
+
pm === "npm"
|
|
157
|
+
? ["install", ...packages, "--userconfig", tempNpmrcPath]
|
|
158
|
+
: ["install", ...packages]
|
|
139
159
|
|
|
140
160
|
// In JSON mode, capture stdout/stderr instead of inheriting
|
|
141
|
-
const child = spawn(
|
|
161
|
+
const child = spawn(pm, pmArgs, {
|
|
142
162
|
stdio: isJson ? "pipe" : "inherit",
|
|
143
163
|
env: { ...process.env, LPM_TOKEN: token },
|
|
144
164
|
})
|
|
145
165
|
|
|
146
166
|
const cleanup = () => {
|
|
147
|
-
if (
|
|
148
|
-
|
|
167
|
+
if (pm === "npm") {
|
|
168
|
+
// Remove temp file
|
|
169
|
+
if (tempNpmrcPath && fs.existsSync(tempNpmrcPath)) {
|
|
170
|
+
fs.unlinkSync(tempNpmrcPath)
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// Restore original .npmrc (or remove if there wasn't one)
|
|
174
|
+
if (existingNpmrc !== null) {
|
|
175
|
+
fs.writeFileSync(projectNpmrcPath, existingNpmrc)
|
|
176
|
+
} else if (fs.existsSync(projectNpmrcPath)) {
|
|
177
|
+
fs.unlinkSync(projectNpmrcPath)
|
|
178
|
+
}
|
|
149
179
|
}
|
|
150
180
|
}
|
|
151
181
|
|
|
@@ -168,11 +198,19 @@ export async function install(packages, options) {
|
|
|
168
198
|
packages: packages.map(p => ({ name: p })),
|
|
169
199
|
npmOutput: (stdout + stderr).trim(),
|
|
170
200
|
warnings: [],
|
|
171
|
-
errors: code !== 0 ? [
|
|
201
|
+
errors: code !== 0 ? [`${pm} install failed with code ${code}`] : [],
|
|
172
202
|
}
|
|
173
203
|
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`)
|
|
174
204
|
if (code !== 0) process.exit(code)
|
|
175
|
-
if (fetchSkills)
|
|
205
|
+
if (fetchSkills) {
|
|
206
|
+
if (explicitPackages) {
|
|
207
|
+
for (const pkg of packages.filter(isLpmPackage)) {
|
|
208
|
+
await skillsInstall(pkg, { json: true })
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
await skillsInstall(null, { json: true })
|
|
212
|
+
}
|
|
213
|
+
}
|
|
176
214
|
})
|
|
177
215
|
|
|
178
216
|
child.on("error", err => {
|
|
@@ -182,7 +220,7 @@ export async function install(packages, options) {
|
|
|
182
220
|
packages: packages.map(p => ({ name: p })),
|
|
183
221
|
npmOutput: "",
|
|
184
222
|
warnings: [],
|
|
185
|
-
errors: [`Failed to start
|
|
223
|
+
errors: [`Failed to start ${pm}: ${err.message}`],
|
|
186
224
|
}
|
|
187
225
|
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`)
|
|
188
226
|
process.exit(1)
|
|
@@ -191,19 +229,25 @@ export async function install(packages, options) {
|
|
|
191
229
|
child.on("close", async code => {
|
|
192
230
|
cleanup()
|
|
193
231
|
if (code !== 0) {
|
|
194
|
-
log.error(
|
|
232
|
+
log.error(`${pm} install failed with code ${code}`)
|
|
195
233
|
process.exit(code)
|
|
196
234
|
} else {
|
|
197
235
|
log.success("Packages installed successfully.")
|
|
198
236
|
if (fetchSkills) {
|
|
199
237
|
console.log("")
|
|
200
|
-
|
|
238
|
+
if (explicitPackages) {
|
|
239
|
+
for (const pkg of packages.filter(isLpmPackage)) {
|
|
240
|
+
await skillsInstall(pkg)
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
await skillsInstall(null)
|
|
244
|
+
}
|
|
201
245
|
}
|
|
202
246
|
}
|
|
203
247
|
})
|
|
204
248
|
|
|
205
249
|
child.on("error", err => {
|
|
206
|
-
log.error(`Failed to start
|
|
250
|
+
log.error(`Failed to start ${pm}: ${err.message}`)
|
|
207
251
|
cleanup()
|
|
208
252
|
process.exit(1)
|
|
209
253
|
})
|
package/lib/commands/npmrc.js
CHANGED
|
@@ -82,15 +82,41 @@ export async function npmrc(options) {
|
|
|
82
82
|
: `${registryUrl}/api/registry`
|
|
83
83
|
const registryHost = fullRegistryUrl.replace(/^https?:/, "")
|
|
84
84
|
|
|
85
|
-
//
|
|
85
|
+
// Determine proxy vs scoped mode
|
|
86
|
+
let useScoped = !!options?.scoped
|
|
87
|
+
|
|
88
|
+
// Read existing .npmrc and check for custom default registry
|
|
86
89
|
let npmrcContent = ""
|
|
87
90
|
if (fs.existsSync(npmrcPath)) {
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
const existingContent = fs.readFileSync(npmrcPath, "utf8")
|
|
92
|
+
|
|
93
|
+
// Detect custom default registry (not npmjs.org and not lpm.dev)
|
|
94
|
+
if (!useScoped) {
|
|
95
|
+
const registryLine = existingContent
|
|
96
|
+
.split("\n")
|
|
97
|
+
.find(
|
|
98
|
+
line =>
|
|
99
|
+
line.match(/^registry=/) &&
|
|
100
|
+
!line.includes("registry.npmjs.org") &&
|
|
101
|
+
!line.includes("lpm.dev"),
|
|
102
|
+
)
|
|
103
|
+
if (registryLine) {
|
|
104
|
+
log.warn(`Found existing default registry: ${registryLine.trim()}`)
|
|
105
|
+
log.warn("Using --scoped mode to avoid overriding it.")
|
|
106
|
+
log.warn(
|
|
107
|
+
"To use LPM as your default registry, remove the existing registry= line first.",
|
|
108
|
+
)
|
|
109
|
+
useScoped = true
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Remove old LPM config lines (both scoped and unscoped formats)
|
|
114
|
+
npmrcContent = existingContent
|
|
90
115
|
.split("\n")
|
|
91
116
|
.filter(line => {
|
|
92
117
|
return (
|
|
93
118
|
!line.includes("@lpm.dev:registry") &&
|
|
119
|
+
!line.match(/^registry=.*lpm\.dev/) &&
|
|
94
120
|
!line.includes("lpm.dev/api/registry/:_authToken") &&
|
|
95
121
|
!line.includes("_authToken=lpm_") &&
|
|
96
122
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: matching literal npm token placeholder
|
|
@@ -102,8 +128,12 @@ export async function npmrc(options) {
|
|
|
102
128
|
.trim()
|
|
103
129
|
}
|
|
104
130
|
|
|
131
|
+
const registryLine = useScoped
|
|
132
|
+
? `@lpm.dev:registry=${fullRegistryUrl}`
|
|
133
|
+
: `registry=${fullRegistryUrl}`
|
|
134
|
+
|
|
105
135
|
const lpmConfig = `# LPM Registry (generated by lpm npmrc — do not commit)
|
|
106
|
-
|
|
136
|
+
${registryLine}
|
|
107
137
|
${registryHost}/:_authToken=${readToken}`
|
|
108
138
|
|
|
109
139
|
npmrcContent = npmrcContent ? `${npmrcContent}\n\n${lpmConfig}` : lpmConfig
|
|
@@ -129,6 +159,13 @@ ${registryHost}/:_authToken=${readToken}`
|
|
|
129
159
|
log.info(".npmrc added to .gitignore to prevent token leaks.")
|
|
130
160
|
}
|
|
131
161
|
|
|
162
|
+
if (useScoped) {
|
|
163
|
+
log.info("Only @lpm.dev packages will route through lpm.dev.")
|
|
164
|
+
} else {
|
|
165
|
+
log.info("All packages (LPM + npm) will route through lpm.dev.")
|
|
166
|
+
log.info("Note: First install may update resolved URLs in your lockfile.")
|
|
167
|
+
}
|
|
168
|
+
|
|
132
169
|
const expiryDate = new Date(expiresAt).toLocaleDateString("en-US", {
|
|
133
170
|
year: "numeric",
|
|
134
171
|
month: "long",
|
package/lib/commands/publish.js
CHANGED
|
@@ -522,8 +522,63 @@ export async function publish(options = {}) {
|
|
|
522
522
|
swiftManifest,
|
|
523
523
|
})
|
|
524
524
|
|
|
525
|
+
// 7b. Check for stale skills (compare local with published)
|
|
526
|
+
spinner.text = "Checking skills..."
|
|
527
|
+
const prePublishWarnings = []
|
|
528
|
+
try {
|
|
529
|
+
const localSkillsDir = path.join(process.cwd(), ".lpm", "skills")
|
|
530
|
+
if (fs.existsSync(localSkillsDir)) {
|
|
531
|
+
const skillFiles = fs
|
|
532
|
+
.readdirSync(localSkillsDir)
|
|
533
|
+
.filter(f => f.endsWith(".md"))
|
|
534
|
+
.sort()
|
|
535
|
+
|
|
536
|
+
if (skillFiles.length > 0) {
|
|
537
|
+
// Read local skills content
|
|
538
|
+
const localSkills = skillFiles.map(f => ({
|
|
539
|
+
name: f.replace(/\.md$/, ""),
|
|
540
|
+
rawContent: fs.readFileSync(path.join(localSkillsDir, f), "utf8"),
|
|
541
|
+
}))
|
|
542
|
+
|
|
543
|
+
// Fetch published skills for comparison
|
|
544
|
+
const cleanedName = name.startsWith("@lpm.dev/")
|
|
545
|
+
? name.replace("@lpm.dev/", "")
|
|
546
|
+
: `${owner}.${packageName}`
|
|
547
|
+
const skillsResponse = await request(
|
|
548
|
+
`/skills?name=${encodeURIComponent(cleanedName)}`,
|
|
549
|
+
{ method: "GET" },
|
|
550
|
+
).catch(() => null)
|
|
551
|
+
|
|
552
|
+
if (skillsResponse?.ok) {
|
|
553
|
+
const published = await skillsResponse.json()
|
|
554
|
+
if (published.available && published.skills?.length > 0) {
|
|
555
|
+
const pubSorted = [...published.skills]
|
|
556
|
+
.map(s => ({ name: s.name, rawContent: s.rawContent }))
|
|
557
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
558
|
+
|
|
559
|
+
const isIdentical =
|
|
560
|
+
localSkills.length === pubSorted.length &&
|
|
561
|
+
localSkills.every(
|
|
562
|
+
(local, i) =>
|
|
563
|
+
local.name === pubSorted[i].name &&
|
|
564
|
+
local.rawContent === pubSorted[i].rawContent,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if (isIdentical) {
|
|
568
|
+
prePublishWarnings.push(
|
|
569
|
+
`Agent Skills haven't changed since v${published.version} - consider reviewing them for accuracy`,
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
} catch {
|
|
577
|
+
// Non-critical — skip skills staleness check
|
|
578
|
+
}
|
|
579
|
+
|
|
525
580
|
spinner.stop()
|
|
526
|
-
displayQualityReport(qualityResult)
|
|
581
|
+
displayQualityReport({ ...qualityResult, warnings: prePublishWarnings })
|
|
527
582
|
|
|
528
583
|
// --dry-run: print summary and exit without uploading
|
|
529
584
|
if (dryRun) {
|
|
@@ -848,11 +903,16 @@ export async function publish(options = {}) {
|
|
|
848
903
|
)
|
|
849
904
|
}
|
|
850
905
|
|
|
851
|
-
//
|
|
906
|
+
// Server may still return warnings for edge cases not caught client-side
|
|
852
907
|
if (publishResult.warnings?.length > 0) {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
908
|
+
const newWarnings = publishResult.warnings.filter(
|
|
909
|
+
w => !prePublishWarnings.includes(w),
|
|
910
|
+
)
|
|
911
|
+
if (newWarnings.length > 0) {
|
|
912
|
+
console.log("")
|
|
913
|
+
for (const warning of newWarnings) {
|
|
914
|
+
log.warn(warning)
|
|
915
|
+
}
|
|
856
916
|
}
|
|
857
917
|
}
|
|
858
918
|
} catch (error) {
|
package/lib/commands/setup.js
CHANGED
|
@@ -8,6 +8,7 @@ import { log, printHeader } from "../ui.js"
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Remove existing LPM config lines from .npmrc content.
|
|
11
|
+
* Handles both scoped (@lpm.dev:registry=) and unscoped (registry=...lpm.dev) formats.
|
|
11
12
|
* @param {string} content
|
|
12
13
|
* @returns {string}
|
|
13
14
|
*/
|
|
@@ -17,6 +18,7 @@ function removeLpmLines(content) {
|
|
|
17
18
|
.filter(line => {
|
|
18
19
|
return (
|
|
19
20
|
!line.includes("@lpm.dev:registry") &&
|
|
21
|
+
!line.match(/^registry=.*lpm\.dev/) &&
|
|
20
22
|
!line.includes("lpm.dev/api/registry/:_authToken") &&
|
|
21
23
|
!line.includes("_authToken=lpm_") &&
|
|
22
24
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: matching literal npm token placeholder
|
|
@@ -28,6 +30,24 @@ function removeLpmLines(content) {
|
|
|
28
30
|
.trim()
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Check if .npmrc has a custom default registry that is NOT npmjs.org or lpm.dev.
|
|
35
|
+
* @param {string} content - Existing .npmrc content
|
|
36
|
+
* @returns {string|null} The custom registry line, or null if none found
|
|
37
|
+
*/
|
|
38
|
+
function detectCustomRegistry(content) {
|
|
39
|
+
return (
|
|
40
|
+
content
|
|
41
|
+
.split("\n")
|
|
42
|
+
.find(
|
|
43
|
+
line =>
|
|
44
|
+
line.match(/^registry=/) &&
|
|
45
|
+
!line.includes("registry.npmjs.org") &&
|
|
46
|
+
!line.includes("lpm.dev"),
|
|
47
|
+
) || null
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
/**
|
|
32
52
|
* Configure .npmrc for LPM packages.
|
|
33
53
|
*
|
|
@@ -55,6 +75,9 @@ export async function setup(options) {
|
|
|
55
75
|
: `${registryUrl}/api/registry`
|
|
56
76
|
const registryHost = fullRegistryUrl.replace(/^https?:/, "")
|
|
57
77
|
|
|
78
|
+
// Determine proxy vs scoped mode
|
|
79
|
+
let useScoped = !!options?.scoped
|
|
80
|
+
|
|
58
81
|
// ── OIDC mode: exchange CI token for a real short-lived install token ──
|
|
59
82
|
if (options?.oidc) {
|
|
60
83
|
let oidcToken
|
|
@@ -70,11 +93,29 @@ export async function setup(options) {
|
|
|
70
93
|
if (oidcToken) {
|
|
71
94
|
let npmrcContent = ""
|
|
72
95
|
if (fs.existsSync(npmrcPath)) {
|
|
73
|
-
|
|
96
|
+
const existingContent = fs.readFileSync(npmrcPath, "utf8")
|
|
97
|
+
|
|
98
|
+
// Detect custom default registry
|
|
99
|
+
if (!useScoped) {
|
|
100
|
+
const customRegistry = detectCustomRegistry(existingContent)
|
|
101
|
+
if (customRegistry) {
|
|
102
|
+
log.warn(
|
|
103
|
+
`Found existing default registry: ${customRegistry.trim()}`,
|
|
104
|
+
)
|
|
105
|
+
log.warn("Using --scoped mode to avoid overriding it.")
|
|
106
|
+
useScoped = true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
npmrcContent = removeLpmLines(existingContent)
|
|
74
111
|
}
|
|
75
112
|
|
|
113
|
+
const registryLine = useScoped
|
|
114
|
+
? `@lpm.dev:registry=${fullRegistryUrl}`
|
|
115
|
+
: `registry=${fullRegistryUrl}`
|
|
116
|
+
|
|
76
117
|
const lpmConfig = `# LPM Registry (OIDC — expires in 30 minutes)
|
|
77
|
-
|
|
118
|
+
${registryLine}
|
|
78
119
|
${registryHost}/:_authToken=${oidcToken}`
|
|
79
120
|
|
|
80
121
|
npmrcContent = npmrcContent
|
|
@@ -83,13 +124,16 @@ ${registryHost}/:_authToken=${oidcToken}`
|
|
|
83
124
|
fs.writeFileSync(npmrcPath, `${npmrcContent}\n`)
|
|
84
125
|
|
|
85
126
|
p.note(
|
|
86
|
-
|
|
127
|
+
`${registryLine}\nToken: OIDC read-only (30 min)`,
|
|
87
128
|
".npmrc configuration",
|
|
88
129
|
)
|
|
89
130
|
|
|
90
131
|
console.log("")
|
|
91
132
|
log.success(".npmrc configured with OIDC read-only token.")
|
|
92
133
|
log.info("Token expires in 30 minutes. No LPM_TOKEN secret needed.")
|
|
134
|
+
if (!useScoped) {
|
|
135
|
+
log.info("All packages (LPM + npm) will route through lpm.dev.")
|
|
136
|
+
}
|
|
93
137
|
console.log("")
|
|
94
138
|
|
|
95
139
|
p.outro("Setup complete!")
|
|
@@ -104,8 +148,24 @@ ${registryHost}/:_authToken=${oidcToken}`
|
|
|
104
148
|
if (fs.existsSync(npmrcPath)) {
|
|
105
149
|
npmrcContent = fs.readFileSync(npmrcPath, "utf8")
|
|
106
150
|
|
|
107
|
-
//
|
|
108
|
-
if (
|
|
151
|
+
// Detect custom default registry
|
|
152
|
+
if (!useScoped) {
|
|
153
|
+
const customRegistry = detectCustomRegistry(npmrcContent)
|
|
154
|
+
if (customRegistry) {
|
|
155
|
+
log.warn(`Found existing default registry: ${customRegistry.trim()}`)
|
|
156
|
+
log.warn("Using --scoped mode to avoid overriding it.")
|
|
157
|
+
log.warn(
|
|
158
|
+
"To use LPM as your default registry, remove the existing registry= line first.",
|
|
159
|
+
)
|
|
160
|
+
useScoped = true
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check if already configured (both scoped and unscoped formats)
|
|
165
|
+
if (
|
|
166
|
+
npmrcContent.includes("@lpm.dev:registry") ||
|
|
167
|
+
npmrcContent.match(/^registry=.*lpm\.dev/m)
|
|
168
|
+
) {
|
|
109
169
|
const overwrite = await p.confirm({
|
|
110
170
|
message: ".npmrc already has LPM configuration. Overwrite?",
|
|
111
171
|
initialValue: false,
|
|
@@ -120,10 +180,14 @@ ${registryHost}/:_authToken=${oidcToken}`
|
|
|
120
180
|
}
|
|
121
181
|
}
|
|
122
182
|
|
|
183
|
+
const registryLine = useScoped
|
|
184
|
+
? `@lpm.dev:registry=${fullRegistryUrl}`
|
|
185
|
+
: `registry=${fullRegistryUrl}`
|
|
186
|
+
|
|
123
187
|
// Add LPM registry config with placeholder
|
|
124
188
|
const lpmConfig = `
|
|
125
189
|
# LPM Registry
|
|
126
|
-
|
|
190
|
+
${registryLine}
|
|
127
191
|
${registryHost}/:_authToken=\${LPM_TOKEN}
|
|
128
192
|
`.trim()
|
|
129
193
|
|
|
@@ -131,12 +195,18 @@ ${registryHost}/:_authToken=\${LPM_TOKEN}
|
|
|
131
195
|
fs.writeFileSync(npmrcPath, `${npmrcContent}\n`)
|
|
132
196
|
|
|
133
197
|
p.note(
|
|
134
|
-
|
|
198
|
+
`${registryLine}\n${registryHost}/:_authToken=\${LPM_TOKEN}`,
|
|
135
199
|
".npmrc configuration",
|
|
136
200
|
)
|
|
137
201
|
|
|
138
202
|
console.log("")
|
|
139
|
-
log.success(".npmrc configured for LPM
|
|
203
|
+
log.success(".npmrc configured for LPM registry.")
|
|
204
|
+
if (useScoped) {
|
|
205
|
+
log.info("Only @lpm.dev packages will route through lpm.dev.")
|
|
206
|
+
} else {
|
|
207
|
+
log.info("All packages (LPM + npm) will route through lpm.dev.")
|
|
208
|
+
log.info("Note: First install may update resolved URLs in your lockfile.")
|
|
209
|
+
}
|
|
140
210
|
log.info("For local development: Run `lpm npmrc` to generate a token.")
|
|
141
211
|
log.info("For CI/CD: Set the LPM_TOKEN environment variable.")
|
|
142
212
|
log.info("For CI/CD with OIDC: Run `lpm setup --oidc` (no secrets needed).")
|
package/lib/commands/skills.js
CHANGED
|
@@ -322,7 +322,18 @@ function getLpmDependencies() {
|
|
|
322
322
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
323
323
|
|
|
324
324
|
return Object.entries(allDeps)
|
|
325
|
-
.filter(([name]) =>
|
|
325
|
+
.filter(([name, version]) => {
|
|
326
|
+
if (!name.startsWith("@lpm.dev/")) return false
|
|
327
|
+
// Skip local file/link references — they have their own .lpm/skills/
|
|
328
|
+
if (
|
|
329
|
+
version.startsWith("file:") ||
|
|
330
|
+
version.startsWith("link:") ||
|
|
331
|
+
version.startsWith("workspace:")
|
|
332
|
+
) {
|
|
333
|
+
return false
|
|
334
|
+
}
|
|
335
|
+
return true
|
|
336
|
+
})
|
|
326
337
|
.map(([name, version]) => ({
|
|
327
338
|
fullName: name,
|
|
328
339
|
shortName: name.replace("@lpm.dev/", ""),
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { getPackageManager, getRegistryUrl, getToken } from "../config.js"
|
|
5
|
+
import { createSpinner, log, printHeader } from "../ui.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Uninstall LPM packages.
|
|
9
|
+
* Creates a temporary .npmrc with auth, runs the package manager's uninstall, and cleans up.
|
|
10
|
+
*
|
|
11
|
+
* @param {string[]} packages - Package names to uninstall
|
|
12
|
+
* @param {object} [options]
|
|
13
|
+
* @param {string} [options.pm] - Package manager override (npm, pnpm, yarn, bun)
|
|
14
|
+
*/
|
|
15
|
+
export async function uninstall(packages, options) {
|
|
16
|
+
printHeader()
|
|
17
|
+
|
|
18
|
+
if (!packages || packages.length === 0) {
|
|
19
|
+
log.error("Please specify at least one package to uninstall.")
|
|
20
|
+
log.info("Usage: lpm uninstall @lpm.dev/owner.package-name")
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const token = await getToken()
|
|
25
|
+
if (!token) {
|
|
26
|
+
log.error(
|
|
27
|
+
'You must be logged in to uninstall packages. Run "lpm login" first.',
|
|
28
|
+
)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pm = getPackageManager(options?.pm)
|
|
33
|
+
const spinner = createSpinner(
|
|
34
|
+
`Preparing to uninstall ${packages.join(", ")}...`,
|
|
35
|
+
).start()
|
|
36
|
+
|
|
37
|
+
const baseRegistryUrl = getRegistryUrl()
|
|
38
|
+
const registryUrl = baseRegistryUrl.endsWith("/api/registry")
|
|
39
|
+
? baseRegistryUrl
|
|
40
|
+
: `${baseRegistryUrl}/api/registry`
|
|
41
|
+
const registryHost = registryUrl.replace(/^https?:/, "")
|
|
42
|
+
|
|
43
|
+
const npmrcContent = `${registryHost}/:_authToken=${token}
|
|
44
|
+
@lpm.dev:registry=${registryUrl}
|
|
45
|
+
`
|
|
46
|
+
|
|
47
|
+
const projectNpmrcPath = path.resolve(process.cwd(), ".npmrc")
|
|
48
|
+
let existingNpmrc = null
|
|
49
|
+
let tempNpmrcPath = null
|
|
50
|
+
|
|
51
|
+
if (pm === "npm") {
|
|
52
|
+
tempNpmrcPath = path.resolve(process.cwd(), `.npmrc.lpm-${Date.now()}`)
|
|
53
|
+
fs.writeFileSync(tempNpmrcPath, npmrcContent)
|
|
54
|
+
} else {
|
|
55
|
+
if (fs.existsSync(projectNpmrcPath)) {
|
|
56
|
+
existingNpmrc = fs.readFileSync(projectNpmrcPath, "utf8")
|
|
57
|
+
}
|
|
58
|
+
fs.writeFileSync(projectNpmrcPath, npmrcContent)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
spinner.succeed(`Configuration generated. Running ${pm} uninstall...`)
|
|
62
|
+
|
|
63
|
+
const pmArgs =
|
|
64
|
+
pm === "npm"
|
|
65
|
+
? ["uninstall", ...packages, "--userconfig", tempNpmrcPath]
|
|
66
|
+
: ["uninstall", ...packages]
|
|
67
|
+
|
|
68
|
+
const child = spawn(pm, pmArgs, {
|
|
69
|
+
stdio: "inherit",
|
|
70
|
+
env: { ...process.env, LPM_TOKEN: token },
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const cleanup = () => {
|
|
74
|
+
if (pm === "npm") {
|
|
75
|
+
if (tempNpmrcPath && fs.existsSync(tempNpmrcPath)) {
|
|
76
|
+
fs.unlinkSync(tempNpmrcPath)
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
if (existingNpmrc !== null) {
|
|
80
|
+
fs.writeFileSync(projectNpmrcPath, existingNpmrc)
|
|
81
|
+
} else if (fs.existsSync(projectNpmrcPath)) {
|
|
82
|
+
fs.unlinkSync(projectNpmrcPath)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
child.on("close", code => {
|
|
88
|
+
cleanup()
|
|
89
|
+
if (code !== 0) {
|
|
90
|
+
log.error(`${pm} uninstall failed with code ${code}`)
|
|
91
|
+
process.exit(code)
|
|
92
|
+
} else {
|
|
93
|
+
log.success("Packages uninstalled successfully.")
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
child.on("error", err => {
|
|
98
|
+
log.error(`Failed to start ${pm}: ${err.message}`)
|
|
99
|
+
cleanup()
|
|
100
|
+
process.exit(1)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
process.on("SIGINT", () => {
|
|
104
|
+
cleanup()
|
|
105
|
+
process.exit()
|
|
106
|
+
})
|
|
107
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -183,6 +183,32 @@ export function setRetries(count) {
|
|
|
183
183
|
config.set("retries", count)
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// Package Manager Configuration
|
|
188
|
+
// ============================================================================
|
|
189
|
+
|
|
190
|
+
const VALID_PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn", "bun"])
|
|
191
|
+
const DEFAULT_PACKAGE_MANAGER = "npm"
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get the configured package manager for install/uninstall operations.
|
|
195
|
+
* Priority: --pm flag > LPM_PACKAGE_MANAGER env > config > "npm"
|
|
196
|
+
* @param {string} [override] - CLI flag override (--pm value)
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
export function getPackageManager(override) {
|
|
200
|
+
const pm =
|
|
201
|
+
override ||
|
|
202
|
+
process.env.LPM_PACKAGE_MANAGER ||
|
|
203
|
+
config.get("packageManager", DEFAULT_PACKAGE_MANAGER)
|
|
204
|
+
|
|
205
|
+
if (!VALID_PACKAGE_MANAGERS.has(pm)) {
|
|
206
|
+
return DEFAULT_PACKAGE_MANAGER
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return pm
|
|
210
|
+
}
|
|
211
|
+
|
|
186
212
|
// ============================================================================
|
|
187
213
|
// General Configuration Access
|
|
188
214
|
// ============================================================================
|
|
@@ -197,6 +223,7 @@ export async function getAllConfig() {
|
|
|
197
223
|
|
|
198
224
|
return {
|
|
199
225
|
registryUrl: getRegistryUrl(),
|
|
226
|
+
packageManager: getPackageManager(),
|
|
200
227
|
timeout: getTimeout(),
|
|
201
228
|
retries: getRetries(),
|
|
202
229
|
secureStorage: usingKeychain ? "keychain" : "encrypted-file",
|
|
@@ -213,6 +240,7 @@ export function getConfigValue(key) {
|
|
|
213
240
|
const configMap = {
|
|
214
241
|
registry: getRegistryUrl,
|
|
215
242
|
registryUrl: getRegistryUrl,
|
|
243
|
+
packageManager: getPackageManager,
|
|
216
244
|
timeout: getTimeout,
|
|
217
245
|
retries: getRetries,
|
|
218
246
|
}
|
|
@@ -232,6 +260,15 @@ export function setConfigValue(key, value) {
|
|
|
232
260
|
const configMap = {
|
|
233
261
|
registry: setRegistryUrl,
|
|
234
262
|
registryUrl: setRegistryUrl,
|
|
263
|
+
packageManager: val => {
|
|
264
|
+
if (VALID_PACKAGE_MANAGERS.has(val)) {
|
|
265
|
+
config.set("packageManager", val)
|
|
266
|
+
} else {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Invalid package manager "${val}". Valid options: ${[...VALID_PACKAGE_MANAGERS].join(", ")}`,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
},
|
|
235
272
|
timeout: val => setTimeout(Number(val)),
|
|
236
273
|
retries: val => setRetries(Number(val)),
|
|
237
274
|
}
|
package/lib/quality/display.js
CHANGED
|
@@ -38,9 +38,9 @@ function progressBar(value, max, width = 18) {
|
|
|
38
38
|
/**
|
|
39
39
|
* Display quality report in the terminal.
|
|
40
40
|
*
|
|
41
|
-
* @param {{ score: number, checks: Array, meta: object }} result
|
|
41
|
+
* @param {{ score: number, checks: Array, meta: object, warnings?: string[] }} result
|
|
42
42
|
*/
|
|
43
|
-
export function displayQualityReport({ score, checks, meta }) {
|
|
43
|
+
export function displayQualityReport({ score, checks, meta, warnings }) {
|
|
44
44
|
const tierColor = TIER_COLORS[meta.tier] || chalk.white
|
|
45
45
|
const tierLabel = TIER_LABELS[meta.tier] || meta.tier
|
|
46
46
|
|
|
@@ -76,6 +76,14 @@ export function displayQualityReport({ score, checks, meta }) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// Show warnings (e.g., stale skills)
|
|
80
|
+
if (warnings?.length > 0) {
|
|
81
|
+
if (failed.length === 0) console.log("")
|
|
82
|
+
for (const warning of warnings) {
|
|
83
|
+
console.log(` ${chalk.yellow("⚠")} ${chalk.yellow(warning)}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
if (failed.length > 0 && score < 90) {
|
|
80
88
|
console.log(
|
|
81
89
|
chalk.dim(
|