esprit-cli 0.7.0 → 0.7.2
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 +28 -2
- package/bin/esprit.js +13 -3
- package/npm/postinstall.mjs +14 -113
- package/package.json +3 -2
- package/scripts/build.sh +98 -0
- package/scripts/install.sh +253 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Esprit is an autonomous security assessment tool that uses AI agents to perform
|
|
|
11
11
|
### Option 1: Install with curl
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
curl -fsSL https://
|
|
14
|
+
curl -fsSL https://raw.githubusercontent.com/improdead/Esprit/main/scripts/install.sh | bash
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
### Option 2: Homebrew
|
|
@@ -24,7 +24,7 @@ brew install esprit
|
|
|
24
24
|
### Option 3: npm
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
npm install -g
|
|
27
|
+
npm install -g github:improdead/Esprit
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
### Option 4: From Source
|
|
@@ -38,6 +38,26 @@ poetry install
|
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
+
## First-Run Onboarding
|
|
42
|
+
|
|
43
|
+
On first use, Esprit opens a one-time onboarding wizard before any Docker checks.
|
|
44
|
+
|
|
45
|
+
- Ghost-themed first-run setup flow
|
|
46
|
+
- Guided setup for theme, providers, and preferred model
|
|
47
|
+
- Skippable with reminder until completed
|
|
48
|
+
|
|
49
|
+
After onboarding, run `esprit` to open the interactive launchpad UI.
|
|
50
|
+
|
|
51
|
+
- Guided setup for target and scan mode
|
|
52
|
+
- Unified theme across onboarding and scanning TUI
|
|
53
|
+
- Local directory scan input with tab autocomplete
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
esprit
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
41
61
|
## Choose Your Setup
|
|
42
62
|
|
|
43
63
|
Esprit supports two runtime modes depending on how you want to run scans.
|
|
@@ -153,6 +173,12 @@ esprit provider status # Check all connected providers
|
|
|
153
173
|
esprit provider logout <provider> # Disconnect a provider
|
|
154
174
|
```
|
|
155
175
|
|
|
176
|
+
OpenCode notes:
|
|
177
|
+
|
|
178
|
+
- Public OpenCode models can be used without credentials when available on your machine.
|
|
179
|
+
- If a public OpenCode model hits rate limits or upstream instability, Esprit auto-falls back to another healthy public model.
|
|
180
|
+
- Recommended public models: `opencode/minimax-m2.5-free`, `opencode/kimi-k2.5-free`, `opencode/gpt-5-nano`.
|
|
181
|
+
|
|
156
182
|
---
|
|
157
183
|
|
|
158
184
|
## Configuration
|
package/bin/esprit.js
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
4
5
|
const path = require("node:path");
|
|
5
6
|
const { spawnSync } = require("node:child_process");
|
|
6
7
|
|
|
7
8
|
const isWindows = process.platform === "win32";
|
|
8
9
|
const binaryName = isWindows ? "esprit.exe" : "esprit";
|
|
9
|
-
const installDir = path.
|
|
10
|
+
const installDir = path.join(os.homedir(), ".esprit", "bin");
|
|
10
11
|
const binaryPath = path.join(installDir, binaryName);
|
|
11
|
-
const installerPath = path.resolve(__dirname, "..", "
|
|
12
|
+
const installerPath = path.resolve(__dirname, "..", "scripts", "install.sh");
|
|
12
13
|
|
|
13
14
|
function ensureInstalled() {
|
|
14
15
|
if (fs.existsSync(binaryPath)) {
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
if (isWindows) {
|
|
20
|
+
console.error("[esprit] installer currently supports macOS/Linux. Use WSL on Windows.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const bootstrap = spawnSync("bash", [installerPath], {
|
|
19
25
|
stdio: "inherit",
|
|
26
|
+
env: {
|
|
27
|
+
...process.env,
|
|
28
|
+
ESPRIT_SKIP_DOCKER_WARM: process.env.ESPRIT_SKIP_DOCKER_WARM || "1",
|
|
29
|
+
},
|
|
20
30
|
});
|
|
21
31
|
if (bootstrap.status !== 0) {
|
|
22
32
|
process.exit(bootstrap.status || 1);
|
package/npm/postinstall.mjs
CHANGED
|
@@ -1,126 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import os from "node:os";
|
|
6
4
|
import { spawnSync } from "node:child_process";
|
|
7
5
|
import { fileURLToPath } from "node:url";
|
|
8
6
|
|
|
9
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
8
|
const __dirname = path.dirname(__filename);
|
|
11
|
-
const
|
|
12
|
-
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
13
|
-
const installDir = path.join(projectRoot, "npm", ".esprit-bin");
|
|
14
|
-
const repo = "improdead/Esprit";
|
|
9
|
+
const installerPath = path.resolve(__dirname, "..", "scripts", "install.sh");
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (platform === "darwin") {
|
|
21
|
-
platform = "macos";
|
|
22
|
-
} else if (platform === "linux") {
|
|
23
|
-
platform = "linux";
|
|
24
|
-
} else if (platform === "win32") {
|
|
25
|
-
platform = "windows";
|
|
26
|
-
} else {
|
|
27
|
-
throw new Error(`Unsupported platform: ${platform}`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (arch === "arm64") {
|
|
31
|
-
arch = "arm64";
|
|
32
|
-
} else if (arch === "x64") {
|
|
33
|
-
arch = "x86_64";
|
|
34
|
-
} else {
|
|
35
|
-
throw new Error(`Unsupported architecture: ${arch}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (platform === "windows" && arch !== "x86_64") {
|
|
39
|
-
throw new Error(`Unsupported platform/arch: ${platform}/${arch}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return { platform, arch, target: `${platform}-${arch}` };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function buildAssetName(version, target) {
|
|
46
|
-
const ext = target.startsWith("windows-") ? ".zip" : ".tar.gz";
|
|
47
|
-
return `esprit-${version}-${target}${ext}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function downloadFile(url, destPath) {
|
|
51
|
-
const response = await fetch(url);
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
throw new Error(`Download failed (${response.status}) ${url}`);
|
|
54
|
-
}
|
|
55
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
56
|
-
await fs.writeFile(destPath, Buffer.from(arrayBuffer));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function ensureDir(dir) {
|
|
60
|
-
await fs.mkdir(dir, { recursive: true });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function runCommand(command, args) {
|
|
64
|
-
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
65
|
-
if (result.status !== 0) {
|
|
66
|
-
throw new Error(`Command failed: ${command} ${args.join(" ")}`);
|
|
67
|
-
}
|
|
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);
|
|
68
14
|
}
|
|
69
15
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} else {
|
|
79
|
-
runCommand("unzip", ["-o", archivePath, "-d", extractDir]);
|
|
80
|
-
}
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
runCommand("tar", ["-xzf", archivePath, "-C", extractDir]);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function installBinary() {
|
|
88
|
-
const pkg = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
89
|
-
const version = process.env.ESPRIT_VERSION || pkg.version;
|
|
90
|
-
const { target } = getTarget();
|
|
91
|
-
const assetName = buildAssetName(version, target);
|
|
92
|
-
const downloadUrl = `https://github.com/${repo}/releases/download/v${version}/${assetName}`;
|
|
93
|
-
|
|
94
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "esprit-npm-"));
|
|
95
|
-
const archivePath = path.join(tempRoot, assetName);
|
|
96
|
-
const extractDir = path.join(tempRoot, "extract");
|
|
97
|
-
await ensureDir(extractDir);
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
process.stdout.write(`[esprit] downloading ${assetName}\n`);
|
|
101
|
-
await downloadFile(downloadUrl, archivePath);
|
|
102
|
-
extractArchive(archivePath, extractDir);
|
|
103
|
-
|
|
104
|
-
const binaryName = target.startsWith("windows-") ? "esprit.exe" : "esprit";
|
|
105
|
-
const extractedPath = path.join(extractDir, binaryName);
|
|
106
|
-
await fs.access(extractedPath);
|
|
107
|
-
|
|
108
|
-
await ensureDir(installDir);
|
|
109
|
-
const outputPath = path.join(installDir, binaryName);
|
|
110
|
-
await fs.copyFile(extractedPath, outputPath);
|
|
111
|
-
|
|
112
|
-
if (!target.startsWith("windows-")) {
|
|
113
|
-
await fs.chmod(outputPath, 0o755);
|
|
114
|
-
}
|
|
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
|
+
});
|
|
115
24
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} finally {
|
|
119
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
120
|
-
}
|
|
25
|
+
if (result.status !== 0) {
|
|
26
|
+
process.exit(result.status || 1);
|
|
121
27
|
}
|
|
122
|
-
|
|
123
|
-
installBinary().catch((error) => {
|
|
124
|
-
process.stderr.write(`[esprit] install failed: ${error.message}\n`);
|
|
125
|
-
process.exit(1);
|
|
126
|
-
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "esprit-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "AI-powered penetration testing agent",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://esprit.dev",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"bin",
|
|
19
19
|
"npm",
|
|
20
|
+
"scripts",
|
|
20
21
|
"README.md",
|
|
21
22
|
"LICENSE"
|
|
22
23
|
],
|
|
@@ -26,4 +27,4 @@
|
|
|
26
27
|
"engines": {
|
|
27
28
|
"node": ">=18"
|
|
28
29
|
}
|
|
29
|
-
}
|
|
30
|
+
}
|
package/scripts/build.sh
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
6
|
+
|
|
7
|
+
RED='\033[0;31m'
|
|
8
|
+
GREEN='\033[0;32m'
|
|
9
|
+
YELLOW='\033[1;33m'
|
|
10
|
+
BLUE='\033[0;34m'
|
|
11
|
+
NC='\033[0m' # No Color
|
|
12
|
+
|
|
13
|
+
echo -e "${BLUE}Esprit Build Script${NC}"
|
|
14
|
+
echo "================================"
|
|
15
|
+
|
|
16
|
+
OS="$(uname -s)"
|
|
17
|
+
ARCH="$(uname -m)"
|
|
18
|
+
|
|
19
|
+
case "$OS" in
|
|
20
|
+
Linux*) OS_NAME="linux";;
|
|
21
|
+
Darwin*) OS_NAME="macos";;
|
|
22
|
+
MINGW*|MSYS*|CYGWIN*) OS_NAME="windows";;
|
|
23
|
+
*) OS_NAME="unknown";;
|
|
24
|
+
esac
|
|
25
|
+
|
|
26
|
+
case "$ARCH" in
|
|
27
|
+
x86_64|amd64) ARCH_NAME="x86_64";;
|
|
28
|
+
arm64|aarch64) ARCH_NAME="arm64";;
|
|
29
|
+
*) ARCH_NAME="$ARCH";;
|
|
30
|
+
esac
|
|
31
|
+
|
|
32
|
+
echo -e "${YELLOW}Platform:${NC} $OS_NAME-$ARCH_NAME"
|
|
33
|
+
|
|
34
|
+
cd "$PROJECT_ROOT"
|
|
35
|
+
|
|
36
|
+
if ! command -v poetry &> /dev/null; then
|
|
37
|
+
echo -e "${RED}Error: Poetry is not installed${NC}"
|
|
38
|
+
echo "Please install Poetry first: https://python-poetry.org/docs/#installation"
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo -e "\n${BLUE}Installing dependencies...${NC}"
|
|
43
|
+
poetry install --with dev
|
|
44
|
+
|
|
45
|
+
VERSION=$(poetry version -s)
|
|
46
|
+
echo -e "${YELLOW}Version:${NC} $VERSION"
|
|
47
|
+
|
|
48
|
+
echo -e "\n${BLUE}Cleaning previous builds...${NC}"
|
|
49
|
+
rm -rf build/ dist/
|
|
50
|
+
|
|
51
|
+
echo -e "\n${BLUE}Building binary with PyInstaller...${NC}"
|
|
52
|
+
poetry run pyinstaller esprit.spec --noconfirm
|
|
53
|
+
|
|
54
|
+
RELEASE_DIR="dist/release"
|
|
55
|
+
mkdir -p "$RELEASE_DIR"
|
|
56
|
+
|
|
57
|
+
BINARY_NAME="esprit-${VERSION}-${OS_NAME}-${ARCH_NAME}"
|
|
58
|
+
|
|
59
|
+
if [ "$OS_NAME" = "windows" ]; then
|
|
60
|
+
if [ ! -f "dist/esprit.exe" ]; then
|
|
61
|
+
echo -e "${RED}Build failed: Binary not found${NC}"
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
BINARY_NAME="${BINARY_NAME}.exe"
|
|
65
|
+
cp "dist/esprit.exe" "$RELEASE_DIR/$BINARY_NAME"
|
|
66
|
+
echo -e "\n${BLUE}Creating zip...${NC}"
|
|
67
|
+
ARCHIVE_NAME="${BINARY_NAME%.exe}.zip"
|
|
68
|
+
|
|
69
|
+
if command -v 7z &> /dev/null; then
|
|
70
|
+
7z a "$RELEASE_DIR/$ARCHIVE_NAME" "$RELEASE_DIR/$BINARY_NAME"
|
|
71
|
+
else
|
|
72
|
+
powershell -Command "Compress-Archive -Path '$RELEASE_DIR/$BINARY_NAME' -DestinationPath '$RELEASE_DIR/$ARCHIVE_NAME'"
|
|
73
|
+
fi
|
|
74
|
+
echo -e "${GREEN}Created:${NC} $RELEASE_DIR/$ARCHIVE_NAME"
|
|
75
|
+
else
|
|
76
|
+
if [ ! -f "dist/esprit" ]; then
|
|
77
|
+
echo -e "${RED}Build failed: Binary not found${NC}"
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
cp "dist/esprit" "$RELEASE_DIR/$BINARY_NAME"
|
|
81
|
+
chmod +x "$RELEASE_DIR/$BINARY_NAME"
|
|
82
|
+
echo -e "\n${BLUE}Creating tarball...${NC}"
|
|
83
|
+
ARCHIVE_NAME="${BINARY_NAME}.tar.gz"
|
|
84
|
+
tar -czvf "$RELEASE_DIR/$ARCHIVE_NAME" -C "$RELEASE_DIR" "$BINARY_NAME"
|
|
85
|
+
echo -e "${GREEN}Created:${NC} $RELEASE_DIR/$ARCHIVE_NAME"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
echo -e "\n${GREEN}Build successful!${NC}"
|
|
89
|
+
echo "================================"
|
|
90
|
+
echo -e "${YELLOW}Binary:${NC} $RELEASE_DIR/$BINARY_NAME"
|
|
91
|
+
|
|
92
|
+
SIZE=$(ls -lh "$RELEASE_DIR/$BINARY_NAME" | awk '{print $5}')
|
|
93
|
+
echo -e "${YELLOW}Size:${NC} $SIZE"
|
|
94
|
+
|
|
95
|
+
echo -e "\n${BLUE}Testing binary...${NC}"
|
|
96
|
+
"$RELEASE_DIR/$BINARY_NAME" --help > /dev/null 2>&1 && echo -e "${GREEN}Binary test passed!${NC}" || echo -e "${RED}Binary test failed${NC}"
|
|
97
|
+
|
|
98
|
+
echo -e "\n${GREEN}Done!${NC}"
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
APP="esprit"
|
|
6
|
+
REPO_URL="${ESPRIT_REPO_URL:-https://github.com/improdead/Esprit.git}"
|
|
7
|
+
REPO_REF="${ESPRIT_REPO_REF:-main}"
|
|
8
|
+
INSTALL_ROOT="${ESPRIT_HOME:-$HOME/.esprit}"
|
|
9
|
+
BIN_DIR="$INSTALL_ROOT/bin"
|
|
10
|
+
RUNTIME_DIR="$INSTALL_ROOT/runtime"
|
|
11
|
+
VENV_DIR="$INSTALL_ROOT/venv"
|
|
12
|
+
LAUNCHER_PATH="$BIN_DIR/$APP"
|
|
13
|
+
ESPRIT_IMAGE="${ESPRIT_IMAGE:-improdead/esprit-sandbox:latest}"
|
|
14
|
+
|
|
15
|
+
MUTED='\033[0;2m'
|
|
16
|
+
RED='\033[0;31m'
|
|
17
|
+
GREEN='\033[0;32m'
|
|
18
|
+
YELLOW='\033[1;33m'
|
|
19
|
+
CYAN='\033[0;36m'
|
|
20
|
+
NC='\033[0m'
|
|
21
|
+
|
|
22
|
+
FORCE=false
|
|
23
|
+
for arg in "$@"; do
|
|
24
|
+
case "$arg" in
|
|
25
|
+
--force|-f) FORCE=true ;;
|
|
26
|
+
esac
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
print_message() {
|
|
30
|
+
local level="$1"
|
|
31
|
+
local message="$2"
|
|
32
|
+
local color="$NC"
|
|
33
|
+
case "$level" in
|
|
34
|
+
success) color="$GREEN" ;;
|
|
35
|
+
warning) color="$YELLOW" ;;
|
|
36
|
+
error) color="$RED" ;;
|
|
37
|
+
esac
|
|
38
|
+
echo -e "${color}${message}${NC}"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
require_command() {
|
|
42
|
+
local cmd="$1"
|
|
43
|
+
local install_hint="$2"
|
|
44
|
+
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
45
|
+
print_message error "Missing required command: $cmd"
|
|
46
|
+
print_message info "$install_hint"
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
choose_python() {
|
|
52
|
+
local candidate
|
|
53
|
+
for candidate in python3.13 python3.12 python3; do
|
|
54
|
+
if ! command -v "$candidate" >/dev/null 2>&1; then
|
|
55
|
+
continue
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if "$candidate" - <<'PY' >/dev/null 2>&1
|
|
59
|
+
import sys
|
|
60
|
+
raise SystemExit(0 if sys.version_info >= (3, 12) else 1)
|
|
61
|
+
PY
|
|
62
|
+
then
|
|
63
|
+
echo "$candidate"
|
|
64
|
+
return 0
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
sync_runtime_repo() {
|
|
72
|
+
print_message info "${MUTED}Syncing Esprit runtime source...${NC}"
|
|
73
|
+
|
|
74
|
+
if [ -d "$RUNTIME_DIR/.git" ]; then
|
|
75
|
+
git -C "$RUNTIME_DIR" remote set-url origin "$REPO_URL"
|
|
76
|
+
git -C "$RUNTIME_DIR" fetch --depth 1 origin "$REPO_REF"
|
|
77
|
+
git -C "$RUNTIME_DIR" checkout -q FETCH_HEAD
|
|
78
|
+
else
|
|
79
|
+
rm -rf "$RUNTIME_DIR"
|
|
80
|
+
git clone --depth 1 --branch "$REPO_REF" "$REPO_URL" "$RUNTIME_DIR"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
local runtime_commit
|
|
84
|
+
runtime_commit=$(git -C "$RUNTIME_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
85
|
+
print_message success "✓ Runtime ready (${runtime_commit})"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
install_python_runtime() {
|
|
89
|
+
local py_bin="$1"
|
|
90
|
+
|
|
91
|
+
mkdir -p "$INSTALL_ROOT"
|
|
92
|
+
if [ "$FORCE" = true ] || [ ! -x "$VENV_DIR/bin/python" ]; then
|
|
93
|
+
print_message info "${MUTED}Creating virtual environment...${NC}"
|
|
94
|
+
rm -rf "$VENV_DIR"
|
|
95
|
+
"$py_bin" -m venv "$VENV_DIR"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
print_message info "${MUTED}Installing Esprit dependencies (this can take a few minutes)...${NC}"
|
|
99
|
+
"$VENV_DIR/bin/python" -m pip install --upgrade pip setuptools wheel >/dev/null
|
|
100
|
+
"$VENV_DIR/bin/pip" install --upgrade "$RUNTIME_DIR" >/dev/null
|
|
101
|
+
print_message success "✓ Python runtime installed"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
write_launcher() {
|
|
105
|
+
mkdir -p "$BIN_DIR"
|
|
106
|
+
|
|
107
|
+
cat > "$LAUNCHER_PATH" <<'EOF'
|
|
108
|
+
#!/usr/bin/env bash
|
|
109
|
+
set -euo pipefail
|
|
110
|
+
|
|
111
|
+
ROOT="${ESPRIT_HOME:-$HOME/.esprit}"
|
|
112
|
+
BIN="$ROOT/venv/bin/esprit"
|
|
113
|
+
|
|
114
|
+
if [ ! -x "$BIN" ]; then
|
|
115
|
+
echo "Esprit runtime not found. Re-run the installer."
|
|
116
|
+
exit 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
exec "$BIN" "$@"
|
|
120
|
+
EOF
|
|
121
|
+
|
|
122
|
+
chmod 755 "$LAUNCHER_PATH"
|
|
123
|
+
print_message success "✓ Installed launcher at $LAUNCHER_PATH"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setup_path() {
|
|
127
|
+
local shell_name
|
|
128
|
+
shell_name=$(basename "${SHELL:-sh}")
|
|
129
|
+
|
|
130
|
+
local config_file=""
|
|
131
|
+
case "$shell_name" in
|
|
132
|
+
zsh)
|
|
133
|
+
config_file="${ZDOTDIR:-$HOME}/.zshrc"
|
|
134
|
+
;;
|
|
135
|
+
bash)
|
|
136
|
+
config_file="$HOME/.bashrc"
|
|
137
|
+
;;
|
|
138
|
+
fish)
|
|
139
|
+
config_file="$HOME/.config/fish/config.fish"
|
|
140
|
+
;;
|
|
141
|
+
*)
|
|
142
|
+
config_file="$HOME/.profile"
|
|
143
|
+
;;
|
|
144
|
+
esac
|
|
145
|
+
|
|
146
|
+
if [ "$shell_name" = "fish" ]; then
|
|
147
|
+
local line="fish_add_path $BIN_DIR"
|
|
148
|
+
if [ -f "$config_file" ] && grep -Fxq "$line" "$config_file" 2>/dev/null; then
|
|
149
|
+
return
|
|
150
|
+
fi
|
|
151
|
+
if [ ! -f "$config_file" ]; then
|
|
152
|
+
mkdir -p "$(dirname "$config_file")"
|
|
153
|
+
touch "$config_file"
|
|
154
|
+
fi
|
|
155
|
+
echo "" >> "$config_file"
|
|
156
|
+
echo "# esprit" >> "$config_file"
|
|
157
|
+
echo "$line" >> "$config_file"
|
|
158
|
+
print_message info "${MUTED}Added esprit to PATH in ${NC}$config_file"
|
|
159
|
+
return
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
local export_line="export PATH=$BIN_DIR:\$PATH"
|
|
163
|
+
if [ -f "$config_file" ] && grep -Fxq "$export_line" "$config_file" 2>/dev/null; then
|
|
164
|
+
return
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
if [ ! -f "$config_file" ]; then
|
|
168
|
+
touch "$config_file"
|
|
169
|
+
fi
|
|
170
|
+
if [ -w "$config_file" ]; then
|
|
171
|
+
echo "" >> "$config_file"
|
|
172
|
+
echo "# esprit" >> "$config_file"
|
|
173
|
+
echo "$export_line" >> "$config_file"
|
|
174
|
+
print_message info "${MUTED}Added esprit to PATH in ${NC}$config_file"
|
|
175
|
+
else
|
|
176
|
+
print_message warning "Could not update $config_file automatically."
|
|
177
|
+
print_message info "Add this line manually: $export_line"
|
|
178
|
+
fi
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
warm_docker_image() {
|
|
182
|
+
if [ "${ESPRIT_SKIP_DOCKER_WARM:-0}" = "1" ]; then
|
|
183
|
+
return
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
187
|
+
print_message warning "Docker not found (required for local/provider scans)."
|
|
188
|
+
print_message info "Esprit Cloud scans still work without Docker."
|
|
189
|
+
return
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
if ! docker info >/dev/null 2>&1; then
|
|
193
|
+
print_message warning "Docker daemon is not running."
|
|
194
|
+
print_message info "Start Docker for local/provider scans."
|
|
195
|
+
return
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
print_message info "${MUTED}Pulling sandbox image (optional warm-up)...${NC}"
|
|
199
|
+
local pull_output
|
|
200
|
+
local pull_status=0
|
|
201
|
+
pull_output="$(docker pull "$ESPRIT_IMAGE" 2>&1)" || pull_status=$?
|
|
202
|
+
|
|
203
|
+
if [ "$pull_status" -eq 0 ]; then
|
|
204
|
+
print_message success "✓ Sandbox image ready"
|
|
205
|
+
return
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
echo -e "$pull_output"
|
|
209
|
+
|
|
210
|
+
if [[ "$(uname -m)" == "arm64" ]] && echo "$pull_output" | grep -qi "no matching manifest" && echo "$pull_output" | grep -qi "arm64"; then
|
|
211
|
+
print_message warning "Native arm64 image missing; retrying with linux/amd64 emulation..."
|
|
212
|
+
if docker pull --platform linux/amd64 "$ESPRIT_IMAGE" >/dev/null; then
|
|
213
|
+
print_message success "✓ Sandbox image ready (linux/amd64 emulation)"
|
|
214
|
+
return
|
|
215
|
+
fi
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
print_message warning "Sandbox pull skipped (will retry at first local scan)."
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
main() {
|
|
222
|
+
require_command git "Install git and re-run the installer."
|
|
223
|
+
require_command curl "Install curl and re-run the installer."
|
|
224
|
+
|
|
225
|
+
local py_bin
|
|
226
|
+
py_bin=$(choose_python || true)
|
|
227
|
+
if [ -z "$py_bin" ]; then
|
|
228
|
+
print_message error "Python 3.12+ is required."
|
|
229
|
+
print_message info "Install Python 3.12 and re-run this installer."
|
|
230
|
+
exit 1
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
print_message info "${CYAN}Installing Esprit${NC} ${MUTED}(source mode)${NC}"
|
|
234
|
+
print_message info "${MUTED}Runtime source:${NC} $REPO_URL@$REPO_REF"
|
|
235
|
+
print_message info "${MUTED}Install root:${NC} $INSTALL_ROOT"
|
|
236
|
+
|
|
237
|
+
sync_runtime_repo
|
|
238
|
+
install_python_runtime "$py_bin"
|
|
239
|
+
write_launcher
|
|
240
|
+
setup_path
|
|
241
|
+
warm_docker_image
|
|
242
|
+
|
|
243
|
+
local version
|
|
244
|
+
version=$("$LAUNCHER_PATH" --version 2>/dev/null || echo "unknown")
|
|
245
|
+
print_message success "✓ ${version} ready"
|
|
246
|
+
|
|
247
|
+
echo ""
|
|
248
|
+
echo -e "${MUTED}Run this now (or open a new terminal):${NC}"
|
|
249
|
+
echo -e " ${MUTED}export PATH=$BIN_DIR:\$PATH${NC}"
|
|
250
|
+
echo -e " ${MUTED}$APP --help${NC}"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
main
|