adanos-cli 1.20.2__tar.gz → 1.20.4__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 (49) hide show
  1. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/.github/workflows/cli-binaries.yml +38 -11
  2. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/CHANGELOG.md +11 -0
  3. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/PKG-INFO +33 -2
  4. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/README.md +32 -1
  5. adanos_cli-1.20.4/install.ps1 +40 -0
  6. adanos_cli-1.20.4/install.sh +110 -0
  7. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/scripts/build_cli_binary.py +1 -1
  8. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/scripts/generate_homebrew_formula.py +2 -2
  9. adanos_cli-1.20.4/scripts/pyinstaller_entrypoint.py +8 -0
  10. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/__init__.py +1 -1
  11. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/main.py +63 -1
  12. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_distribution.py +75 -1
  13. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_onboarding.py +43 -0
  14. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/.github/workflows/ci.yml +0 -0
  15. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/.github/workflows/publish-pypi.yml +0 -0
  16. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/.gitignore +0 -0
  17. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/LICENSE +0 -0
  18. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/pyproject.toml +0 -0
  19. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/__main__.py +0 -0
  20. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/activity_log.py +0 -0
  21. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/commands/__init__.py +0 -0
  22. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/commands/auth.py +0 -0
  23. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/commands/config.py +0 -0
  24. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/commands/extensions.py +0 -0
  25. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/config.py +0 -0
  26. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/endpoints.py +0 -0
  27. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/nlp.py +0 -0
  28. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/shell_history.py +0 -0
  29. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/summaries.py +0 -0
  30. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/tty.py +0 -0
  31. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/update_notifier.py +0 -0
  32. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/utils.py +0 -0
  33. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/src/adanos_cli/watchlists.py +0 -0
  34. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/__init__.py +0 -0
  35. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/conftest.py +0 -0
  36. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_account.py +0 -0
  37. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_activity_log.py +0 -0
  38. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_agent_contract.py +0 -0
  39. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_auth.py +0 -0
  40. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_config_runtime.py +0 -0
  41. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_diagnostics.py +0 -0
  42. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_endpoint_coverage.py +0 -0
  43. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_extensions.py +0 -0
  44. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_logs.py +0 -0
  45. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_power_features.py +0 -0
  46. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_shell.py +0 -0
  47. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_tty.py +0 -0
  48. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_update_notifier.py +0 -0
  49. {adanos_cli-1.20.2 → adanos_cli-1.20.4}/tests/test_watchlists.py +0 -0
@@ -5,6 +5,9 @@ on:
5
5
  types:
6
6
  - published
7
7
 
8
+ env:
9
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
10
+
8
11
  jobs:
9
12
  build:
10
13
  name: Build CLI Binary (${{ matrix.target }})
@@ -37,7 +40,7 @@ jobs:
37
40
  run: python scripts/build_cli_binary.py --output-dir dist-binaries
38
41
 
39
42
  - name: Upload binary artifacts
40
- uses: actions/upload-artifact@v4
43
+ uses: actions/upload-artifact@v6
41
44
  with:
42
45
  name: cli-binary-${{ matrix.target }}
43
46
  if-no-files-found: error
@@ -55,7 +58,7 @@ jobs:
55
58
  ref: ${{ github.event.release.tag_name }}
56
59
 
57
60
  - name: Download binary artifacts
58
- uses: actions/download-artifact@v5
61
+ uses: actions/download-artifact@v7
59
62
  with:
60
63
  path: release-artifacts
61
64
 
@@ -79,10 +82,9 @@ jobs:
79
82
  ls -la release-upload
80
83
 
81
84
  - name: Publish release assets
82
- uses: softprops/action-gh-release@v2
83
- with:
84
- tag_name: ${{ github.event.release.tag_name }}
85
- files: release-upload/*
85
+ env:
86
+ GH_TOKEN: ${{ github.token }}
87
+ run: gh release upload "${{ github.event.release.tag_name }}" release-upload/* --clobber
86
88
 
87
89
  homebrew-formula:
88
90
  name: Generate Homebrew Formula
@@ -90,13 +92,15 @@ jobs:
90
92
  runs-on: ubuntu-latest
91
93
  permissions:
92
94
  contents: write
95
+ env:
96
+ HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
93
97
  steps:
94
98
  - uses: actions/checkout@v5
95
99
  with:
96
100
  ref: ${{ github.event.release.tag_name }}
97
101
 
98
102
  - name: Download binary artifacts
99
- uses: actions/download-artifact@v5
103
+ uses: actions/download-artifact@v7
100
104
  with:
101
105
  path: formula-artifacts
102
106
 
@@ -129,13 +133,36 @@ jobs:
129
133
  --output dist/homebrew/adanos-cli.rb
130
134
 
131
135
  - name: Upload Homebrew formula artifact
132
- uses: actions/upload-artifact@v4
136
+ uses: actions/upload-artifact@v6
133
137
  with:
134
138
  name: homebrew-formula
135
139
  path: dist/homebrew/adanos-cli.rb
136
140
 
137
141
  - name: Attach Homebrew formula to release
138
- uses: softprops/action-gh-release@v2
142
+ env:
143
+ GH_TOKEN: ${{ github.token }}
144
+ run: gh release upload "${{ github.event.release.tag_name }}" dist/homebrew/adanos-cli.rb --clobber
145
+
146
+ - name: Checkout Homebrew tap repository
147
+ if: ${{ env.HOMEBREW_TAP_TOKEN != '' }}
148
+ uses: actions/checkout@v5
139
149
  with:
140
- tag_name: ${{ github.event.release.tag_name }}
141
- files: dist/homebrew/adanos-cli.rb
150
+ repository: adanos-software/homebrew-tap
151
+ token: ${{ env.HOMEBREW_TAP_TOKEN }}
152
+ path: homebrew-tap
153
+
154
+ - name: Publish formula to Homebrew tap
155
+ if: ${{ env.HOMEBREW_TAP_TOKEN != '' }}
156
+ run: |
157
+ mkdir -p homebrew-tap/Formula
158
+ cp dist/homebrew/adanos-cli.rb homebrew-tap/Formula/adanos-cli.rb
159
+ cd homebrew-tap
160
+ git config user.name "github-actions[bot]"
161
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
162
+ git add Formula/adanos-cli.rb
163
+ if git diff --cached --quiet; then
164
+ echo "Homebrew tap already up to date."
165
+ exit 0
166
+ fi
167
+ git commit -m "Update adanos-cli formula for ${{ github.event.release.tag_name }}"
168
+ git push origin HEAD:main
@@ -5,6 +5,17 @@ All notable changes to `adanos-cli` will be documented in this file.
5
5
  Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
6
  Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
7
 
8
+ ## [1.20.4] - 2026-03-17
9
+
10
+ ### Added
11
+ - Added `adanos onboard recover --email ...` so existing users can trigger the secure email-based recovery flow from the CLI without exposing recovery tokens in terminal output.
12
+
13
+ ## [1.20.3] - 2026-03-16
14
+
15
+ ### Fixed
16
+ - Standalone release binaries now boot through a package-safe PyInstaller entry point instead of failing on relative imports.
17
+ - Shell installer checksum verification now accepts the `release-upload/` paths emitted by the binary release workflow.
18
+
8
19
  ## [1.20.2] - 2026-03-15
9
20
 
10
21
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adanos-cli
3
- Version: 1.20.2
3
+ Version: 1.20.4
4
4
  Summary: CLI for the Adanos Finance Sentiment API
5
5
  Project-URL: Homepage, https://adanos.org
6
6
  Project-URL: API_Docs, https://api.adanos.org/docs
@@ -43,6 +43,31 @@ The CLI is versioned independently from the API backend. It targets the public A
43
43
  pipx install adanos-cli
44
44
  ```
45
45
 
46
+ ### cURL
47
+
48
+ ```bash
49
+ curl -fsSL https://raw.githubusercontent.com/adanos-software/adanos-cli/main/install.sh | bash
50
+ ```
51
+
52
+ The shell installer downloads the latest standalone binary for:
53
+ - macOS arm64
54
+ - macOS x86_64
55
+ - Linux x86_64
56
+
57
+ By default it installs to `~/.local/bin`. Override with `ADANOS_INSTALL_DIR=/your/path`.
58
+
59
+ ### Homebrew (macOS / Linux)
60
+
61
+ ```bash
62
+ brew install adanos-software/tap/adanos-cli
63
+ ```
64
+
65
+ ### PowerShell (Windows)
66
+
67
+ ```powershell
68
+ irm https://raw.githubusercontent.com/adanos-software/adanos-cli/main/install.ps1 | iex
69
+ ```
70
+
46
71
  ### Plain pip
47
72
 
48
73
  ```bash
@@ -110,6 +135,12 @@ Persist a key locally:
110
135
  adanos login --api-key sk_live_xxx
111
136
  ```
112
137
 
138
+ Request a recovery email for an existing account:
139
+
140
+ ```bash
141
+ adanos onboard recover --email you@example.com
142
+ ```
143
+
113
144
  Use profiles:
114
145
 
115
146
  ```bash
@@ -198,7 +229,7 @@ Tagged releases build standalone archives for:
198
229
  - macOS x86_64
199
230
  - Linux x86_64
200
231
 
201
- The repo also generates a Homebrew formula artifact for each tagged binary release.
232
+ The repo also generates a Homebrew formula artifact for each tagged binary release and can publish it to `adanos-software/homebrew-tap` when `HOMEBREW_TAP_TOKEN` is configured.
202
233
 
203
234
  PyPI publishing also happens from this repo, not from the API monorepo.
204
235
 
@@ -17,6 +17,31 @@ The CLI is versioned independently from the API backend. It targets the public A
17
17
  pipx install adanos-cli
18
18
  ```
19
19
 
20
+ ### cURL
21
+
22
+ ```bash
23
+ curl -fsSL https://raw.githubusercontent.com/adanos-software/adanos-cli/main/install.sh | bash
24
+ ```
25
+
26
+ The shell installer downloads the latest standalone binary for:
27
+ - macOS arm64
28
+ - macOS x86_64
29
+ - Linux x86_64
30
+
31
+ By default it installs to `~/.local/bin`. Override with `ADANOS_INSTALL_DIR=/your/path`.
32
+
33
+ ### Homebrew (macOS / Linux)
34
+
35
+ ```bash
36
+ brew install adanos-software/tap/adanos-cli
37
+ ```
38
+
39
+ ### PowerShell (Windows)
40
+
41
+ ```powershell
42
+ irm https://raw.githubusercontent.com/adanos-software/adanos-cli/main/install.ps1 | iex
43
+ ```
44
+
20
45
  ### Plain pip
21
46
 
22
47
  ```bash
@@ -84,6 +109,12 @@ Persist a key locally:
84
109
  adanos login --api-key sk_live_xxx
85
110
  ```
86
111
 
112
+ Request a recovery email for an existing account:
113
+
114
+ ```bash
115
+ adanos onboard recover --email you@example.com
116
+ ```
117
+
87
118
  Use profiles:
88
119
 
89
120
  ```bash
@@ -172,7 +203,7 @@ Tagged releases build standalone archives for:
172
203
  - macOS x86_64
173
204
  - Linux x86_64
174
205
 
175
- The repo also generates a Homebrew formula artifact for each tagged binary release.
206
+ The repo also generates a Homebrew formula artifact for each tagged binary release and can publish it to `adanos-software/homebrew-tap` when `HOMEBREW_TAP_TOKEN` is configured.
176
207
 
177
208
  PyPI publishing also happens from this repo, not from the API monorepo.
178
209
 
@@ -0,0 +1,40 @@
1
+ $ErrorActionPreference = "Stop"
2
+
3
+ $packageName = "adanos-cli"
4
+
5
+ function Write-Info($message) {
6
+ Write-Host $message
7
+ }
8
+
9
+ function Get-PythonCommand {
10
+ foreach ($candidate in @("py", "python", "python3")) {
11
+ if (Get-Command $candidate -ErrorAction SilentlyContinue) {
12
+ return $candidate
13
+ }
14
+ }
15
+ throw "Python is required. Install Python 3.10+ and re-run this script."
16
+ }
17
+
18
+ $python = Get-PythonCommand
19
+ $pipx = Get-Command pipx -ErrorAction SilentlyContinue
20
+
21
+ if (-not $pipx) {
22
+ Write-Info "Installing pipx..."
23
+ & $python -m pip install --user --upgrade pip pipx
24
+ & $python -m pipx ensurepath | Out-Null
25
+ }
26
+
27
+ $pipx = Get-Command pipx -ErrorAction SilentlyContinue
28
+
29
+ if ($pipx) {
30
+ Write-Info "Installing $packageName with pipx..."
31
+ & $pipx.Source install --force $packageName
32
+ } else {
33
+ Write-Info "pipx is not available in the current session. Falling back to pip --user."
34
+ & $python -m pip install --user --upgrade $packageName
35
+ }
36
+
37
+ Write-Info ""
38
+ Write-Info "Installed $packageName."
39
+ Write-Info "Verify with:"
40
+ Write-Info " adanos --version"
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REPO="adanos-software/adanos-cli"
5
+ LATEST_BASE_URL="https://github.com/${REPO}/releases/latest/download"
6
+ DEFAULT_INSTALL_DIR="${HOME}/.local/bin"
7
+ INSTALL_DIR="${ADANOS_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
8
+
9
+ log() {
10
+ printf '%s\n' "$*"
11
+ }
12
+
13
+ fail() {
14
+ printf 'error: %s\n' "$*" >&2
15
+ exit 1
16
+ }
17
+
18
+ require_cmd() {
19
+ command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1"
20
+ }
21
+
22
+ detect_asset() {
23
+ local os arch
24
+ os="$(uname -s)"
25
+ arch="$(uname -m)"
26
+
27
+ case "$os" in
28
+ Darwin)
29
+ case "$arch" in
30
+ arm64|aarch64) printf 'adanos-darwin-arm64.tar.gz' ;;
31
+ x86_64|amd64) printf 'adanos-darwin-x86_64.tar.gz' ;;
32
+ *) fail "unsupported macOS architecture: ${arch}" ;;
33
+ esac
34
+ ;;
35
+ Linux)
36
+ case "$arch" in
37
+ x86_64|amd64) printf 'adanos-linux-x86_64.tar.gz' ;;
38
+ *) fail "unsupported Linux architecture: ${arch}" ;;
39
+ esac
40
+ ;;
41
+ *)
42
+ fail "unsupported operating system: ${os}"
43
+ ;;
44
+ esac
45
+ }
46
+
47
+ verify_checksum() {
48
+ local archive_path checksums_path asset_name expected actual
49
+ archive_path="$1"
50
+ checksums_path="$2"
51
+ asset_name="$3"
52
+
53
+ expected="$(awk -v asset="$asset_name" '$2 ~ ("(^|/)" asset "$") {print $1; exit}' "$checksums_path")"
54
+ [ -n "$expected" ] || fail "could not find checksum for ${asset_name}"
55
+
56
+ if command -v shasum >/dev/null 2>&1; then
57
+ actual="$(shasum -a 256 "$archive_path" | awk '{print $1}')"
58
+ elif command -v sha256sum >/dev/null 2>&1; then
59
+ actual="$(sha256sum "$archive_path" | awk '{print $1}')"
60
+ else
61
+ fail "missing checksum tool (shasum or sha256sum)"
62
+ fi
63
+
64
+ [ "$actual" = "$expected" ] || fail "checksum mismatch for ${asset_name}"
65
+ }
66
+
67
+ print_path_hint() {
68
+ case ":${PATH}:" in
69
+ *":${INSTALL_DIR}:"*) ;;
70
+ *)
71
+ log
72
+ log "Add this to your shell profile if needed:"
73
+ log " export PATH=\"${INSTALL_DIR}:\$PATH\""
74
+ ;;
75
+ esac
76
+ }
77
+
78
+ main() {
79
+ require_cmd curl
80
+ require_cmd tar
81
+ require_cmd mktemp
82
+ require_cmd install
83
+
84
+ local asset_name tmp_dir archive_path checksums_path
85
+ asset_name="$(detect_asset)"
86
+ tmp_dir="$(mktemp -d)"
87
+ archive_path="${tmp_dir}/${asset_name}"
88
+ checksums_path="${tmp_dir}/SHA256SUMS.txt"
89
+
90
+ log "Downloading ${asset_name}..."
91
+ curl -fsSL "${LATEST_BASE_URL}/${asset_name}" -o "$archive_path"
92
+ curl -fsSL "${LATEST_BASE_URL}/SHA256SUMS.txt" -o "$checksums_path"
93
+
94
+ verify_checksum "$archive_path" "$checksums_path" "$asset_name"
95
+
96
+ mkdir -p "${INSTALL_DIR}"
97
+ tar -xzf "$archive_path" -C "$tmp_dir"
98
+ install -m 0755 "${tmp_dir}/adanos" "${INSTALL_DIR}/adanos"
99
+
100
+ rm -rf "$tmp_dir"
101
+
102
+ log
103
+ log "Installed adanos to ${INSTALL_DIR}/adanos"
104
+ print_path_hint
105
+ log
106
+ log "Verify with:"
107
+ log " adanos --version"
108
+ }
109
+
110
+ main "$@"
@@ -15,7 +15,7 @@ from pathlib import Path
15
15
 
16
16
  REPO_ROOT = Path(__file__).resolve().parent.parent
17
17
  CLI_SRC = REPO_ROOT / "src"
18
- CLI_ENTRYPOINT = CLI_SRC / "adanos_cli" / "__main__.py"
18
+ CLI_ENTRYPOINT = REPO_ROOT / "scripts" / "pyinstaller_entrypoint.py"
19
19
  PACKAGE_INIT = CLI_SRC / "adanos_cli" / "__init__.py"
20
20
 
21
21
 
@@ -19,7 +19,7 @@ def render_formula(
19
19
  ) -> str:
20
20
  return f"""class AdanosCli < Formula
21
21
  desc "Comprehensive CLI for the Adanos Finance Sentiment API"
22
- homepage "https://api.adanos.org"
22
+ homepage "https://adanos.org"
23
23
  version "{version}"
24
24
 
25
25
  on_macos do
@@ -42,7 +42,7 @@ def render_formula(
42
42
  end
43
43
 
44
44
  test do
45
- assert_match "adanos", shell_output("#{bin}/adanos --help")
45
+ assert_match "adanos", shell_output("#{{bin}}/adanos --help")
46
46
  end
47
47
  end
48
48
  """
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env python3
2
+ """PyInstaller entry point for the standalone adanos binary."""
3
+
4
+ from adanos_cli.main import main
5
+
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
@@ -1,4 +1,4 @@
1
1
  """Adanos CLI package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "1.20.2"
4
+ __version__ = "1.20.4"
@@ -67,6 +67,7 @@ from .watchlists import (
67
67
 
68
68
  SUPPORT_CONTACT_EMAIL = "support@adanos.org"
69
69
  PAID_ACCOUNT_TYPES = {"hobby", "professional"}
70
+ DEFAULT_RECOVERY_REQUEST_URL = "https://adanos.org/api/recover"
70
71
 
71
72
 
72
73
  def _load_sdk_client_class() -> Any:
@@ -95,7 +96,9 @@ def _print_onboarding_guide(base_url: str, *, has_api_key: bool = False) -> None
95
96
  print("3) Manual alternative:")
96
97
  print(' adanos onboard register --name "Your Name" --email "you@example.com" --purpose "CLI usage for stocks and crypto"')
97
98
  print(" adanos onboard redeem --token <delivery_token> --save")
98
- print("4) Start using the API:")
99
+ print("4) If you lost access to an existing key:")
100
+ print(' adanos onboard recover --email "you@example.com"')
101
+ print("5) Start using the API:")
99
102
  print(' adanos ask "How does TSLA look?"')
100
103
  print(f"API base URL: {base_url}")
101
104
 
@@ -916,6 +919,14 @@ def _request_onboard_redeem(base_url: str, token: str) -> tuple[int, dict[str, A
916
919
  return response.status_code, data, message
917
920
 
918
921
 
922
+ def _request_onboard_recover(recovery_url: str, payload: dict[str, str]) -> tuple[int, dict[str, Any] | None, str]:
923
+ with httpx.Client(timeout=30.0) as http:
924
+ response = http.post(recovery_url, json=payload)
925
+ data = _decode_json_dict(response)
926
+ message = _extract_error_message(response)
927
+ return response.status_code, data, message
928
+
929
+
919
930
  def _request_account_status(
920
931
  base_url: str, api_key: str, *, timeout_s: float = 30.0
921
932
  ) -> tuple[int, dict[str, Any] | None, str, dict[str, str]]:
@@ -1628,6 +1639,46 @@ def _run_onboard_redeem(args: Namespace, base_url: str, *, json_mode: bool) -> i
1628
1639
  return 0
1629
1640
 
1630
1641
 
1642
+ def _run_onboard_recover(args: Namespace, *, json_mode: bool) -> int:
1643
+ recovery_url = str(args.recovery_url or DEFAULT_RECOVERY_REQUEST_URL).strip()
1644
+ status_code, data, message = _request_onboard_recover(recovery_url, {"email": args.email})
1645
+ if status_code not in {200, 202}:
1646
+ _emit_error(
1647
+ json_mode=json_mode,
1648
+ code="onboard_recover_failed",
1649
+ message=message if message else "Recovery request failed",
1650
+ hint="Retry later or open https://adanos.org/key to use the hosted recovery form.",
1651
+ status_code=status_code,
1652
+ )
1653
+ return 1
1654
+
1655
+ if not isinstance(data, dict) or data.get("success") is not True:
1656
+ _emit_error(
1657
+ json_mode=json_mode,
1658
+ code="onboard_recover_failed",
1659
+ message="Response did not include structured recovery data",
1660
+ status_code=status_code,
1661
+ )
1662
+ return 1
1663
+
1664
+ if json_mode:
1665
+ print_json(
1666
+ with_json_metadata(
1667
+ data,
1668
+ kind="onboard_recovery",
1669
+ command="onboard",
1670
+ subcommand="recover",
1671
+ )
1672
+ )
1673
+ else:
1674
+ print("Recovery request accepted.")
1675
+ print(str(data.get("message") or "If an account exists, further recovery instructions will be sent separately."))
1676
+ print("Next step:")
1677
+ print(" Check your email for the secure recovery link.")
1678
+
1679
+ return 0
1680
+
1681
+
1631
1682
  def _run_onboard_wizard(args: Namespace, base_url: str, *, json_mode: bool, runtime_has_key: bool) -> int:
1632
1683
  if json_mode:
1633
1684
  _emit_error(
@@ -1789,6 +1840,12 @@ def _build_parser() -> argparse.ArgumentParser:
1789
1840
  p_onboard_redeem.add_argument("--json", action="store_true")
1790
1841
  p_onboard_redeem.set_defaults(_handler="onboard_redeem")
1791
1842
 
1843
+ p_onboard_recover = onboard_subs.add_parser("recover", help="Request a recovery email for an existing API key")
1844
+ p_onboard_recover.add_argument("--email", required=True, help="Registered email address for the API account")
1845
+ p_onboard_recover.add_argument("--recovery-url", help=argparse.SUPPRESS)
1846
+ p_onboard_recover.add_argument("--json", action="store_true")
1847
+ p_onboard_recover.set_defaults(_handler="onboard_recover")
1848
+
1792
1849
  p_onboard.set_defaults(_handler="onboard_guide")
1793
1850
 
1794
1851
  p_auth = subs.add_parser("auth", help="Manage local auth profiles and active credentials")
@@ -2111,6 +2168,7 @@ def _requires_api_key(args: Namespace) -> bool:
2111
2168
  "onboard_wizard",
2112
2169
  "onboard_register",
2113
2170
  "onboard_redeem",
2171
+ "onboard_recover",
2114
2172
  "auth_login",
2115
2173
  "auth_logout",
2116
2174
  "auth_list",
@@ -3354,6 +3412,7 @@ def _main_impl(raw_argv: list[str], *, argv_supplied: bool) -> int:
3354
3412
  "adanos onboard wizard",
3355
3413
  'adanos onboard register --name "Your Name" --email "you@example.com" --purpose "CLI usage for stocks and crypto"',
3356
3414
  "adanos onboard redeem --token <delivery_token> --save",
3415
+ 'adanos onboard recover --email "you@example.com"',
3357
3416
  ],
3358
3417
  },
3359
3418
  },
@@ -3380,6 +3439,9 @@ def _main_impl(raw_argv: list[str], *, argv_supplied: bool) -> int:
3380
3439
  if handler == "onboard_redeem":
3381
3440
  return _run_onboard_redeem(args, runtime_cfg.base_url, json_mode=args.json)
3382
3441
 
3442
+ if handler == "onboard_recover":
3443
+ return _run_onboard_recover(args, json_mode=args.json)
3444
+
3383
3445
  if _requires_api_key(args) and not runtime_cfg.api_key:
3384
3446
  if args.json:
3385
3447
  _emit_error(
@@ -2,13 +2,19 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
6
+ import stat
7
+ import subprocess
5
8
  import sys
9
+ from pathlib import Path
6
10
 
7
11
  from adanos_cli import __version__
8
12
  import scripts.generate_homebrew_formula as homebrew_formula
9
13
  from scripts.build_cli_binary import CLI_ENTRYPOINT, CLI_SRC, build_pyinstaller_command, detect_target, read_package_version
10
14
  from scripts.generate_homebrew_formula import render_formula
11
15
 
16
+ REPO_ROOT = Path(__file__).resolve().parents[1]
17
+
12
18
 
13
19
  def test_render_formula_includes_all_targets() -> None:
14
20
  formula = render_formula(
@@ -62,6 +68,13 @@ def test_build_pyinstaller_command_uses_repo_local_paths(tmp_path) -> None:
62
68
  assert "stocksentiment" in command
63
69
 
64
70
 
71
+ def test_pyinstaller_entrypoint_imports_main() -> None:
72
+ entrypoint = CLI_ENTRYPOINT.read_text(encoding="utf-8")
73
+
74
+ assert "from adanos_cli.main import main" in entrypoint
75
+ assert "raise SystemExit(main())" in entrypoint
76
+
77
+
65
78
  def test_render_formula_includes_install_and_test_blocks() -> None:
66
79
  formula = render_formula(
67
80
  version="1.20.0",
@@ -74,8 +87,9 @@ def test_render_formula_includes_install_and_test_blocks() -> None:
74
87
  )
75
88
 
76
89
  assert "class AdanosCli < Formula" in formula
90
+ assert 'homepage "https://adanos.org"' in formula
77
91
  assert "def install" in formula
78
- assert 'assert_match "adanos"' in formula
92
+ assert 'assert_match "adanos", shell_output("#{bin}/adanos --help")' in formula
79
93
 
80
94
 
81
95
  def test_homebrew_formula_main_writes_output_file(tmp_path, monkeypatch) -> None:
@@ -108,3 +122,63 @@ def test_homebrew_formula_main_writes_output_file(tmp_path, monkeypatch) -> None
108
122
 
109
123
  assert output_path.exists()
110
124
  assert 'version "1.20.0"' in output_path.read_text(encoding="utf-8")
125
+
126
+
127
+ def test_install_shell_script_uses_latest_release_assets() -> None:
128
+ script = (REPO_ROOT / "install.sh").read_text(encoding="utf-8")
129
+
130
+ assert "adanos-software/adanos-cli" in script
131
+ assert "releases/latest/download" in script
132
+ assert "SHA256SUMS.txt" in script
133
+ assert "adanos-darwin-arm64.tar.gz" in script
134
+ assert "adanos-darwin-x86_64.tar.gz" in script
135
+ assert "adanos-linux-x86_64.tar.gz" in script
136
+ assert "ADANOS_INSTALL_DIR" in script
137
+
138
+
139
+ def test_install_powershell_script_bootstraps_pipx() -> None:
140
+ script = (REPO_ROOT / "install.ps1").read_text(encoding="utf-8")
141
+
142
+ assert "adanos-cli" in script
143
+ assert "pipx" in script
144
+ assert "install --force" in script
145
+ assert "Verify with:" in script
146
+
147
+
148
+ def test_readme_documents_shell_homebrew_and_powershell_install() -> None:
149
+ readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
150
+
151
+ assert "curl -fsSL https://raw.githubusercontent.com/adanos-software/adanos-cli/main/install.sh | bash" in readme
152
+ assert "brew install adanos-software/tap/adanos-cli" in readme
153
+ assert "irm https://raw.githubusercontent.com/adanos-software/adanos-cli/main/install.ps1 | iex" in readme
154
+
155
+
156
+ def test_install_shell_script_accepts_release_upload_checksum_prefix(tmp_path) -> None:
157
+ archive_path = tmp_path / "adanos-darwin-arm64.tar.gz"
158
+ archive_path.write_bytes(b"fake-archive")
159
+
160
+ checksum = subprocess.check_output(
161
+ ["shasum", "-a", "256", str(archive_path)],
162
+ text=True,
163
+ ).split()[0]
164
+ checksums_path = tmp_path / "SHA256SUMS.txt"
165
+ checksums_path.write_text(
166
+ f"{checksum} release-upload/{archive_path.name}\n",
167
+ encoding="utf-8",
168
+ )
169
+
170
+ script_text = (REPO_ROOT / "install.sh").read_text(encoding="utf-8")
171
+ script_without_main = script_text.rsplit('main "$@"', 1)[0]
172
+ harness_path = tmp_path / "install-harness.sh"
173
+ harness_path.write_text(
174
+ script_without_main
175
+ + f'\nverify_checksum "{archive_path}" "{checksums_path}" "{archive_path.name}"\n',
176
+ encoding="utf-8",
177
+ )
178
+ harness_path.chmod(harness_path.stat().st_mode | stat.S_IXUSR)
179
+
180
+ subprocess.run(
181
+ ["bash", str(harness_path)],
182
+ check=True,
183
+ env={**os.environ, "HOME": str(tmp_path)},
184
+ )
@@ -76,6 +76,7 @@ def test_onboard_guide_is_cli_first(capsys) -> None:
76
76
  assert "adanos login --api-key sk_live_xxx" in out
77
77
  assert "adanos onboard wizard" in out
78
78
  assert "adanos onboard register" in out
79
+ assert "adanos onboard recover" in out
79
80
  assert "curl -s" not in out
80
81
 
81
82
 
@@ -158,6 +159,48 @@ def test_onboard_register_returns_token_and_next_step(capsys) -> None:
158
159
  assert f"adanos onboard redeem --token {token} --save" in out
159
160
 
160
161
 
162
+ @respx.mock
163
+ def test_onboard_recover_requests_email_confirmation(capsys) -> None:
164
+ respx.post("https://adanos.org/api/recover").mock(
165
+ return_value=httpx.Response(
166
+ 200,
167
+ json={
168
+ "success": True,
169
+ "action": "accepted",
170
+ "message": "If an active account exists for this email, further recovery instructions will be sent separately.",
171
+ },
172
+ )
173
+ )
174
+
175
+ rc = cli_main.main(["onboard", "recover", "--email", "alex@example.com"])
176
+ out = capsys.readouterr().out
177
+ assert rc == 0
178
+ assert "Recovery request accepted." in out
179
+ assert "Check your email for the secure recovery link." in out
180
+
181
+
182
+ @respx.mock
183
+ def test_onboard_recover_json_outputs_structured_response(capsys) -> None:
184
+ respx.post("https://adanos.org/api/recover").mock(
185
+ return_value=httpx.Response(
186
+ 200,
187
+ json={
188
+ "success": True,
189
+ "action": "accepted",
190
+ "message": "If an active account exists for this email, further recovery instructions will be sent separately.",
191
+ },
192
+ )
193
+ )
194
+
195
+ rc = cli_main.main(["--output", "json", "onboard", "recover", "--email", "alex@example.com", "--json"])
196
+ captured = capsys.readouterr()
197
+ assert rc == 0
198
+ payload = json.loads(captured.out)
199
+ assert payload["kind"] == "onboard_recovery"
200
+ assert payload["success"] is True
201
+ assert payload["action"] == "accepted"
202
+
203
+
161
204
  @respx.mock
162
205
  def test_onboard_redeem_save_writes_config(tmp_path, monkeypatch) -> None:
163
206
  token = "kt_a8Kj2mNpQrStUvWxYz1234567890AbCdEfGhIjKlMnO"
File without changes
File without changes
File without changes