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 +21 -0
- package/README.md +91 -0
- package/install.sh +312 -0
- package/launcher/seshions.js +54 -0
- package/package.json +49 -0
- package/scripts/runtime/postinstall.mjs +37 -0
- package/scripts/runtime/resolve-runtime.mjs +216 -0
- package/uninstall.sh +98 -0
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 "$@"
|