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 CHANGED
@@ -8,37 +8,37 @@ re-verification, build-script attestation, and more.
8
8
  ## Install
9
9
 
10
10
  ```bash
11
- npx pmsec enable
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 enable
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
- ## Commands
32
+ ## Usage
33
33
 
34
- | Command | Description |
34
+ | Invocation | Description |
35
35
  | --- | --- |
36
- | `pmsec enable` | Write the hardening bundle (3-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. |
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 3-day default), `--force` (overwrite stricter existing cooldowns; default is monotonic), `--json`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmsec",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
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 = 3;
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 <command> [options]
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. \`set\` flips on every safe-by-default key each
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 3)
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 enable
41
- npx pmsec enable --days 7
42
- npx pmsec enable --days 1 --force
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 = { command: null, json: false, only: null, days: BUNDLE_DAYS, force: false, help: false, version: false };
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
- opts.command = positional[0] ?? null;
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, env, home, platform) {
93
+ async function gatherStatus(targets, ctx) {
89
94
  return Promise.all(targets.map(async t => {
90
- const r = await t.read(env, home, platform);
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 }, env, home, platform, out, err) {
117
- const rows = await gatherStatus(targets, env, home, platform);
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 enable\`\n`);
124
- if (failingExtras.length) err.write(`pmsec: ${failingExtras.length} hardening setting(s) not at safe value — run \`pmsec enable\`\n`);
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 = shQuote(p);
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, env, home, platform, out, err) {
141
- const results = [];
142
- const failures = [];
143
- const warnings = [];
144
- for (const t of targets) {
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(env, home, platform);
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, env, home, platform);
157
- results.push({ tool: t.name, path: r.path, days: effective, requested: days, kept, forced: force, ok: true, warn });
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
- failures.push({ tool: t.name, error: explainFsError(e, t.name) });
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, env, home, platform, out, err) {
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(env, home, platform);
185
- results.push({ tool: t.name, path: r.path, removed: r.removed, ok: true });
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
- failures.push({ tool: t.name, error: explainFsError(e, t.name) });
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 || !opts.command) { out.write(USAGE); return opts.help ? 0 : 2; }
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.command === "check") return await runCheck(targets, opts, env, home, platform, out, err);
217
- if (opts.command === "enable") return await runEnable(targets, opts.json, opts.days, opts.force, env, home, platform, out, err);
218
- if (opts.command === "disable") return await runDisable(targets, opts.json, env, home, platform, out, err);
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 { detectVersion, gte } from "../util/version.mjs";
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(env, home) { return bunConfigPath(env, home); }
13
+ export function path(ctx) { return bunConfigPath(ctx.env, ctx.home); }
14
14
 
15
- export function preflight() {
16
- const v = detectVersion("bun");
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(env, home) {
23
- const p = path(env, home);
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, env, home) {
31
- const p = path(env, home);
32
- const before = await readSafe(p);
33
- const after = setKey(before, key, `${key} = ${days * 86400}`, { section });
34
- await writeAtomic(p, after);
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(env, home) {
39
- const p = path(env, home);
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);
@@ -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(env, home) { return cargoConfigPath(env, home); }
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(env, home) {
22
- const p = path(env, home);
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, env, home) {
29
- const p = path(env, home);
30
- const before = await readSafe(p);
31
- const after = setKey(before, key, `${key} = "${days}d"`, { section });
32
- await writeAtomic(p, after);
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(env, home) {
37
- const p = path(env, home);
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);
@@ -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 { detectVersion, gte } from "../util/version.mjs";
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(env, home, platform) { return miseConfigPath(env, home, platform); }
15
+ export function path(ctx) { return miseConfigPath(ctx.env, ctx.home, ctx.platform); }
16
16
 
17
- export function preflight() {
18
- const v = detectVersion("mise");
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(env, home, platform) {
37
- const p = path(env, home, platform);
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, env, home, platform) {
47
- const p = path(env, home, platform);
48
- const before = await readSafe(p);
49
- let after = setKey(before, key, `${key} = "${days}d"`, { section });
50
- after = applyExtras(after, extras);
51
- await writeAtomic(p, after);
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(env, home, platform) {
56
- const p = path(env, home, platform);
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 { detectVersion, gte } from "../util/version.mjs";
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(env, home) { return npmrcPath(env, home); }
15
+ export function path(ctx) { return npmrcPath(ctx.env, ctx.home); }
16
16
 
17
- export function preflight() {
18
- const v = detectVersion("npm");
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(env, home) {
25
- const p = path(env, home);
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, env, home) {
36
- const p = path(env, home);
37
- const before = await readSafe(p);
38
- let after = setKey(before, key, `${key}=${days}`);
39
- after = applyExtras(after, extras);
40
- await writeAtomic(p, after);
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(env, home) {
45
- const p = path(env, home);
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);
@@ -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(env, home, platform) { return pnpmRcPath(env, home, platform); }
20
+ export function path(ctx) { return pnpmRcPath(ctx.env, ctx.home, ctx.platform); }
21
21
 
22
- function pnpmVersion(env) {
23
- return detectVersion("pnpm", ["--version"], { env, overrideKey: "PMSEC_PNPM_VERSION" });
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(env) {
27
- const v = pnpmVersion(env);
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(env, home, platform) {
34
- const p = path(env, home, platform);
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(env);
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, env, home, platform) {
55
- const p = path(env, home, platform);
56
- const before = await readSafe(p);
57
- let after = setKey(before, key, `${key}=${days * 24 * 60}`);
58
- after = applyExtras(after, extras);
59
- await writeAtomic(p, after);
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(env, home, platform) {
64
- const p = path(env, home, platform);
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 { detectVersion, gte } from "../util/version.mjs";
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(env, home, platform) { return uvConfigPath(env, home, platform); }
12
+ export function path(ctx) { return uvConfigPath(ctx.env, ctx.home, ctx.platform); }
13
13
 
14
- export function preflight() {
15
- const v = detectVersion("uv");
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(env, home, platform) {
30
- const p = path(env, home, platform);
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, env, home, platform) {
37
- const p = path(env, home, platform);
38
- const before = await readSafe(p);
39
- const after = setKey(before, key, `${key} = "${days} days"`);
40
- await writeAtomic(p, after);
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(env, home, platform) {
45
- const p = path(env, home, platform);
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);
@@ -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 { detectVersion, gte } from "../util/version.mjs";
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(env, home) { return yarnrcPath(env, home); }
16
+ export function path(ctx) { return yarnrcPath(ctx.env, ctx.home); }
17
17
 
18
- export function preflight() {
19
- const v = detectVersion("yarn");
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(env, home) {
34
- const p = path(env, home);
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, env, home) {
44
- const p = path(env, home);
45
- const before = await readSafe(p);
46
- let after = setKey(before, key, `${key}: "${days}d"`, { sep: SEP });
47
- after = applyExtras(after, extras);
48
- await writeAtomic(p, after);
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(env, home) {
53
- const p = path(env, home);
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;
@@ -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
+ }