esprit-cli 0.7.3 → 0.7.4

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.
package/README.md CHANGED
@@ -24,10 +24,16 @@ brew install esprit
24
24
  ### Option 3: npm
25
25
 
26
26
  ```bash
27
- npm install -g github:improdead/Esprit
27
+ npm install -g esprit-cli@latest
28
28
  ```
29
29
 
30
- ### Option 4: From Source
30
+ ### Option 4: pip (PyPI)
31
+
32
+ ```bash
33
+ pip install esprit-cli
34
+ ```
35
+
36
+ ### Option 5: From Source
31
37
 
32
38
  ```bash
33
39
  git clone https://github.com/improdead/Esprit.git
@@ -40,7 +46,7 @@ poetry install
40
46
 
41
47
  ## Interactive Onboarding
42
48
 
43
- Run `esprit` with no arguments to open the launchpad onboarding UI.
49
+ Run `esprit` with no arguments to open the interactive launchpad UI.
44
50
 
45
51
  - Guided setup for provider, model, target, and scan mode
46
52
  - Unified theme across onboarding and scanning TUI
package/bin/esprit.js CHANGED
@@ -9,25 +9,34 @@ const isWindows = process.platform === "win32";
9
9
  const binaryName = isWindows ? "esprit.exe" : "esprit";
10
10
  const installDir = path.join(os.homedir(), ".esprit", "bin");
11
11
  const binaryPath = path.join(installDir, binaryName);
12
- const installerPath = path.resolve(__dirname, "..", "scripts", "install.sh");
12
+ const installerPath = path.resolve(
13
+ __dirname,
14
+ "..",
15
+ "scripts",
16
+ isWindows ? "install.ps1" : "install.sh"
17
+ );
13
18
 
14
19
  function ensureInstalled() {
15
20
  if (fs.existsSync(binaryPath)) {
16
21
  return;
17
22
  }
18
23
 
19
- if (isWindows) {
20
- console.error("[esprit] installer currently supports macOS/Linux. Use WSL on Windows.");
21
- process.exit(1);
22
- }
24
+ const bootstrapEnv = {
25
+ ...process.env,
26
+ ESPRIT_SKIP_DOCKER_WARM: process.env.ESPRIT_SKIP_DOCKER_WARM || "1",
27
+ };
28
+
29
+ const bootstrap = isWindows
30
+ ? spawnSync(
31
+ "powershell",
32
+ ["-ExecutionPolicy", "Bypass", "-File", installerPath],
33
+ { stdio: "inherit", env: bootstrapEnv }
34
+ )
35
+ : spawnSync("bash", [installerPath], {
36
+ stdio: "inherit",
37
+ env: bootstrapEnv,
38
+ });
23
39
 
24
- const bootstrap = spawnSync("bash", [installerPath], {
25
- stdio: "inherit",
26
- env: {
27
- ...process.env,
28
- ESPRIT_SKIP_DOCKER_WARM: process.env.ESPRIT_SKIP_DOCKER_WARM || "1",
29
- },
30
- });
31
40
  if (bootstrap.status !== 0) {
32
41
  process.exit(bootstrap.status || 1);
33
42
  }
@@ -6,21 +6,30 @@ import { fileURLToPath } from "node:url";
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
- const installerPath = path.resolve(__dirname, "..", "scripts", "install.sh");
9
+ const isWindows = process.platform === "win32";
10
+ const installerPath = path.resolve(
11
+ __dirname,
12
+ "..",
13
+ "scripts",
14
+ isWindows ? "install.ps1" : "install.sh"
15
+ );
10
16
 
11
- if (process.platform === "win32") {
12
- process.stdout.write("[esprit] npm install currently supports macOS/Linux. Use WSL on Windows.\n");
13
- process.exit(0);
14
- }
17
+ const installEnv = {
18
+ ...process.env,
19
+ // npm installs should be fast/predictable; sandbox image is pulled at first scan.
20
+ ESPRIT_SKIP_DOCKER_WARM: "1",
21
+ };
15
22
 
16
- const result = spawnSync("bash", [installerPath], {
17
- stdio: "inherit",
18
- env: {
19
- ...process.env,
20
- // npm installs should be fast/predictable; sandbox image is pulled at first scan.
21
- ESPRIT_SKIP_DOCKER_WARM: "1",
22
- },
23
- });
23
+ const result = isWindows
24
+ ? spawnSync(
25
+ "powershell",
26
+ ["-ExecutionPolicy", "Bypass", "-File", installerPath],
27
+ { stdio: "inherit", env: installEnv }
28
+ )
29
+ : spawnSync("bash", [installerPath], {
30
+ stdio: "inherit",
31
+ env: installEnv,
32
+ });
24
33
 
25
34
  if (result.status !== 0) {
26
35
  process.exit(result.status || 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esprit-cli",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "AI-powered penetration testing agent",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://esprit.dev",
@@ -0,0 +1,228 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ Installs the Esprit runtime on Windows.
5
+ .DESCRIPTION
6
+ Mirrors the functionality of install.sh for Windows/PowerShell:
7
+ 1. Clones Esprit repo to ~/.esprit/runtime
8
+ 2. Creates a Python venv at ~/.esprit/venv
9
+ 3. Installs dependencies via pip
10
+ 4. Creates an esprit.cmd launcher
11
+ 5. Adds the bin dir to user PATH
12
+ 6. Optionally warms the Docker image
13
+ #>
14
+
15
+ param(
16
+ [switch]$Force
17
+ )
18
+
19
+ $ErrorActionPreference = 'Stop'
20
+
21
+ $APP = 'esprit'
22
+ $REPO_URL = if ($env:ESPRIT_REPO_URL) { $env:ESPRIT_REPO_URL } else { 'https://github.com/improdead/Esprit.git' }
23
+ $REPO_REF = if ($env:ESPRIT_REPO_REF) { $env:ESPRIT_REPO_REF } else { 'main' }
24
+ $INSTALL_ROOT = if ($env:ESPRIT_HOME) { $env:ESPRIT_HOME } else { Join-Path $env:USERPROFILE '.esprit' }
25
+ $BIN_DIR = Join-Path $INSTALL_ROOT 'bin'
26
+ $RUNTIME_DIR = Join-Path $INSTALL_ROOT 'runtime'
27
+ $VENV_DIR = Join-Path $INSTALL_ROOT 'venv'
28
+ $LAUNCHER_CMD = Join-Path $BIN_DIR 'esprit.cmd'
29
+ $ESPRIT_IMAGE = if ($env:ESPRIT_IMAGE) { $env:ESPRIT_IMAGE } else { 'improdead/esprit-sandbox:latest' }
30
+
31
+ function Print-Message {
32
+ param([string]$Level, [string]$Message)
33
+ switch ($Level) {
34
+ 'success' { Write-Host $Message -ForegroundColor Green }
35
+ 'warning' { Write-Host $Message -ForegroundColor Yellow }
36
+ 'error' { Write-Host $Message -ForegroundColor Red }
37
+ 'info' { Write-Host $Message -ForegroundColor DarkGray }
38
+ default { Write-Host $Message }
39
+ }
40
+ }
41
+
42
+ function Require-Command {
43
+ param([string]$Cmd, [string]$Hint)
44
+ if (-not (Get-Command $Cmd -ErrorAction SilentlyContinue)) {
45
+ Print-Message 'error' "Missing required command: $Cmd"
46
+ Print-Message 'info' $Hint
47
+ exit 1
48
+ }
49
+ }
50
+
51
+ function Choose-Python {
52
+ # Try py launcher first (standard on Windows), then bare python
53
+ foreach ($candidate in @('py -3.13', 'py -3.12', 'python')) {
54
+ try {
55
+ $parts = $candidate -split ' ', 2
56
+ $exe = $parts[0]
57
+ $args = if ($parts.Length -gt 1) { $parts[1] } else { $null }
58
+
59
+ if (-not (Get-Command $exe -ErrorAction SilentlyContinue)) { continue }
60
+
61
+ $checkArgs = @()
62
+ if ($args) { $checkArgs += $args }
63
+ $checkArgs += '-c'
64
+ $checkArgs += 'import sys; raise SystemExit(0 if sys.version_info >= (3, 12) else 1)'
65
+
66
+ $proc = & $exe @checkArgs 2>$null
67
+ if ($LASTEXITCODE -eq 0) { return $candidate }
68
+ } catch {
69
+ continue
70
+ }
71
+ }
72
+ return $null
73
+ }
74
+
75
+ function Invoke-Python {
76
+ param([string]$Candidate, [string[]]$Arguments)
77
+ $parts = $Candidate -split ' ', 2
78
+ $exe = $parts[0]
79
+ $allArgs = @()
80
+ if ($parts.Length -gt 1) { $allArgs += $parts[1] }
81
+ $allArgs += $Arguments
82
+ & $exe @allArgs
83
+ if ($LASTEXITCODE -ne 0) {
84
+ throw "Command '$Candidate $($Arguments -join ' ')' failed with exit code $LASTEXITCODE"
85
+ }
86
+ }
87
+
88
+ function Sync-RuntimeRepo {
89
+ Print-Message 'info' 'Syncing Esprit runtime source...'
90
+
91
+ if (Test-Path (Join-Path $RUNTIME_DIR '.git')) {
92
+ & git -C $RUNTIME_DIR remote set-url origin $REPO_URL
93
+ & git -C $RUNTIME_DIR fetch --depth 1 origin $REPO_REF
94
+ if ($LASTEXITCODE -ne 0) { throw 'git fetch failed' }
95
+ & git -C $RUNTIME_DIR checkout -q FETCH_HEAD
96
+ if ($LASTEXITCODE -ne 0) { throw 'git checkout failed' }
97
+ } else {
98
+ if (Test-Path $RUNTIME_DIR) { Remove-Item -Recurse -Force $RUNTIME_DIR }
99
+ & git clone --depth 1 --branch $REPO_REF $REPO_URL $RUNTIME_DIR
100
+ if ($LASTEXITCODE -ne 0) { throw 'git clone failed' }
101
+ }
102
+
103
+ $commit = & git -C $RUNTIME_DIR rev-parse --short HEAD 2>$null
104
+ if (-not $commit) { $commit = 'unknown' }
105
+ Print-Message 'success' "✓ Runtime ready ($commit)"
106
+ }
107
+
108
+ function Install-PythonRuntime {
109
+ param([string]$PyBin)
110
+
111
+ if (-not (Test-Path $INSTALL_ROOT)) {
112
+ New-Item -ItemType Directory -Path $INSTALL_ROOT -Force | Out-Null
113
+ }
114
+
115
+ $venvPython = Join-Path $VENV_DIR 'Scripts\python.exe'
116
+ if ($Force -or -not (Test-Path $venvPython)) {
117
+ Print-Message 'info' 'Creating virtual environment...'
118
+ if (Test-Path $VENV_DIR) { Remove-Item -Recurse -Force $VENV_DIR }
119
+ Invoke-Python $PyBin @('-m', 'venv', $VENV_DIR)
120
+ }
121
+
122
+ Print-Message 'info' 'Installing Esprit dependencies (this can take a few minutes)...'
123
+ $venvPip = Join-Path $VENV_DIR 'Scripts\pip.exe'
124
+ & $venvPython -m pip install --upgrade pip setuptools wheel --quiet
125
+ if ($LASTEXITCODE -ne 0) { throw 'pip upgrade failed' }
126
+ & $venvPip install --upgrade $RUNTIME_DIR --quiet
127
+ if ($LASTEXITCODE -ne 0) { throw 'pip install failed' }
128
+ Print-Message 'success' '✓ Python runtime installed'
129
+ }
130
+
131
+ function Write-Launcher {
132
+ if (-not (Test-Path $BIN_DIR)) {
133
+ New-Item -ItemType Directory -Path $BIN_DIR -Force | Out-Null
134
+ }
135
+
136
+ $launcherContent = @"
137
+ @echo off
138
+ setlocal
139
+ set "ROOT=%ESPRIT_HOME%"
140
+ if "%ROOT%"=="" set "ROOT=%USERPROFILE%\.esprit"
141
+ set "BIN=%ROOT%\venv\Scripts\esprit.exe"
142
+ if not exist "%BIN%" (
143
+ echo Esprit runtime not found. Re-run the installer.
144
+ exit /b 1
145
+ )
146
+ "%BIN%" %*
147
+ "@
148
+
149
+ Set-Content -Path $LAUNCHER_CMD -Value $launcherContent -Encoding ASCII
150
+ Print-Message 'success' "✓ Installed launcher at $LAUNCHER_CMD"
151
+ }
152
+
153
+ function Setup-Path {
154
+ $currentPath = [Environment]::GetEnvironmentVariable('Path', 'User')
155
+ if ($currentPath -and ($currentPath -split ';' | ForEach-Object { $_.TrimEnd('\') }) -contains $BIN_DIR.TrimEnd('\')) {
156
+ return
157
+ }
158
+
159
+ $newPath = "$BIN_DIR;$currentPath"
160
+ [Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
161
+ # Also update the current session
162
+ $env:Path = "$BIN_DIR;$env:Path"
163
+ Print-Message 'info' "Added $BIN_DIR to user PATH"
164
+ }
165
+
166
+ function Warm-DockerImage {
167
+ if ($env:ESPRIT_SKIP_DOCKER_WARM -eq '1') { return }
168
+
169
+ if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
170
+ Print-Message 'warning' 'Docker not found (required for local/provider scans).'
171
+ Print-Message 'info' 'Esprit Cloud scans still work without Docker.'
172
+ return
173
+ }
174
+
175
+ $null = & docker info 2>&1
176
+ if ($LASTEXITCODE -ne 0) {
177
+ Print-Message 'warning' 'Docker daemon is not running.'
178
+ Print-Message 'info' 'Start Docker for local/provider scans.'
179
+ return
180
+ }
181
+
182
+ Print-Message 'info' 'Pulling sandbox image (optional warm-up)...'
183
+ $pullOutput = & docker pull $ESPRIT_IMAGE 2>&1 | Out-String
184
+ if ($LASTEXITCODE -eq 0) {
185
+ Print-Message 'success' '✓ Sandbox image ready'
186
+ return
187
+ }
188
+
189
+ Write-Host $pullOutput
190
+ Print-Message 'warning' 'Sandbox pull skipped (will retry at first local scan).'
191
+ }
192
+
193
+ # ── Main ──────────────────────────────────────────────────────────────────────
194
+
195
+ try {
196
+ Require-Command 'git' 'Install git and re-run the installer.'
197
+
198
+ $pyBin = Choose-Python
199
+ if (-not $pyBin) {
200
+ Print-Message 'error' 'Python 3.12+ is required.'
201
+ Print-Message 'info' 'Install Python 3.12 and re-run this installer.'
202
+ exit 1
203
+ }
204
+
205
+ Print-Message 'info' "Installing Esprit (source mode)"
206
+ Print-Message 'info' "Runtime source: $REPO_URL@$REPO_REF"
207
+ Print-Message 'info' "Install root: $INSTALL_ROOT"
208
+
209
+ Sync-RuntimeRepo
210
+ Install-PythonRuntime -PyBin $pyBin
211
+ Write-Launcher
212
+ Setup-Path
213
+ Warm-DockerImage
214
+
215
+ $version = 'unknown'
216
+ try {
217
+ $version = & $LAUNCHER_CMD --version 2>$null
218
+ if (-not $version) { $version = 'unknown' }
219
+ } catch { }
220
+ Print-Message 'success' "✓ $version ready"
221
+
222
+ Write-Host ''
223
+ Print-Message 'info' 'You may need to restart your terminal for PATH changes to take effect.'
224
+ Print-Message 'info' " $APP --help"
225
+ } catch {
226
+ Print-Message 'error' "Installation failed: $_"
227
+ exit 1
228
+ }
@@ -48,6 +48,35 @@ require_command() {
48
48
  fi
49
49
  }
50
50
 
51
+ verify_signature() {
52
+ if ! command -v gpg &> /dev/null; then
53
+ echo "⚠ gpg not found — skipping signature verification"
54
+ return 0
55
+ fi
56
+
57
+ # Import the Esprit public key
58
+ local key_url="https://raw.githubusercontent.com/improdead/Esprit/main/keys/esprit-release.pub"
59
+ if curl -fsSL "$key_url" | gpg --import 2>/dev/null; then
60
+ echo "✓ Esprit release key imported"
61
+ else
62
+ echo "⚠ Could not import release key — skipping verification"
63
+ return 0
64
+ fi
65
+
66
+ # Verify if a signature file exists
67
+ if [ -f "$1.sig" ]; then
68
+ if gpg --verify "$1.sig" "$1" 2>/dev/null; then
69
+ echo "✓ Signature verified"
70
+ else
71
+ echo "⚠ Signature verification failed"
72
+ echo " The download may have been tampered with."
73
+ echo " Continue anyway? (y/N)"
74
+ read -r response
75
+ [ "$response" = "y" ] || exit 1
76
+ fi
77
+ fi
78
+ }
79
+
51
80
  choose_python() {
52
81
  local candidate
53
82
  for candidate in python3.13 python3.12 python3; do
@@ -235,6 +264,7 @@ main() {
235
264
  print_message info "${MUTED}Install root:${NC} $INSTALL_ROOT"
236
265
 
237
266
  sync_runtime_repo
267
+ verify_signature "$RUNTIME_DIR"
238
268
  install_python_runtime "$py_bin"
239
269
  write_launcher
240
270
  setup_path