@yob/depcollector 0.1.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/README.md +160 -0
- package/bin/depcollector.mjs +2 -0
- package/dist/git.d.ts +4 -0
- package/dist/git.js +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +100 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +81 -0
- package/dist/summarize.d.ts +1 -0
- package/dist/summarize.js +47 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# depcollector
|
|
2
|
+
|
|
3
|
+
A CLI tool that snapshots the state of your npm dependencies. It reads your
|
|
4
|
+
`package.json` and `package-lock.json`, queries the npm registry, and outputs
|
|
5
|
+
a JSON report containing each dependency's locked version, its release date,
|
|
6
|
+
and the latest available version with its release date. It also captures the
|
|
7
|
+
current git SHA and commit timestamp.
|
|
8
|
+
|
|
9
|
+
## Why?
|
|
10
|
+
|
|
11
|
+
Keeping dependencies up to date is important, but it's hard to track *how*
|
|
12
|
+
out of date they are across projects over time. depcollector produces a
|
|
13
|
+
structured snapshot you can store (e.g. in S3) and trend over time to answer
|
|
14
|
+
questions like:
|
|
15
|
+
|
|
16
|
+
- How old are my locked dependency versions?
|
|
17
|
+
- How far behind latest is each dependency?
|
|
18
|
+
- Are things getting better or worse over time?
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Node.js >= 20
|
|
23
|
+
- A `package.json` and `package-lock.json` in the target directory
|
|
24
|
+
- A git repository (for SHA and commit timestamp)
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g depcollector
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or run directly with npx:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx depcollector
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Run in any directory containing a `package.json` and `package-lock.json`:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
depcollector
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This prints a JSON report to stdout:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"ecosystem": "npm",
|
|
51
|
+
"collectedAt": "2026-02-08T11:24:42.590Z",
|
|
52
|
+
"gitSha": "c75e008...",
|
|
53
|
+
"gitTimestamp": "2026-02-08T22:24:33+11:00",
|
|
54
|
+
"dependencies": [
|
|
55
|
+
{
|
|
56
|
+
"name": "typescript",
|
|
57
|
+
"direct": true,
|
|
58
|
+
"currentVersion": "5.9.3",
|
|
59
|
+
"currentVersionDate": "2025-09-30T21:19:38.784Z",
|
|
60
|
+
"latestVersion": "5.9.3",
|
|
61
|
+
"latestVersionDate": "2025-09-30T21:19:38.784Z"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `--at-commit`
|
|
68
|
+
|
|
69
|
+
By default, "latest version" means the latest version available right now. If
|
|
70
|
+
you want to know what the latest version was *at the time of the commit*
|
|
71
|
+
instead, use the `--at-commit` flag:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
depcollector --at-commit
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This caps the "latest version" to the most recent release that existed at the
|
|
78
|
+
git commit timestamp. This is useful for historical analysis -- for example,
|
|
79
|
+
running depcollector against older commits to understand how out of date
|
|
80
|
+
dependencies were at that point in time, without the answer being skewed by
|
|
81
|
+
versions released after the commit.
|
|
82
|
+
|
|
83
|
+
### `--transitive`
|
|
84
|
+
|
|
85
|
+
By default, only direct dependencies (those listed in `dependencies` and
|
|
86
|
+
`devDependencies` in `package.json`) are included. Use `--transitive` to
|
|
87
|
+
include all transitive dependencies from the lockfile as well:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
depcollector --transitive
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Each dependency in the output includes a `"direct"` field indicating whether
|
|
94
|
+
it is a direct dependency (`true`) or a transitive one (`false`). If the same
|
|
95
|
+
package appears at multiple versions in the dependency tree, each distinct
|
|
96
|
+
version is reported separately.
|
|
97
|
+
|
|
98
|
+
### `--project-name <name>`
|
|
99
|
+
|
|
100
|
+
Include a project name in the output. Useful for distinguishing reports when
|
|
101
|
+
collecting dependency data across multiple projects:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
depcollector --project-name myapp
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This adds a `"projectName"` key to the top level of the JSON output.
|
|
108
|
+
|
|
109
|
+
### `--manifest-path <path>`
|
|
110
|
+
|
|
111
|
+
Include the in-repo path to the manifest in the output. This is intended for
|
|
112
|
+
monorepos where dependency data might be collected for multiple packages:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
depcollector --manifest-path packages/api/package.json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This adds a `"manifestPath"` key to the top level of the JSON output. The
|
|
119
|
+
value is not validated -- it can be any string.
|
|
120
|
+
|
|
121
|
+
### Saving output
|
|
122
|
+
|
|
123
|
+
The output is plain JSON on stdout, so you can pipe it wherever you like:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Save to a file
|
|
127
|
+
depcollector > deps-snapshot.json
|
|
128
|
+
|
|
129
|
+
# Upload to S3
|
|
130
|
+
depcollector | aws s3 cp - s3://my-bucket/deps/$(date +%Y-%m-%d).json
|
|
131
|
+
|
|
132
|
+
# Pretty-print with jq
|
|
133
|
+
depcollector | jq .
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Output format
|
|
137
|
+
|
|
138
|
+
| Field | Description |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `ecosystem` | Always `"npm"` |
|
|
141
|
+
| `projectName` | Project name (present when `--project-name` is used) |
|
|
142
|
+
| `manifestPath` | In-repo manifest path (present when `--manifest-path` is used) |
|
|
143
|
+
| `collectedAt` | ISO 8601 timestamp of when the report was generated |
|
|
144
|
+
| `gitSha` | The current HEAD commit SHA |
|
|
145
|
+
| `gitTimestamp` | The commit timestamp of HEAD |
|
|
146
|
+
| `dependencies[]` | Array of dependency info objects |
|
|
147
|
+
| `dependencies[].name` | Package name |
|
|
148
|
+
| `dependencies[].direct` | `true` for direct dependencies, `false` for transitive |
|
|
149
|
+
| `dependencies[].currentVersion` | Version locked in package-lock.json |
|
|
150
|
+
| `dependencies[].currentVersionDate` | Release date of the locked version |
|
|
151
|
+
| `dependencies[].latestVersion` | Latest available version (or latest as of commit with `--at-commit`) |
|
|
152
|
+
| `dependencies[].latestVersionDate` | Release date of the latest version |
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
157
|
+
|
|
158
|
+
## Author
|
|
159
|
+
|
|
160
|
+
[James Healy](https://yob.id.au)
|
package/dist/git.d.ts
ADDED
package/dist/git.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
export function getGitInfo() {
|
|
3
|
+
const sha = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
|
|
4
|
+
const timestamp = execSync('git log -1 --format=%cI', {
|
|
5
|
+
encoding: 'utf-8',
|
|
6
|
+
}).trim();
|
|
7
|
+
return { sha, timestamp };
|
|
8
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { getPackageInfo } from './registry.js';
|
|
5
|
+
import { getGitInfo } from './git.js';
|
|
6
|
+
const CONCURRENCY = 5;
|
|
7
|
+
async function loadJson(filePath) {
|
|
8
|
+
const content = await readFile(filePath, 'utf-8');
|
|
9
|
+
return JSON.parse(content);
|
|
10
|
+
}
|
|
11
|
+
function getLockedVersions(packageJson, lockfile, transitive) {
|
|
12
|
+
if (!lockfile.lockfileVersion || lockfile.lockfileVersion < 2) {
|
|
13
|
+
throw new Error(`package-lock.json lockfileVersion ${lockfile.lockfileVersion ?? 1} is not supported. ` +
|
|
14
|
+
'Please regenerate your lockfile with npm 7+ (lockfileVersion 2 or 3).');
|
|
15
|
+
}
|
|
16
|
+
if (!lockfile.packages) {
|
|
17
|
+
throw new Error("package-lock.json has no 'packages' field.");
|
|
18
|
+
}
|
|
19
|
+
if (transitive) {
|
|
20
|
+
return getAllLockedVersions(lockfile.packages);
|
|
21
|
+
}
|
|
22
|
+
const allDeps = {
|
|
23
|
+
...packageJson.dependencies,
|
|
24
|
+
...packageJson.devDependencies,
|
|
25
|
+
};
|
|
26
|
+
const result = [];
|
|
27
|
+
for (const name of Object.keys(allDeps)) {
|
|
28
|
+
const entry = lockfile.packages[`node_modules/${name}`];
|
|
29
|
+
if (entry?.version) {
|
|
30
|
+
result.push({ name, version: entry.version });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function getAllLockedVersions(packages) {
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const result = [];
|
|
38
|
+
for (const [path, entry] of Object.entries(packages)) {
|
|
39
|
+
if (!path.startsWith('node_modules/') || !entry.version)
|
|
40
|
+
continue;
|
|
41
|
+
// Extract package name from the last node_modules/ segment
|
|
42
|
+
// e.g. "node_modules/a/node_modules/@scope/b" -> "@scope/b"
|
|
43
|
+
const name = path.substring(path.lastIndexOf('node_modules/') + 13);
|
|
44
|
+
// Deduplicate by name+version to avoid redundant registry calls
|
|
45
|
+
const key = `${name}@${entry.version}`;
|
|
46
|
+
if (!seen.has(key)) {
|
|
47
|
+
seen.add(key);
|
|
48
|
+
result.push({ name, version: entry.version });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
async function processInBatches(locked, directNames, concurrency, cutoff) {
|
|
54
|
+
const results = [];
|
|
55
|
+
for (let i = 0; i < locked.length; i += concurrency) {
|
|
56
|
+
const batch = locked.slice(i, i + concurrency);
|
|
57
|
+
const batchResults = await Promise.all(batch.map((dep) => getPackageInfo(dep.name, dep.version, directNames.has(dep.name), cutoff)));
|
|
58
|
+
results.push(...batchResults);
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
program
|
|
63
|
+
.name('depcollector')
|
|
64
|
+
.description('Collect dependency version and age information from package.json and package-lock.json')
|
|
65
|
+
.option('--at-commit', 'Cap latest version to what was available at the time of the commit')
|
|
66
|
+
.option('--transitive', 'Include transitive dependencies')
|
|
67
|
+
.option('--project-name <name>', 'Include a project name in the output')
|
|
68
|
+
.option('--manifest-path <path>', 'Include the in-repo path to the manifest in the output')
|
|
69
|
+
.parse();
|
|
70
|
+
async function main() {
|
|
71
|
+
const opts = program.opts();
|
|
72
|
+
const cwd = process.cwd();
|
|
73
|
+
const [packageJson, lockfile] = await Promise.all([
|
|
74
|
+
loadJson(join(cwd, 'package.json')),
|
|
75
|
+
loadJson(join(cwd, 'package-lock.json')),
|
|
76
|
+
]);
|
|
77
|
+
const locked = getLockedVersions(packageJson, lockfile, opts.transitive ?? false);
|
|
78
|
+
locked.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
79
|
+
const directNames = new Set(Object.keys({
|
|
80
|
+
...packageJson.dependencies,
|
|
81
|
+
...packageJson.devDependencies,
|
|
82
|
+
}));
|
|
83
|
+
const gitInfo = getGitInfo();
|
|
84
|
+
const cutoff = opts.atCommit ? new Date(gitInfo.timestamp) : undefined;
|
|
85
|
+
const dependencies = await processInBatches(locked, directNames, CONCURRENCY, cutoff);
|
|
86
|
+
const result = {
|
|
87
|
+
ecosystem: 'npm',
|
|
88
|
+
...(opts.projectName && { projectName: opts.projectName }),
|
|
89
|
+
...(opts.manifestPath && { manifestPath: opts.manifestPath }),
|
|
90
|
+
collectedAt: new Date().toISOString(),
|
|
91
|
+
gitSha: gitInfo.sha,
|
|
92
|
+
gitTimestamp: gitInfo.timestamp,
|
|
93
|
+
dependencies,
|
|
94
|
+
};
|
|
95
|
+
console.log(JSON.stringify(result, null, 2));
|
|
96
|
+
}
|
|
97
|
+
main().catch((err) => {
|
|
98
|
+
console.error(err);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
});
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
function compareVersions(a, b) {
|
|
2
|
+
const pa = a.split('.').map(Number);
|
|
3
|
+
const pb = b.split('.').map(Number);
|
|
4
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
5
|
+
const na = pa[i] ?? 0;
|
|
6
|
+
const nb = pb[i] ?? 0;
|
|
7
|
+
if (na !== nb)
|
|
8
|
+
return na - nb;
|
|
9
|
+
}
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
function findLatestBefore(time, versions, cutoff) {
|
|
13
|
+
let best;
|
|
14
|
+
for (const [version, dateStr] of Object.entries(time)) {
|
|
15
|
+
if (version === 'created' || version === 'modified')
|
|
16
|
+
continue;
|
|
17
|
+
if (version.includes('-'))
|
|
18
|
+
continue;
|
|
19
|
+
if (versions[version]?.deprecated)
|
|
20
|
+
continue;
|
|
21
|
+
const date = new Date(dateStr);
|
|
22
|
+
if (date <= cutoff && (!best || compareVersions(version, best) > 0)) {
|
|
23
|
+
best = version;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!best)
|
|
27
|
+
return undefined;
|
|
28
|
+
return { version: best, date: time[best] };
|
|
29
|
+
}
|
|
30
|
+
const registryCache = new Map();
|
|
31
|
+
async function fetchRegistryData(name) {
|
|
32
|
+
const cached = registryCache.get(name);
|
|
33
|
+
if (cached)
|
|
34
|
+
return cached;
|
|
35
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
|
|
36
|
+
const res = await fetch(url, {
|
|
37
|
+
headers: { Accept: 'application/json' },
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error(`Failed to fetch registry info for ${name}: ${res.status} ${res.statusText}`);
|
|
41
|
+
}
|
|
42
|
+
const data = (await res.json());
|
|
43
|
+
registryCache.set(name, data);
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
export async function getPackageInfo(name, currentVersion, direct, cutoff) {
|
|
47
|
+
let data;
|
|
48
|
+
try {
|
|
49
|
+
data = await fetchRegistryData(name);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.error(`Warning: ${err instanceof Error ? err.message : String(err)}`);
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
direct,
|
|
56
|
+
currentVersion,
|
|
57
|
+
currentVersionDate: '',
|
|
58
|
+
latestVersion: '',
|
|
59
|
+
latestVersionDate: '',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
let latestVersion;
|
|
63
|
+
let latestVersionDate;
|
|
64
|
+
if (cutoff) {
|
|
65
|
+
const found = findLatestBefore(data.time, data.versions, cutoff);
|
|
66
|
+
latestVersion = found?.version ?? data['dist-tags'].latest;
|
|
67
|
+
latestVersionDate = found?.date ?? data.time[latestVersion] ?? 'unknown';
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
latestVersion = data['dist-tags'].latest;
|
|
71
|
+
latestVersionDate = data.time[latestVersion] ?? 'unknown';
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
name,
|
|
75
|
+
direct,
|
|
76
|
+
currentVersion,
|
|
77
|
+
currentVersionDate: data.time[currentVersion] ?? 'unknown',
|
|
78
|
+
latestVersion,
|
|
79
|
+
latestVersionDate,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
const MS_PER_YEAR = 365.25 * 24 * 60 * 60 * 1000;
|
|
5
|
+
function calculateLibyears(result) {
|
|
6
|
+
let total = 0;
|
|
7
|
+
for (const dep of result.dependencies) {
|
|
8
|
+
if (dep.currentVersionDate === "unknown" || dep.latestVersionDate === "unknown")
|
|
9
|
+
continue;
|
|
10
|
+
const current = new Date(dep.currentVersionDate).getTime();
|
|
11
|
+
const latest = new Date(dep.latestVersionDate).getTime();
|
|
12
|
+
const diff = latest - current;
|
|
13
|
+
if (diff > 0) {
|
|
14
|
+
total += diff / MS_PER_YEAR;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return total;
|
|
18
|
+
}
|
|
19
|
+
program
|
|
20
|
+
.name("depcollector-summarize")
|
|
21
|
+
.description("Summarize multiple depcollector JSON reports into a CSV")
|
|
22
|
+
.argument("<directory>", "Directory containing depcollector JSON files")
|
|
23
|
+
.parse();
|
|
24
|
+
async function main() {
|
|
25
|
+
const dir = program.args[0];
|
|
26
|
+
const files = (await readdir(dir)).filter((f) => f.endsWith(".json")).sort();
|
|
27
|
+
const rows = [];
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const content = await readFile(join(dir, file), "utf-8");
|
|
30
|
+
const result = JSON.parse(content);
|
|
31
|
+
const libyears = calculateLibyears(result);
|
|
32
|
+
rows.push({
|
|
33
|
+
gitSha: result.gitSha,
|
|
34
|
+
gitTimestamp: result.gitTimestamp,
|
|
35
|
+
libyears: libyears.toFixed(2),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
rows.sort((a, b) => a.gitTimestamp.localeCompare(b.gitTimestamp));
|
|
39
|
+
console.log("commit_sha,commit_timestamp,libyears");
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
console.log(`${row.gitSha},${row.gitTimestamp},${row.libyears}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
main().catch((err) => {
|
|
45
|
+
console.error(err);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface LockedDependency {
|
|
2
|
+
name: string;
|
|
3
|
+
version: string;
|
|
4
|
+
}
|
|
5
|
+
export interface DependencyInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
direct: boolean;
|
|
8
|
+
currentVersion: string;
|
|
9
|
+
currentVersionDate: string;
|
|
10
|
+
latestVersion: string;
|
|
11
|
+
latestVersionDate: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CollectionResult {
|
|
14
|
+
ecosystem: 'npm';
|
|
15
|
+
projectName?: string;
|
|
16
|
+
manifestPath?: string;
|
|
17
|
+
collectedAt: string;
|
|
18
|
+
gitSha: string;
|
|
19
|
+
gitTimestamp: string;
|
|
20
|
+
dependencies: DependencyInfo[];
|
|
21
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yob/depcollector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Collect dependency version and age information from package.json and package-lock.json",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"depcollector": "./bin/depcollector.mjs"
|
|
8
|
+
},
|
|
9
|
+
"author": "James Healy (https://yob.id.au)",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"formatting": "prettier --check .",
|
|
13
|
+
"formatting:fix": "prettier --write .",
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
15
|
+
"start": "tsx src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"bin"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"dependencies",
|
|
26
|
+
"versions",
|
|
27
|
+
"audit",
|
|
28
|
+
"npm"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22",
|
|
33
|
+
"prettier": "^3.8.1",
|
|
34
|
+
"tsx": "^4",
|
|
35
|
+
"typescript": "^5"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"commander": "^14.0.3"
|
|
39
|
+
}
|
|
40
|
+
}
|