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.
Files changed (109) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.github/FUNDING.yml +3 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
  7. package/.github/SECURITY.md +32 -0
  8. package/.github/assets/logo-horizontal-dark.png +0 -0
  9. package/.github/assets/logo-horizontal-dark.svg +17 -0
  10. package/.github/assets/logo-horizontal.png +0 -0
  11. package/.github/assets/logo-horizontal.svg +17 -0
  12. package/.github/assets/logo.png +0 -0
  13. package/.github/assets/logo.svg +12 -0
  14. package/.github/assets/social-preview.png +0 -0
  15. package/.github/assets/social-preview.svg +50 -0
  16. package/.github/dependabot.yml +21 -0
  17. package/.github/workflows/ci.yml +80 -0
  18. package/.github/workflows/dependabot-auto-merge.yml +32 -0
  19. package/CHANGELOG.md +366 -0
  20. package/CLAUDE.md +340 -0
  21. package/CONTRIBUTING.md +141 -0
  22. package/LICENSE +110 -0
  23. package/README.md +201 -31
  24. package/RELEASING.md +148 -0
  25. package/STYLE.md +202 -0
  26. package/assets/hero.jpeg +0 -0
  27. package/assets/mode-indicators.jpeg +0 -0
  28. package/assets/real-time-indicator.jpeg +0 -0
  29. package/assets/supported-tools.jpeg +0 -0
  30. package/bin/lacy +1028 -0
  31. package/docs/ADDING-BACKENDS.md +124 -0
  32. package/docs/DEVTO-ARTICLE.md +94 -0
  33. package/docs/DOCS.md +68 -0
  34. package/docs/GROWTH-STRATEGY.md +119 -0
  35. package/docs/HN-RESPONSES.md +122 -0
  36. package/docs/LAUNCH-COPY-FINAL.md +105 -0
  37. package/docs/MARKETING.md +411 -0
  38. package/docs/NATURAL_LANGUAGE_DETECTION.md +204 -0
  39. package/docs/UGC_VIDEO_SCRIPT.md +114 -0
  40. package/docs/articles/devto-how-i-made-my-terminal-understand-english.md +117 -0
  41. package/docs/demo-color-transition.gif +0 -0
  42. package/docs/demo-full.gif +0 -0
  43. package/docs/demo-indicator.gif +0 -0
  44. package/docs/launch-thread-may6.sh +158 -0
  45. package/docs/videos/README.md +189 -0
  46. package/docs/videos/generate_frames.py +510 -0
  47. package/docs/videos/generate_frames_v2.py +729 -0
  48. package/docs/videos/generate_short.py +328 -0
  49. package/docs/videos/generate_short_v2.py +526 -0
  50. package/docs/videos/lacy-shell-demo-v2.mp4 +0 -0
  51. package/docs/videos/lacy-shell-demo.mp4 +0 -0
  52. package/docs/videos/lacy-shell-short-v2.mp4 +0 -0
  53. package/docs/videos/lacy-shell-short.mp4 +0 -0
  54. package/install.sh +1009 -0
  55. package/lacy.plugin.bash +75 -0
  56. package/lacy.plugin.fish +43 -0
  57. package/lacy.plugin.zsh +65 -0
  58. package/lib/animations.zsh +3 -0
  59. package/lib/bash/completions.bash +40 -0
  60. package/lib/bash/execute.bash +233 -0
  61. package/lib/bash/init.bash +40 -0
  62. package/lib/bash/keybindings.bash +134 -0
  63. package/lib/bash/prompt.bash +85 -0
  64. package/lib/commands/info.sh +25 -0
  65. package/lib/config.zsh +3 -0
  66. package/lib/constants.zsh +3 -0
  67. package/lib/core/animations.sh +271 -0
  68. package/lib/core/commands.sh +297 -0
  69. package/lib/core/config.sh +340 -0
  70. package/lib/core/constants.sh +366 -0
  71. package/lib/core/context.sh +260 -0
  72. package/lib/core/detection.sh +417 -0
  73. package/lib/core/mcp.sh +741 -0
  74. package/lib/core/modes.sh +123 -0
  75. package/lib/core/preheat.sh +496 -0
  76. package/lib/core/spinner.sh +174 -0
  77. package/lib/core/telemetry.sh +99 -0
  78. package/lib/detection.zsh +3 -0
  79. package/lib/execute.zsh +3 -0
  80. package/lib/fish/config.fish +66 -0
  81. package/lib/fish/detection.fish +90 -0
  82. package/lib/fish/execute.fish +105 -0
  83. package/lib/fish/keybindings.fish +42 -0
  84. package/lib/fish/prompt.fish +30 -0
  85. package/lib/keybindings.zsh +3 -0
  86. package/lib/mcp.zsh +3 -0
  87. package/lib/modes.zsh +3 -0
  88. package/lib/preheat.zsh +3 -0
  89. package/lib/prompt.zsh +3 -0
  90. package/lib/spinner.zsh +3 -0
  91. package/lib/zsh/completions.zsh +60 -0
  92. package/lib/zsh/execute.zsh +294 -0
  93. package/lib/zsh/init.zsh +26 -0
  94. package/lib/zsh/keybindings.zsh +551 -0
  95. package/lib/zsh/prompt.zsh +90 -0
  96. package/package.json +42 -27
  97. package/packages/lacy/README.md +61 -0
  98. package/packages/lacy/commands/info.sh +25 -0
  99. package/{index.mjs → packages/lacy/index.mjs} +247 -20
  100. package/packages/lacy/package-lock.json +71 -0
  101. package/packages/lacy/package.json +42 -0
  102. package/script/release.ts +487 -0
  103. package/squirrel.toml +36 -0
  104. package/tests/test_bash.bash +163 -0
  105. package/tests/test_core.sh +607 -0
  106. package/tests/test_gemini.sh +119 -0
  107. package/tests/test_gemini_mcp.sh +126 -0
  108. package/tests/test_preheat_server.zsh +446 -0
  109. 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