seshions 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 seshions contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Seshions
2
+
3
+ Terminal session orchestrator for running multiple coding agents in parallel.
4
+
5
+ ## What It Does
6
+
7
+ - Launch and track multiple AI coding sessions in one dashboard
8
+ - Attach/detach quickly with keyboard-first controls
9
+ - Group sessions by workflow
10
+ - Optional git worktree isolation per session
11
+ - Persist session state across restarts via tmux
12
+
13
+ ## Requirements
14
+
15
+ - Node.js 18+
16
+ - tmux
17
+ - At least one coding tool installed (`claude`, `codex`, `gemini`, `opencode`, or custom shell command)
18
+
19
+ ## Install (One Command)
20
+
21
+ Run immediately (no global install):
22
+
23
+ ```bash
24
+ npx seshions@latest
25
+ ```
26
+
27
+ Install globally:
28
+
29
+ ```bash
30
+ npm install -g seshions
31
+ seshions
32
+ ```
33
+
34
+ The npm launcher downloads the matching native runtime automatically and caches it in `~/.seshions/runtime`.
35
+
36
+ ## Local Development
37
+
38
+ ```bash
39
+ bun install
40
+ bun run build
41
+ bun run typecheck
42
+ bun test
43
+ ```
44
+
45
+ ## Run
46
+
47
+ ```bash
48
+ bun run dist/index.js
49
+ ```
50
+
51
+ ## Build Binary
52
+
53
+ ```bash
54
+ bun run compile
55
+ ```
56
+
57
+ ## Install Script
58
+
59
+ Alternative manual installer:
60
+
61
+ ```bash
62
+ export SESHIONS_REPO="danhergir/seshions"
63
+ curl -fsSL "https://raw.githubusercontent.com/${SESHIONS_REPO}/main/install.sh" | bash
64
+ ```
65
+
66
+ ## Uninstall
67
+
68
+ If installed with npm:
69
+
70
+ ```bash
71
+ npm uninstall -g seshions
72
+ ```
73
+
74
+ Remove cached runtime + state data:
75
+
76
+ ```bash
77
+ rm -rf ~/.seshions
78
+ ```
79
+
80
+ If installed with the manual installer:
81
+
82
+ ```bash
83
+ export SESHIONS_REPO="danhergir/seshions"
84
+ curl -fsSL "https://raw.githubusercontent.com/${SESHIONS_REPO}/main/uninstall.sh" | bash
85
+ ```
86
+
87
+ Optional full cleanup (manual installer):
88
+
89
+ ```bash
90
+ curl -fsSL "https://raw.githubusercontent.com/${SESHIONS_REPO}/main/uninstall.sh" | bash -s -- --purge-data
91
+ ```
package/install.sh ADDED
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Seshions Installer
4
+ # Usage: SESHIONS_REPO=danhergir/seshions curl -fsSL "https://raw.githubusercontent.com/${SESHIONS_REPO}/main/install.sh" | bash
5
+ #
6
+
7
+ set -euo pipefail
8
+
9
+ APP=seshions
10
+ REPO="${SESHIONS_REPO:-danhergir/seshions}"
11
+
12
+ # Colors
13
+ MUTED='\033[0;2m'
14
+ RED='\033[0;31m'
15
+ GREEN='\033[0;32m'
16
+ BLUE='\033[0;34m'
17
+ NC='\033[0m'
18
+
19
+ INSTALL_DIR="${SESHIONS_INSTALL_DIR:-$HOME/.seshions/bin}"
20
+
21
+ usage() {
22
+ cat <<EOF
23
+ Seshions Installer
24
+
25
+ Usage: install.sh [options]
26
+
27
+ Options:
28
+ -h, --help Display this help message
29
+ -v, --version <version> Install a specific version (e.g., 1.0.0)
30
+ -b, --binary <path> Install from a local binary instead of downloading
31
+ --no-modify-path Don't modify shell config files
32
+
33
+ Examples:
34
+ SESHIONS_REPO=danhergir/seshions curl -fsSL "https://raw.githubusercontent.com/\$SESHIONS_REPO/main/install.sh" | bash
35
+ SESHIONS_REPO=danhergir/seshions curl -fsSL "https://raw.githubusercontent.com/\$SESHIONS_REPO/main/install.sh" | bash -s -- --version 1.0.0
36
+ ./install.sh --binary /path/to/seshions
37
+ EOF
38
+ }
39
+
40
+ requested_version=""
41
+ no_modify_path=false
42
+ binary_path=""
43
+
44
+ while [[ $# -gt 0 ]]; do
45
+ case "$1" in
46
+ -h|--help)
47
+ usage
48
+ exit 0
49
+ ;;
50
+ -v|--version)
51
+ if [[ -n "${2:-}" ]]; then
52
+ requested_version="$2"
53
+ shift 2
54
+ else
55
+ echo -e "${RED}Error: --version requires a version argument${NC}"
56
+ exit 1
57
+ fi
58
+ ;;
59
+ -b|--binary)
60
+ if [[ -n "${2:-}" ]]; then
61
+ binary_path="$2"
62
+ shift 2
63
+ else
64
+ echo -e "${RED}Error: --binary requires a path argument${NC}"
65
+ exit 1
66
+ fi
67
+ ;;
68
+ --no-modify-path)
69
+ no_modify_path=true
70
+ shift
71
+ ;;
72
+ *)
73
+ echo -e "${RED}Warning: Unknown option '$1'${NC}" >&2
74
+ shift
75
+ ;;
76
+ esac
77
+ done
78
+
79
+ mkdir -p "$INSTALL_DIR"
80
+
81
+ # Detect platform
82
+ detect_platform() {
83
+ local os arch
84
+
85
+ case "$(uname -s)" in
86
+ Darwin) os="darwin" ;;
87
+ Linux) os="linux" ;;
88
+ *) echo -e "${RED}Unsupported OS: $(uname -s)${NC}"; exit 1 ;;
89
+ esac
90
+
91
+ case "$(uname -m)" in
92
+ x86_64|amd64) arch="x64" ;;
93
+ arm64|aarch64) arch="arm64" ;;
94
+ *) echo -e "${RED}Unsupported architecture: $(uname -m)${NC}"; exit 1 ;;
95
+ esac
96
+
97
+ echo "${os}-${arch}"
98
+ }
99
+
100
+ # Check for tmux and offer to install
101
+ check_tmux() {
102
+ if command -v tmux &> /dev/null; then
103
+ return 0
104
+ fi
105
+
106
+ echo -e "${MUTED}tmux is not installed.${NC}"
107
+ echo "Seshions requires tmux to function."
108
+ echo ""
109
+
110
+ local os_type="$(uname -s)"
111
+
112
+ if [[ "$os_type" == "Darwin" ]]; then
113
+ if command -v brew &> /dev/null; then
114
+ read -p "Install tmux via Homebrew? [Y/n] " -n 1 -r
115
+ echo
116
+ if [[ ! $REPLY =~ ^[Nn]$ ]]; then
117
+ echo -e "Installing tmux..."
118
+ brew install tmux
119
+ fi
120
+ else
121
+ echo "Install tmux with: brew install tmux"
122
+ echo "(Install Homebrew first: https://brew.sh)"
123
+ fi
124
+ else
125
+ if command -v apt-get &> /dev/null; then
126
+ read -p "Install tmux via apt? [Y/n] " -n 1 -r
127
+ echo
128
+ if [[ ! $REPLY =~ ^[Nn]$ ]]; then
129
+ echo -e "Installing tmux..."
130
+ sudo apt-get update && sudo apt-get install -y tmux
131
+ fi
132
+ elif command -v dnf &> /dev/null; then
133
+ read -p "Install tmux via dnf? [Y/n] " -n 1 -r
134
+ echo
135
+ if [[ ! $REPLY =~ ^[Nn]$ ]]; then
136
+ echo -e "Installing tmux..."
137
+ sudo dnf install -y tmux
138
+ fi
139
+ elif command -v pacman &> /dev/null; then
140
+ read -p "Install tmux via pacman? [Y/n] " -n 1 -r
141
+ echo
142
+ if [[ ! $REPLY =~ ^[Nn]$ ]]; then
143
+ echo -e "Installing tmux..."
144
+ sudo pacman -S --noconfirm tmux
145
+ fi
146
+ else
147
+ echo "Please install tmux manually:"
148
+ echo " sudo apt install tmux # Debian/Ubuntu"
149
+ echo " sudo dnf install tmux # Fedora"
150
+ echo " sudo pacman -S tmux # Arch"
151
+ fi
152
+ fi
153
+
154
+ if ! command -v tmux &> /dev/null; then
155
+ echo ""
156
+ read -p "tmux not found. Continue anyway? [y/N] " -n 1 -r
157
+ echo
158
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
159
+ exit 1
160
+ fi
161
+ else
162
+ echo -e "${GREEN}tmux installed successfully!${NC}"
163
+ fi
164
+ }
165
+
166
+ check_tmux
167
+
168
+ if [ -n "$binary_path" ]; then
169
+ if [ ! -f "$binary_path" ]; then
170
+ echo -e "${RED}Error: Binary not found at ${binary_path}${NC}"
171
+ exit 1
172
+ fi
173
+ specific_version="local"
174
+ else
175
+ platform=$(detect_platform)
176
+ filename="$APP-$platform.tar.gz"
177
+
178
+ if [ -z "$requested_version" ]; then
179
+ url="https://github.com/$REPO/releases/latest/download/$filename"
180
+ specific_version=$(curl -sI "https://github.com/$REPO/releases/latest" | grep -i "location:" | sed -n 's/.*tag\/v\([^[:space:]]*\).*/\1/p' | tr -d '\r')
181
+
182
+ if [[ -z "$specific_version" ]]; then
183
+ # Fallback to API
184
+ specific_version=$(curl -s "https://api.github.com/repos/$REPO/releases/latest" | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
185
+ fi
186
+
187
+ if [[ -z "$specific_version" ]]; then
188
+ echo -e "${RED}Failed to fetch version information${NC}"
189
+ exit 1
190
+ fi
191
+ else
192
+ requested_version="${requested_version#v}"
193
+ url="https://github.com/$REPO/releases/download/v${requested_version}/$filename"
194
+ specific_version=$requested_version
195
+
196
+ # Verify release exists
197
+ http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/$REPO/releases/tag/v${requested_version}")
198
+ if [ "$http_status" = "404" ]; then
199
+ echo -e "${RED}Error: Release v${requested_version} not found${NC}"
200
+ echo -e "${MUTED}Available releases: https://github.com/$REPO/releases${NC}"
201
+ exit 1
202
+ fi
203
+ fi
204
+ fi
205
+
206
+ check_version() {
207
+ if command -v seshions >/dev/null 2>&1; then
208
+ installed_version=$(seshions --version 2>/dev/null || echo "")
209
+ if [[ "$installed_version" == "$specific_version" ]]; then
210
+ echo -e "${MUTED}Version ${NC}$specific_version${MUTED} already installed${NC}"
211
+ exit 0
212
+ else
213
+ echo -e "${MUTED}Installed version: ${NC}$installed_version"
214
+ fi
215
+ fi
216
+ }
217
+
218
+ download_and_install() {
219
+ echo -e "\n${MUTED}Installing ${NC}$APP ${MUTED}version: ${NC}$specific_version"
220
+ echo -e "${MUTED}Platform: ${NC}$platform"
221
+
222
+ local tmp_dir="${TMPDIR:-/tmp}/$APP-$$"
223
+ mkdir -p "$tmp_dir"
224
+
225
+ echo -e "${MUTED}Downloading...${NC}"
226
+ if ! curl -#fL -o "$tmp_dir/$filename" "$url"; then
227
+ echo -e "${RED}Download failed. The release may not have binaries for your platform.${NC}"
228
+ echo -e "${MUTED}You can install from source instead:${NC}"
229
+ echo -e " git clone https://github.com/$REPO.git"
230
+ echo -e " cd seshions && bun install && bun run build"
231
+ rm -rf "$tmp_dir"
232
+ exit 1
233
+ fi
234
+
235
+ # Extract tarball
236
+ tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
237
+
238
+ # Find the binary (could be in subdirectory)
239
+ local binary_path
240
+ if [ -f "$tmp_dir/$APP" ]; then
241
+ binary_path="$tmp_dir/$APP"
242
+ elif [ -f "$tmp_dir/$APP-$platform/$APP" ]; then
243
+ binary_path="$tmp_dir/$APP-$platform/$APP"
244
+ else
245
+ echo -e "${RED}Binary not found in archive${NC}"
246
+ rm -rf "$tmp_dir"
247
+ exit 1
248
+ fi
249
+
250
+ mv "$binary_path" "$INSTALL_DIR/$APP"
251
+ chmod 755 "$INSTALL_DIR/$APP"
252
+ rm -rf "$tmp_dir"
253
+
254
+ }
255
+
256
+ install_from_binary() {
257
+ echo -e "\n${MUTED}Installing ${NC}$APP ${MUTED}from: ${NC}$binary_path"
258
+ cp "$binary_path" "$INSTALL_DIR/$APP"
259
+ chmod 755 "$INSTALL_DIR/$APP"
260
+ }
261
+
262
+ if [ -n "$binary_path" ]; then
263
+ install_from_binary
264
+ else
265
+ check_version
266
+ download_and_install
267
+ fi
268
+
269
+ # Add to PATH
270
+ add_to_path() {
271
+ local config_file=$1
272
+ local command=$2
273
+
274
+ if grep -Fxq "$command" "$config_file" 2>/dev/null; then
275
+ return 0
276
+ elif [[ -w $config_file ]]; then
277
+ echo -e "\n# seshions" >> "$config_file"
278
+ echo "$command" >> "$config_file"
279
+ echo -e "${MUTED}Added to PATH in ${NC}$config_file"
280
+ fi
281
+ }
282
+
283
+ if [[ "$no_modify_path" != "true" ]]; then
284
+ if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
285
+ current_shell=$(basename "$SHELL")
286
+ case $current_shell in
287
+ fish)
288
+ config_file="$HOME/.config/fish/config.fish"
289
+ [[ -f "$config_file" ]] && add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
290
+ ;;
291
+ zsh)
292
+ config_file="${ZDOTDIR:-$HOME}/.zshrc"
293
+ [[ -f "$config_file" ]] && add_to_path "$config_file" "export PATH=\"$INSTALL_DIR:\$PATH\""
294
+ ;;
295
+ *)
296
+ config_file="$HOME/.bashrc"
297
+ [[ -f "$config_file" ]] && add_to_path "$config_file" "export PATH=\"$INSTALL_DIR:\$PATH\""
298
+ ;;
299
+ esac
300
+ fi
301
+ fi
302
+
303
+ echo ""
304
+ echo -e "${GREEN}Installation complete!${NC}"
305
+ echo ""
306
+ echo -e " Run ${GREEN}seshions${NC} to open Seshions"
307
+ echo ""
308
+ echo -e " ${MUTED}Binary: ${NC}$INSTALL_DIR/$APP"
309
+ echo ""
310
+ echo -e " ${MUTED}Restart your shell or run:${NC}"
311
+ echo -e " export PATH=\"$INSTALL_DIR:\$PATH\""
312
+ echo ""
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process"
4
+ import { readFile } from "node:fs/promises"
5
+ import { ensureRuntime } from "../scripts/runtime/resolve-runtime.mjs"
6
+
7
+ async function readPackageVersion() {
8
+ const packagePath = new URL("../package.json", import.meta.url)
9
+ const pkg = JSON.parse(await readFile(packagePath, "utf8"))
10
+ return String(pkg.version || "").replace(/^v/, "")
11
+ }
12
+
13
+ async function main() {
14
+ const args = process.argv.slice(2)
15
+
16
+ if (args.includes("--version") || args.includes("-v")) {
17
+ console.log(await readPackageVersion())
18
+ return
19
+ }
20
+
21
+ const desiredVersion = process.env.SESHIONS_RUNTIME_VERSION || await readPackageVersion()
22
+ const runtime = await ensureRuntime({
23
+ version: desiredVersion,
24
+ allowLatestFallback: true
25
+ })
26
+
27
+ const child = spawn(runtime.binaryPath, args, {
28
+ stdio: "inherit",
29
+ env: {
30
+ ...process.env,
31
+ NODE_PTY_PREBUILDS: runtime.prebuildsPath
32
+ }
33
+ })
34
+
35
+ child.on("exit", (code, signal) => {
36
+ if (signal) {
37
+ process.kill(process.pid, signal)
38
+ return
39
+ }
40
+ process.exit(code ?? 0)
41
+ })
42
+
43
+ child.on("error", (error) => {
44
+ const message = error instanceof Error ? error.message : String(error)
45
+ console.error(`[seshions] Failed to launch runtime: ${message}`)
46
+ process.exit(1)
47
+ })
48
+ }
49
+
50
+ main().catch((error) => {
51
+ const message = error instanceof Error ? error.message : String(error)
52
+ console.error(`[seshions] ${message}`)
53
+ process.exit(1)
54
+ })
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "seshions",
3
+ "version": "0.2.1",
4
+ "description": "OpenTUI-based Agent Management",
5
+ "type": "module",
6
+ "main": "launcher/seshions.js",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "files": [
11
+ "launcher",
12
+ "scripts/runtime",
13
+ "README.md",
14
+ "LICENSE",
15
+ "install.sh",
16
+ "uninstall.sh"
17
+ ],
18
+ "bin": {
19
+ "seshions": "launcher/seshions.js"
20
+ },
21
+ "scripts": {
22
+ "build": "bun run scripts/build.ts",
23
+ "compile": "bun run scripts/compile.ts",
24
+ "compile:all": "bun run scripts/compile.ts --all",
25
+ "release": "bun run scripts/release.ts",
26
+ "postinstall": "node scripts/runtime/postinstall.mjs",
27
+ "dev": "bun run scripts/build.ts && bun run dist/index.js",
28
+ "start": "bun run dist/index.js",
29
+ "test": "bun test",
30
+ "test:watch": "bun test --watch",
31
+ "typecheck": "tsc --noEmit"
32
+ },
33
+ "dependencies": {
34
+ "@opentui/core": "^0.1.79",
35
+ "@opentui/solid": "^0.1.79",
36
+ "fuzzysort": "^3.0.0",
37
+ "node-pty": "1.0.0",
38
+ "remeda": "^2.0.0",
39
+ "solid-js": "^1.9.0"
40
+ },
41
+ "devDependencies": {
42
+ "@babel/core": "^7.24.0",
43
+ "@babel/preset-typescript": "^7.24.0",
44
+ "@tsconfig/bun": "^1.0.0",
45
+ "@types/bun": "latest",
46
+ "babel-preset-solid": "^1.8.0",
47
+ "typescript": "^5.3.0"
48
+ }
49
+ }
@@ -0,0 +1,37 @@
1
+ import { readFile } from "node:fs/promises"
2
+ import { ensureRuntime, getRuntimeInfo } from "./resolve-runtime.mjs"
3
+
4
+ async function readPackageVersion() {
5
+ const packagePath = new URL("../../package.json", import.meta.url)
6
+ const pkg = JSON.parse(await readFile(packagePath, "utf8"))
7
+ return String(pkg.version || "").replace(/^v/, "")
8
+ }
9
+
10
+ async function main() {
11
+ const skip = process.env.SESHIONS_SKIP_POSTINSTALL === "1" || process.env.CI === "true"
12
+ if (skip) {
13
+ return
14
+ }
15
+
16
+ const globalInstall = process.env.npm_config_global === "true"
17
+ const force = process.env.SESHIONS_POSTINSTALL_FORCE === "1"
18
+
19
+ if (!globalInstall && !force) {
20
+ console.log("[seshions] Runtime download deferred. It will install on first run.")
21
+ return
22
+ }
23
+
24
+ try {
25
+ const version = await readPackageVersion()
26
+ const runtime = await ensureRuntime({ version, allowLatestFallback: true })
27
+ console.log(`[seshions] Runtime ready: v${runtime.version} (${runtime.platform})`)
28
+ } catch (error) {
29
+ const { platform, repo, runtimeRoot } = getRuntimeInfo()
30
+ const message = error instanceof Error ? error.message : String(error)
31
+ console.warn(`[seshions] Runtime prefetch skipped: ${message}`)
32
+ console.warn(`[seshions] Platform: ${platform} | Repo: ${repo} | Cache: ${runtimeRoot}`)
33
+ console.warn("[seshions] First run will retry download automatically.")
34
+ }
35
+ }
36
+
37
+ main()
@@ -0,0 +1,216 @@
1
+ import { spawnSync } from "node:child_process"
2
+ import { Readable } from "node:stream"
3
+ import { pipeline } from "node:stream/promises"
4
+ import { createWriteStream } from "node:fs"
5
+ import { access, chmod, mkdir, readFile, rename, rm } from "node:fs/promises"
6
+ import { constants as fsConstants } from "node:fs"
7
+ import os from "node:os"
8
+ import path from "node:path"
9
+
10
+ const APP = "seshions"
11
+ const DEFAULT_REPO = "danhergir/seshions"
12
+ const USER_AGENT = "seshions-runtime-installer"
13
+
14
+ function normalizeVersion(version) {
15
+ return String(version).replace(/^v/, "")
16
+ }
17
+
18
+ function detectPlatform() {
19
+ const osName = process.platform
20
+ const arch = process.arch
21
+
22
+ if (osName !== "darwin" && osName !== "linux") {
23
+ throw new Error(`Unsupported OS: ${osName}. Seshions runtime supports macOS and Linux.`)
24
+ }
25
+
26
+ if (arch !== "arm64" && arch !== "x64") {
27
+ throw new Error(`Unsupported architecture: ${arch}. Supported architectures: arm64, x64.`)
28
+ }
29
+
30
+ return `${osName}-${arch}`
31
+ }
32
+
33
+ function getRepo() {
34
+ return process.env.SESHIONS_REPO || DEFAULT_REPO
35
+ }
36
+
37
+ function getRuntimeRoot() {
38
+ return process.env.SESHIONS_RUNTIME_DIR || path.join(os.homedir(), ".seshions", "runtime")
39
+ }
40
+
41
+ function getArchiveName(platform) {
42
+ return `${APP}-${platform}.tar.gz`
43
+ }
44
+
45
+ function getReleaseAssetUrl(repo, version, platform) {
46
+ return `https://github.com/${repo}/releases/download/v${version}/${getArchiveName(platform)}`
47
+ }
48
+
49
+ function getRuntimeDir(runtimeRoot, version, platform) {
50
+ return path.join(runtimeRoot, version, platform)
51
+ }
52
+
53
+ async function pathExists(targetPath) {
54
+ try {
55
+ await access(targetPath)
56
+ return true
57
+ } catch {
58
+ return false
59
+ }
60
+ }
61
+
62
+ async function isExecutable(filePath) {
63
+ try {
64
+ await access(filePath, fsConstants.X_OK)
65
+ return true
66
+ } catch {
67
+ return false
68
+ }
69
+ }
70
+
71
+ async function fetchLatestVersion(repo) {
72
+ const response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
73
+ headers: {
74
+ "User-Agent": USER_AGENT,
75
+ "Accept": "application/vnd.github+json"
76
+ }
77
+ })
78
+
79
+ if (!response.ok) {
80
+ throw new Error(`Failed to resolve latest release for ${repo} (HTTP ${response.status})`)
81
+ }
82
+
83
+ const data = await response.json()
84
+ if (!data?.tag_name) {
85
+ throw new Error(`Latest release response for ${repo} did not include tag_name`)
86
+ }
87
+
88
+ return normalizeVersion(data.tag_name)
89
+ }
90
+
91
+ async function downloadFile(url, destination) {
92
+ const response = await fetch(url, {
93
+ headers: {
94
+ "User-Agent": USER_AGENT,
95
+ "Accept": "application/octet-stream"
96
+ }
97
+ })
98
+
99
+ if (!response.ok || !response.body) {
100
+ const error = new Error(`Download failed (${response.status}) for ${url}`)
101
+ error.statusCode = response.status
102
+ throw error
103
+ }
104
+
105
+ const writable = createWriteStream(destination, { mode: 0o600 })
106
+ await pipeline(Readable.fromWeb(response.body), writable)
107
+ }
108
+
109
+ async function extractArchive(archivePath, destinationDir) {
110
+ const result = spawnSync(
111
+ "tar",
112
+ ["-xzf", archivePath, "-C", destinationDir, "--strip-components=1"],
113
+ { stdio: "pipe" }
114
+ )
115
+
116
+ if (result.error) {
117
+ throw new Error(`Failed to run tar: ${result.error.message}`)
118
+ }
119
+
120
+ if (result.status !== 0) {
121
+ const stderr = (result.stderr || "").toString().trim()
122
+ throw new Error(`Failed to extract runtime archive${stderr ? `: ${stderr}` : ""}`)
123
+ }
124
+ }
125
+
126
+ async function installRuntimeVersion({ repo, version, platform, runtimeRoot }) {
127
+ const runtimeDir = getRuntimeDir(runtimeRoot, version, platform)
128
+ const binaryPath = path.join(runtimeDir, APP)
129
+
130
+ if (await isExecutable(binaryPath)) {
131
+ return {
132
+ binaryPath,
133
+ prebuildsPath: path.join(runtimeDir, "prebuilds"),
134
+ runtimeDir,
135
+ version,
136
+ platform,
137
+ repo
138
+ }
139
+ }
140
+
141
+ const tmpDir = `${runtimeDir}.tmp-${process.pid}`
142
+ const archivePath = path.join(tmpDir, getArchiveName(platform))
143
+ const url = getReleaseAssetUrl(repo, version, platform)
144
+
145
+ await rm(tmpDir, { recursive: true, force: true })
146
+ await mkdir(tmpDir, { recursive: true })
147
+
148
+ try {
149
+ await downloadFile(url, archivePath)
150
+ await extractArchive(archivePath, tmpDir)
151
+
152
+ const extractedBinaryPath = path.join(tmpDir, APP)
153
+ if (!(await pathExists(extractedBinaryPath))) {
154
+ throw new Error(`Runtime archive was missing '${APP}' binary`)
155
+ }
156
+
157
+ await chmod(extractedBinaryPath, 0o755)
158
+ await mkdir(path.dirname(runtimeDir), { recursive: true })
159
+ await rm(runtimeDir, { recursive: true, force: true })
160
+ await rename(tmpDir, runtimeDir)
161
+
162
+ return {
163
+ binaryPath: path.join(runtimeDir, APP),
164
+ prebuildsPath: path.join(runtimeDir, "prebuilds"),
165
+ runtimeDir,
166
+ version,
167
+ platform,
168
+ repo
169
+ }
170
+ } catch (error) {
171
+ await rm(tmpDir, { recursive: true, force: true })
172
+ throw error
173
+ }
174
+ }
175
+
176
+ async function readPackageVersion() {
177
+ const packagePath = new URL("../../package.json", import.meta.url)
178
+ const pkg = JSON.parse(await readFile(packagePath, "utf8"))
179
+ return normalizeVersion(pkg.version)
180
+ }
181
+
182
+ export async function ensureRuntime(options = {}) {
183
+ const repo = options.repo || getRepo()
184
+ const platform = options.platform || detectPlatform()
185
+ const runtimeRoot = options.runtimeRoot || getRuntimeRoot()
186
+ const allowLatestFallback = options.allowLatestFallback !== false
187
+
188
+ let version = options.version ? normalizeVersion(options.version) : normalizeVersion(process.env.SESHIONS_RUNTIME_VERSION || "")
189
+ if (!version) {
190
+ version = await readPackageVersion()
191
+ }
192
+
193
+ try {
194
+ return await installRuntimeVersion({ repo, version, platform, runtimeRoot })
195
+ } catch (error) {
196
+ const statusCode = typeof error?.statusCode === "number" ? error.statusCode : undefined
197
+ if (!allowLatestFallback || (statusCode !== 404 && statusCode !== 403)) {
198
+ throw error
199
+ }
200
+
201
+ const latestVersion = await fetchLatestVersion(repo)
202
+ if (latestVersion === version) {
203
+ throw error
204
+ }
205
+
206
+ return installRuntimeVersion({ repo, version: latestVersion, platform, runtimeRoot })
207
+ }
208
+ }
209
+
210
+ export function getRuntimeInfo() {
211
+ return {
212
+ platform: detectPlatform(),
213
+ repo: getRepo(),
214
+ runtimeRoot: getRuntimeRoot()
215
+ }
216
+ }
package/uninstall.sh ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Seshions Uninstaller
4
+ # Usage: SESHIONS_REPO=danhergir/seshions curl -fsSL "https://raw.githubusercontent.com/${SESHIONS_REPO}/main/uninstall.sh" | bash
5
+ #
6
+
7
+ set -euo pipefail
8
+
9
+ # Colors
10
+ RED='\033[0;31m'
11
+ GREEN='\033[0;32m'
12
+ YELLOW='\033[1;33m'
13
+ BLUE='\033[0;34m'
14
+ NC='\033[0m' # No Color
15
+
16
+ INSTALL_DIR="${SESHIONS_INSTALL_DIR:-$HOME/.seshions/bin}"
17
+ BIN_DIR="${SESHIONS_BIN_DIR:-$HOME/.local/bin}"
18
+ DATA_DIR="${SESHIONS_DATA_DIR:-$HOME/.seshions}"
19
+
20
+ purge_data=false
21
+
22
+ while [[ $# -gt 0 ]]; do
23
+ case "$1" in
24
+ --purge-data)
25
+ purge_data=true
26
+ shift
27
+ ;;
28
+ -h|--help)
29
+ cat <<EOF
30
+ Seshions Uninstaller
31
+
32
+ Usage:
33
+ uninstall.sh [--purge-data]
34
+
35
+ Options:
36
+ --purge-data Remove ~/.seshions data (state.db, config.json, logs)
37
+ EOF
38
+ exit 0
39
+ ;;
40
+ *)
41
+ echo -e "${YELLOW}[seshions]${NC} Unknown option: $1"
42
+ shift
43
+ ;;
44
+ esac
45
+ done
46
+
47
+ log() {
48
+ echo -e "${BLUE}[seshions]${NC} $1"
49
+ }
50
+
51
+ success() {
52
+ echo -e "${GREEN}[seshions]${NC} $1"
53
+ }
54
+
55
+ warn() {
56
+ echo -e "${YELLOW}[seshions]${NC} $1"
57
+ }
58
+
59
+ main() {
60
+ echo ""
61
+ echo -e "${BLUE}╭───────────────────────────────────╮${NC}"
62
+ echo -e "${BLUE}│ ${RED}Seshions Uninstaller${BLUE} │${NC}"
63
+ echo -e "${BLUE}╰───────────────────────────────────╯${NC}"
64
+ echo ""
65
+
66
+ for cmd in seshions; do
67
+ if [ -f "$INSTALL_DIR/$cmd" ]; then
68
+ log "Removing $INSTALL_DIR/$cmd..."
69
+ rm -f "$INSTALL_DIR/$cmd"
70
+ fi
71
+ done
72
+
73
+ for cmd in seshions; do
74
+ if [ -L "$BIN_DIR/$cmd" ] || [ -f "$BIN_DIR/$cmd" ]; then
75
+ log "Removing $BIN_DIR/$cmd..."
76
+ rm -f "$BIN_DIR/$cmd"
77
+ fi
78
+ done
79
+
80
+ if [ -d "$INSTALL_DIR" ] && [ -z "$(ls -A "$INSTALL_DIR" 2>/dev/null)" ]; then
81
+ log "Removing empty install directory $INSTALL_DIR..."
82
+ rmdir "$INSTALL_DIR" || true
83
+ fi
84
+
85
+ if [ "$purge_data" = true ] && [ -d "$DATA_DIR" ]; then
86
+ log "Purging data directory $DATA_DIR..."
87
+ rm -rf "$DATA_DIR"
88
+ fi
89
+
90
+ echo ""
91
+ success "Seshions has been uninstalled"
92
+ echo ""
93
+ warn "Note: PATH entries in shell config files were not removed"
94
+ warn "User data is preserved by default. Use --purge-data to remove ~/.seshions data."
95
+ echo ""
96
+ }
97
+
98
+ main "$@"