lacy 0.6.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -464
- package/index.mjs +490 -0
- package/package.json +26 -37
- package/LICENSE.md +0 -134
- package/bin/lash +0 -30
- package/scripts/install.js +0 -127
- package/scripts/run-issue-labeler.sh +0 -12
package/README.md
CHANGED
|
@@ -1,492 +1,61 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lacy
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<a href="https://stuff.charm.sh/crush/charm-crush.png"><img width="450" alt="Charm Crush Logo" src="https://github.com/user-attachments/assets/adc1a6f4-b284-4603-836c-59038caa2e8b" /></a><br />
|
|
5
|
-
<a href="https://github.com/lacymorrow/lash/releases"><img src="https://img.shields.io/github/release/lacymorrow/lash" alt="Latest Release"></a>
|
|
6
|
-
<a href="https://github.com/lacymorrow/lash/actions"><img src="https://github.com/lacymorrow/lash/workflows/build/badge.svg" alt="Build Status"></a>
|
|
7
|
-
</p>
|
|
3
|
+
Interactive installer for [Lacy Shell](https://github.com/lacymorrow/lacy) — talk directly to your shell.
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
### Features
|
|
12
|
-
|
|
13
|
-
- **Multi-Model:** choose from a wide range of LLMs or add your own via OpenAI- or Anthropic-compatible APIs
|
|
14
|
-
- **Flexible:** switch LLMs mid-session while preserving context
|
|
15
|
-
- **Session-Based:** maintain multiple work sessions and contexts per project
|
|
16
|
-
- **LSP-Enhanced:** uses LSPs for additional context, just like you do
|
|
17
|
-
- **Extensible:** add capabilities via MCPs (`http`, `stdio`, and `sse`)
|
|
18
|
-
- **Works Everywhere:** first-class support in terminals on macOS, Linux, and Windows (PowerShell and WSL)
|
|
19
|
-
- **Modes:** Shell, Agent, and Auto routing (first run defaults to Auto)
|
|
20
|
-
|
|
21
|
-
### Installation
|
|
22
|
-
|
|
23
|
-
NPM:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npm install -g @lacymorrow/lash
|
|
27
|
-
lash --version
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Homebrew:
|
|
5
|
+
## Install
|
|
31
6
|
|
|
32
7
|
```bash
|
|
33
|
-
|
|
34
|
-
brew install lacymorrow/tap/lash
|
|
35
|
-
lash --version
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Windows users:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
# Winget
|
|
42
|
-
winget install charmbracelet.crush
|
|
43
|
-
|
|
44
|
-
# Scoop
|
|
45
|
-
scoop bucket add charm https://github.com/charmbracelet/scoop-bucket.git
|
|
46
|
-
scoop install crush
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
<details>
|
|
50
|
-
<summary><strong>Nix (NUR)</strong></summary>
|
|
51
|
-
|
|
52
|
-
Crush is available via [NUR](https://github.com/nix-community/NUR) in `nur.repos.charmbracelet.crush`.
|
|
53
|
-
|
|
54
|
-
You can also try out Crush via `nix-shell`:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
# Add the NUR channel.
|
|
58
|
-
nix-channel --add https://github.com/nix-community/NUR/archive/main.tar.gz nur
|
|
59
|
-
nix-channel --update
|
|
60
|
-
|
|
61
|
-
# Get Crush in a Nix shell.
|
|
62
|
-
nix-shell -p '(import <nur> { pkgs = import <nixpkgs> {}; }).repos.charmbracelet.crush'
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
</details>
|
|
66
|
-
|
|
67
|
-
<details>
|
|
68
|
-
<summary><strong>Debian/Ubuntu</strong></summary>
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
# Download .deb package from GitHub releases
|
|
72
|
-
wget https://github.com/lacymorrow/lash/releases/latest/download/lash_Linux_x86_64.deb
|
|
73
|
-
sudo dpkg -i lash_Linux_x86_64.deb
|
|
74
|
-
|
|
75
|
-
# Or download and extract binary directly
|
|
76
|
-
wget https://github.com/lacymorrow/lash/releases/latest/download/lash_Linux_x86_64.tar.gz
|
|
77
|
-
tar -xzf lash_Linux_x86_64.tar.gz
|
|
78
|
-
sudo mv lash/lash /usr/local/bin/
|
|
8
|
+
npx lacy
|
|
79
9
|
```
|
|
80
10
|
|
|
81
|
-
|
|
11
|
+
Features:
|
|
12
|
+
- Arrow-key tool selection
|
|
13
|
+
- Auto-detects installed AI CLI tools
|
|
14
|
+
- Offers to install lash if selected
|
|
15
|
+
- Automatic shell restart
|
|
82
16
|
|
|
83
|
-
|
|
84
|
-
<summary><strong>Fedora/RHEL</strong></summary>
|
|
17
|
+
## Uninstall
|
|
85
18
|
|
|
86
19
|
```bash
|
|
87
|
-
|
|
88
|
-
name=Charm
|
|
89
|
-
baseurl=https://repo.charm.sh/yum/
|
|
90
|
-
enabled=1
|
|
91
|
-
gpgcheck=1
|
|
92
|
-
gpgkey=https://repo.charm.sh/yum/gpg.key' | sudo tee /etc/yum.repos.d/charm.repo
|
|
93
|
-
sudo yum install crush
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
</details>
|
|
97
|
-
|
|
98
|
-
Or, download it:
|
|
99
|
-
|
|
100
|
-
- [Packages][releases] are available in Debian and RPM formats
|
|
101
|
-
- [Binaries][releases] are available for Linux, macOS, Windows, FreeBSD, OpenBSD, and NetBSD
|
|
102
|
-
|
|
103
|
-
[releases]: https://github.com/lacymorrow/lash/releases
|
|
104
|
-
|
|
105
|
-
Or just install it with Go:
|
|
106
|
-
|
|
107
|
-
```
|
|
108
|
-
go install github.com/lacymorrow/lash@latest
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
> [!WARNING]
|
|
112
|
-
> Productivity may increase when using Lash and you may find yourself nerd
|
|
113
|
-
> sniped when first using the application. If the symptoms persist, join the
|
|
114
|
-
> [Discord][discord] and nerd snipe the rest of us.
|
|
115
|
-
|
|
116
|
-
### Getting Started
|
|
117
|
-
|
|
118
|
-
The quickest way to get started is to grab an API key for your preferred
|
|
119
|
-
provider such as Anthropic, OpenAI, Groq, or OpenRouter and run `lash`. You'll be prompted to enter your API key.
|
|
120
|
-
|
|
121
|
-
That said, you can also set environment variables for preferred providers.
|
|
122
|
-
|
|
123
|
-
| Environment Variable | Provider |
|
|
124
|
-
| -------------------------- | -------------------------------------------------- |
|
|
125
|
-
| `ANTHROPIC_API_KEY` | Anthropic |
|
|
126
|
-
| `OPENAI_API_KEY` | OpenAI |
|
|
127
|
-
| `OPENROUTER_API_KEY` | OpenRouter |
|
|
128
|
-
| `GEMINI_API_KEY` | Google Gemini |
|
|
129
|
-
| `VERTEXAI_PROJECT` | Google Cloud VertexAI (Gemini) |
|
|
130
|
-
| `VERTEXAI_LOCATION` | Google Cloud VertexAI (Gemini) |
|
|
131
|
-
| `GROQ_API_KEY` | Groq |
|
|
132
|
-
| `AWS_ACCESS_KEY_ID` | AWS Bedrock (Claude) |
|
|
133
|
-
| `AWS_SECRET_ACCESS_KEY` | AWS Bedrock (Claude) |
|
|
134
|
-
| `AWS_REGION` | AWS Bedrock (Claude) |
|
|
135
|
-
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI models |
|
|
136
|
-
| `AZURE_OPENAI_API_KEY` | Azure OpenAI models (optional when using Entra ID) |
|
|
137
|
-
| `AZURE_OPENAI_API_VERSION` | Azure OpenAI models |
|
|
138
|
-
|
|
139
|
-
### Models Catalog
|
|
140
|
-
|
|
141
|
-
Lash uses the Catwalk model catalog from the upstream project for defaults. You can override or add providers in your configuration.
|
|
142
|
-
|
|
143
|
-
### Configuration
|
|
144
|
-
|
|
145
|
-
Lash runs great with no configuration. If you do want to customize it, configuration follows the upstream file names for compatibility and is read with the following priority:
|
|
146
|
-
|
|
147
|
-
1. `.crush.json`
|
|
148
|
-
2. `crush.json`
|
|
149
|
-
3. `$HOME/.config/crush/crush.json` (Windows: `%USERPROFILE%\AppData\Local\crush\crush.json`)
|
|
150
|
-
|
|
151
|
-
Configuration itself is stored as a JSON object:
|
|
152
|
-
|
|
153
|
-
```json
|
|
154
|
-
{
|
|
155
|
-
"this-setting": {"this": "that"},
|
|
156
|
-
"that-setting": ["ceci", "cela"]
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Lash stores ephemeral data, such as application state, in this location:
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
# Project-relative (default)
|
|
164
|
-
./.lash/
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### LSPs
|
|
168
|
-
|
|
169
|
-
Lash can use LSPs for additional context. LSPs can be added manually like so:
|
|
170
|
-
|
|
171
|
-
```json
|
|
172
|
-
{
|
|
173
|
-
"$schema": "https://charm.land/crush.json",
|
|
174
|
-
"lsp": {
|
|
175
|
-
"go": {
|
|
176
|
-
"command": "gopls",
|
|
177
|
-
"env": {
|
|
178
|
-
"GOTOOLCHAIN": "go1.24.5"
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
"typescript": {
|
|
182
|
-
"command": "typescript-language-server",
|
|
183
|
-
"args": ["--stdio"]
|
|
184
|
-
},
|
|
185
|
-
"nix": {
|
|
186
|
-
"command": "nil"
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
20
|
+
npx lacy --uninstall
|
|
190
21
|
```
|
|
191
22
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
Lash supports Model Context Protocol (MCP) servers through three
|
|
195
|
-
transport types: `stdio` for command-line servers, `http` for HTTP endpoints,
|
|
196
|
-
and `sse` for Server-Sent Events. Environment variable expansion is supported
|
|
197
|
-
using `$(echo $VAR)` syntax.
|
|
23
|
+
## Options
|
|
198
24
|
|
|
199
|
-
```json
|
|
200
|
-
{
|
|
201
|
-
"$schema": "https://charm.land/crush.json",
|
|
202
|
-
"mcp": {
|
|
203
|
-
"filesystem": {
|
|
204
|
-
"type": "stdio",
|
|
205
|
-
"command": "node",
|
|
206
|
-
"args": ["/path/to/mcp-server.js"],
|
|
207
|
-
"env": {
|
|
208
|
-
"NODE_ENV": "production"
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
"github": {
|
|
212
|
-
"type": "http",
|
|
213
|
-
"url": "https://example.com/mcp/",
|
|
214
|
-
"headers": {
|
|
215
|
-
"Authorization": "$(echo Bearer $EXAMPLE_MCP_TOKEN)"
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
"streaming-service": {
|
|
219
|
-
"type": "sse",
|
|
220
|
-
"url": "https://example.com/mcp/sse",
|
|
221
|
-
"headers": {
|
|
222
|
-
"API-Key": "$(echo $API_KEY)"
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
25
|
```
|
|
26
|
+
Usage:
|
|
27
|
+
npx lacy Install Lacy Shell
|
|
28
|
+
npx lacy --uninstall Uninstall Lacy Shell
|
|
228
29
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
The `.crushignore` file uses the same syntax as `.gitignore` and can be placed
|
|
234
|
-
in the root of your project or in subdirectories.
|
|
235
|
-
|
|
236
|
-
### Allowing Tools
|
|
237
|
-
|
|
238
|
-
By default, Lash will ask you for permission before running tool calls. If you'd like, you can allow tools to be executed without prompting you for permissions. Use this with care.
|
|
239
|
-
|
|
240
|
-
```json
|
|
241
|
-
{
|
|
242
|
-
"$schema": "https://charm.land/crush.json",
|
|
243
|
-
"permissions": {
|
|
244
|
-
"allowed_tools": [
|
|
245
|
-
"view",
|
|
246
|
-
"ls",
|
|
247
|
-
"grep",
|
|
248
|
-
"edit",
|
|
249
|
-
"mcp_context7_get-library-doc"
|
|
250
|
-
]
|
|
251
|
-
}
|
|
252
|
-
}
|
|
30
|
+
Options:
|
|
31
|
+
-h, --help Show help message
|
|
32
|
+
-u, --uninstall Uninstall Lacy Shell
|
|
253
33
|
```
|
|
254
34
|
|
|
255
|
-
|
|
35
|
+
## What is Lacy Shell?
|
|
256
36
|
|
|
257
|
-
|
|
37
|
+
Lacy Shell is a ZSH plugin that routes natural language to AI and commands to your shell — automatically.
|
|
258
38
|
|
|
259
|
-
To prevent requests or tool calls from hanging indefinitely, you can configure global caps under `options`:
|
|
260
|
-
|
|
261
|
-
```json
|
|
262
|
-
{
|
|
263
|
-
"$schema": "https://charm.land/crush.json",
|
|
264
|
-
"options": {
|
|
265
|
-
"request_timeout_seconds": 300,
|
|
266
|
-
"tool_call_timeout_seconds": 120
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
39
|
```
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
Built-in tools like `bash`, `fetch`, `download`, and `sourcegraph` already enforce their own per-call timeouts; the global caps add an extra safeguard.
|
|
275
|
-
|
|
276
|
-
### Local Models
|
|
277
|
-
|
|
278
|
-
Local models can also be configured via OpenAI-compatible API. Here are two common examples:
|
|
279
|
-
|
|
280
|
-
#### Ollama
|
|
281
|
-
|
|
282
|
-
```json
|
|
283
|
-
{
|
|
284
|
-
"providers": {
|
|
285
|
-
"ollama": {
|
|
286
|
-
"name": "Ollama",
|
|
287
|
-
"base_url": "http://localhost:11434/v1/",
|
|
288
|
-
"type": "openai",
|
|
289
|
-
"models": [
|
|
290
|
-
{
|
|
291
|
-
"name": "Qwen 3 30B",
|
|
292
|
-
"id": "qwen3:30b",
|
|
293
|
-
"context_window": 256000,
|
|
294
|
-
"default_max_tokens": 20000
|
|
295
|
-
}
|
|
296
|
-
]
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
40
|
+
❯ ls -la → runs in shell
|
|
41
|
+
❯ what files are here → AI answers
|
|
42
|
+
❯ git status → runs in shell
|
|
43
|
+
❯ fix the build error → AI answers
|
|
300
44
|
```
|
|
301
45
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
```json
|
|
305
|
-
{
|
|
306
|
-
"providers": {
|
|
307
|
-
"lmstudio": {
|
|
308
|
-
"name": "LM Studio",
|
|
309
|
-
"base_url": "http://localhost:1234/v1/",
|
|
310
|
-
"type": "openai",
|
|
311
|
-
"models": [
|
|
312
|
-
{
|
|
313
|
-
"name": "Qwen 3 30B",
|
|
314
|
-
"id": "qwen/qwen3-30b-a3b-2507",
|
|
315
|
-
"context_window": 256000,
|
|
316
|
-
"default_max_tokens": 20000
|
|
317
|
-
}
|
|
318
|
-
]
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
### Custom Providers
|
|
325
|
-
|
|
326
|
-
Lash supports custom provider configurations for both OpenAI-compatible and Anthropic-compatible APIs.
|
|
327
|
-
|
|
328
|
-
#### OpenAI-Compatible APIs
|
|
46
|
+
Works with: **lash**, **claude**, **opencode**, **gemini**, **codex**
|
|
329
47
|
|
|
330
|
-
|
|
331
|
-
API. Don't forget to set `DEEPSEEK_API_KEY` in your environment.
|
|
332
|
-
|
|
333
|
-
```json
|
|
334
|
-
{
|
|
335
|
-
"$schema": "https://charm.land/crush.json",
|
|
336
|
-
"providers": {
|
|
337
|
-
"deepseek": {
|
|
338
|
-
"type": "openai",
|
|
339
|
-
"base_url": "https://api.deepseek.com/v1",
|
|
340
|
-
"api_key": "$DEEPSEEK_API_KEY",
|
|
341
|
-
"models": [
|
|
342
|
-
{
|
|
343
|
-
"id": "deepseek-chat",
|
|
344
|
-
"name": "Deepseek V3",
|
|
345
|
-
"cost_per_1m_in": 0.27,
|
|
346
|
-
"cost_per_1m_out": 1.1,
|
|
347
|
-
"cost_per_1m_in_cached": 0.07,
|
|
348
|
-
"cost_per_1m_out_cached": 1.1,
|
|
349
|
-
"context_window": 64000,
|
|
350
|
-
"default_max_tokens": 5000
|
|
351
|
-
}
|
|
352
|
-
]
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
#### Anthropic-Compatible APIs
|
|
359
|
-
|
|
360
|
-
Custom Anthropic-compatible providers follow this format:
|
|
361
|
-
|
|
362
|
-
```json
|
|
363
|
-
{
|
|
364
|
-
"$schema": "https://charm.land/crush.json",
|
|
365
|
-
"providers": {
|
|
366
|
-
"custom-anthropic": {
|
|
367
|
-
"type": "anthropic",
|
|
368
|
-
"base_url": "https://api.anthropic.com/v1",
|
|
369
|
-
"api_key": "$ANTHROPIC_API_KEY",
|
|
370
|
-
"extra_headers": {
|
|
371
|
-
"anthropic-version": "2023-06-01"
|
|
372
|
-
},
|
|
373
|
-
"models": [
|
|
374
|
-
{
|
|
375
|
-
"id": "claude-sonnet-4-20250514",
|
|
376
|
-
"name": "Claude Sonnet 4",
|
|
377
|
-
"cost_per_1m_in": 3,
|
|
378
|
-
"cost_per_1m_out": 15,
|
|
379
|
-
"cost_per_1m_in_cached": 3.75,
|
|
380
|
-
"cost_per_1m_out_cached": 0.3,
|
|
381
|
-
"context_window": 200000,
|
|
382
|
-
"default_max_tokens": 50000,
|
|
383
|
-
"can_reason": true,
|
|
384
|
-
"supports_attachments": true
|
|
385
|
-
}
|
|
386
|
-
]
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### Amazon Bedrock
|
|
393
|
-
|
|
394
|
-
Lash supports running Anthropic models through Bedrock, with caching disabled.
|
|
395
|
-
|
|
396
|
-
* A Bedrock provider will appear once you have AWS configured, i.e. `aws configure`
|
|
397
|
-
* Crush also expects the `AWS_REGION` or `AWS_DEFAULT_REGION` to be set
|
|
398
|
-
* To use a specific AWS profile set `AWS_PROFILE` in your environment, i.e. `AWS_PROFILE=myprofile crush`
|
|
399
|
-
|
|
400
|
-
### Vertex AI Platform
|
|
401
|
-
|
|
402
|
-
Vertex AI will appear in the list of available providers when `VERTEXAI_PROJECT` and `VERTEXAI_LOCATION` are set. You will also need to be authenticated:
|
|
403
|
-
|
|
404
|
-
```bash
|
|
405
|
-
gcloud auth application-default login
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
To add specific models to the configuration, configure as such:
|
|
409
|
-
|
|
410
|
-
```json
|
|
411
|
-
{
|
|
412
|
-
"$schema": "https://charm.land/crush.json",
|
|
413
|
-
"providers": {
|
|
414
|
-
"vertexai": {
|
|
415
|
-
"models": [
|
|
416
|
-
{
|
|
417
|
-
"id": "claude-sonnet-4@20250514",
|
|
418
|
-
"name": "VertexAI Sonnet 4",
|
|
419
|
-
"cost_per_1m_in": 3,
|
|
420
|
-
"cost_per_1m_out": 15,
|
|
421
|
-
"cost_per_1m_in_cached": 3.75,
|
|
422
|
-
"cost_per_1m_out_cached": 0.3,
|
|
423
|
-
"context_window": 200000,
|
|
424
|
-
"default_max_tokens": 50000,
|
|
425
|
-
"can_reason": true,
|
|
426
|
-
"supports_attachments": true
|
|
427
|
-
}
|
|
428
|
-
]
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
### A Note on Claude Max and GitHub Copilot
|
|
435
|
-
|
|
436
|
-
Lash only supports model providers through official, compliant APIs. We do not
|
|
437
|
-
support or endorse any methods that rely on personal Claude Max and GitHub Copilot
|
|
438
|
-
accounts or OAuth workarounds, which may violate Anthropic and Microsoft’s
|
|
439
|
-
Terms of Service.
|
|
440
|
-
|
|
441
|
-
We’re committed to building sustainable, trusted integrations with model
|
|
442
|
-
providers. If you’re a provider interested in working with us,
|
|
443
|
-
[reach out](mailto:vt100@charm.sh).
|
|
444
|
-
|
|
445
|
-
### Logging
|
|
446
|
-
|
|
447
|
-
Logs are stored in `./.lash/logs/lash.log` relative to your project.
|
|
448
|
-
|
|
449
|
-
The CLI also contains some helper commands to make perusing recent logs easier:
|
|
48
|
+
## Alternative Install Methods
|
|
450
49
|
|
|
451
50
|
```bash
|
|
452
|
-
#
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
# Print the last 500 lines
|
|
456
|
-
lash logs --tail 500
|
|
457
|
-
|
|
458
|
-
# Follow logs in real time
|
|
459
|
-
lash logs --follow
|
|
460
|
-
```
|
|
51
|
+
# curl
|
|
52
|
+
curl -fsSL https://lacy.sh/install | bash
|
|
461
53
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
```json
|
|
466
|
-
{
|
|
467
|
-
"$schema": "https://charm.land/crush.json",
|
|
468
|
-
"options": {
|
|
469
|
-
"debug": true,
|
|
470
|
-
"debug_lsp": true
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
### Lash-specific Configuration
|
|
476
|
-
|
|
477
|
-
Lash adds an optional `lash` namespace to configuration for mode and safety controls while remaining compatible with upstream `crush.json`:
|
|
478
|
-
|
|
479
|
-
```json
|
|
480
|
-
{
|
|
481
|
-
"$schema": "https://charm.land/crush.json",
|
|
482
|
-
"lash": {
|
|
483
|
-
"mode": "Auto",
|
|
484
|
-
"yolo": false,
|
|
485
|
-
"safety": { "confirm_agent_exec": true }
|
|
486
|
-
}
|
|
487
|
-
}
|
|
54
|
+
# Homebrew
|
|
55
|
+
brew tap lacymorrow/tap
|
|
56
|
+
brew install lacy
|
|
488
57
|
```
|
|
489
58
|
|
|
490
|
-
|
|
59
|
+
## License
|
|
491
60
|
|
|
492
|
-
|
|
61
|
+
MIT
|
package/index.mjs
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { execSync, spawn } from 'child_process';
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, rmSync } from 'fs';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
const INSTALL_DIR = join(homedir(), '.lacy');
|
|
11
|
+
const INSTALL_DIR_OLD = join(homedir(), '.lacy-shell');
|
|
12
|
+
const CONFIG_FILE = join(INSTALL_DIR, 'config.yaml');
|
|
13
|
+
const ZSHRC = join(homedir(), '.zshrc');
|
|
14
|
+
const REPO_URL = 'https://github.com/lacymorrow/lacy.git';
|
|
15
|
+
|
|
16
|
+
const TOOLS = [
|
|
17
|
+
{ value: 'lash', label: 'lash', hint: 'recommended' },
|
|
18
|
+
{ value: 'claude', label: 'claude', hint: 'Claude Code CLI' },
|
|
19
|
+
{ value: 'opencode', label: 'opencode', hint: 'OpenCode CLI' },
|
|
20
|
+
{ value: 'gemini', label: 'gemini', hint: 'Google Gemini CLI' },
|
|
21
|
+
{ value: 'codex', label: 'codex', hint: 'OpenAI Codex CLI' },
|
|
22
|
+
{ value: 'custom', label: 'Custom', hint: 'enter your own command' },
|
|
23
|
+
{ value: 'auto', label: 'Auto-detect', hint: 'use first available' },
|
|
24
|
+
{ value: 'none', label: 'None', hint: "I'll install one later" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function commandExists(cmd) {
|
|
28
|
+
try {
|
|
29
|
+
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isInstalled() {
|
|
37
|
+
return existsSync(INSTALL_DIR) || existsSync(INSTALL_DIR_OLD);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isInteractive() {
|
|
41
|
+
return process.stdin.isTTY && process.stdout.isTTY;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function restartShell(message = 'Restart shell now to apply changes?') {
|
|
45
|
+
if (!isInteractive()) return;
|
|
46
|
+
|
|
47
|
+
const restart = await p.confirm({
|
|
48
|
+
message,
|
|
49
|
+
initialValue: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (p.isCancel(restart)) return;
|
|
53
|
+
|
|
54
|
+
if (restart) {
|
|
55
|
+
p.log.info('Restarting shell...');
|
|
56
|
+
// Use spawn with shell to exec into zsh
|
|
57
|
+
const child = spawn('zsh', ['-l'], {
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
shell: false,
|
|
60
|
+
});
|
|
61
|
+
child.on('exit', () => process.exit(0));
|
|
62
|
+
// Keep the process alive until zsh exits
|
|
63
|
+
await new Promise(() => {});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Uninstall
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
async function uninstall() {
|
|
72
|
+
console.clear();
|
|
73
|
+
p.intro(pc.magenta(pc.bold(` Lacy Shell `)));
|
|
74
|
+
|
|
75
|
+
if (!isInstalled()) {
|
|
76
|
+
p.log.warn('Lacy Shell is not installed');
|
|
77
|
+
p.outro('Nothing to uninstall');
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const confirm = await p.confirm({
|
|
82
|
+
message: 'Are you sure you want to uninstall Lacy Shell?',
|
|
83
|
+
initialValue: false,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
87
|
+
p.cancel('Uninstall cancelled');
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Remove from .zshrc
|
|
92
|
+
const zshrcSpinner = p.spinner();
|
|
93
|
+
zshrcSpinner.start('Removing from .zshrc');
|
|
94
|
+
|
|
95
|
+
if (existsSync(ZSHRC)) {
|
|
96
|
+
let content = readFileSync(ZSHRC, 'utf-8');
|
|
97
|
+
// Remove source line and comment
|
|
98
|
+
content = content
|
|
99
|
+
.split('\n')
|
|
100
|
+
.filter(line => !line.includes('lacy.plugin.zsh') && line.trim() !== '# Lacy Shell')
|
|
101
|
+
.join('\n');
|
|
102
|
+
writeFileSync(ZSHRC, content);
|
|
103
|
+
zshrcSpinner.stop('Removed from .zshrc');
|
|
104
|
+
} else {
|
|
105
|
+
zshrcSpinner.stop('No .zshrc found');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Remove installation directories
|
|
109
|
+
const removeSpinner = p.spinner();
|
|
110
|
+
removeSpinner.start('Removing installation');
|
|
111
|
+
|
|
112
|
+
if (existsSync(INSTALL_DIR)) {
|
|
113
|
+
rmSync(INSTALL_DIR, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
if (existsSync(INSTALL_DIR_OLD)) {
|
|
116
|
+
rmSync(INSTALL_DIR_OLD, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
removeSpinner.stop('Installation removed');
|
|
120
|
+
|
|
121
|
+
p.log.success('Lacy Shell uninstalled');
|
|
122
|
+
|
|
123
|
+
await restartShell('Restart shell now?');
|
|
124
|
+
|
|
125
|
+
p.outro(`Run ${pc.cyan('source ~/.zshrc')} or restart your terminal.`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Install
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
async function install() {
|
|
133
|
+
console.clear();
|
|
134
|
+
p.intro(pc.magenta(pc.bold(` Lacy Shell `)));
|
|
135
|
+
|
|
136
|
+
// Check prerequisites
|
|
137
|
+
const prerequisites = p.spinner();
|
|
138
|
+
prerequisites.start('Checking prerequisites');
|
|
139
|
+
|
|
140
|
+
const missing = [];
|
|
141
|
+
if (!commandExists('zsh')) missing.push('zsh');
|
|
142
|
+
if (!commandExists('git')) missing.push('git');
|
|
143
|
+
|
|
144
|
+
if (missing.length > 0) {
|
|
145
|
+
prerequisites.stop('Prerequisites check failed');
|
|
146
|
+
p.log.error(`Missing required tools: ${missing.join(', ')}`);
|
|
147
|
+
p.outro(pc.red('Please install missing prerequisites and try again.'));
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
prerequisites.stop('Prerequisites OK');
|
|
152
|
+
|
|
153
|
+
// Detect installed tools
|
|
154
|
+
const detected = [];
|
|
155
|
+
for (const tool of ['lash', 'claude', 'opencode', 'gemini', 'codex']) {
|
|
156
|
+
if (commandExists(tool)) {
|
|
157
|
+
detected.push(tool);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (detected.length > 0) {
|
|
162
|
+
p.log.info(`Detected: ${detected.map(t => pc.green(t)).join(', ')}`);
|
|
163
|
+
} else {
|
|
164
|
+
p.log.warn('No AI CLI tools detected');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Tool selection
|
|
168
|
+
const selectedTool = await p.select({
|
|
169
|
+
message: 'Which AI CLI tool do you want to use?',
|
|
170
|
+
options: TOOLS.map(t => ({
|
|
171
|
+
value: t.value,
|
|
172
|
+
label: t.label,
|
|
173
|
+
hint: detected.includes(t.value)
|
|
174
|
+
? pc.green('installed')
|
|
175
|
+
: t.hint,
|
|
176
|
+
})),
|
|
177
|
+
initialValue: detected[0] || 'lash',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (p.isCancel(selectedTool)) {
|
|
181
|
+
p.cancel('Installation cancelled');
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Prompt for custom command if selected
|
|
186
|
+
let customCommand = '';
|
|
187
|
+
if (selectedTool === 'custom') {
|
|
188
|
+
customCommand = await p.text({
|
|
189
|
+
message: 'Enter your custom command (query will be appended as a quoted argument):',
|
|
190
|
+
placeholder: 'claude --dangerously-skip-permissions -p',
|
|
191
|
+
validate(value) {
|
|
192
|
+
if (!value || value.trim().length === 0) return 'Command cannot be empty';
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (p.isCancel(customCommand)) {
|
|
197
|
+
p.cancel('Installation cancelled');
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
p.log.info(`Custom command: ${pc.cyan(customCommand)}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Offer to install lash if selected but not installed
|
|
205
|
+
if (selectedTool === 'lash' && !commandExists('lash')) {
|
|
206
|
+
const installLash = await p.confirm({
|
|
207
|
+
message: 'lash is not installed. Would you like to install it now?',
|
|
208
|
+
initialValue: true,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (p.isCancel(installLash)) {
|
|
212
|
+
p.cancel('Installation cancelled');
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (installLash) {
|
|
217
|
+
const lashSpinner = p.spinner();
|
|
218
|
+
lashSpinner.start('Installing lash');
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
if (commandExists('npm')) {
|
|
222
|
+
execSync('npm install -g lash-cli', { stdio: 'pipe' });
|
|
223
|
+
lashSpinner.stop('lash installed');
|
|
224
|
+
} else if (commandExists('brew')) {
|
|
225
|
+
execSync('brew tap lacymorrow/tap && brew install lash', { stdio: 'pipe' });
|
|
226
|
+
lashSpinner.stop('lash installed');
|
|
227
|
+
} else {
|
|
228
|
+
lashSpinner.stop('Could not install lash');
|
|
229
|
+
p.log.warn('Please install npm or homebrew, then run: npm install -g lash-cli');
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
lashSpinner.stop('lash installation failed');
|
|
233
|
+
p.log.warn('You can install it manually later: npm install -g lash-cli');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Clone/update repository
|
|
239
|
+
const installSpinner = p.spinner();
|
|
240
|
+
installSpinner.start('Installing Lacy');
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
if (existsSync(INSTALL_DIR)) {
|
|
244
|
+
// Update existing
|
|
245
|
+
try {
|
|
246
|
+
execSync('git pull origin main', { cwd: INSTALL_DIR, stdio: 'pipe' });
|
|
247
|
+
} catch {
|
|
248
|
+
// Ignore pull errors, use existing
|
|
249
|
+
}
|
|
250
|
+
installSpinner.stop('Lacy updated');
|
|
251
|
+
} else {
|
|
252
|
+
execSync(`git clone --depth 1 ${REPO_URL} "${INSTALL_DIR}"`, { stdio: 'pipe' });
|
|
253
|
+
installSpinner.stop('Lacy installed');
|
|
254
|
+
}
|
|
255
|
+
} catch (e) {
|
|
256
|
+
installSpinner.stop('Installation failed');
|
|
257
|
+
p.log.error(`Could not clone repository: ${e.message}`);
|
|
258
|
+
p.outro(pc.red('Installation failed'));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Configure .zshrc
|
|
263
|
+
const zshrcSpinner = p.spinner();
|
|
264
|
+
zshrcSpinner.start('Configuring shell');
|
|
265
|
+
|
|
266
|
+
const sourceLine = `source ${INSTALL_DIR}/lacy.plugin.zsh`;
|
|
267
|
+
|
|
268
|
+
if (existsSync(ZSHRC)) {
|
|
269
|
+
const zshrcContent = readFileSync(ZSHRC, 'utf-8');
|
|
270
|
+
|
|
271
|
+
if (zshrcContent.includes('lacy.plugin.zsh')) {
|
|
272
|
+
zshrcSpinner.stop('Already configured');
|
|
273
|
+
} else {
|
|
274
|
+
appendFileSync(ZSHRC, `\n# Lacy Shell\n${sourceLine}\n`);
|
|
275
|
+
zshrcSpinner.stop('Added to .zshrc');
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
writeFileSync(ZSHRC, `# Lacy Shell\n${sourceLine}\n`);
|
|
279
|
+
zshrcSpinner.stop('Created .zshrc');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Create config
|
|
283
|
+
const configSpinner = p.spinner();
|
|
284
|
+
configSpinner.start('Creating configuration');
|
|
285
|
+
|
|
286
|
+
mkdirSync(INSTALL_DIR, { recursive: true });
|
|
287
|
+
|
|
288
|
+
const activeToolValue = selectedTool === 'auto' || selectedTool === 'none' ? '' : selectedTool;
|
|
289
|
+
|
|
290
|
+
const customCommandLine = selectedTool === 'custom' && customCommand
|
|
291
|
+
? ` custom_command: "${customCommand}"`
|
|
292
|
+
: ` # custom_command: "your-command -flags"`;
|
|
293
|
+
|
|
294
|
+
const configContent = `# Lacy Shell Configuration
|
|
295
|
+
# https://github.com/lacymorrow/lacy
|
|
296
|
+
|
|
297
|
+
# AI CLI tool selection
|
|
298
|
+
# Options: lash, claude, opencode, gemini, codex, custom, or empty for auto-detect
|
|
299
|
+
agent_tools:
|
|
300
|
+
active: ${activeToolValue}
|
|
301
|
+
${customCommandLine}
|
|
302
|
+
|
|
303
|
+
# API Keys (optional - only needed if no CLI tool is installed)
|
|
304
|
+
api_keys:
|
|
305
|
+
# openai: "your-key-here"
|
|
306
|
+
# anthropic: "your-key-here"
|
|
307
|
+
|
|
308
|
+
# Operating modes
|
|
309
|
+
modes:
|
|
310
|
+
default: auto # Options: shell, agent, auto
|
|
311
|
+
|
|
312
|
+
# Smart auto-detection settings
|
|
313
|
+
auto_detection:
|
|
314
|
+
enabled: true
|
|
315
|
+
confidence_threshold: 0.7
|
|
316
|
+
`;
|
|
317
|
+
|
|
318
|
+
writeFileSync(CONFIG_FILE, configContent);
|
|
319
|
+
configSpinner.stop('Configuration created');
|
|
320
|
+
|
|
321
|
+
// Success message
|
|
322
|
+
p.log.success(pc.green('Installation complete!'));
|
|
323
|
+
|
|
324
|
+
p.note(
|
|
325
|
+
`${pc.cyan('what files are here')} ${pc.dim('→ AI answers')}
|
|
326
|
+
${pc.cyan('ls -la')} ${pc.dim('→ runs in shell')}
|
|
327
|
+
|
|
328
|
+
Commands:
|
|
329
|
+
${pc.cyan('mode')} ${pc.dim('Show/change mode')}
|
|
330
|
+
${pc.cyan('tool')} ${pc.dim('Show/change AI tool')}
|
|
331
|
+
${pc.cyan('ask "q"')} ${pc.dim('Direct query to AI')}`,
|
|
332
|
+
'Try it'
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (selectedTool === 'none' || (selectedTool === 'auto' && detected.length === 0)) {
|
|
336
|
+
p.log.warn('Remember to install an AI CLI tool:');
|
|
337
|
+
console.log(` ${pc.cyan('npm install -g lash-cli')}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
await restartShell();
|
|
341
|
+
|
|
342
|
+
p.outro(pc.dim('Learn more: https://github.com/lacymorrow/lacy'));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ============================================================================
|
|
346
|
+
// Main
|
|
347
|
+
// ============================================================================
|
|
348
|
+
|
|
349
|
+
async function main() {
|
|
350
|
+
const args = process.argv.slice(2);
|
|
351
|
+
|
|
352
|
+
// Handle flags
|
|
353
|
+
if (args.includes('--uninstall') || args.includes('-u')) {
|
|
354
|
+
await uninstall();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
359
|
+
console.log(`
|
|
360
|
+
${pc.magenta(pc.bold('Lacy Shell'))} - Talk directly to your shell
|
|
361
|
+
|
|
362
|
+
${pc.bold('Usage:')}
|
|
363
|
+
npx lacy Install Lacy Shell
|
|
364
|
+
npx lacy --uninstall Uninstall Lacy Shell
|
|
365
|
+
|
|
366
|
+
${pc.bold('Options:')}
|
|
367
|
+
-h, --help Show this help message
|
|
368
|
+
-u, --uninstall Uninstall Lacy Shell
|
|
369
|
+
|
|
370
|
+
${pc.bold('Other install methods:')}
|
|
371
|
+
curl -fsSL https://lacy.sh/install | bash
|
|
372
|
+
brew install lacymorrow/tap/lacy
|
|
373
|
+
|
|
374
|
+
${pc.dim('https://github.com/lacymorrow/lacy')}
|
|
375
|
+
`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// If already installed, offer choices
|
|
380
|
+
if (isInstalled()) {
|
|
381
|
+
console.clear();
|
|
382
|
+
p.intro(pc.magenta(pc.bold(` Lacy Shell `)));
|
|
383
|
+
|
|
384
|
+
const action = await p.select({
|
|
385
|
+
message: 'Lacy Shell is already installed. What would you like to do?',
|
|
386
|
+
options: [
|
|
387
|
+
{ value: 'update', label: 'Update', hint: 'pull latest changes' },
|
|
388
|
+
{ value: 'reinstall', label: 'Reinstall', hint: 'fresh installation' },
|
|
389
|
+
{ value: 'uninstall', label: 'Uninstall', hint: 'remove Lacy Shell' },
|
|
390
|
+
{ value: 'cancel', label: 'Cancel', hint: 'do nothing' },
|
|
391
|
+
],
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
if (p.isCancel(action) || action === 'cancel') {
|
|
395
|
+
p.cancel('Cancelled');
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (action === 'uninstall') {
|
|
400
|
+
// Skip the intro since we already showed it
|
|
401
|
+
const confirm = await p.confirm({
|
|
402
|
+
message: 'Are you sure you want to uninstall Lacy Shell?',
|
|
403
|
+
initialValue: false,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
407
|
+
p.cancel('Uninstall cancelled');
|
|
408
|
+
process.exit(0);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Remove from .zshrc
|
|
412
|
+
const zshrcSpinner = p.spinner();
|
|
413
|
+
zshrcSpinner.start('Removing from .zshrc');
|
|
414
|
+
|
|
415
|
+
if (existsSync(ZSHRC)) {
|
|
416
|
+
let content = readFileSync(ZSHRC, 'utf-8');
|
|
417
|
+
content = content
|
|
418
|
+
.split('\n')
|
|
419
|
+
.filter(line => !line.includes('lacy.plugin.zsh') && line.trim() !== '# Lacy Shell')
|
|
420
|
+
.join('\n');
|
|
421
|
+
writeFileSync(ZSHRC, content);
|
|
422
|
+
zshrcSpinner.stop('Removed from .zshrc');
|
|
423
|
+
} else {
|
|
424
|
+
zshrcSpinner.stop('No .zshrc found');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Remove installation
|
|
428
|
+
const removeSpinner = p.spinner();
|
|
429
|
+
removeSpinner.start('Removing installation');
|
|
430
|
+
|
|
431
|
+
if (existsSync(INSTALL_DIR)) {
|
|
432
|
+
rmSync(INSTALL_DIR, { recursive: true, force: true });
|
|
433
|
+
}
|
|
434
|
+
if (existsSync(INSTALL_DIR_OLD)) {
|
|
435
|
+
rmSync(INSTALL_DIR_OLD, { recursive: true, force: true });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
removeSpinner.stop('Installation removed');
|
|
439
|
+
|
|
440
|
+
p.log.success('Lacy Shell uninstalled');
|
|
441
|
+
|
|
442
|
+
await restartShell('Restart shell now?');
|
|
443
|
+
|
|
444
|
+
p.outro(`Run ${pc.cyan('source ~/.zshrc')} or restart your terminal.`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (action === 'update') {
|
|
449
|
+
const updateSpinner = p.spinner();
|
|
450
|
+
updateSpinner.start('Updating Lacy');
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
execSync('git pull origin main', { cwd: INSTALL_DIR, stdio: 'pipe' });
|
|
454
|
+
updateSpinner.stop('Lacy updated');
|
|
455
|
+
p.log.success('Update complete!');
|
|
456
|
+
|
|
457
|
+
await restartShell();
|
|
458
|
+
|
|
459
|
+
p.outro(`Run ${pc.cyan('source ~/.zshrc')} or restart your terminal.`);
|
|
460
|
+
} catch {
|
|
461
|
+
updateSpinner.stop('Update failed');
|
|
462
|
+
p.log.error('Could not update. Try reinstalling instead.');
|
|
463
|
+
p.outro('');
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (action === 'reinstall') {
|
|
469
|
+
// Remove existing and continue to install
|
|
470
|
+
const removeSpinner = p.spinner();
|
|
471
|
+
removeSpinner.start('Removing existing installation');
|
|
472
|
+
|
|
473
|
+
if (existsSync(INSTALL_DIR)) {
|
|
474
|
+
rmSync(INSTALL_DIR, { recursive: true, force: true });
|
|
475
|
+
}
|
|
476
|
+
if (existsSync(INSTALL_DIR_OLD)) {
|
|
477
|
+
rmSync(INSTALL_DIR_OLD, { recursive: true, force: true });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
removeSpinner.stop('Removed');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
await install();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
main().catch((e) => {
|
|
488
|
+
p.log.error(e.message);
|
|
489
|
+
process.exit(1);
|
|
490
|
+
});
|
package/package.json
CHANGED
|
@@ -1,50 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lacy",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Install Lacy Shell - talk directly to your shell",
|
|
5
|
+
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"lacy": "index.mjs"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs"
|
|
11
|
+
],
|
|
9
12
|
"scripts": {
|
|
10
|
-
"
|
|
11
|
-
},
|
|
12
|
-
"repository": {
|
|
13
|
-
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/lacymorrow/lash.git"
|
|
13
|
+
"start": "node index.mjs"
|
|
15
14
|
},
|
|
16
15
|
"keywords": [
|
|
16
|
+
"lacy",
|
|
17
|
+
"shell",
|
|
18
|
+
"zsh",
|
|
17
19
|
"ai",
|
|
18
|
-
"assistant",
|
|
19
|
-
"terminal",
|
|
20
20
|
"cli",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"mcp",
|
|
24
|
-
"shell"
|
|
21
|
+
"agent",
|
|
22
|
+
"natural-language"
|
|
25
23
|
],
|
|
26
|
-
"author": "Lacy Morrow
|
|
24
|
+
"author": "Lacy Morrow",
|
|
27
25
|
"license": "MIT",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/lacymorrow/lacy.git",
|
|
29
|
+
"directory": "packages/lacy"
|
|
30
30
|
},
|
|
31
|
-
"homepage": "https://github.com/lacymorrow/
|
|
32
|
-
"
|
|
33
|
-
"
|
|
31
|
+
"homepage": "https://github.com/lacymorrow/lacy",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@clack/prompts": "^0.7.0",
|
|
34
|
+
"picocolors": "^1.0.0"
|
|
34
35
|
},
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
],
|
|
40
|
-
"cpu": [
|
|
41
|
-
"x64",
|
|
42
|
-
"arm64"
|
|
43
|
-
],
|
|
44
|
-
"files": [
|
|
45
|
-
"bin/",
|
|
46
|
-
"scripts/",
|
|
47
|
-
"README.md",
|
|
48
|
-
"LICENSE.md"
|
|
49
|
-
]
|
|
50
|
-
}
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/LICENSE.md
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
# Functional Source License, Version 1.1, MIT Future License
|
|
2
|
-
|
|
3
|
-
## Abbreviation
|
|
4
|
-
|
|
5
|
-
FSL-1.1-MIT
|
|
6
|
-
|
|
7
|
-
## Notice
|
|
8
|
-
|
|
9
|
-
Copyright 2025 Charmbracelet, Inc
|
|
10
|
-
|
|
11
|
-
## Terms and Conditions
|
|
12
|
-
|
|
13
|
-
### Licensor ("We")
|
|
14
|
-
|
|
15
|
-
The party offering the Software under these Terms and Conditions.
|
|
16
|
-
|
|
17
|
-
### The Software
|
|
18
|
-
|
|
19
|
-
The "Software" is each version of the software that we make available under
|
|
20
|
-
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
-
Conditions with the Software.
|
|
22
|
-
|
|
23
|
-
### License Grant
|
|
24
|
-
|
|
25
|
-
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
-
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
-
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
-
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
-
|
|
30
|
-
### Permitted Purpose
|
|
31
|
-
|
|
32
|
-
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
-
means making the Software available to others in a commercial product or
|
|
34
|
-
service that:
|
|
35
|
-
|
|
36
|
-
1. substitutes for the Software;
|
|
37
|
-
|
|
38
|
-
2. substitutes for any other product or service we offer using the Software
|
|
39
|
-
that exists as of the date we make the Software available; or
|
|
40
|
-
|
|
41
|
-
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
-
|
|
43
|
-
Permitted Purposes specifically include using the Software:
|
|
44
|
-
|
|
45
|
-
1. for your internal use and access;
|
|
46
|
-
|
|
47
|
-
2. for non-commercial education;
|
|
48
|
-
|
|
49
|
-
3. for non-commercial research; and
|
|
50
|
-
|
|
51
|
-
4. in connection with professional services that you provide to a licensee
|
|
52
|
-
using the Software in accordance with these Terms and Conditions.
|
|
53
|
-
|
|
54
|
-
### Patents
|
|
55
|
-
|
|
56
|
-
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
-
patents, the license grant above includes a license under our patents. If you
|
|
58
|
-
make a claim against any party that the Software infringes or contributes to
|
|
59
|
-
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
-
immediately.
|
|
61
|
-
|
|
62
|
-
### Redistribution
|
|
63
|
-
|
|
64
|
-
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
-
the Software.
|
|
66
|
-
|
|
67
|
-
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
-
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
-
remove any copyright notices provided in or with the Software.
|
|
70
|
-
|
|
71
|
-
### Disclaimer
|
|
72
|
-
|
|
73
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
-
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
-
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
-
|
|
77
|
-
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
-
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
-
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
-
|
|
81
|
-
### Trademarks
|
|
82
|
-
|
|
83
|
-
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
-
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
-
trademarks, trade names, service marks or product names.
|
|
86
|
-
|
|
87
|
-
## Grant of Future License
|
|
88
|
-
|
|
89
|
-
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
-
the MIT license that is effective on the second anniversary of the date we make
|
|
91
|
-
the Software available. On or after that date, you may use the Software under
|
|
92
|
-
the MIT license, in which case the following will apply:
|
|
93
|
-
|
|
94
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
95
|
-
this software and associated documentation files (the "Software"), to deal in
|
|
96
|
-
the Software without restriction, including without limitation the rights to
|
|
97
|
-
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
98
|
-
of the Software, and to permit persons to whom the Software is furnished to do
|
|
99
|
-
so, subject to the following conditions:
|
|
100
|
-
|
|
101
|
-
The above copyright notice and this permission notice shall be included in all
|
|
102
|
-
copies or substantial portions of the Software.
|
|
103
|
-
|
|
104
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
105
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
106
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
107
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
108
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
109
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
110
|
-
SOFTWARE.
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
MIT License
|
|
115
|
-
|
|
116
|
-
Copyright (c) 2025-03-21 - 2025-05-30 Kujtim Hoxha
|
|
117
|
-
|
|
118
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
119
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
120
|
-
in the Software without restriction, including without limitation the rights
|
|
121
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
122
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
123
|
-
furnished to do so, subject to the following conditions:
|
|
124
|
-
|
|
125
|
-
The above copyright notice and this permission notice shall be included in all
|
|
126
|
-
copies or substantial portions of the Software.
|
|
127
|
-
|
|
128
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
129
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
130
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
131
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
132
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
133
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
134
|
-
SOFTWARE.
|
package/bin/lash
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { spawn } = require('child_process');
|
|
5
|
-
|
|
6
|
-
const platform = process.platform;
|
|
7
|
-
const binaryName = platform === 'win32' ? 'lash.exe' : 'lash';
|
|
8
|
-
const binaryPath = path.join(__dirname, binaryName);
|
|
9
|
-
|
|
10
|
-
// Check if binary exists
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
if (!fs.existsSync(binaryPath)) {
|
|
13
|
-
console.error('lash binary not found. Please reinstall the package.');
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Execute the binary with all arguments
|
|
18
|
-
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
19
|
-
stdio: 'inherit',
|
|
20
|
-
windowsHide: false
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
child.on('exit', (code) => {
|
|
24
|
-
process.exit(code);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
child.on('error', (err) => {
|
|
28
|
-
console.error('Failed to start lash:', err.message);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
});
|
package/scripts/install.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const https = require('https');
|
|
6
|
-
const { execSync } = require('child_process');
|
|
7
|
-
|
|
8
|
-
const packageJson = require('../package.json');
|
|
9
|
-
const version = packageJson.version;
|
|
10
|
-
|
|
11
|
-
// Determine platform and architecture
|
|
12
|
-
const platform = process.platform;
|
|
13
|
-
const arch = process.arch;
|
|
14
|
-
|
|
15
|
-
// Map Node.js platform/arch to GoReleaser naming
|
|
16
|
-
const platformMap = {
|
|
17
|
-
'darwin': 'Darwin',
|
|
18
|
-
'linux': 'Linux',
|
|
19
|
-
'win32': 'Windows'
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const archMap = {
|
|
23
|
-
'x64': 'x86_64',
|
|
24
|
-
'arm64': 'arm64'
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const mappedPlatform = platformMap[platform];
|
|
28
|
-
const mappedArch = archMap[arch];
|
|
29
|
-
|
|
30
|
-
if (!mappedPlatform || !mappedArch) {
|
|
31
|
-
console.error(`Unsupported platform: ${platform} ${arch}`);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Construct download URL
|
|
36
|
-
const fileName = `lash_${version}_${mappedPlatform}_${mappedArch}`;
|
|
37
|
-
const archiveExt = platform === 'win32' ? 'zip' : 'tar.gz';
|
|
38
|
-
const downloadUrl = `https://github.com/lacymorrow/lash/releases/download/v${version}/${fileName}.${archiveExt}`;
|
|
39
|
-
|
|
40
|
-
console.log(`Downloading lash v${version} for ${platform} ${arch}...`);
|
|
41
|
-
console.log(`URL: ${downloadUrl}`);
|
|
42
|
-
|
|
43
|
-
// Create bin directory
|
|
44
|
-
const binDir = path.join(__dirname, '..', 'bin');
|
|
45
|
-
if (!fs.existsSync(binDir)) {
|
|
46
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Download and extract
|
|
50
|
-
const tempFile = path.join(binDir, `lash.${archiveExt}`);
|
|
51
|
-
|
|
52
|
-
function download(url, dest) {
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
const file = fs.createWriteStream(dest);
|
|
55
|
-
https.get(url, (response) => {
|
|
56
|
-
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
57
|
-
// Follow redirect
|
|
58
|
-
return download(response.headers.location, dest).then(resolve).catch(reject);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (response.statusCode !== 200) {
|
|
62
|
-
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
response.pipe(file);
|
|
67
|
-
file.on('finish', () => {
|
|
68
|
-
file.close();
|
|
69
|
-
resolve();
|
|
70
|
-
});
|
|
71
|
-
}).on('error', reject);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function install() {
|
|
76
|
-
try {
|
|
77
|
-
await download(downloadUrl, tempFile);
|
|
78
|
-
|
|
79
|
-
// Extract the archive
|
|
80
|
-
const extractDir = path.join(binDir, 'temp');
|
|
81
|
-
if (!fs.existsSync(extractDir)) {
|
|
82
|
-
fs.mkdirSync(extractDir, { recursive: true });
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (platform === 'win32') {
|
|
86
|
-
// Extract zip (requires unzip or 7z)
|
|
87
|
-
try {
|
|
88
|
-
execSync(`powershell -command "Expand-Archive -Path '${tempFile}' -DestinationPath '${extractDir}' -Force"`, { stdio: 'inherit' });
|
|
89
|
-
} catch (e) {
|
|
90
|
-
console.error('Failed to extract with PowerShell, trying 7z...');
|
|
91
|
-
execSync(`7z x "${tempFile}" -o"${extractDir}"`, { stdio: 'inherit' });
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
// Extract tar.gz
|
|
95
|
-
execSync(`tar -xzf "${tempFile}" -C "${extractDir}"`, { stdio: 'inherit' });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Find and move the binary
|
|
99
|
-
const binaryName = platform === 'win32' ? 'lash.exe' : 'lash';
|
|
100
|
-
const extractedBinary = path.join(extractDir, fileName, binaryName);
|
|
101
|
-
const finalBinary = path.join(binDir, binaryName);
|
|
102
|
-
|
|
103
|
-
if (fs.existsSync(extractedBinary)) {
|
|
104
|
-
fs.copyFileSync(extractedBinary, finalBinary);
|
|
105
|
-
|
|
106
|
-
// Make executable on Unix systems
|
|
107
|
-
if (platform !== 'win32') {
|
|
108
|
-
fs.chmodSync(finalBinary, 0o755);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
console.log(`✅ lash v${version} installed successfully!`);
|
|
112
|
-
console.log(`Binary location: ${finalBinary}`);
|
|
113
|
-
} else {
|
|
114
|
-
throw new Error(`Binary not found in extracted archive: ${extractedBinary}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Cleanup
|
|
118
|
-
fs.rmSync(tempFile, { force: true });
|
|
119
|
-
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
120
|
-
|
|
121
|
-
} catch (error) {
|
|
122
|
-
console.error('Installation failed:', error.message);
|
|
123
|
-
process.exit(1);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
install();
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
ISSUES=$(gh issue list --state=all --limit=1000 --json "number" -t '{{range .}}{{printf "%.0f\n" .number}}{{end}}')
|
|
2
|
-
PRS=$(gh pr list --state=all --limit=1000 --json "number" -t '{{range .}}{{printf "%.0f\n" .number}}{{end}}')
|
|
3
|
-
|
|
4
|
-
for issue in $ISSUES; do
|
|
5
|
-
echo "Dispatching issue-labeler.yml for $issue"
|
|
6
|
-
gh workflow run issue-labeler.yml -f issue-number="$issue"
|
|
7
|
-
done
|
|
8
|
-
|
|
9
|
-
for pr in $PRS; do
|
|
10
|
-
echo "Dispatching issue-labeler.yml for $pr"
|
|
11
|
-
gh workflow run issue-labeler.yml -f issue-number="$pr"
|
|
12
|
-
done
|