Skip to content

Commit bae7b8d

Browse files
committed
feat: add deep doc links to --help and update-available notice
[#58] Deep documentation links in CLI help: - program --help shows 'Documentation: https://fulll.github.io/github-code-search/' - upgrade --help points to /usage/upgrade - query --help points to /usage/search-syntax - --exclude-repositories/--exclude-extracts mention /usage/filtering - --format/--output-type mention /usage/output-formats - --group-by-team-prefix mentions /usage/team-grouping [#59] Update-available notice after non-interactive search: - import checkForUpdate() from upgrade.ts - After CI/non-interactive output, race a 2s timeout against checkForUpdate - If a newer version exists, print a yellow framed notice to stderr (never pollutes piped stdout, never shown in interactive TUI mode) Also fix toSorted/toReversed lint warnings in config.mts Part of #60, closes #58, closes #59
1 parent 35ec1cc commit bae7b8d

3 files changed

Lines changed: 115 additions & 30 deletions

File tree

docs/.vitepress/config.mts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ function buildBlogSidebarItems(): { text: string; link: string }[] {
1212
try {
1313
files = readdirSync(blogDir)
1414
.filter((f) => f.endsWith(".md") && f !== "index.md")
15-
.sort()
16-
.reverse();
15+
.toSorted()
16+
.toReversed();
1717
} catch {
1818
// blog dir may not exist during the very first build
1919
}
@@ -178,10 +178,7 @@ export default defineConfig({
178178
"/blog/": [
179179
{
180180
text: "What's New",
181-
items: [
182-
{ text: "All releases", link: "/blog/" },
183-
...buildBlogSidebarItems(),
184-
],
181+
items: [{ text: "All releases", link: "/blog/" }, ...buildBlogSidebarItems()],
185182
},
186183
],
187184
"/architecture/": [

github-code-search.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { aggregate, normaliseExtractRef, normaliseRepo } from "./src/aggregate.t
2020
import { fetchAllResults, fetchRepoTeams } from "./src/api.ts";
2121
import { buildOutput } from "./src/output.ts";
2222
import { groupByTeamPrefix, flattenTeamSections } from "./src/group.ts";
23+
import { checkForUpdate } from "./src/upgrade.ts";
2324
import { runInteractive } from "./src/tui.ts";
2425
import type { OutputFormat, OutputType } from "./src/types.ts";
2526

@@ -54,6 +55,7 @@ function addSearchOptions(cmd: Command): Command {
5455
"Comma-separated list of repositories to exclude.",
5556
"Short form (without org prefix) or full form accepted:",
5657
" repoA,repoB OR myorg/repoA,myorg/repoB",
58+
"Docs: https://fulll.github.io/github-code-search/usage/filtering",
5759
].join("\n"),
5860
"",
5961
)
@@ -64,17 +66,28 @@ function addSearchOptions(cmd: Command): Command {
6466
"Format (shortest): repoName:path:matchIndex",
6567
" e.g. repoA:src/foo.ts:0,repoB:lib/core.ts:2",
6668
"Full form also accepted: myorg/repoA:src/foo.ts:0",
69+
"Docs: https://fulll.github.io/github-code-search/usage/filtering",
6770
].join("\n"),
6871
"",
6972
)
7073
.option(
7174
"--no-interactive",
7275
"Disable interactive mode (non-interactive). Also triggered by CI=true env var.",
7376
)
74-
.option("--format <format>", "Output format: markdown (default) or json", "markdown")
77+
.option(
78+
"--format <format>",
79+
[
80+
"Output format: markdown (default) or json.",
81+
"Docs: https://fulll.github.io/github-code-search/usage/output-formats",
82+
].join("\n"),
83+
"markdown",
84+
)
7585
.option(
7686
"--output-type <type>",
77-
"Output type: repo-and-matches (default) or repo-only",
87+
[
88+
"Output type: repo-and-matches (default) or repo-only.",
89+
"Docs: https://fulll.github.io/github-code-search/usage/output-formats",
90+
].join("\n"),
7891
"repo-and-matches",
7992
)
8093
.option(
@@ -89,6 +102,7 @@ function addSearchOptions(cmd: Command): Command {
89102
"Example: squad-,chapter-",
90103
"Repos are first grouped by the first prefix (single-team, then multi-team),",
91104
"then by the next prefix, and so on. Repos matching no prefix go into 'other'.",
105+
"Docs: https://fulll.github.io/github-code-search/usage/team-grouping",
92106
].join("\n"),
93107
"",
94108
)
@@ -166,6 +180,28 @@ async function searchAction(
166180
groupByTeamPrefix: opts.groupByTeamPrefix,
167181
}),
168182
);
183+
// Check for a newer version and notify on stderr so it never pollutes piped output.
184+
// Race against a 2 s timeout so slow networks never delay the exit.
185+
const latestTag = await Promise.race([
186+
checkForUpdate(VERSION, GITHUB_TOKEN),
187+
new Promise<null>((res) => setTimeout(() => res(null), 2000)),
188+
]);
189+
if (latestTag) {
190+
const w = 55;
191+
const bar = "─".repeat(w);
192+
const pad = (s: string) => s + " ".repeat(Math.max(0, w - s.length));
193+
process.stderr.write(
194+
pc.yellow(
195+
[
196+
`╭─ Update available ${"─".repeat(w - 18)}╮`,
197+
`│ ${pad(`github-code-search ${VERSION}${latestTag}`)} │`,
198+
`│ ${pad("Run: github-code-search upgrade")} │`,
199+
`╰${bar}╯`,
200+
"",
201+
].join("\n"),
202+
),
203+
);
204+
}
169205
} else {
170206
await runInteractive(
171207
groups,
@@ -186,12 +222,17 @@ async function searchAction(
186222
program
187223
.name("github-code-search")
188224
.version(VERSION_FULL, "-V, --version", "Output version, commit, OS and architecture")
189-
.description("Interactive GitHub code search with per-repo aggregation");
225+
.description("Interactive GitHub code search with per-repo aggregation")
226+
.addHelpText("after", "\nDocumentation:\n https://fulll.github.io/github-code-search/");
190227

191228
// `upgrade` subcommand — does NOT require GITHUB_TOKEN (uses it only if set)
192229
program
193230
.command("upgrade")
194231
.description("Check for a new release and auto-upgrade the binary")
232+
.addHelpText(
233+
"after",
234+
"\nDocumentation:\n https://fulll.github.io/github-code-search/usage/upgrade",
235+
)
195236
.option("--debug", "Print debug information for troubleshooting")
196237
.action(async (opts: { debug?: boolean }) => {
197238
const { performUpgrade } = await import("./src/upgrade.ts");
@@ -226,7 +267,12 @@ program
226267

227268
// `query` subcommand — the default (backward-compat: `gcs <query> --org <org>`)
228269
const queryCmd = addSearchOptions(
229-
new Command("query").description("Search GitHub code (default command when no subcommand given)"),
270+
new Command("query")
271+
.description("Search GitHub code (default command when no subcommand given)")
272+
.addHelpText(
273+
"after",
274+
"\nDocumentation:\n https://fulll.github.io/github-code-search/usage/search-syntax",
275+
),
230276
).action(async (query: string, opts) => {
231277
await searchAction(query, opts);
232278
});

src/upgrade.test.ts

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -161,22 +161,38 @@ describe("fetchLatestRelease", () => {
161161

162162
it("returns release data from the GitHub API", async () => {
163163
globalThis.fetch = (async () =>
164-
new Response(JSON.stringify({ tag_name: "v1.2.0", html_url: "https://github.com/fulll/github-code-search/releases/tag/v1.2.0", assets: [] }), {
165-
status: 200,
166-
headers: { "content-type": "application/json" },
167-
})) as typeof fetch;
164+
new Response(
165+
JSON.stringify({
166+
tag_name: "v1.2.0",
167+
html_url: "https://github.com/fulll/github-code-search/releases/tag/v1.2.0",
168+
assets: [],
169+
}),
170+
{
171+
status: 200,
172+
headers: { "content-type": "application/json" },
173+
},
174+
)) as typeof fetch;
168175
const release = await fetchLatestRelease("faketoken");
169176
expect(release.tag_name).toBe("v1.2.0");
170-
expect(release.html_url).toBe("https://github.com/fulll/github-code-search/releases/tag/v1.2.0");
177+
expect(release.html_url).toBe(
178+
"https://github.com/fulll/github-code-search/releases/tag/v1.2.0",
179+
);
171180
expect(release.assets).toHaveLength(0);
172181
});
173182

174183
it("works without a token", async () => {
175184
globalThis.fetch = (async () =>
176-
new Response(JSON.stringify({ tag_name: "v2.0.0", html_url: "https://github.com/fulll/github-code-search/releases/tag/v2.0.0", assets: [] }), {
177-
status: 200,
178-
headers: { "content-type": "application/json" },
179-
})) as typeof fetch;
185+
new Response(
186+
JSON.stringify({
187+
tag_name: "v2.0.0",
188+
html_url: "https://github.com/fulll/github-code-search/releases/tag/v2.0.0",
189+
assets: [],
190+
}),
191+
{
192+
status: 200,
193+
headers: { "content-type": "application/json" },
194+
},
195+
)) as typeof fetch;
180196
const release = await fetchLatestRelease();
181197
expect(release.tag_name).toBe("v2.0.0");
182198
});
@@ -210,10 +226,17 @@ describe("performUpgrade", () => {
210226

211227
it("prints 'up to date' when no newer version", async () => {
212228
globalThis.fetch = (async () =>
213-
new Response(JSON.stringify({ tag_name: "v1.0.0", html_url: "https://github.com/fulll/github-code-search/releases/tag/v1.0.0", assets: [] }), {
214-
status: 200,
215-
headers: { "content-type": "application/json" },
216-
})) as typeof fetch;
229+
new Response(
230+
JSON.stringify({
231+
tag_name: "v1.0.0",
232+
html_url: "https://github.com/fulll/github-code-search/releases/tag/v1.0.0",
233+
assets: [],
234+
}),
235+
{
236+
status: 200,
237+
headers: { "content-type": "application/json" },
238+
},
239+
)) as typeof fetch;
217240

218241
const writes: string[] = [];
219242
const origWrite = process.stdout.write.bind(process.stdout);
@@ -233,10 +256,17 @@ describe("performUpgrade", () => {
233256

234257
it("throws when no matching binary asset found in the release", async () => {
235258
globalThis.fetch = (async () =>
236-
new Response(JSON.stringify({ tag_name: "v9.9.9", html_url: "https://github.com/fulll/github-code-search/releases/tag/v9.9.9", assets: [] }), {
237-
status: 200,
238-
headers: { "content-type": "application/json" },
239-
})) as typeof fetch;
259+
new Response(
260+
JSON.stringify({
261+
tag_name: "v9.9.9",
262+
html_url: "https://github.com/fulll/github-code-search/releases/tag/v9.9.9",
263+
assets: [],
264+
}),
265+
{
266+
status: 200,
267+
headers: { "content-type": "application/json" },
268+
},
269+
)) as typeof fetch;
240270

241271
await expect(performUpgrade("1.0.0", "/tmp/test-binary-noasset")).rejects.toThrow(
242272
"No binary found",
@@ -254,7 +284,11 @@ describe("checkForUpdate", () => {
254284
it("returns the latest version tag when a newer version exists", async () => {
255285
globalThis.fetch = (async () =>
256286
new Response(
257-
JSON.stringify({ tag_name: "v2.0.0", html_url: "https://github.com/fulll/github-code-search/releases/tag/v2.0.0", assets: [] }),
287+
JSON.stringify({
288+
tag_name: "v2.0.0",
289+
html_url: "https://github.com/fulll/github-code-search/releases/tag/v2.0.0",
290+
assets: [],
291+
}),
258292
{ status: 200, headers: { "content-type": "application/json" } },
259293
)) as typeof fetch;
260294
const result = await checkForUpdate("1.0.0");
@@ -264,7 +298,11 @@ describe("checkForUpdate", () => {
264298
it("returns null when already on the latest version", async () => {
265299
globalThis.fetch = (async () =>
266300
new Response(
267-
JSON.stringify({ tag_name: "v1.0.0", html_url: "https://github.com/fulll/github-code-search/releases/tag/v1.0.0", assets: [] }),
301+
JSON.stringify({
302+
tag_name: "v1.0.0",
303+
html_url: "https://github.com/fulll/github-code-search/releases/tag/v1.0.0",
304+
assets: [],
305+
}),
268306
{ status: 200, headers: { "content-type": "application/json" } },
269307
)) as typeof fetch;
270308
const result = await checkForUpdate("1.0.0");
@@ -316,7 +354,11 @@ describe("performUpgrade — download path", () => {
316354
callCount++;
317355
if (callCount === 1) {
318356
return new Response(
319-
JSON.stringify({ tag_name: "v9.9.9", html_url: "https://github.com/fulll/github-code-search/releases/tag/v9.9.9", assets: [makeAsset(assetName)] }),
357+
JSON.stringify({
358+
tag_name: "v9.9.9",
359+
html_url: "https://github.com/fulll/github-code-search/releases/tag/v9.9.9",
360+
assets: [makeAsset(assetName)],
361+
}),
320362
{
321363
status: 200,
322364
headers: { "content-type": "application/json" },

0 commit comments

Comments
 (0)