pmsec 0.2.0__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.0 → pmsec-0.2.2}/PKG-INFO +15 -1
- {pmsec-0.2.0 → pmsec-0.2.2}/README.md +14 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/pyproject.toml +1 -1
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/cli.py +4 -3
- pmsec-0.2.2/src/pmsec/util/io.py +104 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/uv.lock +1 -1
- pmsec-0.2.0/src/pmsec/util/io.py +0 -47
- {pmsec-0.2.0 → pmsec-0.2.2}/.gitignore +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/__init__.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/__main__.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/tools/__init__.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/tools/bun.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/tools/cargo.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/tools/mise.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/tools/npm.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/tools/pnpm.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/tools/uv.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/tools/yarn.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/util/__init__.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/util/lines.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/util/paths.py +0 -0
- {pmsec-0.2.0 → pmsec-0.2.2}/src/pmsec/util/version.py +0 -0
- {pmsec-0.2.0 → 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
|
|
|
@@ -37,7 +38,6 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
37
38
|
|
|
38
39
|
s = sub.add_parser("set", parents=[common], help="apply cooldown")
|
|
39
40
|
s.add_argument("days", type=int, help="cooldown in days (must be > 0)")
|
|
40
|
-
s.add_argument("--force", action="store_true", help="apply even if a tool's installed version is too old")
|
|
41
41
|
|
|
42
42
|
sub.add_parser("unset", parents=[common], help="remove cooldown")
|
|
43
43
|
|
|
@@ -102,10 +102,11 @@ def _check(args, targets, env, home, platform, out, err):
|
|
|
102
102
|
def _explain_fs_error(exc: BaseException, tool: str) -> str:
|
|
103
103
|
if isinstance(exc, PermissionError):
|
|
104
104
|
path = getattr(exc, "filename", "") or ""
|
|
105
|
+
q = shlex.quote(path) if path else ""
|
|
105
106
|
return (
|
|
106
107
|
f"{tool}: cannot write {path} (PermissionError). "
|
|
107
|
-
f"Check file ownership: `ls -la {
|
|
108
|
-
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}`."
|
|
109
110
|
)
|
|
110
111
|
if isinstance(exc, OSError) and exc.errno == 30:
|
|
111
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.0/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
|