pmsec 0.7.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmsec",
3
- "version": "0.7.0",
3
+ "version": "0.9.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,7 +12,7 @@ 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;
@@ -30,7 +31,7 @@ Commands:
30
31
 
31
32
  Options:
32
33
  --tool TOOL[,TOOL] Restrict to specific tools (npm,pnpm,yarn,bun,cargo,mise,uv)
33
- --days N Override cooldown days (default 3)
34
+ --days N Override cooldown days (default 1)
34
35
  --force Overwrite stricter existing cooldowns (otherwise enable is monotonic)
35
36
  --json Emit JSON output
36
37
  -V, --version Show version
@@ -79,20 +80,20 @@ function selectTools(only) {
79
80
  return found;
80
81
  }
81
82
 
82
- function preflightWarn(t) {
83
+ function preflightWarn(t, ctx) {
83
84
  if (typeof t.preflight !== "function") return null;
84
- const pf = t.preflight();
85
+ const pf = t.preflight(ctx);
85
86
  return pf?.message ?? null;
86
87
  }
87
88
 
88
- async function gatherStatus(targets, env, home, platform) {
89
+ async function gatherStatus(targets, ctx) {
89
90
  return Promise.all(targets.map(async t => {
90
- const r = await t.read(env, home, platform);
91
+ const r = await t.read(ctx);
91
92
  return {
92
93
  tool: t.name, key: t.key, path: r.path,
93
94
  configured: r.configured, days: r.days,
94
95
  extras: r.extras ?? [],
95
- warn: preflightWarn(t)
96
+ warn: preflightWarn(t, ctx)
96
97
  };
97
98
  }));
98
99
  }
@@ -113,8 +114,8 @@ function renderHuman(rows, min) {
113
114
  return lines.join("\n") + "\n";
114
115
  }
115
116
 
116
- async function runCheck(targets, { json, days }, env, home, platform, out, err) {
117
- const rows = await gatherStatus(targets, env, home, platform);
117
+ async function runCheck(targets, { json, days }, ctx, out, err) {
118
+ const rows = await gatherStatus(targets, ctx);
118
119
  const failingPrimary = rows.filter(r => r.days === null || r.days < days);
119
120
  const failingExtras = rows.flatMap(r => r.extras.filter(e => !e.ok));
120
121
  const ok = failingPrimary.length === 0 && failingExtras.length === 0;
@@ -125,41 +126,38 @@ async function runCheck(targets, { json, days }, env, home, platform, out, err)
125
126
  return ok ? 0 : 1;
126
127
  }
127
128
 
128
- function shQuote(s) { return s ? `'${String(s).replace(/'/g, `'\\''`)}'` : ""; }
129
-
130
129
  function explainFsError(e, tool) {
131
130
  if (e?.code === "EACCES" || e?.code === "EPERM") {
132
131
  const p = e.path ?? "";
133
- const q = shQuote(p);
132
+ const q = p ? shellQuote(p) : "";
134
133
  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
134
  }
136
135
  if (e?.code === "EROFS") return `${tool}: ${e.path ?? ""} is on a read-only filesystem (EROFS).`;
137
136
  return `${tool}: ${e?.message ?? e}`;
138
137
  }
139
138
 
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 });
139
+ async function runEnable(targets, json, days, force, ctx, out, err) {
140
+ // Each tool writes a different config file; safe to run in parallel.
141
+ // Promise.all preserves input order in the results array.
142
+ const results = await Promise.all(targets.map(async t => {
143
+ const warn = preflightWarn(t, ctx);
147
144
  try {
148
145
  let effective = days;
149
146
  let kept = false;
150
147
  if (!force) {
151
- const current = await t.read(env, home, platform);
148
+ const current = await t.read(ctx);
152
149
  const currentDays = current.days ?? 0;
153
150
  effective = Math.max(currentDays, days);
154
151
  kept = currentDays >= days && currentDays > 0;
155
152
  }
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 });
153
+ const r = await t.write(effective, ctx);
154
+ return { tool: t.name, path: r.path, days: effective, requested: days, kept, forced: force, ok: true, warn };
158
155
  } 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 });
156
+ return { tool: t.name, path: e?.path ?? null, days, requested: days, kept: false, forced: force, ok: false, error: explainFsError(e, t.name), warn };
161
157
  }
162
- }
158
+ }));
159
+ const failures = results.filter(r => !r.ok).map(r => ({ tool: r.tool, error: r.error }));
160
+ const warnings = results.filter(r => r.warn).map(r => ({ tool: r.tool, warn: r.warn }));
163
161
  if (json) out.write(JSON.stringify({ enabled: true, bundleDays: days, results, warnings, ok: failures.length === 0 }, null, 2) + "\n");
164
162
  else for (const r of results) {
165
163
  if (r.ok) {
@@ -176,18 +174,16 @@ async function runEnable(targets, json, days, force, env, home, platform, out, e
176
174
  return failures.length ? 1 : 0;
177
175
  }
178
176
 
179
- async function runDisable(targets, json, env, home, platform, out, err) {
180
- const results = [];
181
- const failures = [];
182
- for (const t of targets) {
177
+ async function runDisable(targets, json, ctx, out, err) {
178
+ const results = await Promise.all(targets.map(async t => {
183
179
  try {
184
- const r = await t.unset(env, home, platform);
185
- results.push({ tool: t.name, path: r.path, removed: r.removed, ok: true });
180
+ const r = await t.unset(ctx);
181
+ return { tool: t.name, path: r.path, removed: r.removed, ok: true };
186
182
  } 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) });
183
+ return { tool: t.name, path: e?.path ?? null, removed: false, ok: false, error: explainFsError(e, t.name) };
189
184
  }
190
- }
185
+ }));
186
+ const failures = results.filter(r => !r.ok).map(r => ({ tool: r.tool, error: r.error }));
191
187
  if (json) out.write(JSON.stringify({ results, ok: failures.length === 0 }, null, 2) + "\n");
192
188
  else for (const r of results) {
193
189
  if (!r.ok) out.write(`FAIL ${r.tool.padEnd(4)} ${r.error}\n`);
@@ -212,10 +208,11 @@ export async function run(argv, {
212
208
  let targets;
213
209
  try { targets = selectTools(opts.only); }
214
210
  catch (e) { err.write(`pmsec: ${e.message}\n`); return 2; }
211
+ const ctx = { env, home, platform };
215
212
  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);
213
+ if (opts.command === "check") return await runCheck(targets, opts, ctx, out, err);
214
+ if (opts.command === "enable") return await runEnable(targets, opts.json, opts.days, opts.force, ctx, out, err);
215
+ if (opts.command === "disable") return await runDisable(targets, opts.json, ctx, out, err);
219
216
  err.write(`pmsec: unknown command "${opts.command}"\n`);
220
217
  return 2;
221
218
  } catch (e) {
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);
@@ -1,4 +1,4 @@
1
- import { npmrcPath } from "../util/paths.mjs";
1
+ import { pnpmRcPath } 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";
@@ -9,33 +9,42 @@ 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
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`.
12
+ // Once detected, an absent line in the rc file is still effectively in force,
13
+ // so `read()` reports it as ok with `defaultEnforced: true`.
14
14
  export const extras = [
15
15
  { key: "trust-policy", expected: "no-downgrade", line: "trust-policy=no-downgrade" },
16
16
  { key: "block-exotic-subdeps", expected: "true", line: "block-exotic-subdeps=true", defaultSinceMajor: 11 },
17
17
  { key: "strict-dep-builds", expected: "true", line: "strict-dep-builds=true" }
18
18
  ];
19
19
 
20
- export function path(env, home) { return npmrcPath(env, home); }
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) {
34
- const p = path(env, home);
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) {
51
60
  };
52
61
  }
53
62
 
54
- export async function write(days, env, home) {
55
- const p = path(env, home);
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) {
64
- const p = path(env, home);
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;
@@ -5,6 +5,20 @@ export function npmrcPath(env = process.env, home = homedir()) {
5
5
  return env.NPM_CONFIG_USERCONFIG || join(home, ".npmrc");
6
6
  }
7
7
 
8
+ // pnpm reads its global rc from a dedicated location separate from ~/.npmrc.
9
+ // Writing pnpm-only keys here keeps npm from warning (and, in npm 12, erroring)
10
+ // about unknown user config. pnpm itself respects XDG_CONFIG_HOME on every OS.
11
+ export function pnpmRcPath(env = process.env, home = homedir(), platform = process.platform) {
12
+ if (env.PMSEC_PNPM_CONFIG_FILE) return env.PMSEC_PNPM_CONFIG_FILE;
13
+ if (env.XDG_CONFIG_HOME) return join(env.XDG_CONFIG_HOME, "pnpm", "rc");
14
+ if (platform === "darwin") return join(home, "Library", "Preferences", "pnpm", "rc");
15
+ if (platform === "win32") {
16
+ const base = env.LOCALAPPDATA || join(home, "AppData", "Local");
17
+ return join(base, "pnpm", "config", "rc");
18
+ }
19
+ return join(home, ".config", "pnpm", "rc");
20
+ }
21
+
8
22
  export function uvConfigPath(env = process.env, home = homedir(), platform = process.platform) {
9
23
  if (env.UV_CONFIG_FILE) return env.UV_CONFIG_FILE;
10
24
  if (platform === "win32") {
@@ -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
+ }