fere-cli 0.2.0.dev14__tar.gz → 0.2.0.dev17__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.
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/PKG-INFO +5 -3
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/README.md +3 -1
- fere_cli-0.2.0.dev17/install.sh +272 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/pyproject.toml +2 -2
- fere_cli-0.2.0.dev17/tests/test_install_sh.py +599 -0
- fere_cli-0.2.0.dev14/install.sh +0 -114
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/.gitignore +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/__init__.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/async_util.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/banner.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/commands/__init__.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/commands/auth.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/commands/chat.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/commands/earn.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/commands/hooks.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/commands/limit_order.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/commands/portfolio.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/commands/swap.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/commands/utility.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/config.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/main.py +0 -0
- {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev17}/src/fere_cli/output.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fere-cli
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev17
|
|
4
4
|
Summary: Terminal CLI for FereAI crypto trading and research
|
|
5
5
|
Author-email: Fere AI <info@fere.ai>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Requires-Dist: click>=8.1
|
|
9
|
-
Requires-Dist: fere-sdk>=0.2.0.
|
|
9
|
+
Requires-Dist: fere-sdk>=0.2.0.dev17
|
|
10
10
|
Requires-Dist: rich>=13.0
|
|
11
11
|
Provides-Extra: dev
|
|
12
12
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
@@ -19,11 +19,13 @@ Terminal-first interface for FereAI crypto trading and research. Wraps the [Fere
|
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
22
|
+
Recommended (script installer, handles pipx/pip and PATH setup):
|
|
23
|
+
|
|
22
24
|
```bash
|
|
23
25
|
curl -fsSL https://api.fereai.xyz/install.sh | sh
|
|
24
26
|
```
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
Manual (advanced, for humans in controlled environments only):
|
|
27
29
|
|
|
28
30
|
```bash
|
|
29
31
|
pip install fere-cli
|
|
@@ -4,11 +4,13 @@ Terminal-first interface for FereAI crypto trading and research. Wraps the [Fere
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
+
Recommended (script installer, handles pipx/pip and PATH setup):
|
|
8
|
+
|
|
7
9
|
```bash
|
|
8
10
|
curl -fsSL https://api.fereai.xyz/install.sh | sh
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Manual (advanced, for humans in controlled environments only):
|
|
12
14
|
|
|
13
15
|
```bash
|
|
14
16
|
pip install fere-cli
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# FereAI CLI Installer
|
|
3
|
+
# Usage (recommended):
|
|
4
|
+
# curl -fsSL https://api.fereai.xyz/install.sh | sh
|
|
5
|
+
# Install a specific version or channel:
|
|
6
|
+
# curl -fsSL https://api.fereai.xyz/install.sh | sh -s -- 1.2.3
|
|
7
|
+
# curl -fsSL https://api.fereai.xyz/install.sh | sh -s -- latest
|
|
8
|
+
# Or manual install:
|
|
9
|
+
# pipx install fere-cli
|
|
10
|
+
# python -m pip install --user fere-cli
|
|
11
|
+
#
|
|
12
|
+
# Installs the `fere` CLI tool via pipx (recommended) or pip.
|
|
13
|
+
# Supports macOS and Linux.
|
|
14
|
+
#
|
|
15
|
+
# Note: uses `local` keyword which is not in POSIX but is supported by
|
|
16
|
+
# all target shells (dash, bash, zsh, ash) on macOS and Linux.
|
|
17
|
+
|
|
18
|
+
set -eu
|
|
19
|
+
|
|
20
|
+
PACKAGE="fere-cli"
|
|
21
|
+
|
|
22
|
+
# Optional target/channel argument: stable (default), latest, or explicit version (X.Y.Z)
|
|
23
|
+
TARGET="${1:-stable}"
|
|
24
|
+
|
|
25
|
+
print_usage_and_exit() {
|
|
26
|
+
echo "Usage: install.sh [stable|latest|X.Y.Z]" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
case "$TARGET" in
|
|
31
|
+
stable) ;;
|
|
32
|
+
latest) ;;
|
|
33
|
+
*)
|
|
34
|
+
echo "$TARGET" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' || print_usage_and_exit
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
37
|
+
|
|
38
|
+
# Colors (disabled if not a TTY)
|
|
39
|
+
if [ -t 1 ]; then
|
|
40
|
+
RED='\033[0;31m'
|
|
41
|
+
GREEN='\033[0;32m'
|
|
42
|
+
YELLOW='\033[0;33m'
|
|
43
|
+
DIM='\033[2m'
|
|
44
|
+
RESET='\033[0m'
|
|
45
|
+
else
|
|
46
|
+
RED=''
|
|
47
|
+
GREEN=''
|
|
48
|
+
YELLOW=''
|
|
49
|
+
DIM=''
|
|
50
|
+
RESET=''
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
info() {
|
|
54
|
+
printf "%s%s%s\n" "$DIM" "$1" "$RESET"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
success() {
|
|
58
|
+
printf "%s%s%s\n" "$GREEN" "$1" "$RESET"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
warn() {
|
|
62
|
+
printf "%s%s%s\n" "$YELLOW" "$1" "$RESET" >&2
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
error() {
|
|
66
|
+
printf "%sError: %s%s\n" "$RED" "$1" "$RESET" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Detect OS
|
|
71
|
+
OS="$(uname -s)"
|
|
72
|
+
case "$OS" in
|
|
73
|
+
Darwin) OS_NAME="macOS" ;;
|
|
74
|
+
Linux) OS_NAME="Linux" ;;
|
|
75
|
+
*) error "Unsupported OS: $OS. Use 'pip install $PACKAGE' instead." ;;
|
|
76
|
+
esac
|
|
77
|
+
|
|
78
|
+
info "Installing FereAI CLI on ${OS_NAME} (target: ${TARGET})..."
|
|
79
|
+
|
|
80
|
+
# Check Python 3.10+
|
|
81
|
+
check_python() {
|
|
82
|
+
local checked="" ver major minor cmd
|
|
83
|
+
for cmd in python3 python; do
|
|
84
|
+
if command -v "$cmd" >/dev/null 2>&1; then
|
|
85
|
+
ver=$("$cmd" -c 'import sys; print("{}.{}".format(sys.version_info.major, sys.version_info.minor))' 2>/dev/null || true)
|
|
86
|
+
if [ -n "$ver" ]; then
|
|
87
|
+
major=$(printf "%s" "$ver" | cut -d. -f1)
|
|
88
|
+
minor=$(printf "%s" "$ver" | cut -d. -f2)
|
|
89
|
+
checked="${checked} $cmd($ver)"
|
|
90
|
+
if [ "$major" -gt 3 ] || { [ "$major" -eq 3 ] && [ "$minor" -ge 10 ]; }; then
|
|
91
|
+
echo "$cmd"
|
|
92
|
+
return 0
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
fi
|
|
96
|
+
done
|
|
97
|
+
warn "Checked Python interpreters:${checked}"
|
|
98
|
+
return 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
PYTHON=$(check_python) || error "Python 3.10+ is required. Install it from https://python.org"
|
|
102
|
+
info "Found Python: $("$PYTHON" --version 2>/dev/null || echo "$PYTHON")"
|
|
103
|
+
|
|
104
|
+
# Derive package spec and extra args from target
|
|
105
|
+
PKG_SPEC="$PACKAGE"
|
|
106
|
+
PIP_EXTRA_ARGS=""
|
|
107
|
+
PIPX_EXTRA_ARGS=""
|
|
108
|
+
|
|
109
|
+
case "$TARGET" in
|
|
110
|
+
stable)
|
|
111
|
+
PKG_SPEC="$PACKAGE"
|
|
112
|
+
;;
|
|
113
|
+
latest)
|
|
114
|
+
PKG_SPEC="$PACKAGE"
|
|
115
|
+
PIP_EXTRA_ARGS="--pre"
|
|
116
|
+
# Single-token form (--opt=value) required; pipx receives it as one arg.
|
|
117
|
+
PIPX_EXTRA_ARGS="--pip-args=--pre"
|
|
118
|
+
;;
|
|
119
|
+
*)
|
|
120
|
+
# Explicit version
|
|
121
|
+
PKG_SPEC="$PACKAGE==$TARGET"
|
|
122
|
+
;;
|
|
123
|
+
esac
|
|
124
|
+
|
|
125
|
+
# Prefer pipx for isolated installs.
|
|
126
|
+
# Always use --force for idempotent behavior (fresh install or reinstall).
|
|
127
|
+
# This matches the pattern used by Deno, Bun, and uv installers, and avoids
|
|
128
|
+
# the fragile try-then-force-retry pattern with stderr suppression.
|
|
129
|
+
install_with_pipx() {
|
|
130
|
+
if command -v pipx >/dev/null 2>&1; then
|
|
131
|
+
info "Installing with pipx..."
|
|
132
|
+
if [ -n "$PIPX_EXTRA_ARGS" ]; then
|
|
133
|
+
pipx install --force "$PIPX_EXTRA_ARGS" "$PKG_SPEC" || return 1
|
|
134
|
+
else
|
|
135
|
+
pipx install --force "$PKG_SPEC" || return 1
|
|
136
|
+
fi
|
|
137
|
+
return 0
|
|
138
|
+
fi
|
|
139
|
+
return 1
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
install_pipx() {
|
|
143
|
+
info "Installing pipx..."
|
|
144
|
+
local _pipx_install_ok=0
|
|
145
|
+
if command -v brew >/dev/null 2>&1; then
|
|
146
|
+
brew install pipx && _pipx_install_ok=1
|
|
147
|
+
fi
|
|
148
|
+
# pip fallback for both macOS (if brew failed) and other systems
|
|
149
|
+
if [ "$_pipx_install_ok" != "1" ]; then
|
|
150
|
+
"$PYTHON" -m pip install --user pipx 2>/dev/null || {
|
|
151
|
+
"$PYTHON" -m ensurepip --default-pip 2>/dev/null &&
|
|
152
|
+
"$PYTHON" -m pip install --user pipx 2>/dev/null
|
|
153
|
+
} && _pipx_install_ok=1
|
|
154
|
+
fi
|
|
155
|
+
return $((1 - _pipx_install_ok))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
install_with_pip() {
|
|
159
|
+
# Note: PIP_EXTRA_ARGS must be a single token; see PIPX_EXTRA_ARGS comment.
|
|
160
|
+
info "Installing with pip (user install)..."
|
|
161
|
+
if [ -n "$PIP_EXTRA_ARGS" ]; then
|
|
162
|
+
"$PYTHON" -m pip install --user "$PIP_EXTRA_ARGS" "$PKG_SPEC"
|
|
163
|
+
else
|
|
164
|
+
"$PYTHON" -m pip install --user "$PKG_SPEC"
|
|
165
|
+
fi
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
detect_user_bin() {
|
|
169
|
+
local user_base dir
|
|
170
|
+
# Prefer Python's user base if available
|
|
171
|
+
if command -v "$PYTHON" >/dev/null 2>&1; then
|
|
172
|
+
user_base=$("$PYTHON" -c 'import site, sys; sys.stdout.write(site.getuserbase())' 2>/dev/null || echo "")
|
|
173
|
+
if [ -n "$user_base" ] && [ -d "$user_base/bin" ]; then
|
|
174
|
+
echo "$user_base/bin"
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# Common fallbacks
|
|
180
|
+
for dir in "$HOME/.local/bin" "$HOME/bin"; do
|
|
181
|
+
if [ -d "$dir" ]; then
|
|
182
|
+
echo "$dir"
|
|
183
|
+
return 0
|
|
184
|
+
fi
|
|
185
|
+
done
|
|
186
|
+
|
|
187
|
+
return 1
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
detect_shell_rc() {
|
|
191
|
+
local shell_path
|
|
192
|
+
shell_path="${SHELL:-}"
|
|
193
|
+
case "$shell_path" in
|
|
194
|
+
*zsh) echo "~/.zshrc" ;;
|
|
195
|
+
*bash)
|
|
196
|
+
if [ "$OS" = "Darwin" ]; then
|
|
197
|
+
echo "~/.bash_profile"
|
|
198
|
+
else
|
|
199
|
+
echo "~/.bashrc"
|
|
200
|
+
fi
|
|
201
|
+
;;
|
|
202
|
+
*fish) echo "~/.config/fish/config.fish" ;;
|
|
203
|
+
*) echo "~/.profile" ;;
|
|
204
|
+
esac
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
installed_with=""
|
|
208
|
+
|
|
209
|
+
# Try pipx first, then install pipx, then fall back to pip
|
|
210
|
+
if install_with_pipx; then
|
|
211
|
+
installed_with="pipx"
|
|
212
|
+
elif install_pipx; then
|
|
213
|
+
# Freshly-installed pipx may not be on PATH yet; resolve its location.
|
|
214
|
+
_user_base=$("$PYTHON" -c 'import site; print(site.getuserbase())' 2>/dev/null || echo "")
|
|
215
|
+
if [ -n "$_user_base" ] && [ -x "$_user_base/bin/pipx" ]; then
|
|
216
|
+
export PATH="$_user_base/bin:$PATH"
|
|
217
|
+
pipx ensurepath 2>/dev/null || true
|
|
218
|
+
fi
|
|
219
|
+
if install_with_pipx; then
|
|
220
|
+
installed_with="pipx"
|
|
221
|
+
else
|
|
222
|
+
warn "pipx not available, falling back to pip..."
|
|
223
|
+
install_with_pip || error "Installation failed. Please install manually: pip install $PACKAGE"
|
|
224
|
+
installed_with="pip"
|
|
225
|
+
fi
|
|
226
|
+
else
|
|
227
|
+
warn "pipx not available, falling back to pip..."
|
|
228
|
+
install_with_pip || error "Installation failed. Please install manually: pip install $PACKAGE"
|
|
229
|
+
installed_with="pip"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
info "Installed $PKG_SPEC using: $installed_with"
|
|
233
|
+
|
|
234
|
+
# Verify installation
|
|
235
|
+
if command -v fere >/dev/null 2>&1; then
|
|
236
|
+
version_str=$(fere --version 2>/dev/null || echo "")
|
|
237
|
+
success "FereAI CLI installed successfully!"
|
|
238
|
+
if [ -n "$version_str" ]; then
|
|
239
|
+
info "Installed version: $version_str"
|
|
240
|
+
fi
|
|
241
|
+
echo ""
|
|
242
|
+
info "Get started:"
|
|
243
|
+
echo " fere auth --name my-agent # Set up your agent"
|
|
244
|
+
echo " fere chat \"hello\" # Chat with the AI"
|
|
245
|
+
echo " fere --help # See all commands"
|
|
246
|
+
else
|
|
247
|
+
warn "Installation complete, but 'fere' is not in PATH."
|
|
248
|
+
user_bin=$(detect_user_bin || echo "")
|
|
249
|
+
rc_file=$(detect_shell_rc)
|
|
250
|
+
echo ""
|
|
251
|
+
if [ -n "$user_bin" ]; then
|
|
252
|
+
case "$rc_file" in
|
|
253
|
+
*fish*)
|
|
254
|
+
echo "Run this command in your fish shell:"
|
|
255
|
+
echo ""
|
|
256
|
+
echo " fish_add_path \"$user_bin\""
|
|
257
|
+
echo ""
|
|
258
|
+
echo "This takes effect immediately in all fish sessions."
|
|
259
|
+
;;
|
|
260
|
+
*)
|
|
261
|
+
echo "To fix this, add the following line to $rc_file:"
|
|
262
|
+
echo ""
|
|
263
|
+
echo " export PATH=\"$user_bin:\$PATH\""
|
|
264
|
+
echo ""
|
|
265
|
+
echo "Then restart your terminal or reload your shell (e.g. 'source $rc_file')."
|
|
266
|
+
;;
|
|
267
|
+
esac
|
|
268
|
+
else
|
|
269
|
+
echo "Could not detect your user bin directory automatically."
|
|
270
|
+
echo "Make sure the directory containing the 'fere' executable is on your PATH."
|
|
271
|
+
fi
|
|
272
|
+
fi
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fere-cli"
|
|
3
|
-
version = "0.2.0.
|
|
3
|
+
version = "0.2.0.dev17"
|
|
4
4
|
description = "Terminal CLI for FereAI crypto trading and research"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
authors = [{ name = "Fere AI", email = "info@fere.ai" }]
|
|
9
9
|
dependencies = [
|
|
10
|
-
"fere-sdk>=0.2.0.
|
|
10
|
+
"fere-sdk>=0.2.0.dev17",
|
|
11
11
|
"click>=8.1",
|
|
12
12
|
"rich>=13.0",
|
|
13
13
|
]
|
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import stat
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
HERE = Path(__file__).resolve().parent # tests/
|
|
8
|
+
CLI_DIR = HERE.parent # cli/
|
|
9
|
+
INSTALL_SH = CLI_DIR / "install.sh"
|
|
10
|
+
if not INSTALL_SH.exists():
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
pytest.skip(
|
|
14
|
+
f"install.sh not found at {INSTALL_SH}", allow_module_level=True
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _make_executable(path: Path, content: str) -> None:
|
|
19
|
+
path.write_text(content)
|
|
20
|
+
mode = path.stat().st_mode
|
|
21
|
+
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_install_fails_without_python(tmp_path: Path) -> None:
|
|
25
|
+
"""Installer should fail when no Python is available."""
|
|
26
|
+
bin_dir = tmp_path / "bin"
|
|
27
|
+
bin_dir.mkdir(parents=True)
|
|
28
|
+
home_dir = tmp_path / "home"
|
|
29
|
+
home_dir.mkdir(parents=True)
|
|
30
|
+
|
|
31
|
+
# Symlink essential system utilities but NOT python
|
|
32
|
+
skip = {"python", "python3", "python3.11", "python3.12", "python3.13"}
|
|
33
|
+
for sysdir in ["/usr/bin", "/bin"]:
|
|
34
|
+
p = Path(sysdir)
|
|
35
|
+
if p.is_dir():
|
|
36
|
+
for entry in p.iterdir():
|
|
37
|
+
if (
|
|
38
|
+
entry.name not in skip
|
|
39
|
+
and not entry.name.startswith("python")
|
|
40
|
+
and not (bin_dir / entry.name).exists()
|
|
41
|
+
):
|
|
42
|
+
try:
|
|
43
|
+
(bin_dir / entry.name).symlink_to(entry)
|
|
44
|
+
except OSError:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
env = os.environ.copy()
|
|
48
|
+
env["PATH"] = str(bin_dir)
|
|
49
|
+
env["HOME"] = str(home_dir)
|
|
50
|
+
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
["/bin/sh", str(INSTALL_SH)],
|
|
53
|
+
env=env,
|
|
54
|
+
stdout=subprocess.PIPE,
|
|
55
|
+
stderr=subprocess.PIPE,
|
|
56
|
+
text=True,
|
|
57
|
+
)
|
|
58
|
+
assert result.returncode != 0
|
|
59
|
+
out = result.stdout + result.stderr
|
|
60
|
+
assert "Python 3.10+" in out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_install_prints_path_guidance_when_fere_missing(tmp_path: Path) -> None:
|
|
64
|
+
"""Installer should print PATH guidance when fere is not on PATH."""
|
|
65
|
+
bin_dir = tmp_path / "bin"
|
|
66
|
+
bin_dir.mkdir(parents=True)
|
|
67
|
+
home_dir = tmp_path / "home"
|
|
68
|
+
home_dir.mkdir(parents=True)
|
|
69
|
+
# Create .local/bin so detect_user_bin finds it via getuserbase
|
|
70
|
+
local_bin = home_dir / ".local" / "bin"
|
|
71
|
+
local_bin.mkdir(parents=True)
|
|
72
|
+
|
|
73
|
+
fake_python = bin_dir / "python3"
|
|
74
|
+
_make_executable(
|
|
75
|
+
fake_python,
|
|
76
|
+
"#!/bin/sh\n"
|
|
77
|
+
'if [ "$1" = "--version" ]; then\n'
|
|
78
|
+
' echo "Python 3.11.0"\n'
|
|
79
|
+
" exit 0\n"
|
|
80
|
+
'elif [ "$1" = "-c" ]; then\n'
|
|
81
|
+
' case "$2" in\n'
|
|
82
|
+
' *version_info*) echo "3.11" ;;\n'
|
|
83
|
+
' *getuserbase*) printf "%s" "$HOME/.local" ;;\n'
|
|
84
|
+
' *) echo "3.11" ;;\n'
|
|
85
|
+
" esac\n"
|
|
86
|
+
" exit 0\n"
|
|
87
|
+
'elif [ "$1" = "-m" ]; then\n'
|
|
88
|
+
" exit 0\n" # simulate successful pip/site invocations
|
|
89
|
+
"fi\n"
|
|
90
|
+
"exit 0\n",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
fake_pipx = bin_dir / "pipx"
|
|
94
|
+
_make_executable(
|
|
95
|
+
fake_pipx,
|
|
96
|
+
"#!/bin/sh\n"
|
|
97
|
+
'if [ "$1" = "ensurepath" ]; then exit 0; fi\n'
|
|
98
|
+
"exit 1\n",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Stub brew to prevent real package manager invocations on macOS
|
|
102
|
+
fake_brew = bin_dir / "brew"
|
|
103
|
+
_make_executable(fake_brew, "#!/bin/sh\nexit 1\n")
|
|
104
|
+
|
|
105
|
+
env = os.environ.copy()
|
|
106
|
+
env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"
|
|
107
|
+
env["HOME"] = str(home_dir)
|
|
108
|
+
env["SHELL"] = "/bin/bash"
|
|
109
|
+
|
|
110
|
+
result = subprocess.run(
|
|
111
|
+
["/bin/sh", str(INSTALL_SH)],
|
|
112
|
+
env=env,
|
|
113
|
+
stdout=subprocess.PIPE,
|
|
114
|
+
stderr=subprocess.PIPE,
|
|
115
|
+
text=True,
|
|
116
|
+
)
|
|
117
|
+
out = result.stdout + result.stderr
|
|
118
|
+
# Installer exits 0 (install succeeded) but warns fere isn't on PATH
|
|
119
|
+
assert result.returncode == 0
|
|
120
|
+
assert "Installation complete, but 'fere' is not in PATH." in out
|
|
121
|
+
# Verify specific PATH guidance (not the generic fallback)
|
|
122
|
+
assert "export PATH=" in out
|
|
123
|
+
expected_rc = (
|
|
124
|
+
".bash_profile" if platform.system() == "Darwin" else ".bashrc"
|
|
125
|
+
)
|
|
126
|
+
assert expected_rc in out
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_invalid_version_argument(tmp_path: Path) -> None:
|
|
130
|
+
"""Installer should reject invalid version arguments."""
|
|
131
|
+
bin_dir = tmp_path / "bin"
|
|
132
|
+
bin_dir.mkdir(parents=True)
|
|
133
|
+
|
|
134
|
+
env = os.environ.copy()
|
|
135
|
+
env["PATH"] = str(bin_dir)
|
|
136
|
+
env["HOME"] = str(tmp_path / "home")
|
|
137
|
+
|
|
138
|
+
result = subprocess.run(
|
|
139
|
+
["/bin/sh", str(INSTALL_SH), "not-a-version"],
|
|
140
|
+
env=env,
|
|
141
|
+
stdout=subprocess.PIPE,
|
|
142
|
+
stderr=subprocess.PIPE,
|
|
143
|
+
text=True,
|
|
144
|
+
)
|
|
145
|
+
assert result.returncode != 0
|
|
146
|
+
out = result.stdout + result.stderr
|
|
147
|
+
assert "Usage:" in out
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_explicit_version_in_pkg_spec(tmp_path: Path) -> None:
|
|
151
|
+
"""Passing X.Y.Z should produce fere-cli==X.Y.Z in output."""
|
|
152
|
+
bin_dir = tmp_path / "bin"
|
|
153
|
+
bin_dir.mkdir(parents=True)
|
|
154
|
+
home_dir = tmp_path / "home"
|
|
155
|
+
home_dir.mkdir(parents=True)
|
|
156
|
+
local_bin = home_dir / ".local" / "bin"
|
|
157
|
+
local_bin.mkdir(parents=True)
|
|
158
|
+
|
|
159
|
+
fake_python = bin_dir / "python3"
|
|
160
|
+
_make_executable(
|
|
161
|
+
fake_python,
|
|
162
|
+
"#!/bin/sh\n"
|
|
163
|
+
'if [ "$1" = "--version" ]; then\n'
|
|
164
|
+
' echo "Python 3.11.0"\n'
|
|
165
|
+
" exit 0\n"
|
|
166
|
+
'elif [ "$1" = "-c" ]; then\n'
|
|
167
|
+
' case "$2" in\n'
|
|
168
|
+
' *version_info*) echo "3.11" ;;\n'
|
|
169
|
+
' *getuserbase*) printf "%s" "$HOME/.local" ;;\n'
|
|
170
|
+
' *) echo "3.11" ;;\n'
|
|
171
|
+
" esac\n"
|
|
172
|
+
" exit 0\n"
|
|
173
|
+
'elif [ "$1" = "-m" ]; then\n'
|
|
174
|
+
" exit 0\n"
|
|
175
|
+
"fi\n"
|
|
176
|
+
"exit 0\n",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
fake_pipx = bin_dir / "pipx"
|
|
180
|
+
_make_executable(
|
|
181
|
+
fake_pipx,
|
|
182
|
+
"#!/bin/sh\n"
|
|
183
|
+
'if [ "$1" = "ensurepath" ]; then exit 0; fi\n'
|
|
184
|
+
"exit 1\n",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
fake_brew = bin_dir / "brew"
|
|
188
|
+
_make_executable(fake_brew, "#!/bin/sh\nexit 1\n")
|
|
189
|
+
|
|
190
|
+
env = os.environ.copy()
|
|
191
|
+
env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"
|
|
192
|
+
env["HOME"] = str(home_dir)
|
|
193
|
+
env["SHELL"] = "/bin/bash"
|
|
194
|
+
|
|
195
|
+
result = subprocess.run(
|
|
196
|
+
["/bin/sh", str(INSTALL_SH), "1.2.3"],
|
|
197
|
+
env=env,
|
|
198
|
+
stdout=subprocess.PIPE,
|
|
199
|
+
stderr=subprocess.PIPE,
|
|
200
|
+
text=True,
|
|
201
|
+
)
|
|
202
|
+
out = result.stdout + result.stderr
|
|
203
|
+
assert (
|
|
204
|
+
result.returncode == 0
|
|
205
|
+
), f"Installer exited {result.returncode}:\n{out}"
|
|
206
|
+
assert "fere-cli==1.2.3" in out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_install_succeeds_with_pipx(tmp_path: Path) -> None:
|
|
210
|
+
"""Primary pipx install path should succeed end-to-end."""
|
|
211
|
+
bin_dir = tmp_path / "bin"
|
|
212
|
+
bin_dir.mkdir(parents=True)
|
|
213
|
+
home_dir = tmp_path / "home"
|
|
214
|
+
home_dir.mkdir(parents=True)
|
|
215
|
+
|
|
216
|
+
fake_python = bin_dir / "python3"
|
|
217
|
+
_make_executable(
|
|
218
|
+
fake_python,
|
|
219
|
+
"#!/bin/sh\n"
|
|
220
|
+
'if [ "$1" = "--version" ]; then\n'
|
|
221
|
+
' echo "Python 3.11.0"\n'
|
|
222
|
+
" exit 0\n"
|
|
223
|
+
'elif [ "$1" = "-c" ]; then\n'
|
|
224
|
+
' case "$2" in\n'
|
|
225
|
+
' *version_info*) echo "3.11" ;;\n'
|
|
226
|
+
' *getuserbase*) printf "%s" "$HOME/.local" ;;\n'
|
|
227
|
+
' *) echo "3.11" ;;\n'
|
|
228
|
+
" esac\n"
|
|
229
|
+
" exit 0\n"
|
|
230
|
+
'elif [ "$1" = "-m" ]; then\n'
|
|
231
|
+
" exit 0\n"
|
|
232
|
+
"fi\n"
|
|
233
|
+
"exit 0\n",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Key difference: fake_pipx exits 0 for install (happy path)
|
|
237
|
+
fake_pipx = bin_dir / "pipx"
|
|
238
|
+
_make_executable(
|
|
239
|
+
fake_pipx,
|
|
240
|
+
"#!/bin/sh\n"
|
|
241
|
+
'if [ "$1" = "install" ]; then exit 0; fi\n'
|
|
242
|
+
'if [ "$1" = "ensurepath" ]; then exit 0; fi\n'
|
|
243
|
+
"exit 1\n",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Provide a fake fere binary so post-install check succeeds
|
|
247
|
+
fake_fere = bin_dir / "fere"
|
|
248
|
+
_make_executable(
|
|
249
|
+
fake_fere,
|
|
250
|
+
"#!/bin/sh\n"
|
|
251
|
+
'if [ "$1" = "--version" ]; then echo "fere 0.1.0"; fi\n'
|
|
252
|
+
"exit 0\n",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
fake_brew = bin_dir / "brew"
|
|
256
|
+
_make_executable(fake_brew, "#!/bin/sh\nexit 1\n")
|
|
257
|
+
|
|
258
|
+
env = os.environ.copy()
|
|
259
|
+
env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"
|
|
260
|
+
env["HOME"] = str(home_dir)
|
|
261
|
+
env["SHELL"] = "/bin/bash"
|
|
262
|
+
|
|
263
|
+
result = subprocess.run(
|
|
264
|
+
["/bin/sh", str(INSTALL_SH)],
|
|
265
|
+
env=env,
|
|
266
|
+
stdout=subprocess.PIPE,
|
|
267
|
+
stderr=subprocess.PIPE,
|
|
268
|
+
text=True,
|
|
269
|
+
)
|
|
270
|
+
out = result.stdout + result.stderr
|
|
271
|
+
assert (
|
|
272
|
+
result.returncode == 0
|
|
273
|
+
), f"Installer exited {result.returncode}:\n{out}"
|
|
274
|
+
assert "Installed fere-cli using: pipx" in out
|
|
275
|
+
assert "FereAI CLI installed successfully!" in out
|
|
276
|
+
assert "falling back to pip" not in out
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_freshly_installed_pipx_is_found(tmp_path: Path) -> None:
|
|
280
|
+
"""After install_pipx, the installer should resolve pipx in user base."""
|
|
281
|
+
bin_dir = tmp_path / "bin"
|
|
282
|
+
bin_dir.mkdir(parents=True)
|
|
283
|
+
home_dir = tmp_path / "home"
|
|
284
|
+
home_dir.mkdir(parents=True)
|
|
285
|
+
local_bin = home_dir / ".local" / "bin"
|
|
286
|
+
local_bin.mkdir(parents=True)
|
|
287
|
+
|
|
288
|
+
fake_python = bin_dir / "python3"
|
|
289
|
+
_make_executable(
|
|
290
|
+
fake_python,
|
|
291
|
+
"#!/bin/sh\n"
|
|
292
|
+
'if [ "$1" = "--version" ]; then\n'
|
|
293
|
+
' echo "Python 3.11.0"\n'
|
|
294
|
+
" exit 0\n"
|
|
295
|
+
'elif [ "$1" = "-c" ]; then\n'
|
|
296
|
+
' case "$2" in\n'
|
|
297
|
+
' *version_info*) echo "3.11" ;;\n'
|
|
298
|
+
' *getuserbase*) printf "%s" "$HOME/.local" ;;\n'
|
|
299
|
+
' *site*) printf "%s" "$HOME/.local" ;;\n'
|
|
300
|
+
' *) echo "3.11" ;;\n'
|
|
301
|
+
" esac\n"
|
|
302
|
+
" exit 0\n"
|
|
303
|
+
'elif [ "$1" = "-m" ]; then\n'
|
|
304
|
+
" exit 0\n"
|
|
305
|
+
"fi\n"
|
|
306
|
+
"exit 0\n",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# No pipx on initial PATH — simulates fresh install scenario.
|
|
310
|
+
# Place pipx in ~/.local/bin (where pip --user puts it)
|
|
311
|
+
# so the installer's PATH resolution finds it after install_pipx.
|
|
312
|
+
fake_pipx = local_bin / "pipx"
|
|
313
|
+
_make_executable(
|
|
314
|
+
fake_pipx,
|
|
315
|
+
"#!/bin/sh\n"
|
|
316
|
+
'if [ "$1" = "install" ]; then exit 0; fi\n'
|
|
317
|
+
'if [ "$1" = "ensurepath" ]; then exit 0; fi\n'
|
|
318
|
+
"exit 1\n",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
fake_fere = local_bin / "fere"
|
|
322
|
+
_make_executable(
|
|
323
|
+
fake_fere,
|
|
324
|
+
"#!/bin/sh\n"
|
|
325
|
+
'if [ "$1" = "--version" ]; then echo "fere 0.1.0"; fi\n'
|
|
326
|
+
"exit 0\n",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
fake_brew = bin_dir / "brew"
|
|
330
|
+
_make_executable(fake_brew, "#!/bin/sh\nexit 1\n")
|
|
331
|
+
|
|
332
|
+
env = os.environ.copy()
|
|
333
|
+
# Only bin_dir on PATH — no local_bin, so pipx not initially found
|
|
334
|
+
env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"
|
|
335
|
+
env["HOME"] = str(home_dir)
|
|
336
|
+
env["SHELL"] = "/bin/bash"
|
|
337
|
+
|
|
338
|
+
result = subprocess.run(
|
|
339
|
+
["/bin/sh", str(INSTALL_SH)],
|
|
340
|
+
env=env,
|
|
341
|
+
stdout=subprocess.PIPE,
|
|
342
|
+
stderr=subprocess.PIPE,
|
|
343
|
+
text=True,
|
|
344
|
+
)
|
|
345
|
+
out = result.stdout + result.stderr
|
|
346
|
+
assert (
|
|
347
|
+
result.returncode == 0
|
|
348
|
+
), f"Installer exited {result.returncode}:\n{out}"
|
|
349
|
+
assert "Installed fere-cli using: pipx" in out
|
|
350
|
+
assert "falling back to pip" not in out
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_latest_channel_passes_pre_flag(tmp_path: Path) -> None:
|
|
354
|
+
"""Passing 'latest' should include --pre in the install invocation."""
|
|
355
|
+
bin_dir = tmp_path / "bin"
|
|
356
|
+
bin_dir.mkdir(parents=True)
|
|
357
|
+
home_dir = tmp_path / "home"
|
|
358
|
+
home_dir.mkdir(parents=True)
|
|
359
|
+
local_bin = home_dir / ".local" / "bin"
|
|
360
|
+
local_bin.mkdir(parents=True)
|
|
361
|
+
|
|
362
|
+
fake_python = bin_dir / "python3"
|
|
363
|
+
_make_executable(
|
|
364
|
+
fake_python,
|
|
365
|
+
"#!/bin/sh\n"
|
|
366
|
+
'if [ "$1" = "--version" ]; then\n'
|
|
367
|
+
' echo "Python 3.11.0"\n'
|
|
368
|
+
" exit 0\n"
|
|
369
|
+
'elif [ "$1" = "-c" ]; then\n'
|
|
370
|
+
' case "$2" in\n'
|
|
371
|
+
' *version_info*) echo "3.11" ;;\n'
|
|
372
|
+
' *getuserbase*) printf "%s" "$HOME/.local" ;;\n'
|
|
373
|
+
' *) echo "3.11" ;;\n'
|
|
374
|
+
" esac\n"
|
|
375
|
+
" exit 0\n"
|
|
376
|
+
'elif [ "$1" = "-m" ]; then\n'
|
|
377
|
+
" exit 0\n"
|
|
378
|
+
"fi\n"
|
|
379
|
+
"exit 0\n",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# pipx that logs its args so we can verify --pip-args=--pre
|
|
383
|
+
fake_pipx = bin_dir / "pipx"
|
|
384
|
+
_make_executable(
|
|
385
|
+
fake_pipx,
|
|
386
|
+
"#!/bin/sh\n"
|
|
387
|
+
'echo "pipx $*"\n'
|
|
388
|
+
'if [ "$1" = "install" ]; then exit 0; fi\n'
|
|
389
|
+
'if [ "$1" = "ensurepath" ]; then exit 0; fi\n'
|
|
390
|
+
"exit 1\n",
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
fake_fere = bin_dir / "fere"
|
|
394
|
+
_make_executable(
|
|
395
|
+
fake_fere,
|
|
396
|
+
"#!/bin/sh\n"
|
|
397
|
+
'if [ "$1" = "--version" ]; then echo "fere 0.2.0-pre"; fi\n'
|
|
398
|
+
"exit 0\n",
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
fake_brew = bin_dir / "brew"
|
|
402
|
+
_make_executable(fake_brew, "#!/bin/sh\nexit 1\n")
|
|
403
|
+
|
|
404
|
+
env = os.environ.copy()
|
|
405
|
+
env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"
|
|
406
|
+
env["HOME"] = str(home_dir)
|
|
407
|
+
env["SHELL"] = "/bin/bash"
|
|
408
|
+
|
|
409
|
+
result = subprocess.run(
|
|
410
|
+
["/bin/sh", str(INSTALL_SH), "latest"],
|
|
411
|
+
env=env,
|
|
412
|
+
stdout=subprocess.PIPE,
|
|
413
|
+
stderr=subprocess.PIPE,
|
|
414
|
+
text=True,
|
|
415
|
+
)
|
|
416
|
+
out = result.stdout + result.stderr
|
|
417
|
+
assert (
|
|
418
|
+
result.returncode == 0
|
|
419
|
+
), f"Installer exited {result.returncode}:\n{out}"
|
|
420
|
+
assert "--pip-args=--pre" in out
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def test_pip_fallback_when_pipx_unavailable(tmp_path: Path) -> None:
|
|
424
|
+
"""Freshly-installed pipx not found on PATH falls back to pip."""
|
|
425
|
+
bin_dir = tmp_path / "bin"
|
|
426
|
+
bin_dir.mkdir(parents=True)
|
|
427
|
+
home_dir = tmp_path / "home"
|
|
428
|
+
home_dir.mkdir(parents=True)
|
|
429
|
+
local_bin = home_dir / ".local" / "bin"
|
|
430
|
+
local_bin.mkdir(parents=True)
|
|
431
|
+
|
|
432
|
+
fake_python = bin_dir / "python3"
|
|
433
|
+
_make_executable(
|
|
434
|
+
fake_python,
|
|
435
|
+
"#!/bin/sh\n"
|
|
436
|
+
'if [ "$1" = "--version" ]; then\n'
|
|
437
|
+
' echo "Python 3.11.0"\n'
|
|
438
|
+
" exit 0\n"
|
|
439
|
+
'elif [ "$1" = "-c" ]; then\n'
|
|
440
|
+
' case "$2" in\n'
|
|
441
|
+
' *version_info*) echo "3.11" ;;\n'
|
|
442
|
+
' *getuserbase*) printf "%s" "$HOME/.local" ;;\n'
|
|
443
|
+
' *) echo "3.11" ;;\n'
|
|
444
|
+
" esac\n"
|
|
445
|
+
" exit 0\n"
|
|
446
|
+
'elif [ "$1" = "-m" ]; then\n'
|
|
447
|
+
" exit 0\n"
|
|
448
|
+
"fi\n"
|
|
449
|
+
"exit 0\n",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# No pipx binary on PATH — install_with_pipx returns 1;
|
|
453
|
+
# install_pipx succeeds (fake python -m exits 0) but the
|
|
454
|
+
# freshly-installed pipx binary is not found at
|
|
455
|
+
# _user_base/bin/pipx, so the inner else falls back to pip.
|
|
456
|
+
fake_brew = bin_dir / "brew"
|
|
457
|
+
_make_executable(fake_brew, "#!/bin/sh\nexit 1\n")
|
|
458
|
+
|
|
459
|
+
env = os.environ.copy()
|
|
460
|
+
env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"
|
|
461
|
+
env["HOME"] = str(home_dir)
|
|
462
|
+
env["SHELL"] = "/bin/bash"
|
|
463
|
+
|
|
464
|
+
result = subprocess.run(
|
|
465
|
+
["/bin/sh", str(INSTALL_SH)],
|
|
466
|
+
env=env,
|
|
467
|
+
stdout=subprocess.PIPE,
|
|
468
|
+
stderr=subprocess.PIPE,
|
|
469
|
+
text=True,
|
|
470
|
+
)
|
|
471
|
+
out = result.stdout + result.stderr
|
|
472
|
+
assert (
|
|
473
|
+
result.returncode == 0
|
|
474
|
+
), f"Installer exited {result.returncode}:\n{out}"
|
|
475
|
+
assert "falling back to pip" in out
|
|
476
|
+
assert "Installed fere-cli using: pip" in out
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def test_pip_fallback_when_install_pipx_fails(tmp_path: Path) -> None:
|
|
480
|
+
"""install_pipx returns 1 → else branch falls back to pip."""
|
|
481
|
+
bin_dir = tmp_path / "bin"
|
|
482
|
+
bin_dir.mkdir(parents=True)
|
|
483
|
+
home_dir = tmp_path / "home"
|
|
484
|
+
home_dir.mkdir(parents=True)
|
|
485
|
+
local_bin = home_dir / ".local" / "bin"
|
|
486
|
+
local_bin.mkdir(parents=True)
|
|
487
|
+
|
|
488
|
+
# Python that fails on -m for pipx/ensurepip
|
|
489
|
+
fake_python = bin_dir / "python3"
|
|
490
|
+
_make_executable(
|
|
491
|
+
fake_python,
|
|
492
|
+
"#!/bin/sh\n"
|
|
493
|
+
'if [ "$1" = "--version" ]; then\n'
|
|
494
|
+
' echo "Python 3.11.0"\n'
|
|
495
|
+
" exit 0\n"
|
|
496
|
+
'elif [ "$1" = "-c" ]; then\n'
|
|
497
|
+
' case "$2" in\n'
|
|
498
|
+
' *version_info*) echo "3.11" ;;\n'
|
|
499
|
+
' *getuserbase*) printf "%s" "$HOME/.local" ;;\n'
|
|
500
|
+
' *) echo "3.11" ;;\n'
|
|
501
|
+
" esac\n"
|
|
502
|
+
" exit 0\n"
|
|
503
|
+
'elif [ "$1" = "-m" ]; then\n'
|
|
504
|
+
' case "$2" in\n'
|
|
505
|
+
" pip)\n"
|
|
506
|
+
' case "$3" in\n'
|
|
507
|
+
" install)\n"
|
|
508
|
+
" # fail pipx install, allow fere-cli\n"
|
|
509
|
+
' case "$*" in\n'
|
|
510
|
+
" *pipx*) exit 1 ;;\n"
|
|
511
|
+
" *) exit 0 ;;\n"
|
|
512
|
+
" esac ;;\n"
|
|
513
|
+
" *) exit 0 ;;\n"
|
|
514
|
+
" esac ;;\n"
|
|
515
|
+
" ensurepip) exit 1 ;;\n"
|
|
516
|
+
" *) exit 0 ;;\n"
|
|
517
|
+
" esac\n"
|
|
518
|
+
"fi\n"
|
|
519
|
+
"exit 0\n",
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
# No pipx on PATH, brew fails — install_pipx will return 1
|
|
523
|
+
fake_brew = bin_dir / "brew"
|
|
524
|
+
_make_executable(fake_brew, "#!/bin/sh\nexit 1\n")
|
|
525
|
+
|
|
526
|
+
env = os.environ.copy()
|
|
527
|
+
env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"
|
|
528
|
+
env["HOME"] = str(home_dir)
|
|
529
|
+
env["SHELL"] = "/bin/bash"
|
|
530
|
+
|
|
531
|
+
result = subprocess.run(
|
|
532
|
+
["/bin/sh", str(INSTALL_SH)],
|
|
533
|
+
env=env,
|
|
534
|
+
stdout=subprocess.PIPE,
|
|
535
|
+
stderr=subprocess.PIPE,
|
|
536
|
+
text=True,
|
|
537
|
+
)
|
|
538
|
+
out = result.stdout + result.stderr
|
|
539
|
+
assert (
|
|
540
|
+
result.returncode == 0
|
|
541
|
+
), f"Installer exited {result.returncode}:\n{out}"
|
|
542
|
+
assert "falling back to pip" in out
|
|
543
|
+
assert "Installed fere-cli using: pip" in out
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def test_latest_channel_pip_fallback(tmp_path: Path) -> None:
|
|
547
|
+
"""Latest channel via pip fallback should pass --pre flag."""
|
|
548
|
+
bin_dir = tmp_path / "bin"
|
|
549
|
+
bin_dir.mkdir(parents=True)
|
|
550
|
+
home_dir = tmp_path / "home"
|
|
551
|
+
home_dir.mkdir(parents=True)
|
|
552
|
+
local_bin = home_dir / ".local" / "bin"
|
|
553
|
+
local_bin.mkdir(parents=True)
|
|
554
|
+
|
|
555
|
+
# Python that logs pip args so we can verify --pre
|
|
556
|
+
fake_python = bin_dir / "python3"
|
|
557
|
+
_make_executable(
|
|
558
|
+
fake_python,
|
|
559
|
+
"#!/bin/sh\n"
|
|
560
|
+
'if [ "$1" = "--version" ]; then\n'
|
|
561
|
+
' echo "Python 3.11.0"\n'
|
|
562
|
+
" exit 0\n"
|
|
563
|
+
'elif [ "$1" = "-c" ]; then\n'
|
|
564
|
+
' case "$2" in\n'
|
|
565
|
+
' *version_info*) echo "3.11" ;;\n'
|
|
566
|
+
' *getuserbase*) printf "%s" "$HOME/.local" ;;\n'
|
|
567
|
+
' *) echo "3.11" ;;\n'
|
|
568
|
+
" esac\n"
|
|
569
|
+
" exit 0\n"
|
|
570
|
+
'elif [ "$1" = "-m" ]; then\n'
|
|
571
|
+
" shift\n"
|
|
572
|
+
' echo "python -m $*"\n'
|
|
573
|
+
" exit 0\n"
|
|
574
|
+
"fi\n"
|
|
575
|
+
"exit 0\n",
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# No pipx — forces pip fallback
|
|
579
|
+
fake_brew = bin_dir / "brew"
|
|
580
|
+
_make_executable(fake_brew, "#!/bin/sh\nexit 1\n")
|
|
581
|
+
|
|
582
|
+
env = os.environ.copy()
|
|
583
|
+
env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"
|
|
584
|
+
env["HOME"] = str(home_dir)
|
|
585
|
+
env["SHELL"] = "/bin/bash"
|
|
586
|
+
|
|
587
|
+
result = subprocess.run(
|
|
588
|
+
["/bin/sh", str(INSTALL_SH), "latest"],
|
|
589
|
+
env=env,
|
|
590
|
+
stdout=subprocess.PIPE,
|
|
591
|
+
stderr=subprocess.PIPE,
|
|
592
|
+
text=True,
|
|
593
|
+
)
|
|
594
|
+
out = result.stdout + result.stderr
|
|
595
|
+
assert (
|
|
596
|
+
result.returncode == 0
|
|
597
|
+
), f"Installer exited {result.returncode}:\n{out}"
|
|
598
|
+
assert "falling back to pip" in out
|
|
599
|
+
assert "--pre" in out
|
fere_cli-0.2.0.dev14/install.sh
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# FereAI CLI Installer
|
|
3
|
-
# Usage: curl -fsSL https://api.fereai.xyz/install.sh | sh
|
|
4
|
-
#
|
|
5
|
-
# Installs the `fere` CLI tool via pipx (recommended) or pip.
|
|
6
|
-
# Supports macOS and Linux.
|
|
7
|
-
|
|
8
|
-
set -e
|
|
9
|
-
|
|
10
|
-
VERSION="0.1.0"
|
|
11
|
-
PACKAGE="fere-cli"
|
|
12
|
-
|
|
13
|
-
# Colors (disabled if not a TTY)
|
|
14
|
-
if [ -t 1 ]; then
|
|
15
|
-
RED='\033[0;31m'
|
|
16
|
-
GREEN='\033[0;32m'
|
|
17
|
-
YELLOW='\033[0;33m'
|
|
18
|
-
DIM='\033[2m'
|
|
19
|
-
RESET='\033[0m'
|
|
20
|
-
else
|
|
21
|
-
RED=''
|
|
22
|
-
GREEN=''
|
|
23
|
-
YELLOW=''
|
|
24
|
-
DIM=''
|
|
25
|
-
RESET=''
|
|
26
|
-
fi
|
|
27
|
-
|
|
28
|
-
info() { printf "${DIM}%s${RESET}\n" "$1"; }
|
|
29
|
-
success() { printf "${GREEN}%s${RESET}\n" "$1"; }
|
|
30
|
-
warn() { printf "${YELLOW}%s${RESET}\n" "$1"; }
|
|
31
|
-
error() { printf "${RED}Error: %s${RESET}\n" "$1" >&2; exit 1; }
|
|
32
|
-
|
|
33
|
-
# Detect OS
|
|
34
|
-
OS="$(uname -s)"
|
|
35
|
-
case "$OS" in
|
|
36
|
-
Darwin) OS_NAME="macOS" ;;
|
|
37
|
-
Linux) OS_NAME="Linux" ;;
|
|
38
|
-
*) error "Unsupported OS: $OS. Use pip install $PACKAGE instead." ;;
|
|
39
|
-
esac
|
|
40
|
-
|
|
41
|
-
info "Installing FereAI CLI v${VERSION} on ${OS_NAME}..."
|
|
42
|
-
|
|
43
|
-
# Check Python 3.10+
|
|
44
|
-
check_python() {
|
|
45
|
-
for cmd in python3 python; do
|
|
46
|
-
if command -v "$cmd" >/dev/null 2>&1; then
|
|
47
|
-
ver=$("$cmd" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
|
|
48
|
-
major=$(echo "$ver" | cut -d. -f1)
|
|
49
|
-
minor=$(echo "$ver" | cut -d. -f2)
|
|
50
|
-
if [ "$major" -ge 3 ] && [ "$minor" -ge 10 ]; then
|
|
51
|
-
echo "$cmd"
|
|
52
|
-
return 0
|
|
53
|
-
fi
|
|
54
|
-
fi
|
|
55
|
-
done
|
|
56
|
-
return 1
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
PYTHON=$(check_python) || error "Python 3.10+ is required. Install it from https://python.org"
|
|
60
|
-
info "Found Python: $($PYTHON --version)"
|
|
61
|
-
|
|
62
|
-
# Prefer pipx for isolated installs
|
|
63
|
-
install_with_pipx() {
|
|
64
|
-
if command -v pipx >/dev/null 2>&1; then
|
|
65
|
-
info "Installing with pipx..."
|
|
66
|
-
pipx install "$PACKAGE" || pipx upgrade "$PACKAGE"
|
|
67
|
-
return 0
|
|
68
|
-
fi
|
|
69
|
-
return 1
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
install_pipx() {
|
|
73
|
-
info "Installing pipx..."
|
|
74
|
-
if command -v brew >/dev/null 2>&1; then
|
|
75
|
-
brew install pipx
|
|
76
|
-
elif [ "$OS" = "Linux" ]; then
|
|
77
|
-
$PYTHON -m pip install --user pipx 2>/dev/null || \
|
|
78
|
-
$PYTHON -m ensurepip --default-pip 2>/dev/null && \
|
|
79
|
-
$PYTHON -m pip install --user pipx
|
|
80
|
-
else
|
|
81
|
-
$PYTHON -m pip install --user pipx
|
|
82
|
-
fi
|
|
83
|
-
pipx ensurepath 2>/dev/null || true
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
install_with_pip() {
|
|
87
|
-
info "Installing with pip..."
|
|
88
|
-
$PYTHON -m pip install --user "$PACKAGE"
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
# Try pipx first, then install pipx, then fall back to pip
|
|
92
|
-
if install_with_pipx; then
|
|
93
|
-
: # success
|
|
94
|
-
elif install_pipx && install_with_pipx; then
|
|
95
|
-
: # success
|
|
96
|
-
else
|
|
97
|
-
warn "pipx not available, falling back to pip..."
|
|
98
|
-
install_with_pip
|
|
99
|
-
fi
|
|
100
|
-
|
|
101
|
-
# Verify installation
|
|
102
|
-
if command -v fere >/dev/null 2>&1; then
|
|
103
|
-
success "FereAI CLI installed successfully!"
|
|
104
|
-
echo ""
|
|
105
|
-
info "Get started:"
|
|
106
|
-
echo " fere auth # Set up your agent"
|
|
107
|
-
echo " fere chat \"hello\" # Chat with the AI"
|
|
108
|
-
echo " fere --help # See all commands"
|
|
109
|
-
else
|
|
110
|
-
warn "Installation complete, but 'fere' is not in PATH."
|
|
111
|
-
warn "You may need to restart your shell or add ~/.local/bin to PATH:"
|
|
112
|
-
echo ""
|
|
113
|
-
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
|
|
114
|
-
fi
|
|
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
|