lacy 1.8.11 → 1.8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.github/FUNDING.yml +3 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
  7. package/.github/SECURITY.md +32 -0
  8. package/.github/assets/logo-horizontal-dark.png +0 -0
  9. package/.github/assets/logo-horizontal-dark.svg +17 -0
  10. package/.github/assets/logo-horizontal.png +0 -0
  11. package/.github/assets/logo-horizontal.svg +17 -0
  12. package/.github/assets/logo.png +0 -0
  13. package/.github/assets/logo.svg +12 -0
  14. package/.github/assets/social-preview.png +0 -0
  15. package/.github/assets/social-preview.svg +50 -0
  16. package/.github/dependabot.yml +21 -0
  17. package/.github/workflows/ci.yml +80 -0
  18. package/.github/workflows/dependabot-auto-merge.yml +32 -0
  19. package/CHANGELOG.md +366 -0
  20. package/CLAUDE.md +340 -0
  21. package/CONTRIBUTING.md +141 -0
  22. package/LICENSE +110 -0
  23. package/README.md +201 -31
  24. package/RELEASING.md +148 -0
  25. package/STYLE.md +202 -0
  26. package/assets/hero.jpeg +0 -0
  27. package/assets/mode-indicators.jpeg +0 -0
  28. package/assets/real-time-indicator.jpeg +0 -0
  29. package/assets/supported-tools.jpeg +0 -0
  30. package/bin/lacy +1028 -0
  31. package/docs/ADDING-BACKENDS.md +124 -0
  32. package/docs/DEVTO-ARTICLE.md +94 -0
  33. package/docs/DOCS.md +68 -0
  34. package/docs/GROWTH-STRATEGY.md +119 -0
  35. package/docs/HN-RESPONSES.md +122 -0
  36. package/docs/LAUNCH-COPY-FINAL.md +105 -0
  37. package/docs/MARKETING.md +411 -0
  38. package/docs/NATURAL_LANGUAGE_DETECTION.md +204 -0
  39. package/docs/UGC_VIDEO_SCRIPT.md +114 -0
  40. package/docs/articles/devto-how-i-made-my-terminal-understand-english.md +117 -0
  41. package/docs/demo-color-transition.gif +0 -0
  42. package/docs/demo-full.gif +0 -0
  43. package/docs/demo-indicator.gif +0 -0
  44. package/docs/launch-thread-may6.sh +158 -0
  45. package/docs/videos/README.md +189 -0
  46. package/docs/videos/generate_frames.py +510 -0
  47. package/docs/videos/generate_frames_v2.py +729 -0
  48. package/docs/videos/generate_short.py +328 -0
  49. package/docs/videos/generate_short_v2.py +526 -0
  50. package/docs/videos/lacy-shell-demo-v2.mp4 +0 -0
  51. package/docs/videos/lacy-shell-demo.mp4 +0 -0
  52. package/docs/videos/lacy-shell-short-v2.mp4 +0 -0
  53. package/docs/videos/lacy-shell-short.mp4 +0 -0
  54. package/install.sh +1009 -0
  55. package/lacy.plugin.bash +75 -0
  56. package/lacy.plugin.fish +43 -0
  57. package/lacy.plugin.zsh +65 -0
  58. package/lib/animations.zsh +3 -0
  59. package/lib/bash/completions.bash +40 -0
  60. package/lib/bash/execute.bash +233 -0
  61. package/lib/bash/init.bash +40 -0
  62. package/lib/bash/keybindings.bash +134 -0
  63. package/lib/bash/prompt.bash +85 -0
  64. package/lib/commands/info.sh +25 -0
  65. package/lib/config.zsh +3 -0
  66. package/lib/constants.zsh +3 -0
  67. package/lib/core/animations.sh +271 -0
  68. package/lib/core/commands.sh +297 -0
  69. package/lib/core/config.sh +340 -0
  70. package/lib/core/constants.sh +366 -0
  71. package/lib/core/context.sh +260 -0
  72. package/lib/core/detection.sh +417 -0
  73. package/lib/core/mcp.sh +741 -0
  74. package/lib/core/modes.sh +123 -0
  75. package/lib/core/preheat.sh +496 -0
  76. package/lib/core/spinner.sh +174 -0
  77. package/lib/core/telemetry.sh +99 -0
  78. package/lib/detection.zsh +3 -0
  79. package/lib/execute.zsh +3 -0
  80. package/lib/fish/config.fish +66 -0
  81. package/lib/fish/detection.fish +90 -0
  82. package/lib/fish/execute.fish +105 -0
  83. package/lib/fish/keybindings.fish +42 -0
  84. package/lib/fish/prompt.fish +30 -0
  85. package/lib/keybindings.zsh +3 -0
  86. package/lib/mcp.zsh +3 -0
  87. package/lib/modes.zsh +3 -0
  88. package/lib/preheat.zsh +3 -0
  89. package/lib/prompt.zsh +3 -0
  90. package/lib/spinner.zsh +3 -0
  91. package/lib/zsh/completions.zsh +60 -0
  92. package/lib/zsh/execute.zsh +294 -0
  93. package/lib/zsh/init.zsh +26 -0
  94. package/lib/zsh/keybindings.zsh +551 -0
  95. package/lib/zsh/prompt.zsh +90 -0
  96. package/package.json +42 -27
  97. package/packages/lacy/README.md +61 -0
  98. package/packages/lacy/commands/info.sh +25 -0
  99. package/{index.mjs → packages/lacy/index.mjs} +247 -20
  100. package/packages/lacy/package-lock.json +71 -0
  101. package/packages/lacy/package.json +42 -0
  102. package/script/release.ts +487 -0
  103. package/squirrel.toml +36 -0
  104. package/tests/test_bash.bash +163 -0
  105. package/tests/test_core.sh +607 -0
  106. package/tests/test_gemini.sh +119 -0
  107. package/tests/test_gemini_mcp.sh +126 -0
  108. package/tests/test_preheat_server.zsh +446 -0
  109. package/uninstall.sh +52 -0
package/package.json CHANGED
@@ -1,42 +1,57 @@
1
1
  {
2
2
  "name": "lacy",
3
- "version": "1.8.11",
4
- "description": "Install lacytalk to your terminal",
5
- "type": "module",
6
- "bin": {
7
- "lacy": "index.mjs"
8
- },
9
- "files": [
10
- "index.mjs"
11
- ],
3
+ "version": "1.8.13",
4
+ "description": "Talk to your terminal AI agent routing for your shell",
5
+ "main": "lacy.plugin.zsh",
12
6
  "scripts": {
13
- "start": "node index.mjs",
14
- "release": "npm publish",
15
- "release:beta": "npm version prerelease --preid=beta && npm publish --tag beta"
7
+ "install": "./install.sh",
8
+ "release": "bun run script/release.ts",
9
+ "release:beta": "bun run script/release.ts --beta"
16
10
  },
17
11
  "keywords": [
18
- "lacy",
19
12
  "shell",
20
13
  "terminal",
21
- "ai",
14
+ "zsh",
15
+ "bash",
16
+ "shell-plugin",
22
17
  "cli",
23
- "developer-tools",
18
+ "ai",
24
19
  "ai-agent",
25
- "natural-language"
20
+ "ai-shell",
21
+ "mcp",
22
+ "natural-language",
23
+ "claude",
24
+ "gemini",
25
+ "opencode",
26
+ "codex",
27
+ "developer-tools"
26
28
  ],
27
- "author": "Lacy Morrow",
28
- "license": "MIT",
29
+ "author": "Lacy Shell Team",
30
+ "license": "FSL-1.1-MIT",
31
+ "dependencies": {},
32
+ "devDependencies": {
33
+ "@clack/prompts": "^1.0.0",
34
+ "picocolors": "^1.0.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
29
39
  "repository": {
30
40
  "type": "git",
31
- "url": "git+https://github.com/lacymorrow/lacy.git",
32
- "directory": "packages/lacy"
41
+ "url": "https://github.com/lacymorrow/lacy.git"
33
42
  },
34
- "homepage": "https://lacy.sh",
35
- "dependencies": {
36
- "@clack/prompts": "^0.7.0",
37
- "picocolors": "^1.0.0"
43
+ "bugs": {
44
+ "url": "https://github.com/lacymorrow/lacy/issues"
38
45
  },
39
- "engines": {
40
- "node": ">=18"
41
- }
46
+ "homepage": "https://lacy.sh",
47
+ "funding": [
48
+ {
49
+ "type": "github",
50
+ "url": "https://github.com/sponsors/lacymorrow"
51
+ },
52
+ {
53
+ "type": "individual",
54
+ "url": "https://www.buymeacoffee.com/lm"
55
+ }
56
+ ]
42
57
  }
@@ -0,0 +1,61 @@
1
+ # lacy
2
+
3
+ Interactive installer for [Lacy Shell](https://github.com/lacymorrow/lacy) — talk directly to your shell.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx lacy
9
+ ```
10
+
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
16
+
17
+ ## Uninstall
18
+
19
+ ```bash
20
+ npx lacy --uninstall
21
+ ```
22
+
23
+ ## Options
24
+
25
+ ```
26
+ Usage:
27
+ npx lacy Install Lacy Shell
28
+ npx lacy --uninstall Uninstall Lacy Shell
29
+
30
+ Options:
31
+ -h, --help Show help message
32
+ -u, --uninstall Uninstall Lacy Shell
33
+ ```
34
+
35
+ ## What is Lacy Shell?
36
+
37
+ Lacy routes natural language to AI and commands to your shell — automatically.
38
+
39
+ ```
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
44
+ ```
45
+
46
+ Works with: **lash**, **claude**, **opencode**, **gemini**, **codex**
47
+
48
+ ## Alternative Install Methods
49
+
50
+ ```bash
51
+ # curl
52
+ curl -fsSL https://lacy.sh/install | bash
53
+
54
+ # Homebrew
55
+ brew tap lacymorrow/tap
56
+ brew install lacy
57
+ ```
58
+
59
+ ## License
60
+
61
+ MIT
@@ -0,0 +1,25 @@
1
+ #!/bin/bash
2
+
3
+ # Lacy Shell - Info command
4
+ # Shows basic information and guides users to setup
5
+
6
+ _lacy_info_version() {
7
+ local pkg="${HOME}/.lacy/package.json"
8
+ if [[ -f "$pkg" ]]; then
9
+ grep '"version"' "$pkg" 2>/dev/null | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"//' | sed 's/".*//'
10
+ else
11
+ echo "unknown"
12
+ fi
13
+ }
14
+
15
+ printf '\033[38;5;75m%s\033[0m\n' "🔧 Lacy Shell v$(_lacy_info_version)"
16
+ echo
17
+ printf '%s\n' "Lacy Shell detects natural language and routes it to AI coding agents."
18
+ echo
19
+ printf '%s\n' "Quick tips:"
20
+ printf ' • %s\n' "Type normally for shell commands"
21
+ printf ' • %s\n' "Type natural language for AI assistance"
22
+ printf ' • %s\n' "Press Ctrl+Space to toggle modes"
23
+ echo
24
+ printf '%b\n' "Run '\033[38;5;200mlacy setup\033[0m' to configure your AI tool and settings."
25
+ printf '%b\n' "Run '\033[38;5;200mlacy mode\033[0m' to see current mode and legend."
@@ -11,7 +11,7 @@ import {
11
11
  appendFileSync,
12
12
  rmSync,
13
13
  } from "fs";
14
- import { homedir } from "os";
14
+ import { homedir, tmpdir } from "os";
15
15
  import { join, dirname } from "path";
16
16
  import { fileURLToPath } from "url";
17
17
 
@@ -19,6 +19,7 @@ const INSTALL_DIR = join(homedir(), ".lacy");
19
19
  const INSTALL_DIR_OLD = join(homedir(), ".lacy-shell");
20
20
  const CONFIG_FILE = join(INSTALL_DIR, "config.yaml");
21
21
  const REPO_URL = "https://github.com/lacymorrow/lacy.git";
22
+ const TARBALL_URL = "https://github.com/lacymorrow/lacy/archive/refs/heads";
22
23
 
23
24
  // Version — read from installed package.json (single source of truth),
24
25
  // fall back to this npm package's own package.json
@@ -80,7 +81,11 @@ function detectShell() {
80
81
  const shell = process.env.SHELL || "";
81
82
  const base = shell.split("/").pop();
82
83
  if (base === "bash") return "bash";
83
- return "zsh"; // default
84
+ if (base === "zsh") return "zsh";
85
+ // Unknown $SHELL — check what's available
86
+ if (commandExists("zsh")) return "zsh";
87
+ if (commandExists("bash")) return "bash";
88
+ return "bash";
84
89
  }
85
90
 
86
91
  function getShellConfig(shell) {
@@ -164,6 +169,11 @@ const TOOLS = [
164
169
  { value: "opencode", label: "opencode", hint: "OpenCode CLI" },
165
170
  { value: "gemini", label: "gemini", hint: "Google Gemini CLI" },
166
171
  { value: "codex", label: "codex", hint: "OpenAI Codex CLI" },
172
+ { value: "hermes", label: "hermes (beta)", hint: "Hermes Agent — Nous Research" },
173
+ { value: "copilot", label: "copilot (beta)", hint: "GitHub Copilot CLI" },
174
+ { value: "goose", label: "goose (beta)", hint: "Block's Goose AI agent" },
175
+ { value: "amp", label: "amp", hint: "Sourcegraph Amp CLI" },
176
+ { value: "aider", label: "aider", hint: "Aider AI pair programming — pipx install aider-chat" },
167
177
  { value: "custom", label: "Custom", hint: "enter your own command" },
168
178
  { value: "auto", label: "Auto-detect", hint: "use first available" },
169
179
  { value: "none", label: "None", hint: "I'll install one later" },
@@ -185,6 +195,14 @@ function commandExists(cmd) {
185
195
  }
186
196
  }
187
197
 
198
+ function installViaTarball(branch) {
199
+ const tmpFile = join(tmpdir(), `lacy-${Date.now()}.tar.gz`);
200
+ execSync(`curl -fsSL "${TARBALL_URL}/${branch}.tar.gz" -o "${tmpFile}"`, { stdio: "pipe" });
201
+ mkdirSync(INSTALL_DIR, { recursive: true });
202
+ execSync(`tar xzf "${tmpFile}" --strip-components=1 -C "${INSTALL_DIR}"`, { stdio: "pipe" });
203
+ try { rmSync(tmpFile); } catch {}
204
+ }
205
+
188
206
  function isInstalled() {
189
207
  return existsSync(INSTALL_DIR) || existsSync(INSTALL_DIR_OLD);
190
208
  }
@@ -238,6 +256,13 @@ async function restartShell(
238
256
  // Restore terminal state before handing off to the new shell
239
257
  restoreTerminalState();
240
258
 
259
+ // Remove signal handlers so Ctrl+C in the child shell doesn't kill Node.
260
+ // The child shell manages its own signals; Node just waits for it to exit.
261
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
262
+ process.removeAllListeners(sig);
263
+ }
264
+ process.on("SIGINT", () => {});
265
+
241
266
  // Spawn a new login shell that inherits our stdio, then exit Node.
242
267
  // We use spawn (not execSync) to avoid creating a nested shell —
243
268
  // execSync("exec ...") only replaces the *child* process, not Node,
@@ -378,11 +403,20 @@ async function install() {
378
403
  if (shell === "bash") {
379
404
  if (commandExists("bash")) {
380
405
  try {
381
- const bashVer = execSync('bash -c "echo ${BASH_VERSINFO[0]}"', {
382
- stdio: "pipe",
383
- })
384
- .toString()
385
- .trim();
406
+ // Use $SHELL if it's bash on macOS, plain `bash` resolves to /bin/bash (3.2)
407
+ // even when the user has a newer bash (e.g. 5.x from Homebrew) as their login shell
408
+ const shellEnv = process.env.SHELL || "";
409
+ const userBash = shellEnv.endsWith("/bash") ? shellEnv : "bash";
410
+ let bashVer;
411
+ try {
412
+ bashVer = execSync(`"${userBash}" -c "echo \\${BASH_VERSINFO[0]}"`, {
413
+ stdio: "pipe",
414
+ }).toString().trim();
415
+ } catch {
416
+ bashVer = execSync('bash -c "echo ${BASH_VERSINFO[0]}"', {
417
+ stdio: "pipe",
418
+ }).toString().trim();
419
+ }
386
420
  if (parseInt(bashVer) < 4) {
387
421
  missing.push(
388
422
  `bash 4+ (found bash ${bashVer}, upgrade with: brew install bash)`,
@@ -398,8 +432,6 @@ async function install() {
398
432
  if (!commandExists("zsh")) missing.push("zsh");
399
433
  }
400
434
 
401
- if (!commandExists("git")) missing.push("git");
402
-
403
435
  if (missing.length > 0) {
404
436
  prerequisites.stop("Prerequisites check failed");
405
437
  p.log.error(`Missing required tools: ${missing.join(", ")}`);
@@ -411,7 +443,7 @@ async function install() {
411
443
 
412
444
  // Detect installed tools
413
445
  let detected = [];
414
- for (const tool of ["lash", "claude", "opencode", "gemini", "codex"]) {
446
+ for (const tool of TOOLS.filter((t) => !["custom", "auto", "none"].includes(t.value)).map((t) => t.value)) {
415
447
  if (commandExists(tool)) {
416
448
  detected.push(tool);
417
449
  }
@@ -552,28 +584,217 @@ async function install() {
552
584
  }
553
585
  }
554
586
 
587
+ // Offer to install hermes if selected but not installed
588
+ if (selectedTool === "hermes" && !commandExists("hermes")) {
589
+ const installHermes = await p.confirm({
590
+ message: "hermes is not installed. Would you like to install it now?",
591
+ initialValue: true,
592
+ });
593
+
594
+ if (p.isCancel(installHermes)) {
595
+ p.cancel("Installation cancelled");
596
+ process.exit(0);
597
+ }
598
+
599
+ if (installHermes) {
600
+ p.log.info("Running hermes installer...");
601
+
602
+ try {
603
+ if (commandExists("curl")) {
604
+ execSync(
605
+ "curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash",
606
+ { stdio: "inherit" },
607
+ );
608
+ p.log.success("hermes installed");
609
+ } else {
610
+ p.log.warn(
611
+ "curl not found. Install manually: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash",
612
+ );
613
+ }
614
+ } catch (e) {
615
+ p.log.warn(
616
+ "Installation failed. You can install manually: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash",
617
+ );
618
+ }
619
+ }
620
+ }
621
+
622
+ // Offer to install copilot if selected but not installed
623
+ if (selectedTool === "copilot" && !commandExists("copilot")) {
624
+ const installCopilot = await p.confirm({
625
+ message: "copilot is not installed. Would you like to install it now?",
626
+ initialValue: true,
627
+ });
628
+
629
+ if (p.isCancel(installCopilot)) {
630
+ p.cancel("Installation cancelled");
631
+ process.exit(0);
632
+ }
633
+
634
+ if (installCopilot) {
635
+ p.log.info("Installing copilot via gh extension...");
636
+
637
+ try {
638
+ if (commandExists("gh")) {
639
+ execSync("gh extension install github/gh-copilot", { stdio: "inherit" });
640
+ p.log.success("copilot installed");
641
+ } else {
642
+ p.log.warn(
643
+ "gh CLI not found. Install manually: gh extension install github/gh-copilot",
644
+ );
645
+ }
646
+ } catch (e) {
647
+ p.log.warn(
648
+ `Installation failed: ${e.message}. You can install manually: gh extension install github/gh-copilot`,
649
+ );
650
+ }
651
+ }
652
+ }
653
+
654
+ // Offer to install goose if selected but not installed
655
+ if (selectedTool === "goose" && !commandExists("goose")) {
656
+ const installGoose = await p.confirm({
657
+ message: "goose is not installed. Would you like to install it now?",
658
+ initialValue: true,
659
+ });
660
+
661
+ if (p.isCancel(installGoose)) {
662
+ p.cancel("Installation cancelled");
663
+ process.exit(0);
664
+ }
665
+
666
+ if (installGoose) {
667
+ const gooseSpinner = p.spinner();
668
+ gooseSpinner.start("Installing goose");
669
+
670
+ try {
671
+ if (commandExists("brew")) {
672
+ execSync("brew install goose", { stdio: "pipe" });
673
+ gooseSpinner.stop("goose installed");
674
+ } else {
675
+ gooseSpinner.stop("Could not install goose");
676
+ p.log.warn(
677
+ "Please install homebrew, then run: brew install goose",
678
+ );
679
+ }
680
+ } catch (e) {
681
+ gooseSpinner.stop(`goose installation failed: ${e.message}`);
682
+ p.log.warn(
683
+ "You can install it manually later: brew install goose",
684
+ );
685
+ }
686
+ }
687
+ }
688
+
689
+ // Offer to install amp if selected but not installed
690
+ if (selectedTool === "amp" && !commandExists("amp")) {
691
+ const installAmp = await p.confirm({
692
+ message: "amp is not installed. Would you like to install it now?",
693
+ initialValue: true,
694
+ });
695
+
696
+ if (p.isCancel(installAmp)) {
697
+ p.cancel("Installation cancelled");
698
+ process.exit(0);
699
+ }
700
+
701
+ if (installAmp) {
702
+ const ampSpinner = p.spinner();
703
+ ampSpinner.start("Installing amp");
704
+
705
+ try {
706
+ if (commandExists("npm")) {
707
+ execSync("npm install -g @sourcegraph/amp", { stdio: "pipe" });
708
+ ampSpinner.stop("amp installed");
709
+ } else {
710
+ ampSpinner.stop("Could not install amp");
711
+ p.log.warn(
712
+ "Please install npm, then run: npm install -g @sourcegraph/amp",
713
+ );
714
+ }
715
+ } catch (e) {
716
+ ampSpinner.stop("amp installation failed");
717
+ p.log.warn(
718
+ "You can install it manually later: npm install -g @sourcegraph/amp",
719
+ );
720
+ }
721
+ }
722
+ }
723
+
724
+ // Offer to install aider if selected but not installed
725
+ if (selectedTool === "aider" && !commandExists("aider")) {
726
+ const installAider = await p.confirm({
727
+ message: "aider is not installed. Would you like to install it now?",
728
+ initialValue: true,
729
+ });
730
+
731
+ if (p.isCancel(installAider)) {
732
+ p.cancel("Installation cancelled");
733
+ process.exit(0);
734
+ }
735
+
736
+ if (installAider) {
737
+ const aiderSpinner = p.spinner();
738
+ aiderSpinner.start("Installing aider");
739
+
740
+ try {
741
+ if (commandExists("pipx")) {
742
+ execSync("pipx install aider-chat", { stdio: "pipe" });
743
+ aiderSpinner.stop("aider installed");
744
+ } else if (commandExists("pip3")) {
745
+ execSync("pip3 install --user aider-chat", { stdio: "pipe" });
746
+ aiderSpinner.stop("aider installed");
747
+ } else {
748
+ aiderSpinner.stop("Could not install aider");
749
+ p.log.warn(
750
+ "Please install pipx, then run: pipx install aider-chat",
751
+ );
752
+ }
753
+ } catch (e) {
754
+ aiderSpinner.stop("aider installation failed");
755
+ p.log.warn(
756
+ "You can install it manually later: pipx install aider-chat",
757
+ );
758
+ }
759
+ }
760
+ }
761
+
555
762
  // Clone/update repository
556
763
  const installSpinner = p.spinner();
557
764
  installSpinner.start("Installing Lacy");
558
765
 
766
+ const hasGit = commandExists("git");
767
+
559
768
  try {
560
769
  if (existsSync(INSTALL_DIR)) {
561
770
  // Update existing
562
771
  try {
563
- execSync("git pull origin main", { cwd: INSTALL_DIR, stdio: "pipe" });
772
+ if (hasGit && existsSync(join(INSTALL_DIR, ".git"))) {
773
+ execSync("git pull origin main", { cwd: INSTALL_DIR, stdio: "pipe" });
774
+ } else {
775
+ installViaTarball("main");
776
+ }
564
777
  } catch {
565
- // Ignore pull errors, use existing
778
+ // Ignore update errors, use existing
566
779
  }
567
780
  installSpinner.stop("Lacy updated");
568
781
  } else {
569
- execSync(`git clone --depth 1 ${REPO_URL} "${INSTALL_DIR}"`, {
570
- stdio: "pipe",
571
- });
782
+ if (hasGit) {
783
+ try {
784
+ execSync(`git clone --depth 1 ${REPO_URL} "${INSTALL_DIR}"`, {
785
+ stdio: "pipe",
786
+ });
787
+ } catch {
788
+ installViaTarball("main");
789
+ }
790
+ } else {
791
+ installViaTarball("main");
792
+ }
572
793
  installSpinner.stop("Lacy installed");
573
794
  }
574
795
  } catch (e) {
575
796
  installSpinner.stop("Installation failed");
576
- p.log.error(`Could not clone repository: ${e.message}`);
797
+ p.log.error(`Could not download repository: ${e.message}`);
577
798
  p.outro(pc.red("Installation failed"));
578
799
  process.exit(1);
579
800
  }
@@ -776,7 +997,7 @@ ${pc.dim("https://github.com/lacymorrow/lacy")}
776
997
  const active = readConfigValue("active");
777
998
  const mode = readConfigValue("default");
778
999
  const detected = [];
779
- for (const tool of ["lash", "claude", "opencode", "gemini", "codex"]) {
1000
+ for (const tool of TOOLS.filter((t) => !["custom", "auto", "none"].includes(t.value)).map((t) => t.value)) {
780
1001
  if (commandExists(tool)) detected.push(tool);
781
1002
  }
782
1003
 
@@ -937,7 +1158,7 @@ ${pc.dim("https://github.com/lacymorrow/lacy")}
937
1158
  ` Mode: ${pc.cyan(modeDisplay)}`,
938
1159
  ``,
939
1160
  ` ${pc.bold("AI CLI tools:")}`,
940
- ...["lash", "claude", "opencode", "gemini", "codex"].map((t) =>
1161
+ ...TOOLS.filter((t) => !["custom", "auto", "none"].includes(t.value)).map(({ value: t }) =>
941
1162
  commandExists(t)
942
1163
  ? ` ${pc.green("✓")} ${t}`
943
1164
  : ` ${pc.dim("○")} ${pc.dim(t)}`,
@@ -960,7 +1181,11 @@ ${pc.dim("https://github.com/lacymorrow/lacy")}
960
1181
  ? INSTALL_DIR
961
1182
  : INSTALL_DIR_OLD;
962
1183
  try {
963
- execSync("git pull origin main", { cwd: updateDir, stdio: "pipe" });
1184
+ if (commandExists("git") && existsSync(join(updateDir, ".git"))) {
1185
+ execSync("git pull origin main", { cwd: updateDir, stdio: "pipe" });
1186
+ } else {
1187
+ installViaTarball("main");
1188
+ }
964
1189
  const updatedVersion = getVersion();
965
1190
  updateSpinner.stop(`Lacy updated to v${updatedVersion}`);
966
1191
  trackEvent("update", "npx");
@@ -1011,7 +1236,9 @@ ${pc.dim("https://github.com/lacymorrow/lacy")}
1011
1236
  await install();
1012
1237
  }
1013
1238
 
1014
- main().catch((e) => {
1239
+ main().then(() => {
1240
+ process.exit(0);
1241
+ }).catch((e) => {
1015
1242
  restoreTerminalState();
1016
1243
  p.log.error(e.message);
1017
1244
  process.exit(1);
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "lacy",
3
+ "version": "1.7.2",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "lacy",
9
+ "version": "1.7.2",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "@clack/prompts": "^0.7.0",
13
+ "picocolors": "^1.0.0"
14
+ },
15
+ "bin": {
16
+ "lacy": "index.mjs"
17
+ },
18
+ "engines": {
19
+ "node": ">=18"
20
+ }
21
+ },
22
+ "node_modules/@clack/core": {
23
+ "version": "0.3.5",
24
+ "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.5.tgz",
25
+ "integrity": "sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "picocolors": "^1.0.0",
29
+ "sisteransi": "^1.0.5"
30
+ }
31
+ },
32
+ "node_modules/@clack/prompts": {
33
+ "version": "0.7.0",
34
+ "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz",
35
+ "integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==",
36
+ "bundleDependencies": [
37
+ "is-unicode-supported"
38
+ ],
39
+ "license": "MIT",
40
+ "dependencies": {
41
+ "@clack/core": "^0.3.3",
42
+ "is-unicode-supported": "*",
43
+ "picocolors": "^1.0.0",
44
+ "sisteransi": "^1.0.5"
45
+ }
46
+ },
47
+ "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
48
+ "version": "1.3.0",
49
+ "inBundle": true,
50
+ "license": "MIT",
51
+ "engines": {
52
+ "node": ">=12"
53
+ },
54
+ "funding": {
55
+ "url": "https://github.com/sponsors/sindresorhus"
56
+ }
57
+ },
58
+ "node_modules/picocolors": {
59
+ "version": "1.2.0",
60
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
61
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
62
+ "license": "ISC"
63
+ },
64
+ "node_modules/sisteransi": {
65
+ "version": "1.0.5",
66
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
67
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
68
+ "license": "MIT"
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "lacy",
3
+ "version": "1.8.11",
4
+ "description": "Install lacy — talk to your terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "lacy": "index.mjs"
8
+ },
9
+ "files": [
10
+ "index.mjs"
11
+ ],
12
+ "scripts": {
13
+ "start": "node index.mjs",
14
+ "release": "npm publish",
15
+ "release:beta": "npm version prerelease --preid=beta && npm publish --tag beta"
16
+ },
17
+ "keywords": [
18
+ "lacy",
19
+ "shell",
20
+ "terminal",
21
+ "ai",
22
+ "cli",
23
+ "developer-tools",
24
+ "ai-agent",
25
+ "natural-language"
26
+ ],
27
+ "author": "Lacy Morrow",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/lacymorrow/lacy.git",
32
+ "directory": "packages/lacy"
33
+ },
34
+ "homepage": "https://lacy.sh",
35
+ "dependencies": {
36
+ "@clack/prompts": "^0.7.0",
37
+ "picocolors": "^1.0.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ }
42
+ }