pmsec 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {pmsec-0.2.1 → pmsec-0.2.2}/PKG-INFO +15 -1
- {pmsec-0.2.1 → pmsec-0.2.2}/README.md +14 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/pyproject.toml +1 -1
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/cli.py +4 -2
- pmsec-0.2.2/src/pmsec/util/io.py +104 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/uv.lock +1 -1
- pmsec-0.2.1/src/pmsec/util/io.py +0 -47
- {pmsec-0.2.1 → pmsec-0.2.2}/.gitignore +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/__init__.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/__main__.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/tools/__init__.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/tools/bun.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/tools/cargo.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/tools/mise.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/tools/npm.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/tools/pnpm.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/tools/uv.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/tools/yarn.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/util/__init__.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/util/lines.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/util/paths.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/src/pmsec/util/version.py +0 -0
- {pmsec-0.2.1 → pmsec-0.2.2}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pmsec
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Inspect and apply install-time cooldown (min-release-age / exclude-newer) for npm and uv.
|
|
5
5
|
Project-URL: Homepage, https://github.com/HikaruEgashira/pmsec
|
|
6
6
|
Project-URL: Repository, https://github.com/HikaruEgashira/pmsec
|
|
@@ -26,6 +26,20 @@ uvx pmsec set 7
|
|
|
26
26
|
uvx pmsec unset
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
```bash
|
|
30
|
+
npx @hikae/pmsec check --min 7
|
|
31
|
+
npx @hikae/pmsec set 7
|
|
32
|
+
npx @hikae/pmsec unset
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
If your environment already enforces cooldown (or routes through a proxy
|
|
36
|
+
index), bootstrap pmsec by overriding just for that call:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uvx --index https://pypi.org/simple --exclude-newer-package pmsec=2099-01-01 pmsec check
|
|
40
|
+
npx --registry=https://registry.npmjs.org/ --min-release-age=0 @hikae/pmsec check
|
|
41
|
+
```
|
|
42
|
+
|
|
29
43
|
## Supported tools
|
|
30
44
|
|
|
31
45
|
npm, pnpm, yarn 4+, bun, cargo (RFC #3801), mise, uv
|
|
@@ -13,6 +13,20 @@ uvx pmsec set 7
|
|
|
13
13
|
uvx pmsec unset
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
```bash
|
|
17
|
+
npx @hikae/pmsec check --min 7
|
|
18
|
+
npx @hikae/pmsec set 7
|
|
19
|
+
npx @hikae/pmsec unset
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
If your environment already enforces cooldown (or routes through a proxy
|
|
23
|
+
index), bootstrap pmsec by overriding just for that call:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uvx --index https://pypi.org/simple --exclude-newer-package pmsec=2099-01-01 pmsec check
|
|
27
|
+
npx --registry=https://registry.npmjs.org/ --min-release-age=0 @hikae/pmsec check
|
|
28
|
+
```
|
|
29
|
+
|
|
16
30
|
## Supported tools
|
|
17
31
|
|
|
18
32
|
npm, pnpm, yarn 4+, bun, cargo (RFC #3801), mise, uv
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import json
|
|
5
|
+
import shlex
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
@@ -101,10 +102,11 @@ def _check(args, targets, env, home, platform, out, err):
|
|
|
101
102
|
def _explain_fs_error(exc: BaseException, tool: str) -> str:
|
|
102
103
|
if isinstance(exc, PermissionError):
|
|
103
104
|
path = getattr(exc, "filename", "") or ""
|
|
105
|
+
q = shlex.quote(path) if path else ""
|
|
104
106
|
return (
|
|
105
107
|
f"{tool}: cannot write {path} (PermissionError). "
|
|
106
|
-
f"Check file ownership: `ls -la {
|
|
107
|
-
f"run `sudo chown $(id -u):$(id -g) {
|
|
108
|
+
f"Check file ownership: `ls -la {q}` — if owned by root, "
|
|
109
|
+
f"run `sudo chown -h $(id -u):$(id -g) {q}`."
|
|
108
110
|
)
|
|
109
111
|
if isinstance(exc, OSError) and exc.errno == 30:
|
|
110
112
|
path = getattr(exc, "filename", "") or ""
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import secrets
|
|
5
|
+
import shlex
|
|
6
|
+
import stat
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _is_perm_err(exc: BaseException) -> bool:
|
|
13
|
+
return isinstance(exc, PermissionError) or (isinstance(exc, OSError) and exc.errno in (1, 13))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_symlink(path: Path) -> bool:
|
|
17
|
+
try:
|
|
18
|
+
return stat.S_ISLNK(path.lstat().st_mode)
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _under_home(path: Path, home: Path) -> bool:
|
|
24
|
+
try:
|
|
25
|
+
target = path.resolve(strict=False)
|
|
26
|
+
root = home.resolve(strict=False)
|
|
27
|
+
except OSError:
|
|
28
|
+
return False
|
|
29
|
+
try:
|
|
30
|
+
target.relative_to(root)
|
|
31
|
+
return True
|
|
32
|
+
except ValueError:
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _reclaim(path: Path, home: Path, err) -> bool:
|
|
37
|
+
if sys.platform == "win32" or not hasattr(os, "geteuid"):
|
|
38
|
+
return False
|
|
39
|
+
if not path.exists():
|
|
40
|
+
return False
|
|
41
|
+
if _is_symlink(path):
|
|
42
|
+
err.write(f"pmsec: refusing to chown symlink {path}; remove or replace it manually.\n")
|
|
43
|
+
return False
|
|
44
|
+
if not _under_home(path, home):
|
|
45
|
+
err.write(f"pmsec: refusing to chown {path} outside HOME ({home}); fix ownership manually.\n")
|
|
46
|
+
return False
|
|
47
|
+
quoted = shlex.quote(str(path))
|
|
48
|
+
err.write(
|
|
49
|
+
f"pmsec: {path} not writable; running `sudo chown -h $(id -u):$(id -g) {quoted}`. "
|
|
50
|
+
"You may be prompted for your password.\n"
|
|
51
|
+
)
|
|
52
|
+
err.flush()
|
|
53
|
+
r = subprocess.run(["sudo", "chown", "-h", f"{os.geteuid()}:{os.getegid()}", str(path)])
|
|
54
|
+
return r.returncode == 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _atomic_replace(path: Path, text: str) -> None:
|
|
58
|
+
parent = path.parent
|
|
59
|
+
tmp = parent / f".{path.name}.{os.getpid()}.{secrets.token_hex(4)}.tmp"
|
|
60
|
+
try:
|
|
61
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
62
|
+
try:
|
|
63
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
64
|
+
f.write(text)
|
|
65
|
+
f.flush()
|
|
66
|
+
try:
|
|
67
|
+
os.fsync(f.fileno())
|
|
68
|
+
except OSError:
|
|
69
|
+
pass
|
|
70
|
+
except BaseException:
|
|
71
|
+
try:
|
|
72
|
+
tmp.unlink()
|
|
73
|
+
except FileNotFoundError:
|
|
74
|
+
pass
|
|
75
|
+
raise
|
|
76
|
+
os.replace(tmp, path)
|
|
77
|
+
except BaseException:
|
|
78
|
+
try:
|
|
79
|
+
tmp.unlink()
|
|
80
|
+
except FileNotFoundError:
|
|
81
|
+
pass
|
|
82
|
+
raise
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def write_atomic(path: Path, text: str, *, backup: bool = True, home: Path | None = None, err=sys.stderr) -> None:
|
|
86
|
+
home = Path.home() if home is None else home
|
|
87
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
if path.exists() and _is_symlink(path):
|
|
89
|
+
raise OSError(f"refusing to write through symlink {path}")
|
|
90
|
+
if backup and path.exists():
|
|
91
|
+
bak = path.with_suffix(path.suffix + ".bak")
|
|
92
|
+
if not bak.exists():
|
|
93
|
+
try:
|
|
94
|
+
_atomic_replace(bak, path.read_text("utf-8"))
|
|
95
|
+
except OSError as exc:
|
|
96
|
+
if not _is_perm_err(exc) or not _reclaim(bak if bak.exists() else path, home, err):
|
|
97
|
+
raise
|
|
98
|
+
_atomic_replace(bak, path.read_text("utf-8"))
|
|
99
|
+
try:
|
|
100
|
+
_atomic_replace(path, text)
|
|
101
|
+
except OSError as exc:
|
|
102
|
+
if not _is_perm_err(exc) or not _reclaim(path, home, err):
|
|
103
|
+
raise
|
|
104
|
+
_atomic_replace(path, text)
|
pmsec-0.2.1/src/pmsec/util/io.py
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import subprocess
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _is_perm_err(exc: BaseException) -> bool:
|
|
10
|
-
return isinstance(exc, PermissionError) or (isinstance(exc, OSError) and exc.errno in (1, 13))
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _reclaim(path: Path, err) -> bool:
|
|
14
|
-
if sys.platform == "win32" or not hasattr(os, "geteuid"):
|
|
15
|
-
return False
|
|
16
|
-
target = path if path.exists() else path.parent
|
|
17
|
-
err.write(
|
|
18
|
-
f"pmsec: {path} not writable; running `sudo chown $(id -u):$(id -g) {target}`. "
|
|
19
|
-
"You may be prompted for your password.\n"
|
|
20
|
-
)
|
|
21
|
-
err.flush()
|
|
22
|
-
r = subprocess.run(["sudo", "chown", f"{os.geteuid()}:{os.getegid()}", str(target)])
|
|
23
|
-
return r.returncode == 0
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def write_atomic(path: Path, text: str, *, backup: bool = True, err=sys.stderr) -> None:
|
|
27
|
-
try:
|
|
28
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
-
except OSError as exc:
|
|
30
|
-
if not _is_perm_err(exc) or not _reclaim(path.parent, err):
|
|
31
|
-
raise
|
|
32
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
-
if backup and path.exists():
|
|
34
|
-
bak = path.with_suffix(path.suffix + ".bak")
|
|
35
|
-
if not bak.exists():
|
|
36
|
-
try:
|
|
37
|
-
bak.write_text(path.read_text("utf-8"), "utf-8")
|
|
38
|
-
except OSError as exc:
|
|
39
|
-
if not _is_perm_err(exc) or not _reclaim(bak, err):
|
|
40
|
-
raise
|
|
41
|
-
bak.write_text(path.read_text("utf-8"), "utf-8")
|
|
42
|
-
try:
|
|
43
|
-
path.write_text(text, "utf-8")
|
|
44
|
-
except OSError as exc:
|
|
45
|
-
if not _is_perm_err(exc) or not _reclaim(path, err):
|
|
46
|
-
raise
|
|
47
|
-
path.write_text(text, "utf-8")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|