code-aide 1.15.5__tar.gz → 1.15.7__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.5 → code_aide-1.15.7}/PKG-INFO +1 -1
- {code_aide-1.15.5 → code_aide-1.15.7}/script-archive/amp-install.sh +111 -40
- {code_aide-1.15.5 → code_aide-1.15.7}/script-archive/claude-install.sh +29 -1
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/__init__.py +1 -1
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/commands_actions.py +5 -2
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/data/tools.json +2 -2
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/install.py +49 -13
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/prereqs.py +9 -14
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_commands_actions.py +34 -1
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_install.py +16 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_prereqs.py +3 -9
- {code_aide-1.15.5 → code_aide-1.15.7}/.github/workflows/ci.yml +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/.github/workflows/publish.yml +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/.gitignore +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/.gitlab-ci.yml +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/.pre-commit-config.yaml +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/AGENTS.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/CLAUDE.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/LICENSE +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/README.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/TODO.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/pyproject.toml +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/script-archive/README.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/script-archive/cursor-install.sh +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/specs/2026-05-03-qwen3.6-code-quality-analysis-original.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/specs/2026-05-03-qwen3.6-code-quality-analysis.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/specs/auto-migrate-deprecated-installs.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/specs/claude-native-installer-migration.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/specs/missing-coding-llm-cli-tools.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/specs/pre-commit-uv-setup.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/specs/remove-bundled-version-baseline.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/specs/unify-upgrade-eligibility-with-shared-evaluator.md +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/__main__.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/commands_tools.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/config.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/console.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/constants.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/detection.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/detection_types.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/entry.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/install_types.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/operation_handlers.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/operations.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/package_info.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/package_managers.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/status.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/status_print.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/src/code_aide/versions.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_commands_tools.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_config.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_console.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_constants.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_detection.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_entry.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_error_paths.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_install_types.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_operations.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_package_managers.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_status.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/tests/test_versions.py +0 -0
- {code_aide-1.15.5 → code_aide-1.15.7}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-aide
|
|
3
|
-
Version: 1.15.
|
|
3
|
+
Version: 1.15.7
|
|
4
4
|
Summary: Manage AI coding CLI tools (Claude, 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
|
|
@@ -8,40 +8,108 @@ AMP_STORAGE_BASE="${AMP_STORAGE_BASE:-https://static.ampcode.com}"
|
|
|
8
8
|
AMP_URL="${AMP_URL:-https://ampcode.com}"
|
|
9
9
|
AMP_VERSION="${AMP_VERSION:-}"
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
if [[ -t 2 ]]; then
|
|
12
|
+
TTY_OUTPUT=1
|
|
13
|
+
else
|
|
14
|
+
TTY_OUTPUT=0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
if [[ $TTY_OUTPUT -eq 1 && -z "${NO_COLOR:-}" ]] && command -v tput >/dev/null 2>&1; then
|
|
18
|
+
COLOR_COUNT="$(tput colors 2>/dev/null || printf '0')"
|
|
19
|
+
else
|
|
20
|
+
COLOR_COUNT=0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if [[ "$COLOR_COUNT" =~ ^[0-9]+$ && "$COLOR_COUNT" -ge 8 ]]; then
|
|
24
|
+
BOLD="$(tput bold 2>/dev/null || true)"
|
|
25
|
+
DIM="$(tput dim 2>/dev/null || true)"
|
|
26
|
+
RESET="$(tput sgr0 2>/dev/null || true)"
|
|
27
|
+
ACCENT="$(tput setaf 6 2>/dev/null || true)"
|
|
28
|
+
SUCCESS="$(tput setaf 2 2>/dev/null || true)"
|
|
29
|
+
WARNING="$(tput setaf 3 2>/dev/null || true)"
|
|
30
|
+
ERROR="$(tput setaf 1 2>/dev/null || true)"
|
|
31
|
+
else
|
|
32
|
+
BOLD=''
|
|
33
|
+
DIM=''
|
|
34
|
+
RESET=''
|
|
35
|
+
ACCENT=''
|
|
36
|
+
SUCCESS=''
|
|
37
|
+
WARNING=''
|
|
38
|
+
ERROR=''
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
case ":$TTY_OUTPUT:${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
|
|
42
|
+
:1:*UTF-8* | :1:*utf8* | :1:*UTF8*)
|
|
43
|
+
ORB_LINES=' · · ·
|
|
44
|
+
· ◍ ● ◍ ·
|
|
45
|
+
· ● AMP ● ·
|
|
46
|
+
· ◍ ● ◍ ·
|
|
47
|
+
· · ·'
|
|
48
|
+
STEP_GLYPH='·──◍'
|
|
49
|
+
SUCCESS_GLYPH='◍──·'
|
|
50
|
+
WARN_GLYPH='·─!'
|
|
51
|
+
ERROR_GLYPH='·─╳'
|
|
52
|
+
;;
|
|
53
|
+
*)
|
|
54
|
+
ORB_LINES=' . . .
|
|
55
|
+
. o O o .
|
|
56
|
+
. O AMP O .
|
|
57
|
+
. o O o .
|
|
58
|
+
. . .'
|
|
59
|
+
STEP_GLYPH='-->'
|
|
60
|
+
SUCCESS_GLYPH='<--'
|
|
61
|
+
WARN_GLYPH='!'
|
|
62
|
+
ERROR_GLYPH='x'
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
print_header() {
|
|
67
|
+
if [[ $TTY_OUTPUT -eq 1 ]]; then
|
|
68
|
+
printf '\n' >&2
|
|
69
|
+
printf '%s\n' "$ORB_LINES" | while IFS= read -r line; do
|
|
70
|
+
printf '%s%s%s\n' "$ACCENT" "$line" "$RESET" >&2
|
|
71
|
+
done
|
|
72
|
+
printf '\n%s%s%s\n' "$BOLD$ACCENT" 'AMP CLI INSTALLER' "$RESET" >&2
|
|
73
|
+
printf '%s%s%s\n\n' "$DIM" \
|
|
74
|
+
'Installs Amp into ~/.amp/bin and links it onto your PATH.' "$RESET" >&2
|
|
75
|
+
else
|
|
76
|
+
printf 'Amp CLI installer\n' >&2
|
|
77
|
+
fi
|
|
24
78
|
}
|
|
25
79
|
|
|
26
|
-
trap cleanup INT TERM
|
|
27
|
-
|
|
28
80
|
log() {
|
|
29
|
-
|
|
81
|
+
printf '%s%s%s %s\n' "$ACCENT" "$STEP_GLYPH" "$RESET" "$1" >&2
|
|
30
82
|
}
|
|
31
83
|
|
|
32
84
|
warn() {
|
|
33
|
-
|
|
85
|
+
printf '%s%s%s %s\n' "$WARNING" "$WARN_GLYPH" "$RESET" "$1" >&2
|
|
34
86
|
}
|
|
35
87
|
|
|
36
88
|
error() {
|
|
37
|
-
|
|
89
|
+
printf '%s%s%s %b\n' "$ERROR" "$ERROR_GLYPH" "$RESET" "$1" >&2
|
|
38
90
|
exit 1
|
|
39
91
|
}
|
|
40
92
|
|
|
41
93
|
success() {
|
|
42
|
-
|
|
94
|
+
printf '%s%s%s %s%s%s\n' "$SUCCESS" "$SUCCESS_GLYPH" "$RESET" \
|
|
95
|
+
"$BOLD" "$1" "$RESET" >&2
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
detail() {
|
|
99
|
+
printf ' %s%s%s\n' "$DIM" "$1" "$RESET" >&2
|
|
43
100
|
}
|
|
44
101
|
|
|
102
|
+
# Cleanup on interrupt
|
|
103
|
+
cleanup() {
|
|
104
|
+
printf '\n' >&2
|
|
105
|
+
warn 'Installation interrupted'
|
|
106
|
+
rm -f "$AMP_HOME/amp-install-"* 2>/dev/null || true
|
|
107
|
+
rm -f "$BIN_DIR/tmp."* 2>/dev/null || true
|
|
108
|
+
exit 1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
trap cleanup INT TERM
|
|
112
|
+
|
|
45
113
|
# Check if command exists
|
|
46
114
|
command_exists() {
|
|
47
115
|
command -v "$1" >/dev/null 2>&1
|
|
@@ -155,8 +223,9 @@ downloader() {
|
|
|
155
223
|
download_file() {
|
|
156
224
|
local url="$1"
|
|
157
225
|
local output_file="$2"
|
|
226
|
+
local label="${3:-$(basename "$output_file")}"
|
|
158
227
|
|
|
159
|
-
log "Downloading $
|
|
228
|
+
log "Downloading $label"
|
|
160
229
|
|
|
161
230
|
# Use secure temporary file
|
|
162
231
|
local temp_file
|
|
@@ -171,8 +240,9 @@ download_file() {
|
|
|
171
240
|
download_gzipped_file() {
|
|
172
241
|
local url="$1"
|
|
173
242
|
local output_file="$2"
|
|
243
|
+
local label="${3:-$(basename "$output_file").gz}"
|
|
174
244
|
|
|
175
|
-
log "Downloading $
|
|
245
|
+
log "Downloading $label"
|
|
176
246
|
|
|
177
247
|
local temp_gz_file
|
|
178
248
|
temp_gz_file=$(mktemp "$(dirname "$output_file")/tmp.XXXXXX.gz")
|
|
@@ -198,7 +268,7 @@ verify_checksum() {
|
|
|
198
268
|
local file="$1"
|
|
199
269
|
local expected_checksum="$2"
|
|
200
270
|
|
|
201
|
-
log "Verifying checksum
|
|
271
|
+
log "Verifying checksum"
|
|
202
272
|
|
|
203
273
|
local actual_checksum
|
|
204
274
|
actual_checksum=$($SHA256_CMD "$file" | cut -d' ' -f1)
|
|
@@ -223,8 +293,8 @@ verify_signature() {
|
|
|
223
293
|
local signature_path="$AMP_HOME/amp-install-signature.minisign"
|
|
224
294
|
local pubkey_path="$AMP_HOME/signing-key.pub"
|
|
225
295
|
|
|
226
|
-
download_file "$signature_url" "$signature_path"
|
|
227
|
-
download_file "$AMP_URL/.well-known/signing-key.pub" "$pubkey_path"
|
|
296
|
+
download_file "$signature_url" "$signature_path" 'signature'
|
|
297
|
+
download_file "$AMP_URL/.well-known/signing-key.pub" "$pubkey_path" 'signing key'
|
|
228
298
|
|
|
229
299
|
if ! minisign -Vm "$file" -x "$signature_path" -p "$pubkey_path" >/dev/null 2>&1; then
|
|
230
300
|
rm -f "$signature_path" "$pubkey_path"
|
|
@@ -240,7 +310,7 @@ fetch_latest_version() {
|
|
|
240
310
|
local version_url="$AMP_STORAGE_BASE/cli/cli-version.txt"
|
|
241
311
|
local version_path="$AMP_HOME/amp-install-version.txt"
|
|
242
312
|
|
|
243
|
-
download_file "$version_url" "$version_path"
|
|
313
|
+
download_file "$version_url" "$version_path" 'latest version'
|
|
244
314
|
cat "$version_path"
|
|
245
315
|
rm -f "$version_path"
|
|
246
316
|
}
|
|
@@ -262,7 +332,7 @@ install_amp_binary() {
|
|
|
262
332
|
else
|
|
263
333
|
# Fetch version first to ensure consistent downloads (avoids race conditions
|
|
264
334
|
# where a new version is published mid-download)
|
|
265
|
-
log "Fetching latest version
|
|
335
|
+
log "Fetching latest version"
|
|
266
336
|
version=$(fetch_latest_version)
|
|
267
337
|
log "Installing version: $version"
|
|
268
338
|
fi
|
|
@@ -280,16 +350,16 @@ install_amp_binary() {
|
|
|
280
350
|
local checksum_path="$AMP_HOME/amp-install-checksum.txt"
|
|
281
351
|
|
|
282
352
|
# Download checksum first
|
|
283
|
-
download_file "$checksum_url" "$checksum_path"
|
|
353
|
+
download_file "$checksum_url" "$checksum_path" 'checksum'
|
|
284
354
|
local expected_checksum
|
|
285
355
|
expected_checksum=$(cat "$checksum_path")
|
|
286
356
|
|
|
287
357
|
if has_gzip_support; then
|
|
288
358
|
# Download compressed binary and verify the decompressed bytes against checksum
|
|
289
|
-
download_gzipped_file "${binary_url}.gz" "$binary_path"
|
|
359
|
+
download_gzipped_file "${binary_url}.gz" "$binary_path" 'Amp binary'
|
|
290
360
|
else
|
|
291
361
|
warn "gzip not found; downloading uncompressed binary"
|
|
292
|
-
download_file "$binary_url" "$binary_path"
|
|
362
|
+
download_file "$binary_url" "$binary_path" 'Amp binary'
|
|
293
363
|
fi
|
|
294
364
|
|
|
295
365
|
# Verify checksum
|
|
@@ -445,20 +515,20 @@ update_shell_profile() {
|
|
|
445
515
|
|
|
446
516
|
# Ask user before modifying shell config
|
|
447
517
|
local tilde_profile="${shell_profile/#$HOME/\~}"
|
|
448
|
-
|
|
518
|
+
printf '\n'
|
|
449
519
|
if [[ -t 0 ]]; then
|
|
450
520
|
# Interactive: ask user
|
|
451
521
|
read -p "Add ~/.local/bin to your PATH in $tilde_profile? [y/n] " -n 1 -r
|
|
452
|
-
|
|
522
|
+
printf '\n'
|
|
453
523
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
454
524
|
log "Skipped modifying shell config."
|
|
455
525
|
log "To use amp, add ~/.local/bin to your PATH manually:"
|
|
456
|
-
|
|
526
|
+
printf ' %s\n' "$path_export"
|
|
457
527
|
return
|
|
458
528
|
fi
|
|
459
529
|
else
|
|
460
530
|
# Non-interactive: add automatically
|
|
461
|
-
log "Adding ~/.local/bin to PATH in $tilde_profile
|
|
531
|
+
log "Adding ~/.local/bin to PATH in $tilde_profile"
|
|
462
532
|
fi
|
|
463
533
|
|
|
464
534
|
# Create config file if it doesn't exist
|
|
@@ -475,16 +545,18 @@ update_shell_profile() {
|
|
|
475
545
|
} >> "$shell_profile"
|
|
476
546
|
|
|
477
547
|
success "Added ~/.local/bin to PATH in $tilde_profile"
|
|
478
|
-
|
|
548
|
+
printf '\n'
|
|
479
549
|
log "To use amp immediately, run:"
|
|
480
|
-
|
|
550
|
+
printf ' %s\n' "$path_export"
|
|
481
551
|
}
|
|
482
552
|
|
|
483
553
|
# Main installation
|
|
484
554
|
main() {
|
|
485
|
-
|
|
555
|
+
print_header
|
|
556
|
+
detail "Install directory: $BIN_DIR"
|
|
486
557
|
|
|
487
558
|
# Check prerequisites
|
|
559
|
+
log "Checking prerequisites"
|
|
488
560
|
check_prereqs
|
|
489
561
|
|
|
490
562
|
# Create directories
|
|
@@ -501,10 +573,9 @@ main() {
|
|
|
501
573
|
# Update shell profile
|
|
502
574
|
update_shell_profile
|
|
503
575
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
log "Visit $AMP_URL for documentation"
|
|
576
|
+
success "Amp CLI installed"
|
|
577
|
+
detail "Run 'amp --help' to get started"
|
|
578
|
+
detail "Docs: $AMP_URL/manual"
|
|
508
579
|
}
|
|
509
580
|
|
|
510
581
|
main "$@"
|
|
@@ -155,11 +155,39 @@ chmod +x "$binary_path"
|
|
|
155
155
|
|
|
156
156
|
# Run claude install to set up launcher and shell integration
|
|
157
157
|
echo "Setting up Claude Code..."
|
|
158
|
-
|
|
158
|
+
install_code=0
|
|
159
|
+
"$binary_path" install ${TARGET:+"$TARGET"} || install_code=$?
|
|
159
160
|
|
|
160
161
|
# Clean up downloaded file
|
|
161
162
|
rm -f "$binary_path"
|
|
162
163
|
|
|
164
|
+
if [ "$install_code" -ne 0 ]; then
|
|
165
|
+
# A signal death mid-install kills the binary's TUI with no chance to
|
|
166
|
+
# restore the terminal, leaving the user's shell in raw mode (typed
|
|
167
|
+
# characters stop echoing). Restore it before printing anything.
|
|
168
|
+
if [ "$install_code" -ge 128 ] && [ -t 0 ]; then
|
|
169
|
+
stty sane 2>/dev/null || true
|
|
170
|
+
fi
|
|
171
|
+
# Red when stderr is a terminal, so the explanation stands out from the
|
|
172
|
+
# surrounding install output; plain when piped or captured
|
|
173
|
+
red="" reset=""
|
|
174
|
+
if [ -t 2 ]; then
|
|
175
|
+
red=$'\033[31m'
|
|
176
|
+
reset=$'\033[0m'
|
|
177
|
+
fi
|
|
178
|
+
# Signal deaths (exit code 128+N) print nothing of their own — bash shows
|
|
179
|
+
# only e.g. "Killed". 137 = SIGKILL, which on Linux is almost always the
|
|
180
|
+
# kernel OOM killer on small hosts; macOS has no equivalent OOM kill, so
|
|
181
|
+
# the out-of-memory explanation is Linux-only.
|
|
182
|
+
if [ "$install_code" -eq 137 ] && [ "$os" = "linux" ]; then
|
|
183
|
+
echo "${red}Installation was killed before it could finish (exit code 137). This usually means the system ran out of memory.${reset}" >&2
|
|
184
|
+
echo "${red}Claude Code needs roughly 512MB of free memory to install. Free up memory, then run this script again.${reset}" >&2
|
|
185
|
+
elif [ "$install_code" -ge 128 ]; then
|
|
186
|
+
echo "${red}Installation was killed before it could finish (exit code $install_code).${reset}" >&2
|
|
187
|
+
fi
|
|
188
|
+
exit "$install_code"
|
|
189
|
+
fi
|
|
190
|
+
|
|
163
191
|
echo ""
|
|
164
192
|
echo "✅ Installation complete!"
|
|
165
193
|
echo ""
|
|
@@ -73,6 +73,7 @@ def cmd_install(args: argparse.Namespace) -> None:
|
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
installed = []
|
|
76
|
+
installed_bin_dirs = []
|
|
76
77
|
failed = []
|
|
77
78
|
|
|
78
79
|
for tool in tools_to_install:
|
|
@@ -82,8 +83,10 @@ def cmd_install(args: argparse.Namespace) -> None:
|
|
|
82
83
|
else:
|
|
83
84
|
info(f"=== Installing {tool} ===")
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
outcome = install_tool(tool, dryrun=dryrun)
|
|
87
|
+
if outcome:
|
|
86
88
|
installed.append(tool)
|
|
89
|
+
installed_bin_dirs.extend(outcome.bin_dirs)
|
|
87
90
|
else:
|
|
88
91
|
failed.append(tool)
|
|
89
92
|
|
|
@@ -120,7 +123,7 @@ def cmd_install(args: argparse.Namespace) -> None:
|
|
|
120
123
|
print(f" {tool_config['next_steps']}")
|
|
121
124
|
|
|
122
125
|
print()
|
|
123
|
-
check_path_directories(
|
|
126
|
+
check_path_directories(installed_bin_dirs)
|
|
124
127
|
success("All installations completed successfully!")
|
|
125
128
|
|
|
126
129
|
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"command": "claude",
|
|
29
29
|
"install_type": "script",
|
|
30
30
|
"install_url": "https://claude.ai/install.sh",
|
|
31
|
-
"install_sha256": "
|
|
31
|
+
"install_sha256": "b3f79015b54c751440a6488f07b1b64f9088742b9052bc1bd356d13108320d2a",
|
|
32
32
|
"version_url": "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest",
|
|
33
33
|
"prerequisites": [],
|
|
34
34
|
"min_node_version": null,
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"command": "amp",
|
|
65
65
|
"install_type": "script",
|
|
66
66
|
"install_url": "https://ampcode.com/install.sh",
|
|
67
|
-
"install_sha256": "
|
|
67
|
+
"install_sha256": "4ceab71af4f5a126d8ffe0e1af3b06f210be67eae93c8e4583793a52064fc2e2",
|
|
68
68
|
"install_script_path_prepend": [
|
|
69
69
|
"~/.local/bin"
|
|
70
70
|
],
|
|
@@ -8,6 +8,7 @@ import shutil
|
|
|
8
8
|
import subprocess
|
|
9
9
|
import tarfile
|
|
10
10
|
import tempfile
|
|
11
|
+
from dataclasses import dataclass
|
|
11
12
|
from typing import Any, Dict, Optional
|
|
12
13
|
|
|
13
14
|
from code_aide.constants import TOOLS
|
|
@@ -18,6 +19,29 @@ from code_aide.versions import check_script_tool, fetch_url
|
|
|
18
19
|
DOWNLOAD_TIMEOUT_SECONDS = 120
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class InstallOutcome:
|
|
24
|
+
"""Result of an install, including directories that may need PATH setup."""
|
|
25
|
+
|
|
26
|
+
succeeded: bool
|
|
27
|
+
bin_dirs: tuple[str, ...] = ()
|
|
28
|
+
|
|
29
|
+
def __bool__(self) -> bool:
|
|
30
|
+
return self.succeeded
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_npm_global_bin_dir() -> Optional[str]:
|
|
34
|
+
"""Return the directory where the active npm installs global executables."""
|
|
35
|
+
try:
|
|
36
|
+
prefix = run_command(["npm", "prefix", "--global"]).stdout.strip()
|
|
37
|
+
except (OSError, subprocess.CalledProcessError):
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
if not prefix:
|
|
41
|
+
return None
|
|
42
|
+
return os.path.join(prefix, "bin")
|
|
43
|
+
|
|
44
|
+
|
|
21
45
|
def run_pkg_command(
|
|
22
46
|
base_cmd: list,
|
|
23
47
|
pkg_name: str,
|
|
@@ -265,12 +289,14 @@ def install_direct_download(
|
|
|
265
289
|
return False
|
|
266
290
|
|
|
267
291
|
|
|
268
|
-
def install_tool(
|
|
292
|
+
def install_tool(
|
|
293
|
+
tool_name: str, dryrun: bool = False, force: bool = False
|
|
294
|
+
) -> InstallOutcome:
|
|
269
295
|
"""Install a tool based on its configuration."""
|
|
270
296
|
tool_config = TOOLS.get(tool_name)
|
|
271
297
|
if not tool_config:
|
|
272
298
|
error(f"Unknown tool: {tool_name}")
|
|
273
|
-
return False
|
|
299
|
+
return InstallOutcome(False)
|
|
274
300
|
|
|
275
301
|
if dryrun:
|
|
276
302
|
info(f"[DRYRUN] Checking {tool_config['name']}...")
|
|
@@ -283,7 +309,7 @@ def install_tool(tool_name: str, dryrun: bool = False, force: bool = False) -> b
|
|
|
283
309
|
info(f"{tool_config['command']} already installed at {tool_path}")
|
|
284
310
|
else:
|
|
285
311
|
warning(f"{tool_config['command']} already installed at {tool_path}")
|
|
286
|
-
return True
|
|
312
|
+
return InstallOutcome(True)
|
|
287
313
|
if command_exists(tool_config["command"]) and force:
|
|
288
314
|
tool_path = shutil.which(tool_config["command"])
|
|
289
315
|
if dryrun:
|
|
@@ -301,7 +327,7 @@ def install_tool(tool_name: str, dryrun: bool = False, force: bool = False) -> b
|
|
|
301
327
|
freebsd_port = tool_config.get("freebsd_port")
|
|
302
328
|
if not freebsd_port:
|
|
303
329
|
error(f"{tool_config['name']} is not available on FreeBSD (no port exists)")
|
|
304
|
-
return False
|
|
330
|
+
return InstallOutcome(False)
|
|
305
331
|
pkg_repo = tool_config.get("freebsd_pkg_repo")
|
|
306
332
|
if dryrun:
|
|
307
333
|
repo_flag = f" -r {pkg_repo}" if pkg_repo else ""
|
|
@@ -309,7 +335,7 @@ def install_tool(tool_name: str, dryrun: bool = False, force: bool = False) -> b
|
|
|
309
335
|
f"[DRYRUN] Would install FreeBSD port: "
|
|
310
336
|
f"pkg install{repo_flag} {freebsd_port}"
|
|
311
337
|
)
|
|
312
|
-
return True
|
|
338
|
+
return InstallOutcome(True)
|
|
313
339
|
try:
|
|
314
340
|
run_pkg_command(
|
|
315
341
|
["sudo", "pkg", "install", "-y"],
|
|
@@ -321,27 +347,32 @@ def install_tool(tool_name: str, dryrun: bool = False, force: bool = False) -> b
|
|
|
321
347
|
info(tool_config["next_steps"])
|
|
322
348
|
if "docs_url" in tool_config:
|
|
323
349
|
info(f"Documentation: {tool_config['docs_url']}")
|
|
324
|
-
return True
|
|
350
|
+
return InstallOutcome(True)
|
|
325
351
|
except subprocess.CalledProcessError as exc:
|
|
326
352
|
error(f"Failed to install {tool_config['name']}: {exc.stderr}")
|
|
327
|
-
return False
|
|
353
|
+
return InstallOutcome(False)
|
|
328
354
|
except Exception as exc:
|
|
329
355
|
error(f"Failed to install {tool_config['name']}: {exc}")
|
|
330
|
-
return False
|
|
356
|
+
return InstallOutcome(False)
|
|
331
357
|
|
|
332
358
|
try:
|
|
333
359
|
install_type = get_tool_install_type(tool_config)
|
|
334
360
|
|
|
335
361
|
if install_type == InstallType.NPM:
|
|
336
362
|
npm_package = tool_config["npm_package"]
|
|
363
|
+
bin_dirs = ()
|
|
337
364
|
if dryrun:
|
|
338
365
|
info(f"[DRYRUN] Would install npm package: {npm_package}")
|
|
339
366
|
else:
|
|
340
367
|
run_command(["npm", "install", "-g", npm_package], check=True)
|
|
368
|
+
npm_bin_dir = get_npm_global_bin_dir()
|
|
369
|
+
if npm_bin_dir:
|
|
370
|
+
bin_dirs = (npm_bin_dir,)
|
|
341
371
|
success(f"{tool_config['name']} installed successfully")
|
|
342
372
|
info(tool_config["next_steps"])
|
|
343
373
|
if "docs_url" in tool_config:
|
|
344
374
|
info(f"Documentation: {tool_config['docs_url']}")
|
|
375
|
+
return InstallOutcome(True, bin_dirs)
|
|
345
376
|
|
|
346
377
|
elif install_type == InstallType.SCRIPT:
|
|
347
378
|
install_url = tool_config["install_url"]
|
|
@@ -361,7 +392,7 @@ def install_tool(tool_name: str, dryrun: bool = False, force: bool = False) -> b
|
|
|
361
392
|
if "docs_url" in tool_config:
|
|
362
393
|
info(f"Documentation: {tool_config['docs_url']}")
|
|
363
394
|
else:
|
|
364
|
-
return False
|
|
395
|
+
return InstallOutcome(False)
|
|
365
396
|
|
|
366
397
|
elif install_type == InstallType.DIRECT_DOWNLOAD:
|
|
367
398
|
if install_direct_download(tool_name, tool_config, dryrun):
|
|
@@ -373,13 +404,18 @@ def install_tool(tool_name: str, dryrun: bool = False, force: bool = False) -> b
|
|
|
373
404
|
if "docs_url" in tool_config:
|
|
374
405
|
info(f"Documentation: {tool_config['docs_url']}")
|
|
375
406
|
else:
|
|
376
|
-
return False
|
|
407
|
+
return InstallOutcome(False)
|
|
377
408
|
|
|
378
|
-
|
|
409
|
+
if not dryrun and tool_config.get("bin_dir"):
|
|
410
|
+
return InstallOutcome(
|
|
411
|
+
True, (os.path.expanduser(tool_config["bin_dir"]),)
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
return InstallOutcome(True)
|
|
379
415
|
|
|
380
416
|
except subprocess.CalledProcessError as exc:
|
|
381
417
|
error(f"Failed to install {tool_config['name']}: {exc.stderr}")
|
|
382
|
-
return False
|
|
418
|
+
return InstallOutcome(False)
|
|
383
419
|
except Exception as exc:
|
|
384
420
|
error(f"Failed to install {tool_config['name']}: {exc}")
|
|
385
|
-
return False
|
|
421
|
+
return InstallOutcome(False)
|
|
@@ -149,28 +149,23 @@ def is_tool_installed(tool_name: str) -> bool:
|
|
|
149
149
|
return command_exists(tool_config["command"])
|
|
150
150
|
|
|
151
151
|
|
|
152
|
-
def check_path_directories(
|
|
152
|
+
def check_path_directories(directories: Optional[List[str]] = None) -> None:
|
|
153
153
|
"""Check if binary installation directories are in PATH and warn if not.
|
|
154
154
|
|
|
155
|
-
When *
|
|
156
|
-
|
|
155
|
+
When *directories* is given, only those directories are checked. Otherwise
|
|
156
|
+
a small set of common directories is used.
|
|
157
157
|
"""
|
|
158
158
|
current_path = os.environ.get("PATH", "")
|
|
159
159
|
path_entries = current_path.split(os.pathsep)
|
|
160
160
|
|
|
161
|
-
if
|
|
161
|
+
if directories is not None:
|
|
162
162
|
seen: set = set()
|
|
163
163
|
dirs_to_check: List[str] = []
|
|
164
|
-
for
|
|
165
|
-
|
|
166
|
-
if not
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if bin_dir:
|
|
170
|
-
expanded = os.path.expanduser(bin_dir)
|
|
171
|
-
if expanded not in seen:
|
|
172
|
-
seen.add(expanded)
|
|
173
|
-
dirs_to_check.append(expanded)
|
|
164
|
+
for directory in directories:
|
|
165
|
+
expanded = os.path.expanduser(directory)
|
|
166
|
+
if expanded not in seen:
|
|
167
|
+
seen.add(expanded)
|
|
168
|
+
dirs_to_check.append(expanded)
|
|
174
169
|
else:
|
|
175
170
|
dirs_to_check = [
|
|
176
171
|
os.path.expanduser("~/.local/bin"),
|
|
@@ -8,6 +8,7 @@ from unittest import mock
|
|
|
8
8
|
|
|
9
9
|
from code_aide import constants, commands_actions
|
|
10
10
|
from code_aide import entry
|
|
11
|
+
from code_aide.install import InstallOutcome
|
|
11
12
|
from code_aide.operations import UpgradeResult
|
|
12
13
|
|
|
13
14
|
|
|
@@ -39,7 +40,7 @@ class TestCmdInstall(unittest.TestCase):
|
|
|
39
40
|
mock.patch.dict(constants._TOOLS_DATA, tools, clear=True),
|
|
40
41
|
mock.patch.object(commands_actions, "validate_tools"),
|
|
41
42
|
mock.patch.object(
|
|
42
|
-
commands_actions, "install_tool", return_value=True
|
|
43
|
+
commands_actions, "install_tool", return_value=InstallOutcome(True)
|
|
43
44
|
) as mock_install_tool,
|
|
44
45
|
mock.patch.object(commands_actions, "check_prerequisites") as mock_prereqs,
|
|
45
46
|
):
|
|
@@ -50,6 +51,38 @@ class TestCmdInstall(unittest.TestCase):
|
|
|
50
51
|
mock_install_tool.assert_called_once_with("default_tool", dryrun=True)
|
|
51
52
|
mock_prereqs.assert_not_called()
|
|
52
53
|
|
|
54
|
+
def test_checks_bin_directories_reported_by_installers(self):
|
|
55
|
+
tools = {
|
|
56
|
+
"test": {
|
|
57
|
+
"name": "Test Tool",
|
|
58
|
+
"command": "test",
|
|
59
|
+
"install_type": "npm",
|
|
60
|
+
"next_steps": "run test",
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
args = type(
|
|
64
|
+
"Args",
|
|
65
|
+
(),
|
|
66
|
+
{"tools": ["test"], "dryrun": False, "install_prerequisites": False},
|
|
67
|
+
)()
|
|
68
|
+
|
|
69
|
+
with (
|
|
70
|
+
mock.patch.dict(constants._TOOLS_DATA, tools, clear=True),
|
|
71
|
+
mock.patch.object(commands_actions, "validate_tools"),
|
|
72
|
+
mock.patch.object(commands_actions, "check_prerequisites"),
|
|
73
|
+
mock.patch.object(
|
|
74
|
+
commands_actions,
|
|
75
|
+
"install_tool",
|
|
76
|
+
return_value=InstallOutcome(True, ("/home/test/.npm-packages/bin",)),
|
|
77
|
+
),
|
|
78
|
+
mock.patch.object(
|
|
79
|
+
commands_actions, "check_path_directories"
|
|
80
|
+
) as mock_check_path,
|
|
81
|
+
):
|
|
82
|
+
commands_actions.cmd_install(args)
|
|
83
|
+
|
|
84
|
+
mock_check_path.assert_called_once_with(["/home/test/.npm-packages/bin"])
|
|
85
|
+
|
|
53
86
|
|
|
54
87
|
class TestCmdUpdateVersions(unittest.TestCase):
|
|
55
88
|
"""Tests for cmd_update_versions."""
|
|
@@ -286,6 +286,16 @@ class TestInstallDirectDownload(unittest.TestCase):
|
|
|
286
286
|
class TestInstallTool(unittest.TestCase):
|
|
287
287
|
"""Tests for install_tool behavior."""
|
|
288
288
|
|
|
289
|
+
def test_get_npm_global_bin_dir_uses_active_prefix(self):
|
|
290
|
+
npm_prefix = mock.Mock(stdout="/home/test/.npm-packages\n")
|
|
291
|
+
with mock.patch.object(
|
|
292
|
+
cli_install, "run_command", return_value=npm_prefix
|
|
293
|
+
) as mock_run:
|
|
294
|
+
result = cli_install.get_npm_global_bin_dir()
|
|
295
|
+
|
|
296
|
+
self.assertEqual(result, "/home/test/.npm-packages/bin")
|
|
297
|
+
mock_run.assert_called_once_with(["npm", "prefix", "--global"])
|
|
298
|
+
|
|
289
299
|
def test_force_reinstalls_even_when_binary_exists(self):
|
|
290
300
|
tool_config = {
|
|
291
301
|
"name": "Test Tool",
|
|
@@ -301,11 +311,17 @@ class TestInstallTool(unittest.TestCase):
|
|
|
301
311
|
mock.patch.object(
|
|
302
312
|
cli_install.shutil, "which", return_value="/usr/local/bin/test-tool"
|
|
303
313
|
),
|
|
314
|
+
mock.patch.object(
|
|
315
|
+
cli_install,
|
|
316
|
+
"get_npm_global_bin_dir",
|
|
317
|
+
return_value="/home/test/.npm-packages/bin",
|
|
318
|
+
),
|
|
304
319
|
mock.patch.object(cli_install, "run_command") as mock_run,
|
|
305
320
|
):
|
|
306
321
|
result = cli_install.install_tool("test", force=True)
|
|
307
322
|
|
|
308
323
|
self.assertTrue(result)
|
|
324
|
+
self.assertEqual(result.bin_dirs, ("/home/test/.npm-packages/bin",))
|
|
309
325
|
mock_run.assert_called_once_with(
|
|
310
326
|
["npm", "install", "-g", "test-tool"], check=True
|
|
311
327
|
)
|
|
@@ -210,14 +210,12 @@ class TestCheckPathDirectories(unittest.TestCase):
|
|
|
210
210
|
"""Tests for check_path_directories."""
|
|
211
211
|
|
|
212
212
|
def test_warns_when_existing_bin_dir_not_in_path(self):
|
|
213
|
-
tool_config = {"name": "Test", "command": "test", "bin_dir": "/custom/bin"}
|
|
214
213
|
with (
|
|
215
|
-
mock.patch.dict(constants._TOOLS_DATA, {"test": tool_config}, clear=True),
|
|
216
214
|
mock.patch.dict("os.environ", {"PATH": "/usr/bin"}, clear=False),
|
|
217
215
|
mock.patch.object(cli_prereqs.os.path, "isdir", return_value=True),
|
|
218
216
|
mock.patch("builtins.print") as mock_print,
|
|
219
217
|
):
|
|
220
|
-
cli_prereqs.check_path_directories(["
|
|
218
|
+
cli_prereqs.check_path_directories(["/custom/bin"])
|
|
221
219
|
# Should have printed at least the bin dir as a warning detail
|
|
222
220
|
printed = " ".join(
|
|
223
221
|
str(call.args[0]) for call in mock_print.call_args_list if call.args
|
|
@@ -225,27 +223,23 @@ class TestCheckPathDirectories(unittest.TestCase):
|
|
|
225
223
|
self.assertIn("/custom/bin", printed)
|
|
226
224
|
|
|
227
225
|
def test_silent_when_dir_already_in_path(self):
|
|
228
|
-
tool_config = {"name": "Test", "command": "test", "bin_dir": "/custom/bin"}
|
|
229
226
|
with (
|
|
230
|
-
mock.patch.dict(constants._TOOLS_DATA, {"test": tool_config}, clear=True),
|
|
231
227
|
mock.patch.dict(
|
|
232
228
|
"os.environ", {"PATH": "/custom/bin:/usr/bin"}, clear=False
|
|
233
229
|
),
|
|
234
230
|
mock.patch.object(cli_prereqs.os.path, "isdir", return_value=True),
|
|
235
231
|
mock.patch("builtins.print") as mock_print,
|
|
236
232
|
):
|
|
237
|
-
cli_prereqs.check_path_directories(["
|
|
233
|
+
cli_prereqs.check_path_directories(["/custom/bin"])
|
|
238
234
|
self.assertFalse(mock_print.called)
|
|
239
235
|
|
|
240
236
|
def test_silent_when_bin_dir_does_not_exist(self):
|
|
241
|
-
tool_config = {"name": "Test", "command": "test", "bin_dir": "/missing/bin"}
|
|
242
237
|
with (
|
|
243
|
-
mock.patch.dict(constants._TOOLS_DATA, {"test": tool_config}, clear=True),
|
|
244
238
|
mock.patch.dict("os.environ", {"PATH": "/usr/bin"}, clear=False),
|
|
245
239
|
mock.patch.object(cli_prereqs.os.path, "isdir", return_value=False),
|
|
246
240
|
mock.patch("builtins.print") as mock_print,
|
|
247
241
|
):
|
|
248
|
-
cli_prereqs.check_path_directories(["
|
|
242
|
+
cli_prereqs.check_path_directories(["/missing/bin"])
|
|
249
243
|
self.assertFalse(mock_print.called)
|
|
250
244
|
|
|
251
245
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_aide-1.15.5 → code_aide-1.15.7}/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.5 → code_aide-1.15.7}/specs/unify-upgrade-eligibility-with-shared-evaluator.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|