Skip to content

Commit 199912e

Browse files
authored
Merge pull request #108 from fulll/feat/windows-install-ps1
Add full Windows support (new targets, install.ps1, build flags, ICO, metadata)
2 parents 642d37d + 02b1324 commit 199912e

File tree

12 files changed

+1044
-105
lines changed

12 files changed

+1044
-105
lines changed

.github/workflows/cd.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ jobs:
3030
- runner: ubuntu-latest
3131
target: bun-windows-x64
3232
artifact: github-code-search-windows-x64.exe
33+
- runner: ubuntu-latest
34+
target: bun-windows-x64-baseline
35+
artifact: github-code-search-windows-x64-baseline.exe
36+
- runner: ubuntu-latest
37+
target: bun-windows-x64-modern
38+
artifact: github-code-search-windows-x64-modern.exe
39+
- runner: ubuntu-latest
40+
target: bun-windows-arm64
41+
artifact: github-code-search-windows-arm64.exe
3342
# macOS — must run on macOS for ad-hoc codesigning
3443
- runner: macos-latest
3544
target: bun-darwin-x64

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,22 @@ keyboard-driven TUI, fine-grained extract selection, markdown/JSON output.
1414

1515
## Quick start
1616

17+
**macOS / Linux**
18+
1719
```bash
1820
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
1921
curl -fsSL https://raw.githubusercontent.com/fulll/github-code-search/main/install.sh | bash
2022
github-code-search query "TODO" --org my-org
2123
```
2224

25+
**Windows** (PowerShell)
26+
27+
```powershell
28+
$env:GITHUB_TOKEN = "ghp_xxxxxxxxxxxxxxxxxxxx"
29+
powershell -c "irm https://raw.githubusercontent.com/fulll/github-code-search/main/install.ps1 | iex"
30+
github-code-search query "TODO" --org my-org
31+
```
32+
2333
## Features
2434

2535
- **Org-wide search** — queries all repositories in a GitHub organization in one command, with automatic pagination up to 1 000 results

build.test.ts

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
import { describe, it, expect } from "bun:test";
2+
import {
3+
parseTarget,
4+
parseTargetArg,
5+
isWindowsTarget,
6+
getOutfile,
7+
getBuildCompileOptions,
8+
buildLabel,
9+
buildCopyrightLine,
10+
type WindowsMeta,
11+
} from "./build";
12+
// ─── parseTargetArg ─────────────────────────────────────────────────────────────
13+
14+
describe("parseTargetArg", () => {
15+
it("extracts the target value from argv", () => {
16+
expect(parseTargetArg(["bun", "build.ts", "--target=bun-linux-x64"])).toBe("bun-linux-x64");
17+
});
18+
19+
it("returns null when no --target= flag is present", () => {
20+
expect(parseTargetArg(["bun", "build.ts"])).toBeNull();
21+
});
22+
23+
it("returns null for empty argv", () => {
24+
expect(parseTargetArg([])).toBeNull();
25+
});
26+
27+
it("ignores unrelated flags", () => {
28+
expect(parseTargetArg(["bun", "--verbose", "--target=bun-windows-x64"])).toBe(
29+
"bun-windows-x64",
30+
);
31+
});
32+
33+
it("returns empty string when --target= has no value", () => {
34+
expect(parseTargetArg(["bun", "--target="])).toBe("");
35+
});
36+
});
37+
// ─── parseTarget ─────────────────────────────────────────────────────────────
38+
39+
describe("parseTarget", () => {
40+
it("parses bun-linux-x64", () => {
41+
expect(parseTarget("bun-linux-x64")).toEqual({ os: "linux", arch: "x64" });
42+
});
43+
44+
it("parses bun-linux-x64-baseline", () => {
45+
expect(parseTarget("bun-linux-x64-baseline")).toEqual({
46+
os: "linux",
47+
arch: "x64-baseline",
48+
});
49+
});
50+
51+
it("parses bun-linux-arm64", () => {
52+
expect(parseTarget("bun-linux-arm64")).toEqual({
53+
os: "linux",
54+
arch: "arm64",
55+
});
56+
});
57+
58+
it("parses bun-linux-arm64-musl", () => {
59+
expect(parseTarget("bun-linux-arm64-musl")).toEqual({
60+
os: "linux",
61+
arch: "arm64-musl",
62+
});
63+
});
64+
65+
it("parses bun-darwin-x64", () => {
66+
expect(parseTarget("bun-darwin-x64")).toEqual({
67+
os: "darwin",
68+
arch: "x64",
69+
});
70+
});
71+
72+
it("parses bun-darwin-arm64", () => {
73+
expect(parseTarget("bun-darwin-arm64")).toEqual({
74+
os: "darwin",
75+
arch: "arm64",
76+
});
77+
});
78+
79+
it("parses bun-windows-x64", () => {
80+
expect(parseTarget("bun-windows-x64")).toEqual({
81+
os: "windows",
82+
arch: "x64",
83+
});
84+
});
85+
86+
it("parses bun-windows-x64-baseline", () => {
87+
expect(parseTarget("bun-windows-x64-baseline")).toEqual({
88+
os: "windows",
89+
arch: "x64-baseline",
90+
});
91+
});
92+
93+
it("parses bun-windows-x64-modern", () => {
94+
expect(parseTarget("bun-windows-x64-modern")).toEqual({
95+
os: "windows",
96+
arch: "x64-modern",
97+
});
98+
});
99+
100+
it("parses bun-windows-arm64", () => {
101+
expect(parseTarget("bun-windows-arm64")).toEqual({
102+
os: "windows",
103+
arch: "arm64",
104+
});
105+
});
106+
107+
it("returns native platform when target is null", () => {
108+
const result = parseTarget(null);
109+
// We can only assert the shape, not the exact values (depends on the runner)
110+
expect(typeof result.os).toBe("string");
111+
expect(typeof result.arch).toBe("string");
112+
expect(result.os.length).toBeGreaterThan(0);
113+
});
114+
115+
it("returns native platform when target is undefined", () => {
116+
const result = parseTarget(undefined);
117+
expect(typeof result.os).toBe("string");
118+
expect(typeof result.arch).toBe("string");
119+
});
120+
121+
// baseline must be parsed before plain x64 (order matters in the if-chain)
122+
it("does not confuse windows-x64-baseline with windows-x64", () => {
123+
expect(parseTarget("bun-windows-x64-baseline").arch).toBe("x64-baseline");
124+
expect(parseTarget("bun-windows-x64").arch).toBe("x64");
125+
});
126+
127+
it("does not confuse linux-x64-baseline with linux-x64", () => {
128+
expect(parseTarget("bun-linux-x64-baseline").arch).toBe("x64-baseline");
129+
expect(parseTarget("bun-linux-x64").arch).toBe("x64");
130+
});
131+
132+
// Regression: on Windows, process.platform is "win32" which isWindowsTarget()
133+
// does not recognise. parseTarget(null) must normalise it to "windows".
134+
// We can't execute the null path on a non-Windows runner, but we can assert the
135+
// downstream contract: "win32" alone must NOT satisfy isWindowsTarget (proving
136+
// normalisation is necessary) and getOutfile("windows", null) must add .exe.
137+
it("isWindowsTarget rejects bare win32 — normalisation in parseTarget is required", () => {
138+
expect(isWindowsTarget("win32")).toBe(false);
139+
expect(isWindowsTarget("windows")).toBe(true);
140+
expect(getOutfile("windows", null)).toBe("./dist/github-code-search.exe");
141+
});
142+
143+
// Regression: the end-of-function fallback (unrecognised target string) must
144+
// also normalise win32 → windows, not leak the raw Node.js platform alias.
145+
it("never returns win32 as os for unknown target strings", () => {
146+
const result = parseTarget("bun-completely-unknown-future-target");
147+
expect(result.os).not.toBe("win32");
148+
});
149+
});
150+
151+
// ─── isWindowsTarget ─────────────────────────────────────────────────────────
152+
153+
describe("isWindowsTarget", () => {
154+
it("returns true for windows", () => {
155+
expect(isWindowsTarget("windows")).toBe(true);
156+
});
157+
158+
it("returns false for linux", () => {
159+
expect(isWindowsTarget("linux")).toBe(false);
160+
});
161+
162+
it("returns false for darwin", () => {
163+
expect(isWindowsTarget("darwin")).toBe(false);
164+
});
165+
});
166+
167+
// ─── getOutfile ───────────────────────────────────────────────────────────────
168+
169+
describe("getOutfile", () => {
170+
it("adds .exe suffix for windows targets", () => {
171+
expect(getOutfile("windows", "bun-windows-x64")).toBe(
172+
"./dist/github-code-search-windows-x64.exe",
173+
);
174+
});
175+
176+
it("adds .exe suffix for windows-x64-modern", () => {
177+
expect(getOutfile("windows", "bun-windows-x64-modern")).toBe(
178+
"./dist/github-code-search-windows-x64-modern.exe",
179+
);
180+
});
181+
182+
it("adds .exe suffix for windows-x64-baseline", () => {
183+
expect(getOutfile("windows", "bun-windows-x64-baseline")).toBe(
184+
"./dist/github-code-search-windows-x64-baseline.exe",
185+
);
186+
});
187+
188+
it("adds .exe suffix for windows-arm64", () => {
189+
expect(getOutfile("windows", "bun-windows-arm64")).toBe(
190+
"./dist/github-code-search-windows-arm64.exe",
191+
);
192+
});
193+
194+
it("does not add .exe for linux targets", () => {
195+
expect(getOutfile("linux", "bun-linux-x64")).toBe("./dist/github-code-search-linux-x64");
196+
});
197+
198+
it("does not add .exe for darwin targets", () => {
199+
expect(getOutfile("darwin", "bun-darwin-arm64")).toBe("./dist/github-code-search-darwin-arm64");
200+
});
201+
202+
it("omits suffix for native (no target)", () => {
203+
const outfile = getOutfile("linux", null);
204+
expect(outfile).toBe("./dist/github-code-search");
205+
});
206+
207+
it("strips the bun- prefix from the suffix", () => {
208+
expect(getOutfile("linux", "bun-linux-arm64")).toBe("./dist/github-code-search-linux-arm64");
209+
});
210+
});
211+
212+
// ─── getBuildCompileOptions ───────────────────────────────────────────────────
213+
214+
const FULL_META: WindowsMeta = {
215+
iconPath: "/abs/path/favicon.ico",
216+
title: "github-code-search",
217+
publisher: "fulll",
218+
appVersion: "1.2.3",
219+
description: "Interactive GitHub code search",
220+
copyright: "Copyright © 2026 fulll — MIT",
221+
};
222+
223+
describe("getBuildCompileOptions", () => {
224+
it("returns only outfile for non-windows targets", () => {
225+
const opts = getBuildCompileOptions("linux", "./dist/foo", FULL_META);
226+
expect(opts).toEqual({ outfile: "./dist/foo" });
227+
});
228+
229+
it("returns only outfile for darwin", () => {
230+
const opts = getBuildCompileOptions("darwin", "./dist/foo", FULL_META);
231+
expect(opts).toEqual({ outfile: "./dist/foo" });
232+
});
233+
234+
it("includes hideConsole for windows target", () => {
235+
const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META);
236+
expect(opts).toMatchObject({
237+
outfile: "./dist/foo.exe",
238+
windows: { hideConsole: true },
239+
});
240+
});
241+
242+
it("sets icon from iconPath", () => {
243+
const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as {
244+
windows: WindowsMeta & { icon: string; hideConsole: boolean };
245+
};
246+
expect(opts.windows.icon).toBe("/abs/path/favicon.ico");
247+
});
248+
249+
it("sets title", () => {
250+
const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as {
251+
windows: { title: string };
252+
};
253+
expect(opts.windows.title).toBe("github-code-search");
254+
});
255+
256+
it("sets publisher", () => {
257+
const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as {
258+
windows: { publisher: string };
259+
};
260+
expect(opts.windows.publisher).toBe("fulll");
261+
});
262+
263+
it("sets version from appVersion", () => {
264+
const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as {
265+
windows: { version: string };
266+
};
267+
expect(opts.windows.version).toBe("1.2.3");
268+
});
269+
270+
it("sets description", () => {
271+
const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as {
272+
windows: { description: string };
273+
};
274+
expect(opts.windows.description).toBe("Interactive GitHub code search");
275+
});
276+
277+
it("sets copyright", () => {
278+
const opts = getBuildCompileOptions("windows", "./dist/foo.exe", FULL_META) as {
279+
windows: { copyright: string };
280+
};
281+
expect(opts.windows.copyright).toBe("Copyright © 2026 fulll — MIT");
282+
});
283+
284+
it("works with empty meta (all windows fields undefined)", () => {
285+
const opts = getBuildCompileOptions("windows", "./dist/foo.exe") as {
286+
outfile: string;
287+
windows: Record<string, unknown>;
288+
};
289+
expect(opts.outfile).toBe("./dist/foo.exe");
290+
expect(opts.windows.icon).toBeUndefined();
291+
expect(opts.windows.title).toBeUndefined();
292+
});
293+
});
294+
295+
// ─── buildLabel ──────────────────────────────────────────────────────────────
296+
297+
describe("buildLabel", () => {
298+
it("formats the label string correctly", () => {
299+
expect(buildLabel("1.9.0", "abc1234", "linux", "x64")).toBe("1.9.0 (abc1234 · linux/x64)");
300+
});
301+
302+
it("works with windows target", () => {
303+
expect(buildLabel("1.9.0", "abc1234", "windows", "x64-modern")).toBe(
304+
"1.9.0 (abc1234 · windows/x64-modern)",
305+
);
306+
});
307+
308+
it("works with dev commit", () => {
309+
expect(buildLabel("1.9.0", "dev", "darwin", "arm64")).toBe("1.9.0 (dev · darwin/arm64)");
310+
});
311+
});
312+
313+
// ─── buildCopyrightLine ──────────────────────────────────────────────────────
314+
315+
describe("buildCopyrightLine", () => {
316+
it("formats the copyright string correctly", () => {
317+
expect(buildCopyrightLine(2026, "fulll", "MIT")).toBe("Copyright © 2026 fulll — MIT");
318+
});
319+
320+
it("uses the provided year", () => {
321+
expect(buildCopyrightLine(2030, "Acme Corp", "Apache-2.0")).toBe(
322+
"Copyright © 2030 Acme Corp — Apache-2.0",
323+
);
324+
});
325+
});

0 commit comments

Comments
 (0)