ai-code-switcher 0.1.22__tar.gz → 0.1.23__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.
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/PKG-INFO +10 -2
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/README.md +9 -1
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/pyproject.toml +1 -1
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/ai_code_switcher.egg-info/PKG-INFO +10 -2
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/ai_code_switcher.egg-info/SOURCES.txt +3 -0
- ai_code_switcher-0.1.23/src/code_ai/__init__.py +1 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/cli.py +25 -3
- ai_code_switcher-0.1.23/src/code_ai/grok.py +68 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/launcher.py +7 -1
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_cli_upgrade.py +66 -5
- ai_code_switcher-0.1.23/tests/test_grok.py +86 -0
- ai_code_switcher-0.1.23/tests/test_grok_unix_execution.py +40 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_launcher.py +29 -0
- ai_code_switcher-0.1.22/src/code_ai/__init__.py +0 -1
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/setup.cfg +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/ai_code_switcher.egg-info/dependency_links.txt +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/ai_code_switcher.egg-info/entry_points.txt +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/ai_code_switcher.egg-info/requires.txt +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/ai_code_switcher.egg-info/top_level.txt +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/buddy.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/config.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/__init__.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/app.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/apps.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/bridge.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/config.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/env.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/platforms/__init__.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/platforms/base.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/platforms/macos.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/platforms/windows.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/ui/app.js +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/ui/icon.icns +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/ui/icon.ico +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/ui/icon.png +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/ui/index.html +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/ui/style.css +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/models.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/profiles.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_desktop_app_import.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_desktop_apps.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_desktop_backend.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_desktop_base.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_desktop_bridge.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_desktop_cli.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_desktop_config.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_desktop_env.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_desktop_icons.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_desktop_macos.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_desktop_windows.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_integration.py +0 -0
- {ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/tests/test_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ai-code-switcher
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.23
|
|
4
4
|
Summary: Switch AI coding tool profiles and launch the correct CLI
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -97,7 +97,15 @@ The supported AI CLIs are:
|
|
|
97
97
|
`code-ai upgrade` also refreshes the Grok binary in each login profile's
|
|
98
98
|
credentials directory. Grok stores a separate binary under `GROK_HOME/bin`, so
|
|
99
99
|
updating only the global npm package can leave a login profile running an old
|
|
100
|
-
version.
|
|
100
|
+
version. The upgrade enables npm install scripts and verifies the actual binary
|
|
101
|
+
version in the default home and every login home; an unchanged or missing binary
|
|
102
|
+
causes the command to fail. Restart running Grok sessions after upgrading.
|
|
103
|
+
|
|
104
|
+
For Grok, `code-ai run` prefers the official package in the active npm global
|
|
105
|
+
prefix, so an older native installation earlier on `PATH` cannot shadow the
|
|
106
|
+
package updated by `code-ai upgrade`. Native installations remain supported
|
|
107
|
+
when the npm package is absent. Login credentials still use the profile's own
|
|
108
|
+
`GROK_HOME`.
|
|
101
109
|
|
|
102
110
|
## Profiles and configuration
|
|
103
111
|
|
|
@@ -84,7 +84,15 @@ The supported AI CLIs are:
|
|
|
84
84
|
`code-ai upgrade` also refreshes the Grok binary in each login profile's
|
|
85
85
|
credentials directory. Grok stores a separate binary under `GROK_HOME/bin`, so
|
|
86
86
|
updating only the global npm package can leave a login profile running an old
|
|
87
|
-
version.
|
|
87
|
+
version. The upgrade enables npm install scripts and verifies the actual binary
|
|
88
|
+
version in the default home and every login home; an unchanged or missing binary
|
|
89
|
+
causes the command to fail. Restart running Grok sessions after upgrading.
|
|
90
|
+
|
|
91
|
+
For Grok, `code-ai run` prefers the official package in the active npm global
|
|
92
|
+
prefix, so an older native installation earlier on `PATH` cannot shadow the
|
|
93
|
+
package updated by `code-ai upgrade`. Native installations remain supported
|
|
94
|
+
when the npm package is absent. Login credentials still use the profile's own
|
|
95
|
+
`GROK_HOME`.
|
|
88
96
|
|
|
89
97
|
## Profiles and configuration
|
|
90
98
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ai-code-switcher"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.23"
|
|
8
8
|
description = "Switch AI coding tool profiles and launch the correct CLI"
|
|
9
9
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ai-code-switcher
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.23
|
|
4
4
|
Summary: Switch AI coding tool profiles and launch the correct CLI
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -97,7 +97,15 @@ The supported AI CLIs are:
|
|
|
97
97
|
`code-ai upgrade` also refreshes the Grok binary in each login profile's
|
|
98
98
|
credentials directory. Grok stores a separate binary under `GROK_HOME/bin`, so
|
|
99
99
|
updating only the global npm package can leave a login profile running an old
|
|
100
|
-
version.
|
|
100
|
+
version. The upgrade enables npm install scripts and verifies the actual binary
|
|
101
|
+
version in the default home and every login home; an unchanged or missing binary
|
|
102
|
+
causes the command to fail. Restart running Grok sessions after upgrading.
|
|
103
|
+
|
|
104
|
+
For Grok, `code-ai run` prefers the official package in the active npm global
|
|
105
|
+
prefix, so an older native installation earlier on `PATH` cannot shadow the
|
|
106
|
+
package updated by `code-ai upgrade`. Native installations remain supported
|
|
107
|
+
when the npm package is absent. Login credentials still use the profile's own
|
|
108
|
+
`GROK_HOME`.
|
|
101
109
|
|
|
102
110
|
## Profiles and configuration
|
|
103
111
|
|
{ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/ai_code_switcher.egg-info/SOURCES.txt
RENAMED
|
@@ -10,6 +10,7 @@ src/code_ai/__init__.py
|
|
|
10
10
|
src/code_ai/buddy.py
|
|
11
11
|
src/code_ai/cli.py
|
|
12
12
|
src/code_ai/config.py
|
|
13
|
+
src/code_ai/grok.py
|
|
13
14
|
src/code_ai/launcher.py
|
|
14
15
|
src/code_ai/models.py
|
|
15
16
|
src/code_ai/profiles.py
|
|
@@ -41,6 +42,8 @@ tests/test_desktop_env.py
|
|
|
41
42
|
tests/test_desktop_icons.py
|
|
42
43
|
tests/test_desktop_macos.py
|
|
43
44
|
tests/test_desktop_windows.py
|
|
45
|
+
tests/test_grok.py
|
|
46
|
+
tests/test_grok_unix_execution.py
|
|
44
47
|
tests/test_integration.py
|
|
45
48
|
tests/test_launcher.py
|
|
46
49
|
tests/test_models.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.23"
|
|
@@ -8,6 +8,7 @@ import typer
|
|
|
8
8
|
from .config import load_config, save_config
|
|
9
9
|
from .profiles import list_profiles, add_profile, remove_profile, show_profile
|
|
10
10
|
from .launcher import launch
|
|
11
|
+
from .grok import find_npm_installation, home_binary, check_version as check_grok_version
|
|
11
12
|
from .buddy import (
|
|
12
13
|
RARITIES, SPECIES, RARITY_COLORS,
|
|
13
14
|
roll_by_name, bruteforce_user_id,
|
|
@@ -299,18 +300,35 @@ def _run_npm(args, env):
|
|
|
299
300
|
return subprocess.run(["npm"] + args, env=env)
|
|
300
301
|
|
|
301
302
|
|
|
303
|
+
def _verify_grok(command, env, version):
|
|
304
|
+
try:
|
|
305
|
+
check_grok_version(command, env, version)
|
|
306
|
+
except RuntimeError as exc:
|
|
307
|
+
typer.echo(str(exc), err=True)
|
|
308
|
+
sys.exit(1)
|
|
309
|
+
|
|
310
|
+
|
|
302
311
|
def upgrade():
|
|
303
312
|
typer.echo("Upgrading claude, codex, grok CLI...")
|
|
304
313
|
|
|
305
314
|
env = os.environ.copy()
|
|
306
315
|
env.pop("GROK_HOME", None)
|
|
307
|
-
result = _run_npm(["install", "-g"] + UPGRADE_PACKAGES, env)
|
|
316
|
+
result = _run_npm(["install", "-g", "--ignore-scripts=false"] + UPGRADE_PACKAGES, env)
|
|
308
317
|
if result.returncode:
|
|
309
318
|
sys.exit(result.returncode)
|
|
310
319
|
|
|
320
|
+
installation = find_npm_installation()
|
|
321
|
+
if not installation:
|
|
322
|
+
typer.echo("Cannot find the updated @xai-official/grok installation in the active npm prefix.", err=True)
|
|
323
|
+
sys.exit(1)
|
|
324
|
+
_verify_grok(installation.command, env, installation.version)
|
|
325
|
+
default_home = os.path.expanduser("~/.grok")
|
|
326
|
+
_verify_grok(home_binary(default_home), env, installation.version)
|
|
327
|
+
typer.echo(f"Grok CLI {installation.version} verified at {installation.command}")
|
|
328
|
+
|
|
311
329
|
# Grok's npm launcher prefers GROK_HOME/bin even when that binary is old.
|
|
312
330
|
# Re-run its installer in each login home after updating the global package.
|
|
313
|
-
refreshed = {os.path.normcase(os.path.realpath(
|
|
331
|
+
refreshed = {os.path.normcase(os.path.realpath(default_home))}
|
|
314
332
|
for name, profile in load_config().get("profiles", {}).items():
|
|
315
333
|
if (profile.get("type") != "grok" or profile.get("mode") != "login"
|
|
316
334
|
or not profile.get("credentials_path")):
|
|
@@ -321,10 +339,14 @@ def upgrade():
|
|
|
321
339
|
continue
|
|
322
340
|
typer.echo(f"Refreshing Grok CLI for login profile '{name}'...")
|
|
323
341
|
profile_env = dict(env, GROK_HOME=home)
|
|
324
|
-
result = _run_npm([
|
|
342
|
+
result = _run_npm([
|
|
343
|
+
"rebuild", "-g", "--ignore-scripts=false", "--foreground-scripts", "@xai-official/grok",
|
|
344
|
+
], profile_env)
|
|
325
345
|
if result.returncode:
|
|
326
346
|
typer.echo(f"Failed to refresh Grok CLI for profile '{name}'.", err=True)
|
|
327
347
|
sys.exit(result.returncode)
|
|
348
|
+
_verify_grok(home_binary(home), profile_env, installation.version)
|
|
349
|
+
typer.echo(f"Grok CLI {installation.version} verified for profile '{name}'.")
|
|
328
350
|
refreshed.add(normalized_home)
|
|
329
351
|
sys.exit(0)
|
|
330
352
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Locate and verify the Grok installation managed by npm."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class GrokInstallation:
|
|
15
|
+
command: str
|
|
16
|
+
version: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def find_npm_installation() -> Optional[GrokInstallation]:
|
|
20
|
+
npm = shutil.which("npm.cmd" if sys.platform == "win32" else "npm")
|
|
21
|
+
if not npm:
|
|
22
|
+
return None
|
|
23
|
+
try:
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
[npm, "prefix", "-g"], capture_output=True, text=True, timeout=10,
|
|
26
|
+
)
|
|
27
|
+
prefix = result.stdout.strip()
|
|
28
|
+
if result.returncode or not prefix:
|
|
29
|
+
return None
|
|
30
|
+
if sys.platform == "win32":
|
|
31
|
+
command = os.path.join(prefix, "grok.cmd")
|
|
32
|
+
modules = os.path.join(prefix, "node_modules")
|
|
33
|
+
else:
|
|
34
|
+
command = os.path.join(prefix, "bin", "grok")
|
|
35
|
+
modules = os.path.join(prefix, "lib", "node_modules")
|
|
36
|
+
if not os.path.isfile(command):
|
|
37
|
+
return None
|
|
38
|
+
package_file = os.path.join(modules, "@xai-official", "grok", "package.json")
|
|
39
|
+
with open(package_file, encoding="utf-8") as package:
|
|
40
|
+
metadata = json.load(package)
|
|
41
|
+
if (not isinstance(metadata, dict)
|
|
42
|
+
or metadata.get("name") != "@xai-official/grok"
|
|
43
|
+
or not isinstance(metadata.get("version"), str)):
|
|
44
|
+
return None
|
|
45
|
+
return GrokInstallation(command, metadata["version"])
|
|
46
|
+
except (OSError, subprocess.SubprocessError, ValueError):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def home_binary(home: str) -> str:
|
|
51
|
+
name = "grok.exe" if sys.platform == "win32" else "grok"
|
|
52
|
+
return os.path.join(home, "bin", name)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def check_version(command: str, env: dict, expected: str) -> None:
|
|
56
|
+
try:
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
[command, "--version"], env=env,
|
|
59
|
+
capture_output=True, text=True, timeout=10,
|
|
60
|
+
)
|
|
61
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
62
|
+
raise RuntimeError(f"Cannot verify Grok CLI at '{command}': {exc}") from exc
|
|
63
|
+
match = re.search(r"\bgrok\s+(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)", result.stdout)
|
|
64
|
+
if result.returncode or not match:
|
|
65
|
+
raise RuntimeError(f"Cannot verify Grok CLI version at '{command}'.")
|
|
66
|
+
actual = match.group(1)
|
|
67
|
+
if actual != expected:
|
|
68
|
+
raise RuntimeError(f"Grok CLI at '{command}' is {actual}; expected {expected}.")
|
|
@@ -6,6 +6,7 @@ import subprocess
|
|
|
6
6
|
from typing import List
|
|
7
7
|
|
|
8
8
|
from .models import profile_from_dict, ApiProfile, LoginProfile
|
|
9
|
+
from .grok import find_npm_installation
|
|
9
10
|
|
|
10
11
|
ENV_MAP = {
|
|
11
12
|
"claude": {
|
|
@@ -166,6 +167,11 @@ def launch(profile_dict, extra_args, use_default_args=True):
|
|
|
166
167
|
else:
|
|
167
168
|
cmd_path = shutil.which(cmd)
|
|
168
169
|
|
|
170
|
+
if ptype == "grok":
|
|
171
|
+
installation = find_npm_installation()
|
|
172
|
+
if installation:
|
|
173
|
+
cmd_path = installation.command
|
|
174
|
+
|
|
169
175
|
if not cmd_path:
|
|
170
176
|
print(f"Error: '{cmd}' not found in PATH. Install it first.")
|
|
171
177
|
sys.exit(1)
|
|
@@ -194,4 +200,4 @@ def launch(profile_dict, extra_args, use_default_args=True):
|
|
|
194
200
|
else:
|
|
195
201
|
# On Unix, update os.environ and use execvp
|
|
196
202
|
os.environ.update(env)
|
|
197
|
-
os.execvp(
|
|
203
|
+
os.execvp(cmd_path, full_cmd)
|
|
@@ -7,13 +7,23 @@ from src.code_ai import cli
|
|
|
7
7
|
from src.code_ai.cli import UPGRADE_PACKAGES
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
@pytest.fixture(autouse=True)
|
|
11
|
+
def installed_grok(monkeypatch, tmp_path):
|
|
12
|
+
installation = SimpleNamespace(command=str(tmp_path / "npm-grok"), version="1.0.46")
|
|
13
|
+
monkeypatch.setattr(cli, "find_npm_installation", lambda: installation)
|
|
14
|
+
monkeypatch.setattr(cli, "load_config", lambda: {"profiles": {}})
|
|
15
|
+
checks = []
|
|
16
|
+
monkeypatch.setattr(cli, "check_grok_version", lambda command, env, version: checks.append((command, env, version)))
|
|
17
|
+
return installation, checks
|
|
18
|
+
|
|
19
|
+
|
|
10
20
|
def test_upgrade_packages_use_official_grok_cli_instead_of_gemini():
|
|
11
21
|
assert "@xai-official/grok" in UPGRADE_PACKAGES
|
|
12
22
|
assert "@google/gemini-cli" not in UPGRADE_PACKAGES
|
|
13
23
|
|
|
14
24
|
|
|
15
25
|
@pytest.mark.parametrize("platform", ["win32", "linux", "darwin"])
|
|
16
|
-
def test_upgrade_refreshes_each_grok_login_home(monkeypatch, tmp_path, platform):
|
|
26
|
+
def test_upgrade_refreshes_each_grok_login_home(monkeypatch, tmp_path, platform, installed_grok):
|
|
17
27
|
homes = [str(tmp_path / "account-a"), str(tmp_path / "account-b")]
|
|
18
28
|
monkeypatch.setattr(cli.sys, "platform", platform)
|
|
19
29
|
monkeypatch.setenv("GROK_HOME", homes[0])
|
|
@@ -41,12 +51,21 @@ def test_upgrade_refreshes_each_grok_login_home(monkeypatch, tmp_path, platform)
|
|
|
41
51
|
assert "GROK_HOME" not in calls[0][1]["env"]
|
|
42
52
|
assert [call[1]["env"]["GROK_HOME"] for call in calls[1:]] == homes
|
|
43
53
|
if platform == "win32":
|
|
44
|
-
assert calls[0][0] == "npm install -g " + " ".join(UPGRADE_PACKAGES)
|
|
54
|
+
assert calls[0][0] == "npm install -g --ignore-scripts=false " + " ".join(UPGRADE_PACKAGES)
|
|
45
55
|
assert all(call[1]["shell"] for call in calls)
|
|
46
|
-
assert [call[0] for call in calls[1:]] == ["npm rebuild -g @xai-official/grok"] * 2
|
|
56
|
+
assert [call[0] for call in calls[1:]] == ["npm rebuild -g --ignore-scripts=false --foreground-scripts @xai-official/grok"] * 2
|
|
47
57
|
else:
|
|
48
|
-
assert calls[0][0] == ["npm", "install", "-g"] + UPGRADE_PACKAGES
|
|
49
|
-
assert [call[0] for call in calls[1:]] == [["npm", "rebuild", "-g", "@xai-official/grok"]] * 2
|
|
58
|
+
assert calls[0][0] == ["npm", "install", "-g", "--ignore-scripts=false"] + UPGRADE_PACKAGES
|
|
59
|
+
assert [call[0] for call in calls[1:]] == [["npm", "rebuild", "-g", "--ignore-scripts=false", "--foreground-scripts", "@xai-official/grok"]] * 2
|
|
60
|
+
installation, checks = installed_grok
|
|
61
|
+
binary_name = "grok.exe" if platform == "win32" else "grok"
|
|
62
|
+
assert [check[0] for check in checks] == [
|
|
63
|
+
installation.command,
|
|
64
|
+
os.path.join(os.path.expanduser("~/.grok"), "bin", binary_name),
|
|
65
|
+
os.path.join(homes[0], "bin", binary_name),
|
|
66
|
+
os.path.join(homes[1], "bin", binary_name),
|
|
67
|
+
]
|
|
68
|
+
assert all(check[2] == "1.0.46" for check in checks)
|
|
50
69
|
assert os.environ["GROK_HOME"] == homes[0]
|
|
51
70
|
|
|
52
71
|
|
|
@@ -121,3 +140,45 @@ def test_upgrade_reports_grok_home_refresh_failure(monkeypatch, tmp_path, capsys
|
|
|
121
140
|
|
|
122
141
|
assert exc.value.code == 9
|
|
123
142
|
assert "grok-a" in capsys.readouterr().err
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_upgrade_rejects_successful_rebuild_that_leaves_profile_binary_old(monkeypatch, tmp_path, capsys):
|
|
146
|
+
home = str(tmp_path / "account-a")
|
|
147
|
+
monkeypatch.setattr(cli, "load_config", lambda: {"profiles": {
|
|
148
|
+
"grok-a": {"type": "grok", "mode": "login", "credentials_path": home},
|
|
149
|
+
}})
|
|
150
|
+
monkeypatch.setattr(cli.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=0))
|
|
151
|
+
|
|
152
|
+
def check(command, env, version):
|
|
153
|
+
if env.get("GROK_HOME") == home:
|
|
154
|
+
raise RuntimeError("Grok CLI is still 0.2.118; expected 1.0.46")
|
|
155
|
+
|
|
156
|
+
monkeypatch.setattr(cli, "check_grok_version", check, raising=False)
|
|
157
|
+
with pytest.raises(SystemExit) as exc:
|
|
158
|
+
cli.upgrade()
|
|
159
|
+
assert exc.value.code == 1
|
|
160
|
+
assert "0.2.118" in capsys.readouterr().err
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_upgrade_rejects_stale_default_binary(monkeypatch, capsys, installed_grok):
|
|
164
|
+
monkeypatch.setattr(cli.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=0))
|
|
165
|
+
installation, _ = installed_grok
|
|
166
|
+
|
|
167
|
+
def check(command, env, version):
|
|
168
|
+
if command != installation.command:
|
|
169
|
+
raise RuntimeError("Grok CLI is still 0.2.118; expected 1.0.46")
|
|
170
|
+
|
|
171
|
+
monkeypatch.setattr(cli, "check_grok_version", check, raising=False)
|
|
172
|
+
with pytest.raises(SystemExit) as exc:
|
|
173
|
+
cli.upgrade()
|
|
174
|
+
assert exc.value.code == 1
|
|
175
|
+
assert "0.2.118" in capsys.readouterr().err
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_upgrade_fails_when_npm_installation_cannot_be_found(monkeypatch, capsys):
|
|
179
|
+
monkeypatch.setattr(cli.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=0))
|
|
180
|
+
monkeypatch.setattr(cli, "find_npm_installation", lambda: None, raising=False)
|
|
181
|
+
with pytest.raises(SystemExit) as exc:
|
|
182
|
+
cli.upgrade()
|
|
183
|
+
assert exc.value.code == 1
|
|
184
|
+
assert "npm" in capsys.readouterr().err
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from src.code_ai import grok
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.parametrize("platform", ["win32", "darwin", "linux"])
|
|
12
|
+
def test_find_npm_installation_uses_active_prefix(monkeypatch, tmp_path, platform):
|
|
13
|
+
monkeypatch.setattr(grok.sys, "platform", platform)
|
|
14
|
+
command = tmp_path / ("grok.cmd" if platform == "win32" else "bin/grok")
|
|
15
|
+
package = tmp_path / ("node_modules" if platform == "win32" else "lib/node_modules") / "@xai-official/grok"
|
|
16
|
+
command.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
command.write_text("launcher", encoding="utf-8")
|
|
18
|
+
package.mkdir(parents=True)
|
|
19
|
+
(package / "package.json").write_text(json.dumps({"name": "@xai-official/grok", "version": "1.0.46"}))
|
|
20
|
+
monkeypatch.setattr(grok.shutil, "which", lambda name: "npm.cmd" if platform == "win32" else "npm")
|
|
21
|
+
calls = []
|
|
22
|
+
|
|
23
|
+
def run(args, **kwargs):
|
|
24
|
+
calls.append((args, kwargs))
|
|
25
|
+
return SimpleNamespace(returncode=0, stdout=str(tmp_path) + "\n", stderr="")
|
|
26
|
+
|
|
27
|
+
monkeypatch.setattr(grok.subprocess, "run", run)
|
|
28
|
+
installation = grok.find_npm_installation()
|
|
29
|
+
|
|
30
|
+
assert installation.command == str(command)
|
|
31
|
+
assert installation.version == "1.0.46"
|
|
32
|
+
assert calls[0][0][1:] == ["prefix", "-g"]
|
|
33
|
+
assert calls[0][1]["timeout"] == 10
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_find_npm_installation_returns_none_when_npm_is_missing(monkeypatch):
|
|
37
|
+
monkeypatch.setattr(grok.shutil, "which", lambda name: None)
|
|
38
|
+
assert grok.find_npm_installation() is None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_find_npm_installation_returns_none_when_prefix_query_fails(monkeypatch):
|
|
42
|
+
monkeypatch.setattr(grok.shutil, "which", lambda name: "npm")
|
|
43
|
+
monkeypatch.setattr(grok.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=1, stdout="", stderr="unavailable"))
|
|
44
|
+
assert grok.find_npm_installation() is None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_find_npm_installation_does_not_treat_an_empty_prefix_as_cwd(monkeypatch):
|
|
48
|
+
monkeypatch.setattr(grok.shutil, "which", lambda name: "npm")
|
|
49
|
+
monkeypatch.setattr(grok.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=0, stdout="\n", stderr=""))
|
|
50
|
+
assert grok.find_npm_installation() is None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_check_version_rejects_stale_binary_despite_successful_exit(monkeypatch):
|
|
54
|
+
monkeypatch.setattr(grok.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=0, stdout="grok 0.2.118 (old)\n", stderr=""))
|
|
55
|
+
with pytest.raises(RuntimeError, match="0.2.118.*1.0.46"):
|
|
56
|
+
grok.check_version("/profile/bin/grok", {}, "1.0.46")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_check_version_uses_profile_environment(monkeypatch):
|
|
60
|
+
calls = []
|
|
61
|
+
env = {"GROK_HOME": "/profile"}
|
|
62
|
+
|
|
63
|
+
def run(args, **kwargs):
|
|
64
|
+
calls.append((args, kwargs))
|
|
65
|
+
return SimpleNamespace(returncode=0, stdout="grok 1.0.46 (current)\n", stderr="")
|
|
66
|
+
|
|
67
|
+
monkeypatch.setattr(grok.subprocess, "run", run)
|
|
68
|
+
grok.check_version("/profile/bin/grok", env, "1.0.46")
|
|
69
|
+
assert calls[0][0] == ["/profile/bin/grok", "--version"]
|
|
70
|
+
assert calls[0][1]["env"] is env
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_check_version_reports_missing_binary(monkeypatch):
|
|
74
|
+
def run(*args, **kwargs):
|
|
75
|
+
raise FileNotFoundError("missing")
|
|
76
|
+
|
|
77
|
+
monkeypatch.setattr(grok.subprocess, "run", run)
|
|
78
|
+
with pytest.raises(RuntimeError, match="/profile/bin/grok"):
|
|
79
|
+
grok.check_version("/profile/bin/grok", {}, "1.0.46")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_home_binary_uses_platform_filename(monkeypatch):
|
|
83
|
+
monkeypatch.setattr(grok.sys, "platform", "darwin")
|
|
84
|
+
assert grok.home_binary("/profile") == os.path.join("/profile", "bin", "grok")
|
|
85
|
+
monkeypatch.setattr(grok.sys, "platform", "win32")
|
|
86
|
+
assert grok.home_binary("/profile") == os.path.join("/profile", "bin", "grok.exe")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="Requires Unix executable scripts")
|
|
12
|
+
def test_grok_launch_executes_updated_npm_binary_with_isolated_home(tmp_path):
|
|
13
|
+
old_bin = tmp_path / "old-install"
|
|
14
|
+
prefix = tmp_path / "npm-prefix"
|
|
15
|
+
old_bin.mkdir()
|
|
16
|
+
(prefix / "bin").mkdir(parents=True)
|
|
17
|
+
package = prefix / "lib/node_modules/@xai-official/grok"
|
|
18
|
+
package.mkdir(parents=True)
|
|
19
|
+
(package / "package.json").write_text(json.dumps({"name": "@xai-official/grok", "version": "1.0.46"}))
|
|
20
|
+
scripts = {
|
|
21
|
+
old_bin / "npm": "#!/bin/sh\nprintf '%s\\n' " + shlex.quote(str(prefix)) + "\n",
|
|
22
|
+
old_bin / "grok": "#!/bin/sh\necho 'grok 0.2.118 (old)'\n",
|
|
23
|
+
prefix / "bin/grok": "#!/bin/sh\necho 'grok 1.0.46 (current)'\nprintf 'profile=%s\\n' \"$GROK_HOME\"\n",
|
|
24
|
+
}
|
|
25
|
+
for path, script in scripts.items():
|
|
26
|
+
path.write_text(script, encoding="utf-8")
|
|
27
|
+
path.chmod(0o755)
|
|
28
|
+
home = str(tmp_path / "profile with spaces")
|
|
29
|
+
profile = {"name": "account-a", "type": "grok", "mode": "login", "credentials_path": home}
|
|
30
|
+
env = os.environ.copy()
|
|
31
|
+
env["PATH"] = str(old_bin) + os.pathsep + env.get("PATH", "")
|
|
32
|
+
env["PYTHONPATH"] = str(Path(__file__).resolve().parents[1] / "src")
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
[sys.executable, "-c", "from code_ai.launcher import launch; launch(" + repr(profile) + ", ['--version'])"],
|
|
35
|
+
env=env, capture_output=True, text=True, timeout=15,
|
|
36
|
+
)
|
|
37
|
+
assert result.returncode == 0, result.stderr
|
|
38
|
+
assert "grok 1.0.46" in result.stdout
|
|
39
|
+
assert "0.2.118" not in result.stdout
|
|
40
|
+
assert "profile=" + home in result.stdout
|
|
@@ -325,3 +325,32 @@ def test_launch_windows_passes_env_correctly():
|
|
|
325
325
|
env = mock_run.call_args[1]["env"]
|
|
326
326
|
assert env["ANTHROPIC_BASE_URL"] == "https://example.com"
|
|
327
327
|
assert "ANTHROPIC_AUTH_TOKEN" in env
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@pytest.mark.parametrize("platform", ["darwin", "linux"])
|
|
331
|
+
def test_launch_grok_uses_updated_npm_entry_instead_of_shadowing_path(monkeypatch, tmp_path, platform):
|
|
332
|
+
from types import SimpleNamespace
|
|
333
|
+
from src.code_ai import launcher
|
|
334
|
+
|
|
335
|
+
profile_home = str(tmp_path / "grok-account")
|
|
336
|
+
npm_command = str(tmp_path / "npm-prefix" / "bin" / "grok")
|
|
337
|
+
monkeypatch.setattr(launcher.sys, "platform", platform)
|
|
338
|
+
monkeypatch.setattr(launcher.shutil, "which", lambda name: "/old-install/grok")
|
|
339
|
+
monkeypatch.setattr(launcher, "find_npm_installation", lambda: SimpleNamespace(command=npm_command, version="1.0.46"))
|
|
340
|
+
with patch.object(launcher.os, "execvp") as execute:
|
|
341
|
+
with patch.dict(os.environ):
|
|
342
|
+
launcher.launch({"type": "grok", "mode": "login", "name": "grok-account", "credentials_path": profile_home}, ["--version"])
|
|
343
|
+
assert os.environ["GROK_HOME"] == profile_home
|
|
344
|
+
execute.assert_called_once_with(npm_command, [npm_command, "--version"])
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_launch_grok_without_npm_preserves_native_install(monkeypatch, tmp_path):
|
|
348
|
+
from src.code_ai import launcher
|
|
349
|
+
|
|
350
|
+
monkeypatch.setattr(launcher.sys, "platform", "darwin")
|
|
351
|
+
monkeypatch.setattr(launcher.shutil, "which", lambda name: "/native/bin/grok")
|
|
352
|
+
monkeypatch.setattr(launcher, "find_npm_installation", lambda: None)
|
|
353
|
+
with patch.object(launcher.os, "execvp") as execute:
|
|
354
|
+
with patch.dict(os.environ):
|
|
355
|
+
launcher.launch({"type": "grok", "mode": "login", "credentials_path": str(tmp_path / "native-account")}, ["--version"])
|
|
356
|
+
execute.assert_called_once_with("/native/bin/grok", ["/native/bin/grok", "--version"])
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.22"
|
|
File without changes
|
|
File without changes
|
{ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/ai_code_switcher.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/ai_code_switcher.egg-info/requires.txt
RENAMED
|
File without changes
|
{ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/ai_code_switcher.egg-info/top_level.txt
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
|
{ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/platforms/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ai_code_switcher-0.1.22 → ai_code_switcher-0.1.23}/src/code_ai/desktop/platforms/windows.py
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
|