dep-report 0.0.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.
- package/README.md +262 -0
- package/dist/cli.js +170 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# dep-report
|
|
2
|
+
|
|
3
|
+
Zero-config CLI tool that generates version-controlled snapshots of dependency risk.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g dep-report
|
|
9
|
+
# or
|
|
10
|
+
pnpm add -g dep-report
|
|
11
|
+
# or
|
|
12
|
+
bun add -g dep-report
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or use with npx (no installation needed):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx dep-report
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
Run in your project directory:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
dep-report
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This will:
|
|
30
|
+
1. Detect your package manager (npm, pnpm, or bun)
|
|
31
|
+
2. Scan for outdated packages
|
|
32
|
+
3. Enrich with registry metadata (publish dates, age)
|
|
33
|
+
4. Generate reports in `.dep-report/reports/`
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Initialize Configuration
|
|
38
|
+
|
|
39
|
+
To customize behavior, initialize the configuration:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dep-report init
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This creates `.dep-report/config.json` and `.dep-report/notes.json` in your project.
|
|
46
|
+
|
|
47
|
+
### Basic Audit
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
dep-report
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Scans for outdated packages and generates reports based on your configuration.
|
|
54
|
+
|
|
55
|
+
### Reports
|
|
56
|
+
|
|
57
|
+
Reports are generated in `.dep-report/reports/`:
|
|
58
|
+
- `YYYY-MM-DD_outdated.md` - Daily snapshot (markdown)
|
|
59
|
+
- `YYYY-MM-DD_outdated.html` - Daily snapshot (HTML)
|
|
60
|
+
- `latest.md` - Always points to the most recent markdown report
|
|
61
|
+
- `latest.html` - Always points to the most recent HTML report
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
Configuration is stored in `.dep-report/config.json`. Run `dep-report init` to create it.
|
|
66
|
+
|
|
67
|
+
### Configuration Options
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"staleThreshold": "18 months",
|
|
72
|
+
"ignorePatterns": ["@types/*", "eslint-*"],
|
|
73
|
+
"formats": {
|
|
74
|
+
"markdown": true,
|
|
75
|
+
"html": true
|
|
76
|
+
},
|
|
77
|
+
"concurrency": 5,
|
|
78
|
+
"failConditions": {
|
|
79
|
+
"stale": false,
|
|
80
|
+
"major": false
|
|
81
|
+
},
|
|
82
|
+
"reportEmptyState": true
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### `staleThreshold` (string)
|
|
87
|
+
|
|
88
|
+
Duration string indicating when a package is considered "stale". Format: `"N days"`, `"N weeks"`, `"N months"`, or `"N years"`.
|
|
89
|
+
|
|
90
|
+
Example: `"18 months"`, `"90 days"`, `"2 years"`
|
|
91
|
+
|
|
92
|
+
#### `ignorePatterns` (string[])
|
|
93
|
+
|
|
94
|
+
Array of glob patterns to exclude from reports. Uses [minimatch](https://github.com/isaacs/minimatch) for pattern matching.
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
- `["@types/*"]` - Ignore all @types packages
|
|
98
|
+
- `["eslint-*", "@eslint/*"]` - Ignore eslint-related packages
|
|
99
|
+
- `["package-name"]` - Ignore specific package
|
|
100
|
+
|
|
101
|
+
#### `formats` (object)
|
|
102
|
+
|
|
103
|
+
Controls which report formats are generated:
|
|
104
|
+
- `markdown`: Generate markdown reports (`.md`)
|
|
105
|
+
- `html`: Generate HTML reports (`.html`)
|
|
106
|
+
|
|
107
|
+
#### `concurrency` (number)
|
|
108
|
+
|
|
109
|
+
Number of concurrent registry API requests. Default: `5`. Increase for faster processing (but be respectful of rate limits).
|
|
110
|
+
|
|
111
|
+
#### `failConditions` (object)
|
|
112
|
+
|
|
113
|
+
Exit code conditions for CI/CD integration:
|
|
114
|
+
- `stale`: Exit with code 1 if any packages exceed `staleThreshold`
|
|
115
|
+
- `major`: Exit with code 1 if any packages have major version updates available
|
|
116
|
+
|
|
117
|
+
#### `reportEmptyState` (boolean)
|
|
118
|
+
|
|
119
|
+
Whether to generate reports when no outdated packages are found. Default: `true`.
|
|
120
|
+
|
|
121
|
+
### Notes
|
|
122
|
+
|
|
123
|
+
Add custom notes to packages in `.dep-report/notes.json`:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"package-name": "Known issue: waiting for v2.0.0 release",
|
|
128
|
+
"another-package": "Upgrade blocked by breaking changes"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Notes appear in reports next to the package entry.
|
|
133
|
+
|
|
134
|
+
## Usage Examples
|
|
135
|
+
|
|
136
|
+
### CI/CD Integration
|
|
137
|
+
|
|
138
|
+
Fail the build if major updates are available:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"failConditions": {
|
|
143
|
+
"major": true
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
dep-report || exit 1
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Ignore Dev Dependencies
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"ignorePatterns": ["@types/*", "eslint-*", "@eslint/*", "typescript", "prettier"]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Only Markdown Reports
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"formats": {
|
|
165
|
+
"markdown": true,
|
|
166
|
+
"html": false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Custom Stale Threshold
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"staleThreshold": "6 months"
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Edge Cases & Limitations
|
|
180
|
+
|
|
181
|
+
### Exotic Versions
|
|
182
|
+
|
|
183
|
+
Packages with non-semver versions (e.g., `file:`, `git+`, `workspace:`) are marked as `Exotic` risk level. Age calculation may not be available for these packages.
|
|
184
|
+
|
|
185
|
+
### Missing Packages
|
|
186
|
+
|
|
187
|
+
If a package is listed in `package.json` but not installed, it's marked as `NotInstalled`.
|
|
188
|
+
|
|
189
|
+
### Registry Connectivity
|
|
190
|
+
|
|
191
|
+
The tool requires internet connectivity to fetch package metadata from the npm registry. If the registry is unreachable, the tool will exit with an error.
|
|
192
|
+
|
|
193
|
+
### Package Manager Detection
|
|
194
|
+
|
|
195
|
+
The tool detects package managers by looking for lockfiles in this priority order:
|
|
196
|
+
1. `pnpm-lock.yaml` → pnpm
|
|
197
|
+
2. `bun.lock` or `bun.lockb` → bun
|
|
198
|
+
3. `package-lock.json` → npm
|
|
199
|
+
|
|
200
|
+
If no lockfile is found, the tool will exit with an error.
|
|
201
|
+
|
|
202
|
+
### Rate Limiting
|
|
203
|
+
|
|
204
|
+
The tool respects npm registry rate limits by:
|
|
205
|
+
- Processing packages in batches (default: 5 concurrent requests)
|
|
206
|
+
- Adding a 500ms delay between batches
|
|
207
|
+
- You can adjust `concurrency` in config, but be mindful of rate limits
|
|
208
|
+
|
|
209
|
+
### Age Calculation
|
|
210
|
+
|
|
211
|
+
Age is calculated based on when the **currently installed version** was published, not when the latest version was published. This answers: "How old is the dependency we're actively using?"
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# Install dependencies
|
|
217
|
+
bun install
|
|
218
|
+
|
|
219
|
+
# Run in development mode
|
|
220
|
+
bun run dev
|
|
221
|
+
|
|
222
|
+
# Build for production
|
|
223
|
+
bun run build
|
|
224
|
+
|
|
225
|
+
# Run tests
|
|
226
|
+
bun test
|
|
227
|
+
|
|
228
|
+
# Type check
|
|
229
|
+
bun run typecheck
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Testing
|
|
233
|
+
|
|
234
|
+
### Quick Test (Fast)
|
|
235
|
+
```bash
|
|
236
|
+
bun test # Unit + integration tests (<5 seconds)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Sandbox Test (Thorough)
|
|
240
|
+
```bash
|
|
241
|
+
bun run test:sandbox # Real API calls (~30 seconds)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
See [sandbox/README.md](sandbox/README.md) for details.
|
|
245
|
+
|
|
246
|
+
### Before Releasing
|
|
247
|
+
1. Run `bun test` (must pass)
|
|
248
|
+
2. Run `bun run test:sandbox` (must pass)
|
|
249
|
+
3. Visually inspect HTML reports
|
|
250
|
+
4. Run `npm run build` and test CLI manually
|
|
251
|
+
|
|
252
|
+
## Project Status
|
|
253
|
+
|
|
254
|
+
✅ **Production Ready**
|
|
255
|
+
- ✅ Package manager detection (npm, pnpm, bun)
|
|
256
|
+
- ✅ Outdated package scanning
|
|
257
|
+
- ✅ Registry enrichment with age calculation
|
|
258
|
+
- ✅ Risk & age calculation
|
|
259
|
+
- ✅ Markdown & HTML report generation
|
|
260
|
+
- ✅ Configuration system
|
|
261
|
+
- ✅ Notes system
|
|
262
|
+
- ✅ Comprehensive test suite
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{Command as At}from"commander";import{existsSync as ot,mkdirSync as nt,writeFileSync as k,readFileSync as Ft}from"fs";import{join as y}from"path";import{existsSync as at}from"fs";import{join as it}from"path";function D(t=process.cwd()){let e=[{file:"pnpm-lock.yaml",manager:"pnpm"},{file:"bun.lock",manager:"bun"},{file:"bun.lockb",manager:"bun"},{file:"package-lock.json",manager:"npm"}];for(let{file:r,manager:o}of e){let s=it(t,r);if(at(s))return{manager:o,lockfile:r}}return null}function N(t){return{npm:"npm outdated --json",pnpm:"pnpm outdated --json",bun:"bun outdated --json"}[t]}import{exec as ct}from"child_process";import{promisify as dt}from"util";var lt=dt(ct);async function T(t,e=process.cwd()){let r=N(t);try{let{stdout:o,stderr:s}=await lt(r,{cwd:e,maxBuffer:10485760}),c=o.trim()||s.trim();if(!c)return{};try{return JSON.parse(c)}catch(i){if(c.includes("All packages are up to date")||c==="{}")return{};throw new Error(`Failed to parse outdated output: ${i instanceof Error?i.message:String(i)}`)}}catch(o){if(o.stdout)try{return JSON.parse(o.stdout)}catch{throw o.code==="ENOENT"?new Error(`Package manager "${t}" not found. Please install it first.`):new Error(`Failed to execute outdated command: ${o.message}`)}if(o.code==="ENOENT")throw new Error(`Package manager "${t}" not found. Please install it first.`);return{}}}function I(t){let e=[];for(let[r,o]of Object.entries(t)){if(typeof o!="object"||o===null||Array.isArray(o))continue;let s=o.current||o.installed||o.version||"-",c=o.latest||o.wanted||s,i=o.wanted||o.current||c,a=o.type||o.dependencyType||mt(r);!r||!c||e.push({name:r,current:String(s),wanted:String(i),latest:String(c),type:a})}return e}function mt(t){return t.startsWith("@types/")||t.includes("-test")||t.includes("-spec")||t==="typescript"||t==="eslint"||t.startsWith("eslint-")||t.startsWith("@eslint/")?"devDependencies":"dependencies"}var x=class{constructor(e="https://registry.npmjs.org"){this.baseUrl=e}async getMetadata(e){try{let r=`${this.baseUrl}/${encodeURIComponent(e)}`,o=await fetch(r,{headers:{Accept:"application/json"}});if(!o.ok){if(o.status===404)return null;throw new Error(`Registry request failed: ${o.status}`)}return await o.json()}catch{return null}}};var A=new x;async function pt(t,e=A,r=new Date){let s=await(typeof e=="string"?new x(e):e).getMetadata(t.name);if(!s||!s.time)return{...t,currentPublishedAt:null,latestPublishedAt:null,age:null,isStale:!1,risk:"Exotic"};let c=s.time[t.current]?new Date(s.time[t.current]):null,i=s.time[t.latest]?new Date(s.time[t.latest]):null,a=null;if(c){let p=r.getTime()-c.getTime();a=Math.floor(p/(1e3*60*60*24))}return{...t,currentPublishedAt:c,latestPublishedAt:i,age:a,isStale:!1,risk:"Exotic"}}async function F(t,e=5,r=A,o=new Date){let s=typeof r=="string"?new x(r):r,c=[];for(let i=0;i<t.length;i+=e){let a=t.slice(i,i+e),p=await Promise.all(a.map(d=>pt(d,s,o)));c.push(...p),i+e<t.length&&await new Promise(d=>setTimeout(d,500))}return c}import{diff as ut,valid as _}from"semver";function ft(t){return!t||t==="-"||t==="missing"?!1:/^(file:|git\+|https?:|link:|workspace:)/.test(t)}function gt(t,e){if(ft(t))return"Exotic";if(!t||t==="-"||t==="missing")return"NotInstalled";if(!_(t)||!_(e))return"Exotic";try{switch(ut(t,e)){case"major":return"Major";case"minor":return"Minor";case"patch":return"Patch";default:return"Patch"}}catch{return"Exotic"}}function L(t,e=null){return t.map(r=>{let o=gt(r.current,r.latest),s=e!==null&&r.age!==null&&r.age>e;return{...r,risk:o,isStale:s}})}import{format as z}from"date-fns";function ht(t){if(t===null)return"Unknown";if(t<30)return`${t}d`;if(t<365)return`${Math.floor(t/30)}m`;let e=Math.floor(t/365),r=Math.floor(t%365/30);return r>0?`${e}y ${r}m`:`${e}y`}function M(t,e=new Date){let r=z(e,"yyyy-MM-dd"),o=z(e,"yyyy-MM-dd HH:mm:ss");if(t.length===0)return`# Dependency Report (${r})
|
|
3
|
+
|
|
4
|
+
Generated at: ${o}
|
|
5
|
+
|
|
6
|
+
\u2705 All dependencies are up to date
|
|
7
|
+
`;let s=[...t].sort((i,a)=>{let p={Major:0,Minor:1,Patch:2,Exotic:3,NotInstalled:4},d=(p[i.risk]||99)-(p[a.risk]||99);return d!==0?d:i.age===null&&a.age===null?0:i.age===null?1:a.age===null?-1:a.age-i.age}),c=`# Dependency Report (${r})
|
|
8
|
+
|
|
9
|
+
Generated at: ${o}
|
|
10
|
+
|
|
11
|
+
## Outdated Packages (${t.length})
|
|
12
|
+
|
|
13
|
+
| Package | Current | Latest | Risk | Age | Stale? | Notes |
|
|
14
|
+
|---------|---------|--------|------|-----|--------|-------|
|
|
15
|
+
`;for(let i of s){let a=ht(i.age),p=i.risk,d=i.isStale?"Yes":"No",w=i.note||"";c+=`| ${i.name} | ${i.current} | ${i.latest} | ${p} | ${a} | ${d} | ${w} |
|
|
16
|
+
`}return c}import{format as U}from"date-fns";function b(t){let e={"&":"&","<":"<",">":">",'"':""","'":"'"};return t.replace(/[&<>"']/g,r=>e[r])}function yt(t){if(t===null)return"Unknown";if(t<30)return`${t}d`;if(t<365)return`${Math.floor(t/30)}m`;let e=Math.floor(t/365),r=Math.floor(t%365/30);return r>0?`${e}y ${r}m`:`${e}y`}function wt(t){switch(t){case"Major":return"#dc2626";case"Minor":return"#ea580c";case"Patch":return"#ca8a04";case"Exotic":return"#6b7280";case"NotInstalled":return"#9ca3af";default:return"#6b7280"}}function O(t,e=new Date){let r=U(e,"yyyy-MM-dd"),o=U(e,"yyyy-MM-dd HH:mm:ss"),s=[...t].sort((a,p)=>{let d={Major:0,Minor:1,Patch:2,Exotic:3,NotInstalled:4},w=(d[a.risk]||99)-(d[p.risk]||99);return w!==0?w:a.age===null&&p.age===null?0:a.age===null?1:p.age===null?-1:p.age-a.age}),c="";for(let a of s){let p=yt(a.age),d=wt(a.risk),w=a.isStale?"Yes":"No",C=a.isStale?"stale-yes":"stale-no",$=b(a.note||"");c+=`
|
|
17
|
+
<tr>
|
|
18
|
+
<td class="package-name">${b(a.name)}</td>
|
|
19
|
+
<td>${b(a.current)}</td>
|
|
20
|
+
<td>${b(a.latest)}</td>
|
|
21
|
+
<td><span class="risk-badge" style="background-color: ${d}">${b(a.risk)}</span></td>
|
|
22
|
+
<td>${b(p)}</td>
|
|
23
|
+
<td class="${C}">${w}</td>
|
|
24
|
+
<td class="notes">${$}</td>
|
|
25
|
+
</tr>`}let i=t.length===0?`
|
|
26
|
+
<div class="empty-state">
|
|
27
|
+
<h2>\u2705 All dependencies are up to date</h2>
|
|
28
|
+
</div>
|
|
29
|
+
`:"";return`<!DOCTYPE html>
|
|
30
|
+
<html lang="en">
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="UTF-8">
|
|
33
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
34
|
+
<title>Dependency Report (${r})</title>
|
|
35
|
+
<style>
|
|
36
|
+
* {
|
|
37
|
+
margin: 0;
|
|
38
|
+
padding: 0;
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
}
|
|
41
|
+
body {
|
|
42
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
43
|
+
line-height: 1.6;
|
|
44
|
+
color: #1f2937;
|
|
45
|
+
background-color: #f9fafb;
|
|
46
|
+
padding: 2rem;
|
|
47
|
+
}
|
|
48
|
+
.container {
|
|
49
|
+
max-width: 1400px;
|
|
50
|
+
margin: 0 auto;
|
|
51
|
+
background: white;
|
|
52
|
+
border-radius: 8px;
|
|
53
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
54
|
+
padding: 2rem;
|
|
55
|
+
}
|
|
56
|
+
h1 {
|
|
57
|
+
color: #111827;
|
|
58
|
+
margin-bottom: 0.5rem;
|
|
59
|
+
font-size: 2rem;
|
|
60
|
+
}
|
|
61
|
+
.timestamp {
|
|
62
|
+
color: #6b7280;
|
|
63
|
+
font-size: 0.875rem;
|
|
64
|
+
margin-bottom: 2rem;
|
|
65
|
+
}
|
|
66
|
+
.empty-state {
|
|
67
|
+
text-align: center;
|
|
68
|
+
padding: 4rem 2rem;
|
|
69
|
+
color: #059669;
|
|
70
|
+
}
|
|
71
|
+
.empty-state h2 {
|
|
72
|
+
font-size: 1.5rem;
|
|
73
|
+
font-weight: 500;
|
|
74
|
+
}
|
|
75
|
+
table {
|
|
76
|
+
width: 100%;
|
|
77
|
+
border-collapse: collapse;
|
|
78
|
+
margin-top: 1rem;
|
|
79
|
+
}
|
|
80
|
+
thead {
|
|
81
|
+
background-color: #f3f4f6;
|
|
82
|
+
}
|
|
83
|
+
th {
|
|
84
|
+
text-align: left;
|
|
85
|
+
padding: 0.75rem 1rem;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
color: #374151;
|
|
88
|
+
font-size: 0.875rem;
|
|
89
|
+
text-transform: uppercase;
|
|
90
|
+
letter-spacing: 0.05em;
|
|
91
|
+
border-bottom: 2px solid #e5e7eb;
|
|
92
|
+
}
|
|
93
|
+
td {
|
|
94
|
+
padding: 0.75rem 1rem;
|
|
95
|
+
border-bottom: 1px solid #e5e7eb;
|
|
96
|
+
}
|
|
97
|
+
tbody tr:hover {
|
|
98
|
+
background-color: #f9fafb;
|
|
99
|
+
}
|
|
100
|
+
.package-name {
|
|
101
|
+
font-weight: 500;
|
|
102
|
+
color: #111827;
|
|
103
|
+
}
|
|
104
|
+
.risk-badge {
|
|
105
|
+
display: inline-block;
|
|
106
|
+
padding: 0.25rem 0.75rem;
|
|
107
|
+
border-radius: 4px;
|
|
108
|
+
color: white;
|
|
109
|
+
font-size: 0.75rem;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
text-transform: uppercase;
|
|
112
|
+
letter-spacing: 0.05em;
|
|
113
|
+
}
|
|
114
|
+
.stale-yes {
|
|
115
|
+
color: #dc2626;
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
}
|
|
118
|
+
.stale-no {
|
|
119
|
+
color: #059669;
|
|
120
|
+
}
|
|
121
|
+
.notes {
|
|
122
|
+
color: #6b7280;
|
|
123
|
+
font-style: italic;
|
|
124
|
+
}
|
|
125
|
+
@media (max-width: 768px) {
|
|
126
|
+
body {
|
|
127
|
+
padding: 1rem;
|
|
128
|
+
}
|
|
129
|
+
.container {
|
|
130
|
+
padding: 1rem;
|
|
131
|
+
}
|
|
132
|
+
table {
|
|
133
|
+
font-size: 0.875rem;
|
|
134
|
+
}
|
|
135
|
+
th, td {
|
|
136
|
+
padding: 0.5rem;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
140
|
+
</head>
|
|
141
|
+
<body>
|
|
142
|
+
<div class="container">
|
|
143
|
+
<h1>Dependency Report (${r})</h1>
|
|
144
|
+
<div class="timestamp">Generated at: ${o}</div>
|
|
145
|
+
${i||`
|
|
146
|
+
<h2 style="margin-top: 1.5rem; margin-bottom: 1rem; font-size: 1.25rem; color: #374151;">
|
|
147
|
+
Outdated Packages (${t.length})
|
|
148
|
+
</h2>
|
|
149
|
+
<table>
|
|
150
|
+
<thead>
|
|
151
|
+
<tr>
|
|
152
|
+
<th>Package</th>
|
|
153
|
+
<th>Current</th>
|
|
154
|
+
<th>Latest</th>
|
|
155
|
+
<th>Risk</th>
|
|
156
|
+
<th>Age</th>
|
|
157
|
+
<th>Stale?</th>
|
|
158
|
+
<th>Notes</th>
|
|
159
|
+
</tr>
|
|
160
|
+
</thead>
|
|
161
|
+
<tbody>
|
|
162
|
+
${c}
|
|
163
|
+
</tbody>
|
|
164
|
+
</table>
|
|
165
|
+
`}
|
|
166
|
+
</div>
|
|
167
|
+
</body>
|
|
168
|
+
</html>`}import{existsSync as J,access as kt,constants as bt,mkdirSync as $t,writeFileSync as xt,unlinkSync as Pt}from"fs";import{join as H}from"path";import{promisify as St}from"util";var jt=St(kt);async function G(t=process.cwd()){let e=H(t,"node_modules");if(!J(e))throw new Error('node_modules directory not found. Please run "npm install", "pnpm install", or "bun install" first.')}async function W(t){if(!J(t))try{$t(t,{recursive:!0})}catch(r){throw new Error(`Cannot create directory ${t}: ${r instanceof Error?r.message:String(r)}`)}try{await jt(t,bt.W_OK)}catch{throw new Error(`No write permission for directory: ${t}`)}let e=H(t,`.write-test-${Date.now()}.tmp`);try{xt(e,"ok"),Pt(e)}catch(r){throw new Error(`Cannot write to directory ${t}: ${r instanceof Error?r.message:String(r)}`)}}function j(t,e){try{return JSON.parse(t)}catch(r){throw r instanceof SyntaxError?new Error(`Invalid JSON in ${e}: ${r.message}`):r}}var m={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",cyan:"\x1B[36m"},Y=["-","\\","|","/"],E=null,q="",n={info:t=>{console.log(`${m.cyan}[INFO]${m.reset} ${t}`)},success:t=>{console.log(`${m.green}${m.bright}[OK]${m.reset} ${m.green}${t}${m.reset}`)},warn:t=>{console.warn(`${m.yellow}[WARN]${m.reset} ${t}`)},error:t=>{console.error(`${m.red}${m.bright}[ERR]${m.reset} ${m.red}${t}${m.reset}`)},progress:(t,e,r)=>{let o=Math.round(t/e*100);process.stdout.write(`\r${m.cyan}${r}${m.reset} ${m.dim}(${t}/${e}, ${o}%)${m.reset}`),t===e&&process.stdout.write(`
|
|
169
|
+
`)},startSpinner:t=>{q=t;let e=0;E=setInterval(()=>{process.stdout.write(`\r${m.cyan}${Y[e]}${m.reset} ${q}`),e=(e+1)%Y.length},100)},stopSpinner:()=>{E&&(clearInterval(E),E=null,process.stdout.write("\r"+" ".repeat(process.stdout.columns||80)+"\r"))}};import{format as st}from"date-fns";import{existsSync as Rt,readFileSync as Ct}from"fs";import{join as Mt}from"path";import{z as g}from"zod";var l={staleThreshold:"18 months",ignorePatterns:[],formats:{markdown:!0,html:!0},concurrency:5,failConditions:{stale:!1,major:!1},reportEmptyState:!0},Et=g.object({staleThreshold:g.string().default(l.staleThreshold),ignorePatterns:g.array(g.string()).default(l.ignorePatterns),formats:g.object({markdown:g.boolean().default(l.formats.markdown),html:g.boolean().default(l.formats.html)}).default(l.formats),concurrency:g.number().int().positive().default(l.concurrency),failConditions:g.object({stale:g.boolean().default(l.failConditions.stale),major:g.boolean().default(l.failConditions.major)}).default(l.failConditions),reportEmptyState:g.boolean().default(l.reportEmptyState)}).passthrough();function B(t){try{return Et.parse(t)}catch(e){if(e instanceof g.ZodError){let r=e.errors.map(o=>`${o.path.join(".")}: ${o.message}`).join(", ");throw new Error(`Invalid config: ${r}`)}throw e}}function K(t=process.cwd()){let e=Mt(t,".dep-report","config.json");if(!Rt(e))return n.info("No config.json found, using defaults"),l;try{let r=Ct(e,"utf-8"),o=j(r,e),s={staleThreshold:o.staleThreshold??l.staleThreshold,ignorePatterns:o.ignorePatterns??l.ignorePatterns,formats:{markdown:o.formats?.markdown??l.formats.markdown,html:o.formats?.html??l.formats.html},concurrency:o.concurrency??l.concurrency,failConditions:{stale:o.failConditions?.stale??l.failConditions.stale,major:o.failConditions?.major??l.failConditions.major},reportEmptyState:o.reportEmptyState??l.reportEmptyState},c=B(s);return n.info("Loaded config from .dep-report/config.json"),c}catch(r){return n.warn(`Failed to load config: ${r instanceof Error?r.message:String(r)}`),n.warn("Using default configuration"),l}}function Ot(t){let r=t.trim().toLowerCase().match(/^(\d+)\s+(year|years|month|months|day|days|week|weeks)$/);if(!r)throw new Error(`Invalid duration format: "${t}". Expected format: "18 months", "2 years", "90 days", etc.`);let o=parseInt(r[1],10),s=r[2];switch(s){case"day":case"days":return o*24*60*60*1e3;case"week":case"weeks":return o*7*24*60*60*1e3;case"month":case"months":return o*30*24*60*60*1e3;case"year":case"years":return o*365*24*60*60*1e3;default:throw new Error(`Unsupported time unit: ${s}`)}}function V(t){let e=Ot(t);return Math.floor(e/(1e3*60*60*24))}import{existsSync as vt,readFileSync as Dt}from"fs";import{join as Nt}from"path";function Z(t=process.cwd()){let e=Nt(t,".dep-report","notes.json");if(!vt(e))return{};try{let r=Dt(e,"utf-8"),o=j(r,e);if(typeof o!="object"||o===null||Array.isArray(o))return n.warn("Invalid notes.json format, expected object"),{};for(let[s,c]of Object.entries(o))typeof c!="string"&&(n.warn(`Invalid note for ${s}, expected string`),delete o[s]);return n.info(`Loaded ${Object.keys(o).length} notes from notes.json`),o}catch(r){return n.warn(`Failed to load notes: ${r instanceof Error?r.message:String(r)}`),{}}}function Q(t,e){return t.map(r=>{let o=e[r.name];return o?{...r,note:o}:r})}import{minimatch as Tt}from"minimatch";function X(t,e){return e.length===0?t:t.filter(r=>{for(let o of e)if(Tt(r.name,o))return!1;return!0})}import{existsSync as P,mkdirSync as tt,writeFileSync as v}from"fs";import{join as S}from"path";async function et(t=process.cwd(),e=!1){let r=S(t,".dep-report"),o=S(r,"config.json"),s=S(r,"notes.json"),c=S(r,"reports"),i=S(r,".gitignore");if(P(r)?n.info(".dep-report/ directory already exists"):(tt(r,{recursive:!0}),n.success("Created .dep-report/ directory")),P(c)||(tt(c,{recursive:!0}),n.success("Created .dep-report/reports/ directory")),!P(o)||e){let a=JSON.stringify(l,null,2);v(o,a,"utf-8"),n.success("Created .dep-report/config.json")}else n.info("config.json already exists, skipping");if(P(s))n.info("notes.json already exists, skipping");else{let a=JSON.stringify({},null,2);v(s,a,"utf-8"),n.success("Created .dep-report/notes.json")}P(i)?n.info(".gitignore already exists, skipping"):(v(i,`.cache.json
|
|
170
|
+
`,"utf-8"),n.success("Created .dep-report/.gitignore")),n.success("Initialization complete!"),n.info("You can now customize .dep-report/config.json and add notes to .dep-report/notes.json")}var It="https://registry.npmjs.org";async function rt(t=It,e=5e3){let r=new AbortController,o=setTimeout(()=>r.abort(),e);try{let s=`${t}/lodash`;return(await fetch(s,{headers:{Accept:"application/json"},signal:r.signal})).ok}catch{return!1}finally{clearTimeout(o)}}var R=new At,_t=new URL("../package.json",import.meta.url),Lt=JSON.parse(Ft(_t,"utf-8")),zt=Lt.version??"0.0.0";R.name("dep-report").description("Zero-config CLI tool that generates version-controlled snapshots of dependency risk").version(zt);R.command("init").description("Scaffold .dep-report/ directory structure").option("--include-config","Force overwrite config.json even if it exists").action(async t=>{try{await et(process.cwd(),t.includeConfig),process.exit(0)}catch(e){n.error(e instanceof Error?e.message:String(e)),process.exit(1)}});R.action(async()=>{try{let t=process.cwd(),e=K(t);n.info("Checking prerequisites..."),await G(t);let r=y(t,".dep-report");await W(r),n.info("Detecting package manager...");let o=D(t);o||(n.error("No package manager detected. Please ensure you have package-lock.json, pnpm-lock.yaml, bun.lock, or bun.lockb in your project."),process.exit(1)),n.success(`Detected: ${o.manager}`),n.info("Scanning for outdated packages...");let s=await T(o.manager,t);if(Object.keys(s).length===0){if(n.success("No outdated packages found!"),e.reportEmptyState){let u=y(t,".dep-report","reports");ot(u)||nt(u,{recursive:!0});let f=st(new Date,"yyyy-MM-dd");if(e.formats.markdown){let h=M([]);k(y(u,`${f}_outdated.md`),h),k(y(u,"latest.md"),h),n.success(`Report generated: .dep-report/reports/${f}_outdated.md`)}if(e.formats.html){let h=O([]);k(y(u,`${f}_outdated.html`),h),k(y(u,"latest.html"),h),n.success(`HTML report generated: .dep-report/reports/${f}_outdated.html`)}}process.exit(0)}n.info(`Found ${Object.keys(s).length} outdated packages`);let c=I(s);n.startSpinner("Checking registry connectivity...");let i=await rt();n.stopSpinner(),i||(n.error("Unable to reach the npm registry."),n.info("If you have a cache, try running with --refresh."),process.exit(1)),n.success("Registry connectivity confirmed"),n.startSpinner(`Enriching ${c.length} packages with registry metadata...`);let a=await F(c,e.concurrency);n.stopSpinner(),n.success("Enrichment complete");let p=V(e.staleThreshold),d=L(a,p),w=d.length;d=X(d,e.ignorePatterns),w>d.length&&n.info(`Filtered out ${w-d.length} packages based on ignorePatterns`);let C=Z(t);if(d=Q(d,C),e.formats.markdown||e.formats.html){n.info("Generating reports...");let u=y(t,".dep-report","reports");ot(u)||nt(u,{recursive:!0});let f=st(new Date,"yyyy-MM-dd");if(e.formats.markdown){let h=M(d);k(y(u,`${f}_outdated.md`),h),k(y(u,"latest.md"),h),n.success(`Report generated: .dep-report/reports/${f}_outdated.md`),n.success("Latest report: .dep-report/reports/latest.md")}if(e.formats.html){let h=O(d);k(y(u,`${f}_outdated.html`),h),k(y(u,"latest.html"),h),n.success(`HTML report generated: .dep-report/reports/${f}_outdated.html`),n.success("Latest HTML report: .dep-report/reports/latest.html")}}let $=!1;e.failConditions.stale&&d.some(f=>f.isStale)&&(n.error("Found stale packages (--fail-if-stale)"),$=!0),e.failConditions.major&&d.some(f=>f.risk==="Major")&&(n.error("Found major version updates (--fail-if-major)"),$=!0),process.exit($?1:0)}catch(t){n.error(t instanceof Error?t.message:String(t)),process.exit(1)}});R.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dep-report",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Zero-config CLI tool that generates version-controlled snapshots of dependency risk",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dep-report": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup src/cli.ts --format esm --minify --out-dir dist",
|
|
12
|
+
"dev": "bun run src/cli.ts",
|
|
13
|
+
"test": "bun test",
|
|
14
|
+
"test:sandbox:install": "bun run sandbox/scripts/install.ts",
|
|
15
|
+
"test:sandbox": "bun run sandbox/scripts/run-all.ts",
|
|
16
|
+
"test:sandbox:validate": "bun run sandbox/scripts/validate.ts",
|
|
17
|
+
"typecheck": "tsc --noEmit"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"dependencies",
|
|
21
|
+
"audit",
|
|
22
|
+
"outdated",
|
|
23
|
+
"npm",
|
|
24
|
+
"pnpm",
|
|
25
|
+
"bun"
|
|
26
|
+
],
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^12.1.0",
|
|
37
|
+
"date-fns": "^4.1.0",
|
|
38
|
+
"escape-html": "^1.0.3",
|
|
39
|
+
"minimatch": "^10.0.1",
|
|
40
|
+
"semver": "^7.6.3",
|
|
41
|
+
"zod": "^3.23.8"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/escape-html": "^1.0.2",
|
|
45
|
+
"@types/minimatch": "^5.1.2",
|
|
46
|
+
"@types/node": "^22.10.2",
|
|
47
|
+
"@types/semver": "^7.5.8",
|
|
48
|
+
"bun-types": "^1.3.6",
|
|
49
|
+
"tsup": "^8.3.5",
|
|
50
|
+
"typescript": "^5.7.2"
|
|
51
|
+
}
|
|
52
|
+
}
|