Skip to content

Commit f91d389

Browse files
committed
core: add --version
1 parent 2bf7a5d commit f91d389

File tree

9 files changed

+180
-117
lines changed

9 files changed

+180
-117
lines changed

AGENTS.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@
1313
- Never use `/tmp`; prefer `.llm/scratch`. Out-of-tree directory access forces manual approval, `.llm/scratch` lets you work autonomously
1414
- Never write utilities as Bash scripts; always use TypeScript + Bun
1515
- Never wrap `await foo()` in parentheses in an if statement
16-
- NEVER use the Glob tool without a path parameter
16+
- NEVER use the Glob tool without a path parameter; if none is needed, use "*"
17+
- Always make tests declarative rather than imperative. Tests describe filesystem state (repos, store entries, config), the `fixture()` function materializes it. No imperative setup.
18+
- Never use mocks. Tests call actual functions against temporary filesystems.
19+
- Always run tests in isolation. Each test gets its own HOME and cwd via fixture. Cleanup via `await using`.
20+
- Always build paths and read environment variables lazily e.g. `src/core/config.ts` reads `process.env.HOME` at call time, not import time. This is what makes per-test isolation work without preloads.

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@spader/dotllm",
3-
"version": "1.2.0",
3+
"version": "1.2.1",
44
"description": "A simple CLI to clone, manage, and link repositories for LLM reference",
55
"type": "module",
66
"repository": {
@@ -14,6 +14,8 @@
1414
"dotllm": "src/cli/index.ts"
1515
},
1616
"scripts": {
17+
"clean": "bun tools/clean.ts",
18+
"build": "bun tools/build.ts",
1719
"check": "bunx tsc --noEmit",
1820
"prepublishOnly": "bun run check"
1921
},
@@ -27,6 +29,9 @@
2729
"./core": "./src/core/index.ts",
2830
"./core/*": "./src/core/*.ts"
2931
},
32+
"imports": {
33+
"#tools/*": "./tools/*.ts"
34+
},
3035
"engines": {
3136
"bun": ">=1.1.0"
3237
},

src/cli/index.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,27 @@ import { build, type Cli } from "@spader/dotllm/cli/yargs";
44
import { add, remove, list, link, sync, which, cd } from "@spader/dotllm/cli/commands/index";
55

66
export namespace DotLlmCli {
7-
export const def: Cli = {
8-
name: "dotllm",
9-
description: "Manage git repo references symlinked into .llm/reference/",
10-
commands: {
11-
add,
12-
remove,
13-
list,
14-
link,
15-
sync,
16-
which,
17-
cd,
18-
},
19-
};
7+
export async function run(): Promise<void> {
8+
const raw = await Bun.file(new URL("../../package.json", import.meta.url)).json() as { version?: unknown };
9+
const version = typeof raw.version === "string" ? raw.version : undefined;
10+
11+
const def: Cli = {
12+
name: "dotllm",
13+
description: "Manage git repo references symlinked into .llm/reference/",
14+
version,
15+
commands: {
16+
add,
17+
remove,
18+
list,
19+
link,
20+
sync,
21+
which,
22+
cd,
23+
},
24+
};
2025

21-
export function run(): void {
2226
build(def).parse();
2327
}
2428
}
2529

26-
DotLlmCli.run();
30+
await DotLlmCli.run();

src/cli/yargs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type Command = {
3636
export type Cli = {
3737
name: string;
3838
description: string;
39+
version?: string;
3940
options?: Options;
4041
commands: Record<string, Command>;
4142
};
@@ -109,6 +110,9 @@ export function help(
109110
...(def.options ?? {}),
110111
help: { alias: "h", type: "boolean", description: "Show help" },
111112
};
113+
if ("version" in def && def.version) {
114+
opts.version = { alias: "v", type: "boolean", description: "Show version" };
115+
}
112116

113117
if (Object.keys(opts).length > 0) {
114118
if (prev) console.log("");
@@ -214,6 +218,10 @@ function configure(
214218
.option("help", { alias: "h", type: "boolean", describe: "Show help" })
215219
.check(check(def, root, path))
216220
.fail(fail(def, root, path));
221+
222+
if (path.length === 0 && "version" in def && def.version) {
223+
y.version(def.version).alias("version", "v");
224+
}
217225
}
218226

219227
function command(

test/SPEC.md

Lines changed: 0 additions & 52 deletions
This file was deleted.

test/cli/version.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import fs from "fs"
2+
import path from "path"
3+
import { test, expect } from "bun:test"
4+
import { Build } from "#tools/build"
5+
import { fixture } from "../fixture"
6+
7+
test("dotllm --version matches installed package.json", async () => {
8+
await using env = await fixture()
9+
10+
fs.writeFileSync(
11+
path.join(env.path, "package.json"),
12+
JSON.stringify({ name: "dotllm-version-test", private: true }, null, 2),
13+
)
14+
15+
const built = await Build.build({ pack: env.path })
16+
const tar = built.tar
17+
expect(path.isAbsolute(tar)).toBe(true)
18+
const installed = Bun.spawnSync(["npm", "install", "--no-package-lock", tar], {
19+
cwd: env.path,
20+
stdout: "pipe",
21+
stderr: "pipe",
22+
})
23+
expect(installed.exitCode).toBe(0)
24+
25+
const raw = await Bun.file(path.join(env.path, "node_modules", "@spader", "dotllm", "package.json")).json() as { version?: unknown }
26+
const version = typeof raw.version === "string" ? raw.version : ""
27+
expect(version.length > 0).toBe(true)
28+
29+
const bin = path.join(env.path, "node_modules", ".bin", "dotllm")
30+
const proc = Bun.spawnSync([bin, "--version"], {
31+
cwd: env.path,
32+
stdout: "pipe",
33+
stderr: "pipe",
34+
})
35+
36+
expect(proc.exitCode).toBe(0)
37+
expect(proc.stdout.toString().trim()).toBe(version)
38+
})

tools/build.ts

Lines changed: 80 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,89 @@
11
import path from "path";
22
import fs from "fs";
33

4-
async function main() {
5-
const root = path.resolve(import.meta.dir, "..");
6-
const out = path.join(root, ".build");
7-
8-
fs.rmSync(out, { recursive: true, force: true });
9-
10-
// Compile all source files in one shot. `packages: "external"` keeps npm
11-
// deps and self-referencing @spader/dotllm/* imports as bare specifiers.
12-
const sources = Array.from(new Bun.Glob("src/**/*.ts").scanSync(root));
13-
const result = await Bun.build({
14-
entrypoints: sources.map(f => path.join(root, f)),
15-
outdir: out,
16-
root,
17-
target: "bun",
18-
packages: "external",
19-
});
20-
21-
if (!result.success) {
22-
for (const log of result.logs) console.error(log);
23-
process.exit(1);
24-
}
4+
export namespace Build {
5+
type Opt = {
6+
root?: string;
7+
out?: string;
8+
pack?: string;
9+
};
2510

26-
// Ensure CLI entry point is executable
27-
fs.chmodSync(path.join(out, "src/cli/index.js"), 0o755);
28-
29-
// Generate a publish-ready package.json with JS paths
30-
const pkg = await Bun.file(path.join(root, "package.json")).json();
31-
pkg.bin = { dotllm: "src/cli/index.js" };
32-
pkg.exports = {
33-
"./cli": "./src/cli/index.js",
34-
"./cli/*": "./src/cli/*.js",
35-
"./core": "./src/core/index.js",
36-
"./core/*": "./src/core/*.js",
11+
type Result = {
12+
files: number;
13+
out: string;
14+
tar: string;
3715
};
38-
pkg.files = ["src/**/*.js", "README.md"];
39-
delete pkg.scripts;
40-
delete pkg.devDependencies;
41-
await Bun.write(path.join(out, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
42-
43-
fs.copyFileSync(path.join(root, "README.md"), path.join(out, "README.md"));
44-
45-
// Create npm tarball
46-
const pack = Bun.spawnSync(["npm", "pack"], { cwd: out, stdout: "pipe", stderr: "pipe" });
47-
if (pack.exitCode !== 0) {
48-
console.error(pack.stderr.toString());
49-
process.exit(1);
16+
17+
export async function build(opt: Opt = {}): Promise<Result> {
18+
const root = opt.root ?? path.resolve(import.meta.dir, "..");
19+
const out = opt.out ?? path.join(root, ".build");
20+
const packDir = opt.pack ?? out;
21+
22+
fs.rmSync(out, { recursive: true, force: true });
23+
fs.mkdirSync(packDir, { recursive: true });
24+
25+
const src = Array.from(new Bun.Glob("src/**/*.ts").scanSync(root));
26+
const res = await Bun.build({
27+
entrypoints: src.map(f => path.join(root, f)),
28+
outdir: out,
29+
root,
30+
target: "bun",
31+
packages: "external",
32+
});
33+
34+
if (!res.success) {
35+
for (const log of res.logs) console.error(log);
36+
return Promise.reject(new Error("build failed"));
37+
}
38+
39+
fs.chmodSync(path.join(out, "src/cli/index.js"), 0o755);
40+
41+
const raw = await Bun.file(path.join(root, "package.json")).json() as Record<string, unknown>;
42+
raw.bin = { dotllm: "src/cli/index.js" };
43+
raw.exports = {
44+
"./cli": "./src/cli/index.js",
45+
"./cli/*": "./src/cli/*.js",
46+
"./core": "./src/core/index.js",
47+
"./core/*": "./src/core/*.js",
48+
};
49+
raw.files = ["src/**/*.js", "README.md"];
50+
delete raw.scripts;
51+
delete raw.devDependencies;
52+
delete raw.imports;
53+
await Bun.write(path.join(out, "package.json"), JSON.stringify(raw, null, 2) + "\n");
54+
55+
fs.copyFileSync(path.join(root, "README.md"), path.join(out, "README.md"));
56+
57+
const pack = Bun.spawnSync(["npm", "pack", "--json", "--pack-destination", packDir], {
58+
cwd: out,
59+
stdout: "pipe",
60+
stderr: "pipe",
61+
});
62+
if (pack.exitCode !== 0) {
63+
return Promise.reject(new Error(pack.stderr.toString() || "npm pack failed"));
64+
}
65+
66+
const json = JSON.parse(pack.stdout.toString()) as { filename?: string }[];
67+
const file = json[0]?.filename ?? "";
68+
if (file.length === 0) {
69+
return Promise.reject(new Error("npm pack did not return filename"));
70+
}
71+
72+
return {
73+
files: src.length,
74+
out,
75+
tar: path.join(packDir, file),
76+
};
5077
}
5178

52-
console.log(`${sources.length} files compiled`);
53-
console.log(`.build/${pack.stdout.toString().trim()}`);
79+
export async function main(): Promise<void> {
80+
}
5481
}
5582

56-
main();
83+
if (import.meta.main) {
84+
const res = await Build.build();
85+
const root = path.resolve(import.meta.dir, "..");
86+
const rel = path.relative(root, res.tar);
87+
console.log(`${res.files} files compiled`);
88+
console.log(rel);
89+
}

tools/clean.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import path from "path";
2+
import fs from "fs";
3+
4+
export namespace CleanTool {
5+
export function clean(root?: string): string {
6+
const base = root ?? path.resolve(import.meta.dir, "..");
7+
const out = path.join(base, ".build");
8+
fs.rmSync(out, { recursive: true, force: true });
9+
return out;
10+
}
11+
12+
export async function main(): Promise<void> {
13+
const out = clean();
14+
const root = path.resolve(import.meta.dir, "..");
15+
const rel = path.relative(root, out);
16+
console.log(rel);
17+
}
18+
}
19+
20+
if (import.meta.main) {
21+
await CleanTool.main();
22+
}

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"@spader/dotllm/cli": ["src/cli/index.ts"],
1616
"@spader/dotllm/cli/*": ["src/cli/*"],
1717
"@spader/dotllm/core": ["src/core/index.ts"],
18-
"@spader/dotllm/core/*": ["src/core/*"]
18+
"@spader/dotllm/core/*": ["src/core/*"],
19+
"#tools/*": ["tools/*"]
1920
}
2021
},
2122
"include": ["src/**/*.ts", "test/**/*.ts"]

0 commit comments

Comments
 (0)