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 CHANGED
@@ -1,492 +1,61 @@
1
- # Lash
1
+ # lacy
2
2
 
3
- <p align="center">
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
- Terminal-based AI assistant for developers. A login-shell-friendly fork of Charmbracelet Crush with Shell, Agent, and Auto modes, plus built-in MCP support.
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
- brew tap lacymorrow/tap
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
- </details>
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
- <details>
84
- <summary><strong>Fedora/RHEL</strong></summary>
17
+ ## Uninstall
85
18
 
86
19
  ```bash
87
- echo '[charm]
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
- ### MCPs
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
- ### Ignoring Files
230
-
231
- Lash respects `.gitignore` by default. You can also create a `.crushignore` file to specify additional files and directories that should be ignored when providing context.
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
- You can also skip all permission prompts entirely by running Lash with the `--yolo` flag (or setting `lash.yolo` in config). Be careful with this feature.
35
+ ## What is Lacy Shell?
256
36
 
257
- ### Timeouts
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
- - `request_timeout_seconds`: Maximum duration for a single agent request. When reached, the request is canceled.
272
- - `tool_call_timeout_seconds`: Maximum duration for each individual tool call. Tools with their own shorter timeouts still apply; this acts as a safety cap.
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
- #### LM Studio
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
- Here’s an example configuration for Deepseek, which uses an OpenAI-compatible
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
- # Print the last 1000 lines
453
- lash logs
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
- Want more logging? Run `lash` with the `--debug` flag, or enable it in the
463
- config:
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
- ### License
59
+ ## License
491
60
 
492
- FSL-1.1-MIT (MIT Future). See `LICENSE`.
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": "0.6.4",
4
- "description": "Terminal-based AI assistant for developers. A login-shell-friendly fork of Charmbracelet Crush with Shell, Agent, and Auto modes, plus built-in MCP support.",
5
- "main": "bin/lash",
3
+ "version": "1.3.0",
4
+ "description": "Install Lacy Shell - talk directly to your shell",
5
+ "type": "module",
6
6
  "bin": {
7
- "lash": "bin/lash"
7
+ "lacy": "index.mjs"
8
8
  },
9
+ "files": [
10
+ "index.mjs"
11
+ ],
9
12
  "scripts": {
10
- "postinstall": "node scripts/install.js"
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
- "developer-tools",
22
- "llm",
23
- "mcp",
24
- "shell"
21
+ "agent",
22
+ "natural-language"
25
23
  ],
26
- "author": "Lacy Morrow <me@lacymorrow.com>",
24
+ "author": "Lacy Morrow",
27
25
  "license": "MIT",
28
- "bugs": {
29
- "url": "https://github.com/lacymorrow/lash/issues"
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/lash#readme",
32
- "engines": {
33
- "node": ">=16"
31
+ "homepage": "https://github.com/lacymorrow/lacy",
32
+ "dependencies": {
33
+ "@clack/prompts": "^0.7.0",
34
+ "picocolors": "^1.0.0"
34
35
  },
35
- "os": [
36
- "darwin",
37
- "linux",
38
- "win32"
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
- });
@@ -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