lacy 1.6.5 → 1.7.0-beta.1
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/index.mjs +626 -248
- package/package.json +4 -2
package/index.mjs
CHANGED
|
@@ -1,32 +1,86 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import * as p from
|
|
4
|
-
import pc from
|
|
5
|
-
import { execSync
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
appendFileSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
} from "fs";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
|
|
17
|
+
const INSTALL_DIR = join(homedir(), ".lacy");
|
|
18
|
+
const INSTALL_DIR_OLD = join(homedir(), ".lacy-shell");
|
|
19
|
+
const CONFIG_FILE = join(INSTALL_DIR, "config.yaml");
|
|
20
|
+
const REPO_URL = "https://github.com/lacymorrow/lacy.git";
|
|
21
|
+
|
|
22
|
+
// Shell detection and per-shell configuration
|
|
23
|
+
function detectShell() {
|
|
24
|
+
const shell = process.env.SHELL || "";
|
|
25
|
+
const base = shell.split("/").pop();
|
|
26
|
+
if (base === "bash") return "bash";
|
|
27
|
+
return "zsh"; // default (fish not yet supported)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getShellConfig(shell) {
|
|
31
|
+
switch (shell) {
|
|
32
|
+
case "bash":
|
|
33
|
+
return {
|
|
34
|
+
rcFile:
|
|
35
|
+
process.platform === "darwin"
|
|
36
|
+
? join(homedir(), ".bash_profile")
|
|
37
|
+
: join(homedir(), ".bashrc"),
|
|
38
|
+
extraRcFile:
|
|
39
|
+
process.platform === "darwin" ? join(homedir(), ".bashrc") : null,
|
|
40
|
+
pluginFile: "lacy.plugin.bash",
|
|
41
|
+
shellCmd: "bash",
|
|
42
|
+
rcName: process.platform === "darwin" ? ".bash_profile" : ".bashrc",
|
|
43
|
+
};
|
|
44
|
+
default: // zsh
|
|
45
|
+
return {
|
|
46
|
+
rcFile: join(homedir(), ".zshrc"),
|
|
47
|
+
extraRcFile: null,
|
|
48
|
+
pluginFile: "lacy.plugin.zsh",
|
|
49
|
+
shellCmd: "zsh",
|
|
50
|
+
rcName: ".zshrc",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// All RC files that might contain lacy config (for uninstall)
|
|
56
|
+
const ALL_RC_FILES = [
|
|
57
|
+
join(homedir(), ".zshrc"),
|
|
58
|
+
join(homedir(), ".bashrc"),
|
|
59
|
+
join(homedir(), ".bash_profile"),
|
|
60
|
+
join(homedir(), ".config", "fish", "conf.d", "lacy.fish"),
|
|
61
|
+
];
|
|
15
62
|
|
|
16
63
|
const TOOLS = [
|
|
17
|
-
{ value:
|
|
18
|
-
{ value:
|
|
19
|
-
{ value:
|
|
20
|
-
{ value:
|
|
21
|
-
{ value:
|
|
22
|
-
{ value:
|
|
23
|
-
{ value:
|
|
24
|
-
{ value:
|
|
64
|
+
{ value: "lash", label: "lash", hint: "recommended" },
|
|
65
|
+
{ value: "claude", label: "claude", hint: "Claude Code CLI" },
|
|
66
|
+
{ value: "opencode", label: "opencode", hint: "OpenCode CLI" },
|
|
67
|
+
{ value: "gemini", label: "gemini", hint: "Google Gemini CLI" },
|
|
68
|
+
{ value: "codex", label: "codex", hint: "OpenAI Codex CLI" },
|
|
69
|
+
{ value: "custom", label: "Custom", hint: "enter your own command" },
|
|
70
|
+
{ value: "auto", label: "Auto-detect", hint: "use first available" },
|
|
71
|
+
{ value: "none", label: "None", hint: "I'll install one later" },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const MODES = [
|
|
75
|
+
{ value: "auto", label: "Auto", hint: "smart detection (recommended)" },
|
|
76
|
+
{ value: "shell", label: "Shell", hint: "all commands execute directly" },
|
|
77
|
+
{ value: "agent", label: "Agent", hint: "all input goes to AI" },
|
|
25
78
|
];
|
|
26
79
|
|
|
27
80
|
function commandExists(cmd) {
|
|
81
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(cmd)) return false;
|
|
28
82
|
try {
|
|
29
|
-
execSync(`command -v ${cmd}`, { stdio:
|
|
83
|
+
execSync(`command -v ${cmd}`, { stdio: "ignore" });
|
|
30
84
|
return true;
|
|
31
85
|
} catch {
|
|
32
86
|
return false;
|
|
@@ -41,7 +95,35 @@ function isInteractive() {
|
|
|
41
95
|
return process.stdin.isTTY && process.stdout.isTTY;
|
|
42
96
|
}
|
|
43
97
|
|
|
44
|
-
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Config helpers
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
function readConfigValue(key) {
|
|
103
|
+
if (!existsSync(CONFIG_FILE)) return "";
|
|
104
|
+
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
105
|
+
const match = content.match(new RegExp(`^[\\s]*${key}:\\s*(.*)$`, "m"));
|
|
106
|
+
if (!match) return "";
|
|
107
|
+
return match[1].replace(/["']/g, "").replace(/#.*/, "").trim();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writeConfigValue(key, value) {
|
|
111
|
+
if (!existsSync(CONFIG_FILE)) return;
|
|
112
|
+
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
113
|
+
const regex = new RegExp(`^(\\s*${key}:)\\s*.*$`, "m");
|
|
114
|
+
if (regex.test(content)) {
|
|
115
|
+
writeFileSync(CONFIG_FILE, content.replace(regex, `$1 ${value}`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Shell restart
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
async function restartShell(
|
|
124
|
+
message = "Restart shell now to apply changes?",
|
|
125
|
+
shellCmd = null,
|
|
126
|
+
) {
|
|
45
127
|
if (!isInteractive()) return;
|
|
46
128
|
|
|
47
129
|
const restart = await p.confirm({
|
|
@@ -52,15 +134,16 @@ async function restartShell(message = 'Restart shell now to apply changes?') {
|
|
|
52
134
|
if (p.isCancel(restart)) return;
|
|
53
135
|
|
|
54
136
|
if (restart) {
|
|
55
|
-
|
|
56
|
-
// Use
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
137
|
+
const cmd = shellCmd || getShellConfig(detectShell()).shellCmd;
|
|
138
|
+
// Use exec to replace the current process (no nested shell)
|
|
139
|
+
p.log.info(`Restarting ${cmd}...`);
|
|
140
|
+
try {
|
|
141
|
+
execSync(`exec ${cmd} -l`, { stdio: "inherit" });
|
|
142
|
+
} catch {
|
|
143
|
+
// exec replaces the process so this only runs if it fails
|
|
144
|
+
p.log.warn(`Could not restart. Please run: ${cmd} -l`);
|
|
145
|
+
}
|
|
146
|
+
process.exit(0);
|
|
64
147
|
}
|
|
65
148
|
}
|
|
66
149
|
|
|
@@ -68,46 +151,77 @@ async function restartShell(message = 'Restart shell now to apply changes?') {
|
|
|
68
151
|
// Uninstall
|
|
69
152
|
// ============================================================================
|
|
70
153
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
154
|
+
// Remove lacy lines from an RC file
|
|
155
|
+
function removeLacyFromFile(filePath) {
|
|
156
|
+
if (!existsSync(filePath)) return false;
|
|
157
|
+
const content = readFileSync(filePath, "utf-8");
|
|
158
|
+
if (!content.includes("lacy.plugin") && !content.includes(".lacy/bin"))
|
|
159
|
+
return false;
|
|
160
|
+
const cleaned = content
|
|
161
|
+
.split("\n")
|
|
162
|
+
.filter(
|
|
163
|
+
(line) =>
|
|
164
|
+
!line.includes("lacy.plugin") &&
|
|
165
|
+
line.trim() !== "# Lacy Shell" &&
|
|
166
|
+
!line.includes(".lacy/bin"),
|
|
167
|
+
)
|
|
168
|
+
.join("\n");
|
|
169
|
+
writeFileSync(filePath, cleaned);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
74
172
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
173
|
+
// Shared uninstall logic — removes RC lines and install dirs, optionally keeps config
|
|
174
|
+
async function doUninstall({ askConfirm = true } = {}) {
|
|
175
|
+
if (askConfirm) {
|
|
176
|
+
const confirm = await p.confirm({
|
|
177
|
+
message: "Are you sure you want to uninstall Lacy Shell?",
|
|
178
|
+
initialValue: false,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
182
|
+
p.cancel("Uninstall cancelled");
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
79
185
|
}
|
|
80
186
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
187
|
+
// Ask about config
|
|
188
|
+
let keepConfig = false;
|
|
189
|
+
if (existsSync(CONFIG_FILE)) {
|
|
190
|
+
const configChoice = await p.confirm({
|
|
191
|
+
message: "Keep configuration for future reinstall?",
|
|
192
|
+
initialValue: true,
|
|
193
|
+
});
|
|
194
|
+
if (!p.isCancel(configChoice)) {
|
|
195
|
+
keepConfig = configChoice;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
85
198
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
199
|
+
// Backup config if keeping
|
|
200
|
+
let configBackup = null;
|
|
201
|
+
if (keepConfig && existsSync(CONFIG_FILE)) {
|
|
202
|
+
configBackup = readFileSync(CONFIG_FILE, "utf-8");
|
|
89
203
|
}
|
|
90
204
|
|
|
91
|
-
// Remove from
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
205
|
+
// Remove from all possible RC files
|
|
206
|
+
const rcSpinner = p.spinner();
|
|
207
|
+
rcSpinner.start("Removing from shell configs");
|
|
208
|
+
|
|
209
|
+
let removedFrom = [];
|
|
210
|
+
for (const rcFile of ALL_RC_FILES) {
|
|
211
|
+
if (removeLacyFromFile(rcFile)) {
|
|
212
|
+
removedFrom.push(rcFile.split("/").pop());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (removedFrom.length > 0) {
|
|
217
|
+
rcSpinner.stop(`Removed from ${removedFrom.join(", ")}`);
|
|
104
218
|
} else {
|
|
105
|
-
|
|
219
|
+
rcSpinner.stop("No shell configs to clean");
|
|
106
220
|
}
|
|
107
221
|
|
|
108
222
|
// Remove installation directories
|
|
109
223
|
const removeSpinner = p.spinner();
|
|
110
|
-
removeSpinner.start(
|
|
224
|
+
removeSpinner.start("Removing installation");
|
|
111
225
|
|
|
112
226
|
if (existsSync(INSTALL_DIR)) {
|
|
113
227
|
rmSync(INSTALL_DIR, { recursive: true, force: true });
|
|
@@ -116,15 +230,38 @@ async function uninstall() {
|
|
|
116
230
|
rmSync(INSTALL_DIR_OLD, { recursive: true, force: true });
|
|
117
231
|
}
|
|
118
232
|
|
|
119
|
-
|
|
233
|
+
// Restore config if keeping
|
|
234
|
+
if (configBackup) {
|
|
235
|
+
mkdirSync(INSTALL_DIR, { recursive: true });
|
|
236
|
+
writeFileSync(CONFIG_FILE, configBackup);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
removeSpinner.stop(
|
|
240
|
+
configBackup ? "Installation removed (config preserved)" : "Installation removed",
|
|
241
|
+
);
|
|
120
242
|
|
|
121
|
-
p.log.success(
|
|
243
|
+
p.log.success("Lacy Shell uninstalled");
|
|
122
244
|
|
|
123
|
-
await restartShell(
|
|
245
|
+
await restartShell("Restart shell now?");
|
|
124
246
|
|
|
125
|
-
p.outro(
|
|
247
|
+
p.outro("Restart your terminal to apply changes.");
|
|
126
248
|
}
|
|
127
249
|
|
|
250
|
+
async function uninstall() {
|
|
251
|
+
console.clear();
|
|
252
|
+
p.intro(pc.magenta(pc.bold(` Lacy Shell `)));
|
|
253
|
+
|
|
254
|
+
if (!isInstalled()) {
|
|
255
|
+
p.log.warn("Lacy Shell is not installed");
|
|
256
|
+
p.outro("Nothing to uninstall");
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await doUninstall();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// setup() is handled by the already-installed dashboard in main()
|
|
264
|
+
|
|
128
265
|
// ============================================================================
|
|
129
266
|
// Install
|
|
130
267
|
// ============================================================================
|
|
@@ -133,102 +270,137 @@ async function install() {
|
|
|
133
270
|
console.clear();
|
|
134
271
|
p.intro(pc.magenta(pc.bold(` Lacy Shell `)));
|
|
135
272
|
|
|
273
|
+
// Detect shell
|
|
274
|
+
const shell = detectShell();
|
|
275
|
+
const shellConfig = getShellConfig(shell);
|
|
276
|
+
p.log.info(`Detected shell: ${pc.cyan(shell)}`);
|
|
277
|
+
|
|
136
278
|
// Check prerequisites
|
|
137
279
|
const prerequisites = p.spinner();
|
|
138
|
-
prerequisites.start(
|
|
280
|
+
prerequisites.start("Checking prerequisites");
|
|
139
281
|
|
|
140
282
|
const missing = [];
|
|
141
|
-
|
|
142
|
-
|
|
283
|
+
|
|
284
|
+
// Check for the target shell
|
|
285
|
+
if (shell === "bash") {
|
|
286
|
+
if (commandExists("bash")) {
|
|
287
|
+
try {
|
|
288
|
+
const bashVer = execSync('bash -c "echo ${BASH_VERSINFO[0]}"', {
|
|
289
|
+
stdio: "pipe",
|
|
290
|
+
})
|
|
291
|
+
.toString()
|
|
292
|
+
.trim();
|
|
293
|
+
if (parseInt(bashVer) < 4) {
|
|
294
|
+
missing.push(
|
|
295
|
+
`bash 4+ (found bash ${bashVer}, upgrade with: brew install bash)`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
missing.push("bash 4+");
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
missing.push("bash");
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
if (!commandExists("zsh")) missing.push("zsh");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!commandExists("git")) missing.push("git");
|
|
143
309
|
|
|
144
310
|
if (missing.length > 0) {
|
|
145
|
-
prerequisites.stop(
|
|
146
|
-
p.log.error(`Missing required tools: ${missing.join(
|
|
147
|
-
p.outro(pc.red(
|
|
311
|
+
prerequisites.stop("Prerequisites check failed");
|
|
312
|
+
p.log.error(`Missing required tools: ${missing.join(", ")}`);
|
|
313
|
+
p.outro(pc.red("Please install missing prerequisites and try again."));
|
|
148
314
|
process.exit(1);
|
|
149
315
|
}
|
|
150
316
|
|
|
151
|
-
prerequisites.stop(
|
|
317
|
+
prerequisites.stop("Prerequisites OK");
|
|
152
318
|
|
|
153
319
|
// Detect installed tools
|
|
154
320
|
let detected = [];
|
|
155
|
-
for (const tool of [
|
|
321
|
+
for (const tool of ["lash", "claude", "opencode", "gemini", "codex"]) {
|
|
156
322
|
if (commandExists(tool)) {
|
|
157
323
|
detected.push(tool);
|
|
158
324
|
}
|
|
159
325
|
}
|
|
160
326
|
|
|
161
327
|
if (detected.length > 0) {
|
|
162
|
-
p.log.info(`Detected: ${detected.map(t => pc.green(t)).join(
|
|
328
|
+
p.log.info(`Detected: ${detected.map((t) => pc.green(t)).join(", ")}`);
|
|
163
329
|
} else {
|
|
164
|
-
p.log.warn(
|
|
165
|
-
p.log.info(
|
|
330
|
+
p.log.warn("No AI CLI tools detected");
|
|
331
|
+
p.log.info("Lacy Shell requires an AI CLI tool to work.");
|
|
166
332
|
|
|
167
333
|
const installLashNow = await p.confirm({
|
|
168
|
-
message: `Would you like to install ${pc.green(
|
|
334
|
+
message: `Would you like to install ${pc.green("lash")} (recommended)?`,
|
|
169
335
|
initialValue: true,
|
|
170
336
|
});
|
|
171
337
|
|
|
172
338
|
if (p.isCancel(installLashNow)) {
|
|
173
|
-
p.cancel(
|
|
339
|
+
p.cancel("Installation cancelled");
|
|
174
340
|
process.exit(0);
|
|
175
341
|
}
|
|
176
342
|
|
|
177
343
|
if (installLashNow) {
|
|
178
344
|
const lashSpinner = p.spinner();
|
|
179
|
-
lashSpinner.start(
|
|
345
|
+
lashSpinner.start("Installing lash");
|
|
180
346
|
|
|
181
347
|
try {
|
|
182
|
-
if (commandExists(
|
|
183
|
-
execSync(
|
|
184
|
-
lashSpinner.stop(
|
|
185
|
-
detected.push(
|
|
186
|
-
} else if (commandExists(
|
|
187
|
-
execSync(
|
|
188
|
-
|
|
189
|
-
|
|
348
|
+
if (commandExists("npm")) {
|
|
349
|
+
execSync("npm install -g lash-cli", { stdio: "pipe" });
|
|
350
|
+
lashSpinner.stop("lash installed");
|
|
351
|
+
detected.push("lash");
|
|
352
|
+
} else if (commandExists("brew")) {
|
|
353
|
+
execSync("brew tap lacymorrow/tap && brew install lash", {
|
|
354
|
+
stdio: "pipe",
|
|
355
|
+
});
|
|
356
|
+
lashSpinner.stop("lash installed");
|
|
357
|
+
detected.push("lash");
|
|
190
358
|
} else {
|
|
191
|
-
lashSpinner.stop(
|
|
192
|
-
p.log.warn(
|
|
359
|
+
lashSpinner.stop("Could not install lash");
|
|
360
|
+
p.log.warn(
|
|
361
|
+
"Please install npm or homebrew, then run: npm install -g lash-cli",
|
|
362
|
+
);
|
|
193
363
|
}
|
|
194
364
|
} catch (e) {
|
|
195
|
-
lashSpinner.stop(
|
|
196
|
-
p.log.warn(
|
|
365
|
+
lashSpinner.stop("lash installation failed");
|
|
366
|
+
p.log.warn(
|
|
367
|
+
"You can install it manually later: npm install -g lash-cli",
|
|
368
|
+
);
|
|
197
369
|
}
|
|
198
370
|
}
|
|
199
371
|
}
|
|
200
372
|
|
|
201
373
|
// Tool selection
|
|
202
374
|
const selectedTool = await p.select({
|
|
203
|
-
message:
|
|
204
|
-
options: TOOLS.map(t => ({
|
|
375
|
+
message: "Which AI CLI tool do you want to use?",
|
|
376
|
+
options: TOOLS.map((t) => ({
|
|
205
377
|
value: t.value,
|
|
206
378
|
label: t.label,
|
|
207
|
-
hint: detected.includes(t.value)
|
|
208
|
-
? pc.green('installed')
|
|
209
|
-
: t.hint,
|
|
379
|
+
hint: detected.includes(t.value) ? pc.green("installed") : t.hint,
|
|
210
380
|
})),
|
|
211
|
-
initialValue: detected[0] ||
|
|
381
|
+
initialValue: detected[0] || "lash",
|
|
212
382
|
});
|
|
213
383
|
|
|
214
384
|
if (p.isCancel(selectedTool)) {
|
|
215
|
-
p.cancel(
|
|
385
|
+
p.cancel("Installation cancelled");
|
|
216
386
|
process.exit(0);
|
|
217
387
|
}
|
|
218
388
|
|
|
219
389
|
// Prompt for custom command if selected
|
|
220
|
-
let customCommand =
|
|
221
|
-
if (selectedTool ===
|
|
390
|
+
let customCommand = "";
|
|
391
|
+
if (selectedTool === "custom") {
|
|
222
392
|
customCommand = await p.text({
|
|
223
|
-
message:
|
|
224
|
-
|
|
393
|
+
message:
|
|
394
|
+
"Enter your custom command (query will be appended as a quoted argument):",
|
|
395
|
+
placeholder: "claude --dangerously-skip-permissions -p",
|
|
225
396
|
validate(value) {
|
|
226
|
-
if (!value || value.trim().length === 0)
|
|
397
|
+
if (!value || value.trim().length === 0)
|
|
398
|
+
return "Command cannot be empty";
|
|
227
399
|
},
|
|
228
400
|
});
|
|
229
401
|
|
|
230
402
|
if (p.isCancel(customCommand)) {
|
|
231
|
-
p.cancel(
|
|
403
|
+
p.cancel("Installation cancelled");
|
|
232
404
|
process.exit(0);
|
|
233
405
|
}
|
|
234
406
|
|
|
@@ -236,96 +408,137 @@ async function install() {
|
|
|
236
408
|
}
|
|
237
409
|
|
|
238
410
|
// Offer to install lash if selected but not installed
|
|
239
|
-
if (selectedTool ===
|
|
411
|
+
if (selectedTool === "lash" && !commandExists("lash")) {
|
|
240
412
|
const installLash = await p.confirm({
|
|
241
|
-
message:
|
|
413
|
+
message: "lash is not installed. Would you like to install it now?",
|
|
242
414
|
initialValue: true,
|
|
243
415
|
});
|
|
244
416
|
|
|
245
417
|
if (p.isCancel(installLash)) {
|
|
246
|
-
p.cancel(
|
|
418
|
+
p.cancel("Installation cancelled");
|
|
247
419
|
process.exit(0);
|
|
248
420
|
}
|
|
249
421
|
|
|
250
422
|
if (installLash) {
|
|
251
423
|
const lashSpinner = p.spinner();
|
|
252
|
-
lashSpinner.start(
|
|
424
|
+
lashSpinner.start("Installing lash");
|
|
253
425
|
|
|
254
426
|
try {
|
|
255
|
-
if (commandExists(
|
|
256
|
-
execSync(
|
|
257
|
-
lashSpinner.stop(
|
|
258
|
-
} else if (commandExists(
|
|
259
|
-
execSync(
|
|
260
|
-
|
|
427
|
+
if (commandExists("npm")) {
|
|
428
|
+
execSync("npm install -g lash-cli", { stdio: "pipe" });
|
|
429
|
+
lashSpinner.stop("lash installed");
|
|
430
|
+
} else if (commandExists("brew")) {
|
|
431
|
+
execSync("brew tap lacymorrow/tap && brew install lash", {
|
|
432
|
+
stdio: "pipe",
|
|
433
|
+
});
|
|
434
|
+
lashSpinner.stop("lash installed");
|
|
261
435
|
} else {
|
|
262
|
-
lashSpinner.stop(
|
|
263
|
-
p.log.warn(
|
|
436
|
+
lashSpinner.stop("Could not install lash");
|
|
437
|
+
p.log.warn(
|
|
438
|
+
"Please install npm or homebrew, then run: npm install -g lash-cli",
|
|
439
|
+
);
|
|
264
440
|
}
|
|
265
441
|
} catch (e) {
|
|
266
|
-
lashSpinner.stop(
|
|
267
|
-
p.log.warn(
|
|
442
|
+
lashSpinner.stop("lash installation failed");
|
|
443
|
+
p.log.warn(
|
|
444
|
+
"You can install it manually later: npm install -g lash-cli",
|
|
445
|
+
);
|
|
268
446
|
}
|
|
269
447
|
}
|
|
270
448
|
}
|
|
271
449
|
|
|
272
450
|
// Clone/update repository
|
|
273
451
|
const installSpinner = p.spinner();
|
|
274
|
-
installSpinner.start(
|
|
452
|
+
installSpinner.start("Installing Lacy");
|
|
275
453
|
|
|
276
454
|
try {
|
|
277
455
|
if (existsSync(INSTALL_DIR)) {
|
|
278
456
|
// Update existing
|
|
279
457
|
try {
|
|
280
|
-
execSync(
|
|
458
|
+
execSync("git pull origin main", { cwd: INSTALL_DIR, stdio: "pipe" });
|
|
281
459
|
} catch {
|
|
282
460
|
// Ignore pull errors, use existing
|
|
283
461
|
}
|
|
284
|
-
installSpinner.stop(
|
|
462
|
+
installSpinner.stop("Lacy updated");
|
|
285
463
|
} else {
|
|
286
|
-
execSync(`git clone --depth 1 ${REPO_URL} "${INSTALL_DIR}"`, {
|
|
287
|
-
|
|
464
|
+
execSync(`git clone --depth 1 ${REPO_URL} "${INSTALL_DIR}"`, {
|
|
465
|
+
stdio: "pipe",
|
|
466
|
+
});
|
|
467
|
+
installSpinner.stop("Lacy installed");
|
|
288
468
|
}
|
|
289
469
|
} catch (e) {
|
|
290
|
-
installSpinner.stop(
|
|
470
|
+
installSpinner.stop("Installation failed");
|
|
291
471
|
p.log.error(`Could not clone repository: ${e.message}`);
|
|
292
|
-
p.outro(pc.red(
|
|
472
|
+
p.outro(pc.red("Installation failed"));
|
|
293
473
|
process.exit(1);
|
|
294
474
|
}
|
|
295
475
|
|
|
296
|
-
// Configure
|
|
297
|
-
const
|
|
298
|
-
|
|
476
|
+
// Configure shell RC file
|
|
477
|
+
const shellSpinner = p.spinner();
|
|
478
|
+
shellSpinner.start(`Configuring ${shell}`);
|
|
479
|
+
|
|
480
|
+
const { rcFile, extraRcFile, pluginFile, rcName } = shellConfig;
|
|
481
|
+
const sourceLine = `source ${INSTALL_DIR}/${pluginFile}`;
|
|
482
|
+
const pathLine = `export PATH="${INSTALL_DIR}/bin:$PATH"`;
|
|
299
483
|
|
|
300
|
-
|
|
484
|
+
// Ensure parent directory exists (for fish: ~/.config/fish/conf.d/)
|
|
485
|
+
const rcDir = rcFile.substring(0, rcFile.lastIndexOf("/"));
|
|
486
|
+
mkdirSync(rcDir, { recursive: true });
|
|
301
487
|
|
|
302
|
-
if (existsSync(
|
|
303
|
-
const
|
|
488
|
+
if (existsSync(rcFile)) {
|
|
489
|
+
const rcContent = readFileSync(rcFile, "utf-8");
|
|
304
490
|
|
|
305
|
-
if (
|
|
306
|
-
|
|
491
|
+
if (rcContent.includes("lacy.plugin")) {
|
|
492
|
+
shellSpinner.stop("Already configured");
|
|
493
|
+
|
|
494
|
+
// Add PATH if missing (upgrade from older install)
|
|
495
|
+
if (!rcContent.includes(".lacy/bin")) {
|
|
496
|
+
appendFileSync(rcFile, `${pathLine}\n`);
|
|
497
|
+
}
|
|
307
498
|
} else {
|
|
308
|
-
appendFileSync(
|
|
309
|
-
|
|
499
|
+
appendFileSync(rcFile, `\n# Lacy Shell\n${sourceLine}\n${pathLine}\n`);
|
|
500
|
+
shellSpinner.stop(`Added to ${rcName}`);
|
|
310
501
|
}
|
|
311
502
|
} else {
|
|
312
|
-
writeFileSync(
|
|
313
|
-
|
|
503
|
+
writeFileSync(rcFile, `# Lacy Shell\n${sourceLine}\n${pathLine}\n`);
|
|
504
|
+
shellSpinner.stop(`Created ${rcName}`);
|
|
314
505
|
}
|
|
315
506
|
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
507
|
+
// For Bash on macOS, also add to .bashrc if it exists
|
|
508
|
+
if (
|
|
509
|
+
extraRcFile &&
|
|
510
|
+
existsSync(extraRcFile) &&
|
|
511
|
+
!readFileSync(extraRcFile, "utf-8").includes("lacy.plugin")
|
|
512
|
+
) {
|
|
513
|
+
appendFileSync(extraRcFile, `\n# Lacy Shell\n${sourceLine}\n${pathLine}\n`);
|
|
514
|
+
}
|
|
319
515
|
|
|
516
|
+
// Create or update config
|
|
517
|
+
const configSpinner = p.spinner();
|
|
320
518
|
mkdirSync(INSTALL_DIR, { recursive: true });
|
|
321
519
|
|
|
322
|
-
const activeToolValue =
|
|
520
|
+
const activeToolValue =
|
|
521
|
+
selectedTool === "auto" || selectedTool === "none" ? "" : selectedTool;
|
|
323
522
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
523
|
+
if (existsSync(CONFIG_FILE)) {
|
|
524
|
+
// Preserve existing config, only update tool selection
|
|
525
|
+
configSpinner.start("Updating configuration");
|
|
526
|
+
if (activeToolValue) {
|
|
527
|
+
writeConfigValue("active", activeToolValue);
|
|
528
|
+
if (selectedTool === "custom" && customCommand) {
|
|
529
|
+
writeConfigValue("custom_command", `"${customCommand}"`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
configSpinner.stop("Configuration preserved");
|
|
533
|
+
} else {
|
|
534
|
+
configSpinner.start("Creating configuration");
|
|
535
|
+
|
|
536
|
+
const customCommandLine =
|
|
537
|
+
selectedTool === "custom" && customCommand
|
|
538
|
+
? ` custom_command: "${customCommand}"`
|
|
539
|
+
: ` # custom_command: "your-command -flags"`;
|
|
327
540
|
|
|
328
|
-
|
|
541
|
+
const configContent = `# Lacy Shell Configuration
|
|
329
542
|
# https://github.com/lacymorrow/lacy
|
|
330
543
|
|
|
331
544
|
# AI CLI tool selection
|
|
@@ -349,31 +562,36 @@ auto_detection:
|
|
|
349
562
|
confidence_threshold: 0.7
|
|
350
563
|
`;
|
|
351
564
|
|
|
352
|
-
|
|
353
|
-
|
|
565
|
+
writeFileSync(CONFIG_FILE, configContent);
|
|
566
|
+
configSpinner.stop("Configuration created");
|
|
567
|
+
}
|
|
354
568
|
|
|
355
569
|
// Success message
|
|
356
|
-
p.log.success(pc.green(
|
|
570
|
+
p.log.success(pc.green("Installation complete!"));
|
|
357
571
|
|
|
358
572
|
p.note(
|
|
359
|
-
`${pc.cyan(
|
|
360
|
-
${pc.cyan(
|
|
573
|
+
`${pc.cyan("what files are here")} ${pc.dim("→ AI answers")}
|
|
574
|
+
${pc.cyan("ls -la")} ${pc.dim("→ runs in shell")}
|
|
361
575
|
|
|
362
576
|
Commands:
|
|
363
|
-
${pc.cyan(
|
|
364
|
-
${pc.cyan(
|
|
365
|
-
${pc.cyan('ask "q"')}
|
|
366
|
-
|
|
577
|
+
${pc.cyan("mode")} ${pc.dim("Show/change mode")}
|
|
578
|
+
${pc.cyan("tool")} ${pc.dim("Show/change AI tool")}
|
|
579
|
+
${pc.cyan('ask "q"')} ${pc.dim("Direct query to AI")}
|
|
580
|
+
${pc.cyan("lacy setup")} ${pc.dim("Interactive settings")}`,
|
|
581
|
+
"Try it",
|
|
367
582
|
);
|
|
368
583
|
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
584
|
+
if (
|
|
585
|
+
selectedTool === "none" ||
|
|
586
|
+
(selectedTool === "auto" && detected.length === 0)
|
|
587
|
+
) {
|
|
588
|
+
p.log.warn("Remember to install an AI CLI tool:");
|
|
589
|
+
console.log(` ${pc.cyan("npm install -g lash-cli")}`);
|
|
372
590
|
}
|
|
373
591
|
|
|
374
592
|
await restartShell();
|
|
375
593
|
|
|
376
|
-
p.outro(pc.dim(
|
|
594
|
+
p.outro(pc.dim("Learn more: https://github.com/lacymorrow/lacy"));
|
|
377
595
|
}
|
|
378
596
|
|
|
379
597
|
// ============================================================================
|
|
@@ -383,139 +601,299 @@ Commands:
|
|
|
383
601
|
async function main() {
|
|
384
602
|
const args = process.argv.slice(2);
|
|
385
603
|
|
|
386
|
-
// Handle
|
|
387
|
-
if (args
|
|
604
|
+
// Handle info subcommand
|
|
605
|
+
if (args[0] === "info") {
|
|
606
|
+
const infoPath = join(INSTALL_DIR, "lib/commands/info.sh");
|
|
607
|
+
if (existsSync(infoPath)) {
|
|
608
|
+
const content = readFileSync(infoPath, "utf-8");
|
|
609
|
+
console.log(content);
|
|
610
|
+
} else {
|
|
611
|
+
console.log(`\n${pc.magenta(pc.bold("🔧 Lacy Shell"))} v${require("./package.json").version || "1.6.5"}\n`);
|
|
612
|
+
console.log("Lacy Shell detects natural language and routes it to AI coding agents.\n");
|
|
613
|
+
console.log("Quick tips:");
|
|
614
|
+
console.log(" • Type normally for shell commands");
|
|
615
|
+
console.log(" • Type natural language for AI assistance");
|
|
616
|
+
console.log(" • Press Ctrl+Space to toggle modes\n");
|
|
617
|
+
console.log(`Run '${pc.cyan("lacy setup")}' to configure your AI tool and settings.`);
|
|
618
|
+
console.log(`Run '${pc.cyan("lacy mode")}' to see current mode and legend.`);
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Handle uninstall subcommand/flag
|
|
624
|
+
if (args[0] === "uninstall") {
|
|
388
625
|
await uninstall();
|
|
389
626
|
return;
|
|
390
627
|
}
|
|
391
628
|
|
|
392
|
-
if (args.includes(
|
|
629
|
+
if (args.includes("--uninstall") || args.includes("-u")) {
|
|
630
|
+
await uninstall();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
393
635
|
console.log(`
|
|
394
|
-
${pc.magenta(pc.bold(
|
|
636
|
+
${pc.magenta(pc.bold("Lacy Shell"))} - Talk directly to your shell
|
|
395
637
|
|
|
396
|
-
${pc.bold(
|
|
638
|
+
${pc.bold("Usage:")}
|
|
397
639
|
npx lacy Install Lacy Shell
|
|
398
640
|
npx lacy --uninstall Uninstall Lacy Shell
|
|
641
|
+
npx lacy setup Interactive settings
|
|
399
642
|
|
|
400
|
-
${pc.bold(
|
|
643
|
+
${pc.bold("Options:")}
|
|
401
644
|
-h, --help Show this help message
|
|
402
645
|
-u, --uninstall Uninstall Lacy Shell
|
|
403
646
|
|
|
404
|
-
${pc.bold(
|
|
647
|
+
${pc.bold("Commands:")}
|
|
648
|
+
setup Interactive settings (tool, mode, config)
|
|
649
|
+
info Show basic information and help
|
|
650
|
+
|
|
651
|
+
${pc.bold("Other install methods:")}
|
|
405
652
|
curl -fsSL https://lacy.sh/install | bash
|
|
406
653
|
brew install lacymorrow/tap/lacy
|
|
407
654
|
|
|
408
|
-
${pc.dim(
|
|
655
|
+
${pc.dim("https://github.com/lacymorrow/lacy")}
|
|
409
656
|
`);
|
|
410
657
|
return;
|
|
411
658
|
}
|
|
412
659
|
|
|
413
|
-
// If already installed,
|
|
660
|
+
// If already installed, show dashboard + menu
|
|
414
661
|
if (isInstalled()) {
|
|
415
662
|
console.clear();
|
|
416
663
|
p.intro(pc.magenta(pc.bold(` Lacy Shell `)));
|
|
417
664
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
{ value: 'cancel', label: 'Cancel', hint: 'do nothing' },
|
|
425
|
-
],
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
if (p.isCancel(action) || action === 'cancel') {
|
|
429
|
-
p.cancel('Cancelled');
|
|
430
|
-
process.exit(0);
|
|
665
|
+
// Show current status
|
|
666
|
+
const active = readConfigValue("active");
|
|
667
|
+
const mode = readConfigValue("default");
|
|
668
|
+
const detected = [];
|
|
669
|
+
for (const tool of ["lash", "claude", "opencode", "gemini", "codex"]) {
|
|
670
|
+
if (commandExists(tool)) detected.push(tool);
|
|
431
671
|
}
|
|
432
672
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
673
|
+
const toolDisplay = active || "auto-detect";
|
|
674
|
+
const modeDisplay = mode || "auto";
|
|
675
|
+
const toolsDisplay =
|
|
676
|
+
detected.length > 0
|
|
677
|
+
? detected.map((t) => pc.green(t)).join(", ")
|
|
678
|
+
: pc.yellow("none");
|
|
679
|
+
|
|
680
|
+
p.note(
|
|
681
|
+
` Tool: ${pc.cyan(toolDisplay)}
|
|
682
|
+
Mode: ${pc.cyan(modeDisplay)}
|
|
683
|
+
Installed: ${toolsDisplay}`,
|
|
684
|
+
"Current config",
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
let loop = true;
|
|
688
|
+
while (loop) {
|
|
689
|
+
const action = await p.select({
|
|
690
|
+
message: "What would you like to do?",
|
|
691
|
+
options: [
|
|
692
|
+
{
|
|
693
|
+
value: "tool",
|
|
694
|
+
label: "Change AI tool",
|
|
695
|
+
hint: `current: ${active || "auto-detect"}`,
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
value: "mode",
|
|
699
|
+
label: "Change mode",
|
|
700
|
+
hint: `current: ${modeDisplay}`,
|
|
701
|
+
},
|
|
702
|
+
{ value: "config", label: "Edit config", hint: "open in $EDITOR" },
|
|
703
|
+
{
|
|
704
|
+
value: "status",
|
|
705
|
+
label: "Status",
|
|
706
|
+
hint: "show full installation info",
|
|
707
|
+
},
|
|
708
|
+
{ value: "update", label: "Update", hint: "pull latest changes" },
|
|
709
|
+
{
|
|
710
|
+
value: "reinstall",
|
|
711
|
+
label: "Reinstall",
|
|
712
|
+
hint: "fresh installation",
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
value: "uninstall",
|
|
716
|
+
label: "Uninstall",
|
|
717
|
+
hint: "remove Lacy Shell",
|
|
718
|
+
},
|
|
719
|
+
{ value: "done", label: "Done" },
|
|
720
|
+
],
|
|
438
721
|
});
|
|
439
722
|
|
|
440
|
-
if (p.isCancel(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Remove from .zshrc
|
|
446
|
-
const zshrcSpinner = p.spinner();
|
|
447
|
-
zshrcSpinner.start('Removing from .zshrc');
|
|
448
|
-
|
|
449
|
-
if (existsSync(ZSHRC)) {
|
|
450
|
-
let content = readFileSync(ZSHRC, 'utf-8');
|
|
451
|
-
content = content
|
|
452
|
-
.split('\n')
|
|
453
|
-
.filter(line => !line.includes('lacy.plugin.zsh') && line.trim() !== '# Lacy Shell')
|
|
454
|
-
.join('\n');
|
|
455
|
-
writeFileSync(ZSHRC, content);
|
|
456
|
-
zshrcSpinner.stop('Removed from .zshrc');
|
|
457
|
-
} else {
|
|
458
|
-
zshrcSpinner.stop('No .zshrc found');
|
|
723
|
+
if (p.isCancel(action) || action === "done") {
|
|
724
|
+
loop = false;
|
|
725
|
+
break;
|
|
459
726
|
}
|
|
460
727
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
728
|
+
if (action === "tool") {
|
|
729
|
+
const selectedTool = await p.select({
|
|
730
|
+
message: "Which AI CLI tool do you want to use?",
|
|
731
|
+
options: TOOLS.filter((t) => t.value !== "none").map((t) => ({
|
|
732
|
+
value: t.value,
|
|
733
|
+
label: t.label,
|
|
734
|
+
hint: detected.includes(t.value) ? pc.green("installed") : t.hint,
|
|
735
|
+
})),
|
|
736
|
+
initialValue: active || detected[0] || "auto",
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
if (p.isCancel(selectedTool)) continue;
|
|
740
|
+
|
|
741
|
+
if (selectedTool === "custom") {
|
|
742
|
+
const customCmd = await p.text({
|
|
743
|
+
message:
|
|
744
|
+
"Enter your custom command (query will be appended as a quoted argument):",
|
|
745
|
+
placeholder: "claude --dangerously-skip-permissions -p",
|
|
746
|
+
validate(value) {
|
|
747
|
+
if (!value || value.trim().length === 0)
|
|
748
|
+
return "Command cannot be empty";
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
if (p.isCancel(customCmd)) continue;
|
|
752
|
+
writeConfigValue("active", "custom");
|
|
753
|
+
writeConfigValue("custom_command", `"${customCmd}"`);
|
|
754
|
+
p.log.success(`Tool set to: ${pc.cyan("custom")} (${customCmd})`);
|
|
755
|
+
} else if (selectedTool === "auto") {
|
|
756
|
+
writeConfigValue("active", "");
|
|
757
|
+
p.log.success(`Tool set to: ${pc.cyan("auto-detect")}`);
|
|
758
|
+
} else {
|
|
759
|
+
writeConfigValue("active", selectedTool);
|
|
760
|
+
p.log.success(`Tool set to: ${pc.cyan(selectedTool)}`);
|
|
761
|
+
}
|
|
464
762
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
if (existsSync(INSTALL_DIR_OLD)) {
|
|
469
|
-
rmSync(INSTALL_DIR_OLD, { recursive: true, force: true });
|
|
763
|
+
await restartShell("Restart shell now to apply changes?");
|
|
764
|
+
loop = false;
|
|
470
765
|
}
|
|
471
766
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
767
|
+
if (action === "mode") {
|
|
768
|
+
const selectedMode = await p.select({
|
|
769
|
+
message: "Which default mode?",
|
|
770
|
+
options: MODES.map((m) => ({
|
|
771
|
+
value: m.value,
|
|
772
|
+
label: m.label,
|
|
773
|
+
hint: m.hint,
|
|
774
|
+
})),
|
|
775
|
+
initialValue: mode || "auto",
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
if (p.isCancel(selectedMode)) continue;
|
|
779
|
+
|
|
780
|
+
writeConfigValue(
|
|
781
|
+
"default",
|
|
782
|
+
`${selectedMode} # Options: shell, agent, auto`,
|
|
783
|
+
);
|
|
784
|
+
p.log.success(`Mode set to: ${pc.cyan(selectedMode)}`);
|
|
785
|
+
|
|
786
|
+
await restartShell("Restart shell now to apply changes?");
|
|
787
|
+
loop = false;
|
|
788
|
+
}
|
|
488
789
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
790
|
+
if (action === "config") {
|
|
791
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
792
|
+
p.log.info(`Opening ${pc.cyan(CONFIG_FILE)} in ${editor}...`);
|
|
793
|
+
try {
|
|
794
|
+
execSync(`${editor} "${CONFIG_FILE}"`, { stdio: "inherit" });
|
|
795
|
+
} catch {
|
|
796
|
+
p.log.warn("Editor closed");
|
|
797
|
+
}
|
|
493
798
|
|
|
494
|
-
await restartShell();
|
|
799
|
+
await restartShell("Restart shell now to apply changes?");
|
|
800
|
+
loop = false;
|
|
801
|
+
}
|
|
495
802
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
803
|
+
if (action === "status") {
|
|
804
|
+
const dir = existsSync(INSTALL_DIR) ? INSTALL_DIR : INSTALL_DIR_OLD;
|
|
805
|
+
let sha = "";
|
|
806
|
+
try {
|
|
807
|
+
sha = execSync("git rev-parse --short HEAD", {
|
|
808
|
+
cwd: dir,
|
|
809
|
+
stdio: "pipe",
|
|
810
|
+
})
|
|
811
|
+
.toString()
|
|
812
|
+
.trim();
|
|
813
|
+
} catch {}
|
|
814
|
+
|
|
815
|
+
const shell = detectShell();
|
|
816
|
+
const rc = getShellConfig(shell).rcFile;
|
|
817
|
+
const rcConfigured =
|
|
818
|
+
existsSync(rc) && readFileSync(rc, "utf-8").includes("lacy.plugin");
|
|
819
|
+
const hasConfig = existsSync(CONFIG_FILE);
|
|
820
|
+
|
|
821
|
+
const lines = [
|
|
822
|
+
` Installed: ${pc.green(dir)}`,
|
|
823
|
+
sha ? ` Version: git ${pc.dim(sha)}` : null,
|
|
824
|
+
` Shell: ${pc.cyan(shell)} ${rcConfigured ? pc.green("configured") : pc.yellow("not configured")}`,
|
|
825
|
+
` Config: ${hasConfig ? pc.green("exists") : pc.yellow("missing")}`,
|
|
826
|
+
` Tool: ${pc.cyan(active || "auto-detect")}`,
|
|
827
|
+
` Mode: ${pc.cyan(modeDisplay)}`,
|
|
828
|
+
``,
|
|
829
|
+
` ${pc.bold("AI CLI tools:")}`,
|
|
830
|
+
...["lash", "claude", "opencode", "gemini", "codex"].map((t) =>
|
|
831
|
+
commandExists(t)
|
|
832
|
+
? ` ${pc.green("✓")} ${t}`
|
|
833
|
+
: ` ${pc.dim("○")} ${pc.dim(t)}`,
|
|
834
|
+
),
|
|
835
|
+
].filter(Boolean);
|
|
836
|
+
|
|
837
|
+
p.note(lines.join("\n"), "Status");
|
|
838
|
+
// Don't break, let user pick another action
|
|
501
839
|
}
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
840
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
841
|
+
if (action === "uninstall") {
|
|
842
|
+
await doUninstall();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
509
845
|
|
|
510
|
-
if (
|
|
511
|
-
|
|
846
|
+
if (action === "update") {
|
|
847
|
+
const updateSpinner = p.spinner();
|
|
848
|
+
updateSpinner.start("Updating Lacy");
|
|
849
|
+
const updateDir = existsSync(INSTALL_DIR)
|
|
850
|
+
? INSTALL_DIR
|
|
851
|
+
: INSTALL_DIR_OLD;
|
|
852
|
+
try {
|
|
853
|
+
execSync("git pull origin main", { cwd: updateDir, stdio: "pipe" });
|
|
854
|
+
updateSpinner.stop("Lacy updated");
|
|
855
|
+
p.log.success("Update complete!");
|
|
856
|
+
await restartShell();
|
|
857
|
+
p.outro("Restart your terminal to apply changes.");
|
|
858
|
+
} catch {
|
|
859
|
+
updateSpinner.stop("Update failed");
|
|
860
|
+
p.log.error("Could not update. Try reinstalling instead.");
|
|
861
|
+
}
|
|
862
|
+
return;
|
|
512
863
|
}
|
|
513
|
-
|
|
514
|
-
|
|
864
|
+
|
|
865
|
+
if (action === "reinstall") {
|
|
866
|
+
const removeSpinner = p.spinner();
|
|
867
|
+
removeSpinner.start("Removing existing installation");
|
|
868
|
+
// Backup config before removing
|
|
869
|
+
let configBackup = null;
|
|
870
|
+
if (existsSync(CONFIG_FILE)) {
|
|
871
|
+
configBackup = readFileSync(CONFIG_FILE, "utf-8");
|
|
872
|
+
}
|
|
873
|
+
if (existsSync(INSTALL_DIR)) {
|
|
874
|
+
rmSync(INSTALL_DIR, { recursive: true, force: true });
|
|
875
|
+
}
|
|
876
|
+
if (existsSync(INSTALL_DIR_OLD)) {
|
|
877
|
+
rmSync(INSTALL_DIR_OLD, { recursive: true, force: true });
|
|
878
|
+
}
|
|
879
|
+
// Restore config so install() sees it and preserves it
|
|
880
|
+
if (configBackup) {
|
|
881
|
+
mkdirSync(INSTALL_DIR, { recursive: true });
|
|
882
|
+
writeFileSync(CONFIG_FILE, configBackup);
|
|
883
|
+
}
|
|
884
|
+
removeSpinner.stop("Removed");
|
|
885
|
+
loop = false;
|
|
886
|
+
// Falls through to install()
|
|
515
887
|
}
|
|
888
|
+
}
|
|
516
889
|
|
|
517
|
-
|
|
890
|
+
// Only reach here if reinstall was selected (or loop ended without return)
|
|
891
|
+
if (!isInstalled()) {
|
|
892
|
+
await install();
|
|
893
|
+
} else {
|
|
894
|
+
p.outro(pc.dim("https://github.com/lacymorrow/lacy"));
|
|
518
895
|
}
|
|
896
|
+
return;
|
|
519
897
|
}
|
|
520
898
|
|
|
521
899
|
await install();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lacy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0-beta.1",
|
|
4
4
|
"description": "Install lacy — talk to your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"index.mjs"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"start": "node index.mjs"
|
|
13
|
+
"start": "node index.mjs",
|
|
14
|
+
"release": "npm publish",
|
|
15
|
+
"release:beta": "npm version prerelease --preid=beta && npm publish --tag beta"
|
|
14
16
|
},
|
|
15
17
|
"keywords": [
|
|
16
18
|
"lacy",
|