fere-cli 0.2.0.dev14__tar.gz → 0.2.0.dev15__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/PKG-INFO +5 -3
  2. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/README.md +3 -1
  3. fere_cli-0.2.0.dev15/install.sh +272 -0
  4. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/pyproject.toml +2 -2
  5. fere_cli-0.2.0.dev15/tests/test_install_sh.py +599 -0
  6. fere_cli-0.2.0.dev14/install.sh +0 -114
  7. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/.gitignore +0 -0
  8. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/__init__.py +0 -0
  9. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/async_util.py +0 -0
  10. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/banner.py +0 -0
  11. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/commands/__init__.py +0 -0
  12. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/commands/auth.py +0 -0
  13. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/commands/chat.py +0 -0
  14. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/commands/earn.py +0 -0
  15. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/commands/hooks.py +0 -0
  16. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/commands/limit_order.py +0 -0
  17. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/commands/portfolio.py +0 -0
  18. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/commands/swap.py +0 -0
  19. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/commands/utility.py +0 -0
  20. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/config.py +0 -0
  21. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/src/fere_cli/main.py +0 -0
  22. {fere_cli-0.2.0.dev14 → fere_cli-0.2.0.dev15}/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.dev14
3
+ Version: 0.2.0.dev15
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.dev14
9
+ Requires-Dist: fere-sdk>=0.2.0.dev15
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
- Or with pip:
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
- Or with pip:
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.dev14"
3
+ version = "0.2.0.dev15"
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.dev14",
10
+ "fere-sdk>=0.2.0.dev15",
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
@@ -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