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.
- {code_aide-1.15.7 → code_aide-1.16.1}/PKG-INFO +37 -29
- {code_aide-1.15.7 → code_aide-1.16.1}/README.md +35 -27
- {code_aide-1.15.7 → code_aide-1.16.1}/pyproject.toml +1 -1
- code_aide-1.16.1/script-archive/antigravity-install.sh +239 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/script-archive/claude-install.sh +24 -0
- code_aide-1.16.1/specs/antigravity-cli-integration.md +35 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/__init__.py +1 -1
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/commands_actions.py +6 -2
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/config.py +21 -1
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/data/tools.json +27 -4
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/operations.py +7 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/status.py +15 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/versions.py +40 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_commands_actions.py +30 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_config.py +63 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_operations.py +21 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_status.py +18 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_versions.py +69 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/.github/workflows/ci.yml +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/.github/workflows/publish.yml +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/.gitignore +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/.gitlab-ci.yml +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/.pre-commit-config.yaml +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/AGENTS.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/CLAUDE.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/LICENSE +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/TODO.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/script-archive/README.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/script-archive/amp-install.sh +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/script-archive/cursor-install.sh +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/specs/2026-05-03-qwen3.6-code-quality-analysis-original.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/specs/2026-05-03-qwen3.6-code-quality-analysis.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/specs/auto-migrate-deprecated-installs.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/specs/claude-native-installer-migration.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/specs/missing-coding-llm-cli-tools.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/specs/pre-commit-uv-setup.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/specs/remove-bundled-version-baseline.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/specs/unify-upgrade-eligibility-with-shared-evaluator.md +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/__main__.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/commands_tools.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/console.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/constants.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/detection.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/detection_types.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/entry.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/install.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/install_types.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/operation_handlers.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/package_info.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/package_managers.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/prereqs.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/src/code_aide/status_print.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_commands_tools.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_console.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_constants.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_detection.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_entry.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_error_paths.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_install.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_install_types.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_package_managers.py +0 -0
- {code_aide-1.15.7 → code_aide-1.16.1}/tests/test_prereqs.py +0 -0
- {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.
|
|
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,
|
|
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
|
|
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 |
|
|
87
|
+
| Cursor CLI | `agent` | Direct download | No |
|
|
87
88
|
| Claude CLI (Claude Code) | `claude` | Script | Yes |
|
|
88
|
-
|
|
|
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
|
|
117
|
-
|
|
118
|
-
| `name`
|
|
119
|
-
| `command`
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
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,
|
|
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
|
|
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 |
|
|
60
|
+
| Cursor CLI | `agent` | Direct download | No |
|
|
60
61
|
| Claude CLI (Claude Code) | `claude` | Script | Yes |
|
|
61
|
-
|
|
|
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
|
|
90
|
-
|
|
91
|
-
| `name`
|
|
92
|
-
| `command`
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
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.
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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":
|
|
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": "
|
|
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
|
|
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":
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_aide-1.15.7 → code_aide-1.16.1}/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
|
{code_aide-1.15.7 → code_aide-1.16.1}/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
|