pmsec 0.8.0 → 0.10.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 +14 -14
- package/package.json +1 -1
- package/src/cli.mjs +57 -58
- package/src/tools/bun.mjs +13 -18
- package/src/tools/cargo.mjs +10 -11
- package/src/tools/mise.mjs +14 -19
- package/src/tools/npm.mjs +14 -19
- package/src/tools/pnpm.mjs +25 -17
- package/src/tools/uv.mjs +13 -18
- package/src/tools/yarn.mjs +14 -19
- package/src/util/io.mjs +1 -1
- package/src/util/version.mjs +11 -1
package/README.md
CHANGED
|
@@ -8,37 +8,37 @@ re-verification, build-script attestation, and more.
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
npx pmsec
|
|
12
|
-
npx pmsec check
|
|
13
|
-
npx pmsec disable
|
|
11
|
+
npx pmsec
|
|
12
|
+
npx pmsec --check
|
|
13
|
+
npx pmsec --disable
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
uvx pmsec
|
|
18
|
-
uvx pmsec check
|
|
19
|
-
uvx pmsec disable
|
|
17
|
+
uvx pmsec
|
|
18
|
+
uvx pmsec --check
|
|
19
|
+
uvx pmsec --disable
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
If your environment already enforces cooldown (or routes through a proxy
|
|
23
23
|
registry), bootstrap pmsec by overriding just for that call:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
npx --registry=https://registry.npmjs.org/ --min-release-age=0 pmsec check
|
|
27
|
-
uvx --index https://pypi.org/simple --exclude-newer-package pmsec=2099-01-01 pmsec check
|
|
26
|
+
npx --registry=https://registry.npmjs.org/ --min-release-age=0 pmsec --check
|
|
27
|
+
uvx --index https://pypi.org/simple --exclude-newer-package pmsec=2099-01-01 pmsec --check
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Zero runtime dependencies, ESM, requires Node 22+.
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## Usage
|
|
33
33
|
|
|
34
|
-
|
|
|
34
|
+
| Invocation | Description |
|
|
35
35
|
| --- | --- |
|
|
36
|
-
| `pmsec
|
|
37
|
-
| `pmsec check` | Read each tool's config; exit 1 if any row is missing or below the bundled value. |
|
|
38
|
-
| `pmsec disable` | Remove every key the bundle set; other keys in the file are preserved. |
|
|
36
|
+
| `pmsec` | Default action: write the hardening bundle (1-day cooldown + per-tool extras) to every selected tool's user config. |
|
|
37
|
+
| `pmsec --check` | Read each tool's config; exit 1 if any row is missing or below the bundled value. |
|
|
38
|
+
| `pmsec --disable` | Remove every key the bundle set; other keys in the file are preserved. |
|
|
39
39
|
| `pmsec --version` | Print the installed pmsec version. |
|
|
40
40
|
|
|
41
|
-
Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv`, `--days N` (override the
|
|
41
|
+
Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv`, `--days N` (override the 1-day default), `--force` (overwrite stricter existing cooldowns; default is monotonic), `--json`. `--check` and `--disable` are mutually exclusive.
|
|
42
42
|
|
|
43
43
|
When the target file is owned by another user (typical: `~/.npmrc` left
|
|
44
44
|
root-owned by an old `sudo npm config set`), the write fails with `EACCES`
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { shellQuote } from "./util/io.mjs";
|
|
3
4
|
import * as npm from "./tools/npm.mjs";
|
|
4
5
|
import * as pnpm from "./tools/pnpm.mjs";
|
|
5
6
|
import * as yarn from "./tools/yarn.mjs";
|
|
@@ -11,37 +12,34 @@ import * as uv from "./tools/uv.mjs";
|
|
|
11
12
|
const TOOLS = [npm, pnpm, yarn, bun, cargo, mise, uv];
|
|
12
13
|
// Default cooldown for the hardening bundle. Override per-invocation with
|
|
13
14
|
// `--days N`; the default tracks the safest value we'd recommend.
|
|
14
|
-
const BUNDLE_DAYS =
|
|
15
|
+
const BUNDLE_DAYS = 1;
|
|
15
16
|
export const VERSION = JSON.parse(
|
|
16
17
|
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
17
18
|
).version;
|
|
18
19
|
|
|
19
|
-
const USAGE = `pmsec
|
|
20
|
+
const USAGE = `pmsec [options]
|
|
20
21
|
|
|
21
22
|
Zero-config install-time supply-chain hardening across npm, pnpm, yarn,
|
|
22
|
-
bun, cargo, mise, uv.
|
|
23
|
-
tool exposes (cooldown, audit-level, trust-policy, hardened mode,
|
|
23
|
+
bun, cargo, mise, uv. Default action enables every safe-by-default key
|
|
24
|
+
each tool exposes (cooldown, audit-level, trust-policy, hardened mode,
|
|
24
25
|
attestation re-verification, ...). No knobs.
|
|
25
26
|
|
|
26
|
-
Commands:
|
|
27
|
-
enable Apply the hardening bundle to all selected tools
|
|
28
|
-
disable Remove the hardening bundle from selected tools
|
|
29
|
-
check Verify the bundle is in place (exit 1 if anything missing)
|
|
30
|
-
|
|
31
27
|
Options:
|
|
28
|
+
--check Verify the bundle is in place (exit 1 if anything missing)
|
|
29
|
+
--disable Remove the hardening bundle from selected tools
|
|
32
30
|
--tool TOOL[,TOOL] Restrict to specific tools (npm,pnpm,yarn,bun,cargo,mise,uv)
|
|
33
|
-
--days N Override cooldown days (default
|
|
31
|
+
--days N Override cooldown days (default 1)
|
|
34
32
|
--force Overwrite stricter existing cooldowns (otherwise enable is monotonic)
|
|
35
33
|
--json Emit JSON output
|
|
36
34
|
-V, --version Show version
|
|
37
35
|
-h, --help Show this help
|
|
38
36
|
|
|
39
37
|
Examples:
|
|
40
|
-
npx pmsec
|
|
41
|
-
npx pmsec
|
|
42
|
-
npx pmsec
|
|
43
|
-
npx pmsec check
|
|
44
|
-
npx pmsec disable --tool npm
|
|
38
|
+
npx pmsec
|
|
39
|
+
npx pmsec --days 7
|
|
40
|
+
npx pmsec --days 1 --force
|
|
41
|
+
npx pmsec --check
|
|
42
|
+
npx pmsec --disable --tool npm
|
|
45
43
|
`;
|
|
46
44
|
|
|
47
45
|
function parseDays(raw) {
|
|
@@ -51,7 +49,13 @@ function parseDays(raw) {
|
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
function parse(argv) {
|
|
54
|
-
const opts = {
|
|
52
|
+
const opts = { mode: "enable", json: false, only: null, days: BUNDLE_DAYS, force: false, help: false, version: false };
|
|
53
|
+
let modeSet = false;
|
|
54
|
+
const setMode = (m) => {
|
|
55
|
+
if (modeSet && opts.mode !== m) throw new Error(`--check and --disable are mutually exclusive`);
|
|
56
|
+
opts.mode = m;
|
|
57
|
+
modeSet = true;
|
|
58
|
+
};
|
|
55
59
|
const positional = [];
|
|
56
60
|
for (let i = 0; i < argv.length; i++) {
|
|
57
61
|
const a = argv[i];
|
|
@@ -59,6 +63,8 @@ function parse(argv) {
|
|
|
59
63
|
else if (a === "-V" || a === "--version") opts.version = true;
|
|
60
64
|
else if (a === "--json") opts.json = true;
|
|
61
65
|
else if (a === "--force") opts.force = true;
|
|
66
|
+
else if (a === "--check") setMode("check");
|
|
67
|
+
else if (a === "--disable") setMode("disable");
|
|
62
68
|
else if (a === "--tool") opts.only = argv[++i].split(",");
|
|
63
69
|
else if (a.startsWith("--tool=")) opts.only = a.slice(7).split(",");
|
|
64
70
|
else if (a === "--days") opts.days = parseDays(argv[++i]);
|
|
@@ -66,8 +72,7 @@ function parse(argv) {
|
|
|
66
72
|
else if (a.startsWith("-")) throw new Error(`unknown flag: ${a}`);
|
|
67
73
|
else positional.push(a);
|
|
68
74
|
}
|
|
69
|
-
|
|
70
|
-
if (positional.length > 1) throw new Error(`unexpected argument: ${positional[1]}`);
|
|
75
|
+
if (positional.length) throw new Error(`unexpected argument: ${positional[0]}`);
|
|
71
76
|
return opts;
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -79,20 +84,20 @@ function selectTools(only) {
|
|
|
79
84
|
return found;
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
function preflightWarn(t) {
|
|
87
|
+
function preflightWarn(t, ctx) {
|
|
83
88
|
if (typeof t.preflight !== "function") return null;
|
|
84
|
-
const pf = t.preflight();
|
|
89
|
+
const pf = t.preflight(ctx);
|
|
85
90
|
return pf?.message ?? null;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
async function gatherStatus(targets,
|
|
93
|
+
async function gatherStatus(targets, ctx) {
|
|
89
94
|
return Promise.all(targets.map(async t => {
|
|
90
|
-
const r = await t.read(
|
|
95
|
+
const r = await t.read(ctx);
|
|
91
96
|
return {
|
|
92
97
|
tool: t.name, key: t.key, path: r.path,
|
|
93
98
|
configured: r.configured, days: r.days,
|
|
94
99
|
extras: r.extras ?? [],
|
|
95
|
-
warn: preflightWarn(t)
|
|
100
|
+
warn: preflightWarn(t, ctx)
|
|
96
101
|
};
|
|
97
102
|
}));
|
|
98
103
|
}
|
|
@@ -113,53 +118,50 @@ function renderHuman(rows, min) {
|
|
|
113
118
|
return lines.join("\n") + "\n";
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
async function runCheck(targets, { json, days },
|
|
117
|
-
const rows = await gatherStatus(targets,
|
|
121
|
+
async function runCheck(targets, { json, days }, ctx, out, err) {
|
|
122
|
+
const rows = await gatherStatus(targets, ctx);
|
|
118
123
|
const failingPrimary = rows.filter(r => r.days === null || r.days < days);
|
|
119
124
|
const failingExtras = rows.flatMap(r => r.extras.filter(e => !e.ok));
|
|
120
125
|
const ok = failingPrimary.length === 0 && failingExtras.length === 0;
|
|
121
126
|
if (json) out.write(JSON.stringify({ bundleDays: days, rows, ok }, null, 2) + "\n");
|
|
122
127
|
else out.write(renderHuman(rows, days));
|
|
123
|
-
if (failingPrimary.length) err.write(`pmsec: ${failingPrimary.length} tool(s) below ${days} days — run \`pmsec
|
|
124
|
-
if (failingExtras.length) err.write(`pmsec: ${failingExtras.length} hardening setting(s) not at safe value — run \`pmsec
|
|
128
|
+
if (failingPrimary.length) err.write(`pmsec: ${failingPrimary.length} tool(s) below ${days} days — run \`pmsec\`\n`);
|
|
129
|
+
if (failingExtras.length) err.write(`pmsec: ${failingExtras.length} hardening setting(s) not at safe value — run \`pmsec\`\n`);
|
|
125
130
|
return ok ? 0 : 1;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
|
-
function shQuote(s) { return s ? `'${String(s).replace(/'/g, `'\\''`)}'` : ""; }
|
|
129
|
-
|
|
130
133
|
function explainFsError(e, tool) {
|
|
131
134
|
if (e?.code === "EACCES" || e?.code === "EPERM") {
|
|
132
135
|
const p = e.path ?? "";
|
|
133
|
-
const q =
|
|
136
|
+
const q = p ? shellQuote(p) : "";
|
|
134
137
|
return `${tool}: cannot write ${p} (${e.code}). Check file ownership: \`ls -la ${q}\` — if owned by root, run \`sudo chown -h $(id -u):$(id -g) ${q}\`.`;
|
|
135
138
|
}
|
|
136
139
|
if (e?.code === "EROFS") return `${tool}: ${e.path ?? ""} is on a read-only filesystem (EROFS).`;
|
|
137
140
|
return `${tool}: ${e?.message ?? e}`;
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
async function runEnable(targets, json, days, force,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
const warn = preflightWarn(t);
|
|
146
|
-
if (warn) warnings.push({ tool: t.name, warn });
|
|
143
|
+
async function runEnable(targets, json, days, force, ctx, out, err) {
|
|
144
|
+
// Each tool writes a different config file; safe to run in parallel.
|
|
145
|
+
// Promise.all preserves input order in the results array.
|
|
146
|
+
const results = await Promise.all(targets.map(async t => {
|
|
147
|
+
const warn = preflightWarn(t, ctx);
|
|
147
148
|
try {
|
|
148
149
|
let effective = days;
|
|
149
150
|
let kept = false;
|
|
150
151
|
if (!force) {
|
|
151
|
-
const current = await t.read(
|
|
152
|
+
const current = await t.read(ctx);
|
|
152
153
|
const currentDays = current.days ?? 0;
|
|
153
154
|
effective = Math.max(currentDays, days);
|
|
154
155
|
kept = currentDays >= days && currentDays > 0;
|
|
155
156
|
}
|
|
156
|
-
const r = await t.write(effective,
|
|
157
|
-
|
|
157
|
+
const r = await t.write(effective, ctx);
|
|
158
|
+
return { tool: t.name, path: r.path, days: effective, requested: days, kept, forced: force, ok: true, warn };
|
|
158
159
|
} catch (e) {
|
|
159
|
-
|
|
160
|
-
results.push({ tool: t.name, path: e?.path ?? null, days, requested: days, kept: false, forced: force, ok: false, error: explainFsError(e, t.name), warn });
|
|
160
|
+
return { tool: t.name, path: e?.path ?? null, days, requested: days, kept: false, forced: force, ok: false, error: explainFsError(e, t.name), warn };
|
|
161
161
|
}
|
|
162
|
-
}
|
|
162
|
+
}));
|
|
163
|
+
const failures = results.filter(r => !r.ok).map(r => ({ tool: r.tool, error: r.error }));
|
|
164
|
+
const warnings = results.filter(r => r.warn).map(r => ({ tool: r.tool, warn: r.warn }));
|
|
163
165
|
if (json) out.write(JSON.stringify({ enabled: true, bundleDays: days, results, warnings, ok: failures.length === 0 }, null, 2) + "\n");
|
|
164
166
|
else for (const r of results) {
|
|
165
167
|
if (r.ok) {
|
|
@@ -176,18 +178,16 @@ async function runEnable(targets, json, days, force, env, home, platform, out, e
|
|
|
176
178
|
return failures.length ? 1 : 0;
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
async function runDisable(targets, json,
|
|
180
|
-
const results =
|
|
181
|
-
const failures = [];
|
|
182
|
-
for (const t of targets) {
|
|
181
|
+
async function runDisable(targets, json, ctx, out, err) {
|
|
182
|
+
const results = await Promise.all(targets.map(async t => {
|
|
183
183
|
try {
|
|
184
|
-
const r = await t.unset(
|
|
185
|
-
|
|
184
|
+
const r = await t.unset(ctx);
|
|
185
|
+
return { tool: t.name, path: r.path, removed: r.removed, ok: true };
|
|
186
186
|
} catch (e) {
|
|
187
|
-
|
|
188
|
-
results.push({ tool: t.name, path: e?.path ?? null, removed: false, ok: false, error: explainFsError(e, t.name) });
|
|
187
|
+
return { tool: t.name, path: e?.path ?? null, removed: false, ok: false, error: explainFsError(e, t.name) };
|
|
189
188
|
}
|
|
190
|
-
}
|
|
189
|
+
}));
|
|
190
|
+
const failures = results.filter(r => !r.ok).map(r => ({ tool: r.tool, error: r.error }));
|
|
191
191
|
if (json) out.write(JSON.stringify({ results, ok: failures.length === 0 }, null, 2) + "\n");
|
|
192
192
|
else for (const r of results) {
|
|
193
193
|
if (!r.ok) out.write(`FAIL ${r.tool.padEnd(4)} ${r.error}\n`);
|
|
@@ -208,16 +208,15 @@ export async function run(argv, {
|
|
|
208
208
|
try { opts = parse(argv); }
|
|
209
209
|
catch (e) { err.write(`pmsec: ${e.message}\n`); return 2; }
|
|
210
210
|
if (opts.version) { out.write(`pmsec ${VERSION}\n`); return 0; }
|
|
211
|
-
if (opts.help
|
|
211
|
+
if (opts.help) { out.write(USAGE); return 0; }
|
|
212
212
|
let targets;
|
|
213
213
|
try { targets = selectTools(opts.only); }
|
|
214
214
|
catch (e) { err.write(`pmsec: ${e.message}\n`); return 2; }
|
|
215
|
+
const ctx = { env, home, platform };
|
|
215
216
|
try {
|
|
216
|
-
if (opts.
|
|
217
|
-
if (opts.
|
|
218
|
-
|
|
219
|
-
err.write(`pmsec: unknown command "${opts.command}"\n`);
|
|
220
|
-
return 2;
|
|
217
|
+
if (opts.mode === "check") return await runCheck(targets, opts, ctx, out, err);
|
|
218
|
+
if (opts.mode === "disable") return await runDisable(targets, opts.json, ctx, out, err);
|
|
219
|
+
return await runEnable(targets, opts.json, opts.days, opts.force, ctx, out, err);
|
|
221
220
|
} catch (e) {
|
|
222
221
|
err.write(`pmsec: ${e.message}\n`);
|
|
223
222
|
return 1;
|
package/src/tools/bun.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { bunConfigPath } from "../util/paths.mjs";
|
|
2
2
|
import { readSafe, writeAtomic } from "../util/io.mjs";
|
|
3
3
|
import { readKey, setKey, removeKey } from "../util/lines.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { buildPreflight } from "../util/version.mjs";
|
|
5
5
|
|
|
6
6
|
export const name = "bun";
|
|
7
7
|
export const key = "minimumReleaseAge";
|
|
@@ -10,33 +10,28 @@ export const docs = "https://bun.com/docs/runtime/bunfig#install";
|
|
|
10
10
|
export const minBin = [1, 3, 0];
|
|
11
11
|
export const extras = [];
|
|
12
12
|
|
|
13
|
-
export function path(
|
|
13
|
+
export function path(ctx) { return bunConfigPath(ctx.env, ctx.home); }
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
if (v === null) return { ok: true, message: null };
|
|
18
|
-
if (gte(v, minBin)) return { ok: true, version: v.raw, message: null };
|
|
19
|
-
return { ok: true, warn: true, version: v.raw, message: `bun ${v.raw} < ${minBin.join(".")}: minimumReleaseAge is silently ignored. Upgrade bun to enforce the cooldown.` };
|
|
20
|
-
}
|
|
15
|
+
export const preflight = buildPreflight(name, minBin,
|
|
16
|
+
"minimumReleaseAge is silently ignored. Upgrade bun to enforce the cooldown.");
|
|
21
17
|
|
|
22
|
-
export async function read(
|
|
23
|
-
const p = path(
|
|
18
|
+
export async function read(ctx) {
|
|
19
|
+
const p = path(ctx);
|
|
24
20
|
const raw = await readSafe(p);
|
|
25
21
|
const value = readKey(raw, key, { section });
|
|
26
22
|
const seconds = value === null ? null : Number(value);
|
|
27
23
|
return { path: p, configured: value, days: seconds === null || Number.isNaN(seconds) ? null : Math.floor(seconds / 86400), extras: [] };
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
export async function write(days,
|
|
31
|
-
const p = path(
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return { path: p, before, after };
|
|
26
|
+
export async function write(days, ctx) {
|
|
27
|
+
const p = path(ctx);
|
|
28
|
+
const text = setKey(await readSafe(p), key, `${key} = ${days * 86400}`, { section });
|
|
29
|
+
await writeAtomic(p, text);
|
|
30
|
+
return { path: p };
|
|
36
31
|
}
|
|
37
32
|
|
|
38
|
-
export async function unset(
|
|
39
|
-
const p = path(
|
|
33
|
+
export async function unset(ctx) {
|
|
34
|
+
const p = path(ctx);
|
|
40
35
|
const before = await readSafe(p);
|
|
41
36
|
const { text: after, removed } = removeKey(before, key, { section });
|
|
42
37
|
if (removed) await writeAtomic(p, after);
|
package/src/tools/cargo.mjs
CHANGED
|
@@ -8,7 +8,7 @@ export const section = "install";
|
|
|
8
8
|
export const docs = "https://rust-lang.github.io/rfcs/3801-package-cooldown.html";
|
|
9
9
|
export const extras = [];
|
|
10
10
|
|
|
11
|
-
export function path(
|
|
11
|
+
export function path(ctx) { return cargoConfigPath(ctx.env, ctx.home); }
|
|
12
12
|
|
|
13
13
|
function parseDays(value) {
|
|
14
14
|
if (value === null) return null;
|
|
@@ -18,23 +18,22 @@ function parseDays(value) {
|
|
|
18
18
|
return /^(w|weeks?)$/i.test(m[2]) ? n * 7 : n;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export async function read(
|
|
22
|
-
const p = path(
|
|
21
|
+
export async function read(ctx) {
|
|
22
|
+
const p = path(ctx);
|
|
23
23
|
const raw = await readSafe(p);
|
|
24
24
|
const value = readKey(raw, key, { section });
|
|
25
25
|
return { path: p, configured: value, days: parseDays(value), extras: [] };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export async function write(days,
|
|
29
|
-
const p = path(
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return { path: p, before, after };
|
|
28
|
+
export async function write(days, ctx) {
|
|
29
|
+
const p = path(ctx);
|
|
30
|
+
const text = setKey(await readSafe(p), key, `${key} = "${days}d"`, { section });
|
|
31
|
+
await writeAtomic(p, text);
|
|
32
|
+
return { path: p };
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
export async function unset(
|
|
37
|
-
const p = path(
|
|
35
|
+
export async function unset(ctx) {
|
|
36
|
+
const p = path(ctx);
|
|
38
37
|
const before = await readSafe(p);
|
|
39
38
|
const { text: after, removed } = removeKey(before, key, { section });
|
|
40
39
|
if (removed) await writeAtomic(p, after);
|
package/src/tools/mise.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { miseConfigPath } from "../util/paths.mjs";
|
|
|
2
2
|
import { readSafe, writeAtomic } from "../util/io.mjs";
|
|
3
3
|
import { readKey, setKey, removeKey } from "../util/lines.mjs";
|
|
4
4
|
import { readExtras, applyExtras, removeExtras } from "../util/extras.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { buildPreflight } from "../util/version.mjs";
|
|
6
6
|
export const name = "mise";
|
|
7
7
|
export const key = "minimum_release_age";
|
|
8
8
|
export const section = "settings";
|
|
@@ -12,14 +12,10 @@ export const extras = [
|
|
|
12
12
|
{ key: "paranoid", expected: "true", line: "paranoid = true", section }
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
-
export function path(
|
|
15
|
+
export function path(ctx) { return miseConfigPath(ctx.env, ctx.home, ctx.platform); }
|
|
16
16
|
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
if (v === null) return { ok: true, message: null };
|
|
20
|
-
if (gte(v, minBin)) return { ok: true, version: v.raw, message: null };
|
|
21
|
-
return { ok: true, warn: true, version: v.raw, message: `mise ${v.raw} < ${minBin.join(".")}: setting was named install_before before 2026.4.22 and minimum_release_age is silently ignored on older mise. Upgrade mise (\`mise self-update\`) to enforce the cooldown.` };
|
|
22
|
-
}
|
|
17
|
+
export const preflight = buildPreflight(name, minBin,
|
|
18
|
+
"setting was named install_before before 2026.4.22 and minimum_release_age is silently ignored on older mise. Upgrade mise (`mise self-update`) to enforce the cooldown.");
|
|
23
19
|
|
|
24
20
|
function parseDays(value) {
|
|
25
21
|
if (value === null) return null;
|
|
@@ -33,8 +29,8 @@ function parseDays(value) {
|
|
|
33
29
|
return n;
|
|
34
30
|
}
|
|
35
31
|
|
|
36
|
-
export async function read(
|
|
37
|
-
const p = path(
|
|
32
|
+
export async function read(ctx) {
|
|
33
|
+
const p = path(ctx);
|
|
38
34
|
const raw = await readSafe(p);
|
|
39
35
|
const value = readKey(raw, key, { section });
|
|
40
36
|
return {
|
|
@@ -43,17 +39,16 @@ export async function read(env, home, platform) {
|
|
|
43
39
|
};
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
export async function write(days,
|
|
47
|
-
const p = path(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return { path: p, before, after };
|
|
42
|
+
export async function write(days, ctx) {
|
|
43
|
+
const p = path(ctx);
|
|
44
|
+
let text = setKey(await readSafe(p), key, `${key} = "${days}d"`, { section });
|
|
45
|
+
text = applyExtras(text, extras);
|
|
46
|
+
await writeAtomic(p, text);
|
|
47
|
+
return { path: p };
|
|
53
48
|
}
|
|
54
49
|
|
|
55
|
-
export async function unset(
|
|
56
|
-
const p = path(
|
|
50
|
+
export async function unset(ctx) {
|
|
51
|
+
const p = path(ctx);
|
|
57
52
|
const before = await readSafe(p);
|
|
58
53
|
const cooldown = removeKey(before, key, { section });
|
|
59
54
|
const ex = removeExtras(cooldown.text, extras);
|
package/src/tools/npm.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { npmrcPath } from "../util/paths.mjs";
|
|
|
2
2
|
import { readSafe, writeAtomic } from "../util/io.mjs";
|
|
3
3
|
import { readKey, setKey, removeKey } from "../util/lines.mjs";
|
|
4
4
|
import { readExtras, applyExtras, removeExtras } from "../util/extras.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { buildPreflight } from "../util/version.mjs";
|
|
6
6
|
|
|
7
7
|
export const name = "npm";
|
|
8
8
|
export const key = "min-release-age";
|
|
@@ -12,17 +12,13 @@ export const extras = [
|
|
|
12
12
|
{ key: "audit-level", expected: "high", line: "audit-level=high" }
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
-
export function path(
|
|
15
|
+
export function path(ctx) { return npmrcPath(ctx.env, ctx.home); }
|
|
16
16
|
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
if (v === null) return { ok: true, message: null };
|
|
20
|
-
if (gte(v, minBin)) return { ok: true, version: v.raw, message: null };
|
|
21
|
-
return { ok: true, warn: true, version: v.raw, message: `npm ${v.raw} < ${minBin.join(".")}: min-release-age is silently ignored. Upgrade npm to enforce the cooldown.` };
|
|
22
|
-
}
|
|
17
|
+
export const preflight = buildPreflight(name, minBin,
|
|
18
|
+
"min-release-age is silently ignored. Upgrade npm to enforce the cooldown.");
|
|
23
19
|
|
|
24
|
-
export async function read(
|
|
25
|
-
const p = path(
|
|
20
|
+
export async function read(ctx) {
|
|
21
|
+
const p = path(ctx);
|
|
26
22
|
const raw = await readSafe(p);
|
|
27
23
|
const value = readKey(raw, key);
|
|
28
24
|
return {
|
|
@@ -32,17 +28,16 @@ export async function read(env, home) {
|
|
|
32
28
|
};
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
export async function write(days,
|
|
36
|
-
const p = path(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return { path: p, before, after };
|
|
31
|
+
export async function write(days, ctx) {
|
|
32
|
+
const p = path(ctx);
|
|
33
|
+
let text = setKey(await readSafe(p), key, `${key}=${days}`);
|
|
34
|
+
text = applyExtras(text, extras);
|
|
35
|
+
await writeAtomic(p, text);
|
|
36
|
+
return { path: p };
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
export async function unset(
|
|
45
|
-
const p = path(
|
|
39
|
+
export async function unset(ctx) {
|
|
40
|
+
const p = path(ctx);
|
|
46
41
|
const before = await readSafe(p);
|
|
47
42
|
const cooldown = removeKey(before, key);
|
|
48
43
|
const ex = removeExtras(cooldown.text, extras);
|
package/src/tools/pnpm.mjs
CHANGED
|
@@ -17,25 +17,34 @@ export const extras = [
|
|
|
17
17
|
{ key: "strict-dep-builds", expected: "true", line: "strict-dep-builds=true" }
|
|
18
18
|
];
|
|
19
19
|
|
|
20
|
-
export function path(
|
|
20
|
+
export function path(ctx) { return pnpmRcPath(ctx.env, ctx.home, ctx.platform); }
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// Cache `pnpm --version` for the lifetime of the process. preflight() and
|
|
23
|
+
// read() both want it; without memoization a single `pmsec check` spawns
|
|
24
|
+
// pnpm twice. Cache key is the override env value so tests with different
|
|
25
|
+
// `PMSEC_PNPM_VERSION` settings invalidate naturally.
|
|
26
|
+
let _versionCache = null;
|
|
27
|
+
function pnpmVersion(ctx) {
|
|
28
|
+
const override = ctx.env?.PMSEC_PNPM_VERSION ?? null;
|
|
29
|
+
if (_versionCache && _versionCache.override === override) return _versionCache.value;
|
|
30
|
+
const v = detectVersion("pnpm", ["--version"], { env: ctx.env, overrideKey: "PMSEC_PNPM_VERSION" });
|
|
31
|
+
_versionCache = { override, value: v };
|
|
32
|
+
return v;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
export function preflight(
|
|
27
|
-
const v = pnpmVersion(
|
|
35
|
+
export function preflight(ctx) {
|
|
36
|
+
const v = pnpmVersion(ctx);
|
|
28
37
|
if (v === null) return { ok: true, message: null };
|
|
29
38
|
if (gte(v, minBin)) return { ok: true, version: v.raw, message: null };
|
|
30
39
|
return { ok: true, warn: true, version: v.raw, message: `pnpm ${v.raw} < ${minBin.join(".")}: minimum-release-age is silently ignored. Upgrade pnpm to enforce the cooldown.` };
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
export async function read(
|
|
34
|
-
const p = path(
|
|
42
|
+
export async function read(ctx) {
|
|
43
|
+
const p = path(ctx);
|
|
35
44
|
const raw = await readSafe(p);
|
|
36
45
|
const value = readKey(raw, key);
|
|
37
46
|
const minutes = value === null ? null : Number(value);
|
|
38
|
-
const v = pnpmVersion(
|
|
47
|
+
const v = pnpmVersion(ctx);
|
|
39
48
|
const defaults = new Map(extras.filter(x => x.defaultSinceMajor).map(x => [x.key, x.defaultSinceMajor]));
|
|
40
49
|
const evaluated = readExtras(raw, extras).map(e => {
|
|
41
50
|
const def = defaults.get(e.key);
|
|
@@ -51,17 +60,16 @@ export async function read(env, home, platform) {
|
|
|
51
60
|
};
|
|
52
61
|
}
|
|
53
62
|
|
|
54
|
-
export async function write(days,
|
|
55
|
-
const p = path(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return { path: p, before, after };
|
|
63
|
+
export async function write(days, ctx) {
|
|
64
|
+
const p = path(ctx);
|
|
65
|
+
let text = setKey(await readSafe(p), key, `${key}=${days * 24 * 60}`);
|
|
66
|
+
text = applyExtras(text, extras);
|
|
67
|
+
await writeAtomic(p, text);
|
|
68
|
+
return { path: p };
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
export async function unset(
|
|
64
|
-
const p = path(
|
|
71
|
+
export async function unset(ctx) {
|
|
72
|
+
const p = path(ctx);
|
|
65
73
|
const before = await readSafe(p);
|
|
66
74
|
const cooldown = removeKey(before, key);
|
|
67
75
|
const ex = removeExtras(cooldown.text, extras);
|
package/src/tools/uv.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { uvConfigPath } from "../util/paths.mjs";
|
|
2
2
|
import { readSafe, writeAtomic } from "../util/io.mjs";
|
|
3
3
|
import { readKey, setKey, removeKey } from "../util/lines.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { buildPreflight } from "../util/version.mjs";
|
|
5
5
|
|
|
6
6
|
export const name = "uv";
|
|
7
7
|
export const key = "exclude-newer";
|
|
@@ -9,14 +9,10 @@ export const docs = "https://docs.astral.sh/uv/reference/settings/#exclude-newer
|
|
|
9
9
|
export const minBin = [0, 9, 17];
|
|
10
10
|
export const extras = [];
|
|
11
11
|
|
|
12
|
-
export function path(
|
|
12
|
+
export function path(ctx) { return uvConfigPath(ctx.env, ctx.home, ctx.platform); }
|
|
13
13
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
if (v === null) return { ok: true, message: null };
|
|
17
|
-
if (gte(v, minBin)) return { ok: true, version: v.raw, message: null };
|
|
18
|
-
return { ok: true, warn: true, version: v.raw, message: `uv ${v.raw} < ${minBin.join(".")}: writing exclude-newer = "N days" will break this uv until you \`uv self update\` (file will fail to parse).` };
|
|
19
|
-
}
|
|
14
|
+
export const preflight = buildPreflight(name, minBin,
|
|
15
|
+
"writing exclude-newer = \"N days\" will break this uv until you `uv self update` (file will fail to parse).");
|
|
20
16
|
|
|
21
17
|
function parseDays(value) {
|
|
22
18
|
if (value === null) return null;
|
|
@@ -26,23 +22,22 @@ function parseDays(value) {
|
|
|
26
22
|
return /^(week|weeks|w)$/i.test(m[2]) ? n * 7 : n;
|
|
27
23
|
}
|
|
28
24
|
|
|
29
|
-
export async function read(
|
|
30
|
-
const p = path(
|
|
25
|
+
export async function read(ctx) {
|
|
26
|
+
const p = path(ctx);
|
|
31
27
|
const raw = await readSafe(p);
|
|
32
28
|
const value = readKey(raw, key);
|
|
33
29
|
return { path: p, configured: value, days: parseDays(value), extras: [] };
|
|
34
30
|
}
|
|
35
31
|
|
|
36
|
-
export async function write(days,
|
|
37
|
-
const p = path(
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return { path: p, before, after };
|
|
32
|
+
export async function write(days, ctx) {
|
|
33
|
+
const p = path(ctx);
|
|
34
|
+
const text = setKey(await readSafe(p), key, `${key} = "${days} days"`);
|
|
35
|
+
await writeAtomic(p, text);
|
|
36
|
+
return { path: p };
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
export async function unset(
|
|
45
|
-
const p = path(
|
|
39
|
+
export async function unset(ctx) {
|
|
40
|
+
const p = path(ctx);
|
|
46
41
|
const before = await readSafe(p);
|
|
47
42
|
const { text: after, removed } = removeKey(before, key);
|
|
48
43
|
if (removed) await writeAtomic(p, after);
|
package/src/tools/yarn.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { yarnrcPath } from "../util/paths.mjs";
|
|
|
2
2
|
import { readSafe, writeAtomic } from "../util/io.mjs";
|
|
3
3
|
import { readKey, setKey, removeKey } from "../util/lines.mjs";
|
|
4
4
|
import { readExtras, applyExtras, removeExtras } from "../util/extras.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { buildPreflight } from "../util/version.mjs";
|
|
6
6
|
|
|
7
7
|
export const name = "yarn";
|
|
8
8
|
export const key = "npmMinimalAgeGate";
|
|
@@ -13,14 +13,10 @@ export const extras = [
|
|
|
13
13
|
{ key: "enableHardenedMode", expected: "true", line: "enableHardenedMode: true", sep: SEP }
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
-
export function path(
|
|
16
|
+
export function path(ctx) { return yarnrcPath(ctx.env, ctx.home); }
|
|
17
17
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
if (v === null) return { ok: true, message: null };
|
|
21
|
-
if (gte(v, minBin)) return { ok: true, version: v.raw, message: null };
|
|
22
|
-
return { ok: true, warn: true, version: v.raw, message: `yarn ${v.raw} < ${minBin.join(".")}: npmMinimalAgeGate is silently ignored. Upgrade yarn (v4.10+) to enforce the cooldown.` };
|
|
23
|
-
}
|
|
18
|
+
export const preflight = buildPreflight(name, minBin,
|
|
19
|
+
"npmMinimalAgeGate is silently ignored. Upgrade yarn (v4.10+) to enforce the cooldown.");
|
|
24
20
|
|
|
25
21
|
function parseDays(value) {
|
|
26
22
|
if (value === null) return null;
|
|
@@ -30,8 +26,8 @@ function parseDays(value) {
|
|
|
30
26
|
return /^(w|weeks?)$/i.test(m[2]) ? n * 7 : n;
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
export async function read(
|
|
34
|
-
const p = path(
|
|
29
|
+
export async function read(ctx) {
|
|
30
|
+
const p = path(ctx);
|
|
35
31
|
const raw = await readSafe(p);
|
|
36
32
|
const value = readKey(raw, key, { sep: SEP });
|
|
37
33
|
return {
|
|
@@ -40,17 +36,16 @@ export async function read(env, home) {
|
|
|
40
36
|
};
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
export async function write(days,
|
|
44
|
-
const p = path(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return { path: p, before, after };
|
|
39
|
+
export async function write(days, ctx) {
|
|
40
|
+
const p = path(ctx);
|
|
41
|
+
let text = setKey(await readSafe(p), key, `${key}: "${days}d"`, { sep: SEP });
|
|
42
|
+
text = applyExtras(text, extras);
|
|
43
|
+
await writeAtomic(p, text);
|
|
44
|
+
return { path: p };
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
export async function unset(
|
|
53
|
-
const p = path(
|
|
47
|
+
export async function unset(ctx) {
|
|
48
|
+
const p = path(ctx);
|
|
54
49
|
const before = await readSafe(p);
|
|
55
50
|
const cooldown = removeKey(before, key, { sep: SEP });
|
|
56
51
|
const ex = removeExtras(cooldown.text, extras);
|
package/src/util/io.mjs
CHANGED
|
@@ -23,7 +23,7 @@ function underHome(p, home) {
|
|
|
23
23
|
return r === h || r.startsWith(h + sep);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function shellQuote(s) { return `'${String(s).replace(/'/g, `'\\''`)}'`; }
|
|
26
|
+
export function shellQuote(s) { return `'${String(s).replace(/'/g, `'\\''`)}'`; }
|
|
27
27
|
|
|
28
28
|
async function reclaim(path, home, err) {
|
|
29
29
|
if (process.platform === "win32" || typeof process.getuid !== "function") return false;
|
package/src/util/version.mjs
CHANGED
|
@@ -19,7 +19,7 @@ export function detectVersion(bin, args = ["--version"], { env, overrideKey } =
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
try {
|
|
22
|
-
const res = spawnSync(bin, args, { encoding: "utf8" });
|
|
22
|
+
const res = spawnSync(bin, args, { encoding: "utf8", timeout: 5000, killSignal: "SIGKILL" });
|
|
23
23
|
if (res.status !== 0) return null;
|
|
24
24
|
return parseSemver(res.stdout);
|
|
25
25
|
} catch { return null; }
|
|
@@ -32,3 +32,13 @@ export function gte(v, target) {
|
|
|
32
32
|
if (v.minor !== b) return v.minor > b;
|
|
33
33
|
return v.patch >= c;
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
export function buildPreflight(name, minBin, suffix) {
|
|
37
|
+
return () => {
|
|
38
|
+
const v = detectVersion(name);
|
|
39
|
+
if (v === null) return { ok: true, message: null };
|
|
40
|
+
if (gte(v, minBin)) return { ok: true, version: v.raw, message: null };
|
|
41
|
+
return { ok: true, warn: true, version: v.raw,
|
|
42
|
+
message: `${name} ${v.raw} < ${minBin.join(".")}: ${suffix}` };
|
|
43
|
+
};
|
|
44
|
+
}
|