pmsec 0.5.1 → 0.6.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
@@ -38,7 +38,7 @@ Zero runtime dependencies, ESM, requires Node 22+.
38
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), `--json`.
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`.
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.5.1",
3
+ "version": "0.6.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/cli.mjs CHANGED
@@ -31,6 +31,7 @@ Commands:
31
31
  Options:
32
32
  --tool TOOL[,TOOL] Restrict to specific tools (npm,pnpm,yarn,bun,cargo,mise,uv)
33
33
  --days N Override cooldown days (default 3)
34
+ --force Overwrite stricter existing cooldowns (otherwise enable is monotonic)
34
35
  --json Emit JSON output
35
36
  -V, --version Show version
36
37
  -h, --help Show this help
@@ -38,6 +39,7 @@ Options:
38
39
  Examples:
39
40
  npx pmsec enable
40
41
  npx pmsec enable --days 7
42
+ npx pmsec enable --days 1 --force
41
43
  npx pmsec check
42
44
  npx pmsec disable --tool npm
43
45
  `;
@@ -49,13 +51,14 @@ function parseDays(raw) {
49
51
  }
50
52
 
51
53
  function parse(argv) {
52
- const opts = { command: null, json: false, only: null, days: BUNDLE_DAYS, help: false, version: false };
54
+ const opts = { command: null, json: false, only: null, days: BUNDLE_DAYS, force: false, help: false, version: false };
53
55
  const positional = [];
54
56
  for (let i = 0; i < argv.length; i++) {
55
57
  const a = argv[i];
56
58
  if (a === "-h" || a === "--help") opts.help = true;
57
59
  else if (a === "-V" || a === "--version") opts.version = true;
58
60
  else if (a === "--json") opts.json = true;
61
+ else if (a === "--force") opts.force = true;
59
62
  else if (a === "--tool") opts.only = argv[++i].split(",");
60
63
  else if (a.startsWith("--tool=")) opts.only = a.slice(7).split(",");
61
64
  else if (a === "--days") opts.days = parseDays(argv[++i]);
@@ -102,8 +105,8 @@ function renderHuman(rows, min) {
102
105
  const tail = r.warn ? `\n ⚠ ${r.warn}` : "";
103
106
  lines.push(`${status} ${r.tool.padEnd(4)} ${r.key} = ${value} [${r.path}]${tail}`);
104
107
  for (const e of r.extras) {
105
- const exStatus = e.configured === null ? "MISSING" : e.ok ? "OK " : "STALE ";
106
- const exValue = e.configured ?? "(unset)";
108
+ const exStatus = e.ok ? "OK " : e.configured === null ? "MISSING" : "STALE ";
109
+ const exValue = e.defaultEnforced ? `(default — runtime enforces ${e.expected})` : (e.configured ?? "(unset)");
107
110
  lines.push(`${exStatus} ${r.tool.padEnd(4)} ${e.key} = ${exValue} [${r.path}]`);
108
111
  }
109
112
  }
@@ -134,7 +137,7 @@ function explainFsError(e, tool) {
134
137
  return `${tool}: ${e?.message ?? e}`;
135
138
  }
136
139
 
137
- async function runEnable(targets, json, days, env, home, platform, out, err) {
140
+ async function runEnable(targets, json, days, force, env, home, platform, out, err) {
138
141
  const results = [];
139
142
  const failures = [];
140
143
  const warnings = [];
@@ -142,15 +145,19 @@ async function runEnable(targets, json, days, env, home, platform, out, err) {
142
145
  const warn = preflightWarn(t);
143
146
  if (warn) warnings.push({ tool: t.name, warn });
144
147
  try {
145
- const current = await t.read(env, home, platform);
146
- const currentDays = current.days ?? 0;
147
- const effective = Math.max(currentDays, days);
148
- const kept = currentDays >= days && currentDays > 0;
148
+ let effective = days;
149
+ let kept = false;
150
+ if (!force) {
151
+ const current = await t.read(env, home, platform);
152
+ const currentDays = current.days ?? 0;
153
+ effective = Math.max(currentDays, days);
154
+ kept = currentDays >= days && currentDays > 0;
155
+ }
149
156
  const r = await t.write(effective, env, home, platform);
150
- results.push({ tool: t.name, path: r.path, days: effective, requested: days, kept, ok: true, warn });
157
+ results.push({ tool: t.name, path: r.path, days: effective, requested: days, kept, forced: force, ok: true, warn });
151
158
  } catch (e) {
152
159
  failures.push({ tool: t.name, error: explainFsError(e, t.name) });
153
- results.push({ tool: t.name, path: e?.path ?? null, days, requested: days, kept: false, ok: false, error: explainFsError(e, t.name), warn });
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 });
154
161
  }
155
162
  }
156
163
  if (json) out.write(JSON.stringify({ enabled: true, bundleDays: days, results, warnings, ok: failures.length === 0 }, null, 2) + "\n");
@@ -207,7 +214,7 @@ export async function run(argv, {
207
214
  catch (e) { err.write(`pmsec: ${e.message}\n`); return 2; }
208
215
  try {
209
216
  if (opts.command === "check") return await runCheck(targets, opts, env, home, platform, out, err);
210
- if (opts.command === "enable") return await runEnable(targets, opts.json, opts.days, 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);
211
218
  if (opts.command === "disable") return await runDisable(targets, opts.json, env, home, platform, out, err);
212
219
  err.write(`pmsec: unknown command "${opts.command}"\n`);
213
220
  return 2;
@@ -8,15 +8,22 @@ export const name = "pnpm";
8
8
  export const key = "minimum-release-age";
9
9
  export const docs = "https://pnpm.io/settings#minimumreleaseage";
10
10
  export const minBin = [10, 6, 0];
11
+ // `defaultSinceMajor`: pnpm major version where the value became the default.
12
+ // Once detected, an absent line in `.npmrc` is still effectively in force, so
13
+ // `read()` reports it as ok with `defaultEnforced: true`.
11
14
  export const extras = [
12
15
  { key: "trust-policy", expected: "no-downgrade", line: "trust-policy=no-downgrade" },
13
- { key: "block-exotic-subdeps", expected: "true", line: "block-exotic-subdeps=true" }
16
+ { key: "block-exotic-subdeps", expected: "true", line: "block-exotic-subdeps=true", defaultSinceMajor: 11 }
14
17
  ];
15
18
 
16
19
  export function path(env, home) { return npmrcPath(env, home); }
17
20
 
18
- export function preflight() {
19
- const v = detectVersion("pnpm");
21
+ function pnpmVersion(env) {
22
+ return detectVersion("pnpm", ["--version"], { env, overrideKey: "PMSEC_PNPM_VERSION" });
23
+ }
24
+
25
+ export function preflight(env) {
26
+ const v = pnpmVersion(env);
20
27
  if (v === null) return { ok: true, message: null };
21
28
  if (gte(v, minBin)) return { ok: true, version: v.raw, message: null };
22
29
  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.` };
@@ -27,10 +34,19 @@ export async function read(env, home) {
27
34
  const raw = await readSafe(p);
28
35
  const value = readKey(raw, key);
29
36
  const minutes = value === null ? null : Number(value);
37
+ const v = pnpmVersion(env);
38
+ const defaults = new Map(extras.filter(x => x.defaultSinceMajor).map(x => [x.key, x.defaultSinceMajor]));
39
+ const evaluated = readExtras(raw, extras).map(e => {
40
+ const def = defaults.get(e.key);
41
+ if (e.configured === null && def && v && v.major >= def) {
42
+ return { ...e, ok: true, defaultEnforced: true };
43
+ }
44
+ return e;
45
+ });
30
46
  return {
31
47
  path: p, configured: value,
32
48
  days: minutes === null || Number.isNaN(minutes) ? null : Math.floor(minutes / (60 * 24)),
33
- extras: readExtras(raw, extras)
49
+ extras: evaluated
34
50
  };
35
51
  }
36
52
 
@@ -1,12 +1,27 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
 
3
- export function detectVersion(bin, args = ["--version"]) {
3
+ function parseSemver(s) {
4
+ const m = String(s || "").match(/(\d+)\.(\d+)\.(\d+)/);
5
+ if (!m) return null;
6
+ return { major: +m[1], minor: +m[2], patch: +m[3], raw: m[0] };
7
+ }
8
+
9
+ // `env[overrideKey]` ("X.Y.Z" or "none") forces the result without spawning the
10
+ // real binary — used by tests to make pnpm 11 default-enforcement behavior
11
+ // deterministic regardless of what's installed locally.
12
+ export function detectVersion(bin, args = ["--version"], { env, overrideKey } = {}) {
13
+ if (env && overrideKey) {
14
+ const o = env[overrideKey];
15
+ if (o === "none") return null;
16
+ if (o) {
17
+ const p = parseSemver(o);
18
+ if (p) return p;
19
+ }
20
+ }
4
21
  try {
5
22
  const res = spawnSync(bin, args, { encoding: "utf8" });
6
23
  if (res.status !== 0) return null;
7
- const m = (res.stdout || "").match(/(\d+)\.(\d+)\.(\d+)/);
8
- if (!m) return null;
9
- return { major: +m[1], minor: +m[2], patch: +m[3], raw: m[0] };
24
+ return parseSemver(res.stdout);
10
25
  } catch { return null; }
11
26
  }
12
27