lacy 1.8.11 → 1.8.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +26 -0
- package/.github/FUNDING.yml +3 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
- package/.github/SECURITY.md +32 -0
- package/.github/assets/logo-horizontal-dark.png +0 -0
- package/.github/assets/logo-horizontal-dark.svg +17 -0
- package/.github/assets/logo-horizontal.png +0 -0
- package/.github/assets/logo-horizontal.svg +17 -0
- package/.github/assets/logo.png +0 -0
- package/.github/assets/logo.svg +12 -0
- package/.github/assets/social-preview.png +0 -0
- package/.github/assets/social-preview.svg +50 -0
- package/.github/dependabot.yml +21 -0
- package/.github/workflows/ci.yml +80 -0
- package/.github/workflows/dependabot-auto-merge.yml +32 -0
- package/CHANGELOG.md +366 -0
- package/CLAUDE.md +340 -0
- package/CONTRIBUTING.md +141 -0
- package/LICENSE +110 -0
- package/README.md +201 -31
- package/RELEASING.md +148 -0
- package/STYLE.md +202 -0
- package/assets/hero.jpeg +0 -0
- package/assets/mode-indicators.jpeg +0 -0
- package/assets/real-time-indicator.jpeg +0 -0
- package/assets/supported-tools.jpeg +0 -0
- package/bin/lacy +1028 -0
- package/docs/ADDING-BACKENDS.md +124 -0
- package/docs/DEVTO-ARTICLE.md +94 -0
- package/docs/DOCS.md +68 -0
- package/docs/GROWTH-STRATEGY.md +119 -0
- package/docs/HN-RESPONSES.md +122 -0
- package/docs/LAUNCH-COPY-FINAL.md +105 -0
- package/docs/MARKETING.md +411 -0
- package/docs/NATURAL_LANGUAGE_DETECTION.md +204 -0
- package/docs/UGC_VIDEO_SCRIPT.md +114 -0
- package/docs/articles/devto-how-i-made-my-terminal-understand-english.md +117 -0
- package/docs/demo-color-transition.gif +0 -0
- package/docs/demo-full.gif +0 -0
- package/docs/demo-indicator.gif +0 -0
- package/docs/launch-thread-may6.sh +158 -0
- package/docs/videos/README.md +189 -0
- package/docs/videos/generate_frames.py +510 -0
- package/docs/videos/generate_frames_v2.py +729 -0
- package/docs/videos/generate_short.py +328 -0
- package/docs/videos/generate_short_v2.py +526 -0
- package/docs/videos/lacy-shell-demo-v2.mp4 +0 -0
- package/docs/videos/lacy-shell-demo.mp4 +0 -0
- package/docs/videos/lacy-shell-short-v2.mp4 +0 -0
- package/docs/videos/lacy-shell-short.mp4 +0 -0
- package/install.sh +1009 -0
- package/lacy.plugin.bash +75 -0
- package/lacy.plugin.fish +43 -0
- package/lacy.plugin.zsh +65 -0
- package/lib/animations.zsh +3 -0
- package/lib/bash/completions.bash +40 -0
- package/lib/bash/execute.bash +233 -0
- package/lib/bash/init.bash +40 -0
- package/lib/bash/keybindings.bash +134 -0
- package/lib/bash/prompt.bash +85 -0
- package/lib/commands/info.sh +25 -0
- package/lib/config.zsh +3 -0
- package/lib/constants.zsh +3 -0
- package/lib/core/animations.sh +271 -0
- package/lib/core/commands.sh +297 -0
- package/lib/core/config.sh +340 -0
- package/lib/core/constants.sh +366 -0
- package/lib/core/context.sh +260 -0
- package/lib/core/detection.sh +417 -0
- package/lib/core/mcp.sh +741 -0
- package/lib/core/modes.sh +123 -0
- package/lib/core/preheat.sh +496 -0
- package/lib/core/spinner.sh +174 -0
- package/lib/core/telemetry.sh +99 -0
- package/lib/detection.zsh +3 -0
- package/lib/execute.zsh +3 -0
- package/lib/fish/config.fish +66 -0
- package/lib/fish/detection.fish +90 -0
- package/lib/fish/execute.fish +105 -0
- package/lib/fish/keybindings.fish +42 -0
- package/lib/fish/prompt.fish +30 -0
- package/lib/keybindings.zsh +3 -0
- package/lib/mcp.zsh +3 -0
- package/lib/modes.zsh +3 -0
- package/lib/preheat.zsh +3 -0
- package/lib/prompt.zsh +3 -0
- package/lib/spinner.zsh +3 -0
- package/lib/zsh/completions.zsh +60 -0
- package/lib/zsh/execute.zsh +294 -0
- package/lib/zsh/init.zsh +26 -0
- package/lib/zsh/keybindings.zsh +551 -0
- package/lib/zsh/prompt.zsh +90 -0
- package/package.json +42 -27
- package/packages/lacy/README.md +61 -0
- package/packages/lacy/commands/info.sh +25 -0
- package/{index.mjs → packages/lacy/index.mjs} +247 -20
- package/packages/lacy/package-lock.json +71 -0
- package/packages/lacy/package.json +42 -0
- package/script/release.ts +487 -0
- package/squirrel.toml +36 -0
- package/tests/test_bash.bash +163 -0
- package/tests/test_core.sh +607 -0
- package/tests/test_gemini.sh +119 -0
- package/tests/test_gemini_mcp.sh +126 -0
- package/tests/test_preheat_server.zsh +446 -0
- package/uninstall.sh +52 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Release script for lacy-shell
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun run release # interactive — prompts for bump type
|
|
7
|
+
* bun run release patch # patch bump (1.5.3 → 1.5.4)
|
|
8
|
+
* bun run release minor # minor bump (1.5.3 → 1.6.0)
|
|
9
|
+
* bun run release major # major bump (1.5.3 → 2.0.0)
|
|
10
|
+
* bun run release 1.6.0 # explicit version
|
|
11
|
+
* bun run release --beta # beta release (1.5.3 → 1.5.4-beta.0)
|
|
12
|
+
* bun run release:beta # alias for --beta
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as p from "@clack/prompts";
|
|
16
|
+
import pc from "picocolors";
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { resolve } from "node:path";
|
|
20
|
+
|
|
21
|
+
const ROOT = resolve(import.meta.dirname, "..");
|
|
22
|
+
const PACKAGE_JSONS = [
|
|
23
|
+
resolve(ROOT, "package.json"),
|
|
24
|
+
resolve(ROOT, "packages/lacy/package.json"),
|
|
25
|
+
];
|
|
26
|
+
const BIN_LACY = resolve(ROOT, "bin/lacy");
|
|
27
|
+
const HOMEBREW_TAP = resolve(ROOT, "../homebrew-tap");
|
|
28
|
+
const HOMEBREW_FORMULA = resolve(HOMEBREW_TAP, "Formula/lacy.rb");
|
|
29
|
+
|
|
30
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function run(cmd: string, opts?: { cwd?: string; stdio?: "inherit" | "pipe" }) {
|
|
33
|
+
return execSync(cmd, {
|
|
34
|
+
cwd: opts?.cwd ?? ROOT,
|
|
35
|
+
stdio: opts?.stdio ?? "pipe",
|
|
36
|
+
encoding: "utf-8",
|
|
37
|
+
shell: "/bin/bash",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readJson(path: string) {
|
|
42
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeJson(path: string, data: Record<string, unknown>) {
|
|
46
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bumpVersion(
|
|
50
|
+
current: string,
|
|
51
|
+
type: "patch" | "minor" | "major",
|
|
52
|
+
): string {
|
|
53
|
+
const base = current.replace(/-.*$/, ""); // strip any prerelease suffix
|
|
54
|
+
const [major, minor, patch] = base.split(".").map(Number);
|
|
55
|
+
switch (type) {
|
|
56
|
+
case "major":
|
|
57
|
+
return `${major + 1}.0.0`;
|
|
58
|
+
case "minor":
|
|
59
|
+
return `${major}.${minor + 1}.0`;
|
|
60
|
+
case "patch":
|
|
61
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function bumpBeta(current: string, bumpType: "patch" | "minor" | "major"): string {
|
|
66
|
+
const betaMatch = current.match(/^(.+)-beta\.(\d+)$/);
|
|
67
|
+
if (betaMatch) {
|
|
68
|
+
// Already a beta — increment the beta number
|
|
69
|
+
return `${betaMatch[1]}-beta.${Number(betaMatch[2]) + 1}`;
|
|
70
|
+
}
|
|
71
|
+
// Not a beta — bump the base version and start at beta.0
|
|
72
|
+
const base = bumpVersion(current, bumpType);
|
|
73
|
+
return `${base}-beta.0`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cancelled(): never {
|
|
77
|
+
p.cancel("Release cancelled.");
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Check if an execSync error is an npm OTP error */
|
|
82
|
+
function isOtpError(err: unknown): boolean {
|
|
83
|
+
const check = (s: string) =>
|
|
84
|
+
s.includes("EOTP") || s.includes("one-time pass");
|
|
85
|
+
if (err instanceof Error) {
|
|
86
|
+
if (check(err.message)) return true;
|
|
87
|
+
if ("stderr" in err && typeof err.stderr === "string" && check(err.stderr))
|
|
88
|
+
return true;
|
|
89
|
+
if ("stdout" in err && typeof err.stdout === "string" && check(err.stdout))
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return check(String(err));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Get a human-readable error message from an execSync error */
|
|
96
|
+
function errorText(err: unknown): string {
|
|
97
|
+
if (err instanceof Error) {
|
|
98
|
+
if ("stderr" in err && typeof err.stderr === "string" && err.stderr.trim())
|
|
99
|
+
return err.stderr.trim();
|
|
100
|
+
return err.message;
|
|
101
|
+
}
|
|
102
|
+
return String(err);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── npm publish ──────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
async function publishNpm(cwd: string, beta = false): Promise<boolean> {
|
|
108
|
+
const tagFlag = beta ? " --tag beta" : "";
|
|
109
|
+
// First attempt: without OTP (works if token is valid and 2FA isn't required)
|
|
110
|
+
const spinner = p.spinner();
|
|
111
|
+
spinner.start(`Publishing to npm${beta ? " (beta)" : ""}`);
|
|
112
|
+
try {
|
|
113
|
+
run(`npm publish --access public${tagFlag}`, { cwd });
|
|
114
|
+
spinner.stop(pc.green(`Published to npm${beta ? " (beta)" : ""}`));
|
|
115
|
+
return true;
|
|
116
|
+
} catch (err: unknown) {
|
|
117
|
+
spinner.stop(pc.yellow("npm publish failed"));
|
|
118
|
+
p.log.message(pc.dim(errorText(err)));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Retry loop — let the user login, provide OTP, or skip
|
|
122
|
+
while (true) {
|
|
123
|
+
const action = await p.select({
|
|
124
|
+
message: "How would you like to proceed?",
|
|
125
|
+
options: [
|
|
126
|
+
{
|
|
127
|
+
value: "otp" as const,
|
|
128
|
+
label: "Enter OTP",
|
|
129
|
+
hint: "publish with one-time password",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
value: "login" as const,
|
|
133
|
+
label: "Log in to npm",
|
|
134
|
+
hint: "run npm login, then retry",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
value: "retry" as const,
|
|
138
|
+
label: "Retry publish",
|
|
139
|
+
hint: "try again without OTP",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
value: "skip" as const,
|
|
143
|
+
label: "Skip npm publish",
|
|
144
|
+
hint: "continue to Homebrew",
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (p.isCancel(action)) {
|
|
150
|
+
p.log.warn("Skipping npm publish");
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (action === "skip") {
|
|
155
|
+
p.log.info("Skipping npm publish");
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (action === "login") {
|
|
160
|
+
p.log.info("Running npm login...");
|
|
161
|
+
try {
|
|
162
|
+
run("npm login", { cwd, stdio: "inherit" });
|
|
163
|
+
p.log.success("Logged in to npm");
|
|
164
|
+
} catch {
|
|
165
|
+
p.log.error("npm login failed");
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// "otp" or "retry"
|
|
171
|
+
let otpFlag = "";
|
|
172
|
+
if (action === "otp") {
|
|
173
|
+
const otp = await p.text({
|
|
174
|
+
message: "npm OTP",
|
|
175
|
+
placeholder: "123456",
|
|
176
|
+
validate: (v) => {
|
|
177
|
+
if (!v || !/^\d{6}$/.test(v.trim())) return "OTP must be 6 digits";
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (p.isCancel(otp)) continue; // back to menu
|
|
182
|
+
otpFlag = ` --otp ${otp}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const retrySpinner = p.spinner();
|
|
186
|
+
retrySpinner.start("Publishing to npm");
|
|
187
|
+
try {
|
|
188
|
+
run(`npm publish --access public${tagFlag}${otpFlag}`, { cwd });
|
|
189
|
+
retrySpinner.stop(pc.green("Published to npm"));
|
|
190
|
+
return true;
|
|
191
|
+
} catch (err: unknown) {
|
|
192
|
+
retrySpinner.stop(pc.red("npm publish failed"));
|
|
193
|
+
p.log.message(pc.dim(errorText(err)));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Homebrew ─────────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
async function publishHomebrew(tag: string, version: string) {
|
|
201
|
+
if (!existsSync(HOMEBREW_FORMULA)) {
|
|
202
|
+
p.log.warn(
|
|
203
|
+
`Homebrew tap not found at ${pc.dim(HOMEBREW_FORMULA)}. Skipping.`,
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const doHomebrew = await p.confirm({
|
|
209
|
+
message: "Update Homebrew formula?",
|
|
210
|
+
initialValue: true,
|
|
211
|
+
});
|
|
212
|
+
if (p.isCancel(doHomebrew) || !doHomebrew) {
|
|
213
|
+
p.log.info("Skipping Homebrew");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const brewSpinner = p.spinner();
|
|
218
|
+
brewSpinner.start("Updating Homebrew formula");
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
// Pull first to avoid rebase conflicts with unstaged changes
|
|
222
|
+
run("git checkout main", { cwd: HOMEBREW_TAP });
|
|
223
|
+
run("git pull --rebase origin main", { cwd: HOMEBREW_TAP });
|
|
224
|
+
|
|
225
|
+
// Download the release tarball and compute SHA256
|
|
226
|
+
const tarballUrl = `https://github.com/lacymorrow/lacy/archive/refs/tags/${tag}.tar.gz`;
|
|
227
|
+
const sha256 = run(
|
|
228
|
+
`curl -sL "${tarballUrl}" | shasum -a 256 | cut -d' ' -f1`,
|
|
229
|
+
).trim();
|
|
230
|
+
|
|
231
|
+
if (!sha256 || sha256.length !== 64) {
|
|
232
|
+
brewSpinner.stop(pc.red("Failed to compute SHA256"));
|
|
233
|
+
p.log.error(`Got: ${sha256}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Update the formula
|
|
238
|
+
let formula = readFileSync(HOMEBREW_FORMULA, "utf-8");
|
|
239
|
+
formula = formula.replace(
|
|
240
|
+
/url "https:\/\/github\.com\/lacymorrow\/lacy\/archive\/refs\/tags\/v[^"]+\.tar\.gz"/,
|
|
241
|
+
`url "${tarballUrl}"`,
|
|
242
|
+
);
|
|
243
|
+
formula = formula.replace(
|
|
244
|
+
/sha256 "[a-f0-9]+"/,
|
|
245
|
+
`sha256 "${sha256}"`,
|
|
246
|
+
);
|
|
247
|
+
writeFileSync(HOMEBREW_FORMULA, formula);
|
|
248
|
+
|
|
249
|
+
// Commit and push
|
|
250
|
+
run("git add Formula/lacy.rb", { cwd: HOMEBREW_TAP });
|
|
251
|
+
run(`git commit -m "lacy: update to ${tag}"`, { cwd: HOMEBREW_TAP });
|
|
252
|
+
run("git push", { cwd: HOMEBREW_TAP });
|
|
253
|
+
|
|
254
|
+
brewSpinner.stop(`Homebrew formula updated to ${pc.green(tag)}`);
|
|
255
|
+
} catch (err: unknown) {
|
|
256
|
+
brewSpinner.stop(pc.red("Homebrew update failed"));
|
|
257
|
+
p.log.error(errorText(err));
|
|
258
|
+
p.log.info(
|
|
259
|
+
`Update manually: ${pc.cyan(`edit ${HOMEBREW_FORMULA}`)}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
async function main() {
|
|
267
|
+
const isBeta = process.argv.includes("--beta");
|
|
268
|
+
const args = process.argv.slice(2).filter((a) => a !== "--beta");
|
|
269
|
+
|
|
270
|
+
console.clear();
|
|
271
|
+
p.intro(
|
|
272
|
+
pc.magenta(
|
|
273
|
+
pc.bold(isBeta ? " Lacy Shell — Beta Release " : " Lacy Shell — Release "),
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// Preflight checks
|
|
278
|
+
const preflight = p.spinner();
|
|
279
|
+
preflight.start("Running preflight checks");
|
|
280
|
+
|
|
281
|
+
const status = run("git status --porcelain").trim();
|
|
282
|
+
if (status) {
|
|
283
|
+
preflight.stop(pc.red("Working tree is not clean"));
|
|
284
|
+
p.log.error("Commit or stash changes first:");
|
|
285
|
+
console.log(pc.dim(status));
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const branch = run("git branch --show-current").trim();
|
|
290
|
+
if (!isBeta && branch !== "main") {
|
|
291
|
+
preflight.stop(pc.red(`On branch '${branch}', not 'main'`));
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
preflight.stop("Preflight OK");
|
|
296
|
+
|
|
297
|
+
// Current version
|
|
298
|
+
const rootPkg = readJson(PACKAGE_JSONS[0]);
|
|
299
|
+
const currentVersion: string = rootPkg.version;
|
|
300
|
+
|
|
301
|
+
// Determine new version
|
|
302
|
+
const arg = args[0];
|
|
303
|
+
let newVersion: string;
|
|
304
|
+
|
|
305
|
+
if (isBeta) {
|
|
306
|
+
// Beta: pick a bump type then apply beta suffix
|
|
307
|
+
const isAlreadyBeta = /-beta\.\d+$/.test(currentVersion);
|
|
308
|
+
|
|
309
|
+
if (isAlreadyBeta) {
|
|
310
|
+
// Already on a beta — offer to bump beta number or start fresh
|
|
311
|
+
const selected = await p.select({
|
|
312
|
+
message: `Current version: ${pc.cyan(currentVersion)}. Beta bump?`,
|
|
313
|
+
options: [
|
|
314
|
+
{
|
|
315
|
+
value: "next" as const,
|
|
316
|
+
label: "next beta",
|
|
317
|
+
hint: `${currentVersion} → ${bumpBeta(currentVersion, "patch")}`,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
value: "patch" as const,
|
|
321
|
+
label: "new patch beta",
|
|
322
|
+
hint: `${currentVersion} → ${bumpVersion(currentVersion, "patch")}-beta.0`,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
value: "minor" as const,
|
|
326
|
+
label: "new minor beta",
|
|
327
|
+
hint: `${currentVersion} → ${bumpVersion(currentVersion, "minor")}-beta.0`,
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
value: "major" as const,
|
|
331
|
+
label: "new major beta",
|
|
332
|
+
hint: `${currentVersion} → ${bumpVersion(currentVersion, "major")}-beta.0`,
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (p.isCancel(selected)) cancelled();
|
|
338
|
+
newVersion =
|
|
339
|
+
selected === "next"
|
|
340
|
+
? bumpBeta(currentVersion, "patch")
|
|
341
|
+
: `${bumpVersion(currentVersion, selected)}-beta.0`;
|
|
342
|
+
} else {
|
|
343
|
+
// Not a beta yet — bump type then add -beta.0
|
|
344
|
+
if (arg === "patch" || arg === "minor" || arg === "major") {
|
|
345
|
+
newVersion = bumpBeta(currentVersion, arg);
|
|
346
|
+
} else {
|
|
347
|
+
const selected = await p.select({
|
|
348
|
+
message: `Current version: ${pc.cyan(currentVersion)}. Bump type for beta?`,
|
|
349
|
+
options: [
|
|
350
|
+
{
|
|
351
|
+
value: "patch" as const,
|
|
352
|
+
label: "patch",
|
|
353
|
+
hint: `${currentVersion} → ${bumpBeta(currentVersion, "patch")}`,
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
value: "minor" as const,
|
|
357
|
+
label: "minor",
|
|
358
|
+
hint: `${currentVersion} → ${bumpBeta(currentVersion, "minor")}`,
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
value: "major" as const,
|
|
362
|
+
label: "major",
|
|
363
|
+
hint: `${currentVersion} → ${bumpBeta(currentVersion, "major")}`,
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (p.isCancel(selected)) cancelled();
|
|
369
|
+
newVersion = bumpBeta(currentVersion, selected);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} else if (arg === "patch" || arg === "minor" || arg === "major") {
|
|
373
|
+
newVersion = bumpVersion(currentVersion, arg);
|
|
374
|
+
} else if (arg && /^\d+\.\d+\.\d+/.test(arg)) {
|
|
375
|
+
newVersion = arg;
|
|
376
|
+
} else {
|
|
377
|
+
const selected = await p.select({
|
|
378
|
+
message: `Current version: ${pc.cyan(currentVersion)}. Bump type?`,
|
|
379
|
+
options: [
|
|
380
|
+
{
|
|
381
|
+
value: "patch" as const,
|
|
382
|
+
label: "patch",
|
|
383
|
+
hint: `${currentVersion} → ${bumpVersion(currentVersion, "patch")}`,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
value: "minor" as const,
|
|
387
|
+
label: "minor",
|
|
388
|
+
hint: `${currentVersion} → ${bumpVersion(currentVersion, "minor")}`,
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
value: "major" as const,
|
|
392
|
+
label: "major",
|
|
393
|
+
hint: `${currentVersion} → ${bumpVersion(currentVersion, "major")}`,
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (p.isCancel(selected)) cancelled();
|
|
399
|
+
newVersion = bumpVersion(currentVersion, selected);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const tag = `v${newVersion}`;
|
|
403
|
+
|
|
404
|
+
const proceed = await p.confirm({
|
|
405
|
+
message: `Release ${pc.cyan(currentVersion)} → ${pc.green(newVersion)} (${tag})?`,
|
|
406
|
+
});
|
|
407
|
+
if (p.isCancel(proceed) || !proceed) cancelled();
|
|
408
|
+
|
|
409
|
+
// 1. Bump versions
|
|
410
|
+
const bumpSpinner = p.spinner();
|
|
411
|
+
bumpSpinner.start("Bumping versions");
|
|
412
|
+
for (const path of PACKAGE_JSONS) {
|
|
413
|
+
const pkg = readJson(path);
|
|
414
|
+
pkg.version = newVersion;
|
|
415
|
+
writeJson(path, pkg);
|
|
416
|
+
}
|
|
417
|
+
// Also bump the fallback version in bin/lacy
|
|
418
|
+
if (existsSync(BIN_LACY)) {
|
|
419
|
+
let binContent = readFileSync(BIN_LACY, "utf-8");
|
|
420
|
+
binContent = binContent.replace(
|
|
421
|
+
/^VERSION_FALLBACK="[^"]*"/m,
|
|
422
|
+
`VERSION_FALLBACK="${newVersion}"`,
|
|
423
|
+
);
|
|
424
|
+
writeFileSync(BIN_LACY, binContent);
|
|
425
|
+
}
|
|
426
|
+
bumpSpinner.stop(
|
|
427
|
+
`Updated ${pc.cyan("package.json")} + ${pc.cyan("bin/lacy")} → ${pc.green(newVersion)}`,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// 2. Changelog
|
|
431
|
+
const lastTag = run(
|
|
432
|
+
"git describe --tags --abbrev=0 2>/dev/null || echo ''",
|
|
433
|
+
).trim();
|
|
434
|
+
let changelog = "";
|
|
435
|
+
if (lastTag) {
|
|
436
|
+
changelog = run(
|
|
437
|
+
`git log ${lastTag}..HEAD --pretty=format:"- %s (%h)" --no-merges`,
|
|
438
|
+
).trim();
|
|
439
|
+
}
|
|
440
|
+
if (!changelog) changelog = `- Release ${tag}`;
|
|
441
|
+
|
|
442
|
+
p.note(changelog, "Changelog");
|
|
443
|
+
|
|
444
|
+
// 3. Commit + tag
|
|
445
|
+
const gitSpinner = p.spinner();
|
|
446
|
+
gitSpinner.start("Committing and tagging");
|
|
447
|
+
run("git add package.json packages/lacy/package.json bin/lacy");
|
|
448
|
+
run(`git commit -m "release: ${tag}" --no-verify`);
|
|
449
|
+
run(`git tag ${tag}`);
|
|
450
|
+
gitSpinner.stop(`Committed and tagged ${pc.green(tag)}`);
|
|
451
|
+
|
|
452
|
+
// 4. Push
|
|
453
|
+
const pushSpinner = p.spinner();
|
|
454
|
+
pushSpinner.start("Pushing to GitHub");
|
|
455
|
+
run(`git push origin ${branch} --no-verify`);
|
|
456
|
+
run(`git push origin ${tag}`);
|
|
457
|
+
pushSpinner.stop("Pushed to GitHub");
|
|
458
|
+
|
|
459
|
+
// 5. GitHub release
|
|
460
|
+
const releaseSpinner = p.spinner();
|
|
461
|
+
releaseSpinner.start("Creating GitHub release");
|
|
462
|
+
const releaseNotes = `## Changes\n\n${changelog}`;
|
|
463
|
+
run(
|
|
464
|
+
`gh release create ${tag} --title "${tag}" --notes "${releaseNotes.replace(/"/g, '\\"')}"${isBeta ? " --prerelease" : ""}`,
|
|
465
|
+
);
|
|
466
|
+
releaseSpinner.stop("GitHub release created");
|
|
467
|
+
|
|
468
|
+
// 6. npm publish
|
|
469
|
+
await publishNpm(resolve(ROOT, "packages/lacy"), isBeta);
|
|
470
|
+
|
|
471
|
+
// 7. Homebrew (skip for beta releases)
|
|
472
|
+
if (!isBeta) {
|
|
473
|
+
await publishHomebrew(tag, newVersion);
|
|
474
|
+
} else {
|
|
475
|
+
p.log.info("Skipping Homebrew for beta release");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Done
|
|
479
|
+
p.outro(
|
|
480
|
+
`${pc.green("✓")} Released ${pc.green(tag)} — ${pc.cyan(`https://github.com/lacymorrow/lacy/releases/tag/${tag}`)}`,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
main().catch((err) => {
|
|
485
|
+
p.log.error(err.message ?? err);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
});
|
package/squirrel.toml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
domains = []
|
|
3
|
+
name = "lacy-sh"
|
|
4
|
+
|
|
5
|
+
[crawler]
|
|
6
|
+
max_pages = 100
|
|
7
|
+
coverage = "surface"
|
|
8
|
+
delay_ms = 100
|
|
9
|
+
timeout_ms = 30000
|
|
10
|
+
user_agent = ""
|
|
11
|
+
follow_redirects = true
|
|
12
|
+
concurrency = 5
|
|
13
|
+
per_host_concurrency = 2
|
|
14
|
+
per_host_delay_ms = 200
|
|
15
|
+
include = []
|
|
16
|
+
exclude = []
|
|
17
|
+
allow_query_params = []
|
|
18
|
+
drop_query_prefixes = [ "utm_", "gclid", "fbclid" ]
|
|
19
|
+
respect_robots = true
|
|
20
|
+
breadth_first = true
|
|
21
|
+
max_prefix_budget = 0.25
|
|
22
|
+
|
|
23
|
+
[rules]
|
|
24
|
+
enable = [ "*" ]
|
|
25
|
+
disable = []
|
|
26
|
+
|
|
27
|
+
[external_links]
|
|
28
|
+
enabled = true
|
|
29
|
+
cache_ttl_days = 7
|
|
30
|
+
timeout_ms = 10000
|
|
31
|
+
concurrency = 5
|
|
32
|
+
|
|
33
|
+
[output]
|
|
34
|
+
format = "console"
|
|
35
|
+
|
|
36
|
+
[rule_options]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Bash-specific integration tests for Lacy Shell
|
|
4
|
+
# Requires Bash 4+ (macOS: brew install bash)
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# /opt/homebrew/bin/bash tests/test_bash.bash # macOS with Homebrew bash
|
|
8
|
+
# bash tests/test_bash.bash # Linux
|
|
9
|
+
|
|
10
|
+
# Bail if Bash < 4
|
|
11
|
+
if [[ ${BASH_VERSINFO[0]} -lt 4 ]]; then
|
|
12
|
+
echo "SKIP: Bash 4+ required (have ${BASH_VERSION})"
|
|
13
|
+
echo " macOS: /opt/homebrew/bin/bash tests/test_bash.bash"
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Note: no set -e — tests use functions that return nonzero intentionally
|
|
18
|
+
|
|
19
|
+
echo "Testing Lacy Shell Bash adapter in: bash ${BASH_VERSION}"
|
|
20
|
+
echo "================================================================"
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
24
|
+
|
|
25
|
+
# Set up environment as the plugin would
|
|
26
|
+
LACY_SHELL_TYPE="bash"
|
|
27
|
+
_LACY_ARR_OFFSET=0
|
|
28
|
+
LACY_SHELL_DIR="$REPO_DIR"
|
|
29
|
+
|
|
30
|
+
# Source core + bash adapter
|
|
31
|
+
source "$REPO_DIR/lib/core/constants.sh"
|
|
32
|
+
source "$REPO_DIR/lib/core/config.sh"
|
|
33
|
+
source "$REPO_DIR/lib/core/modes.sh"
|
|
34
|
+
source "$REPO_DIR/lib/core/spinner.sh"
|
|
35
|
+
source "$REPO_DIR/lib/core/mcp.sh"
|
|
36
|
+
source "$REPO_DIR/lib/core/preheat.sh"
|
|
37
|
+
source "$REPO_DIR/lib/core/detection.sh"
|
|
38
|
+
|
|
39
|
+
# Source bash-specific modules (skip keybindings — needs interactive shell)
|
|
40
|
+
source "$REPO_DIR/lib/bash/prompt.bash"
|
|
41
|
+
source "$REPO_DIR/lib/bash/execute.bash"
|
|
42
|
+
|
|
43
|
+
PASS=0
|
|
44
|
+
FAIL=0
|
|
45
|
+
|
|
46
|
+
assert_eq() {
|
|
47
|
+
local test_name="$1"
|
|
48
|
+
local expected="$2"
|
|
49
|
+
local actual="$3"
|
|
50
|
+
|
|
51
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
52
|
+
PASS=$(( PASS + 1 ))
|
|
53
|
+
else
|
|
54
|
+
echo " FAIL: $test_name"
|
|
55
|
+
echo " Expected: $expected"
|
|
56
|
+
echo " Actual: $actual"
|
|
57
|
+
FAIL=$(( FAIL + 1 ))
|
|
58
|
+
fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# ============================================================================
|
|
62
|
+
# Detection in Bash 4+
|
|
63
|
+
# ============================================================================
|
|
64
|
+
|
|
65
|
+
echo ""
|
|
66
|
+
echo "--- Bash Detection ---"
|
|
67
|
+
|
|
68
|
+
LACY_SHELL_CURRENT_MODE="auto"
|
|
69
|
+
|
|
70
|
+
assert_eq "ls → shell" "shell" "$(lacy_shell_classify_input 'ls -la')"
|
|
71
|
+
assert_eq "what files → agent" "agent" "$(lacy_shell_classify_input 'what files')"
|
|
72
|
+
assert_eq "fix the bug → agent" "agent" "$(lacy_shell_classify_input 'fix the bug')"
|
|
73
|
+
assert_eq "yes → agent" "agent" "$(lacy_shell_classify_input 'yes lets go')"
|
|
74
|
+
assert_eq "empty → neutral" "neutral" "$(lacy_shell_classify_input '')"
|
|
75
|
+
|
|
76
|
+
# Bash-specific: ${var,,} lowercase works
|
|
77
|
+
assert_eq "lowercase" "hello" "$(_lacy_lowercase 'HELLO')"
|
|
78
|
+
|
|
79
|
+
# ============================================================================
|
|
80
|
+
# Bash Prompt
|
|
81
|
+
# ============================================================================
|
|
82
|
+
|
|
83
|
+
echo ""
|
|
84
|
+
echo "--- Bash Prompt ---"
|
|
85
|
+
|
|
86
|
+
# Test prompt building
|
|
87
|
+
PS1='$ '
|
|
88
|
+
LACY_SHELL_ORIGINAL_PS1=""
|
|
89
|
+
LACY_SHELL_BASE_PS1=""
|
|
90
|
+
LACY_SHELL_PROMPT_INITIALIZED=false
|
|
91
|
+
lacy_shell_setup_prompt
|
|
92
|
+
lacy_shell_init_prompt_once
|
|
93
|
+
|
|
94
|
+
# After init, PS1 should contain the mode badge
|
|
95
|
+
if [[ "$PS1" == *"AUTO"* ]]; then
|
|
96
|
+
PASS=$(( PASS + 1 ))
|
|
97
|
+
else
|
|
98
|
+
echo " FAIL: PS1 should contain AUTO badge"
|
|
99
|
+
echo " PS1: $PS1"
|
|
100
|
+
FAIL=$(( FAIL + 1 ))
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Mode switch should update prompt
|
|
104
|
+
lacy_shell_set_mode "shell"
|
|
105
|
+
lacy_shell_update_prompt
|
|
106
|
+
if [[ "$PS1" == *"SHELL"* ]]; then
|
|
107
|
+
PASS=$(( PASS + 1 ))
|
|
108
|
+
else
|
|
109
|
+
echo " FAIL: PS1 should contain SHELL badge after mode switch"
|
|
110
|
+
FAIL=$(( FAIL + 1 ))
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# ============================================================================
|
|
114
|
+
# Bash Mode Switching
|
|
115
|
+
# ============================================================================
|
|
116
|
+
|
|
117
|
+
echo ""
|
|
118
|
+
echo "--- Bash Modes ---"
|
|
119
|
+
|
|
120
|
+
LACY_SHELL_MODE_FILE="/tmp/lacy_test_mode_bash_$$"
|
|
121
|
+
|
|
122
|
+
lacy_shell_set_mode "auto"
|
|
123
|
+
assert_eq "auto mode" "auto" "$LACY_SHELL_CURRENT_MODE"
|
|
124
|
+
|
|
125
|
+
lacy_shell_toggle_mode
|
|
126
|
+
assert_eq "toggle → shell" "shell" "$LACY_SHELL_CURRENT_MODE"
|
|
127
|
+
|
|
128
|
+
lacy_shell_toggle_mode
|
|
129
|
+
assert_eq "toggle → agent" "agent" "$LACY_SHELL_CURRENT_MODE"
|
|
130
|
+
|
|
131
|
+
lacy_shell_toggle_mode
|
|
132
|
+
assert_eq "toggle → auto" "auto" "$LACY_SHELL_CURRENT_MODE"
|
|
133
|
+
|
|
134
|
+
rm -f "$LACY_SHELL_MODE_FILE"
|
|
135
|
+
|
|
136
|
+
# ============================================================================
|
|
137
|
+
# Bash Functions
|
|
138
|
+
# ============================================================================
|
|
139
|
+
|
|
140
|
+
echo ""
|
|
141
|
+
echo "--- Bash Functions ---"
|
|
142
|
+
|
|
143
|
+
# Check that command functions exist
|
|
144
|
+
if type ask &>/dev/null; then PASS=$(( PASS + 1 )); else echo " FAIL: ask function missing"; FAIL=$(( FAIL + 1 )); fi
|
|
145
|
+
if type mode &>/dev/null; then PASS=$(( PASS + 1 )); else echo " FAIL: mode function missing"; FAIL=$(( FAIL + 1 )); fi
|
|
146
|
+
if type tool &>/dev/null; then PASS=$(( PASS + 1 )); else echo " FAIL: tool function missing"; FAIL=$(( FAIL + 1 )); fi
|
|
147
|
+
if type quit &>/dev/null; then PASS=$(( PASS + 1 )); else echo " FAIL: quit function missing"; FAIL=$(( FAIL + 1 )); fi
|
|
148
|
+
|
|
149
|
+
# ============================================================================
|
|
150
|
+
# Results
|
|
151
|
+
# ============================================================================
|
|
152
|
+
|
|
153
|
+
echo ""
|
|
154
|
+
echo "================================================================"
|
|
155
|
+
echo "Results: ${PASS} passed, ${FAIL} failed"
|
|
156
|
+
|
|
157
|
+
if [[ $FAIL -gt 0 ]]; then
|
|
158
|
+
echo "FAILED"
|
|
159
|
+
exit 1
|
|
160
|
+
else
|
|
161
|
+
echo "ALL TESTS PASSED"
|
|
162
|
+
exit 0
|
|
163
|
+
fi
|