Skip to content

Commit 4b2117b

Browse files
committed
fix: make dependency skill CI friendly
1 parent e57488e commit 4b2117b

5 files changed

Lines changed: 209 additions & 181 deletions

File tree

.agents/skills/linea-dependency-maintenance/SKILL.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ metadata:
77

88
# Dependency Maintenance
99

10+
<!-- markdownlint-disable -->
11+
<!-- vale off -->
12+
1013
Use this workflow to maximize safe dependency progress without changing the repository's package-manager contract or hiding remaining risk.
1114

1215
## Preflight
@@ -49,7 +52,7 @@ pnpm why <package> -r
4952
When registry-age gates matter, use the bundled helper as a reproducible first pass:
5053

5154
```bash
52-
node <skill-dir>/scripts/eligible-updates.mjs --manager auto --days 3
55+
node <skill-dir>/scripts/eligible-updates --manager auto --days 3
5356
```
5457

5558
Replace `<skill-dir>` with the directory containing this `SKILL.md`. Adjust `--days` or `--minutes` to match the repo policy.
@@ -117,4 +120,4 @@ Pause before contract deployments, public API breakage, package-manager migratio
117120

118121
- For npm/package-lock repositories, read `references/npm.md`.
119122
- For pnpm workspace/catalog/override repositories, read `references/pnpm.md`.
120-
- For reproducible release-age inventory, run `scripts/eligible-updates.mjs`.
123+
- For reproducible release-age inventory, run `scripts/eligible-updates`.

.agents/skills/linea-dependency-maintenance/references/npm.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# npm Dependency Maintenance
22

3+
<!-- markdownlint-disable -->
4+
<!-- vale off -->
5+
36
Use these notes only for npm repositories with `package-lock.json`, npm CI, or npm-based hosting/deploy detection.
47

58
## Rules

.agents/skills/linea-dependency-maintenance/references/pnpm.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# pnpm Dependency Maintenance
22

3+
<!-- markdownlint-disable -->
4+
<!-- vale off -->
5+
36
Use these notes for pnpm repositories with `pnpm-lock.yaml`, `pnpm-workspace.yaml`, catalogs, overrides, or pnpm-specific CI.
47

58
## Rules
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env node
2+
3+
const { execFileSync } = require("node:child_process");
4+
const { existsSync, readFileSync } = require("node:fs");
5+
const { join } = require("node:path");
6+
7+
const DAY_MS = 24 * 60 * 60 * 1000;
8+
const MINUTE_MS = 60 * 1000;
9+
10+
function parseArgs(argv) {
11+
const args = { manager: "auto", days: 3, minutes: null };
12+
for (let index = 2; index < argv.length; index += 1) {
13+
const arg = argv[index];
14+
const next = argv[index + 1];
15+
if (arg === "--manager" && next) {
16+
args.manager = next;
17+
index += 1;
18+
} else if (arg === "--days" && next) {
19+
args.days = Number(next);
20+
index += 1;
21+
} else if (arg === "--minutes" && next) {
22+
args.minutes = Number(next);
23+
index += 1;
24+
} else if (arg === "--help") {
25+
console.error("Usage: eligible-updates [--manager auto|npm|pnpm] [--days N | --minutes N]");
26+
process.exit(0);
27+
}
28+
}
29+
return args;
30+
}
31+
32+
function detectManager() {
33+
if (existsSync("pnpm-lock.yaml")) return "pnpm";
34+
if (existsSync("package-lock.json")) return "npm";
35+
if (existsSync("package.json")) {
36+
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
37+
if (String(pkg.packageManager ?? "").startsWith("pnpm@")) return "pnpm";
38+
if (String(pkg.packageManager ?? "").startsWith("npm@")) return "npm";
39+
}
40+
return "npm";
41+
}
42+
43+
function runOutdated(manager) {
44+
const command =
45+
manager === "pnpm" ? ["pnpm", ["outdated", "-r", "--format", "json"]] : ["npm", ["outdated", "--json"]];
46+
47+
try {
48+
return execFileSync(command[0], command[1], {
49+
encoding: "utf8",
50+
stdio: ["ignore", "pipe", "pipe"],
51+
});
52+
} catch (error) {
53+
if (error.stdout) return String(error.stdout);
54+
return "{}";
55+
}
56+
}
57+
58+
function parseVersion(version) {
59+
const match = String(version ?? "")
60+
.trim()
61+
.replace(/^v/, "")
62+
.match(/(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
63+
if (!match) return null;
64+
return {
65+
raw: `${match[1]}.${match[2]}.${match[3]}${match[4] ? `-${match[4]}` : ""}`,
66+
major: Number(match[1]),
67+
minor: Number(match[2]),
68+
patch: Number(match[3]),
69+
prerelease: match[4] ?? "",
70+
};
71+
}
72+
73+
function compareVersions(a, b) {
74+
const parsedA = parseVersion(a);
75+
const parsedB = parseVersion(b);
76+
if (!parsedA || !parsedB) return 0;
77+
for (const key of ["major", "minor", "patch"]) {
78+
if (parsedA[key] !== parsedB[key]) return parsedA[key] - parsedB[key];
79+
}
80+
if (parsedA.prerelease && !parsedB.prerelease) return -1;
81+
if (!parsedA.prerelease && parsedB.prerelease) return 1;
82+
return parsedA.prerelease.localeCompare(parsedB.prerelease);
83+
}
84+
85+
function normalizeOutdated(raw) {
86+
if (!raw.trim()) return [];
87+
const parsed = JSON.parse(raw);
88+
const rows = [];
89+
90+
if (Array.isArray(parsed)) {
91+
for (const item of parsed) rows.push(item);
92+
} else {
93+
for (const [name, details] of Object.entries(parsed)) rows.push({ name, ...details });
94+
}
95+
96+
return rows
97+
.map((row) => ({
98+
name: row.name ?? row.packageName ?? row.package,
99+
current: row.current,
100+
wanted: row.wanted,
101+
latest: row.latest,
102+
type: row.dependencyType ?? row.type,
103+
dependent: row.dependent ?? row.workspace ?? row.location,
104+
}))
105+
.filter((row) => row.name && parseVersion(row.current));
106+
}
107+
108+
async function fetchMetadata(name) {
109+
if (typeof fetch !== "function") {
110+
throw new Error("This helper requires Node.js with global fetch support.");
111+
}
112+
const encoded = encodeURIComponent(name).replace("%40", "@");
113+
const response = await fetch(`https://registry.npmjs.org/${encoded}`);
114+
if (!response.ok) {
115+
throw new Error(`Could not fetch npm metadata for ${name}: ${response.status}`);
116+
}
117+
return response.json();
118+
}
119+
120+
function classify(current, target) {
121+
const currentParsed = parseVersion(current);
122+
const targetParsed = parseVersion(target);
123+
if (!currentParsed || !targetParsed) return "unknown";
124+
if (targetParsed.major !== currentParsed.major) return "major";
125+
if (targetParsed.minor !== currentParsed.minor) return "minor";
126+
if (targetParsed.patch !== currentParsed.patch) return "patch";
127+
return "same";
128+
}
129+
130+
async function main() {
131+
const args = parseArgs(process.argv);
132+
const manager = args.manager === "auto" ? detectManager() : args.manager;
133+
if (!["npm", "pnpm"].includes(manager)) throw new Error("--manager must be auto, npm, or pnpm");
134+
135+
const windowMs = args.minutes != null ? args.minutes * MINUTE_MS : args.days * DAY_MS;
136+
if (!Number.isFinite(windowMs) || windowMs < 0) {
137+
throw new Error("Release-age window must be a positive number");
138+
}
139+
140+
const now = new Date();
141+
const cutoff = new Date(now.getTime() - windowMs);
142+
const outdatedRows = normalizeOutdated(runOutdated(manager));
143+
const rows = [];
144+
145+
for (const item of outdatedRows) {
146+
const metadata = await fetchMetadata(item.name);
147+
const times = metadata.time ?? {};
148+
const current = parseVersion(item.current)?.raw;
149+
const versions = Object.keys(metadata.versions ?? {})
150+
.filter((version) => parseVersion(version))
151+
.filter((version) => !parseVersion(version)?.prerelease)
152+
.sort(compareVersions);
153+
154+
const newerEligible = versions
155+
.filter((version) => compareVersions(version, current) > 0)
156+
.filter((version) => times[version] && new Date(times[version]) <= cutoff);
157+
158+
const sameMajor = newerEligible.filter((version) => parseVersion(version).major === parseVersion(current).major);
159+
const eligibleSameMajor = sameMajor.at(-1) ?? "";
160+
const eligibleAny = newerEligible.at(-1) ?? "";
161+
162+
rows.push({
163+
name: item.name,
164+
dependent: item.dependent ?? "",
165+
type: item.type ?? "",
166+
current: item.current,
167+
wanted: item.wanted ?? "",
168+
latest: item.latest ?? metadata["dist-tags"]?.latest ?? "",
169+
latestPublished: times[item.latest] ?? "",
170+
eligibleSameMajor,
171+
eligibleSameMajorPublished: eligibleSameMajor ? times[eligibleSameMajor] : "",
172+
eligibleSameMajorType: eligibleSameMajor ? classify(current, eligibleSameMajor) : "",
173+
eligibleAny,
174+
eligibleAnyPublished: eligibleAny ? times[eligibleAny] : "",
175+
eligibleAnyType: eligibleAny ? classify(current, eligibleAny) : "",
176+
latestIsMajor: item.latest ? classify(current, item.latest) === "major" : false,
177+
});
178+
}
179+
180+
console.log(
181+
JSON.stringify(
182+
{
183+
manager,
184+
cwd: join(process.cwd()),
185+
now: now.toISOString(),
186+
cutoff: cutoff.toISOString(),
187+
rows,
188+
},
189+
null,
190+
2,
191+
),
192+
);
193+
}
194+
195+
main().catch((error) => {
196+
console.error(error instanceof Error ? error.message : String(error));
197+
process.exit(1);
198+
});

0 commit comments

Comments
 (0)