pmsec 0.3.0 → 0.4.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
@@ -28,7 +28,7 @@ npx --registry=https://registry.npmjs.org/ --min-release-age=0 pmsec check
28
28
  uvx --index https://pypi.org/simple --exclude-newer-package pmsec=2099-01-01 pmsec check
29
29
  ```
30
30
 
31
- Zero runtime dependencies, ESM, requires Node 20+.
31
+ Zero runtime dependencies, ESM, requires Node 22+.
32
32
 
33
33
  ## Commands
34
34
 
@@ -37,13 +37,14 @@ Zero runtime dependencies, ESM, requires Node 20+.
37
37
  | `pmsec check [--min N]` | Read each tool's config; exit 1 if any tool is below `N` days or unset. |
38
38
  | `pmsec set <DAYS>` | Write `DAYS`-day cooldown to every selected tool. Always proceeds; if the runtime is too old to honor the key, prints a `⚠` line under the success line. |
39
39
  | `pmsec unset` | Remove only the cooldown key from each config (other keys preserved). |
40
+ | `pmsec --version` | Print the installed pmsec version. |
40
41
 
41
42
  Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv`, `--json`.
42
43
 
43
44
  When the target file is owned by another user (typical: `~/.npmrc` left
44
- root-owned by an old `sudo npm config set`), pmsec auto-runs
45
- `sudo chown $(id -u):$(id -g) <path>` and retries the write. You'll be
46
- prompted for your password once; subsequent runs need no escalation.
45
+ root-owned by an old `sudo npm config set`), the write fails with `EACCES`
46
+ and pmsec prints the exact `chown` command needed to restore ownership —
47
+ re-run after applying it. pmsec never escalates privileges itself.
47
48
 
48
49
  See the [project README](https://github.com/HikaruEgashira/pmsec) for the
49
50
  full table of keys, units, paths, and environment overrides.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "pmsec",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "Inspect and apply install-time cooldown (min-release-age / exclude-newer) for npm and uv.",
7
+ "description": "Inspect and apply install-time cooldown for npm, pnpm, yarn, bun, cargo, mise, and uv.",
8
8
  "homepage": "https://github.com/HikaruEgashira/pmsec#readme",
9
9
  "repository": {
10
10
  "type": "git",
@@ -17,7 +17,7 @@
17
17
  "type": "module",
18
18
  "license": "MIT",
19
19
  "engines": {
20
- "node": ">=20"
20
+ "node": ">=22"
21
21
  },
22
22
  "bin": {
23
23
  "pmsec": "./bin/cli.mjs"
@@ -36,6 +36,11 @@
36
36
  "min-release-age",
37
37
  "exclude-newer",
38
38
  "npm",
39
+ "pnpm",
40
+ "yarn",
41
+ "bun",
42
+ "cargo",
43
+ "mise",
39
44
  "uv",
40
45
  "pmsec"
41
46
  ]
package/src/cli.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { homedir } from "node:os";
2
+ import { readFileSync } from "node:fs";
2
3
  import * as npm from "./tools/npm.mjs";
3
4
  import * as pnpm from "./tools/pnpm.mjs";
4
5
  import * as yarn from "./tools/yarn.mjs";
@@ -9,6 +10,9 @@ import * as uv from "./tools/uv.mjs";
9
10
 
10
11
  const TOOLS = [npm, pnpm, yarn, bun, cargo, mise, uv];
11
12
  const DEFAULT_MIN = 7;
13
+ export const VERSION = JSON.parse(
14
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
15
+ ).version;
12
16
 
13
17
  const USAGE = `pmsec <command> [options]
14
18
 
@@ -21,6 +25,7 @@ Options:
21
25
  --tool TOOL[,TOOL] Restrict to specific tools (npm,pnpm,yarn,bun,cargo,mise,uv)
22
26
  --min DAYS Minimum acceptable days for check (default ${DEFAULT_MIN})
23
27
  --json Emit JSON output
28
+ -V, --version Show version
24
29
  -h, --help Show this help
25
30
 
26
31
  Examples:
@@ -30,11 +35,12 @@ Examples:
30
35
  `;
31
36
 
32
37
  function parse(argv) {
33
- const opts = { command: null, days: null, min: DEFAULT_MIN, json: false, only: null, help: false };
38
+ const opts = { command: null, days: null, min: DEFAULT_MIN, json: false, only: null, help: false, version: false };
34
39
  const positional = [];
35
40
  for (let i = 0; i < argv.length; i++) {
36
41
  const a = argv[i];
37
42
  if (a === "-h" || a === "--help") opts.help = true;
43
+ else if (a === "-V" || a === "--version") opts.version = true;
38
44
  else if (a === "--json") opts.json = true;
39
45
  else if (a === "--min") opts.min = Number(argv[++i]);
40
46
  else if (a.startsWith("--min=")) opts.min = Number(a.slice(6));
@@ -65,7 +71,12 @@ function preflightWarn(t) {
65
71
  async function gatherStatus(targets, env, home, platform) {
66
72
  return Promise.all(targets.map(async t => {
67
73
  const r = await t.read(env, home, platform);
68
- return { tool: t.name, key: t.key, path: r.path, configured: r.configured, days: r.days, warn: preflightWarn(t) };
74
+ return {
75
+ tool: t.name, key: t.key, path: r.path,
76
+ configured: r.configured, days: r.days,
77
+ extras: r.extras ?? [],
78
+ warn: preflightWarn(t)
79
+ };
69
80
  }));
70
81
  }
71
82
 
@@ -76,17 +87,25 @@ function renderHuman(rows, min) {
76
87
  const value = r.configured ?? "(unset)";
77
88
  const tail = r.warn ? `\n ⚠ ${r.warn}` : "";
78
89
  lines.push(`${status} ${r.tool.padEnd(4)} ${r.key} = ${value} [${r.path}]${tail}`);
90
+ for (const e of r.extras) {
91
+ const exStatus = e.configured === null ? "MISSING" : e.ok ? "OK " : "STALE ";
92
+ const exValue = e.configured ?? "(unset)";
93
+ lines.push(`${exStatus} ${r.tool.padEnd(4)} ${e.key} = ${exValue} [${r.path}]`);
94
+ }
79
95
  }
80
96
  return lines.join("\n") + "\n";
81
97
  }
82
98
 
83
99
  async function runCheck(targets, { min, json }, env, home, platform, out, err) {
84
100
  const rows = await gatherStatus(targets, env, home, platform);
85
- const failing = rows.filter(r => r.days === null || r.days < min);
86
- if (json) out.write(JSON.stringify({ min, rows, ok: failing.length === 0 }, null, 2) + "\n");
101
+ const failingPrimary = rows.filter(r => r.days === null || r.days < min);
102
+ const failingExtras = rows.flatMap(r => r.extras.filter(e => !e.ok));
103
+ const ok = failingPrimary.length === 0 && failingExtras.length === 0;
104
+ if (json) out.write(JSON.stringify({ min, rows, ok }, null, 2) + "\n");
87
105
  else out.write(renderHuman(rows, min));
88
- if (failing.length) err.write(`pmsec: ${failing.length} tool(s) below ${min} days\n`);
89
- return failing.length ? 1 : 0;
106
+ if (failingPrimary.length) err.write(`pmsec: ${failingPrimary.length} tool(s) below ${min} days\n`);
107
+ if (failingExtras.length) err.write(`pmsec: ${failingExtras.length} hardening setting(s) not at safe value\n`);
108
+ return ok ? 0 : 1;
90
109
  }
91
110
 
92
111
  function shQuote(s) { return s ? `'${String(s).replace(/'/g, `'\\''`)}'` : ""; }
@@ -102,7 +121,10 @@ function explainFsError(e, tool) {
102
121
  }
103
122
 
104
123
  async function runSet(targets, days, json, env, home, platform, out, err) {
105
- if (!Number.isInteger(days) || days <= 0) throw new Error(`set requires integer DAYS > 0`);
124
+ if (!Number.isInteger(days) || days <= 0) {
125
+ err.write(`pmsec: set requires integer DAYS > 0\n`);
126
+ return 2;
127
+ }
106
128
  const results = [];
107
129
  const failures = [];
108
130
  const warnings = [];
@@ -162,6 +184,7 @@ export async function run(argv, {
162
184
  let opts;
163
185
  try { opts = parse(argv); }
164
186
  catch (e) { err.write(`pmsec: ${e.message}\n`); return 2; }
187
+ if (opts.version) { out.write(`pmsec ${VERSION}\n`); return 0; }
165
188
  if (opts.help || !opts.command) { out.write(USAGE); return opts.help ? 0 : 2; }
166
189
  let targets;
167
190
  try { targets = selectTools(opts.only); }
package/src/tools/bun.mjs CHANGED
@@ -8,6 +8,7 @@ export const key = "minimumReleaseAge";
8
8
  export const section = "install";
9
9
  export const docs = "https://bun.com/docs/runtime/bunfig#install";
10
10
  export const minBin = [1, 3, 0];
11
+ export const extras = [];
11
12
 
12
13
  export function path(env, home) { return bunConfigPath(env, home); }
13
14
 
@@ -23,7 +24,7 @@ export async function read(env, home) {
23
24
  const raw = await readSafe(p);
24
25
  const value = readKey(raw, key, { section });
25
26
  const seconds = value === null ? null : Number(value);
26
- return { path: p, configured: value, days: seconds === null || Number.isNaN(seconds) ? null : Math.floor(seconds / 86400) };
27
+ return { path: p, configured: value, days: seconds === null || Number.isNaN(seconds) ? null : Math.floor(seconds / 86400), extras: [] };
27
28
  }
28
29
 
29
30
  export async function write(days, env, home) {
@@ -6,6 +6,7 @@ export const name = "cargo";
6
6
  export const key = "minimum-release-age";
7
7
  export const section = "install";
8
8
  export const docs = "https://rust-lang.github.io/rfcs/3801-package-cooldown.html";
9
+ export const extras = [];
9
10
 
10
11
  export function path(env, home) { return cargoConfigPath(env, home); }
11
12
 
@@ -21,7 +22,7 @@ export async function read(env, home) {
21
22
  const p = path(env, home);
22
23
  const raw = await readSafe(p);
23
24
  const value = readKey(raw, key, { section });
24
- return { path: p, configured: value, days: parseDays(value) };
25
+ return { path: p, configured: value, days: parseDays(value), extras: [] };
25
26
  }
26
27
 
27
28
  export async function write(days, env, home) {
@@ -1,12 +1,16 @@
1
1
  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
+ import { readExtras, applyExtras, removeExtras } from "../util/extras.mjs";
4
5
  import { detectVersion, gte } from "../util/version.mjs";
5
6
  export const name = "mise";
6
7
  export const key = "minimum_release_age";
7
8
  export const section = "settings";
8
9
  export const docs = "https://mise.jdx.dev/configuration/settings.html#minimum_release_age";
9
10
  export const minBin = [2026, 4, 22];
11
+ export const extras = [
12
+ { key: "paranoid", expected: "true", line: "paranoid = true", section }
13
+ ];
10
14
 
11
15
  export function path(env, home, platform) { return miseConfigPath(env, home, platform); }
12
16
 
@@ -33,13 +37,17 @@ export async function read(env, home, platform) {
33
37
  const p = path(env, home, platform);
34
38
  const raw = await readSafe(p);
35
39
  const value = readKey(raw, key, { section });
36
- return { path: p, configured: value, days: parseDays(value) };
40
+ return {
41
+ path: p, configured: value, days: parseDays(value),
42
+ extras: readExtras(raw, extras)
43
+ };
37
44
  }
38
45
 
39
46
  export async function write(days, env, home, platform) {
40
47
  const p = path(env, home, platform);
41
48
  const before = await readSafe(p);
42
- const after = setKey(before, key, `${key} = "${days}d"`, { section });
49
+ let after = setKey(before, key, `${key} = "${days}d"`, { section });
50
+ after = applyExtras(after, extras);
43
51
  await writeAtomic(p, after);
44
52
  return { path: p, before, after };
45
53
  }
@@ -47,7 +55,9 @@ export async function write(days, env, home, platform) {
47
55
  export async function unset(env, home, platform) {
48
56
  const p = path(env, home, platform);
49
57
  const before = await readSafe(p);
50
- const { text: after, removed } = removeKey(before, key, { section });
51
- if (removed) await writeAtomic(p, after);
58
+ const cooldown = removeKey(before, key, { section });
59
+ const ex = removeExtras(cooldown.text, extras);
60
+ const removed = cooldown.removed || ex.removed;
61
+ if (removed) await writeAtomic(p, ex.text);
52
62
  return { path: p, removed };
53
63
  }
package/src/tools/npm.mjs CHANGED
@@ -1,12 +1,16 @@
1
1
  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
+ import { readExtras, applyExtras, removeExtras } from "../util/extras.mjs";
4
5
  import { detectVersion, gte } from "../util/version.mjs";
5
6
 
6
7
  export const name = "npm";
7
8
  export const key = "min-release-age";
8
9
  export const docs = "https://docs.npmjs.com/cli/v11/using-npm/config#min-release-age";
9
10
  export const minBin = [11, 10, 0];
11
+ export const extras = [
12
+ { key: "audit-level", expected: "high", line: "audit-level=high" }
13
+ ];
10
14
 
11
15
  export function path(env, home) { return npmrcPath(env, home); }
12
16
 
@@ -21,13 +25,18 @@ export async function read(env, home) {
21
25
  const p = path(env, home);
22
26
  const raw = await readSafe(p);
23
27
  const value = readKey(raw, key);
24
- return { path: p, configured: value, days: value === null ? null : Number(value) };
28
+ return {
29
+ path: p, configured: value,
30
+ days: value === null ? null : Number(value),
31
+ extras: readExtras(raw, extras)
32
+ };
25
33
  }
26
34
 
27
35
  export async function write(days, env, home) {
28
36
  const p = path(env, home);
29
37
  const before = await readSafe(p);
30
- const after = setKey(before, key, `${key}=${days}`);
38
+ let after = setKey(before, key, `${key}=${days}`);
39
+ after = applyExtras(after, extras);
31
40
  await writeAtomic(p, after);
32
41
  return { path: p, before, after };
33
42
  }
@@ -35,7 +44,9 @@ export async function write(days, env, home) {
35
44
  export async function unset(env, home) {
36
45
  const p = path(env, home);
37
46
  const before = await readSafe(p);
38
- const { text: after, removed } = removeKey(before, key);
39
- if (removed) await writeAtomic(p, after);
47
+ const cooldown = removeKey(before, key);
48
+ const ex = removeExtras(cooldown.text, extras);
49
+ const removed = cooldown.removed || ex.removed;
50
+ if (removed) await writeAtomic(p, ex.text);
40
51
  return { path: p, removed };
41
52
  }
@@ -1,12 +1,17 @@
1
1
  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
+ import { readExtras, applyExtras, removeExtras } from "../util/extras.mjs";
4
5
  import { detectVersion, gte } from "../util/version.mjs";
5
6
 
6
7
  export const name = "pnpm";
7
8
  export const key = "minimum-release-age";
8
9
  export const docs = "https://pnpm.io/settings#minimumreleaseage";
9
10
  export const minBin = [10, 6, 0];
11
+ export const extras = [
12
+ { key: "trust-policy", expected: "no-downgrade", line: "trust-policy=no-downgrade" },
13
+ { key: "block-exotic-subdeps", expected: "true", line: "block-exotic-subdeps=true" }
14
+ ];
10
15
 
11
16
  export function path(env, home) { return npmrcPath(env, home); }
12
17
 
@@ -22,13 +27,18 @@ export async function read(env, home) {
22
27
  const raw = await readSafe(p);
23
28
  const value = readKey(raw, key);
24
29
  const minutes = value === null ? null : Number(value);
25
- return { path: p, configured: value, days: minutes === null || Number.isNaN(minutes) ? null : Math.floor(minutes / (60 * 24)) };
30
+ return {
31
+ path: p, configured: value,
32
+ days: minutes === null || Number.isNaN(minutes) ? null : Math.floor(minutes / (60 * 24)),
33
+ extras: readExtras(raw, extras)
34
+ };
26
35
  }
27
36
 
28
37
  export async function write(days, env, home) {
29
38
  const p = path(env, home);
30
39
  const before = await readSafe(p);
31
- const after = setKey(before, key, `${key}=${days * 24 * 60}`);
40
+ let after = setKey(before, key, `${key}=${days * 24 * 60}`);
41
+ after = applyExtras(after, extras);
32
42
  await writeAtomic(p, after);
33
43
  return { path: p, before, after };
34
44
  }
@@ -36,7 +46,9 @@ export async function write(days, env, home) {
36
46
  export async function unset(env, home) {
37
47
  const p = path(env, home);
38
48
  const before = await readSafe(p);
39
- const { text: after, removed } = removeKey(before, key);
40
- if (removed) await writeAtomic(p, after);
49
+ const cooldown = removeKey(before, key);
50
+ const ex = removeExtras(cooldown.text, extras);
51
+ const removed = cooldown.removed || ex.removed;
52
+ if (removed) await writeAtomic(p, ex.text);
41
53
  return { path: p, removed };
42
54
  }
package/src/tools/uv.mjs CHANGED
@@ -7,6 +7,7 @@ export const name = "uv";
7
7
  export const key = "exclude-newer";
8
8
  export const docs = "https://docs.astral.sh/uv/reference/settings/#exclude-newer";
9
9
  export const minBin = [0, 9, 17];
10
+ export const extras = [];
10
11
 
11
12
  export function path(env, home, platform) { return uvConfigPath(env, home, platform); }
12
13
 
@@ -29,7 +30,7 @@ export async function read(env, home, platform) {
29
30
  const p = path(env, home, platform);
30
31
  const raw = await readSafe(p);
31
32
  const value = readKey(raw, key);
32
- return { path: p, configured: value, days: parseDays(value) };
33
+ return { path: p, configured: value, days: parseDays(value), extras: [] };
33
34
  }
34
35
 
35
36
  export async function write(days, env, home, platform) {
@@ -1,6 +1,7 @@
1
1
  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
+ import { readExtras, applyExtras, removeExtras } from "../util/extras.mjs";
4
5
  import { detectVersion, gte } from "../util/version.mjs";
5
6
 
6
7
  export const name = "yarn";
@@ -8,6 +9,9 @@ export const key = "npmMinimalAgeGate";
8
9
  export const docs = "https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate";
9
10
  export const minBin = [4, 10, 0];
10
11
  const SEP = ":";
12
+ export const extras = [
13
+ { key: "enableHardenedMode", expected: "true", line: "enableHardenedMode: true", sep: SEP }
14
+ ];
11
15
 
12
16
  export function path(env, home) { return yarnrcPath(env, home); }
13
17
 
@@ -30,13 +34,17 @@ export async function read(env, home) {
30
34
  const p = path(env, home);
31
35
  const raw = await readSafe(p);
32
36
  const value = readKey(raw, key, { sep: SEP });
33
- return { path: p, configured: value, days: parseDays(value) };
37
+ return {
38
+ path: p, configured: value, days: parseDays(value),
39
+ extras: readExtras(raw, extras)
40
+ };
34
41
  }
35
42
 
36
43
  export async function write(days, env, home) {
37
44
  const p = path(env, home);
38
45
  const before = await readSafe(p);
39
- const after = setKey(before, key, `${key}: "${days}d"`, { sep: SEP });
46
+ let after = setKey(before, key, `${key}: "${days}d"`, { sep: SEP });
47
+ after = applyExtras(after, extras);
40
48
  await writeAtomic(p, after);
41
49
  return { path: p, before, after };
42
50
  }
@@ -44,7 +52,9 @@ export async function write(days, env, home) {
44
52
  export async function unset(env, home) {
45
53
  const p = path(env, home);
46
54
  const before = await readSafe(p);
47
- const { text: after, removed } = removeKey(before, key, { sep: SEP });
48
- if (removed) await writeAtomic(p, after);
55
+ const cooldown = removeKey(before, key, { sep: SEP });
56
+ const ex = removeExtras(cooldown.text, extras);
57
+ const removed = cooldown.removed || ex.removed;
58
+ if (removed) await writeAtomic(p, ex.text);
49
59
  return { path: p, removed };
50
60
  }
@@ -0,0 +1,32 @@
1
+ // Per-tool "extras" are fixed-value hardening keys written alongside the main
2
+ // cooldown key on `set`, removed on `unset`, and validated on `check`.
3
+ // Each entry: { key, expected, line, sep?, section? } — `line` is the verbatim
4
+ // text to write (handles per-format quoting/typing); `expected` is the bare
5
+ // value compared against readKey output.
6
+ import { readKey, setKey, removeKey } from "./lines.mjs";
7
+
8
+ export function readExtras(raw, extras) {
9
+ return extras.map(e => {
10
+ const cur = readKey(raw, e.key, { sep: e.sep, section: e.section });
11
+ return { key: e.key, configured: cur, expected: e.expected, ok: cur === e.expected };
12
+ });
13
+ }
14
+
15
+ export function applyExtras(text, extras) {
16
+ let out = text;
17
+ for (const e of extras) {
18
+ out = setKey(out, e.key, e.line, { sep: e.sep, section: e.section });
19
+ }
20
+ return out;
21
+ }
22
+
23
+ export function removeExtras(text, extras) {
24
+ let out = text;
25
+ let removed = false;
26
+ for (const e of extras) {
27
+ const r = removeKey(out, e.key, { sep: e.sep, section: e.section });
28
+ if (r.removed) removed = true;
29
+ out = r.text;
30
+ }
31
+ return { text: out, removed };
32
+ }