pmsec 2026.11.3 → 2026.11.4

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": "2026.11.3",
3
+ "version": "2026.11.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/cli.mjs CHANGED
@@ -188,8 +188,10 @@ function runDoctor(targets, json, ctx, out) {
188
188
  platform: ctx.platform,
189
189
  uid, euid, username,
190
190
  home: ctx.home,
191
- pmsecHome: ctx.env.PMSEC_HOME ?? null,
192
- pmsecHomeSource: ctx.env.PMSEC_HOME ? "PMSEC_HOME" : "HOME",
191
+ // This port resolves paths from HOME only; PMSEC_HOME is a bash/powershell
192
+ // feature (it needs the root->owner chown handback those ports implement).
193
+ pmsecHome: null,
194
+ pmsecHomeSource: "HOME",
193
195
  tools,
194
196
  ok: okFlag,
195
197
  };
@@ -201,7 +203,7 @@ function runDoctor(targets, json, ctx, out) {
201
203
  out.write(` platform : ${ctx.platform}\n`);
202
204
  if (isPosix) out.write(` uid/euid : ${uid}/${euid}${username ? ` (${username})` : ""}\n`);
203
205
  out.write(` HOME : ${ctx.home}\n`);
204
- out.write(` PMSEC_HOME : ${ctx.env.PMSEC_HOME ?? "(unset — using HOME)"}\n\n`);
206
+ out.write(` PMSEC_HOME : (not supported by this port — using HOME)\n\n`);
205
207
  for (const t of tools) {
206
208
  const flag = t.parentWritable ? "ok " : "BLOCK ";
207
209
  const ownerSuffix = t.exists && t.owner !== null ? ` (uid=${t.owner})` : "";
@@ -299,6 +301,9 @@ export async function run(argv, {
299
301
  try { targets = selectTools(opts.only); }
300
302
  catch (e) { err.write(`pmsec: ${e.message}\n`); return 2; }
301
303
  const ctx = { env, home, platform };
304
+ if (env.PMSEC_HOME) {
305
+ err.write(`pmsec: PMSEC_HOME is ignored by the node port; paths resolve from HOME (${home}). Use the bash port for root-context redirection.\n`);
306
+ }
302
307
  try {
303
308
  if (opts.mode === "check") return await runCheck(targets, opts, ctx, out, err);
304
309
  if (opts.mode === "disable") return await runDisable(targets, opts.json, ctx, out, err);
package/src/tools/npm.mjs CHANGED
@@ -29,9 +29,11 @@ export async function read(ctx) {
29
29
  const p = path(ctx);
30
30
  const raw = await readSafe(p);
31
31
  const value = readKey(raw, key);
32
+ // npm stores whole days. Anything else is reported as unparseable (null) so
33
+ // `--check` fails instead of letting NaN slip past the `< days` comparison.
32
34
  return {
33
35
  path: p, configured: value,
34
- days: value === null ? null : Number(value),
36
+ days: value !== null && /^\d+$/.test(value) ? Number(value) : null,
35
37
  extras: readExtras(raw, extras)
36
38
  };
37
39
  }
package/src/util/io.mjs CHANGED
@@ -1,45 +1,26 @@
1
- import { mkdir, readFile, writeFile, rename, lstat, unlink, open } from "node:fs/promises";
2
- import { dirname, resolve, sep } from "node:path";
1
+ import { mkdir, readFile, rename, lstat, unlink, open } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
3
  import { existsSync } from "node:fs";
4
- import { spawnSync } from "node:child_process";
5
4
  import { randomBytes } from "node:crypto";
6
- import { homedir } from "node:os";
7
5
 
8
6
  export async function readSafe(path) {
9
7
  // Editors on Windows may prepend a BOM; it would hide the first header or key.
10
- try { return (await readFile(path, "utf8")).replace(/^\uFEFF/, ""); }
8
+ try { return (await readFile(path, "utf8")).replace(/^/, ""); }
11
9
  catch (e) { if (e.code === "ENOENT") return ""; throw e; }
12
10
  }
13
11
 
14
- function isPermErr(e) { return e?.code === "EACCES" || e?.code === "EPERM"; }
15
-
16
12
  async function isSymlink(p) {
17
13
  try { return (await lstat(p)).isSymbolicLink(); }
18
14
  catch { return false; }
19
15
  }
20
16
 
21
- function underHome(p, home) {
22
- const r = resolve(p);
23
- const h = resolve(home);
24
- return r === h || r.startsWith(h + sep);
25
- }
26
-
27
17
  export function shellQuote(s) { return `'${String(s).replace(/'/g, `'\\''`)}'`; }
28
18
 
29
- async function reclaim(path, home, err) {
30
- if (process.platform === "win32" || typeof process.getuid !== "function") return false;
31
- if (!existsSync(path)) return false;
32
- if (await isSymlink(path)) {
33
- err.write(`pmsec: refusing to chown symlink ${path}; remove or replace it manually.\n`);
34
- return false;
35
- }
36
- if (!underHome(path, home)) {
37
- err.write(`pmsec: refusing to chown ${path} outside HOME (${home}); fix ownership manually.\n`);
38
- return false;
39
- }
40
- err.write(`pmsec: ${path} not writable; running \`sudo chown -h $(id -u):$(id -g) ${shellQuote(path)}\`. You may be prompted for your password.\n`);
41
- const r = spawnSync("sudo", ["chown", "-h", `${process.getuid()}:${process.getgid()}`, path], { stdio: "inherit" });
42
- return r.status === 0;
19
+ function symlinkError(what, p) {
20
+ const e = new Error(`refusing to write ${what} through symlink ${p}`);
21
+ e.code = "ELOOP";
22
+ e.path = p;
23
+ return e;
43
24
  }
44
25
 
45
26
  async function atomicReplace(path, text) {
@@ -59,24 +40,18 @@ async function atomicReplace(path, text) {
59
40
  }
60
41
  }
61
42
 
62
- export async function writeAtomic(path, text, { backup = true, home = homedir(), err = process.stderr } = {}) {
43
+ // Permission errors propagate to the caller, which prints a chown hint. pmsec
44
+ // never escalates privileges itself: an unwritable config under HOME is an
45
+ // operator decision (or a symptom worth seeing), not something to sudo past.
46
+ export async function writeAtomic(path, text, { backup = true } = {}) {
63
47
  await mkdir(dirname(path), { recursive: true });
64
- if (existsSync(path) && await isSymlink(path)) {
65
- const e = new Error(`refusing to write through symlink ${path}`);
66
- e.code = "ELOOP";
67
- throw e;
68
- }
69
- if (backup && existsSync(path) && !existsSync(path + ".bak")) {
70
- try { await atomicReplace(path + ".bak", await readFile(path, "utf8")); }
71
- catch (e) {
72
- const target = existsSync(path + ".bak") ? path + ".bak" : path;
73
- if (!isPermErr(e) || !(await reclaim(target, home, err))) throw e;
74
- await atomicReplace(path + ".bak", await readFile(path, "utf8"));
75
- }
76
- }
77
- try { await atomicReplace(path, text); }
78
- catch (e) {
79
- if (!isPermErr(e) || !(await reclaim(path, home, err))) throw e;
80
- await atomicReplace(path, text);
48
+ if (existsSync(path) && await isSymlink(path)) throw symlinkError("config", path);
49
+ const bak = path + ".bak";
50
+ if (backup && existsSync(path) && !existsSync(bak)) {
51
+ // A dangling symlink fails existsSync; rename() would replace the link
52
+ // itself, but refuse anyway so a planted link is visible, not silently clobbered.
53
+ if (await isSymlink(bak)) throw symlinkError("backup", bak);
54
+ await atomicReplace(bak, await readFile(path, "utf8"));
81
55
  }
56
+ await atomicReplace(path, text);
82
57
  }