code-aide 1.15.7__tar.gz → 1.16.1__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.
Files changed (63) hide show
  1. {code_aide-1.15.7 → code_aide-1.16.1}/PKG-INFO +37 -29
  2. {code_aide-1.15.7 → code_aide-1.16.1}/README.md +35 -27
  3. {code_aide-1.15.7 → code_aide-1.16.1}/pyproject.toml +1 -1
  4. code_aide-1.16.1/script-archive/antigravity-install.sh +239 -0
  5. {code_aide-1.15.7 → code_aide-1.16.1}/script-archive/claude-install.sh +24 -0
  6. code_aide-1.16.1/specs/antigravity-cli-integration.md +35 -0
  7. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/__init__.py +1 -1
  8. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/commands_actions.py +6 -2
  9. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/config.py +21 -1
  10. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/data/tools.json +27 -4
  11. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/operations.py +7 -0
  12. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/status.py +15 -0
  13. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/versions.py +40 -0
  14. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_commands_actions.py +30 -0
  15. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_config.py +63 -0
  16. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_operations.py +21 -0
  17. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_status.py +18 -0
  18. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_versions.py +69 -0
  19. {code_aide-1.15.7 → code_aide-1.16.1}/.github/workflows/ci.yml +0 -0
  20. {code_aide-1.15.7 → code_aide-1.16.1}/.github/workflows/publish.yml +0 -0
  21. {code_aide-1.15.7 → code_aide-1.16.1}/.gitignore +0 -0
  22. {code_aide-1.15.7 → code_aide-1.16.1}/.gitlab-ci.yml +0 -0
  23. {code_aide-1.15.7 → code_aide-1.16.1}/.pre-commit-config.yaml +0 -0
  24. {code_aide-1.15.7 → code_aide-1.16.1}/AGENTS.md +0 -0
  25. {code_aide-1.15.7 → code_aide-1.16.1}/CLAUDE.md +0 -0
  26. {code_aide-1.15.7 → code_aide-1.16.1}/LICENSE +0 -0
  27. {code_aide-1.15.7 → code_aide-1.16.1}/TODO.md +0 -0
  28. {code_aide-1.15.7 → code_aide-1.16.1}/script-archive/README.md +0 -0
  29. {code_aide-1.15.7 → code_aide-1.16.1}/script-archive/amp-install.sh +0 -0
  30. {code_aide-1.15.7 → code_aide-1.16.1}/script-archive/cursor-install.sh +0 -0
  31. {code_aide-1.15.7 → code_aide-1.16.1}/specs/2026-05-03-qwen3.6-code-quality-analysis-original.md +0 -0
  32. {code_aide-1.15.7 → code_aide-1.16.1}/specs/2026-05-03-qwen3.6-code-quality-analysis.md +0 -0
  33. {code_aide-1.15.7 → code_aide-1.16.1}/specs/auto-migrate-deprecated-installs.md +0 -0
  34. {code_aide-1.15.7 → code_aide-1.16.1}/specs/claude-native-installer-migration.md +0 -0
  35. {code_aide-1.15.7 → code_aide-1.16.1}/specs/missing-coding-llm-cli-tools.md +0 -0
  36. {code_aide-1.15.7 → code_aide-1.16.1}/specs/pre-commit-uv-setup.md +0 -0
  37. {code_aide-1.15.7 → code_aide-1.16.1}/specs/remove-bundled-version-baseline.md +0 -0
  38. {code_aide-1.15.7 → code_aide-1.16.1}/specs/unify-upgrade-eligibility-with-shared-evaluator.md +0 -0
  39. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/__main__.py +0 -0
  40. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/commands_tools.py +0 -0
  41. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/console.py +0 -0
  42. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/constants.py +0 -0
  43. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/detection.py +0 -0
  44. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/detection_types.py +0 -0
  45. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/entry.py +0 -0
  46. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/install.py +0 -0
  47. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/install_types.py +0 -0
  48. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/operation_handlers.py +0 -0
  49. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/package_info.py +0 -0
  50. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/package_managers.py +0 -0
  51. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/prereqs.py +0 -0
  52. {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/status_print.py +0 -0
  53. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_commands_tools.py +0 -0
  54. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_console.py +0 -0
  55. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_constants.py +0 -0
  56. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_detection.py +0 -0
  57. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_entry.py +0 -0
  58. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_error_paths.py +0 -0
  59. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_install.py +0 -0
  60. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_install_types.py +0 -0
  61. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_package_managers.py +0 -0
  62. {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_prereqs.py +0 -0
  63. {code_aide-1.15.7 → code_aide-1.16.1}/uv.lock +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-aide
3
- Version: 1.15.7
4
- Summary: Manage AI coding CLI tools (Claude, Copilot, Cursor, Gemini, Amp, Codex)
3
+ Version: 1.16.1
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
7
7
  Project-URL: Issues, https://github.com/dajobe/code-aide/issues
@@ -30,7 +30,8 @@ Description-Content-Type: text/markdown
30
30
  An aide for your AI coding tools.
31
31
 
32
32
  Manages installation, upgrade, removal, and version tracking of AI coding CLI
33
- tools: Claude Code, Copilot, Cursor, Gemini, Amp, Codex, OpenCode, and Kilo.
33
+ tools: Claude Code, Antigravity, Copilot, Cursor, Gemini, Amp, Codex, OpenCode,
34
+ and Kilo.
34
35
 
35
36
  ## Installation
36
37
 
@@ -58,7 +59,7 @@ code-aide status
58
59
  code-aide status -l
59
60
 
60
61
  # Install specific tools
61
- code-aide install claude gemini
62
+ code-aide install claude antigravity
62
63
 
63
64
  # Install all default tools
64
65
  code-aide install
@@ -83,15 +84,19 @@ code-aide update-versions -y
83
84
 
84
85
  | Tool | Command | Install Type | Default |
85
86
  |:-------------------------|:-----------|:----------------|:--------|
86
- | Cursor CLI | `agent` | Direct download | Yes |
87
+ | Cursor CLI | `agent` | Direct download | No |
87
88
  | Claude CLI (Claude Code) | `claude` | Script | Yes |
88
- | Gemini CLI | `gemini` | npm | Yes |
89
+ | Antigravity CLI | `agy` | Script | Yes |
90
+ | Gemini CLI | `gemini` | npm | No |
89
91
  | OpenCode | `opencode` | npm | No |
90
92
  | Kilo CLI | `kilo` | npm | No |
91
93
  | Amp (Sourcegraph) | `amp` | Script | No |
92
94
  | Codex CLI | `codex` | npm | No |
93
95
  | Copilot CLI | `copilot` | npm | No |
94
96
 
97
+ Command aliases are also accepted: `agy` resolves to `antigravity`, and `agent`
98
+ resolves to `cursor`.
99
+
95
100
  ## How Version Data Works
96
101
 
97
102
  code-aide uses a two-layer version data model:
@@ -113,29 +118,32 @@ Each entry in `tools.json` describes one managed tool. Required fields are
113
118
  `name`, `command`, and `install_type`; the others are optional and only
114
119
  consulted when the relevant install method or platform applies.
115
120
 
116
- | Field | Required | Notes |
117
- |:------------------------------|:---------|:---------------------------------------------------------------------------------------------|
118
- | `name` | Yes | Display name for status output |
119
- | `command` | Yes | Binary name (looked up in `PATH`) |
120
- | `install_type` | Yes | One of `npm`, `script`, `direct_download`, `pkg` |
121
- | `version_args` | No | List of arguments passed to the binary to print its version (default: `["--version"]`) |
122
- | `version_url` | No | URL queried by `update-versions` to discover the latest upstream version |
123
- | `latest_version` | Cache | Last known upstream version (filled by `update-versions`) |
124
- | `latest_date` | Cache | Release date for `latest_version` |
125
- | `prerequisites` | No | List of required external commands (e.g. `["npm"]`) |
126
- | `min_node_version` | No | Minimum major Node.js version required |
127
- | `npm_package` | npm | npm package name (e.g. `@anthropic-ai/claude-code`) |
128
- | `install_url` | script | URL of the install script that will be downloaded and piped to `bash` |
129
- | `install_sha256` | script | SHA-256 of the install script body — required for verified installs |
130
- | `install_script_path_prepend` | script | Extra directories prepended to `PATH` when the install script runs (e.g. `["~/.local/bin"]`) |
131
- | `download_url_template` | direct | URL template with `{version}`, `{os}`, `{arch}` placeholders |
132
- | `install_dir` | direct | Directory to extract into; supports `{version}` |
133
- | `bin_dir` | direct | Directory where symlinks for `command` are created (e.g. `~/.local/bin`) |
134
- | `symlinks` | direct | Map of `link_name` → relative path inside `install_dir` to symlink into `bin_dir` |
135
- | `freebsd_port` | FreeBSD | FreeBSD ports/pkg name (used when running on FreeBSD) |
136
- | `freebsd_pkg_repo` | FreeBSD | Optional non-default pkg repository name (passed via `pkg install -r <repo>`) |
137
- | `next_steps` | No | Free-form text printed after a successful install |
138
- | `docs_url` | No | Documentation URL printed after install |
121
+ | Field | Required | Notes |
122
+ |:--------------------------------|:---------|:---------------------------------------------------------------------------------------------|
123
+ | `name` | Yes | Display name for status output |
124
+ | `command` | Yes | Binary name (looked up in `PATH`) |
125
+ | `aliases` | No | Alternate names accepted by commands and resolved to the canonical tool key |
126
+ | `install_type` | Yes | One of `npm`, `script`, `direct_download`, `pkg` |
127
+ | `version_args` | No | List of arguments passed to the binary to print its version (default: `["--version"]`) |
128
+ | `version_url` | No | URL queried by `update-versions` to discover the latest upstream version |
129
+ | `version_manifest_url_template` | No | JSON manifest URL template with `{platform}`; reads its `version` field |
130
+ | `latest_version` | Cache | Last known upstream version (filled by `update-versions`) |
131
+ | `latest_date` | Cache | Release date for `latest_version` |
132
+ | `prerequisites` | No | List of required external commands (e.g. `["npm"]`) |
133
+ | `min_node_version` | No | Minimum major Node.js version required |
134
+ | `npm_package` | npm | npm package name (e.g. `@anthropic-ai/claude-code`) |
135
+ | `install_url` | script | URL of the install script that will be downloaded and piped to `bash` |
136
+ | `install_sha256` | script | SHA-256 of the install script body — required for verified installs |
137
+ | `install_script_path_prepend` | script | Extra directories prepended to `PATH` when the install script runs (e.g. `["~/.local/bin"]`) |
138
+ | `download_url_template` | direct | URL template with `{version}`, `{os}`, `{arch}` placeholders |
139
+ | `install_dir` | direct | Directory to extract into; supports `{version}` |
140
+ | `bin_dir` | direct | Directory where symlinks for `command` are created (e.g. `~/.local/bin`) |
141
+ | `symlinks` | direct | Map of `link_name` relative path inside `install_dir` to symlink into `bin_dir` |
142
+ | `freebsd_port` | FreeBSD | FreeBSD ports/pkg name (used when running on FreeBSD) |
143
+ | `freebsd_pkg_repo` | FreeBSD | Optional non-default pkg repository name (passed via `pkg install -r <repo>`) |
144
+ | `next_steps` | No | Free-form text printed after a successful install |
145
+ | `docs_url` | No | Documentation URL printed after install |
146
+ | `self_updates` | No | Tool updates itself when run; excludes it from managed upgrade actions |
139
147
 
140
148
  ## Features
141
149
 
@@ -3,7 +3,8 @@
3
3
  An aide for your AI coding tools.
4
4
 
5
5
  Manages installation, upgrade, removal, and version tracking of AI coding CLI
6
- tools: Claude Code, Copilot, Cursor, Gemini, Amp, Codex, OpenCode, and Kilo.
6
+ tools: Claude Code, Antigravity, Copilot, Cursor, Gemini, Amp, Codex, OpenCode,
7
+ and Kilo.
7
8
 
8
9
  ## Installation
9
10
 
@@ -31,7 +32,7 @@ code-aide status
31
32
  code-aide status -l
32
33
 
33
34
  # Install specific tools
34
- code-aide install claude gemini
35
+ code-aide install claude antigravity
35
36
 
36
37
  # Install all default tools
37
38
  code-aide install
@@ -56,15 +57,19 @@ code-aide update-versions -y
56
57
 
57
58
  | Tool | Command | Install Type | Default |
58
59
  |:-------------------------|:-----------|:----------------|:--------|
59
- | Cursor CLI | `agent` | Direct download | Yes |
60
+ | Cursor CLI | `agent` | Direct download | No |
60
61
  | Claude CLI (Claude Code) | `claude` | Script | Yes |
61
- | Gemini CLI | `gemini` | npm | Yes |
62
+ | Antigravity CLI | `agy` | Script | Yes |
63
+ | Gemini CLI | `gemini` | npm | No |
62
64
  | OpenCode | `opencode` | npm | No |
63
65
  | Kilo CLI | `kilo` | npm | No |
64
66
  | Amp (Sourcegraph) | `amp` | Script | No |
65
67
  | Codex CLI | `codex` | npm | No |
66
68
  | Copilot CLI | `copilot` | npm | No |
67
69
 
70
+ Command aliases are also accepted: `agy` resolves to `antigravity`, and `agent`
71
+ resolves to `cursor`.
72
+
68
73
  ## How Version Data Works
69
74
 
70
75
  code-aide uses a two-layer version data model:
@@ -86,29 +91,32 @@ Each entry in `tools.json` describes one managed tool. Required fields are
86
91
  `name`, `command`, and `install_type`; the others are optional and only
87
92
  consulted when the relevant install method or platform applies.
88
93
 
89
- | Field | Required | Notes |
90
- |:------------------------------|:---------|:---------------------------------------------------------------------------------------------|
91
- | `name` | Yes | Display name for status output |
92
- | `command` | Yes | Binary name (looked up in `PATH`) |
93
- | `install_type` | Yes | One of `npm`, `script`, `direct_download`, `pkg` |
94
- | `version_args` | No | List of arguments passed to the binary to print its version (default: `["--version"]`) |
95
- | `version_url` | No | URL queried by `update-versions` to discover the latest upstream version |
96
- | `latest_version` | Cache | Last known upstream version (filled by `update-versions`) |
97
- | `latest_date` | Cache | Release date for `latest_version` |
98
- | `prerequisites` | No | List of required external commands (e.g. `["npm"]`) |
99
- | `min_node_version` | No | Minimum major Node.js version required |
100
- | `npm_package` | npm | npm package name (e.g. `@anthropic-ai/claude-code`) |
101
- | `install_url` | script | URL of the install script that will be downloaded and piped to `bash` |
102
- | `install_sha256` | script | SHA-256 of the install script body — required for verified installs |
103
- | `install_script_path_prepend` | script | Extra directories prepended to `PATH` when the install script runs (e.g. `["~/.local/bin"]`) |
104
- | `download_url_template` | direct | URL template with `{version}`, `{os}`, `{arch}` placeholders |
105
- | `install_dir` | direct | Directory to extract into; supports `{version}` |
106
- | `bin_dir` | direct | Directory where symlinks for `command` are created (e.g. `~/.local/bin`) |
107
- | `symlinks` | direct | Map of `link_name` → relative path inside `install_dir` to symlink into `bin_dir` |
108
- | `freebsd_port` | FreeBSD | FreeBSD ports/pkg name (used when running on FreeBSD) |
109
- | `freebsd_pkg_repo` | FreeBSD | Optional non-default pkg repository name (passed via `pkg install -r <repo>`) |
110
- | `next_steps` | No | Free-form text printed after a successful install |
111
- | `docs_url` | No | Documentation URL printed after install |
94
+ | Field | Required | Notes |
95
+ |:--------------------------------|:---------|:---------------------------------------------------------------------------------------------|
96
+ | `name` | Yes | Display name for status output |
97
+ | `command` | Yes | Binary name (looked up in `PATH`) |
98
+ | `aliases` | No | Alternate names accepted by commands and resolved to the canonical tool key |
99
+ | `install_type` | Yes | One of `npm`, `script`, `direct_download`, `pkg` |
100
+ | `version_args` | No | List of arguments passed to the binary to print its version (default: `["--version"]`) |
101
+ | `version_url` | No | URL queried by `update-versions` to discover the latest upstream version |
102
+ | `version_manifest_url_template` | No | JSON manifest URL template with `{platform}`; reads its `version` field |
103
+ | `latest_version` | Cache | Last known upstream version (filled by `update-versions`) |
104
+ | `latest_date` | Cache | Release date for `latest_version` |
105
+ | `prerequisites` | No | List of required external commands (e.g. `["npm"]`) |
106
+ | `min_node_version` | No | Minimum major Node.js version required |
107
+ | `npm_package` | npm | npm package name (e.g. `@anthropic-ai/claude-code`) |
108
+ | `install_url` | script | URL of the install script that will be downloaded and piped to `bash` |
109
+ | `install_sha256` | script | SHA-256 of the install script body — required for verified installs |
110
+ | `install_script_path_prepend` | script | Extra directories prepended to `PATH` when the install script runs (e.g. `["~/.local/bin"]`) |
111
+ | `download_url_template` | direct | URL template with `{version}`, `{os}`, `{arch}` placeholders |
112
+ | `install_dir` | direct | Directory to extract into; supports `{version}` |
113
+ | `bin_dir` | direct | Directory where symlinks for `command` are created (e.g. `~/.local/bin`) |
114
+ | `symlinks` | direct | Map of `link_name` relative path inside `install_dir` to symlink into `bin_dir` |
115
+ | `freebsd_port` | FreeBSD | FreeBSD ports/pkg name (used when running on FreeBSD) |
116
+ | `freebsd_pkg_repo` | FreeBSD | Optional non-default pkg repository name (passed via `pkg install -r <repo>`) |
117
+ | `next_steps` | No | Free-form text printed after a successful install |
118
+ | `docs_url` | No | Documentation URL printed after install |
119
+ | `self_updates` | No | Tool updates itself when run; excludes it from managed upgrade actions |
112
120
 
113
121
  ## Features
114
122
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "code-aide"
3
3
  dynamic = ["version"]
4
- description = "Manage AI coding CLI tools (Claude, Copilot, Cursor, Gemini, Amp, Codex)"
4
+ description = "Manage AI coding CLI tools (Claude, Antigravity, Copilot, Cursor, Gemini, Amp, Codex)"
5
5
  readme = "README.md"
6
6
  authors = [
7
7
  { name = "Dave Beckett" },
@@ -0,0 +1,239 @@
1
+ #!/bin/bash
2
+ #
3
+ # Antigravity CLI - Unix Bootstrapper Script (Bash/Zsh/Fish)
4
+ #
5
+ # Downloads, staging-verifies, and installs the Antigravity CLI flat native build.
6
+ #
7
+
8
+ set -euo pipefail
9
+
10
+ # 1. Default Setup & Constants
11
+ DOWNLOAD_BASE_URL="https://antigravity-cli-auto-updater-974169037036.us-central1.run.app"
12
+ TARGET_DIR="$HOME/.local/bin"
13
+ CUSTOM_DIR=""
14
+
15
+ # Helper: Display usage instructions
16
+ show_usage() {
17
+ echo "Usage: install.sh [options]"
18
+ echo ""
19
+ echo "Options:"
20
+ echo " -d, --dir <path> Specify a custom directory to install the binary"
21
+ echo " -h, --help Display this help menu"
22
+ echo ""
23
+ }
24
+
25
+ # Parse Arguments
26
+ while [ "$#" -gt 0 ]; do
27
+ case $1 in
28
+ -d|--dir)
29
+ if [ -z "${2:-}" ]; then
30
+ echo "[ERROR] Missing path for --dir parameter" >&2
31
+ exit 1
32
+ fi
33
+ CUSTOM_DIR="$2"
34
+ shift
35
+ ;;
36
+ -h|--help)
37
+ show_usage
38
+ exit 0
39
+ ;;
40
+ *)
41
+ echo "[ERROR] Unknown parameter: $1" >&2
42
+ show_usage
43
+ exit 1
44
+ ;;
45
+ esac
46
+ shift
47
+ done
48
+
49
+ # Resolve dynamic installation target directory
50
+ if [ -n "$CUSTOM_DIR" ]; then
51
+ TARGET_DIR="$CUSTOM_DIR"
52
+ fi
53
+
54
+ BINARY_PATH="$TARGET_DIR/agy"
55
+
56
+ # 2. STEP 1: Pre-existence & Dynamic Path Check
57
+ if [ -f "$BINARY_PATH" ]; then
58
+ echo "Notice: 'agy' is already installed at $BINARY_PATH."
59
+ echo "The Antigravity CLI automatically self-updates in the background during regular runs."
60
+ echo ""
61
+ echo "If you want to perform a fresh installation, delete the binary first:"
62
+ echo " rm \"$BINARY_PATH\""
63
+ exit 0
64
+ fi
65
+
66
+ echo "⠋ Detecting system environment..."
67
+
68
+ # 3. Detect Platform
69
+ case "$(uname -s)" in
70
+ Darwin) os="darwin" ;;
71
+ Linux) os="linux" ;;
72
+ *) echo "Fatal: Unsupported operating system: $(uname -s). Antigravity CLI currently supports 64-bit Windows, macOS, and Linux." >&2; exit 1 ;;
73
+ esac
74
+
75
+ case "$(uname -m)" in
76
+ x86_64|amd64) arch="amd64" ;;
77
+ arm64|aarch64) arch="arm64" ;;
78
+ *) echo "Fatal: Unsupported architecture: $(uname -m). Antigravity CLI currently supports 64-bit Windows, macOS, and Linux." >&2; exit 1 ;;
79
+ esac
80
+
81
+ # musl libc detection on Linux
82
+ platform=""
83
+ if [ "$os" = "linux" ]; then
84
+ if [ -f /lib/libc.musl-x86_64.so.1 ] || [ -f /lib/libc.musl-aarch64.so.1 ] || ldd /bin/ls 2>&1 | grep -q musl; then
85
+ platform="linux_${arch}_musl"
86
+ else
87
+ platform="linux_${arch}"
88
+ fi
89
+ else
90
+ platform="${os}_${arch}"
91
+ fi
92
+
93
+ echo "✓ Platform detected: $platform"
94
+
95
+ # 4. Manifest Query & JSON Parsing (POSIX-Compliant)
96
+ echo "⠋ Querying release repository..."
97
+
98
+ # Construct Platform JSON Manifest URL
99
+ MANIFEST_URL="$DOWNLOAD_BASE_URL/manifests/$platform.json"
100
+
101
+ DOWNLOADER=""
102
+ if command -v curl >/dev/null 2>&1; then
103
+ DOWNLOADER="curl"
104
+ elif command -v wget >/dev/null 2>&1; then
105
+ DOWNLOADER="wget"
106
+ else
107
+ echo "Fatal: Either curl or wget is required but neither is installed." >&2
108
+ exit 1
109
+ fi
110
+
111
+ fetch_manifest() {
112
+ if [ "$DOWNLOADER" = "curl" ]; then
113
+ curl -fsSL "$1"
114
+ else
115
+ wget -q -O - "$1"
116
+ fi
117
+ }
118
+
119
+ manifest_json=$(fetch_manifest "$MANIFEST_URL" 2>/dev/null || true)
120
+ if [ -z "$manifest_json" ]; then
121
+ echo "Fatal: Could not connect to the release server to download the manifest. Please check your internet connection or firewall settings." >&2
122
+ exit 1
123
+ fi
124
+
125
+ # POSIX-compliant JSON parser (no jq or grep -o dependencies)
126
+ parse_json_key() {
127
+ local payload="$1"
128
+ local key="$2"
129
+ echo "$payload" | sed -n 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
130
+ }
131
+
132
+ version=$(parse_json_key "$manifest_json" "version")
133
+ url=$(parse_json_key "$manifest_json" "url")
134
+ sha512=$(parse_json_key "$manifest_json" "sha512")
135
+
136
+ if [ -z "$url" ] || [ -z "$sha512" ]; then
137
+ echo "Fatal: Failed to parse release manifest. The manifest may be corrupted or malformed." >&2
138
+ exit 1
139
+ fi
140
+
141
+ echo "✓ Latest available version: $version"
142
+
143
+ # 5. Download & SHA512 Checksum Verification
144
+ STAGING_DIR="$HOME/.cache/antigravity/staging"
145
+ if ! mkdir -p "$STAGING_DIR" 2>/dev/null; then
146
+ echo "Error: Failed to create staging directory at $STAGING_DIR. Please check your home directory write permissions." >&2
147
+ exit 1
148
+ fi
149
+
150
+ is_tar_gz=false
151
+ case "$url" in
152
+ *.tar.gz*) is_tar_gz=true ;;
153
+ esac
154
+
155
+ if [ "$is_tar_gz" = true ]; then
156
+ staging_payload="$STAGING_DIR/agy.tar.gz"
157
+ extracted_binary="$STAGING_DIR/antigravity"
158
+ else
159
+ staging_payload="$STAGING_DIR/agy"
160
+ extracted_binary="$staging_payload"
161
+ fi
162
+
163
+ # Robust cleanup trap to ensure staging files are reaped on any exit (success or failure)
164
+ cleanup() {
165
+ rm -f "${staging_payload:-}" "${extracted_binary:-}" 2>/dev/null || true
166
+ }
167
+ trap cleanup EXIT
168
+
169
+ download_file() {
170
+ local src="$1"
171
+ local dst="$2"
172
+ if [ "$DOWNLOADER" = "curl" ]; then
173
+ curl -fsSL -o "$dst" "$src"
174
+ else
175
+ wget -q -O "$dst" "$src"
176
+ fi
177
+ }
178
+
179
+ echo "⠋ Downloading release package..."
180
+ if ! download_file "$url" "$staging_payload"; then
181
+ echo "Fatal: Failed to download release package from $url. Please check your internet connection or firewall settings." >&2
182
+ exit 1
183
+ fi
184
+
185
+ # Compute OS-Specific SHA512 Checksum
186
+ actual_hash=""
187
+ if [ "$os" = "darwin" ]; then
188
+ actual_hash=$(shasum -a 512 "$staging_payload" | cut -d' ' -f1 || true)
189
+ else
190
+ actual_hash=$(sha512sum "$staging_payload" | cut -d' ' -f1 || true)
191
+ fi
192
+
193
+ if [ "$actual_hash" != "$sha512" ]; then
194
+ echo "Security Halt: The downloaded payload checksum does not match the manifest. The file may be corrupted or compromised. Installation aborted." >&2
195
+ exit 1
196
+ fi
197
+ echo "✓ Download complete and checksum verified."
198
+
199
+ # 6. Direct Binary Extraction & Write Permission Validation
200
+ if ! mkdir -p "$TARGET_DIR" 2>/dev/null; then
201
+ echo "Write Error: Permission denied when attempting to create $TARGET_DIR. Please re-run the installer using the '--dir' flag to specify a writable custom directory." >&2
202
+ exit 1
203
+ fi
204
+
205
+ if [ "$is_tar_gz" = true ]; then
206
+ echo "⠋ Extracting binary from archive..."
207
+ if ! tar -xzf "$staging_payload" -C "$STAGING_DIR" antigravity 2>/dev/null; then
208
+ echo "Extraction Error: Failed to extract binary from archive." >&2
209
+ exit 1
210
+ fi
211
+ else
212
+ echo "⠋ Copying binary directly to destination..."
213
+ fi
214
+
215
+ if ! cp "$extracted_binary" "$BINARY_PATH" 2>/dev/null; then
216
+ echo "Write Error: Permission denied when attempting to write binary to $BINARY_PATH. Please re-run the installer using the '--dir' flag to specify a writable custom directory." >&2
217
+ exit 1
218
+ fi
219
+
220
+ # Ensure Executable Permission Bit (soft fallback if filesystem doesn't support chmod)
221
+ chmod +x "$BINARY_PATH" || echo "Warning: Could not set executable permission bit on $BINARY_PATH. You may need to ensure the partition supports execution." >&2
222
+
223
+ # Clear macOS Gatekeeper Quarantine attribute to prevent unidentified developer blocks
224
+ if [ "$os" = "darwin" ]; then
225
+ xattr -d com.apple.quarantine "$BINARY_PATH" 2>/dev/null || true
226
+ fi
227
+
228
+ # 7. Native Setup Handoff (Go-Native Setup Trigger - POSIX /bin/sh Compliant)
229
+ echo "⠋ Configuring shell environment..."
230
+
231
+ if [ -n "$CUSTOM_DIR" ]; then
232
+ # Run with custom directory and absorb non-fatal soft warning exits
233
+ "$BINARY_PATH" install --dir "$CUSTOM_DIR" || true
234
+ else
235
+ # Run standard configuration and absorb non-fatal soft warning exits
236
+ "$BINARY_PATH" install || true
237
+ fi
238
+
239
+
@@ -11,6 +11,30 @@ if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^(stable|latest|[0-9]+\.[0-9]+\.[0-9]
11
11
  exit 1
12
12
  fi
13
13
 
14
+ # Refuse to run under sudo from a regular user's shell. This installer puts
15
+ # everything under $HOME, which under sudo typically resolves to root's home:
16
+ # the binary lands in /root/.local/bin (or is left root-owned in the user's
17
+ # home, depending on the distro's sudo configuration), and the 'claude'
18
+ # command is then not found in the user's own shell. Plain root with no sudo
19
+ # (containers, CI, root-only systems) is unaffected by this check.
20
+ if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ] && [ -z "${CLAUDE_INSTALL_ALLOW_SUDO:-}" ]; then
21
+ echo "Error: do not run this installer with sudo." >&2
22
+ echo "" >&2
23
+ echo "Claude Code installs into your home directory and does not need root access." >&2
24
+ echo "With sudo, the installation would go into root's home directory instead of" >&2
25
+ echo "yours, and the 'claude' command would not work from your own shell." >&2
26
+ echo "" >&2
27
+ echo "Please re-run the same command without sudo, e.g.:" >&2
28
+ # pinned-dep-allow: display-only guidance text in an error message, not an executed install; install.sh is Anthropic's own installer
29
+ echo " curl -fsSL https://claude.ai/install.sh | bash" >&2
30
+ echo "" >&2
31
+ echo "To intentionally install Claude Code for the root user, re-run with" >&2
32
+ echo "CLAUDE_INSTALL_ALLOW_SUDO=1 set in the installer's environment, e.g.:" >&2
33
+ # pinned-dep-allow: display-only guidance text in an error message, not an executed install; install.sh is Anthropic's own installer
34
+ echo " curl -fsSL https://claude.ai/install.sh | sudo CLAUDE_INSTALL_ALLOW_SUDO=1 bash" >&2
35
+ exit 1
36
+ fi
37
+
14
38
  DOWNLOAD_BASE_URL="https://downloads.claude.ai/claude-code-releases"
15
39
  DOWNLOAD_DIR="$HOME/.claude/downloads"
16
40
 
@@ -0,0 +1,35 @@
1
+ # Add Antigravity CLI support
2
+
3
+ ## Context
4
+
5
+ Google transitioned consumer Gemini CLI access to Antigravity CLI on June 18,
6
+ 2026. Gemini CLI remains valid for enterprise, Google Cloud, and supported API
7
+ key authentication, so code-aide must manage the two commands separately and
8
+ must not remove or migrate existing Gemini installations.
9
+
10
+ ## Design
11
+
12
+ - Add Antigravity as the default tool key `antigravity`, using command `agy`.
13
+ - Accept `agy` as a command-line alias for the canonical `antigravity` key.
14
+ - Install it through Google's native script and verify the archived script's
15
+ SHA-256 before execution.
16
+ - Query the installer's official platform manifest and read its `version` field
17
+ when refreshing the latest-version cache.
18
+ - Record that `agy` uses its native background self-updater, so code-aide does
19
+ not repeatedly invoke an installer that intentionally exits when the binary
20
+ already exists.
21
+ - Keep Gemini CLI available as an opt-in npm tool with updated authentication
22
+ guidance.
23
+ - Do not infer a user's Google subscription or automatically migrate Gemini.
24
+
25
+ The manifest URL uses `{platform}` values such as `darwin_arm64` and
26
+ `linux_amd64`. The formatter supports the macOS and Linux architectures that the
27
+ upstream Unix installer supports.
28
+
29
+ ## Verification
30
+
31
+ 1. Confirm the archived installer exactly matches the configured SHA-256.
32
+ 2. Test manifest URL formatting and JSON version extraction.
33
+ 3. Run `uv run pytest` and `pre-commit run --all-files`.
34
+ 4. Dry-run `code-aide update-versions` and confirm Antigravity reports the
35
+ manifest version.
@@ -1,3 +1,3 @@
1
1
  """code-aide - Manage AI coding CLI tools."""
2
2
 
3
- __version__ = "1.15.7"
3
+ __version__ = "1.16.1"
@@ -37,6 +37,7 @@ from code_aide.config import (
37
37
  load_bundled_tools,
38
38
  load_versions_cache,
39
39
  merge_cached_versions,
40
+ resolve_tool_names,
40
41
  save_updated_versions,
41
42
  )
42
43
 
@@ -65,6 +66,7 @@ def cmd_install(args: argparse.Namespace) -> None:
65
66
  f"{', '.join(tools_to_install)}"
66
67
  )
67
68
 
69
+ tools_to_install = resolve_tool_names(tools_to_install, TOOLS)
68
70
  validate_tools(tools_to_install)
69
71
 
70
72
  if not dryrun:
@@ -144,6 +146,7 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
144
146
  info("All installed tools are up to date")
145
147
  return
146
148
 
149
+ tools_to_upgrade = resolve_tool_names(tools_to_upgrade, TOOLS)
147
150
  validate_tools(tools_to_upgrade)
148
151
 
149
152
  updated = []
@@ -204,6 +207,7 @@ def cmd_remove(args: argparse.Namespace) -> None:
204
207
  tools_to_remove = list(TOOLS.keys())
205
208
  info(f"No tools specified, removing all: {', '.join(tools_to_remove)}")
206
209
 
210
+ tools_to_remove = resolve_tool_names(tools_to_remove, TOOLS)
207
211
  validate_tools(tools_to_remove)
208
212
 
209
213
  removed = []
@@ -253,12 +257,12 @@ def cmd_update_versions(args: argparse.Namespace) -> None:
253
257
  config: Dict[str, Any] = {"tools": tools}
254
258
 
255
259
  if args.tools:
256
- invalid = [tool for tool in args.tools if tool not in tools]
260
+ tool_names = resolve_tool_names(args.tools, tools)
261
+ invalid = [tool for tool in tool_names if tool not in tools]
257
262
  if invalid:
258
263
  error(f"Unknown tool(s): {', '.join(invalid)}")
259
264
  print(f"Available: {', '.join(tools.keys())}", file=sys.stderr)
260
265
  sys.exit(1)
261
- tool_names = args.tools
262
266
  else:
263
267
  tool_names = list(tools.keys())
264
268
 
@@ -118,6 +118,23 @@ def load_tools_config() -> dict:
118
118
  return tools
119
119
 
120
120
 
121
+ def resolve_tool_names(
122
+ names: list[str], tools: Mapping[str, Dict[str, Any]]
123
+ ) -> list[str]:
124
+ """Resolve configured aliases to canonical tool names, preserving order."""
125
+ aliases = {
126
+ alias: tool_name
127
+ for tool_name, tool_config in tools.items()
128
+ for alias in tool_config.get("aliases", [])
129
+ }
130
+ resolved = []
131
+ for name in names:
132
+ canonical = aliases.get(name, name)
133
+ if canonical not in resolved:
134
+ resolved.append(canonical)
135
+ return resolved
136
+
137
+
121
138
  def save_updated_versions(tools: Mapping[str, Dict[str, Any]]) -> None:
122
139
  """Save only dynamic version fields to the user's cache.
123
140
 
@@ -158,7 +175,10 @@ def versions_cache_is_fresh(tools: Mapping[str, Dict[str, Any]]) -> bool:
158
175
  if not tool_config.get("latest_version"):
159
176
  return False
160
177
  elif install_type in (InstallType.SCRIPT, InstallType.DIRECT_DOWNLOAD):
161
- if tool_config.get("version_url") and not tool_config.get("latest_version"):
178
+ has_version_source = tool_config.get("version_url") or tool_config.get(
179
+ "version_manifest_url_template"
180
+ )
181
+ if has_version_source and not tool_config.get("latest_version"):
162
182
  return False
163
183
  return True
164
184
 
@@ -4,6 +4,9 @@
4
4
  "cursor": {
5
5
  "name": "Cursor CLI",
6
6
  "command": "agent",
7
+ "aliases": [
8
+ "agent"
9
+ ],
7
10
  "install_type": "direct_download",
8
11
  "install_url": "https://cursor.com/install",
9
12
  "install_script_note": "Legacy install script archived but no longer used; switched to direct_download which extracts the binary directly without a shell script",
@@ -21,14 +24,14 @@
21
24
  "--version"
22
25
  ],
23
26
  "docs_url": "https://docs.cursor.com/",
24
- "default_install": true
27
+ "default_install": false
25
28
  },
26
29
  "claude": {
27
30
  "name": "Claude CLI (Claude Code)",
28
31
  "command": "claude",
29
32
  "install_type": "script",
30
33
  "install_url": "https://claude.ai/install.sh",
31
- "install_sha256": "b3f79015b54c751440a6488f07b1b64f9088742b9052bc1bd356d13108320d2a",
34
+ "install_sha256": "cde4f1702d3b1695f92b73d26888364e17bca476e17f0fd676484c951d36c125",
32
35
  "version_url": "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest",
33
36
  "prerequisites": [],
34
37
  "min_node_version": null,
@@ -50,15 +53,35 @@
50
53
  "npm"
51
54
  ],
52
55
  "min_node_version": 20,
53
- "next_steps": "Run 'gemini' and choose 'Login with Google' to authenticate",
56
+ "next_steps": "Run 'gemini' and authenticate with a supported enterprise, Google Cloud, or API-key account",
54
57
  "version_args": [
55
58
  "--version"
56
59
  ],
57
60
  "docs_url": "https://github.com/google-gemini/gemini-cli",
58
- "default_install": true,
61
+ "default_install": false,
59
62
  "freebsd_port": "gemini-cli",
60
63
  "freebsd_pkg_repo": "FreeBSD-latest"
61
64
  },
65
+ "antigravity": {
66
+ "name": "Antigravity CLI",
67
+ "command": "agy",
68
+ "aliases": [
69
+ "agy"
70
+ ],
71
+ "install_type": "script",
72
+ "install_url": "https://antigravity.google/cli/install.sh",
73
+ "install_sha256": "ee1ea43ce4e9e56356c4ab6dad907ef357ae4bdfcaadb682735909fb57c9c640",
74
+ "version_manifest_url_template": "https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/{platform}.json",
75
+ "prerequisites": [],
76
+ "min_node_version": null,
77
+ "next_steps": "Run 'agy' to authenticate and start",
78
+ "version_args": [
79
+ "--version"
80
+ ],
81
+ "docs_url": "https://antigravity.google/docs/cli-overview",
82
+ "self_updates": true,
83
+ "default_install": true
84
+ },
62
85
  "amp": {
63
86
  "name": "Amp (Sourcegraph)",
64
87
  "command": "amp",
@@ -187,6 +187,13 @@ def upgrade_tool(tool_name: str) -> UpgradeResult:
187
187
  if is_deprecated_install(tool_name):
188
188
  return _migrate_install_method(tool_name)
189
189
 
190
+ if tool_config.get("self_updates"):
191
+ info(
192
+ f"{tool_config['name']} updates itself in the background when it runs; "
193
+ f"launch '{tool_config['command']}' to allow it to update."
194
+ )
195
+ return UpgradeResult.UNCHANGED
196
+
190
197
  install_info = detect_install_method(tool_name)
191
198
  method = parse_install_method(install_info["method"])
192
199
  detail = install_info["detail"]
@@ -58,6 +58,7 @@ class UpgradeDecision(Enum):
58
58
  UPGRADE = auto()
59
59
  MIGRATION = auto()
60
60
  PACKAGE_MANAGED = auto()
61
+ SELF_UPDATING = auto()
61
62
  NOT_INSTALLED = auto()
62
63
  UNKNOWN = auto()
63
64
 
@@ -183,6 +184,20 @@ class ToolUpgradeEvaluator:
183
184
  package_info: Optional[PackageInfo],
184
185
  ) -> ToolUpgradeAssessment:
185
186
  version_state = self._catalog_version_state(status.get("version"))
187
+ if self.tool_config.get("self_updates"):
188
+ decision = (
189
+ UpgradeDecision.CURRENT
190
+ if version_state == VersionDisplayState.UP_TO_DATE
191
+ else UpgradeDecision.SELF_UPDATING
192
+ )
193
+ return self._result(
194
+ decision=decision,
195
+ version_state=version_state,
196
+ status=status,
197
+ install_info=install_info,
198
+ package_info=package_info,
199
+ )
200
+
186
201
  if version_state == VersionDisplayState.UP_TO_DATE:
187
202
  decision = UpgradeDecision.CURRENT
188
203
  elif version_state == VersionDisplayState.OUTDATED:
@@ -3,6 +3,7 @@
3
3
  import email.utils
4
4
  import hashlib
5
5
  import json
6
+ import platform
6
7
  import re
7
8
  import urllib.request
8
9
  from datetime import datetime, timezone
@@ -116,6 +117,25 @@ def version_is_newer(version_a: str, version_b: str) -> bool:
116
117
  return a_parts > b_parts
117
118
 
118
119
 
120
+ def format_version_manifest_url(url_template: str) -> str:
121
+ """Fill a manifest URL template with the current OS and architecture."""
122
+ os_name = platform.system().lower()
123
+ if os_name not in ("darwin", "linux"):
124
+ raise ValueError(f"Unsupported manifest operating system: {os_name}")
125
+
126
+ machine = platform.machine().lower()
127
+ architecture = {
128
+ "x86_64": "amd64",
129
+ "amd64": "amd64",
130
+ "arm64": "arm64",
131
+ "aarch64": "arm64",
132
+ }.get(machine)
133
+ if architecture is None:
134
+ raise ValueError(f"Unsupported manifest architecture: {machine}")
135
+
136
+ return url_template.format(platform=f"{os_name}_{architecture}")
137
+
138
+
119
139
  def check_npm_tool(
120
140
  tool_name: str, tool_config: Dict[str, Any], verbose: bool = False
121
141
  ) -> Dict[str, Any]:
@@ -176,6 +196,26 @@ def extract_script_version(
176
196
  script_content: bytes,
177
197
  ) -> Optional[str]:
178
198
  """Try to extract a version string from script content or version URL."""
199
+ version_manifest_url_template = tool_config.get("version_manifest_url_template")
200
+ if version_manifest_url_template:
201
+ try:
202
+ version_manifest_url = format_version_manifest_url(
203
+ version_manifest_url_template
204
+ )
205
+ manifest_data, _ = fetch_url(version_manifest_url)
206
+ manifest = json.loads(manifest_data)
207
+ version = manifest.get("version")
208
+ if isinstance(version, str) and version.strip():
209
+ return version.strip()
210
+ except (
211
+ OSError,
212
+ UnicodeDecodeError,
213
+ json.JSONDecodeError,
214
+ TypeError,
215
+ ValueError,
216
+ ):
217
+ pass
218
+
179
219
  version_url = tool_config.get("version_url")
180
220
  if version_url:
181
221
  try:
@@ -83,6 +83,36 @@ class TestCmdInstall(unittest.TestCase):
83
83
 
84
84
  mock_check_path.assert_called_once_with(["/home/test/.npm-packages/bin"])
85
85
 
86
+ def test_resolves_tool_alias_before_installing(self):
87
+ tools = {
88
+ "antigravity": {
89
+ "name": "Antigravity CLI",
90
+ "command": "agy",
91
+ "aliases": ["agy"],
92
+ "install_type": "script",
93
+ "next_steps": "run agy",
94
+ }
95
+ }
96
+ args = type(
97
+ "Args",
98
+ (),
99
+ {"tools": ["agy"], "dryrun": False, "install_prerequisites": False},
100
+ )()
101
+
102
+ with (
103
+ mock.patch.dict(constants._TOOLS_DATA, tools, clear=True),
104
+ mock.patch.object(commands_actions, "check_prerequisites"),
105
+ mock.patch.object(
106
+ commands_actions,
107
+ "install_tool",
108
+ return_value=InstallOutcome(True),
109
+ ) as mock_install,
110
+ mock.patch.object(commands_actions, "check_path_directories"),
111
+ ):
112
+ commands_actions.cmd_install(args)
113
+
114
+ mock_install.assert_called_once_with("antigravity", dryrun=False)
115
+
86
116
 
87
117
  class TestCmdUpdateVersions(unittest.TestCase):
88
118
  """Tests for cmd_update_versions."""
@@ -38,6 +38,7 @@ class TestLoadBundledTools(unittest.TestCase):
38
38
  self.assertIn("claude", tools)
39
39
  self.assertIn("copilot", tools)
40
40
  self.assertIn("gemini", tools)
41
+ self.assertIn("antigravity", tools)
41
42
  self.assertIn("opencode", tools)
42
43
  self.assertIn("kilo", tools)
43
44
  for tool_name, tool_data in tools.items():
@@ -46,6 +47,68 @@ class TestLoadBundledTools(unittest.TestCase):
46
47
  self.assertIn("install_type", tool_data)
47
48
 
48
49
 
50
+ class TestAntigravityConfig(unittest.TestCase):
51
+ """Antigravity remains separate from the supported Gemini CLI entry."""
52
+
53
+ def test_antigravity_uses_verified_script_and_manifest(self):
54
+ tools = code_aide_config.load_bundled_tools()["tools"]
55
+ antigravity = tools["antigravity"]
56
+
57
+ self.assertEqual(antigravity["command"], "agy")
58
+ self.assertIn("agy", antigravity["aliases"])
59
+ self.assertEqual(antigravity["install_type"], "script")
60
+ self.assertIn("install_sha256", antigravity)
61
+ self.assertIn("version_manifest_url_template", antigravity)
62
+ self.assertTrue(antigravity["self_updates"])
63
+ self.assertTrue(antigravity["default_install"])
64
+
65
+ def test_gemini_remains_available_but_is_not_default(self):
66
+ gemini = code_aide_config.load_bundled_tools()["tools"]["gemini"]
67
+
68
+ self.assertEqual(gemini["command"], "gemini")
69
+ self.assertFalse(gemini["default_install"])
70
+
71
+ def test_cursor_remains_available_but_is_not_default(self):
72
+ cursor = code_aide_config.load_bundled_tools()["tools"]["cursor"]
73
+
74
+ self.assertEqual(cursor["command"], "agent")
75
+ self.assertFalse(cursor["default_install"])
76
+
77
+
78
+ class TestResolveToolNames(unittest.TestCase):
79
+ """Tool aliases resolve without creating duplicate catalog entries."""
80
+
81
+ def test_resolves_alias_to_canonical_name(self):
82
+ tools = {
83
+ "antigravity": {"aliases": ["agy"]},
84
+ "gemini": {},
85
+ }
86
+ self.assertEqual(
87
+ code_aide_config.resolve_tool_names(["agy"], tools),
88
+ ["antigravity"],
89
+ )
90
+
91
+ def test_resolves_command_name_alias(self):
92
+ tools = {"cursor": {"aliases": ["agent"]}}
93
+ self.assertEqual(
94
+ code_aide_config.resolve_tool_names(["agent"], tools),
95
+ ["cursor"],
96
+ )
97
+
98
+ def test_deduplicates_alias_and_canonical_name(self):
99
+ tools = {"antigravity": {"aliases": ["agy"]}}
100
+ self.assertEqual(
101
+ code_aide_config.resolve_tool_names(["agy", "antigravity"], tools),
102
+ ["antigravity"],
103
+ )
104
+
105
+ def test_preserves_unknown_name_for_validation(self):
106
+ self.assertEqual(
107
+ code_aide_config.resolve_tool_names(["missing"], {}),
108
+ ["missing"],
109
+ )
110
+
111
+
49
112
  class TestVersionsCacheRoundTrip(unittest.TestCase):
50
113
  """Tests for config save/load versions cache."""
51
114
 
@@ -197,6 +197,27 @@ class TestMigrateInstallMethod(unittest.TestCase):
197
197
  self.assertEqual(result, UpgradeResult.CHANGED)
198
198
  mock_script.assert_called_once()
199
199
 
200
+ def test_upgrade_self_updating_tool_does_not_rerun_installer(self):
201
+ tool_config = {
202
+ "name": "Self-updating Tool",
203
+ "command": "self-tool",
204
+ "install_type": "script",
205
+ "install_url": "https://example.com/install.sh",
206
+ "self_updates": True,
207
+ }
208
+ with (
209
+ mock.patch.dict(constants._TOOLS_DATA, {"test": tool_config}),
210
+ mock.patch.object(cli_operations, "is_tool_installed", return_value=True),
211
+ mock.patch.object(
212
+ cli_operations, "is_deprecated_install", return_value=False
213
+ ),
214
+ mock.patch.object(cli_handlers, "run_install_script") as mock_script,
215
+ ):
216
+ result = cli_operations.upgrade_tool("test")
217
+
218
+ self.assertEqual(result, UpgradeResult.UNCHANGED)
219
+ mock_script.assert_not_called()
220
+
200
221
  def test_upgrade_script_detection_uses_tarball_when_config_direct_download(self):
201
222
  """Mis-detected script install still upgrades via tarball for cursor-like tools."""
202
223
  tool_config = {
@@ -193,6 +193,24 @@ class TestToolUpgradeEvaluator(unittest.TestCase):
193
193
  )
194
194
  self.assertTrue(assessment.actionable_by_upgrade)
195
195
 
196
+ def test_outdated_self_updating_tool_is_not_actionable(self):
197
+ assessment = self._evaluate(
198
+ {
199
+ "name": "Self-updating Tool",
200
+ "command": "self-tool",
201
+ "install_type": "script",
202
+ "latest_version": "2.0.0",
203
+ "self_updates": True,
204
+ },
205
+ status={"installed": True, "version": "1.0.0", "errors": []},
206
+ install_info={"method": "script", "detail": None},
207
+ )
208
+ self.assertEqual(assessment.decision, cli_status.UpgradeDecision.SELF_UPDATING)
209
+ self.assertEqual(
210
+ assessment.version_state, cli_status.VersionDisplayState.OUTDATED
211
+ )
212
+ self.assertFalse(assessment.actionable_by_upgrade)
213
+
196
214
  def test_brew_outdated_is_upgrade(self):
197
215
  assessment = self._evaluate(
198
216
  {
@@ -1,6 +1,7 @@
1
1
  """Unit tests for CLI versioning and update-check helpers."""
2
2
 
3
3
  import unittest
4
+ from unittest import mock
4
5
 
5
6
  from code_aide import versions as cli_versions
6
7
 
@@ -181,6 +182,33 @@ class TestVersionIsNewer(unittest.TestCase):
181
182
  )
182
183
 
183
184
 
185
+ class TestFormatVersionManifestUrl(unittest.TestCase):
186
+ """Tests for platform-specific release manifest URLs."""
187
+
188
+ @mock.patch.object(cli_versions.platform, "machine", return_value="arm64")
189
+ @mock.patch.object(cli_versions.platform, "system", return_value="Darwin")
190
+ def test_formats_macos_arm64(self, _system, _machine):
191
+ result = cli_versions.format_version_manifest_url(
192
+ "https://example.com/manifests/{platform}.json"
193
+ )
194
+ self.assertEqual(result, "https://example.com/manifests/darwin_arm64.json")
195
+
196
+ @mock.patch.object(cli_versions.platform, "machine", return_value="x86_64")
197
+ @mock.patch.object(cli_versions.platform, "system", return_value="Linux")
198
+ def test_formats_linux_amd64(self, _system, _machine):
199
+ result = cli_versions.format_version_manifest_url(
200
+ "https://example.com/manifests/{platform}.json"
201
+ )
202
+ self.assertEqual(result, "https://example.com/manifests/linux_amd64.json")
203
+
204
+ @mock.patch.object(cli_versions.platform, "system", return_value="Windows")
205
+ def test_rejects_unsupported_os(self, _system):
206
+ with self.assertRaises(ValueError):
207
+ cli_versions.format_version_manifest_url(
208
+ "https://example.com/manifests/{platform}.json"
209
+ )
210
+
211
+
184
212
  class TestParseHttpDate(unittest.TestCase):
185
213
  """Tests for parse_http_date."""
186
214
 
@@ -323,3 +351,44 @@ class TestExtractScriptVersion(unittest.TestCase):
323
351
  cli_versions.extract_script_version("amp", {}, b'VERSION="2.0.1"\n'),
324
352
  "2.0.1",
325
353
  )
354
+
355
+ @mock.patch.object(
356
+ cli_versions,
357
+ "format_version_manifest_url",
358
+ return_value="https://example.com/manifests/darwin_arm64.json",
359
+ )
360
+ @mock.patch.object(cli_versions, "fetch_url")
361
+ def test_reads_version_from_json_manifest(self, mock_fetch, mock_format):
362
+ mock_fetch.return_value = (b'{"version": "1.1.1"}', None)
363
+
364
+ result = cli_versions.extract_script_version(
365
+ "antigravity",
366
+ {
367
+ "version_manifest_url_template": (
368
+ "https://example.com/manifests/{platform}.json"
369
+ )
370
+ },
371
+ b"#!/bin/bash\n",
372
+ )
373
+
374
+ self.assertEqual(result, "1.1.1")
375
+ mock_format.assert_called_once()
376
+ mock_fetch.assert_called_once_with(
377
+ "https://example.com/manifests/darwin_arm64.json"
378
+ )
379
+
380
+ @mock.patch.object(cli_versions, "fetch_url")
381
+ def test_invalid_manifest_falls_back_to_script(self, mock_fetch):
382
+ mock_fetch.return_value = (b"not json", None)
383
+
384
+ result = cli_versions.extract_script_version(
385
+ "antigravity",
386
+ {
387
+ "version_manifest_url_template": (
388
+ "https://example.com/manifests/{platform}.json"
389
+ )
390
+ },
391
+ b'VERSION="1.0.0"\n',
392
+ )
393
+
394
+ self.assertEqual(result, "1.0.0")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes