code-aide 1.18.4__tar.gz → 1.19.0__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.
- {code_aide-1.18.4 → code_aide-1.19.0}/PKG-INFO +19 -1
- {code_aide-1.18.4 → code_aide-1.19.0}/README.md +18 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/__init__.py +1 -1
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/console.py +32 -11
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/install.py +16 -7
- code_aide-1.19.0/src/code_aide/npm_cache.py +61 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_install.py +14 -12
- code_aide-1.19.0/tests/test_npm_cache.py +204 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/.github/workflows/ci.yml +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/.github/workflows/publish.yml +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/.gitignore +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/.gitlab-ci.yml +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/.pre-commit-config.yaml +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/AGENTS.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/CLAUDE.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/LICENSE +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/Makefile +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/TODO.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/pyproject.toml +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/script-archive/README.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/script-archive/amp-install.sh +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/script-archive/antigravity-install.sh +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/script-archive/claude-install.sh +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/script-archive/cursor-install.sh +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/specs/2026-05-03-qwen3.6-code-quality-analysis-original.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/specs/2026-05-03-qwen3.6-code-quality-analysis.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/specs/antigravity-cli-integration.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/specs/auto-migrate-deprecated-installs.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/specs/claude-native-installer-migration.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/specs/missing-coding-llm-cli-tools.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/specs/pre-commit-uv-setup.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/specs/remove-bundled-version-baseline.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/specs/unify-upgrade-eligibility-with-shared-evaluator.md +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/__main__.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/commands_actions.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/commands_tools.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/config.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/constants.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/data/tools.json +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/detection.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/detection_types.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/entry.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/install_types.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/operation_handlers.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/operations.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/package_info.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/package_managers.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/prereqs.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/status.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/status_print.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/src/code_aide/versions.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_commands_actions.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_commands_tools.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_config.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_console.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_constants.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_detection.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_entry.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_error_paths.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_install_types.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_operations.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_package_info.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_package_managers.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_prereqs.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_status.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/tests/test_versions.py +0 -0
- {code_aide-1.18.4 → code_aide-1.19.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: code-aide
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.19.0
|
|
4
4
|
Summary: Manage AI coding CLI tools (Claude, Antigravity, Copilot, Cursor, Gemini, Amp, Codex)
|
|
5
5
|
Project-URL: Homepage, https://github.com/dajobe/code-aide
|
|
6
6
|
Project-URL: Repository, https://github.com/dajobe/code-aide
|
|
@@ -234,3 +234,21 @@ uv run pytest tests/test_install.py::TestDetectOsArch -v
|
|
|
234
234
|
## License
|
|
235
235
|
|
|
236
236
|
Apache-2.0
|
|
237
|
+
|
|
238
|
+
## npm download cache lifetime
|
|
239
|
+
|
|
240
|
+
Standalone install, upgrade, removal (including migration), and installation
|
|
241
|
+
scripts use private download caches under
|
|
242
|
+
`~/.cache/code-aide/npm-operations/operation-*`. Each invocation gets its own
|
|
243
|
+
directory on the home filesystem instead of the temporary filesystem. npm
|
|
244
|
+
authentication, user configuration and install prefixes remain in effect; only
|
|
245
|
+
the download-cache setting is overridden in the child environment.
|
|
246
|
+
|
|
247
|
+
Downloads and npm logs in that directory are removed after success, failure,
|
|
248
|
+
timeouts supported by the command runner, and normal Python interruption.
|
|
249
|
+
Captured command errors are still reported. Existing user caches are preserved.
|
|
250
|
+
Dry runs create no operation cache. Concurrent operations never clean another
|
|
251
|
+
operation's cache. A hard kill or machine crash can leave an operation
|
|
252
|
+
directory; inspect it and establish that its operation has stopped before
|
|
253
|
+
explicitly removing it. There is no automatic age-based cleanup of these
|
|
254
|
+
directories.
|
|
@@ -207,3 +207,21 @@ uv run pytest tests/test_install.py::TestDetectOsArch -v
|
|
|
207
207
|
## License
|
|
208
208
|
|
|
209
209
|
Apache-2.0
|
|
210
|
+
|
|
211
|
+
## npm download cache lifetime
|
|
212
|
+
|
|
213
|
+
Standalone install, upgrade, removal (including migration), and installation
|
|
214
|
+
scripts use private download caches under
|
|
215
|
+
`~/.cache/code-aide/npm-operations/operation-*`. Each invocation gets its own
|
|
216
|
+
directory on the home filesystem instead of the temporary filesystem. npm
|
|
217
|
+
authentication, user configuration and install prefixes remain in effect; only
|
|
218
|
+
the download-cache setting is overridden in the child environment.
|
|
219
|
+
|
|
220
|
+
Downloads and npm logs in that directory are removed after success, failure,
|
|
221
|
+
timeouts supported by the command runner, and normal Python interruption.
|
|
222
|
+
Captured command errors are still reported. Existing user caches are preserved.
|
|
223
|
+
Dry runs create no operation cache. Concurrent operations never clean another
|
|
224
|
+
operation's cache. A hard kill or machine crash can leave an operation
|
|
225
|
+
directory; inspect it and establish that its operation has stopped before
|
|
226
|
+
explicitly removing it. There is no automatic age-based cleanup of these
|
|
227
|
+
directories.
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Console output and subprocess helpers for CLI modules."""
|
|
2
2
|
|
|
3
|
+
from contextlib import nullcontext
|
|
4
|
+
from pathlib import Path
|
|
3
5
|
import shutil
|
|
4
6
|
import subprocess
|
|
5
|
-
from typing import List
|
|
7
|
+
from typing import Any, List
|
|
6
8
|
|
|
7
9
|
from code_aide.constants import Colors
|
|
10
|
+
from code_aide.npm_cache import npm_cache_environment, run_npm_process
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
def info(message: str) -> None:
|
|
@@ -33,7 +36,10 @@ def command_exists(command: str) -> bool:
|
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
def run_command(
|
|
36
|
-
cmd: List[str],
|
|
39
|
+
cmd: List[str],
|
|
40
|
+
check: bool = True,
|
|
41
|
+
capture: bool = True,
|
|
42
|
+
timeout: float | None = None,
|
|
37
43
|
) -> subprocess.CompletedProcess:
|
|
38
44
|
"""Run a tool-mutating command with consistent hardening.
|
|
39
45
|
|
|
@@ -47,19 +53,34 @@ def run_command(
|
|
|
47
53
|
Defaults to capture+text mode. With ``check=True`` (default) a
|
|
48
54
|
non-zero exit raises ``CalledProcessError``.
|
|
49
55
|
"""
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
is_npm = bool(cmd) and Path(cmd[0]).name == "npm"
|
|
57
|
+
if is_npm and any(
|
|
58
|
+
arg == "--cache" or arg.startswith("--cache=") for arg in cmd[1:]
|
|
59
|
+
):
|
|
60
|
+
raise ValueError("npm cache overrides are managed by code-aide")
|
|
61
|
+
context = npm_cache_environment() if is_npm else nullcontext(None)
|
|
62
|
+
with context as env:
|
|
63
|
+
kwargs: dict[str, Any] = {}
|
|
64
|
+
if env is not None:
|
|
65
|
+
kwargs["env"] = env
|
|
66
|
+
if timeout is not None:
|
|
67
|
+
kwargs["timeout"] = timeout
|
|
68
|
+
runner = run_npm_process if is_npm else subprocess.run
|
|
69
|
+
if capture:
|
|
70
|
+
return runner(
|
|
71
|
+
cmd,
|
|
72
|
+
check=check,
|
|
73
|
+
capture_output=True,
|
|
74
|
+
text=True,
|
|
75
|
+
stdin=subprocess.DEVNULL,
|
|
76
|
+
**kwargs,
|
|
77
|
+
)
|
|
78
|
+
return runner(
|
|
52
79
|
cmd,
|
|
53
80
|
check=check,
|
|
54
|
-
capture_output=True,
|
|
55
|
-
text=True,
|
|
56
81
|
stdin=subprocess.DEVNULL,
|
|
82
|
+
**kwargs,
|
|
57
83
|
)
|
|
58
|
-
return subprocess.run(
|
|
59
|
-
cmd,
|
|
60
|
-
check=check,
|
|
61
|
-
stdin=subprocess.DEVNULL,
|
|
62
|
-
)
|
|
63
84
|
|
|
64
85
|
|
|
65
86
|
def called_process_error_message(exc: subprocess.CalledProcessError) -> str:
|
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
import platform
|
|
7
7
|
import shutil
|
|
8
8
|
import subprocess
|
|
9
|
+
|
|
9
10
|
import tarfile
|
|
10
11
|
import tempfile
|
|
11
12
|
from dataclasses import dataclass
|
|
@@ -21,6 +22,7 @@ from code_aide.console import (
|
|
|
21
22
|
success,
|
|
22
23
|
warning,
|
|
23
24
|
)
|
|
25
|
+
from code_aide.npm_cache import npm_cache_environment, stop_process_group
|
|
24
26
|
from code_aide.install_types import InstallType, get_tool_install_type
|
|
25
27
|
from code_aide.versions import check_script_tool, fetch_url
|
|
26
28
|
|
|
@@ -113,13 +115,20 @@ def run_install_script(
|
|
|
113
115
|
warning(f"About to execute install script for {tool_name}")
|
|
114
116
|
warning("This will run with your user privileges. Press Ctrl+C to cancel.")
|
|
115
117
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
# Scripts may invoke npm themselves; inherit the same bounded cache.
|
|
119
|
+
with npm_cache_environment(env) as script_env:
|
|
120
|
+
bash_process = subprocess.Popen(
|
|
121
|
+
["bash"],
|
|
122
|
+
stdin=subprocess.PIPE,
|
|
123
|
+
stderr=subprocess.PIPE,
|
|
124
|
+
env=script_env,
|
|
125
|
+
start_new_session=True,
|
|
126
|
+
)
|
|
127
|
+
try:
|
|
128
|
+
_, stderr = bash_process.communicate(input=script_content)
|
|
129
|
+
except BaseException:
|
|
130
|
+
stop_process_group(bash_process)
|
|
131
|
+
raise
|
|
123
132
|
|
|
124
133
|
if bash_process.returncode == 0:
|
|
125
134
|
return True
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Private download-cache lifetimes for npm commands and installer scripts."""
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import tempfile
|
|
9
|
+
from typing import Any, Iterator, Mapping
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@contextmanager
|
|
13
|
+
def npm_cache_environment(
|
|
14
|
+
env: Mapping[str, str] | None = None,
|
|
15
|
+
) -> Iterator[dict[str, str]]:
|
|
16
|
+
"""Keep downloads on the home filesystem and remove only this invocation."""
|
|
17
|
+
root = Path.home() / ".cache" / "code-aide" / "npm-operations"
|
|
18
|
+
current = Path.home()
|
|
19
|
+
for component in (".cache", "code-aide", "npm-operations"):
|
|
20
|
+
current = current / component
|
|
21
|
+
if current.is_symlink():
|
|
22
|
+
raise ValueError(f"npm operation cache parent is a symlink: {current}")
|
|
23
|
+
current.mkdir(exist_ok=True, mode=0o700)
|
|
24
|
+
if current.stat().st_uid != os.getuid():
|
|
25
|
+
raise ValueError(f"npm operation cache parent has another owner: {current}")
|
|
26
|
+
with tempfile.TemporaryDirectory(prefix="operation-", dir=root) as directory:
|
|
27
|
+
child_env = dict(os.environ if env is None else env)
|
|
28
|
+
# npm treats config environment keys case-insensitively.
|
|
29
|
+
for key in list(child_env):
|
|
30
|
+
if key.lower() == "npm_config_cache":
|
|
31
|
+
del child_env[key]
|
|
32
|
+
child_env["npm_config_cache"] = directory
|
|
33
|
+
yield child_env
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def stop_process_group(process: subprocess.Popen) -> None:
|
|
37
|
+
"""Stop the entire private process group before its cache is removed."""
|
|
38
|
+
try:
|
|
39
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
40
|
+
except ProcessLookupError:
|
|
41
|
+
pass
|
|
42
|
+
process.communicate(timeout=5)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_npm_process(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess:
|
|
46
|
+
"""Run npm in its own session and reap its children on cancellation."""
|
|
47
|
+
check = kwargs.pop("check", True)
|
|
48
|
+
timeout = kwargs.pop("timeout", None)
|
|
49
|
+
if kwargs.pop("capture_output", False):
|
|
50
|
+
kwargs["stdout"] = subprocess.PIPE
|
|
51
|
+
kwargs["stderr"] = subprocess.PIPE
|
|
52
|
+
with subprocess.Popen(cmd, start_new_session=True, **kwargs) as process:
|
|
53
|
+
try:
|
|
54
|
+
stdout, stderr = process.communicate(timeout=timeout)
|
|
55
|
+
except BaseException:
|
|
56
|
+
stop_process_group(process)
|
|
57
|
+
raise
|
|
58
|
+
result = subprocess.CompletedProcess(cmd, process.returncode, stdout, stderr)
|
|
59
|
+
if check:
|
|
60
|
+
result.check_returncode()
|
|
61
|
+
return result
|
|
@@ -200,20 +200,22 @@ class TestInstallScriptEnv(unittest.TestCase):
|
|
|
200
200
|
mock_fetch.return_value = (script_content, None)
|
|
201
201
|
env = {"PATH": "/tmp/bin:/usr/bin"}
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
with tempfile.TemporaryDirectory() as home:
|
|
204
|
+
with mock.patch.dict(os.environ, {"HOME": home}):
|
|
205
|
+
result = cli_install.run_install_script(
|
|
206
|
+
"https://example.com/install.sh",
|
|
207
|
+
"Example",
|
|
208
|
+
expected_sha256,
|
|
209
|
+
env=env,
|
|
210
|
+
)
|
|
209
211
|
|
|
210
212
|
self.assertTrue(result)
|
|
211
|
-
mock_popen.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
)
|
|
213
|
+
mock_popen.assert_called_once()
|
|
214
|
+
actual_env = mock_popen.call_args.kwargs["env"]
|
|
215
|
+
self.assertEqual(actual_env["PATH"], env["PATH"])
|
|
216
|
+
self.assertIn("npm_config_cache", actual_env)
|
|
217
|
+
self.assertFalse(os.path.exists(actual_env["npm_config_cache"]))
|
|
218
|
+
self.assertEqual(env, {"PATH": "/tmp/bin:/usr/bin"})
|
|
217
219
|
|
|
218
220
|
|
|
219
221
|
class TestInstallDirectDownloadDryrun(unittest.TestCase):
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Cache lifetime regressions through standalone operation dispatch."""
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import subprocess
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from code_aide import console, constants, install, operations
|
|
12
|
+
from code_aide.npm_cache import npm_cache_environment
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def cache_home(tmp_path, monkeypatch):
|
|
17
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
18
|
+
return tmp_path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.parametrize("route", ["install", "upgrade", "remove"])
|
|
22
|
+
@pytest.mark.parametrize("fail", [False, True])
|
|
23
|
+
def test_operation_downloads_removed(cache_home, monkeypatch, route, fail):
|
|
24
|
+
user_cache = cache_home / "existing-cache"
|
|
25
|
+
user_cache.mkdir()
|
|
26
|
+
marker = user_cache / "keep"
|
|
27
|
+
marker.write_text("existing")
|
|
28
|
+
monkeypatch.setenv("NPM_CONFIG_CACHE", str(user_cache))
|
|
29
|
+
monkeypatch.setenv("npm_config_prefix", "/example/prefix")
|
|
30
|
+
monkeypatch.setenv("NPM_TOKEN", "fake-token")
|
|
31
|
+
original = os.environ.copy()
|
|
32
|
+
seen = []
|
|
33
|
+
|
|
34
|
+
def run(cmd, **kwargs):
|
|
35
|
+
env = kwargs["env"]
|
|
36
|
+
cache = Path(env["npm_config_cache"])
|
|
37
|
+
assert cache.stat().st_mode & 0o777 == 0o700
|
|
38
|
+
assert "NPM_CONFIG_CACHE" not in env
|
|
39
|
+
assert env["npm_config_prefix"] == "/example/prefix"
|
|
40
|
+
assert env["NPM_TOKEN"] == "fake-token"
|
|
41
|
+
(cache / "download").write_bytes(b"synthetic tarball")
|
|
42
|
+
seen.append(cache)
|
|
43
|
+
if fail:
|
|
44
|
+
raise subprocess.CalledProcessError(1, cmd, stderr="fake failure")
|
|
45
|
+
return subprocess.CompletedProcess(cmd, 0, stdout="/example/prefix")
|
|
46
|
+
|
|
47
|
+
cfg = dict(
|
|
48
|
+
name="Fake Tool",
|
|
49
|
+
command="fake-tool",
|
|
50
|
+
install_type="npm",
|
|
51
|
+
npm_package="fake-tool",
|
|
52
|
+
next_steps="Run fake-tool",
|
|
53
|
+
)
|
|
54
|
+
monkeypatch.setitem(constants._TOOLS_DATA, "fake", cfg)
|
|
55
|
+
monkeypatch.setattr(console, "run_npm_process", run)
|
|
56
|
+
monkeypatch.setattr(install, "command_exists", lambda _: False)
|
|
57
|
+
monkeypatch.setattr(install.platform, "system", lambda: "Linux")
|
|
58
|
+
monkeypatch.setattr(operations, "is_tool_installed", lambda _: True)
|
|
59
|
+
monkeypatch.setattr(operations, "is_deprecated_install", lambda _: False)
|
|
60
|
+
monkeypatch.setattr(
|
|
61
|
+
operations,
|
|
62
|
+
"detect_install_method",
|
|
63
|
+
lambda _: {"method": "npm", "detail": "fake-tool"},
|
|
64
|
+
)
|
|
65
|
+
monkeypatch.setattr(operations, "_get_upgrade_snapshot", lambda *a: {})
|
|
66
|
+
monkeypatch.setattr(
|
|
67
|
+
operations,
|
|
68
|
+
"_upgrade_result_from_snapshots",
|
|
69
|
+
lambda *a: operations.UpgradeResult.CHANGED,
|
|
70
|
+
)
|
|
71
|
+
if route == "install":
|
|
72
|
+
assert bool(install.install_tool("fake")) is not fail
|
|
73
|
+
elif route == "upgrade":
|
|
74
|
+
result = operations.upgrade_tool("fake")
|
|
75
|
+
assert (result == operations.UpgradeResult.FAILED) is fail
|
|
76
|
+
else:
|
|
77
|
+
assert operations.remove_tool("fake") is not fail
|
|
78
|
+
assert seen and all(not p.exists() for p in seen)
|
|
79
|
+
assert marker.read_text() == "existing"
|
|
80
|
+
assert os.environ == original
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_concurrent_operations_keep_each_others_downloads(cache_home):
|
|
84
|
+
barrier = threading.Barrier(2)
|
|
85
|
+
|
|
86
|
+
def operation(_):
|
|
87
|
+
with npm_cache_environment() as env:
|
|
88
|
+
path = Path(env["npm_config_cache"])
|
|
89
|
+
(path / "download").write_text("data")
|
|
90
|
+
barrier.wait(timeout=5)
|
|
91
|
+
assert len(list(path.parent.iterdir())) == 2
|
|
92
|
+
barrier.wait(timeout=5)
|
|
93
|
+
return path
|
|
94
|
+
|
|
95
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
96
|
+
paths = list(pool.map(operation, range(2)))
|
|
97
|
+
assert paths[0] != paths[1]
|
|
98
|
+
assert all(not path.exists() for path in paths)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.parametrize("exception", [KeyboardInterrupt, subprocess.TimeoutExpired])
|
|
102
|
+
def test_exception_cleanup(cache_home, monkeypatch, exception):
|
|
103
|
+
seen = []
|
|
104
|
+
|
|
105
|
+
def run(cmd, **kwargs):
|
|
106
|
+
seen.append(Path(kwargs["env"]["npm_config_cache"]))
|
|
107
|
+
if exception is subprocess.TimeoutExpired:
|
|
108
|
+
raise exception(cmd, 0.01)
|
|
109
|
+
raise exception()
|
|
110
|
+
|
|
111
|
+
monkeypatch.setattr(console, "run_npm_process", run)
|
|
112
|
+
with pytest.raises(exception):
|
|
113
|
+
console.run_command(["npm", "install", "fake"], timeout=0.01)
|
|
114
|
+
assert not seen[0].exists()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_non_npm_and_dryrun_do_not_create_cache(cache_home, monkeypatch):
|
|
118
|
+
monkeypatch.setattr(
|
|
119
|
+
console.subprocess, "run", lambda cmd, **kw: subprocess.CompletedProcess(cmd, 0)
|
|
120
|
+
)
|
|
121
|
+
console.run_command(["example", "install"])
|
|
122
|
+
monkeypatch.setattr(install, "command_exists", lambda _: False)
|
|
123
|
+
monkeypatch.setattr(install.platform, "system", lambda: "Linux")
|
|
124
|
+
assert install.install_tool("codex", dryrun=True)
|
|
125
|
+
assert not (cache_home / ".cache").exists()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@pytest.mark.parametrize("returncode", [0, 1])
|
|
129
|
+
def test_script_downloads_removed(cache_home, monkeypatch, returncode):
|
|
130
|
+
# Real bash exercises an installer subprocess rather than mocking its env.
|
|
131
|
+
fake_bin = cache_home / "bin"
|
|
132
|
+
fake_bin.mkdir()
|
|
133
|
+
fake_npm = fake_bin / "npm"
|
|
134
|
+
fake_npm.write_text(
|
|
135
|
+
"#!/bin/sh\n"
|
|
136
|
+
'mkdir -p "$npm_config_cache/nested"\n'
|
|
137
|
+
'printf download > "$npm_config_cache/nested/archive"\n'
|
|
138
|
+
'printf "%s" "$npm_config_cache" > "$HOME/observed-cache"\n'
|
|
139
|
+
f"exit {returncode}\n"
|
|
140
|
+
)
|
|
141
|
+
fake_npm.chmod(0o755)
|
|
142
|
+
monkeypatch.setenv("PATH", str(fake_bin) + os.pathsep + os.environ["PATH"])
|
|
143
|
+
script = b"npm install -g fake-tool\n"
|
|
144
|
+
monkeypatch.setattr(install, "fetch_url", lambda _: (script, None))
|
|
145
|
+
assert install.run_install_script("https://example.invalid", "Fake") is (
|
|
146
|
+
returncode == 0
|
|
147
|
+
)
|
|
148
|
+
observed = Path((cache_home / "observed-cache").read_text())
|
|
149
|
+
assert not observed.exists()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_explicit_cache_override_rejected_before_cache_creation(cache_home):
|
|
153
|
+
with pytest.raises(ValueError, match="cache overrides"):
|
|
154
|
+
console.run_command(["npm", "install", "--cache=/example"])
|
|
155
|
+
assert not (cache_home / ".cache").exists()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_symlink_cache_parent_rejected(cache_home, tmp_path):
|
|
159
|
+
target = tmp_path / "outside"
|
|
160
|
+
target.mkdir(mode=0o755)
|
|
161
|
+
(cache_home / ".cache").symlink_to(target)
|
|
162
|
+
with pytest.raises(ValueError, match="symlink"):
|
|
163
|
+
with npm_cache_environment():
|
|
164
|
+
pytest.fail("must reject symlink")
|
|
165
|
+
assert not list(target.iterdir())
|
|
166
|
+
assert target.stat().st_mode & 0o777 == 0o755
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_real_timeout_stops_descendant_before_cleanup(cache_home, monkeypatch):
|
|
170
|
+
import sys
|
|
171
|
+
import time
|
|
172
|
+
|
|
173
|
+
fake_bin = cache_home / "bin"
|
|
174
|
+
fake_bin.mkdir()
|
|
175
|
+
cache_record = cache_home / "cache-path"
|
|
176
|
+
ready = cache_home / "ready"
|
|
177
|
+
child = cache_home / "writer.py"
|
|
178
|
+
child.write_text(
|
|
179
|
+
"import os, pathlib, time\n"
|
|
180
|
+
"p = pathlib.Path(os.environ['npm_config_cache'])\n"
|
|
181
|
+
"pathlib.Path(os.environ['HOME'], 'ready').write_text('ready')\n"
|
|
182
|
+
"while True:\n"
|
|
183
|
+
" p.mkdir(parents=True, exist_ok=True)\n"
|
|
184
|
+
" (p / 'download').write_text('data')\n"
|
|
185
|
+
" time.sleep(0.01)\n"
|
|
186
|
+
)
|
|
187
|
+
fake_npm = fake_bin / "npm"
|
|
188
|
+
fake_npm.write_text(
|
|
189
|
+
f"#!{sys.executable}\n"
|
|
190
|
+
"import os, pathlib, subprocess, sys, time\n"
|
|
191
|
+
"pathlib.Path(os.environ['HOME'], 'cache-path').write_text(os.environ['npm_config_cache'])\n"
|
|
192
|
+
f"subprocess.Popen([sys.executable, {str(child)!r}])\n"
|
|
193
|
+
"time.sleep(30)\n"
|
|
194
|
+
)
|
|
195
|
+
fake_npm.chmod(0o755)
|
|
196
|
+
monkeypatch.setenv("PATH", str(fake_bin) + os.pathsep + os.environ["PATH"])
|
|
197
|
+
with pytest.raises(subprocess.TimeoutExpired):
|
|
198
|
+
console.run_command(["npm", "install", "fake"], timeout=0.5)
|
|
199
|
+
assert ready.exists()
|
|
200
|
+
cache = Path(cache_record.read_text())
|
|
201
|
+
assert not cache.exists()
|
|
202
|
+
# The descendant would recreate its directory within 10ms if still alive.
|
|
203
|
+
time.sleep(0.1)
|
|
204
|
+
assert not cache.exists()
|
|
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
|
{code_aide-1.18.4 → code_aide-1.19.0}/specs/2026-05-03-qwen3.6-code-quality-analysis-original.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_aide-1.18.4 → code_aide-1.19.0}/specs/unify-upgrade-eligibility-with-shared-evaluator.md
RENAMED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|