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/bin/lacy ADDED
@@ -0,0 +1,1028 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Lacy Shell CLI
4
+ # https://github.com/lacymorrow/lacy
5
+ #
6
+ # Usage: lacy [command] [options]
7
+ #
8
+ # Set LACY_NO_NODE=1 to force bash-only mode (skip fancy Node UI)
9
+
10
+ set -e
11
+
12
+ VERSION_FALLBACK="1.8.11"
13
+ INSTALL_DIR="${HOME}/.lacy"
14
+ INSTALL_DIR_OLD="${HOME}/.lacy-shell"
15
+ CONFIG_FILE="${INSTALL_DIR}/config.yaml"
16
+ REPO_URL="https://github.com/lacymorrow/lacy.git"
17
+
18
+ # Colors (ANSI-C quoting so escape bytes are real, not literal)
19
+ RED=$'\033[0;31m'
20
+ GREEN=$'\033[0;32m'
21
+ YELLOW=$'\033[1;33m'
22
+ BLUE=$'\033[0;34m'
23
+ MAGENTA=$'\033[0;35m'
24
+ CYAN=$'\033[0;36m'
25
+ BOLD=$'\033[1m'
26
+ DIM=$'\033[2m'
27
+ NC=$'\033[0m'
28
+
29
+ # ============================================================================
30
+ # Version — single source of truth is package.json
31
+ # ============================================================================
32
+
33
+ get_version() {
34
+ local pkg_file="${INSTALL_DIR}/package.json"
35
+ if [[ -f "$pkg_file" ]]; then
36
+ # Extract "version": "x.y.z" without jq (pure bash/grep/sed)
37
+ grep '"version"' "$pkg_file" 2>/dev/null | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"//' | sed 's/".*//'
38
+ fi
39
+ }
40
+
41
+ VERSION="$(get_version)"
42
+ VERSION="${VERSION:-$VERSION_FALLBACK}"
43
+
44
+ # ============================================================================
45
+ # Helpers
46
+ # ============================================================================
47
+
48
+ die() { printf "${RED}Error: %s${NC}\n" "$1" >&2; exit 1; }
49
+
50
+ info() { printf "${BLUE}%s${NC}\n" "$1"; }
51
+
52
+ success() { printf "${GREEN}✓${NC} %s\n" "$1"; }
53
+
54
+ warn() { printf "${YELLOW}%s${NC}\n" "$1"; }
55
+
56
+ is_installed() {
57
+ [[ -d "$INSTALL_DIR" ]] || [[ -L "$INSTALL_DIR" ]] || [[ -d "$INSTALL_DIR_OLD" ]]
58
+ }
59
+
60
+ command_exists() {
61
+ command -v "$1" >/dev/null 2>&1
62
+ }
63
+
64
+ detect_shell() {
65
+ local shell
66
+ shell=$(basename "${SHELL:-}")
67
+ case "$shell" in
68
+ zsh|bash) echo "$shell" ;;
69
+ *)
70
+ # Detect from the running process if $SHELL is unhelpful
71
+ if [[ -n "${BASH_VERSION:-}" ]]; then
72
+ echo "bash"
73
+ elif [[ -n "${ZSH_VERSION:-}" ]]; then
74
+ echo "zsh"
75
+ elif command_exists bash; then
76
+ echo "bash"
77
+ elif command_exists zsh; then
78
+ echo "zsh"
79
+ else
80
+ echo "bash"
81
+ fi
82
+ ;;
83
+ esac
84
+ }
85
+
86
+ update_via_tarball() {
87
+ local dir="$1"
88
+ local tarball_url="https://github.com/lacymorrow/lacy/archive/refs/heads/main.tar.gz"
89
+ local tmp_file
90
+ tmp_file=$(mktemp)
91
+ curl -fsSL "$tarball_url" -o "$tmp_file" 2>/dev/null || { rm -f "$tmp_file"; return 1; }
92
+ # Extract over existing directory
93
+ tar xzf "$tmp_file" --strip-components=1 -C "$dir" 2>/dev/null || { rm -f "$tmp_file"; return 1; }
94
+ rm -f "$tmp_file"
95
+ return 0
96
+ }
97
+
98
+ get_rc_file() {
99
+ case "$(detect_shell)" in
100
+ bash)
101
+ if [[ "$OSTYPE" == "darwin"* ]]; then
102
+ echo "${HOME}/.bash_profile"
103
+ else
104
+ echo "${HOME}/.bashrc"
105
+ fi
106
+ ;;
107
+ *) echo "${HOME}/.zshrc" ;;
108
+ esac
109
+ }
110
+
111
+ get_plugin_file() {
112
+ case "$(detect_shell)" in
113
+ bash) echo "lacy.plugin.bash" ;;
114
+ *) echo "lacy.plugin.zsh" ;;
115
+ esac
116
+ }
117
+
118
+ # Parse a simple YAML value (strips inline comments and quotes)
119
+ yaml_value() {
120
+ local file="$1" key="$2"
121
+ grep "^[[:space:]]*${key}:" "$file" 2>/dev/null | head -1 | sed 's/^[^:]*:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d '"' | tr -d "'"
122
+ }
123
+
124
+ # Write a YAML value (replace in-place within a section)
125
+ yaml_write() {
126
+ local file="$1" key="$2" value="$3"
127
+ # Escape sed special chars in value (delimiter is |)
128
+ local escaped_value="${value//\\/\\\\}"
129
+ escaped_value="${escaped_value//|/\\|}"
130
+ escaped_value="${escaped_value//&/\\&}"
131
+ if grep -q "^[[:space:]]*${key}:" "$file" 2>/dev/null; then
132
+ # Replace existing key — handles both "key: value" and "key:" (empty)
133
+ sed -i.bak "s|^\\([[:space:]]*${key}:\\).*|\\1 ${escaped_value}|" "$file"
134
+ rm -f "${file}.bak"
135
+ fi
136
+ }
137
+
138
+ # Try Node CLI for rich interactive experience
139
+ # Falls back silently if Node unavailable or LACY_NO_NODE=1
140
+ # Respects LACY_CHANNEL (default: latest) for beta testing
141
+ try_node() {
142
+ [[ "${LACY_NO_NODE:-}" == "1" ]] && return 1
143
+ command_exists npx && [[ -t 0 ]] || return 1
144
+ local pkg="lacy@${LACY_CHANNEL:-latest}"
145
+ npx --yes "$pkg" "$@" && exit 0
146
+ return 1
147
+ }
148
+
149
+ # Offer to restart the shell
150
+ offer_restart() {
151
+ if [[ -t 0 ]]; then
152
+ printf "\nRestart shell now to apply changes? [Y/n]: "
153
+ read -r restart
154
+ if [[ ! "$restart" =~ ^[Nn]$ ]]; then
155
+ local shell_cmd
156
+ shell_cmd=$(detect_shell)
157
+ info "Restarting ${shell_cmd}..."
158
+ exec "$shell_cmd" -l
159
+ fi
160
+ fi
161
+ }
162
+
163
+ # All RC files (for cleanup)
164
+ ALL_RC_FILES=(
165
+ "${HOME}/.zshrc"
166
+ "${HOME}/.bashrc"
167
+ "${HOME}/.bash_profile"
168
+ "${HOME}/.config/fish/conf.d/lacy.fish"
169
+ )
170
+
171
+ # ============================================================================
172
+ # Commands
173
+ # ============================================================================
174
+
175
+ cmd_install() {
176
+ # Try fancy Node installer first
177
+ try_node "$@" || true
178
+
179
+ # Fallback: run install.sh from repo or curl
180
+ if [[ -f "${INSTALL_DIR}/install.sh" ]]; then
181
+ exec bash "${INSTALL_DIR}/install.sh" "$@"
182
+ fi
183
+
184
+ info "Downloading installer..."
185
+ exec bash <(curl -fsSL https://lacy.sh/install) "$@"
186
+ }
187
+
188
+ cmd_uninstall() {
189
+ if ! is_installed; then
190
+ warn "Lacy Shell is not installed"
191
+ exit 0
192
+ fi
193
+
194
+ # Try fancy Node uninstaller first
195
+ try_node --uninstall || true
196
+
197
+ # Bash fallback
198
+ if [[ -t 0 ]]; then
199
+ printf "Are you sure you want to uninstall Lacy Shell? [y/N]: "
200
+ read -r confirm
201
+ if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
202
+ echo "Cancelled."
203
+ exit 0
204
+ fi
205
+ fi
206
+
207
+ info "Uninstalling Lacy Shell..."
208
+
209
+ # Remove from all RC files
210
+ for rc in "${ALL_RC_FILES[@]}"; do
211
+ if [[ -f "$rc" ]] && grep -q "lacy.plugin" "$rc" 2>/dev/null; then
212
+ local tmp
213
+ tmp=$(mktemp)
214
+ grep -v "lacy.plugin" "$rc" | grep -v "# Lacy Shell" > "$tmp" || true
215
+ grep -v '\.lacy/bin' "$tmp" > "${tmp}.2" || true
216
+ mv "${tmp}.2" "$rc"
217
+ rm -f "$tmp"
218
+ success "Cleaned $(basename "$rc")"
219
+ fi
220
+ done
221
+
222
+ # Detect Homebrew-managed install (symlink to Homebrew prefix)
223
+ local is_brew=false
224
+ if [[ -L "$INSTALL_DIR" ]]; then
225
+ local link_target
226
+ link_target=$(readlink "$INSTALL_DIR" 2>/dev/null || true)
227
+ if [[ "$link_target" == *"/Cellar/"* || "$link_target" == *"/homebrew/"* ]]; then
228
+ is_brew=true
229
+ fi
230
+ fi
231
+
232
+ # Remove ~/.lacy symlink or directory
233
+ if [[ -L "$INSTALL_DIR" ]]; then
234
+ rm -f "$INSTALL_DIR"
235
+ success "Removed $INSTALL_DIR symlink"
236
+ elif [[ -d "$INSTALL_DIR" ]]; then
237
+ rm -rf "$INSTALL_DIR"
238
+ success "Removed $INSTALL_DIR"
239
+ fi
240
+ if [[ -d "$INSTALL_DIR_OLD" ]]; then
241
+ rm -rf "$INSTALL_DIR_OLD"
242
+ success "Removed $INSTALL_DIR_OLD"
243
+ fi
244
+
245
+ # If installed via Homebrew, uninstall the formula too
246
+ if [[ "$is_brew" == true ]] && command_exists brew; then
247
+ info "Removing Homebrew formula..."
248
+ brew uninstall lacymorrow/tap/lacy 2>/dev/null && success "Homebrew formula removed" || true
249
+ fi
250
+
251
+ printf "\n${GREEN}Lacy Shell uninstalled.${NC}\n"
252
+ echo "Restart your terminal to apply changes."
253
+ }
254
+
255
+ cmd_update() {
256
+ if ! is_installed; then
257
+ die "Lacy Shell is not installed. Run: lacy install"
258
+ fi
259
+
260
+ local dir="$INSTALL_DIR"
261
+ [[ -d "$dir" ]] || dir="$INSTALL_DIR_OLD"
262
+
263
+ info "Updating Lacy Shell..."
264
+
265
+ if [[ -d "${dir}/.git" ]] && command_exists git; then
266
+ if git -C "$dir" pull origin main 2>/dev/null || git -C "$dir" pull 2>/dev/null; then
267
+ VERSION="$(get_version)"
268
+ VERSION="${VERSION:-$VERSION_FALLBACK}"
269
+ success "Lacy Shell updated to v${VERSION}"
270
+ else
271
+ die "Update failed. Try: lacy reinstall"
272
+ fi
273
+ elif command_exists curl; then
274
+ update_via_tarball "$dir" || die "Update failed. Try: lacy reinstall"
275
+ VERSION="$(get_version)"
276
+ VERSION="${VERSION:-$VERSION_FALLBACK}"
277
+ success "Lacy Shell updated to v${VERSION}"
278
+ else
279
+ die "Neither git nor curl available. Cannot update."
280
+ fi
281
+ }
282
+
283
+ cmd_reinstall() {
284
+ info "Reinstalling Lacy Shell..."
285
+
286
+ # Backup user config before removing
287
+ local config_backup=""
288
+ if [[ -f "$CONFIG_FILE" ]]; then
289
+ config_backup=$(mktemp)
290
+ cp "$CONFIG_FILE" "$config_backup"
291
+ fi
292
+
293
+ # Remove existing
294
+ [[ -d "$INSTALL_DIR" ]] && rm -rf "$INSTALL_DIR"
295
+ [[ -d "$INSTALL_DIR_OLD" ]] && rm -rf "$INSTALL_DIR_OLD"
296
+
297
+ # Install fresh (git with tarball fallback)
298
+ local installed=false
299
+ if command_exists git; then
300
+ git clone --depth 1 "$REPO_URL" "$INSTALL_DIR" 2>/dev/null && installed=true
301
+ fi
302
+ if [[ "$installed" == false ]] && command_exists curl; then
303
+ local tarball_url="https://github.com/lacymorrow/lacy/archive/refs/heads/main.tar.gz"
304
+ local tmp_file
305
+ tmp_file=$(mktemp)
306
+ curl -fsSL "$tarball_url" -o "$tmp_file" 2>/dev/null && \
307
+ mkdir -p "$INSTALL_DIR" && \
308
+ tar xzf "$tmp_file" --strip-components=1 -C "$INSTALL_DIR" 2>/dev/null && \
309
+ installed=true
310
+ rm -f "$tmp_file"
311
+ fi
312
+ [[ "$installed" == false ]] && die "Install failed. Need git or curl."
313
+
314
+ # Restore user config
315
+ if [[ -n "$config_backup" ]]; then
316
+ mkdir -p "$(dirname "$CONFIG_FILE")"
317
+ cp "$config_backup" "$CONFIG_FILE"
318
+ rm -f "$config_backup"
319
+ success "Configuration preserved"
320
+ fi
321
+
322
+ # Re-read version after reinstall
323
+ VERSION="$(get_version)"
324
+ VERSION="${VERSION:-$VERSION_FALLBACK}"
325
+ success "Lacy Shell reinstalled (v${VERSION})"
326
+
327
+ # Ensure shell is configured
328
+ local rc_file plugin_file source_line
329
+ rc_file=$(get_rc_file)
330
+ plugin_file=$(get_plugin_file)
331
+ source_line="source ${INSTALL_DIR}/${plugin_file}"
332
+
333
+ if [[ -f "$rc_file" ]] && grep -q "lacy.plugin" "$rc_file" 2>/dev/null; then
334
+ success "Shell already configured"
335
+ else
336
+ mkdir -p "$(dirname "$rc_file")"
337
+ printf "\n# Lacy Shell\n%s\n" "$source_line" >> "$rc_file"
338
+ success "Added to $(basename "$rc_file")"
339
+ fi
340
+
341
+ echo ""
342
+ echo "Restart your terminal to apply changes."
343
+ }
344
+
345
+ cmd_status() {
346
+ printf "\n${BOLD}Lacy Shell${NC}\n\n"
347
+
348
+ # Installation
349
+ if is_installed; then
350
+ local dir="$INSTALL_DIR"
351
+ [[ -d "$dir" ]] || dir="$INSTALL_DIR_OLD"
352
+ success "Installed at ${dir}"
353
+
354
+ # Version from git
355
+ if [[ -d "${dir}/.git" ]]; then
356
+ local sha
357
+ sha=$(git -C "$dir" rev-parse --short HEAD 2>/dev/null || echo "unknown")
358
+ printf " Version: %s (git: %s)\n" "$VERSION" "$sha"
359
+ fi
360
+ else
361
+ printf " ${RED}✗${NC} Not installed\n"
362
+ printf "\n Run: ${CYAN}lacy install${NC}\n\n"
363
+ return
364
+ fi
365
+
366
+ # Shell plugin
367
+ local rc_file
368
+ rc_file=$(get_rc_file)
369
+ if [[ -f "$rc_file" ]] && grep -q "lacy.plugin" "$rc_file" 2>/dev/null; then
370
+ success "Shell configured ($(basename "$rc_file"))"
371
+ else
372
+ printf " ${YELLOW}○${NC} Shell not configured\n"
373
+ fi
374
+
375
+ # Config
376
+ if [[ -f "$CONFIG_FILE" ]]; then
377
+ success "Config exists"
378
+ local active
379
+ active=$(yaml_value "$CONFIG_FILE" "active")
380
+ if [[ -n "$active" ]]; then
381
+ printf " Active tool: ${CYAN}%s${NC}\n" "$active"
382
+ else
383
+ printf " Active tool: ${DIM}auto-detect${NC}\n"
384
+ fi
385
+ else
386
+ printf " ${YELLOW}○${NC} No config file\n"
387
+ fi
388
+
389
+ # AI tools
390
+ printf "\n${BOLD}AI CLI Tools${NC}\n"
391
+ for t in lash claude opencode gemini codex hermes copilot goose amp; do
392
+ if command_exists "$t"; then
393
+ success "$t"
394
+ else
395
+ printf " ${DIM}○${NC} ${DIM}%s${NC}\n" "$t"
396
+ fi
397
+ done
398
+
399
+ # PATH
400
+ printf "\n${BOLD}PATH${NC}\n"
401
+ if echo "$PATH" | grep -q "${INSTALL_DIR}/bin"; then
402
+ success "~/.lacy/bin in PATH"
403
+ else
404
+ printf " ${YELLOW}○${NC} ~/.lacy/bin not in PATH\n"
405
+ fi
406
+
407
+ echo ""
408
+ }
409
+
410
+ cmd_info() {
411
+ if [[ -f "${INSTALL_DIR}/lib/commands/info.sh" ]]; then
412
+ bash "${INSTALL_DIR}/lib/commands/info.sh"
413
+ else
414
+ printf '\033[38;5;75m%s\033[0m\n' "🔧 Lacy Shell v$VERSION"
415
+ echo
416
+ printf '%s\n' "Lacy Shell detects natural language and routes it to AI coding agents."
417
+ echo
418
+ printf '%s\n' "Quick tips:"
419
+ printf ' • %s\n' "Type normally for shell commands"
420
+ printf ' • %s\n' "Type natural language for AI assistance"
421
+ printf ' • %s\n' "Press Ctrl+Space to toggle modes"
422
+ echo
423
+ printf '%b\n' "Run '\033[38;5;200mlacy setup\033[0m' to configure your AI tool and settings."
424
+ printf '%b\n' "Run '\033[38;5;200mlacy mode\033[0m' to see current mode and legend."
425
+ fi
426
+ }
427
+
428
+ cmd_doctor() {
429
+ printf "\n${BOLD}Lacy Shell Doctor${NC} ${DIM}v${VERSION}${NC}\n\n"
430
+
431
+ local issues=0
432
+
433
+ # Check installation
434
+ if is_installed; then
435
+ success "Installation found"
436
+ else
437
+ printf " ${RED}✗${NC} Not installed\n"
438
+ printf " Fix: ${CYAN}lacy install${NC}\n"
439
+ issues=$((issues + 1))
440
+ fi
441
+
442
+ # Check shell plugin
443
+ local shell rc_file
444
+ shell=$(detect_shell)
445
+ rc_file=$(get_rc_file)
446
+
447
+ if [[ -f "$rc_file" ]] && grep -q "lacy.plugin" "$rc_file" 2>/dev/null; then
448
+ success "Shell plugin sourced in $(basename "$rc_file")"
449
+ else
450
+ printf " ${RED}✗${NC} Shell plugin not in $(basename "$rc_file")\n"
451
+ printf " Fix: Add ${CYAN}source ~/.lacy/lacy.plugin.%s${NC} to %s\n" "$shell" "$(basename "$rc_file")"
452
+ issues=$((issues + 1))
453
+ fi
454
+
455
+ # Check plugin file exists
456
+ local plugin_file
457
+ plugin_file=$(get_plugin_file)
458
+ if [[ -f "${INSTALL_DIR}/${plugin_file}" ]]; then
459
+ success "Plugin file exists (${plugin_file})"
460
+ else
461
+ printf " ${RED}✗${NC} Plugin file missing: ${INSTALL_DIR}/${plugin_file}\n"
462
+ printf " Fix: ${CYAN}lacy reinstall${NC}\n"
463
+ issues=$((issues + 1))
464
+ fi
465
+
466
+ # Check AI tool
467
+ local found_tool=0
468
+ for t in lash claude opencode gemini codex hermes copilot goose amp; do
469
+ if command_exists "$t"; then
470
+ found_tool=1
471
+ break
472
+ fi
473
+ done
474
+ if [[ $found_tool -eq 1 ]]; then
475
+ success "AI CLI tool available"
476
+ else
477
+ printf " ${YELLOW}!${NC} No AI CLI tool installed\n"
478
+ printf " Fix: ${CYAN}npm install -g lashcode${NC} (recommended) — lash.lacy.sh\n"
479
+ issues=$((issues + 1))
480
+ fi
481
+
482
+ # Check PATH
483
+ if echo "$PATH" | grep -q "${INSTALL_DIR}/bin"; then
484
+ success "~/.lacy/bin in PATH"
485
+ else
486
+ printf " ${YELLOW}!${NC} ~/.lacy/bin not in PATH\n"
487
+ printf " The ${CYAN}lacy${NC} command may not be available in new shells\n"
488
+ issues=$((issues + 1))
489
+ fi
490
+
491
+ # Check config
492
+ if [[ -f "$CONFIG_FILE" ]]; then
493
+ success "Config file exists"
494
+ else
495
+ printf " ${YELLOW}!${NC} No config file\n"
496
+ printf " Fix: ${CYAN}lacy install${NC} to generate one\n"
497
+ issues=$((issues + 1))
498
+ fi
499
+
500
+ # Check git (optional — tarball fallback available)
501
+ if command_exists git; then
502
+ success "git available"
503
+ else
504
+ printf " ${YELLOW}○${NC} git not found (optional, updates use curl fallback)\n"
505
+ fi
506
+
507
+ # Check curl
508
+ if command_exists curl; then
509
+ success "curl available"
510
+ else
511
+ printf " ${YELLOW}!${NC} curl not found\n"
512
+ issues=$((issues + 1))
513
+ fi
514
+
515
+ echo ""
516
+ if [[ $issues -eq 0 ]]; then
517
+ printf "${GREEN}All checks passed!${NC}\n"
518
+ else
519
+ printf "${YELLOW}%d issue(s) found${NC}\n" "$issues"
520
+ fi
521
+ echo ""
522
+ }
523
+
524
+ cmd_config() {
525
+ if [[ ! -f "$CONFIG_FILE" ]]; then
526
+ die "No config file. Run: lacy install"
527
+ fi
528
+
529
+ case "$1" in
530
+ edit)
531
+ local editor="${EDITOR:-${VISUAL:-vi}}"
532
+ exec "$editor" "$CONFIG_FILE"
533
+ ;;
534
+ path)
535
+ echo "$CONFIG_FILE"
536
+ ;;
537
+ show|"")
538
+ cat "$CONFIG_FILE"
539
+ ;;
540
+ *)
541
+ echo "Usage: lacy config [show|edit|path]"
542
+ ;;
543
+ esac
544
+ }
545
+
546
+ cmd_setup() {
547
+ if ! is_installed; then
548
+ die "Lacy Shell is not installed. Run: lacy install"
549
+ fi
550
+
551
+ # Try fancy Node setup first
552
+ try_node setup || true
553
+
554
+ # Bash fallback: simple numbered menu
555
+ cmd_setup_bash
556
+ }
557
+
558
+ cmd_setup_bash() {
559
+ if [[ ! -f "$CONFIG_FILE" ]]; then
560
+ die "No config file. Run: lacy install"
561
+ fi
562
+
563
+ while true; do
564
+ local active mode
565
+ active=$(yaml_value "$CONFIG_FILE" "active")
566
+ mode=$(yaml_value "$CONFIG_FILE" "default")
567
+
568
+ printf "\n${BOLD}Lacy Shell${NC} ${DIM}v${VERSION}${NC}\n\n"
569
+ printf " 1) Change AI tool ${DIM}(current: ${active:-auto-detect})${NC}\n"
570
+ printf " 2) Change mode ${DIM}(current: ${mode:-auto})${NC}\n"
571
+ printf " 3) Edit config ${DIM}(open in \$EDITOR)${NC}\n"
572
+ printf " 4) Back\n"
573
+ printf "\n"
574
+
575
+ read -p "Select [1-4]: " choice
576
+
577
+ case "$choice" in
578
+ 1) setup_tool ;;
579
+ 2) setup_mode ;;
580
+ 3)
581
+ local editor="${EDITOR:-${VISUAL:-vi}}"
582
+ "$editor" "$CONFIG_FILE"
583
+ ;;
584
+ 4|"") break ;;
585
+ *) warn "Invalid choice" ;;
586
+ esac
587
+ done
588
+ }
589
+
590
+ setup_tool() {
591
+ printf "\n${BOLD}Select AI tool${NC}\n\n"
592
+
593
+ local i=1
594
+ local tools=("lash" "claude" "opencode" "gemini" "codex" "hermes" "copilot" "goose" "amp" "custom" "auto")
595
+ local hints=("AI coding agent (recommended)" "Claude Code CLI" "OpenCode CLI" "Google Gemini CLI" "OpenAI Codex CLI" "Hermes Agent CLI" "GitHub Copilot CLI" "Goose CLI" "Sourcegraph Amp CLI" "enter your own command" "use first available")
596
+
597
+ for t in "${tools[@]}"; do
598
+ local hint="${hints[$((i-1))]}"
599
+ if [[ "$t" != "custom" && "$t" != "auto" ]] && command_exists "$t"; then
600
+ printf " ${GREEN}%d)${NC} %-12s ${GREEN}installed${NC}\n" "$i" "$t"
601
+ else
602
+ printf " %d) %-12s ${DIM}%s${NC}\n" "$i" "$t" "$hint"
603
+ fi
604
+ i=$((i + 1))
605
+ done
606
+ printf "\n"
607
+
608
+ read -p "Select [1-${#tools[@]}]: " choice
609
+
610
+ if [[ -z "$choice" ]] || ! [[ "$choice" =~ ^[0-9]+$ ]] || [[ "$choice" -lt 1 ]] || [[ "$choice" -gt ${#tools[@]} ]]; then
611
+ warn "Cancelled"
612
+ return
613
+ fi
614
+
615
+ local selected="${tools[$((choice-1))]}"
616
+
617
+ if [[ "$selected" == "custom" ]]; then
618
+ read -p "Enter custom command: " custom_cmd
619
+ if [[ -z "$custom_cmd" ]]; then
620
+ warn "No command entered. Cancelled."
621
+ return
622
+ fi
623
+ yaml_write "$CONFIG_FILE" "active" "custom"
624
+ yaml_write "$CONFIG_FILE" "custom_command" "\"$custom_cmd\""
625
+ success "Tool set to: custom ($custom_cmd)"
626
+ elif [[ "$selected" == "auto" ]]; then
627
+ yaml_write "$CONFIG_FILE" "active" ""
628
+ success "Tool set to: auto-detect"
629
+ else
630
+ yaml_write "$CONFIG_FILE" "active" "$selected"
631
+ success "Tool set to: $selected"
632
+ fi
633
+
634
+ offer_restart
635
+ }
636
+
637
+ setup_mode() {
638
+ printf "\n${BOLD}Select default mode${NC}\n\n"
639
+ printf " 1) auto ${DIM}smart detection (recommended)${NC}\n"
640
+ printf " 2) shell ${DIM}all commands execute directly${NC}\n"
641
+ printf " 3) agent ${DIM}all input goes to AI${NC}\n"
642
+ printf "\n"
643
+
644
+ read -p "Select [1-3]: " choice
645
+
646
+ case "$choice" in
647
+ 1) yaml_write "$CONFIG_FILE" "default" "auto"; success "Mode set to: auto" ;;
648
+ 2) yaml_write "$CONFIG_FILE" "default" "shell"; success "Mode set to: shell" ;;
649
+ 3) yaml_write "$CONFIG_FILE" "default" "agent"; success "Mode set to: agent" ;;
650
+ *) warn "Cancelled"; return ;;
651
+ esac
652
+
653
+ offer_restart
654
+ }
655
+
656
+ cmd_enter() {
657
+ # Already inside a lacy shell session — show info instead of nesting
658
+ if [[ "${LACY_SHELL_ACTIVE:-}" == "1" ]]; then
659
+ cmd_info
660
+ return
661
+ fi
662
+
663
+ if ! is_installed; then
664
+ die "Lacy Shell is not installed. Run: lacy install"
665
+ fi
666
+
667
+ # Launch an interactive shell — the plugin loads from .zshrc/.bashrc
668
+ local shell
669
+ shell=$(detect_shell)
670
+ exec "$shell" -li
671
+ }
672
+
673
+ cmd_new() {
674
+ local shell_home="${LACY_SHELL_HOME:-${HOME}/.lacy}"
675
+ local last_session_file="${shell_home}/.last_session"
676
+ if [[ -f "$last_session_file" ]]; then
677
+ rm -f "$last_session_file"
678
+ success "Last session cleared"
679
+ else
680
+ info "No saved session to clear"
681
+ fi
682
+ printf '\n%b\n' "Next query will start a fresh context."
683
+ }
684
+
685
+ cmd_resume() {
686
+ local shell_home="${LACY_SHELL_HOME:-${HOME}/.lacy}"
687
+ local last_session_file="${shell_home}/.last_session"
688
+ if [[ ! -f "$last_session_file" ]]; then
689
+ warn "No previous session found"
690
+ printf '\n%b\n' "Start chatting to create a session, then use ${CYAN}resume${NC} to pick it up in another shell."
691
+ return 1
692
+ fi
693
+
694
+ local saved_tool saved_id
695
+ { read -r saved_tool; read -r saved_id; } < "$last_session_file"
696
+
697
+ if [[ -z "$saved_tool" || -z "$saved_id" ]]; then
698
+ warn "Saved session file is empty"
699
+ return 1
700
+ fi
701
+
702
+ printf '\n%b\n' "${BOLD}Last session${NC}"
703
+ printf ' Tool: %s\n' "$saved_tool"
704
+ printf ' Session: %s\n' "$saved_id"
705
+ printf '\n%b\n' "Type ${CYAN}resume${NC} inside Lacy Shell to load this session."
706
+ }
707
+
708
+ cmd_logs() {
709
+ local log_file="${INSTALL_DIR}/logs/queries.log"
710
+
711
+ case "${1:-}" in
712
+ --clear|-c)
713
+ if [[ -f "$log_file" ]]; then
714
+ rm -f "$log_file"
715
+ success "Query log cleared"
716
+ else
717
+ info "No log file found"
718
+ fi
719
+ return
720
+ ;;
721
+ --count)
722
+ if [[ -f "$log_file" ]]; then
723
+ local count
724
+ count=$(wc -l < "$log_file" 2>/dev/null || echo 0)
725
+ printf "Entries: %s\n" "$count"
726
+ else
727
+ echo "0"
728
+ fi
729
+ return
730
+ ;;
731
+ esac
732
+
733
+ if [[ ! -f "$log_file" ]]; then
734
+ info "No queries logged yet."
735
+ printf '\nStart using Lacy Shell and queries will appear here.\n\n'
736
+ return
737
+ fi
738
+
739
+ local limit="${1:-50}"
740
+ if [[ "$limit" =~ ^[0-9]+$ ]]; then
741
+ printf "\n${BOLD}Recent queries${NC} ${DIM}(last %s)${NC}\n\n" "$limit"
742
+ tail -n "$limit" "$log_file" | while IFS=$'\t' read -r ts tool query; do
743
+ printf " ${DIM}%s${NC} ${CYAN}%-12s${NC} %s\n" "$ts" "$tool" "$query"
744
+ done
745
+ else
746
+ printf "\n${BOLD}All queries${NC}\n\n"
747
+ while IFS=$'\t' read -r ts tool query; do
748
+ printf " ${DIM}%s${NC} ${CYAN}%-12s${NC} %s\n" "$ts" "$tool" "$query"
749
+ done < "$log_file"
750
+ fi
751
+ echo ""
752
+ printf "${DIM}Log: %s${NC}\n\n" "$log_file"
753
+ }
754
+
755
+ cmd_changelog() {
756
+ printf "\n${BOLD}Lacy Shell Changelog${NC}\n\n"
757
+
758
+ # Try fetching from GitHub releases API
759
+ if command_exists curl; then
760
+ local json
761
+ json=$(curl -sf --max-time 5 \
762
+ -H "Accept: application/vnd.github+json" \
763
+ "https://api.github.com/repos/lacymorrow/lacy/releases/latest" 2>/dev/null || true)
764
+
765
+ if [[ -n "$json" ]]; then
766
+ local tag body
767
+ tag=$(printf '%s\n' "$json" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"//' | sed 's/".*//')
768
+ body=$(printf '%s\n' "$json" | python3 -c "
769
+ import json, sys
770
+ try:
771
+ d = json.loads(sys.stdin.read())
772
+ print(d.get('body', ''))
773
+ except: pass" 2>/dev/null || true)
774
+
775
+ if [[ -n "$tag" ]]; then
776
+ printf "${CYAN}Latest release: %s${NC}\n\n" "$tag"
777
+ if [[ -n "$body" ]]; then
778
+ printf '%s\n' "$body"
779
+ else
780
+ printf "${DIM}No release notes.${NC}\n"
781
+ fi
782
+ echo ""
783
+ return
784
+ fi
785
+ fi
786
+ fi
787
+
788
+ # Fallback: show local CHANGELOG.md if present
789
+ local changelog_file="${INSTALL_DIR}/CHANGELOG.md"
790
+ if [[ -f "$changelog_file" ]]; then
791
+ head -n 80 "$changelog_file"
792
+ echo ""
793
+ return
794
+ fi
795
+
796
+ # Last resort: recent git log
797
+ if [[ -d "${INSTALL_DIR}/.git" ]]; then
798
+ warn "Could not fetch release notes (offline?). Showing recent commits:"
799
+ echo ""
800
+ git -C "$INSTALL_DIR" log --oneline -20 2>/dev/null || true
801
+ else
802
+ warn "Could not fetch changelog. Run \`lacy update\` to get the latest version."
803
+ fi
804
+ echo ""
805
+ }
806
+
807
+ cmd_completions() {
808
+ local shell_type="${1:-}"
809
+
810
+ # Auto-detect if not specified
811
+ if [[ -z "$shell_type" ]]; then
812
+ shell_type=$(detect_shell)
813
+ fi
814
+
815
+ case "$shell_type" in
816
+ zsh) _completions_zsh ;;
817
+ bash) _completions_bash ;;
818
+ *)
819
+ warn "Unknown shell: $shell_type. Use 'zsh' or 'bash'."
820
+ exit 1
821
+ ;;
822
+ esac
823
+ }
824
+
825
+ _completions_zsh() {
826
+ cat <<'ZSHCOMP'
827
+ #compdef lacy
828
+
829
+ _lacy() {
830
+ local -a commands
831
+ commands=(
832
+ 'setup:Interactive settings (tool, mode, config)'
833
+ 'install:Install Lacy Shell'
834
+ 'uninstall:Remove Lacy Shell'
835
+ 'update:Pull latest changes'
836
+ 'reinstall:Fresh installation'
837
+ 'status:Show installation status'
838
+ 'info:Show basic information'
839
+ 'doctor:Diagnose common issues'
840
+ 'config:Show or edit configuration'
841
+ 'new:Clear saved session'
842
+ 'resume:Show saved session info'
843
+ 'logs:Show recent agent query log'
844
+ 'changelog:Show latest release notes'
845
+ 'version:Show version'
846
+ 'help:Show help'
847
+ )
848
+
849
+ local -a tool_names
850
+ tool_names=(lash claude opencode gemini codex custom auto)
851
+
852
+ case "$words[2]" in
853
+ config)
854
+ local -a config_sub
855
+ config_sub=('show:Print config file' 'edit:Open in $EDITOR' 'path:Print config path')
856
+ _describe 'config subcommand' config_sub
857
+ ;;
858
+ logs)
859
+ local -a logs_sub
860
+ logs_sub=('--clear:Clear the log file' '--count:Print entry count')
861
+ _describe 'logs option' logs_sub
862
+ ;;
863
+ tool)
864
+ case "$words[3]" in
865
+ set) _describe 'tool name' tool_names ;;
866
+ *) local -a tool_sub; tool_sub=('set:Set the active tool' 'list:List available tools')
867
+ _describe 'tool subcommand' tool_sub ;;
868
+ esac
869
+ ;;
870
+ completions)
871
+ _describe 'shell' '(zsh bash)'
872
+ ;;
873
+ *)
874
+ _describe 'lacy command' commands
875
+ ;;
876
+ esac
877
+ }
878
+
879
+ _lacy "$@"
880
+ ZSHCOMP
881
+ }
882
+
883
+ _completions_bash() {
884
+ cat <<'BASHCOMP'
885
+ _lacy_completions() {
886
+ local cur prev
887
+ cur="${COMP_WORDS[COMP_CWORD]}"
888
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
889
+
890
+ local commands="setup install uninstall update reinstall status info doctor config new resume logs changelog version help completions"
891
+ local tool_names="lash claude opencode gemini codex custom auto"
892
+
893
+ case "$prev" in
894
+ config)
895
+ COMPREPLY=( $(compgen -W "show edit path" -- "$cur") )
896
+ return
897
+ ;;
898
+ logs)
899
+ COMPREPLY=( $(compgen -W "--clear --count" -- "$cur") )
900
+ return
901
+ ;;
902
+ set)
903
+ COMPREPLY=( $(compgen -W "$tool_names" -- "$cur") )
904
+ return
905
+ ;;
906
+ tool)
907
+ COMPREPLY=( $(compgen -W "set list" -- "$cur") )
908
+ return
909
+ ;;
910
+ completions)
911
+ COMPREPLY=( $(compgen -W "zsh bash" -- "$cur") )
912
+ return
913
+ ;;
914
+ esac
915
+
916
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
917
+ }
918
+
919
+ complete -F _lacy_completions lacy
920
+ BASHCOMP
921
+ }
922
+
923
+ cmd_version() {
924
+ echo "lacy $VERSION"
925
+ if [[ -d "${INSTALL_DIR}/.git" ]]; then
926
+ local sha
927
+ sha=$(git -C "$INSTALL_DIR" rev-parse --short HEAD 2>/dev/null || echo "")
928
+ [[ -n "$sha" ]] && echo "git: $sha"
929
+ fi
930
+ }
931
+
932
+ cmd_help() {
933
+ local has_node=false
934
+ if command_exists npx && [[ "${LACY_NO_NODE:-}" != "1" ]]; then
935
+ has_node=true
936
+ fi
937
+
938
+ cat <<EOF
939
+ ${BOLD}Lacy Shell${NC} ${DIM}v${VERSION}${NC} — Talk directly to your shell
940
+
941
+ ${BOLD}Usage:${NC}
942
+ lacy Enter Lacy Shell (or re-enter after quit)
943
+ lacy [command] Run a command
944
+
945
+ ${BOLD}Commands:${NC}
946
+ setup Interactive settings (tool, mode, config)
947
+ install Install Lacy Shell (interactive)
948
+ uninstall Remove Lacy Shell completely
949
+ update Pull latest changes
950
+ reinstall Fresh installation
951
+ status Show installation status
952
+ info Show basic information and help
953
+ doctor Diagnose common issues
954
+ config Show/edit configuration
955
+ new Clear saved session (start fresh next time)
956
+ resume Show saved session info
957
+ logs [N] Show last N agent queries (default: 50)
958
+ logs --clear Clear query log
959
+ changelog Show latest release notes
960
+ completions Print shell completion script (pipe to source)
961
+ version Show version
962
+ help Show this help
963
+
964
+ ${BOLD}In-shell commands${NC} (available when Lacy is active):
965
+ mode Show/switch mode (shell/agent/auto)
966
+ tool Show/switch AI tool
967
+ ask "query" Direct query to AI
968
+ new Start fresh context (aliases: reset, clear, /new)
969
+ resume Resume previous session (/resume)
970
+ Ctrl+Space Toggle between modes
971
+
972
+ ${BOLD}Environment:${NC}
973
+ LACY_NO_NODE=1 Force bash-only mode (skip Node UI)
974
+
975
+ ${DIM}https://github.com/lacymorrow/lacy${NC}
976
+ EOF
977
+
978
+ # Show banner again at the bottom
979
+ if [[ "$has_node" == true ]]; then
980
+ print_setup_banner
981
+ fi
982
+ }
983
+
984
+ print_setup_banner() {
985
+ printf "\n"
986
+ printf " ${MAGENTA}${BOLD}╔════════════════════════════════════════════════╗${NC}\n"
987
+ printf " ${MAGENTA}${BOLD}║ ║${NC}\n"
988
+ printf " ${MAGENTA}${BOLD}║ Run ${NC}${BOLD}lacy setup${MAGENTA}${BOLD} for interactive settings ║${NC}\n"
989
+ printf " ${MAGENTA}${BOLD}║ ║${NC}\n"
990
+ printf " ${MAGENTA}${BOLD}╚════════════════════════════════════════════════╝${NC}\n"
991
+ printf "\n"
992
+ }
993
+
994
+ # ============================================================================
995
+ # Main
996
+ # ============================================================================
997
+
998
+ main() {
999
+ local cmd="${1:-}"
1000
+ shift 2>/dev/null || true
1001
+
1002
+ case "$cmd" in
1003
+ setup) cmd_setup "$@" ;;
1004
+ install) cmd_install "$@" ;;
1005
+ uninstall) cmd_uninstall "$@" ;;
1006
+ update) cmd_update "$@" ;;
1007
+ reinstall) cmd_reinstall "$@" ;;
1008
+ status|s) cmd_status "$@" ;;
1009
+ info) cmd_info ;;
1010
+ doctor) cmd_doctor "$@" ;;
1011
+ config|c) cmd_config "$@" ;;
1012
+ new|/new|reset|/reset|clear|/clear) cmd_new "$@" ;;
1013
+ resume|/resume) cmd_resume "$@" ;;
1014
+ logs) cmd_logs "$@" ;;
1015
+ changelog) cmd_changelog "$@" ;;
1016
+ completions) cmd_completions "$@" ;;
1017
+ version|-v|--version) cmd_version ;;
1018
+ help|-h|--help) cmd_help ;;
1019
+ "") cmd_enter ;;
1020
+ *)
1021
+ printf "${RED}Unknown command: %s${NC}\n\n" "$cmd"
1022
+ cmd_help
1023
+ exit 1
1024
+ ;;
1025
+ esac
1026
+ }
1027
+
1028
+ main "$@"