pmsec 2026.10.1 → 2026.10.5
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 +1 -1
- package/package.json +4 -2
- package/src/cli.mjs +5 -4
- package/src/tools/cargo.mjs +23 -29
- package/src/tools/composer.mjs +82 -0
- package/src/tools/pnpm.mjs +57 -18
- package/src/util/jsonconf.mjs +226 -0
- package/src/util/paths.mjs +24 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ uvx --index https://pypi.org/simple --exclude-newer-package pmsec=2099-01-01 pms
|
|
|
25
25
|
| `pmsec --doctor` | Prints read-only path, ownership, and writability diagnostics. |
|
|
26
26
|
| `pmsec --version` | Prints the version. |
|
|
27
27
|
|
|
28
|
-
Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv,bundler,aube`, `--days N`,
|
|
28
|
+
Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv,bundler,aube,composer`, `--days N`,
|
|
29
29
|
`--force`, `--json`.
|
|
30
30
|
|
|
31
31
|
Write failures include the target path and ownership hint. pmsec never escalates
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmsec",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.5",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"description": "Zero-config install-time supply-chain hardening for npm, pnpm, yarn, bun, cargo, mise, uv, and
|
|
7
|
+
"description": "Zero-config install-time supply-chain hardening for npm, pnpm, yarn, bun, cargo, mise, uv, bundler, aube, and composer.",
|
|
8
8
|
"homepage": "https://github.com/HikaruEgashira/pmsec#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
"cargo",
|
|
44
44
|
"mise",
|
|
45
45
|
"uv",
|
|
46
|
+
"bundler",
|
|
47
|
+
"composer",
|
|
46
48
|
"pmsec"
|
|
47
49
|
]
|
|
48
50
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -11,8 +11,9 @@ import * as mise from "./tools/mise.mjs";
|
|
|
11
11
|
import * as uv from "./tools/uv.mjs";
|
|
12
12
|
import * as bundler from "./tools/bundler.mjs";
|
|
13
13
|
import * as aube from "./tools/aube.mjs";
|
|
14
|
+
import * as composer from "./tools/composer.mjs";
|
|
14
15
|
|
|
15
|
-
const TOOLS = [npm, pnpm, yarn, bun, cargo, mise, uv, bundler, aube];
|
|
16
|
+
const TOOLS = [npm, pnpm, yarn, bun, cargo, mise, uv, bundler, aube, composer];
|
|
16
17
|
// Default cooldown for the hardening bundle. Override per-invocation with
|
|
17
18
|
// `--days N`; the default tracks the safest value we'd recommend.
|
|
18
19
|
const BUNDLE_DAYS = 1;
|
|
@@ -23,7 +24,7 @@ export const VERSION = JSON.parse(
|
|
|
23
24
|
const USAGE = `pmsec [options]
|
|
24
25
|
|
|
25
26
|
Zero-config install-time supply-chain hardening across npm, pnpm, yarn,
|
|
26
|
-
bun, cargo, mise, uv, bundler, aube. Default action enables every safe-by-default
|
|
27
|
+
bun, cargo, mise, uv, bundler, aube, composer. Default action enables every safe-by-default
|
|
27
28
|
key each tool exposes (cooldown, audit-level, trust-policy, hardened mode,
|
|
28
29
|
attestation re-verification, ...). No knobs.
|
|
29
30
|
|
|
@@ -31,7 +32,7 @@ Options:
|
|
|
31
32
|
--check Verify the bundle is in place (exit 1 if anything missing)
|
|
32
33
|
--disable Remove the hardening bundle from selected tools
|
|
33
34
|
--doctor Diagnose effective paths/owner/uid (read-only; for unattended-deployment debugging)
|
|
34
|
-
--tool TOOL[,TOOL] Restrict to specific tools (npm,pnpm,yarn,bun,cargo,mise,uv,bundler,aube)
|
|
35
|
+
--tool TOOL[,TOOL] Restrict to specific tools (npm,pnpm,yarn,bun,cargo,mise,uv,bundler,aube,composer)
|
|
35
36
|
--days N Override cooldown days (default 1)
|
|
36
37
|
--force Overwrite stricter existing cooldowns (otherwise enable is monotonic)
|
|
37
38
|
--json Emit JSON output
|
|
@@ -99,7 +100,7 @@ async function gatherStatus(targets, ctx) {
|
|
|
99
100
|
return Promise.all(targets.map(async t => {
|
|
100
101
|
const r = await t.read(ctx);
|
|
101
102
|
return {
|
|
102
|
-
tool: t.name, key: t.key, path: r.path,
|
|
103
|
+
tool: t.name, key: r.key ?? t.key, path: r.path,
|
|
103
104
|
configured: r.configured, days: r.days,
|
|
104
105
|
extras: r.extras ?? [],
|
|
105
106
|
warn: preflightWarn(t, ctx)
|
package/src/tools/cargo.mjs
CHANGED
|
@@ -1,55 +1,49 @@
|
|
|
1
1
|
import { cargoConfigPath } 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
5
|
|
|
5
6
|
export const name = "cargo";
|
|
6
|
-
export const key = "
|
|
7
|
-
export const section = "
|
|
8
|
-
export const docs = "https://rust-lang.
|
|
7
|
+
export const key = "global-min-publish-age";
|
|
8
|
+
export const section = "registry";
|
|
9
|
+
export const docs = "https://doc.rust-lang.org/nightly/cargo/reference/config.html#registryglobal-min-publish-age";
|
|
10
|
+
export const minBin = [1, 100, 0];
|
|
9
11
|
export const extras = [];
|
|
10
12
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
const
|
|
13
|
+
// pmsec <= 2026.10.1 wrote this key, but cargo never supported it. Remove it
|
|
14
|
+
// when we touch the file so users do not mistake it for an enforced setting.
|
|
15
|
+
const legacy = { key: "minimum-release-age", section: "install" };
|
|
14
16
|
|
|
15
17
|
export function path(ctx) { return cargoConfigPath(ctx.env, ctx.home); }
|
|
16
18
|
|
|
17
19
|
function parseDays(value) {
|
|
18
20
|
if (value === null) return null;
|
|
19
|
-
const m = value.match(/^"?\s*(\d+)\s*(d|days?|w|weeks?)\s*"?$/i);
|
|
21
|
+
const m = value.match(/^"?\s*(\d+)\s*(d|days?|w|weeks?|months?)\s*"?$/i);
|
|
20
22
|
if (!m) return null;
|
|
21
23
|
const n = Number(m[1]);
|
|
22
|
-
|
|
24
|
+
if (/^(w|weeks?)$/i.test(m[2])) return n * 7;
|
|
25
|
+
if (/^(months?)$/i.test(m[2])) return n * 30;
|
|
26
|
+
return n;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
if (/^weeks?$/i.test(m[2])) return n * 7;
|
|
31
|
-
if (/^months?$/i.test(m[2])) return n * 30;
|
|
32
|
-
return n;
|
|
29
|
+
export function preflight(ctx) {
|
|
30
|
+
const v = detectVersion("cargo", ["--version"], { env: ctx.env, overrideKey: "PMSEC_CARGO_VERSION" });
|
|
31
|
+
if (v === null) return { ok: true, message: null };
|
|
32
|
+
if (gte(v, minBin)) return { ok: true, version: v.raw, message: null };
|
|
33
|
+
return { ok: true, warn: true, version: v.raw, message: `cargo ${v.raw} < ${minBin.join(".")}: global-min-publish-age is silently ignored. Upgrade cargo to enforce the cooldown.` };
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export async function read(ctx) {
|
|
36
37
|
const p = path(ctx);
|
|
37
38
|
const raw = await readSafe(p);
|
|
38
39
|
const value = readKey(raw, key, { section });
|
|
39
|
-
|
|
40
|
-
const cur = readKey(raw, publishAge.key, { section: publishAge.section });
|
|
41
|
-
const want = days ?? 1;
|
|
42
|
-
const curDays = parsePublishAgeDays(cur);
|
|
43
|
-
return {
|
|
44
|
-
path: p, configured: value, days,
|
|
45
|
-
extras: [{ key: publishAge.key, configured: cur, expected: `"${want} days"`, ok: curDays !== null && curDays >= want }],
|
|
46
|
-
};
|
|
40
|
+
return { path: p, configured: value, days: parseDays(value), extras: [] };
|
|
47
41
|
}
|
|
48
42
|
|
|
49
43
|
export async function write(days, ctx) {
|
|
50
44
|
const p = path(ctx);
|
|
51
|
-
let text = setKey(await readSafe(p), key, `${key} = "${days}
|
|
52
|
-
text =
|
|
45
|
+
let text = setKey(await readSafe(p), key, `${key} = "${days} days"`, { section });
|
|
46
|
+
text = removeKey(text, legacy.key, { section: legacy.section }).text;
|
|
53
47
|
await writeAtomic(p, text);
|
|
54
48
|
return { path: p };
|
|
55
49
|
}
|
|
@@ -58,8 +52,8 @@ export async function unset(ctx) {
|
|
|
58
52
|
const p = path(ctx);
|
|
59
53
|
const before = await readSafe(p);
|
|
60
54
|
const cooldown = removeKey(before, key, { section });
|
|
61
|
-
const
|
|
62
|
-
const removed = cooldown.removed ||
|
|
63
|
-
if (removed) await writeAtomic(p,
|
|
55
|
+
const old = removeKey(cooldown.text, legacy.key, { section: legacy.section });
|
|
56
|
+
const removed = cooldown.removed || old.removed;
|
|
57
|
+
if (removed) await writeAtomic(p, old.text);
|
|
64
58
|
return { path: p, removed };
|
|
65
59
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { composerConfigPath } from "../util/paths.mjs";
|
|
2
|
+
import { readSafe, writeAtomic } from "../util/io.mjs";
|
|
3
|
+
import { readJsonKey, setJsonKey, removeJsonKey } from "../util/jsonconf.mjs";
|
|
4
|
+
|
|
5
|
+
export const name = "composer";
|
|
6
|
+
export const key = "minimum-release-age";
|
|
7
|
+
export const section = "config";
|
|
8
|
+
export const docs = "https://getcomposer.org/doc/06-config.md#minimum-release-age";
|
|
9
|
+
export const extras = [
|
|
10
|
+
{ key: "allow-plugins", expected: "false", line: "false", path: ["config", "allow-plugins"] },
|
|
11
|
+
{ key: "secure-http", expected: "true", line: "true", path: ["config", "secure-http"] },
|
|
12
|
+
{ key: "disable-tls", expected: "false", line: "false", path: ["config", "disable-tls"] },
|
|
13
|
+
{ key: "abandoned", expected: '"fail"', line: '"fail"', path: ["config", "audit", "abandoned"] },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Composer is newer than most tools in the bundle; accept the shorter npm
|
|
17
|
+
// spelling too so a config written by hand is still recognized.
|
|
18
|
+
const PRIMARY_ALIASES = [key, "min-release-age", "minimum_release_age", "minimumReleaseAge"];
|
|
19
|
+
|
|
20
|
+
export function path(ctx) { return composerConfigPath(ctx.env, ctx.home, ctx.platform); }
|
|
21
|
+
|
|
22
|
+
function parseDays(value) {
|
|
23
|
+
if (value === null) return null;
|
|
24
|
+
const v = String(value).trim().replace(/^"|"$/g, "").trim();
|
|
25
|
+
const dur = v.match(/^(\d+)\s*(d|days?|w|weeks?)$/i);
|
|
26
|
+
if (dur) {
|
|
27
|
+
const n = Number(dur[1]);
|
|
28
|
+
return /^(w|weeks?)$/i.test(dur[2]) ? n * 7 : n;
|
|
29
|
+
}
|
|
30
|
+
if (!/^\d+$/.test(v)) return null;
|
|
31
|
+
const n = Number(v);
|
|
32
|
+
// Large values are seconds or minutes (the units Composer uses for its
|
|
33
|
+
// other time settings); small values are days.
|
|
34
|
+
if (n >= 86400) return Math.floor(n / 86400);
|
|
35
|
+
if (n >= 1440 && n % 1440 === 0) return n / 1440;
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function read(ctx) {
|
|
40
|
+
const p = path(ctx);
|
|
41
|
+
const raw = await readSafe(p);
|
|
42
|
+
let configured = null;
|
|
43
|
+
for (const candidate of PRIMARY_ALIASES) {
|
|
44
|
+
const v = readJsonKey(raw, ["config", candidate]);
|
|
45
|
+
if (v !== null) { configured = v; break; }
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
path: p, configured, days: parseDays(configured),
|
|
49
|
+
extras: extras.map(e => {
|
|
50
|
+
const cur = readJsonKey(raw, e.path);
|
|
51
|
+
const safeValues = e.safeValues ?? [e.expected];
|
|
52
|
+
return { key: e.key, configured: cur, expected: e.expected, ok: cur !== null && safeValues.includes(cur) };
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function write(days, ctx) {
|
|
58
|
+
const p = path(ctx);
|
|
59
|
+
let text = await readSafe(p);
|
|
60
|
+
text = setJsonKey(text, ["config", key], `${days}`);
|
|
61
|
+
for (const e of extras) text = setJsonKey(text, e.path, e.line);
|
|
62
|
+
await writeAtomic(p, text);
|
|
63
|
+
return { path: p };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function unset(ctx) {
|
|
67
|
+
const p = path(ctx);
|
|
68
|
+
let text = await readSafe(p);
|
|
69
|
+
let removed = false;
|
|
70
|
+
for (const candidate of PRIMARY_ALIASES) {
|
|
71
|
+
const r = removeJsonKey(text, ["config", candidate]);
|
|
72
|
+
text = r.text;
|
|
73
|
+
if (r.removed) removed = true;
|
|
74
|
+
}
|
|
75
|
+
for (const e of extras) {
|
|
76
|
+
const r = removeJsonKey(text, e.path);
|
|
77
|
+
text = r.text;
|
|
78
|
+
if (r.removed) removed = true;
|
|
79
|
+
}
|
|
80
|
+
if (removed) await writeAtomic(p, text);
|
|
81
|
+
return { path: p, removed };
|
|
82
|
+
}
|
package/src/tools/pnpm.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pnpmRcPath } from "../util/paths.mjs";
|
|
1
|
+
import { pnpmRcPath, pnpmConfigPath } 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";
|
|
@@ -8,21 +8,38 @@ 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
|
+
// pnpm >= 11 only reads pnpm-specific settings from the camelCase YAML config.
|
|
12
|
+
// We keep writing the kebab-case rc file for pnpm 10 and mirror every setting
|
|
13
|
+
// into config.yaml for pnpm 11+. `read()` switches files based on the detected
|
|
14
|
+
// major version so `--check` validates the config the running pnpm actually
|
|
15
|
+
// reads.
|
|
16
|
+
const yamlKey = "minimumReleaseAge";
|
|
11
17
|
// `defaultSinceMajor`: pnpm major version where the value became the default.
|
|
12
|
-
// Once detected, an absent line in the
|
|
13
|
-
// so `read()` reports it as ok with `defaultEnforced: true`.
|
|
18
|
+
// Once detected, an absent line in the active config is still effectively in
|
|
19
|
+
// force, so `read()` reports it as ok with `defaultEnforced: true`.
|
|
14
20
|
export const extras = [
|
|
15
|
-
{ key: "trust-policy", expected: "no-downgrade", line: "trust-policy=no-downgrade" },
|
|
16
|
-
{ key: "block-exotic-subdeps", expected: "true", line: "block-exotic-subdeps=true", defaultSinceMajor: 11 },
|
|
17
|
-
{ key: "strict-dep-builds", expected: "true", line: "strict-dep-builds=true" },
|
|
18
|
-
{ key: "verify-deps-before-run", expected: "error", line: "verify-deps-before-run=error" },
|
|
19
|
-
{ key: "minimum-release-age-strict", expected: "true", line: "minimum-release-age-strict=true" },
|
|
20
|
-
{ key: "dangerously-allow-all-builds", expected: "false", line: "dangerously-allow-all-builds=false" },
|
|
21
|
-
{ key: "trust-lockfile", expected: "false", line: "trust-lockfile=false" },
|
|
22
|
-
{ key: "minimum-release-age-ignore-missing-time", expected: "false", line: "minimum-release-age-ignore-missing-time=false" },
|
|
21
|
+
{ key: "trust-policy", yamlKey: "trustPolicy", expected: "no-downgrade", line: "trust-policy=no-downgrade", yamlLine: "trustPolicy: no-downgrade" },
|
|
22
|
+
{ key: "block-exotic-subdeps", yamlKey: "blockExoticSubdeps", expected: "true", line: "block-exotic-subdeps=true", yamlLine: "blockExoticSubdeps: true", defaultSinceMajor: 11 },
|
|
23
|
+
{ key: "strict-dep-builds", yamlKey: "strictDepBuilds", expected: "true", line: "strict-dep-builds=true", yamlLine: "strictDepBuilds: true" },
|
|
24
|
+
{ key: "verify-deps-before-run", yamlKey: "verifyDepsBeforeRun", expected: "error", line: "verify-deps-before-run=error", yamlLine: "verifyDepsBeforeRun: error" },
|
|
25
|
+
{ key: "minimum-release-age-strict", yamlKey: "minimumReleaseAgeStrict", expected: "true", line: "minimum-release-age-strict=true", yamlLine: "minimumReleaseAgeStrict: true" },
|
|
26
|
+
{ key: "dangerously-allow-all-builds", yamlKey: "dangerouslyAllowAllBuilds", expected: "false", line: "dangerously-allow-all-builds=false", yamlLine: "dangerouslyAllowAllBuilds: false" },
|
|
27
|
+
{ key: "trust-lockfile", yamlKey: "trustLockfile", expected: "false", line: "trust-lockfile=false", yamlLine: "trustLockfile: false" },
|
|
28
|
+
{ key: "minimum-release-age-ignore-missing-time", yamlKey: "minimumReleaseAgeIgnoreMissingTime", expected: "false", line: "minimum-release-age-ignore-missing-time=false", yamlLine: "minimumReleaseAgeIgnoreMissingTime: false" },
|
|
23
29
|
];
|
|
24
30
|
|
|
31
|
+
function yamlExtras() {
|
|
32
|
+
return extras.map(e => ({
|
|
33
|
+
key: e.yamlKey,
|
|
34
|
+
expected: e.expected,
|
|
35
|
+
line: e.yamlLine,
|
|
36
|
+
sep: ":",
|
|
37
|
+
...(e.defaultSinceMajor ? { defaultSinceMajor: e.defaultSinceMajor } : {}),
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
export function path(ctx) { return pnpmRcPath(ctx.env, ctx.home, ctx.platform); }
|
|
42
|
+
export function configPath(ctx) { return pnpmConfigPath(ctx.env, ctx.home, ctx.platform); }
|
|
26
43
|
|
|
27
44
|
// Cache `pnpm --version` for the lifetime of the process. preflight() and
|
|
28
45
|
// read() both want it; without memoization a single `pmsec check` spawns
|
|
@@ -37,6 +54,14 @@ function pnpmVersion(ctx) {
|
|
|
37
54
|
return v;
|
|
38
55
|
}
|
|
39
56
|
|
|
57
|
+
function activeConfig(ctx) {
|
|
58
|
+
const v = pnpmVersion(ctx);
|
|
59
|
+
if (v && v.major >= 11) {
|
|
60
|
+
return { path: configPath(ctx), key: yamlKey, sep: ":", extras: yamlExtras() };
|
|
61
|
+
}
|
|
62
|
+
return { path: path(ctx), key, sep: "=", extras };
|
|
63
|
+
}
|
|
64
|
+
|
|
40
65
|
export function preflight(ctx) {
|
|
41
66
|
const v = pnpmVersion(ctx);
|
|
42
67
|
if (v === null) return { ok: true, message: null };
|
|
@@ -45,13 +70,13 @@ export function preflight(ctx) {
|
|
|
45
70
|
}
|
|
46
71
|
|
|
47
72
|
export async function read(ctx) {
|
|
48
|
-
const
|
|
49
|
-
const raw = await readSafe(
|
|
50
|
-
const value = readKey(raw, key);
|
|
73
|
+
const active = activeConfig(ctx);
|
|
74
|
+
const raw = await readSafe(active.path);
|
|
75
|
+
const value = readKey(raw, active.key, { sep: active.sep });
|
|
51
76
|
const minutes = value === null ? null : Number(value);
|
|
52
77
|
const v = pnpmVersion(ctx);
|
|
53
|
-
const defaults = new Map(extras.filter(x => x.defaultSinceMajor).map(x => [x.key, x.defaultSinceMajor]));
|
|
54
|
-
const evaluated = readExtras(raw, extras).map(e => {
|
|
78
|
+
const defaults = new Map(active.extras.filter(x => x.defaultSinceMajor).map(x => [x.key, x.defaultSinceMajor]));
|
|
79
|
+
const evaluated = readExtras(raw, active.extras).map(e => {
|
|
55
80
|
const def = defaults.get(e.key);
|
|
56
81
|
if (e.configured === null && def && v && v.major >= def) {
|
|
57
82
|
return { ...e, ok: true, defaultEnforced: true };
|
|
@@ -59,7 +84,7 @@ export async function read(ctx) {
|
|
|
59
84
|
return e;
|
|
60
85
|
});
|
|
61
86
|
return {
|
|
62
|
-
path:
|
|
87
|
+
path: active.path, key: active.key, configured: value,
|
|
63
88
|
days: minutes === null || Number.isNaN(minutes) ? null : Math.floor(minutes / (60 * 24)),
|
|
64
89
|
extras: evaluated
|
|
65
90
|
};
|
|
@@ -70,6 +95,11 @@ export async function write(days, ctx) {
|
|
|
70
95
|
let text = setKey(await readSafe(p), key, `${key}=${days * 24 * 60}`);
|
|
71
96
|
text = applyExtras(text, extras);
|
|
72
97
|
await writeAtomic(p, text);
|
|
98
|
+
|
|
99
|
+
const cp = configPath(ctx);
|
|
100
|
+
let yaml = setKey(await readSafe(cp), yamlKey, `${yamlKey}: ${days * 24 * 60}`, { sep: ":" });
|
|
101
|
+
yaml = applyExtras(yaml, yamlExtras());
|
|
102
|
+
await writeAtomic(cp, yaml);
|
|
73
103
|
return { path: p };
|
|
74
104
|
}
|
|
75
105
|
|
|
@@ -78,7 +108,16 @@ export async function unset(ctx) {
|
|
|
78
108
|
const before = await readSafe(p);
|
|
79
109
|
const cooldown = removeKey(before, key);
|
|
80
110
|
const ex = removeExtras(cooldown.text, extras);
|
|
81
|
-
|
|
111
|
+
let removed = cooldown.removed || ex.removed;
|
|
82
112
|
if (removed) await writeAtomic(p, ex.text);
|
|
113
|
+
|
|
114
|
+
const cp = configPath(ctx);
|
|
115
|
+
const yamlBefore = await readSafe(cp);
|
|
116
|
+
const yamlCooldown = removeKey(yamlBefore, yamlKey, { sep: ":" });
|
|
117
|
+
const yamlEx = removeExtras(yamlCooldown.text, yamlExtras());
|
|
118
|
+
if (yamlCooldown.removed || yamlEx.removed) {
|
|
119
|
+
removed = true;
|
|
120
|
+
await writeAtomic(cp, yamlEx.text);
|
|
121
|
+
}
|
|
83
122
|
return { path: p, removed };
|
|
84
123
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Minimal line-oriented JSON object editor.
|
|
2
|
+
//
|
|
3
|
+
// Composer stores its user config as JSON. Pulling in a full JSON round-trip
|
|
4
|
+
// would reorder/reformat unrelated keys (and the bash port has no JSON parser),
|
|
5
|
+
// so pmsec edits the pretty-printed object in place instead. Only the shapes
|
|
6
|
+
// pmsec needs are supported: objects of scalar members, one member per line.
|
|
7
|
+
|
|
8
|
+
function normalizeLines(lines) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const line of lines) {
|
|
11
|
+
let current = "", inString = false, escaped = false;
|
|
12
|
+
for (let i = 0; i < line.length; i++) {
|
|
13
|
+
const ch = line[i];
|
|
14
|
+
if (inString) {
|
|
15
|
+
current += ch;
|
|
16
|
+
if (escaped) escaped = false;
|
|
17
|
+
else if (ch === "\\") escaped = true;
|
|
18
|
+
else if (ch === '"') inString = false;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (ch === '"') { inString = true; current += ch; continue; }
|
|
22
|
+
if (ch === "{" || ch === ",") {
|
|
23
|
+
current += ch;
|
|
24
|
+
if (line.slice(i + 1).trim() !== "") { out.push(current); current = ""; }
|
|
25
|
+
} else if (ch === "}") {
|
|
26
|
+
if (current.trim() !== "") { out.push(current); current = ""; }
|
|
27
|
+
current += ch;
|
|
28
|
+
} else {
|
|
29
|
+
current += ch;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (current.trim() !== "" || out.length === 0) out.push(current);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scanDepth(line, depth) {
|
|
38
|
+
let inString = false, escaped = false;
|
|
39
|
+
for (const ch of line) {
|
|
40
|
+
if (inString) {
|
|
41
|
+
if (escaped) escaped = false;
|
|
42
|
+
else if (ch === "\\") escaped = true;
|
|
43
|
+
else if (ch === '"') inString = false;
|
|
44
|
+
} else if (ch === '"') inString = true;
|
|
45
|
+
else if (ch === "{") depth++;
|
|
46
|
+
else if (ch === "}") depth--;
|
|
47
|
+
}
|
|
48
|
+
return depth;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function depthMaps(lines) {
|
|
52
|
+
const start = [], end = [];
|
|
53
|
+
let depth = 0;
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
start.push(depth);
|
|
56
|
+
depth = scanDepth(line, depth);
|
|
57
|
+
end.push(depth);
|
|
58
|
+
}
|
|
59
|
+
return { start, end };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rootRange(lines, dm) {
|
|
63
|
+
let open = -1;
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
if (lines[i].includes("{")) { open = i; break; }
|
|
66
|
+
}
|
|
67
|
+
if (open < 0) return null;
|
|
68
|
+
const base = dm.start[open];
|
|
69
|
+
const close = dm.end[open] === base
|
|
70
|
+
? open
|
|
71
|
+
: (() => {
|
|
72
|
+
for (let i = open + 1; i < lines.length; i++) if (dm.end[i] === base) return i;
|
|
73
|
+
return -1;
|
|
74
|
+
})();
|
|
75
|
+
if (close < 0) return null;
|
|
76
|
+
return { open, close, start: open + 1, end: close, depth: base + 1 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findMember(lines, dm, range, key) {
|
|
80
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
81
|
+
const re = new RegExp(`^\\s*"${escaped}"\\s*:`);
|
|
82
|
+
for (let i = range.start; i < range.end; i++) {
|
|
83
|
+
if (dm.start[i] === range.depth && re.test(lines[i])) return i;
|
|
84
|
+
}
|
|
85
|
+
return -1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function objectRange(lines, dm, path) {
|
|
89
|
+
let current = rootRange(lines, dm);
|
|
90
|
+
if (!current) return null;
|
|
91
|
+
for (const key of path) {
|
|
92
|
+
const idx = findMember(lines, dm, current, key);
|
|
93
|
+
if (idx < 0) return null;
|
|
94
|
+
let open = -1;
|
|
95
|
+
for (let i = idx; i < current.end; i++) {
|
|
96
|
+
if (lines[i].includes("{")) { open = i; break; }
|
|
97
|
+
}
|
|
98
|
+
if (open < 0) return null;
|
|
99
|
+
const base = dm.start[idx];
|
|
100
|
+
let close = -1;
|
|
101
|
+
if (dm.end[open] === base) close = open;
|
|
102
|
+
else for (let i = open + 1; i < current.end; i++) if (dm.end[i] === base) { close = i; break; }
|
|
103
|
+
if (close < 0) return null;
|
|
104
|
+
current = { open, close, start: open + 1, end: close, depth: base + 1 };
|
|
105
|
+
}
|
|
106
|
+
return current;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function indent(level) { return " ".repeat(level); }
|
|
110
|
+
|
|
111
|
+
function lastContentLine(lines, range) {
|
|
112
|
+
for (let i = range.end - 1; i >= range.start; i--) {
|
|
113
|
+
if (lines[i].trim() !== "") return i;
|
|
114
|
+
}
|
|
115
|
+
return -1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function ensureComma(lines, range) {
|
|
119
|
+
const last = lastContentLine(lines, range);
|
|
120
|
+
if (last >= 0 && !/,\s*$/.test(lines[last])) lines[last] += ",";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function expandEmptyObject(lines, range) {
|
|
124
|
+
if (range.open !== range.close) return lines;
|
|
125
|
+
const line = lines[range.open];
|
|
126
|
+
const openIdx = line.indexOf("{");
|
|
127
|
+
const closeIdx = line.lastIndexOf("}");
|
|
128
|
+
if (openIdx < 0 || closeIdx < openIdx) return lines;
|
|
129
|
+
const head = line.slice(0, openIdx + 1);
|
|
130
|
+
const tail = line.slice(closeIdx + 1);
|
|
131
|
+
const level = range.depth - 1;
|
|
132
|
+
lines[range.open] = head;
|
|
133
|
+
lines.splice(range.open + 1, 0, `${indent(level)}}${tail}`);
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function insertObject(lines, parent, key) {
|
|
138
|
+
let dm = depthMaps(lines);
|
|
139
|
+
let range = objectRange(lines, dm, parent);
|
|
140
|
+
if (!range) return lines;
|
|
141
|
+
if (range.open === range.close) {
|
|
142
|
+
lines = expandEmptyObject(lines, range);
|
|
143
|
+
dm = depthMaps(lines);
|
|
144
|
+
range = objectRange(lines, dm, parent);
|
|
145
|
+
if (!range) return lines;
|
|
146
|
+
}
|
|
147
|
+
ensureComma(lines, range);
|
|
148
|
+
const level = range.depth;
|
|
149
|
+
const memberIndent = indent(level);
|
|
150
|
+
lines.splice(range.end, 0, `${memberIndent}"${key}": {`, `${memberIndent}}`);
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ensureObject(lines, path) {
|
|
155
|
+
let dm = depthMaps(lines);
|
|
156
|
+
if (objectRange(lines, dm, path)) return lines;
|
|
157
|
+
if (path.length === 0) return ["{", "}"];
|
|
158
|
+
lines = ensureObject(lines, path.slice(0, -1));
|
|
159
|
+
lines = insertObject(lines, path.slice(0, -1), path[path.length - 1]);
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function setKeyInRange(lines, range, key, value) {
|
|
164
|
+
let dm = depthMaps(lines);
|
|
165
|
+
let idx = findMember(lines, dm, range, key);
|
|
166
|
+
const memberIndent = indent(range.depth);
|
|
167
|
+
if (idx >= 0) {
|
|
168
|
+
const m = lines[idx].match(/^(\s*"[^"]+"\s*:\s*)(.*?)(\s*,)?\s*$/);
|
|
169
|
+
const comma = m && m[3] ? "," : "";
|
|
170
|
+
lines[idx] = `${m ? m[1] : `${memberIndent}"${key}": `}${value}${comma}`;
|
|
171
|
+
return lines;
|
|
172
|
+
}
|
|
173
|
+
if (range.open === range.close) {
|
|
174
|
+
lines = expandEmptyObject(lines, range);
|
|
175
|
+
dm = depthMaps(lines);
|
|
176
|
+
range = { ...range, start: range.open + 1, end: range.open + 1, close: range.open + 1 };
|
|
177
|
+
}
|
|
178
|
+
ensureComma(lines, range);
|
|
179
|
+
lines.splice(range.end, 0, `${memberIndent}"${key}": ${value}`);
|
|
180
|
+
return lines;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function readJsonKey(text, path) {
|
|
184
|
+
const lines = normalizeLines((text || "").split(/\r?\n/));
|
|
185
|
+
const dm = depthMaps(lines);
|
|
186
|
+
const range = objectRange(lines, dm, path.slice(0, -1));
|
|
187
|
+
if (!range) return null;
|
|
188
|
+
const idx = findMember(lines, dm, range, path[path.length - 1]);
|
|
189
|
+
if (idx < 0) return null;
|
|
190
|
+
const m = lines[idx].match(/^\s*"[^"]+"\s*:\s*(.*?)\s*,?\s*$/);
|
|
191
|
+
return m ? m[1] : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function setJsonKey(text, path, value) {
|
|
195
|
+
let lines = text === "" ? ["{", "}"] : normalizeLines(text.replace(/\r?\n$/, "").split(/\r?\n/));
|
|
196
|
+
if (!rootRange(lines, depthMaps(lines))) lines = ["{", "}"];
|
|
197
|
+
lines = ensureObject(lines, path.slice(0, -1));
|
|
198
|
+
let dm = depthMaps(lines);
|
|
199
|
+
const range = objectRange(lines, dm, path.slice(0, -1));
|
|
200
|
+
if (!range) return text;
|
|
201
|
+
lines = setKeyInRange(lines, range, path[path.length - 1], value);
|
|
202
|
+
return lines.join("\n") + "\n";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function removeJsonKey(text, path) {
|
|
206
|
+
const lines = text === "" ? [] : normalizeLines(text.replace(/\r?\n$/, "").split(/\r?\n/));
|
|
207
|
+
const dm = depthMaps(lines);
|
|
208
|
+
const range = objectRange(lines, dm, path.slice(0, -1));
|
|
209
|
+
if (!range) return { text, removed: false };
|
|
210
|
+
const idx = findMember(lines, dm, range, path[path.length - 1]);
|
|
211
|
+
if (idx < 0) return { text, removed: false };
|
|
212
|
+
lines.splice(idx, 1);
|
|
213
|
+
const dm2 = depthMaps(lines);
|
|
214
|
+
const range2 = objectRange(lines, dm2, path.slice(0, -1));
|
|
215
|
+
if (range2) {
|
|
216
|
+
const hasLater = (() => {
|
|
217
|
+
for (let i = idx; i < range2.end; i++) if (dm2.start[i] === range2.depth) return true;
|
|
218
|
+
return false;
|
|
219
|
+
})();
|
|
220
|
+
if (!hasLater) {
|
|
221
|
+
const prev = lastContentLine(lines, range2);
|
|
222
|
+
if (prev >= 0) lines[prev] = lines[prev].replace(/,\s*$/, "");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { text: lines.join("\n") + (lines.length ? "\n" : ""), removed: true };
|
|
226
|
+
}
|
package/src/util/paths.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
export function npmrcPath(env = process.env, home = homedir()) {
|
|
5
5
|
return env.NPM_CONFIG_USERCONFIG || join(home, ".npmrc");
|
|
@@ -19,6 +19,14 @@ export function pnpmRcPath(env = process.env, home = homedir(), platform = proce
|
|
|
19
19
|
return join(home, ".config", "pnpm", "rc");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// pnpm >= 11 reads pnpm-specific settings from config.yaml (camelCase) instead
|
|
23
|
+
// of the registry/auth-only rc file. Keep it next to the rc path so custom
|
|
24
|
+
// PMSEC_PNPM_CONFIG_FILE overrides remain self-contained.
|
|
25
|
+
export function pnpmConfigPath(env = process.env, home = homedir(), platform = process.platform) {
|
|
26
|
+
if (env.PMSEC_PNPM_CONFIG_YAML) return env.PMSEC_PNPM_CONFIG_YAML;
|
|
27
|
+
return join(dirname(pnpmRcPath(env, home, platform)), "config.yaml");
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
export function uvConfigPath(env = process.env, home = homedir(), platform = process.platform) {
|
|
23
31
|
if (env.UV_CONFIG_FILE) return env.UV_CONFIG_FILE;
|
|
24
32
|
if (platform === "win32") {
|
|
@@ -70,3 +78,18 @@ export function aubeConfigPath(env = process.env, home = homedir(), platform = p
|
|
|
70
78
|
const base = env.XDG_CONFIG_HOME || join(home, ".config");
|
|
71
79
|
return join(base, "aube", "config.toml");
|
|
72
80
|
}
|
|
81
|
+
|
|
82
|
+
// Composer keeps its global config in $COMPOSER_HOME/config.json. The default
|
|
83
|
+
// COMPOSER_HOME follows platform conventions (XDG on Unix, Application Support
|
|
84
|
+
// on macOS, APPDATA on Windows), matching Composer's own resolution. An
|
|
85
|
+
// explicit XDG_CONFIG_HOME wins on every non-Windows platform, including macOS.
|
|
86
|
+
export function composerConfigPath(env = process.env, home = homedir(), platform = process.platform) {
|
|
87
|
+
if (env.COMPOSER_HOME) return join(env.COMPOSER_HOME, "config.json");
|
|
88
|
+
if (platform === "win32") {
|
|
89
|
+
const base = env.APPDATA || join(home, "AppData", "Roaming");
|
|
90
|
+
return join(base, "Composer", "config.json");
|
|
91
|
+
}
|
|
92
|
+
if (env.XDG_CONFIG_HOME) return join(env.XDG_CONFIG_HOME, "composer", "config.json");
|
|
93
|
+
if (platform === "darwin") return join(home, "Library", "Application Support", "composer", "config.json");
|
|
94
|
+
return join(home, ".config", "composer", "config.json");
|
|
95
|
+
}
|