lacy 1.8.11 → 1.8.13
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/.claude/settings.local.json +26 -0
- package/.github/FUNDING.yml +3 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
- package/.github/SECURITY.md +32 -0
- package/.github/assets/logo-horizontal-dark.png +0 -0
- package/.github/assets/logo-horizontal-dark.svg +17 -0
- package/.github/assets/logo-horizontal.png +0 -0
- package/.github/assets/logo-horizontal.svg +17 -0
- package/.github/assets/logo.png +0 -0
- package/.github/assets/logo.svg +12 -0
- package/.github/assets/social-preview.png +0 -0
- package/.github/assets/social-preview.svg +50 -0
- package/.github/dependabot.yml +21 -0
- package/.github/workflows/ci.yml +80 -0
- package/.github/workflows/dependabot-auto-merge.yml +32 -0
- package/CHANGELOG.md +366 -0
- package/CLAUDE.md +340 -0
- package/CONTRIBUTING.md +141 -0
- package/LICENSE +110 -0
- package/README.md +201 -31
- package/RELEASING.md +148 -0
- package/STYLE.md +202 -0
- package/assets/hero.jpeg +0 -0
- package/assets/mode-indicators.jpeg +0 -0
- package/assets/real-time-indicator.jpeg +0 -0
- package/assets/supported-tools.jpeg +0 -0
- package/bin/lacy +1028 -0
- package/docs/ADDING-BACKENDS.md +124 -0
- package/docs/DEVTO-ARTICLE.md +94 -0
- package/docs/DOCS.md +68 -0
- package/docs/GROWTH-STRATEGY.md +119 -0
- package/docs/HN-RESPONSES.md +122 -0
- package/docs/LAUNCH-COPY-FINAL.md +105 -0
- package/docs/MARKETING.md +411 -0
- package/docs/NATURAL_LANGUAGE_DETECTION.md +204 -0
- package/docs/UGC_VIDEO_SCRIPT.md +114 -0
- package/docs/articles/devto-how-i-made-my-terminal-understand-english.md +117 -0
- package/docs/demo-color-transition.gif +0 -0
- package/docs/demo-full.gif +0 -0
- package/docs/demo-indicator.gif +0 -0
- package/docs/launch-thread-may6.sh +158 -0
- package/docs/videos/README.md +189 -0
- package/docs/videos/generate_frames.py +510 -0
- package/docs/videos/generate_frames_v2.py +729 -0
- package/docs/videos/generate_short.py +328 -0
- package/docs/videos/generate_short_v2.py +526 -0
- package/docs/videos/lacy-shell-demo-v2.mp4 +0 -0
- package/docs/videos/lacy-shell-demo.mp4 +0 -0
- package/docs/videos/lacy-shell-short-v2.mp4 +0 -0
- package/docs/videos/lacy-shell-short.mp4 +0 -0
- package/install.sh +1009 -0
- package/lacy.plugin.bash +75 -0
- package/lacy.plugin.fish +43 -0
- package/lacy.plugin.zsh +65 -0
- package/lib/animations.zsh +3 -0
- package/lib/bash/completions.bash +40 -0
- package/lib/bash/execute.bash +233 -0
- package/lib/bash/init.bash +40 -0
- package/lib/bash/keybindings.bash +134 -0
- package/lib/bash/prompt.bash +85 -0
- package/lib/commands/info.sh +25 -0
- package/lib/config.zsh +3 -0
- package/lib/constants.zsh +3 -0
- package/lib/core/animations.sh +271 -0
- package/lib/core/commands.sh +297 -0
- package/lib/core/config.sh +340 -0
- package/lib/core/constants.sh +366 -0
- package/lib/core/context.sh +260 -0
- package/lib/core/detection.sh +417 -0
- package/lib/core/mcp.sh +741 -0
- package/lib/core/modes.sh +123 -0
- package/lib/core/preheat.sh +496 -0
- package/lib/core/spinner.sh +174 -0
- package/lib/core/telemetry.sh +99 -0
- package/lib/detection.zsh +3 -0
- package/lib/execute.zsh +3 -0
- package/lib/fish/config.fish +66 -0
- package/lib/fish/detection.fish +90 -0
- package/lib/fish/execute.fish +105 -0
- package/lib/fish/keybindings.fish +42 -0
- package/lib/fish/prompt.fish +30 -0
- package/lib/keybindings.zsh +3 -0
- package/lib/mcp.zsh +3 -0
- package/lib/modes.zsh +3 -0
- package/lib/preheat.zsh +3 -0
- package/lib/prompt.zsh +3 -0
- package/lib/spinner.zsh +3 -0
- package/lib/zsh/completions.zsh +60 -0
- package/lib/zsh/execute.zsh +294 -0
- package/lib/zsh/init.zsh +26 -0
- package/lib/zsh/keybindings.zsh +551 -0
- package/lib/zsh/prompt.zsh +90 -0
- package/package.json +42 -27
- package/packages/lacy/README.md +61 -0
- package/packages/lacy/commands/info.sh +25 -0
- package/{index.mjs → packages/lacy/index.mjs} +247 -20
- package/packages/lacy/package-lock.json +71 -0
- package/packages/lacy/package.json +42 -0
- package/script/release.ts +487 -0
- package/squirrel.toml +36 -0
- package/tests/test_bash.bash +163 -0
- package/tests/test_core.sh +607 -0
- package/tests/test_gemini.sh +119 -0
- package/tests/test_gemini_mcp.sh +126 -0
- package/tests/test_preheat_server.zsh +446 -0
- package/uninstall.sh +52 -0
package/install.sh
ADDED
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Lacy Shell Installation Script
|
|
4
|
+
# https://github.com/lacymorrow/lacy
|
|
5
|
+
#
|
|
6
|
+
# Install methods:
|
|
7
|
+
# curl -fsSL https://lacy.sh/install | bash
|
|
8
|
+
# npx lacy
|
|
9
|
+
# brew install lacymorrow/tap/lacy
|
|
10
|
+
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
# Colors for output
|
|
14
|
+
RED='\033[0;31m'
|
|
15
|
+
GREEN='\033[0;32m'
|
|
16
|
+
YELLOW='\033[1;33m'
|
|
17
|
+
BLUE='\033[0;34m'
|
|
18
|
+
MAGENTA='\033[0;35m'
|
|
19
|
+
CYAN='\033[0;36m'
|
|
20
|
+
BOLD='\033[1m'
|
|
21
|
+
DIM='\033[2m'
|
|
22
|
+
NC='\033[0m' # No Color
|
|
23
|
+
|
|
24
|
+
# Installation directory
|
|
25
|
+
INSTALL_DIR="${HOME}/.lacy"
|
|
26
|
+
REPO_URL="https://github.com/lacymorrow/lacy.git"
|
|
27
|
+
TARBALL_URL="https://github.com/lacymorrow/lacy/archive/refs/heads"
|
|
28
|
+
CONFIG_FILE="${INSTALL_DIR}/config.yaml"
|
|
29
|
+
|
|
30
|
+
# Release channel (set via --beta, --channel, or LACY_CHANNEL env var)
|
|
31
|
+
LACY_CHANNEL="${LACY_CHANNEL:-latest}"
|
|
32
|
+
|
|
33
|
+
# Analytics — lightweight, anonymous install tracking via Umami
|
|
34
|
+
# No PII collected. Respects DO_NOT_TRACK. See: https://umami.is
|
|
35
|
+
UMAMI_URL="${LACY_UMAMI_URL:-https://analytics.lacy.sh}"
|
|
36
|
+
UMAMI_WEBSITE_ID="${LACY_UMAMI_WEBSITE_ID:-577521d7-3db7-4a77-a45c-3c97f21b5322}"
|
|
37
|
+
|
|
38
|
+
track_event() {
|
|
39
|
+
[[ "${DO_NOT_TRACK:-}" == "1" ]] && return
|
|
40
|
+
[[ "${LACY_NO_TELEMETRY:-}" == "1" ]] && return
|
|
41
|
+
|
|
42
|
+
local event_name="${1:-install}"
|
|
43
|
+
local method="${2:-curl}"
|
|
44
|
+
local version
|
|
45
|
+
version=$(get_installed_version)
|
|
46
|
+
|
|
47
|
+
(curl -sf --connect-timeout 3 --max-time 5 -X POST "${UMAMI_URL}/api/send" \
|
|
48
|
+
-H "Content-Type: application/json" \
|
|
49
|
+
-H "User-Agent: lacy-install/${version:-unknown}" \
|
|
50
|
+
-d "{
|
|
51
|
+
\"type\": \"event\",
|
|
52
|
+
\"payload\": {
|
|
53
|
+
\"hostname\": \"lacy.sh\",
|
|
54
|
+
\"language\": \"\",
|
|
55
|
+
\"referrer\": \"\",
|
|
56
|
+
\"screen\": \"\",
|
|
57
|
+
\"title\": \"Install\",
|
|
58
|
+
\"url\": \"/install/${method}\",
|
|
59
|
+
\"website\": \"${UMAMI_WEBSITE_ID}\",
|
|
60
|
+
\"name\": \"${event_name}\",
|
|
61
|
+
\"data\": {
|
|
62
|
+
\"method\": \"${method}\",
|
|
63
|
+
\"os\": \"$(uname -s 2>/dev/null || echo unknown)\",
|
|
64
|
+
\"arch\": \"$(uname -m 2>/dev/null || echo unknown)\",
|
|
65
|
+
\"shell\": \"${DETECTED_SHELL:-unknown}\",
|
|
66
|
+
\"version\": \"${version:-unknown}\",
|
|
67
|
+
\"channel\": \"${LACY_CHANNEL}\"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}" >/dev/null 2>&1 &)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Validate channel — alphanumeric, hyphens, dots only
|
|
74
|
+
if [[ ! "$LACY_CHANNEL" =~ ^[a-zA-Z0-9._-]+$ ]]; then
|
|
75
|
+
printf "${RED}Invalid channel: %s${NC}\n" "$LACY_CHANNEL" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Version — read from installed or repo package.json
|
|
80
|
+
get_installed_version() {
|
|
81
|
+
local pkg_file="${INSTALL_DIR}/package.json"
|
|
82
|
+
if [[ -f "$pkg_file" ]]; then
|
|
83
|
+
grep '"version"' "$pkg_file" 2>/dev/null | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"//' | sed 's/".*//'
|
|
84
|
+
fi
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Selected tool (set during installation)
|
|
88
|
+
SELECTED_TOOL=""
|
|
89
|
+
CUSTOM_COMMAND=""
|
|
90
|
+
|
|
91
|
+
# Detected shell (set during installation)
|
|
92
|
+
DETECTED_SHELL=""
|
|
93
|
+
|
|
94
|
+
# Detect user's login shell
|
|
95
|
+
detect_user_shell() {
|
|
96
|
+
if [[ -n "$LACY_FORCE_SHELL" ]]; then
|
|
97
|
+
DETECTED_SHELL="$LACY_FORCE_SHELL"
|
|
98
|
+
return
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
local login_shell
|
|
102
|
+
login_shell=$(basename "${SHELL:-}")
|
|
103
|
+
|
|
104
|
+
case "$login_shell" in
|
|
105
|
+
zsh) DETECTED_SHELL="zsh" ;;
|
|
106
|
+
bash) DETECTED_SHELL="bash" ;;
|
|
107
|
+
fish) DETECTED_SHELL="fish" ;;
|
|
108
|
+
*)
|
|
109
|
+
# Detect from running process or what's available
|
|
110
|
+
if [[ -n "${BASH_VERSION:-}" ]]; then
|
|
111
|
+
DETECTED_SHELL="bash"
|
|
112
|
+
elif [[ -n "${ZSH_VERSION:-}" ]]; then
|
|
113
|
+
DETECTED_SHELL="zsh"
|
|
114
|
+
elif command -v zsh >/dev/null 2>&1; then
|
|
115
|
+
DETECTED_SHELL="zsh"
|
|
116
|
+
elif command -v bash >/dev/null 2>&1; then
|
|
117
|
+
DETECTED_SHELL="bash"
|
|
118
|
+
else
|
|
119
|
+
DETECTED_SHELL="bash"
|
|
120
|
+
fi
|
|
121
|
+
;;
|
|
122
|
+
esac
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Get the RC file for the detected shell
|
|
126
|
+
get_rc_file() {
|
|
127
|
+
case "$DETECTED_SHELL" in
|
|
128
|
+
bash)
|
|
129
|
+
# macOS uses .bash_profile for login shells
|
|
130
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
131
|
+
echo "${HOME}/.bash_profile"
|
|
132
|
+
else
|
|
133
|
+
echo "${HOME}/.bashrc"
|
|
134
|
+
fi
|
|
135
|
+
;;
|
|
136
|
+
fish) echo "${HOME}/.config/fish/conf.d/lacy.fish" ;;
|
|
137
|
+
*) echo "${HOME}/.zshrc" ;;
|
|
138
|
+
esac
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Get the plugin file for the detected shell
|
|
142
|
+
get_plugin_file() {
|
|
143
|
+
case "$DETECTED_SHELL" in
|
|
144
|
+
bash) echo "lacy.plugin.bash" ;;
|
|
145
|
+
fish) echo "lacy.plugin.fish" ;;
|
|
146
|
+
*) echo "lacy.plugin.zsh" ;;
|
|
147
|
+
esac
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Get the shell restart command
|
|
151
|
+
get_shell_restart_cmd() {
|
|
152
|
+
case "$DETECTED_SHELL" in
|
|
153
|
+
bash) echo "bash -l" ;;
|
|
154
|
+
fish) echo "fish" ;;
|
|
155
|
+
*) echo "zsh -l" ;;
|
|
156
|
+
esac
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Get the source command for the RC file
|
|
160
|
+
get_source_hint() {
|
|
161
|
+
local rc_file
|
|
162
|
+
rc_file=$(get_rc_file)
|
|
163
|
+
echo "source $rc_file"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Check if we should use Node installer
|
|
167
|
+
use_node_installer() {
|
|
168
|
+
# Skip Node installer if --bash flag is passed
|
|
169
|
+
[[ "$LACY_FORCE_BASH" == "1" ]] && return 1
|
|
170
|
+
|
|
171
|
+
# Check if npx is available and we have an interactive terminal
|
|
172
|
+
# Note: when piped (curl | bash), -t 0 is false but /dev/tty is still available
|
|
173
|
+
if command -v npx >/dev/null 2>&1 && { [[ -t 0 ]] || [[ -c /dev/tty ]]; }; then
|
|
174
|
+
return 0
|
|
175
|
+
fi
|
|
176
|
+
return 1
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Run Node installer via npx
|
|
180
|
+
run_node_installer() {
|
|
181
|
+
local pkg="lacy@${LACY_CHANNEL}"
|
|
182
|
+
|
|
183
|
+
# Quietly check if package exists first
|
|
184
|
+
if ! npm view "$pkg" version >/dev/null 2>&1; then
|
|
185
|
+
return 1
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
if [[ "$LACY_CHANNEL" != "latest" ]]; then
|
|
189
|
+
printf "${MAGENTA}Channel: ${LACY_CHANNEL}${NC}\n"
|
|
190
|
+
fi
|
|
191
|
+
printf "${BLUE}Using interactive installer...${NC}\n"
|
|
192
|
+
printf "\n"
|
|
193
|
+
# Redirect stdin from /dev/tty so the Node process gets an interactive TTY
|
|
194
|
+
# even when this script is piped (curl | bash)
|
|
195
|
+
if npx --yes "$pkg" < /dev/tty; then
|
|
196
|
+
# Restore terminal state — the Node process uses @clack/prompts which
|
|
197
|
+
# toggles raw mode on the tty. If Node exits without restoring it
|
|
198
|
+
# (crash, SIGINT, etc.), the parent shell's tty is left corrupted.
|
|
199
|
+
stty sane 2>/dev/null
|
|
200
|
+
exit 0
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# npx failed for some reason, fall back
|
|
204
|
+
# Restore terminal state in case Node corrupted it before failing
|
|
205
|
+
stty sane 2>/dev/null
|
|
206
|
+
printf "\n"
|
|
207
|
+
printf "${YELLOW}Falling back to standard installer...${NC}\n"
|
|
208
|
+
printf "\n"
|
|
209
|
+
return 1
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
print_banner() {
|
|
213
|
+
printf "\n"
|
|
214
|
+
printf "${MAGENTA}${BOLD}"
|
|
215
|
+
printf " _ \n"
|
|
216
|
+
printf " | | __ _ ___ _ _ \n"
|
|
217
|
+
printf " | | / _\` |/ __| | | | \n"
|
|
218
|
+
printf " | |__| (_| | (__| |_| | \n"
|
|
219
|
+
printf " |_____\__,_|\___|\__, | \n"
|
|
220
|
+
printf " |___/ \n"
|
|
221
|
+
printf "${NC}"
|
|
222
|
+
printf "${CYAN}Talk directly to your shell${NC}\n"
|
|
223
|
+
local version
|
|
224
|
+
version=$(get_installed_version)
|
|
225
|
+
if [[ -n "$version" ]]; then
|
|
226
|
+
printf "${DIM} v${version}${NC}\n"
|
|
227
|
+
fi
|
|
228
|
+
if [[ "$LACY_CHANNEL" != "latest" ]]; then
|
|
229
|
+
printf "${MAGENTA}${BOLD} [${LACY_CHANNEL}]${NC}\n"
|
|
230
|
+
fi
|
|
231
|
+
printf "\n"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Check prerequisites
|
|
235
|
+
check_prerequisites() {
|
|
236
|
+
printf "${BLUE}Checking prerequisites...${NC}\n"
|
|
237
|
+
local missing=0
|
|
238
|
+
|
|
239
|
+
# Check for the target shell
|
|
240
|
+
case "$DETECTED_SHELL" in
|
|
241
|
+
zsh)
|
|
242
|
+
if command -v zsh >/dev/null 2>&1; then
|
|
243
|
+
printf " ${GREEN}✓${NC} zsh\n"
|
|
244
|
+
else
|
|
245
|
+
printf " ${RED}✗${NC} zsh (required)\n"
|
|
246
|
+
missing=1
|
|
247
|
+
fi
|
|
248
|
+
;;
|
|
249
|
+
bash)
|
|
250
|
+
if command -v bash >/dev/null 2>&1; then
|
|
251
|
+
local bash_version user_bash
|
|
252
|
+
# Use the user's $SHELL if it's bash — on macOS, plain `bash` resolves
|
|
253
|
+
# to /bin/bash (3.2) even when the user has a newer bash as their shell
|
|
254
|
+
case "${SHELL:-}" in
|
|
255
|
+
*/bash) user_bash="$SHELL" ;;
|
|
256
|
+
*) user_bash="bash" ;;
|
|
257
|
+
esac
|
|
258
|
+
bash_version=$("$user_bash" -c 'echo ${BASH_VERSINFO[0]}' 2>/dev/null || bash -c 'echo ${BASH_VERSINFO[0]}' 2>/dev/null || echo "0")
|
|
259
|
+
if [[ "$bash_version" -ge 4 ]]; then
|
|
260
|
+
printf " ${GREEN}✓${NC} bash ${bash_version}+\n"
|
|
261
|
+
else
|
|
262
|
+
printf " ${RED}✗${NC} bash 4+ required (found bash ${bash_version})\n"
|
|
263
|
+
printf " ${DIM}Install with: brew install bash${NC}\n"
|
|
264
|
+
missing=1
|
|
265
|
+
fi
|
|
266
|
+
else
|
|
267
|
+
printf " ${RED}✗${NC} bash (required)\n"
|
|
268
|
+
missing=1
|
|
269
|
+
fi
|
|
270
|
+
;;
|
|
271
|
+
esac
|
|
272
|
+
|
|
273
|
+
# Check for curl
|
|
274
|
+
if command -v curl >/dev/null 2>&1; then
|
|
275
|
+
printf " ${GREEN}✓${NC} curl\n"
|
|
276
|
+
else
|
|
277
|
+
printf " ${RED}✗${NC} curl (required)\n"
|
|
278
|
+
missing=1
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# Check for git (optional — curl fallback available)
|
|
282
|
+
if command -v git >/dev/null 2>&1; then
|
|
283
|
+
printf " ${GREEN}✓${NC} git\n"
|
|
284
|
+
else
|
|
285
|
+
printf " ${YELLOW}○${NC} git (not found, will use curl fallback)\n"
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
printf "\n"
|
|
289
|
+
|
|
290
|
+
if [[ $missing -eq 1 ]]; then
|
|
291
|
+
printf "${RED}Please install missing prerequisites and try again.${NC}\n"
|
|
292
|
+
exit 1
|
|
293
|
+
fi
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# Detect installed AI CLI tools
|
|
297
|
+
detect_tools() {
|
|
298
|
+
printf "${BLUE}Detecting AI CLI tools...${NC}\n"
|
|
299
|
+
local found=0
|
|
300
|
+
|
|
301
|
+
for tool in lash claude opencode gemini codex hermes copilot amp; do
|
|
302
|
+
if command -v "$tool" >/dev/null 2>&1; then
|
|
303
|
+
printf " ${GREEN}✓${NC} $tool\n"
|
|
304
|
+
found=1
|
|
305
|
+
fi
|
|
306
|
+
done
|
|
307
|
+
|
|
308
|
+
if [[ $found -eq 0 ]]; then
|
|
309
|
+
printf " ${YELLOW}○${NC} No AI CLI tools found\n"
|
|
310
|
+
printf "\n"
|
|
311
|
+
printf "${YELLOW}Lacy Shell requires an AI CLI tool to work.${NC}\n"
|
|
312
|
+
printf "Would you like to install ${GREEN}lash${NC}? (AI coding agent — lash.lacy.sh)\n"
|
|
313
|
+
printf "\n"
|
|
314
|
+
read -p "Install lash now? [Y/n]: " install_now < /dev/tty 2>/dev/null || install_now="n"
|
|
315
|
+
if [[ ! "$install_now" =~ ^[Nn]$ ]]; then
|
|
316
|
+
install_lash
|
|
317
|
+
printf "\n"
|
|
318
|
+
# Re-check if lash was installed successfully
|
|
319
|
+
if command -v lash >/dev/null 2>&1; then
|
|
320
|
+
printf " ${GREEN}✓${NC} lash\n"
|
|
321
|
+
fi
|
|
322
|
+
fi
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
printf "\n"
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
# Interactive tool selection (bash fallback)
|
|
329
|
+
select_tool() {
|
|
330
|
+
printf "${BOLD}Which AI CLI tool do you want to use?${NC}\n"
|
|
331
|
+
printf "\n"
|
|
332
|
+
printf " 1) lash ${DIM}- AI coding agent (recommended) — lash.lacy.sh${NC}\n"
|
|
333
|
+
printf " 2) claude ${DIM}- Claude Code CLI${NC}\n"
|
|
334
|
+
printf " 3) opencode ${DIM}- OpenCode CLI${NC}\n"
|
|
335
|
+
printf " 4) gemini ${DIM}- Google Gemini CLI${NC}\n"
|
|
336
|
+
printf " 5) codex ${DIM}- OpenAI Codex CLI${NC}\n"
|
|
337
|
+
printf " 6) hermes ${DIM}- Hermes Agent CLI${NC}\n"
|
|
338
|
+
printf " 7) copilot ${DIM}- GitHub Copilot CLI${NC}\n"
|
|
339
|
+
printf " 8) amp ${DIM}- Sourcegraph Amp CLI${NC}\n"
|
|
340
|
+
printf " 9) Auto-detect ${DIM}(use first available)${NC}\n"
|
|
341
|
+
printf " 10) None ${DIM}- I'll install one later${NC}\n"
|
|
342
|
+
printf " 11) Custom ${DIM}- enter your own command${NC}\n"
|
|
343
|
+
printf "\n"
|
|
344
|
+
|
|
345
|
+
local choice
|
|
346
|
+
read -p "Select [1-11, default=9]: " choice < /dev/tty 2>/dev/null || choice="9"
|
|
347
|
+
|
|
348
|
+
case "$choice" in
|
|
349
|
+
1) SELECTED_TOOL="lash" ;;
|
|
350
|
+
2) SELECTED_TOOL="claude" ;;
|
|
351
|
+
3) SELECTED_TOOL="opencode" ;;
|
|
352
|
+
4) SELECTED_TOOL="gemini" ;;
|
|
353
|
+
5) SELECTED_TOOL="codex" ;;
|
|
354
|
+
6) SELECTED_TOOL="hermes" ;;
|
|
355
|
+
7) SELECTED_TOOL="copilot" ;;
|
|
356
|
+
8) SELECTED_TOOL="amp" ;;
|
|
357
|
+
9|"") SELECTED_TOOL="" ;;
|
|
358
|
+
10) SELECTED_TOOL="none" ;;
|
|
359
|
+
11)
|
|
360
|
+
SELECTED_TOOL="custom"
|
|
361
|
+
printf "\n"
|
|
362
|
+
read -p "Enter command (e.g. claude --dangerously-skip-permissions -p): " CUSTOM_COMMAND < /dev/tty 2>/dev/null || CUSTOM_COMMAND=""
|
|
363
|
+
if [[ -z "$CUSTOM_COMMAND" ]]; then
|
|
364
|
+
printf "${RED}No command entered. Falling back to auto-detect.${NC}\n"
|
|
365
|
+
SELECTED_TOOL=""
|
|
366
|
+
fi
|
|
367
|
+
;;
|
|
368
|
+
*) SELECTED_TOOL="" ;;
|
|
369
|
+
esac
|
|
370
|
+
|
|
371
|
+
if [[ -n "$SELECTED_TOOL" && "$SELECTED_TOOL" != "none" && "$SELECTED_TOOL" != "custom" ]]; then
|
|
372
|
+
printf "\n"
|
|
373
|
+
printf "Selected: ${GREEN}$SELECTED_TOOL${NC}\n"
|
|
374
|
+
|
|
375
|
+
# Check if selected tool is installed
|
|
376
|
+
if ! command -v "$SELECTED_TOOL" >/dev/null 2>&1; then
|
|
377
|
+
printf "${YELLOW}Note: $SELECTED_TOOL is not installed.${NC}\n"
|
|
378
|
+
|
|
379
|
+
if [[ "$SELECTED_TOOL" == "lash" ]]; then
|
|
380
|
+
printf "\n"
|
|
381
|
+
read -p "Would you like to install lash now? [y/N]: " do_install_lash < /dev/tty 2>/dev/null || do_install_lash="n"
|
|
382
|
+
if [[ "$do_install_lash" =~ ^[Yy]$ ]]; then
|
|
383
|
+
install_lash
|
|
384
|
+
fi
|
|
385
|
+
else
|
|
386
|
+
printf "You can install it later with:\n"
|
|
387
|
+
case "$SELECTED_TOOL" in
|
|
388
|
+
claude) printf " brew install claude\n" ;;
|
|
389
|
+
opencode) printf " brew install opencode\n" ;;
|
|
390
|
+
gemini) printf " brew install gemini\n" ;;
|
|
391
|
+
codex) printf " npm install -g @openai/codex\n" ;;
|
|
392
|
+
hermes) printf " curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash\n" ;;
|
|
393
|
+
copilot) printf " gh extension install github/gh-copilot\n" ;;
|
|
394
|
+
amp) printf " npm install -g @sourcegraph/amp\n" ;;
|
|
395
|
+
esac
|
|
396
|
+
fi
|
|
397
|
+
fi
|
|
398
|
+
elif [[ "$SELECTED_TOOL" == "custom" ]]; then
|
|
399
|
+
printf "\n"
|
|
400
|
+
printf "Selected: ${GREEN}custom${NC} (${CUSTOM_COMMAND})\n"
|
|
401
|
+
elif [[ "$SELECTED_TOOL" == "none" ]]; then
|
|
402
|
+
printf "\n"
|
|
403
|
+
printf "No tool selected. Lacy will prompt you to install one when needed.\n"
|
|
404
|
+
else
|
|
405
|
+
# Auto-detect: check if any tool is available
|
|
406
|
+
printf "\n"
|
|
407
|
+
local first_tool_found=""
|
|
408
|
+
for t in lash claude opencode gemini codex hermes copilot amp; do
|
|
409
|
+
if command -v "$t" >/dev/null 2>&1; then
|
|
410
|
+
first_tool_found="$t"
|
|
411
|
+
break
|
|
412
|
+
fi
|
|
413
|
+
done
|
|
414
|
+
|
|
415
|
+
if [[ -z "$first_tool_found" ]]; then
|
|
416
|
+
printf "${YELLOW}No AI CLI tools are installed.${NC}\n"
|
|
417
|
+
printf "Would you like to install ${GREEN}lash${NC}? (AI coding agent — lash.lacy.sh)\n"
|
|
418
|
+
printf "\n"
|
|
419
|
+
local do_install=""
|
|
420
|
+
read -p "Install lash now? [Y/n]: " do_install < /dev/tty 2>/dev/null || do_install="n"
|
|
421
|
+
if [[ ! "$do_install" =~ ^[Nn]$ ]]; then
|
|
422
|
+
install_lash
|
|
423
|
+
else
|
|
424
|
+
printf "\n"
|
|
425
|
+
printf "Using: ${GREEN}auto-detect${NC} (first available tool)\n"
|
|
426
|
+
printf "${YELLOW}Note: You'll need to install a tool before using Lacy.${NC}\n"
|
|
427
|
+
fi
|
|
428
|
+
else
|
|
429
|
+
printf "Using: ${GREEN}auto-detect${NC} (currently: ${GREEN}${first_tool_found}${NC})\n"
|
|
430
|
+
fi
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
printf "\n"
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Install lash CLI
|
|
437
|
+
install_lash() {
|
|
438
|
+
printf "${BLUE}Installing lash...${NC}\n"
|
|
439
|
+
|
|
440
|
+
if command -v npm >/dev/null 2>&1; then
|
|
441
|
+
npm install -g lashcode
|
|
442
|
+
printf "${GREEN}✓ lash installed${NC}\n"
|
|
443
|
+
elif command -v brew >/dev/null 2>&1; then
|
|
444
|
+
brew tap lacymorrow/tap
|
|
445
|
+
brew install lash
|
|
446
|
+
printf "${GREEN}✓ lash installed${NC}\n"
|
|
447
|
+
else
|
|
448
|
+
printf "${RED}Could not install lash. Please install npm or homebrew first.${NC}\n"
|
|
449
|
+
fi
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
# Download and extract tarball (curl fallback when git is not available)
|
|
453
|
+
install_via_tarball() {
|
|
454
|
+
local branch="$1"
|
|
455
|
+
local tarball_file
|
|
456
|
+
tarball_file=$(mktemp "${TMPDIR:-/tmp}/lacy-XXXXXX.tar.gz")
|
|
457
|
+
|
|
458
|
+
curl -fsSL "${TARBALL_URL}/${branch}.tar.gz" -o "$tarball_file" 2>/dev/null || {
|
|
459
|
+
rm -f "$tarball_file"
|
|
460
|
+
return 1
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
mkdir -p "$INSTALL_DIR"
|
|
464
|
+
tar xzf "$tarball_file" --strip-components=1 -C "$INSTALL_DIR" 2>/dev/null || {
|
|
465
|
+
rm -f "$tarball_file"
|
|
466
|
+
return 1
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
rm -f "$tarball_file"
|
|
470
|
+
return 0
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
# Clone or update repository
|
|
474
|
+
install_plugin() {
|
|
475
|
+
printf "${BLUE}Installing Lacy...${NC}\n"
|
|
476
|
+
|
|
477
|
+
# Determine git branch: beta channel clones beta branch, otherwise main
|
|
478
|
+
local branch="main"
|
|
479
|
+
if [[ "$LACY_CHANNEL" != "latest" ]]; then
|
|
480
|
+
branch="$LACY_CHANNEL"
|
|
481
|
+
fi
|
|
482
|
+
|
|
483
|
+
local has_git=0
|
|
484
|
+
command -v git >/dev/null 2>&1 && has_git=1
|
|
485
|
+
|
|
486
|
+
if [[ -d "$INSTALL_DIR" ]]; then
|
|
487
|
+
printf "${YELLOW}Existing installation found. Updating...${NC}\n"
|
|
488
|
+
if [[ $has_git -eq 1 && -d "$INSTALL_DIR/.git" ]]; then
|
|
489
|
+
cd "$INSTALL_DIR" || exit 1
|
|
490
|
+
git pull origin "$branch" 2>/dev/null || git pull origin main 2>/dev/null || git pull 2>/dev/null || {
|
|
491
|
+
printf "${YELLOW}Could not update, using existing installation${NC}\n"
|
|
492
|
+
}
|
|
493
|
+
else
|
|
494
|
+
install_via_tarball "$branch" || install_via_tarball "main" || {
|
|
495
|
+
printf "${YELLOW}Could not update, using existing installation${NC}\n"
|
|
496
|
+
}
|
|
497
|
+
fi
|
|
498
|
+
else
|
|
499
|
+
if [[ $has_git -eq 1 ]]; then
|
|
500
|
+
git clone --depth 1 -b "$branch" "$REPO_URL" "$INSTALL_DIR" 2>/dev/null || \
|
|
501
|
+
git clone --depth 1 "$REPO_URL" "$INSTALL_DIR" 2>/dev/null || {
|
|
502
|
+
# Git failed, try curl fallback
|
|
503
|
+
install_via_tarball "$branch" || install_via_tarball "main" || {
|
|
504
|
+
printf "${RED}Failed to install repository${NC}\n"
|
|
505
|
+
exit 1
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
else
|
|
509
|
+
install_via_tarball "$branch" || install_via_tarball "main" || {
|
|
510
|
+
printf "${RED}Failed to download repository${NC}\n"
|
|
511
|
+
exit 1
|
|
512
|
+
}
|
|
513
|
+
fi
|
|
514
|
+
fi
|
|
515
|
+
|
|
516
|
+
local version
|
|
517
|
+
version=$(get_installed_version)
|
|
518
|
+
printf "${GREEN}✓ Lacy installed to $INSTALL_DIR${NC}"
|
|
519
|
+
[[ -n "$version" ]] && printf " ${DIM}(v${version})${NC}"
|
|
520
|
+
printf "\n\n"
|
|
521
|
+
|
|
522
|
+
track_event "install" "curl"
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# Configure shell integration (multi-shell aware)
|
|
526
|
+
configure_shell() {
|
|
527
|
+
printf "${BLUE}Configuring ${DETECTED_SHELL} shell...${NC}\n"
|
|
528
|
+
|
|
529
|
+
local rc_file plugin_file plugin_line rc_name
|
|
530
|
+
|
|
531
|
+
rc_file=$(get_rc_file)
|
|
532
|
+
plugin_file=$(get_plugin_file)
|
|
533
|
+
rc_name=$(basename "$rc_file")
|
|
534
|
+
|
|
535
|
+
plugin_line="source ${INSTALL_DIR}/${plugin_file}"
|
|
536
|
+
|
|
537
|
+
# PATH line for the lacy CLI binary
|
|
538
|
+
local path_line="export PATH=\"${INSTALL_DIR}/bin:\$PATH\""
|
|
539
|
+
|
|
540
|
+
# Check if already configured
|
|
541
|
+
if [[ -f "$rc_file" ]] && grep -q "lacy.plugin" "$rc_file" 2>/dev/null; then
|
|
542
|
+
printf "${GREEN}✓ Already configured in ${rc_name}${NC}\n"
|
|
543
|
+
|
|
544
|
+
# Add PATH if missing (upgrade from older install)
|
|
545
|
+
if ! grep -q '\.lacy/bin' "$rc_file" 2>/dev/null; then
|
|
546
|
+
printf "%s\n" "$path_line" >> "$rc_file"
|
|
547
|
+
printf "${GREEN}✓ Added lacy CLI to PATH in ${rc_name}${NC}\n"
|
|
548
|
+
fi
|
|
549
|
+
else
|
|
550
|
+
# Ensure parent directory exists
|
|
551
|
+
mkdir -p "$(dirname "$rc_file")"
|
|
552
|
+
|
|
553
|
+
# Create RC file if it doesn't exist
|
|
554
|
+
[[ ! -f "$rc_file" ]] && touch "$rc_file"
|
|
555
|
+
|
|
556
|
+
# Add source line + PATH
|
|
557
|
+
{
|
|
558
|
+
printf "\n"
|
|
559
|
+
printf "# Lacy Shell\n"
|
|
560
|
+
printf "%s\n" "$plugin_line"
|
|
561
|
+
printf "%s\n" "$path_line"
|
|
562
|
+
} >> "$rc_file"
|
|
563
|
+
|
|
564
|
+
printf "${GREEN}✓ Added to ${rc_name}${NC}\n"
|
|
565
|
+
fi
|
|
566
|
+
|
|
567
|
+
# For Fish: also ensure the conf.d directory exists and use `source` syntax
|
|
568
|
+
if [[ "$DETECTED_SHELL" == "fish" ]]; then
|
|
569
|
+
mkdir -p "${HOME}/.config/fish/conf.d"
|
|
570
|
+
# Fish sources all *.fish files in conf.d automatically — create a loader
|
|
571
|
+
local fish_conf="${HOME}/.config/fish/conf.d/lacy.fish"
|
|
572
|
+
if [[ ! -f "$fish_conf" ]]; then
|
|
573
|
+
printf "source %s/lacy.plugin.fish\n" "${INSTALL_DIR}" > "$fish_conf"
|
|
574
|
+
printf "${GREEN}✓ Created %s${NC}\n" "$fish_conf"
|
|
575
|
+
fi
|
|
576
|
+
return
|
|
577
|
+
fi
|
|
578
|
+
|
|
579
|
+
# For Bash on macOS, also add to .bashrc if it exists (some terminals source it)
|
|
580
|
+
if [[ "$DETECTED_SHELL" == "bash" && "$OSTYPE" == "darwin"* ]]; then
|
|
581
|
+
local bashrc="${HOME}/.bashrc"
|
|
582
|
+
if [[ -f "$bashrc" ]] && ! grep -q "lacy.plugin" "$bashrc" 2>/dev/null; then
|
|
583
|
+
{
|
|
584
|
+
printf "\n"
|
|
585
|
+
printf "# Lacy Shell\n"
|
|
586
|
+
printf "%s\n" "$plugin_line"
|
|
587
|
+
printf "%s\n" "$path_line"
|
|
588
|
+
} >> "$bashrc"
|
|
589
|
+
printf "${GREEN}✓ Also added to .bashrc${NC}\n"
|
|
590
|
+
fi
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
printf "\n"
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
# Backward compat alias
|
|
597
|
+
configure_zsh() { configure_shell; }
|
|
598
|
+
|
|
599
|
+
# Parse a simple YAML value (strips inline comments and quotes)
|
|
600
|
+
_yaml_value() {
|
|
601
|
+
local file="$1" key="$2"
|
|
602
|
+
grep "^[[:space:]]*${key}:" "$file" 2>/dev/null | head -1 | sed 's/^[^:]*:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d '"' | tr -d "'"
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
# Write a YAML value in-place
|
|
606
|
+
_yaml_write() {
|
|
607
|
+
local file="$1" key="$2" value="$3"
|
|
608
|
+
local escaped_value="${value//\\/\\\\}"
|
|
609
|
+
escaped_value="${escaped_value//|/\\|}"
|
|
610
|
+
escaped_value="${escaped_value//&/\\&}"
|
|
611
|
+
if grep -q "^[[:space:]]*${key}:" "$file" 2>/dev/null; then
|
|
612
|
+
sed -i.bak "s|^\\([[:space:]]*${key}:\\).*|\\1 ${escaped_value}|" "$file"
|
|
613
|
+
rm -f "${file}.bak"
|
|
614
|
+
fi
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
# Create configuration with selected tool (preserves existing config)
|
|
618
|
+
create_config() {
|
|
619
|
+
mkdir -p "$INSTALL_DIR"
|
|
620
|
+
|
|
621
|
+
# Determine active tool value for config
|
|
622
|
+
local active_tool_value=""
|
|
623
|
+
if [[ -n "$SELECTED_TOOL" && "$SELECTED_TOOL" != "none" ]]; then
|
|
624
|
+
active_tool_value="$SELECTED_TOOL"
|
|
625
|
+
fi
|
|
626
|
+
|
|
627
|
+
# If config already exists, update the tool selection only
|
|
628
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
629
|
+
printf "${BLUE}Updating configuration...${NC}\n"
|
|
630
|
+
if [[ -n "$active_tool_value" ]]; then
|
|
631
|
+
_yaml_write "$CONFIG_FILE" "active" "$active_tool_value"
|
|
632
|
+
if [[ "$SELECTED_TOOL" == "custom" && -n "$CUSTOM_COMMAND" ]]; then
|
|
633
|
+
_yaml_write "$CONFIG_FILE" "custom_command" "\"$CUSTOM_COMMAND\""
|
|
634
|
+
fi
|
|
635
|
+
fi
|
|
636
|
+
printf "${GREEN}✓ Configuration preserved at $CONFIG_FILE${NC}\n"
|
|
637
|
+
printf "\n"
|
|
638
|
+
return
|
|
639
|
+
fi
|
|
640
|
+
|
|
641
|
+
# Fresh config
|
|
642
|
+
printf "${BLUE}Creating configuration...${NC}\n"
|
|
643
|
+
|
|
644
|
+
# Build custom_command line
|
|
645
|
+
local custom_command_line=" # custom_command: \"your-command -flags\""
|
|
646
|
+
if [[ "$SELECTED_TOOL" == "custom" && -n "$CUSTOM_COMMAND" ]]; then
|
|
647
|
+
custom_command_line=" custom_command: \"$CUSTOM_COMMAND\""
|
|
648
|
+
fi
|
|
649
|
+
|
|
650
|
+
cat > "$CONFIG_FILE" << EOF
|
|
651
|
+
# Lacy Shell Configuration
|
|
652
|
+
# https://github.com/lacymorrow/lacy
|
|
653
|
+
|
|
654
|
+
# AI CLI tool selection
|
|
655
|
+
# Options: lash, claude, opencode, gemini, codex, custom, or empty for auto-detect
|
|
656
|
+
agent_tools:
|
|
657
|
+
active: $active_tool_value
|
|
658
|
+
$custom_command_line
|
|
659
|
+
|
|
660
|
+
# API Keys (optional - only needed if no CLI tool is installed)
|
|
661
|
+
api_keys:
|
|
662
|
+
# openai: "your-key-here"
|
|
663
|
+
# anthropic: "your-key-here"
|
|
664
|
+
|
|
665
|
+
# Operating modes
|
|
666
|
+
modes:
|
|
667
|
+
default: auto # Options: shell, agent, auto
|
|
668
|
+
|
|
669
|
+
# Smart auto-detection settings
|
|
670
|
+
auto_detection:
|
|
671
|
+
enabled: true
|
|
672
|
+
confidence_threshold: 0.7
|
|
673
|
+
EOF
|
|
674
|
+
|
|
675
|
+
printf "${GREEN}✓ Configuration created at $CONFIG_FILE${NC}\n"
|
|
676
|
+
printf "\n"
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
# Show success message
|
|
680
|
+
show_success() {
|
|
681
|
+
local version
|
|
682
|
+
version=$(get_installed_version)
|
|
683
|
+
printf "${GREEN}${BOLD}Installation complete!${NC}"
|
|
684
|
+
[[ -n "$version" ]] && printf " ${DIM}v${version}${NC}"
|
|
685
|
+
printf "\n"
|
|
686
|
+
printf "\n"
|
|
687
|
+
printf "${BOLD}Try it:${NC}\n"
|
|
688
|
+
printf " ${CYAN}what files are here${NC} ${DIM}→ AI answers${NC}\n"
|
|
689
|
+
printf " ${CYAN}ls -la${NC} ${DIM}→ runs in shell${NC}\n"
|
|
690
|
+
printf "\n"
|
|
691
|
+
printf "${BOLD}Commands:${NC}\n"
|
|
692
|
+
printf " ${CYAN}mode${NC} Show/change mode (shell/agent/auto)\n"
|
|
693
|
+
printf " ${CYAN}tool${NC} Show/change AI tool\n"
|
|
694
|
+
printf " ${CYAN}ask \"query\"${NC} Direct query to AI\n"
|
|
695
|
+
printf " ${CYAN}Ctrl+Space${NC} Toggle between modes\n"
|
|
696
|
+
printf "\n"
|
|
697
|
+
printf "${BOLD}Visual feedback:${NC}\n"
|
|
698
|
+
printf " ${GREEN}▌${NC} Green = will run in shell\n"
|
|
699
|
+
printf " ${MAGENTA}▌${NC} Magenta = will go to AI\n"
|
|
700
|
+
printf "\n"
|
|
701
|
+
|
|
702
|
+
if [[ "$SELECTED_TOOL" == "none" ]] || { [[ -z "$SELECTED_TOOL" ]] && ! command -v lash >/dev/null 2>&1 && ! command -v claude >/dev/null 2>&1; }; then
|
|
703
|
+
printf "${YELLOW}Remember to install an AI CLI tool:${NC}\n"
|
|
704
|
+
printf " npm install -g lashcode # or\n"
|
|
705
|
+
printf " brew install claude\n"
|
|
706
|
+
printf "\n"
|
|
707
|
+
fi
|
|
708
|
+
|
|
709
|
+
printf "${DIM}Learn more: https://github.com/lacymorrow/lacy${NC}\n"
|
|
710
|
+
printf "\n"
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
# Restart shell to apply changes
|
|
714
|
+
restart_shell() {
|
|
715
|
+
# Only prompt if /dev/tty is actually usable (not just that it exists)
|
|
716
|
+
local restart=""
|
|
717
|
+
if [[ -t 0 ]]; then
|
|
718
|
+
printf "\n"
|
|
719
|
+
read -p "Restart shell now to apply changes? [Y/n]: " restart
|
|
720
|
+
elif { true < /dev/tty; } 2>/dev/null; then
|
|
721
|
+
printf "\n"
|
|
722
|
+
read -p "Restart shell now to apply changes? [Y/n]: " restart < /dev/tty
|
|
723
|
+
else
|
|
724
|
+
printf "\nRestart your terminal to apply changes.\n"
|
|
725
|
+
return 0
|
|
726
|
+
fi
|
|
727
|
+
|
|
728
|
+
if [[ ! "$restart" =~ ^[Nn]$ ]]; then
|
|
729
|
+
printf "${BLUE}Restarting shell...${NC}\n"
|
|
730
|
+
local restart_cmd
|
|
731
|
+
restart_cmd=$(get_shell_restart_cmd)
|
|
732
|
+
exec $restart_cmd
|
|
733
|
+
else
|
|
734
|
+
printf "\n"
|
|
735
|
+
printf "Run ${CYAN}$(get_source_hint)${NC} or restart your terminal to apply changes.\n"
|
|
736
|
+
fi
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
# Remove lacy lines from an RC file
|
|
740
|
+
_remove_from_rc() {
|
|
741
|
+
local rc_file="$1"
|
|
742
|
+
local rc_name
|
|
743
|
+
rc_name=$(basename "$rc_file")
|
|
744
|
+
if [[ -f "$rc_file" ]]; then
|
|
745
|
+
if grep -q "lacy.plugin" "$rc_file" 2>/dev/null; then
|
|
746
|
+
printf "${BLUE}Removing from ${rc_name}...${NC}\n"
|
|
747
|
+
local tmp_file
|
|
748
|
+
tmp_file=$(mktemp)
|
|
749
|
+
grep -v "lacy.plugin" "$rc_file" | grep -v "# Lacy Shell" | grep -v '\.lacy/bin' > "$tmp_file" || true
|
|
750
|
+
mv "$tmp_file" "$rc_file"
|
|
751
|
+
printf " ${GREEN}✓${NC} Removed from ${rc_name}\n"
|
|
752
|
+
fi
|
|
753
|
+
fi
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
# Uninstall function
|
|
757
|
+
do_uninstall() {
|
|
758
|
+
printf "${BLUE}Uninstalling Lacy Shell...${NC}\n"
|
|
759
|
+
printf "\n"
|
|
760
|
+
|
|
761
|
+
# Check if installed (directory, symlink, or old install path)
|
|
762
|
+
if [[ ! -d "$INSTALL_DIR" ]] && [[ ! -L "$INSTALL_DIR" ]] && [[ ! -d "${HOME}/.lacy-shell" ]]; then
|
|
763
|
+
printf "${YELLOW}Lacy Shell is not installed${NC}\n"
|
|
764
|
+
exit 0
|
|
765
|
+
fi
|
|
766
|
+
|
|
767
|
+
# Remove from all possible RC files
|
|
768
|
+
_remove_from_rc "${HOME}/.zshrc"
|
|
769
|
+
_remove_from_rc "${HOME}/.bashrc"
|
|
770
|
+
_remove_from_rc "${HOME}/.bash_profile"
|
|
771
|
+
_remove_from_rc "${HOME}/.config/fish/conf.d/lacy.fish"
|
|
772
|
+
|
|
773
|
+
# Detect Homebrew-managed install (symlink to Homebrew prefix)
|
|
774
|
+
local is_brew=false
|
|
775
|
+
if [[ -L "$INSTALL_DIR" ]]; then
|
|
776
|
+
local link_target
|
|
777
|
+
link_target=$(readlink "$INSTALL_DIR" 2>/dev/null || true)
|
|
778
|
+
if [[ "$link_target" == *"/Cellar/"* || "$link_target" == *"/homebrew/"* ]]; then
|
|
779
|
+
is_brew=true
|
|
780
|
+
fi
|
|
781
|
+
fi
|
|
782
|
+
|
|
783
|
+
# Remove installation directories
|
|
784
|
+
if [[ -L "$INSTALL_DIR" ]]; then
|
|
785
|
+
printf "${BLUE}Removing $INSTALL_DIR symlink...${NC}\n"
|
|
786
|
+
rm -f "$INSTALL_DIR"
|
|
787
|
+
printf " ${GREEN}✓${NC} Removed\n"
|
|
788
|
+
elif [[ -d "$INSTALL_DIR" ]]; then
|
|
789
|
+
printf "${BLUE}Removing $INSTALL_DIR...${NC}\n"
|
|
790
|
+
rm -rf "$INSTALL_DIR"
|
|
791
|
+
printf " ${GREEN}✓${NC} Removed\n"
|
|
792
|
+
fi
|
|
793
|
+
if [[ -d "${HOME}/.lacy-shell" ]]; then
|
|
794
|
+
printf "${BLUE}Removing ${HOME}/.lacy-shell...${NC}\n"
|
|
795
|
+
rm -rf "${HOME}/.lacy-shell"
|
|
796
|
+
printf " ${GREEN}✓${NC} Removed\n"
|
|
797
|
+
fi
|
|
798
|
+
|
|
799
|
+
# If installed via Homebrew, uninstall the formula too
|
|
800
|
+
if [[ "$is_brew" == true ]] && command -v brew >/dev/null 2>&1; then
|
|
801
|
+
printf "${BLUE}Removing Homebrew formula...${NC}\n"
|
|
802
|
+
brew uninstall lacymorrow/tap/lacy 2>/dev/null && printf " ${GREEN}✓${NC} Homebrew formula removed\n" || true
|
|
803
|
+
fi
|
|
804
|
+
|
|
805
|
+
printf "\n"
|
|
806
|
+
printf "${GREEN}Lacy Shell uninstalled${NC}\n"
|
|
807
|
+
|
|
808
|
+
track_event "uninstall" "curl"
|
|
809
|
+
|
|
810
|
+
# Restart shell (reuse the safe TTY-aware function)
|
|
811
|
+
detect_user_shell
|
|
812
|
+
restart_shell
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
# Check if already installed and show menu
|
|
816
|
+
check_existing_installation() {
|
|
817
|
+
if [[ -d "$INSTALL_DIR" ]]; then
|
|
818
|
+
print_banner
|
|
819
|
+
printf "${YELLOW}Lacy Shell is already installed.${NC}\n"
|
|
820
|
+
printf "\n"
|
|
821
|
+
printf "What would you like to do?\n"
|
|
822
|
+
printf "\n"
|
|
823
|
+
printf " 1) Update ${DIM}- pull latest changes${NC}\n"
|
|
824
|
+
printf " 2) Reinstall ${DIM}- fresh installation${NC}\n"
|
|
825
|
+
printf " 3) Uninstall ${DIM}- remove Lacy Shell${NC}\n"
|
|
826
|
+
printf " 4) Cancel\n"
|
|
827
|
+
printf "\n"
|
|
828
|
+
|
|
829
|
+
local choice
|
|
830
|
+
read -p "Select [1-4]: " choice < /dev/tty 2>/dev/null || choice="4"
|
|
831
|
+
|
|
832
|
+
case "$choice" in
|
|
833
|
+
1)
|
|
834
|
+
printf "\n"
|
|
835
|
+
printf "${BLUE}Updating Lacy...${NC}\n"
|
|
836
|
+
cd "$INSTALL_DIR" 2>/dev/null || {
|
|
837
|
+
printf "${RED}Could not find installation directory${NC}\n"
|
|
838
|
+
exit 1
|
|
839
|
+
}
|
|
840
|
+
local update_ok=0
|
|
841
|
+
if command -v git >/dev/null 2>&1 && [[ -d "$INSTALL_DIR/.git" ]]; then
|
|
842
|
+
git pull origin main 2>/dev/null || git pull 2>/dev/null && update_ok=1
|
|
843
|
+
else
|
|
844
|
+
install_via_tarball "main" && update_ok=1
|
|
845
|
+
fi
|
|
846
|
+
if [[ $update_ok -eq 1 ]]; then
|
|
847
|
+
local updated_version
|
|
848
|
+
updated_version=$(get_installed_version)
|
|
849
|
+
printf "${GREEN}✓ Lacy updated${NC}"
|
|
850
|
+
[[ -n "$updated_version" ]] && printf " ${DIM}(v${updated_version})${NC}"
|
|
851
|
+
printf "\n"
|
|
852
|
+
track_event "update" "curl"
|
|
853
|
+
restart_shell
|
|
854
|
+
else
|
|
855
|
+
printf "${RED}Update failed. Try reinstalling.${NC}\n"
|
|
856
|
+
fi
|
|
857
|
+
exit 0
|
|
858
|
+
;;
|
|
859
|
+
2)
|
|
860
|
+
printf "\n"
|
|
861
|
+
printf "${BLUE}Removing existing installation...${NC}\n"
|
|
862
|
+
# Backup user config before removing
|
|
863
|
+
local config_backup=""
|
|
864
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
865
|
+
config_backup=$(mktemp)
|
|
866
|
+
cp "$CONFIG_FILE" "$config_backup"
|
|
867
|
+
fi
|
|
868
|
+
rm -rf "$INSTALL_DIR" 2>/dev/null
|
|
869
|
+
# Restore config so create_config() sees it and preserves it
|
|
870
|
+
if [[ -n "$config_backup" ]]; then
|
|
871
|
+
mkdir -p "$INSTALL_DIR"
|
|
872
|
+
cp "$config_backup" "$CONFIG_FILE"
|
|
873
|
+
rm -f "$config_backup"
|
|
874
|
+
fi
|
|
875
|
+
printf "${GREEN}✓ Removed${NC}\n"
|
|
876
|
+
printf "\n"
|
|
877
|
+
# Continue with fresh install
|
|
878
|
+
return 0
|
|
879
|
+
;;
|
|
880
|
+
3)
|
|
881
|
+
do_uninstall
|
|
882
|
+
exit 0
|
|
883
|
+
;;
|
|
884
|
+
4|*)
|
|
885
|
+
printf "\n"
|
|
886
|
+
printf "Cancelled.\n"
|
|
887
|
+
exit 0
|
|
888
|
+
;;
|
|
889
|
+
esac
|
|
890
|
+
fi
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
# Main installation flow (bash)
|
|
894
|
+
main_bash() {
|
|
895
|
+
print_banner
|
|
896
|
+
detect_user_shell
|
|
897
|
+
printf "${DIM}Detected shell: ${DETECTED_SHELL}${NC}\n\n"
|
|
898
|
+
check_prerequisites
|
|
899
|
+
detect_tools
|
|
900
|
+
select_tool
|
|
901
|
+
install_plugin
|
|
902
|
+
configure_shell
|
|
903
|
+
create_config
|
|
904
|
+
show_success
|
|
905
|
+
restart_shell
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
# Main entry point
|
|
909
|
+
main() {
|
|
910
|
+
# Try Node installer first (better UX) — it handles existing installations too
|
|
911
|
+
if use_node_installer && run_node_installer; then
|
|
912
|
+
return
|
|
913
|
+
fi
|
|
914
|
+
|
|
915
|
+
# Bash installer (fallback)
|
|
916
|
+
# Check for existing installation first (interactive menu)
|
|
917
|
+
if [[ -t 0 ]] || [[ -c /dev/tty ]]; then
|
|
918
|
+
check_existing_installation
|
|
919
|
+
fi
|
|
920
|
+
|
|
921
|
+
main_bash
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
# Parse flags that can appear anywhere (--beta, --channel)
|
|
925
|
+
_args=()
|
|
926
|
+
while [[ $# -gt 0 ]]; do
|
|
927
|
+
case "$1" in
|
|
928
|
+
--beta)
|
|
929
|
+
LACY_CHANNEL="beta"
|
|
930
|
+
shift
|
|
931
|
+
;;
|
|
932
|
+
--channel)
|
|
933
|
+
LACY_CHANNEL="${2:?--channel requires a value}"
|
|
934
|
+
shift 2
|
|
935
|
+
;;
|
|
936
|
+
*)
|
|
937
|
+
_args+=("$1")
|
|
938
|
+
shift
|
|
939
|
+
;;
|
|
940
|
+
esac
|
|
941
|
+
done
|
|
942
|
+
set -- "${_args[@]}"
|
|
943
|
+
|
|
944
|
+
# Handle command line arguments
|
|
945
|
+
case "${1:-}" in
|
|
946
|
+
"--help"|"-h")
|
|
947
|
+
printf "Lacy Shell Installation Script\n"
|
|
948
|
+
printf "\n"
|
|
949
|
+
printf "Usage: $0 [options]\n"
|
|
950
|
+
printf "\n"
|
|
951
|
+
printf "Options:\n"
|
|
952
|
+
printf " --help Show this help message\n"
|
|
953
|
+
printf " --uninstall Uninstall Lacy Shell\n"
|
|
954
|
+
printf " --bash Force bash installer (skip Node)\n"
|
|
955
|
+
printf " --shell X Force shell type (zsh, bash)\n"
|
|
956
|
+
printf " --tool X Pre-select tool (lash, claude, opencode, gemini, codex, custom, auto)\n"
|
|
957
|
+
printf " --beta Use beta release channel\n"
|
|
958
|
+
printf " --channel X Use a named release channel (beta, rc, etc.)\n"
|
|
959
|
+
printf "\n"
|
|
960
|
+
printf "Examples:\n"
|
|
961
|
+
printf " curl -fsSL https://lacy.sh/install | bash\n"
|
|
962
|
+
printf " curl -fsSL https://lacy.sh/install/beta | bash\n"
|
|
963
|
+
printf " curl -fsSL https://lacy.sh/install | bash -s -- --beta\n"
|
|
964
|
+
printf " curl -fsSL https://lacy.sh/install | bash -s -- --uninstall\n"
|
|
965
|
+
printf " curl -fsSL https://lacy.sh/install | bash -s -- --tool claude\n"
|
|
966
|
+
printf " curl -fsSL https://lacy.sh/install | bash -s -- --tool custom \"claude -p\"\n"
|
|
967
|
+
printf " npx lacy\n"
|
|
968
|
+
printf " npx lacy --uninstall\n"
|
|
969
|
+
exit 0
|
|
970
|
+
;;
|
|
971
|
+
"--uninstall"|"-u")
|
|
972
|
+
do_uninstall
|
|
973
|
+
;;
|
|
974
|
+
"--bash")
|
|
975
|
+
LACY_FORCE_BASH=1
|
|
976
|
+
shift
|
|
977
|
+
main "$@"
|
|
978
|
+
;;
|
|
979
|
+
"--shell")
|
|
980
|
+
LACY_FORCE_SHELL="$2"
|
|
981
|
+
shift 2
|
|
982
|
+
main "$@"
|
|
983
|
+
;;
|
|
984
|
+
"--tool")
|
|
985
|
+
SELECTED_TOOL="$2"
|
|
986
|
+
if [[ "$SELECTED_TOOL" == "custom" ]]; then
|
|
987
|
+
CUSTOM_COMMAND="$3"
|
|
988
|
+
if [[ -z "$CUSTOM_COMMAND" ]]; then
|
|
989
|
+
printf "${RED}Error: --tool custom requires a command string.${NC}\n"
|
|
990
|
+
printf "Usage: $0 --tool custom \"command -flags\"\n"
|
|
991
|
+
exit 1
|
|
992
|
+
fi
|
|
993
|
+
shift 3
|
|
994
|
+
else
|
|
995
|
+
shift 2
|
|
996
|
+
fi
|
|
997
|
+
print_banner
|
|
998
|
+
detect_user_shell
|
|
999
|
+
check_prerequisites
|
|
1000
|
+
install_plugin
|
|
1001
|
+
configure_shell
|
|
1002
|
+
create_config
|
|
1003
|
+
show_success
|
|
1004
|
+
restart_shell
|
|
1005
|
+
;;
|
|
1006
|
+
*)
|
|
1007
|
+
main "$@"
|
|
1008
|
+
;;
|
|
1009
|
+
esac
|