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/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 "$@"
|