batipanel 0.3.0

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/lib/server.sh ADDED
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env bash
2
+ # batipanel server - Docker-based OpenClaw AI gateway management
3
+ # Provides: b server init|start|stop|status|logs|config|update
4
+
5
+ BATIPANEL_SERVER_DIR="${BATIPANEL_SERVER_DIR:-$BATIPANEL_HOME/server}"
6
+
7
+ # Docker dependency management is in server-docker.sh
8
+ # _require_docker(), _install_docker(), _install_compose_plugin()
9
+
10
+ # docker compose command (v2 plugin or standalone)
11
+ _compose() {
12
+ if docker compose version &>/dev/null 2>&1; then
13
+ docker compose -f "$BATIPANEL_SERVER_DIR/docker-compose.yml" "$@"
14
+ else
15
+ docker-compose -f "$BATIPANEL_SERVER_DIR/docker-compose.yml" "$@"
16
+ fi
17
+ }
18
+
19
+ _server_is_running() {
20
+ _compose ps --status running 2>/dev/null | grep -q "batipanel-server"
21
+ }
22
+
23
+ # === b server start ===
24
+ server_start() {
25
+ _require_docker || return 1
26
+
27
+ if [ ! -f "$BATIPANEL_SERVER_DIR/.env" ]; then
28
+ echo -e "${RED}Server not configured. Run 'b server init' first.${NC}"
29
+ return 1
30
+ fi
31
+
32
+ echo "Starting batipanel server..."
33
+
34
+ local compose_output
35
+ if ! compose_output=$(_compose up -d --pull always 2>&1); then
36
+ echo "$compose_output" | sed 's/^/ /'
37
+ echo -e "${RED}Failed to start server${NC}"
38
+ return 1
39
+ fi
40
+ echo "$compose_output" | sed 's/^/ /'
41
+
42
+ # wait for health
43
+ echo ""
44
+ echo "Waiting for server to be ready..."
45
+ local attempts=0
46
+ while (( attempts < 30 )); do
47
+ if _server_is_running; then
48
+ echo ""
49
+ echo -e "${GREEN}=== Server Running ===${NC}"
50
+ _print_server_info
51
+ return 0
52
+ fi
53
+ sleep 2
54
+ attempts=$((attempts + 1))
55
+ printf "."
56
+ done
57
+
58
+ echo ""
59
+ echo -e "${YELLOW}Server is starting but not yet healthy.${NC}"
60
+ echo " Check logs: b server logs"
61
+ }
62
+
63
+ # === b server stop ===
64
+ server_stop() {
65
+ _require_docker || return 1
66
+
67
+ if [ ! -f "$BATIPANEL_SERVER_DIR/docker-compose.yml" ]; then
68
+ echo -e "${YELLOW}No server configuration found.${NC}"
69
+ return 1
70
+ fi
71
+
72
+ echo "Stopping batipanel server..."
73
+ local compose_output
74
+ if ! compose_output=$(_compose down 2>&1); then
75
+ echo "$compose_output" | sed 's/^/ /'
76
+ echo -e "${RED}Failed to stop server${NC}"
77
+ return 1
78
+ fi
79
+ echo "$compose_output" | sed 's/^/ /'
80
+ echo -e "${GREEN}Server stopped.${NC}"
81
+ }
82
+
83
+ # === b server status ===
84
+ server_status() {
85
+ echo ""
86
+ echo -e "${BLUE}=== Batipanel Server Status ===${NC}"
87
+ echo ""
88
+
89
+ if [ ! -f "$BATIPANEL_SERVER_DIR/.env" ]; then
90
+ echo -e " ${YELLOW}Not configured. Run 'b server init'${NC}"
91
+ return 0
92
+ fi
93
+
94
+ if ! has_cmd docker || ! docker info &>/dev/null 2>&1; then
95
+ echo -e " ${RED}Docker is not running.${NC}"
96
+ return 1
97
+ fi
98
+
99
+ if _server_is_running; then
100
+ echo -e " Status: ${GREEN}Running${NC}"
101
+ _print_server_info
102
+ echo ""
103
+
104
+ # security report
105
+ echo -e " ${BLUE}Security${NC}"
106
+ echo -e " ├─ Container: Docker isolated"
107
+ if grep -q "OPENCLAW_SANDBOX=1" "$BATIPANEL_SERVER_DIR/.env" 2>/dev/null; then
108
+ echo -e " ├─ Sandbox: ${GREEN}enabled${NC}"
109
+ else
110
+ echo -e " ├─ Sandbox: ${YELLOW}disabled${NC}"
111
+ fi
112
+ echo -e " ├─ Network: loopback only"
113
+ echo -e " ├─ Access: allowlist"
114
+ echo -e " └─ API Keys: $(stat -c '%a' "$BATIPANEL_SERVER_DIR/.env" 2>/dev/null || stat -f '%Lp' "$BATIPANEL_SERVER_DIR/.env" 2>/dev/null) permissions"
115
+ else
116
+ echo -e " Status: ${RED}Stopped${NC}"
117
+ echo ""
118
+ echo " Start with: b server start"
119
+ fi
120
+ echo ""
121
+ }
122
+
123
+ # === b server logs ===
124
+ server_logs() {
125
+ _require_docker || return 1
126
+
127
+ if [ ! -f "$BATIPANEL_SERVER_DIR/docker-compose.yml" ]; then
128
+ echo -e "${YELLOW}No server configuration found.${NC}"
129
+ return 1
130
+ fi
131
+
132
+ local follow="${1:-}"
133
+ if [[ "$follow" == "-f" ]]; then
134
+ _compose logs -f --tail 50
135
+ else
136
+ _compose logs --tail 100
137
+ fi
138
+ }
139
+
140
+ # === b server update ===
141
+ server_update() {
142
+ _require_docker || return 1
143
+
144
+ if [ ! -f "$BATIPANEL_SERVER_DIR/.env" ]; then
145
+ echo -e "${RED}Server not configured.${NC}"
146
+ return 1
147
+ fi
148
+
149
+ echo "Updating batipanel server..."
150
+ local compose_output
151
+ if ! compose_output=$(_compose pull 2>&1); then
152
+ echo "$compose_output" | sed 's/^/ /'
153
+ echo -e "${RED}Failed to pull server image${NC}"
154
+ return 1
155
+ fi
156
+ echo "$compose_output" | sed 's/^/ /'
157
+
158
+ echo "Restarting with new image..."
159
+ if ! compose_output=$(_compose up -d 2>&1); then
160
+ echo "$compose_output" | sed 's/^/ /'
161
+ echo -e "${RED}Failed to restart server${NC}"
162
+ return 1
163
+ fi
164
+ echo "$compose_output" | sed 's/^/ /'
165
+
166
+ echo -e "${GREEN}Server updated.${NC}"
167
+ }
168
+
169
+ # === b server config ===
170
+ server_config_cmd() {
171
+ local key="${1:-}"
172
+
173
+ if [ -z "$key" ]; then
174
+ echo -e "${BLUE}=== Server Configuration ===${NC}"
175
+ echo ""
176
+ if [ -f "$BATIPANEL_SERVER_DIR/.env" ]; then
177
+ echo " Environment: $BATIPANEL_SERVER_DIR/.env"
178
+ # show config without sensitive values
179
+ while IFS='=' read -r k v; do
180
+ [[ "$k" =~ ^#.*$ || -z "$k" ]] && continue
181
+ case "$k" in
182
+ *TOKEN*|*KEY*|*SECRET*)
183
+ echo " $k=****"
184
+ ;;
185
+ *)
186
+ echo " $k=$v"
187
+ ;;
188
+ esac
189
+ done < "$BATIPANEL_SERVER_DIR/.env"
190
+ else
191
+ echo " Not configured. Run 'b server init'"
192
+ fi
193
+ echo ""
194
+ return 0
195
+ fi
196
+
197
+ echo -e "${YELLOW}Direct config editing not yet supported.${NC}"
198
+ echo " Edit manually: $BATIPANEL_SERVER_DIR/.env"
199
+ echo " Then restart: b server stop && b server start"
200
+ }
201
+
202
+ # === helpers ===
203
+ _print_server_info() {
204
+ local model="unknown"
205
+ if grep -q "CLAUDE_AI_SESSION_KEY" "$BATIPANEL_SERVER_DIR/.env" 2>/dev/null; then
206
+ model="Claude Opus 4.6 (Max — no API cost)"
207
+ elif grep -q "ANTHROPIC_API_KEY" "$BATIPANEL_SERVER_DIR/.env" 2>/dev/null; then
208
+ model="Claude (API key — usage billing)"
209
+ fi
210
+
211
+ local telegram="not configured"
212
+ if grep -q "TELEGRAM_BOT_TOKEN" "$BATIPANEL_SERVER_DIR/.env" 2>/dev/null; then
213
+ telegram="connected"
214
+ fi
215
+
216
+ echo -e " AI Model: ${GREEN}${model}${NC}"
217
+ echo -e " Telegram: ${GREEN}${telegram}${NC}"
218
+ }
219
+
220
+ # === Router ===
221
+ server_cmd() {
222
+ local subcmd="${1:-}"
223
+ shift 2>/dev/null || true
224
+
225
+ case "$subcmd" in
226
+ init)
227
+ server_init
228
+ ;;
229
+ start)
230
+ server_start
231
+ ;;
232
+ stop)
233
+ server_stop
234
+ ;;
235
+ status)
236
+ server_status
237
+ ;;
238
+ logs)
239
+ server_logs "$@"
240
+ ;;
241
+ update)
242
+ server_update
243
+ ;;
244
+ config)
245
+ server_config_cmd "$@"
246
+ ;;
247
+ *)
248
+ echo -e "${BLUE}=== Batipanel Server ===${NC}"
249
+ echo ""
250
+ echo " b server init Setup Telegram AI bot"
251
+ echo " b server start Start the server"
252
+ echo " b server stop Stop the server"
253
+ echo " b server status Show server status"
254
+ echo " b server logs [-f] View logs (follow with -f)"
255
+ echo " b server update Pull latest image & restart"
256
+ echo " b server config View configuration"
257
+ echo ""
258
+ ;;
259
+ esac
260
+ }
package/lib/session.sh ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ # batipanel session - start, stop, list, project listing
3
+
4
+ tmux_start() {
5
+ local SESSION="$1"
6
+ local LAYOUT="${2:-}"
7
+ local SCRIPT="$BATIPANEL_HOME/projects/${SESSION}.sh"
8
+
9
+ validate_session_name "$SESSION" || return 1
10
+
11
+ if [ ! -f "$SCRIPT" ]; then
12
+ echo -e "${RED}Project not found: ${SESSION}${NC}"
13
+ echo -e "${YELLOW}Available projects:${NC}"
14
+ list_projects
15
+ return 1
16
+ fi
17
+
18
+ if tmux has-session -t "$SESSION" 2>/dev/null; then
19
+ echo -e "${GREEN}Resuming session: $SESSION${NC}"
20
+ else
21
+ echo -e "${BLUE}Starting new session: $SESSION${NC}"
22
+ if [ -n "$LAYOUT" ]; then
23
+ LAYOUT="$LAYOUT" bash "$SCRIPT" "$SESSION"
24
+ else
25
+ bash "$SCRIPT" "$SESSION"
26
+ fi
27
+ fi
28
+
29
+ # iTerm2 -CC mode: opt-in via config, prompt on first encounter
30
+ if [ "${TERM_PROGRAM:-}" = "iTerm.app" ] && [ -z "${BATIPANEL_ITERM_CC:-}" ]; then
31
+ echo ""
32
+ echo -e "${BLUE}iTerm2 detected!${NC}"
33
+ echo " iTerm2 supports native tmux integration (tmux -CC)."
34
+ echo " Panes become native iTerm2 splits instead of tmux UI."
35
+ echo -e " ${YELLOW}Note:${NC} tmux status bar and theme will not be visible in this mode."
36
+ echo ""
37
+ printf "Enable iTerm2 integration? [y/N] "
38
+ local iterm_answer
39
+ read -r iterm_answer
40
+ if [[ "$iterm_answer" == [yY] ]]; then
41
+ BATIPANEL_ITERM_CC="1"
42
+ else
43
+ BATIPANEL_ITERM_CC="0"
44
+ fi
45
+ # persist choice
46
+ _save_config "BATIPANEL_ITERM_CC" "$BATIPANEL_ITERM_CC"
47
+ fi
48
+
49
+ if [ "${BATIPANEL_ITERM_CC:-0}" = "1" ]; then
50
+ exec tmux -CC attach -t "$SESSION"
51
+ else
52
+ exec tmux attach -t "$SESSION"
53
+ fi
54
+ }
55
+
56
+ tmux_stop() {
57
+ local SESSION="$1"
58
+ local FORCE="${2:-}"
59
+ validate_session_name "$SESSION" || return 1
60
+
61
+ if tmux has-session -t "$SESSION" 2>/dev/null; then
62
+ if [[ "$FORCE" != "-f" && -t 0 ]]; then
63
+ printf "Stop session '%s'? [y/N] " "$SESSION"
64
+ local answer
65
+ read -r answer
66
+ if [[ "$answer" != [yY] ]]; then
67
+ echo "Cancelled."
68
+ return 0
69
+ fi
70
+ fi
71
+ tmux kill-session -t "$SESSION" && echo -e "${RED}Stopped: $SESSION${NC}"
72
+ else
73
+ echo -e "${YELLOW}Session not found: $SESSION${NC}"
74
+ fi
75
+ }
76
+
77
+ tmux_list() {
78
+ echo -e "${BLUE}=== Active Sessions ===${NC}"
79
+ tmux ls 2>/dev/null || echo " (none)"
80
+ echo ""
81
+ echo -e "${BLUE}=== Registered Projects ===${NC}"
82
+ list_projects
83
+ }
84
+
85
+ # list registered projects
86
+ list_projects() {
87
+ local found=0
88
+ for f in "$BATIPANEL_HOME"/projects/*.sh; do
89
+ [ -f "$f" ] || continue
90
+ local name
91
+ name=$(basename "$f" .sh)
92
+ echo " - $name"
93
+ found=1
94
+ done
95
+ if (( found == 0 )); then
96
+ echo " (none - run 'b new <name> <path>' to register a project)"
97
+ fi
98
+ }
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env bash
2
+ # batipanel shell-setup - powerline fonts, prompt theme, hostname hiding
3
+
4
+ BATIPANEL_HOME="${BATIPANEL_HOME:-$HOME/.batipanel}"
5
+
6
+ has_cmd() { command -v "$1" &>/dev/null; }
7
+
8
+ # portable sed -i (macOS vs GNU) — may already be defined by caller
9
+ if ! declare -f _sed_i &>/dev/null; then
10
+ _sed_i() {
11
+ if [ "$(uname -s)" = "Darwin" ]; then
12
+ sed -i '' "$@"
13
+ else
14
+ sed -i "$@"
15
+ fi
16
+ }
17
+ fi
18
+
19
+ # === 1. Install powerline fonts ===
20
+ install_powerline_fonts() {
21
+ echo " Setting up Powerline fonts..."
22
+
23
+ local OS
24
+ OS="$(uname -s)"
25
+
26
+ # check if already installed
27
+ if fc-list 2>/dev/null | grep -qi "powerline\|nerd"; then
28
+ echo " Powerline-compatible fonts already installed"
29
+ return 0
30
+ fi
31
+
32
+ case "$OS" in
33
+ Darwin)
34
+ if has_cmd brew; then
35
+ # install Nerd Font (includes powerline glyphs)
36
+ brew tap homebrew/cask-fonts 2>/dev/null || true
37
+ if brew install --cask font-meslo-lg-nerd-font 2>/dev/null; then
38
+ echo " Installed MesloLGS Nerd Font (macOS)"
39
+ else
40
+ echo " Font install via brew failed, trying powerline-fonts..."
41
+ _install_powerline_fonts_git
42
+ fi
43
+ else
44
+ _install_powerline_fonts_git
45
+ fi
46
+ ;;
47
+ Linux)
48
+ if has_cmd apt-get; then
49
+ if sudo apt-get install -y -qq fonts-powerline 2>/dev/null; then
50
+ echo " Installed fonts-powerline (apt)"
51
+ else
52
+ _install_powerline_fonts_git
53
+ fi
54
+ elif has_cmd dnf; then
55
+ if sudo dnf install -y powerline-fonts 2>/dev/null; then
56
+ echo " Installed powerline-fonts (dnf)"
57
+ else
58
+ _install_powerline_fonts_git
59
+ fi
60
+ elif has_cmd pacman; then
61
+ if sudo pacman -S --noconfirm powerline-fonts 2>/dev/null; then
62
+ echo " Installed powerline-fonts (pacman)"
63
+ else
64
+ _install_powerline_fonts_git
65
+ fi
66
+ else
67
+ _install_powerline_fonts_git
68
+ fi
69
+ ;;
70
+ *)
71
+ _install_powerline_fonts_git
72
+ ;;
73
+ esac
74
+ }
75
+
76
+ # fallback: clone and install powerline fonts from GitHub
77
+ _install_powerline_fonts_git() {
78
+ if ! has_cmd git; then
79
+ echo " git not found, skipping font install"
80
+ return 1
81
+ fi
82
+
83
+ local tmpdir
84
+ tmpdir=$(mktemp -d)
85
+ if git clone --depth=1 https://github.com/powerline/fonts.git "$tmpdir/fonts" 2>/dev/null; then
86
+ bash "$tmpdir/fonts/install.sh" 2>/dev/null || true
87
+ echo " Installed powerline fonts from GitHub"
88
+ else
89
+ echo " Failed to clone powerline fonts"
90
+ fi
91
+ rm -rf "$tmpdir"
92
+ }
93
+
94
+ # === 2. Setup zsh with Oh My Zsh + agnoster ===
95
+ setup_zsh_theme() {
96
+ local shell_rc="$1"
97
+
98
+ echo " Configuring zsh theme..."
99
+
100
+ # install Oh My Zsh if not present
101
+ if [ ! -d "$HOME/.oh-my-zsh" ]; then
102
+ echo " Installing Oh My Zsh..."
103
+ if has_cmd curl; then
104
+ RUNZSH=no CHSH=no KEEP_ZSHRC=yes \
105
+ sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" 2>/dev/null || true
106
+ elif has_cmd wget; then
107
+ RUNZSH=no CHSH=no KEEP_ZSHRC=yes \
108
+ sh -c "$(wget -qO- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" 2>/dev/null || true
109
+ else
110
+ echo " curl/wget not found, skipping Oh My Zsh"
111
+ return 1
112
+ fi
113
+ fi
114
+
115
+ if [ ! -d "$HOME/.oh-my-zsh" ]; then
116
+ echo " Oh My Zsh installation failed"
117
+ return 1
118
+ fi
119
+
120
+ # set agnoster theme
121
+ if [ -f "$shell_rc" ]; then
122
+ if grep -q 'ZSH_THEME=' "$shell_rc" 2>/dev/null; then
123
+ _sed_i 's/^ZSH_THEME=.*/ZSH_THEME="agnoster"/' "$shell_rc"
124
+ else
125
+ echo 'ZSH_THEME="agnoster"' >> "$shell_rc"
126
+ fi
127
+ fi
128
+
129
+ # hide hostname: set DEFAULT_USER to current user
130
+ _add_line_if_missing "$shell_rc" "DEFAULT_USER" \
131
+ "DEFAULT_USER=\"\$(whoami)\""
132
+
133
+ echo " Set agnoster theme with hostname hidden"
134
+ }
135
+
136
+ # fallback prompt generator (when themes.sh is not loaded)
137
+ _setup_default_bash_prompt() {
138
+ local prompt_file="$BATIPANEL_HOME/config/bash-prompt.sh"
139
+ mkdir -p "$BATIPANEL_HOME/config"
140
+ cat > "$prompt_file" << 'FALLBACK_EOF'
141
+ #!/usr/bin/env bash
142
+ # batipanel bash prompt - default theme (fallback)
143
+ __batipanel_prompt() {
144
+ local exit_code=$?
145
+ local sep=$'\uE0B0'
146
+ local bg_user="\[\e[44m\]"
147
+ local fg_user="\[\e[97m\]"
148
+ local bg_dir="\[\e[48;5;240m\]"
149
+ local fg_dir="\[\e[97m\]"
150
+ local bg_git="\[\e[42m\]"
151
+ local fg_git="\[\e[30m\]"
152
+ local bg_err="\[\e[41m\]"
153
+ local fg_err="\[\e[97m\]"
154
+ local reset="\[\e[0m\]"
155
+ local t_user_dir="\[\e[34;48;5;240m\]"
156
+ local t_dir_git="\[\e[38;5;240;42m\]"
157
+ local t_dir_end="\[\e[38;5;240m\]"
158
+ local t_git_end="\[\e[32m\]"
159
+ local t_err_dir="\[\e[31;48;5;240m\]"
160
+ local ps=""
161
+ if [ "$exit_code" -ne 0 ]; then
162
+ ps+="${bg_err}${fg_err} ✘ ${exit_code} "
163
+ ps+="${t_err_dir}${sep}"
164
+ fi
165
+ ps+="${bg_user}${fg_user} \\u "
166
+ ps+="${t_user_dir}${sep}"
167
+ ps+="${bg_dir}${fg_dir} \\w "
168
+ local git_branch=""
169
+ if command -v git &>/dev/null; then
170
+ git_branch="$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null)"
171
+ fi
172
+ if [ -n "$git_branch" ]; then
173
+ ps+="${t_dir_git}${sep}"
174
+ local git_icon=$'\uE0A0'
175
+ ps+="${bg_git}${fg_git} ${git_icon} ${git_branch} "
176
+ ps+="${reset}${t_git_end}${sep}${reset} "
177
+ else
178
+ ps+="${reset}${t_dir_end}${sep}${reset} "
179
+ fi
180
+ PS1="$ps"
181
+ }
182
+ PROMPT_COMMAND="__batipanel_prompt"
183
+ FALLBACK_EOF
184
+ }
185
+
186
+ # === 3. Setup bash powerline prompt ===
187
+ setup_bash_prompt() {
188
+ local shell_rc="$1"
189
+
190
+ echo " Configuring bash prompt..."
191
+
192
+ # generate prompt with current theme (uses _generate_themed_prompt from themes.sh)
193
+ local current_theme="${BATIPANEL_THEME:-default}"
194
+ if declare -f _generate_themed_prompt &>/dev/null; then
195
+ _generate_themed_prompt "$current_theme"
196
+ else
197
+ # fallback: generate default prompt directly (standalone install without themes.sh)
198
+ _setup_default_bash_prompt
199
+ fi
200
+
201
+ # source prompt from shell RC
202
+ local prompt_file="$BATIPANEL_HOME/config/bash-prompt.sh"
203
+ local source_line="source \"$prompt_file\""
204
+ _add_line_if_missing "$shell_rc" "bash-prompt.sh" "$source_line"
205
+
206
+ echo " Set powerline-style prompt (hostname hidden)"
207
+ }
208
+
209
+ # === Helper: add line to RC if not already present ===
210
+ _add_line_if_missing() {
211
+ local rc_file="$1"
212
+ local search="$2"
213
+ local line="$3"
214
+
215
+ if [ ! -f "$rc_file" ]; then
216
+ echo "$line" >> "$rc_file"
217
+ return
218
+ fi
219
+
220
+ if ! grep -qF "$search" "$rc_file" 2>/dev/null; then
221
+ {
222
+ echo ""
223
+ echo "# batipanel shell theme"
224
+ echo "$line"
225
+ } >> "$rc_file"
226
+ fi
227
+ }
228
+
229
+ # === Main entry point ===
230
+ setup_shell_environment() {
231
+ local user_shell="$1"
232
+ local shell_rc="$2"
233
+
234
+ echo ""
235
+ echo "Setting up shell environment..."
236
+
237
+ # install powerline fonts
238
+ install_powerline_fonts
239
+
240
+ # configure prompt based on shell
241
+ case "$user_shell" in
242
+ zsh)
243
+ setup_zsh_theme "$shell_rc"
244
+ ;;
245
+ bash)
246
+ setup_bash_prompt "$shell_rc"
247
+ ;;
248
+ *)
249
+ echo " Unsupported shell ($user_shell), skipping prompt setup"
250
+ ;;
251
+ esac
252
+
253
+ echo " Shell environment setup complete"
254
+ }