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 +9 -3
- package/bin/esprit.js +21 -12
- package/npm/postinstall.mjs +22 -13
- package/package.json +1 -1
- package/scripts/install.ps1 +228 -0
- package/scripts/install.sh +30 -0
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
|
|
27
|
+
npm install -g esprit-cli@latest
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
### Option 4:
|
|
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
|
|
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(
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
process.
|
|
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
|
}
|
package/npm/postinstall.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
12
|
-
process.
|
|
13
|
-
|
|
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 =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
@@ -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
|
+
}
|
package/scripts/install.sh
CHANGED
|
@@ -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
|