github-search-cli 9.8.1 → 9.8.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-search-cli",
3
- "version": "9.8.1",
3
+ "version": "9.8.2",
4
4
  "description": "Search GitHub from your terminal — repo search, trending repos, npm package search, bookmarks, deep-dive, compare, export. Keyboard-driven TUI + pipeable CLI.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1",
@@ -0,0 +1,65 @@
1
+ # v9.8.2
2
+
3
+ A maintenance release: twenty-three fixes from a full static audit of the
4
+ codebase, most of them invisible until something went wrong. Nothing user-facing
5
+ was added or removed — every flag, key and command behaves as it did in 9.8.1.
6
+
7
+ ## The one that mattered
8
+
9
+ A GitHub token passed as `--token` could end up in a public URL. When ghfind
10
+ crashed, it built a GitHub issue link straight from `process.argv` — argv
11
+ included your token — and opened it in a browser. The docblock claimed the
12
+ command line was redacted; it was not. Every field that reaches the issue now
13
+ goes through redaction first, and when ghfind sees a credential it prints the
14
+ URL for you to copy instead of opening a browser tab automatically.
15
+
16
+ ## Security, in full
17
+
18
+ - Tokens are no longer sent to `raw.githubusercontent.com` when fetching a
19
+ README — only to `api.github.com`, and only for a private repository.
20
+ - Skill sources and ids from the public skills.sh registry are validated before
21
+ they can reach a shell command or a URL, so a hostile registry entry cannot
22
+ smuggle `;`, `$(`, backticks or a path traversal into `npx skills add`.
23
+ - The config file that stores your token is now written with `0600`
24
+ permissions. On Windows the file still inherits the directory ACL, so this
25
+ closes the hole on POSIX only.
26
+ - `--doctor` masks the token rather than printing a raw prefix.
27
+
28
+ ## Reliability
29
+
30
+ The most user-visible correctness fix: recoverable errors were treated as
31
+ crashes. The top-level catch tested `err.name === "SearchCliError"`, a string
32
+ that is never true, so a rate limit, a network blip or a layout change on
33
+ GitHub's trending page all opened a browser tab asking you to file a bug. That
34
+ branch now works, and those errors print a short explanation and exit.
35
+
36
+ `--compare` and the `ghfind_compare_repos` MCP tool both resolved repository
37
+ names through a free-text search with no `repo:` qualifier and never checked
38
+ the `fullName` that came back, so `ghfind --compare oven/bun oven-sh/bun` could
39
+ quietly compare the wrong two repositories. Both now resolve exactly.
40
+
41
+ State files — bookmarks, history, sessions, saved searches, notifications —
42
+ were written directly to their live path. A crash, a full disk, or a reader
43
+ caught mid-write left a truncated JSON file, which the loader then silently
44
+ replaced with defaults: your data was gone with no error. Writes are now atomic
45
+ (temp file plus rename).
46
+
47
+ ## Smaller fixes
48
+
49
+ Notification IDs restarted after "dismiss all"; `history --delete` removed
50
+ every duplicate entry instead of one; an invalid `--sort` value degraded
51
+ silently instead of erroring; a failed "load more" in the TUI wiped the results
52
+ already on screen; `--markdown` did not escape `|` in cells while `--csv`
53
+ did; the in-memory cache was not a real LRU; local skill scanning re-walked the
54
+ filesystem on every keystroke; and skill ids containing `_` — common on GitHub
55
+ — were wrongly rejected.
56
+
57
+ ## Verification
58
+
59
+ 482 tests, plus `typecheck`, `lint`, `format:check` and `build` all green.
60
+ The security fixes were additionally verified with runnable probes that assert
61
+ no token substring survives URL encoding in the generated crash-report URL, that
62
+ none of twelve `raw.githubusercontent.com` requests carries an `Authorization`
63
+ header, and that seven of eight hostile skill payloads are rejected.
64
+
65
+ `README.md` test count updated to match the real suite.
package/scripts/brew.rb CHANGED
@@ -1,53 +1,53 @@
1
- # typed: true
2
- # frozen_string_literal: true
3
-
4
- # Homebrew formula for ghfind.
5
- #
6
- # NOTE: this file is a template, kept in-tree for a future homebrew tap.
7
- # Before use:
8
- # 1. Replace VERSION with the released version tag.
9
- # 2. Fill SHA256 placeholders from the SHA256SUMS.txt asset attached to
10
- # the GitHub Release (the release workflow generates it).
11
- # 3. Host it in a tap repo (e.g. codersdfs/tap) at Formula/ghfind.rb,
12
- # then: brew install codersdfs/tap/ghfind
13
- # See marketing/README.md (Track B) for the rollout order.
14
-
15
- class Ghfind < Formula
16
- desc "Search GitHub repos from your terminal — trending, bookmarks, deep-dive"
17
- homepage "https://github.com/codersdfs/search-cli"
18
- version "9.5.1"
19
- license "MIT"
20
-
21
- livecheck do
22
- url :stable
23
- regex(/^v?(\d+(?:\.\d+)+)$/i)
24
- end
25
-
26
- on_macos do
27
- if Hardware::CPU.arm?
28
- url "https://github.com/codersdfs/search-cli/releases/download/v#{version}/ghfind-darwin-arm64"
29
- sha256 "SHA256_PLACEHOLDER"
30
- else
31
- url "https://github.com/codersdfs/search-cli/releases/download/v#{version}/ghfind-darwin-x64"
32
- sha256 "SHA256_PLACEHOLDER"
33
- end
34
- end
35
-
36
- on_linux do
37
- if Hardware::CPU.arm? && Hardware::CPU.is_64_bit?
38
- url "https://github.com/codersdfs/search-cli/releases/download/v#{version}/ghfind-linux-arm64"
39
- sha256 "SHA256_PLACEHOLDER"
40
- else
41
- url "https://github.com/codersdfs/search-cli/releases/download/v#{version}/ghfind-linux-x64"
42
- sha256 "SHA256_PLACEHOLDER"
43
- end
44
- end
45
-
46
- def install
47
- bin.install "ghfind" => "ghfind"
48
- end
49
-
50
- test do
51
- assert_match version.to_s, shell_output("#{bin}/ghfind --version")
52
- end
53
- end
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ # Homebrew formula for ghfind.
5
+ #
6
+ # NOTE: this file is a template, kept in-tree for a future homebrew tap.
7
+ # Before use:
8
+ # 1. Replace VERSION with the released version tag.
9
+ # 2. Fill SHA256 placeholders from the SHA256SUMS.txt asset attached to
10
+ # the GitHub Release (the release workflow generates it).
11
+ # 3. Host it in a tap repo (e.g. codersdfs/tap) at Formula/ghfind.rb,
12
+ # then: brew install codersdfs/tap/ghfind
13
+ # See marketing/README.md (Track B) for the rollout order.
14
+
15
+ class Ghfind < Formula
16
+ desc "Search GitHub repos from your terminal — trending, bookmarks, deep-dive"
17
+ homepage "https://github.com/codersdfs/search-cli"
18
+ version "9.5.1"
19
+ license "MIT"
20
+
21
+ livecheck do
22
+ url :stable
23
+ regex(/^v?(\d+(?:\.\d+)+)$/i)
24
+ end
25
+
26
+ on_macos do
27
+ if Hardware::CPU.arm?
28
+ url "https://github.com/codersdfs/search-cli/releases/download/v#{version}/ghfind-darwin-arm64"
29
+ sha256 "SHA256_PLACEHOLDER"
30
+ else
31
+ url "https://github.com/codersdfs/search-cli/releases/download/v#{version}/ghfind-darwin-x64"
32
+ sha256 "SHA256_PLACEHOLDER"
33
+ end
34
+ end
35
+
36
+ on_linux do
37
+ if Hardware::CPU.arm? && Hardware::CPU.is_64_bit?
38
+ url "https://github.com/codersdfs/search-cli/releases/download/v#{version}/ghfind-linux-arm64"
39
+ sha256 "SHA256_PLACEHOLDER"
40
+ else
41
+ url "https://github.com/codersdfs/search-cli/releases/download/v#{version}/ghfind-linux-x64"
42
+ sha256 "SHA256_PLACEHOLDER"
43
+ end
44
+ end
45
+
46
+ def install
47
+ bin.install "ghfind" => "ghfind"
48
+ end
49
+
50
+ test do
51
+ assert_match version.to_s, shell_output("#{bin}/ghfind --version")
52
+ end
53
+ end
@@ -18,6 +18,7 @@ import { join, dirname } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { platform, arch } from "node:os";
20
20
  import { spawnSync } from "node:child_process";
21
+ import { createHash } from "node:crypto";
21
22
  import { inflateRawSync } from "node:zlib";
22
23
 
23
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -96,6 +97,35 @@ const key = `${platform()}-${arch}`;
96
97
  const assetName = map[key];
97
98
  const version = "1.3.14";
98
99
 
100
+ /**
101
+ * Pinned SHA256 of every Bun release asset this script can install, taken
102
+ * from that release's own SHASUMS256.txt. The download is verified before
103
+ * anything is written, so a tampered or truncated archive can never become
104
+ * an executable in vendor/.
105
+ *
106
+ * ponytail: hand-pinned per Bun release — regenerate this table (and bump
107
+ * `version`) whenever the release is bumped; the check is what makes the
108
+ * hardcoded `version` above trustworthy.
109
+ */
110
+ const SHA256 = {
111
+ "bun-windows-x64.zip":
112
+ "0a0620930b6675d7ba440e81f4e0e00d3cfbe096c4b140d3fff02205e9e18922",
113
+ "bun-windows-aarch64.zip":
114
+ "89841f5a57f2348b67ec0839b718f4bf4ea7d07c371c9ba4b77b6c790f918953",
115
+ "bun-darwin-x64.zip":
116
+ "4183df3374623e5bab315c547cfa0974533cd457d86b73b639f7a87974cd6633",
117
+ "bun-darwin-aarch64.zip":
118
+ "d8b96221828ad6f97ac7ac0ab7e95872341af763001e8803e8267652c2652620",
119
+ "bun-linux-x64.zip":
120
+ "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f",
121
+ "bun-linux-x64-musl.zip":
122
+ "14bd9aedeebf1dba67e8def9531c89bc989ecfdf1de42e5bfcaf1b8cd9294719",
123
+ "bun-linux-aarch64.zip":
124
+ "a27ffb63a8310375836e0d6f668ae17fa8d8d18b88c37c821c65331973a19a3b",
125
+ "bun-linux-aarch64-musl.zip":
126
+ "b98e0ad3625c5c00d1d5b5ff55605c7adddbfae151861e68ade57b2d3b8703bb",
127
+ };
128
+
99
129
  if (!assetName) {
100
130
  console.error(
101
131
  `[ghfind] Unsupported platform: ${key}. Install Bun manually: https://bun.sh`,
@@ -116,6 +146,14 @@ try {
116
146
 
117
147
  const buffer = Buffer.from(await res.arrayBuffer());
118
148
 
149
+ const expected = SHA256[assetName];
150
+ const actual = createHash("sha256").update(buffer).digest("hex");
151
+ if (expected !== actual) {
152
+ throw new Error(
153
+ `checksum mismatch for ${assetName} (expected ${expected}, got ${actual})`,
154
+ );
155
+ }
156
+
119
157
  // Find the central directory file header signature (PK\x01\x02)
120
158
  const cdSig = "PK\x01\x02";
121
159
  let cdOffset = buffer.indexOf(cdSig, 0);
package/src/cli.ts CHANGED
@@ -140,6 +140,20 @@ interface CLIFlags {
140
140
  limitExplicit: boolean;
141
141
  }
142
142
 
143
+ /**
144
+ * Valid `--sort` values: the repo strategies plus the npm-package strategies
145
+ * (the `pkg` subcommand validates its own narrower set).
146
+ */
147
+ const SORT_VALUES = [
148
+ "best-match",
149
+ "stars",
150
+ "updated",
151
+ "forks",
152
+ "score",
153
+ "downloads",
154
+ "name",
155
+ ];
156
+
143
157
  function parseArgs(args: string[]): CLIFlags {
144
158
  const flags: CLIFlags = {
145
159
  query: "",
@@ -246,9 +260,17 @@ function parseArgs(args: string[]): CLIFlags {
246
260
  }
247
261
  break;
248
262
  }
249
- case "--sort":
250
- flags.sort = args[++i] as SearchOptions["sort"];
263
+ case "--sort": {
264
+ const sort = args[++i];
265
+ if (!sort || !SORT_VALUES.includes(sort)) {
266
+ console.error(
267
+ `Invalid sort: ${sort ?? "(missing)"} (use best-match|stars|updated|forks).`,
268
+ );
269
+ process.exit(1);
270
+ }
271
+ flags.sort = sort as SearchOptions["sort"];
251
272
  break;
273
+ }
252
274
  case "--token":
253
275
  flags.token = args[++i];
254
276
  break;
@@ -883,10 +905,11 @@ function rowsToCsv(header: string[], rows: string[][]): string {
883
905
 
884
906
  /** Markdown table for the same small local-state tables. */
885
907
  function rowsToMarkdown(header: string[], rows: string[][]): string {
908
+ const cell = (value: string) => value.replace(/\|/g, "\\|");
886
909
  return [
887
- `| ${header.join(" | ")} |`,
910
+ `| ${header.map(cell).join(" | ")} |`,
888
911
  `|${header.map(() => "---").join("|")}|`,
889
- ...rows.map((row) => `| ${row.join(" | ")} |`),
912
+ ...rows.map((row) => `| ${row.map(cell).join(" | ")} |`),
890
913
  ].join("\n");
891
914
  }
892
915
 
@@ -945,7 +968,12 @@ async function runReadme(flags: CLIFlags): Promise<void> {
945
968
  }
946
969
  try {
947
970
  const { owner, name } = repoFromRef(ref); // validates the ref
948
- const readme = await fetchReadme(owner, name, { token: flags.token });
971
+ const readme = await fetchReadme(owner, name, {
972
+ // A bare ref carries no visibility info, so the token is never
973
+ // forwarded to raw.githubusercontent.com.
974
+ token: flags.token,
975
+ private: false,
976
+ });
949
977
  if (!readme) {
950
978
  console.error(`No README found for ${owner}/${name}.`);
951
979
  process.exit(1);
@@ -1224,22 +1252,14 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
1224
1252
  }
1225
1253
  },
1226
1254
  compare: async (_ctx) => {
1227
- const search = createGitHubSearch(undefined, [flags.token ?? ""]);
1228
1255
  const repos: Repo[] = [];
1229
1256
  const missing: string[] = [];
1230
1257
  for (const fullName of flags.compare) {
1231
- const res = await search.search(
1232
- { keywords: [fullName], qualifiers: [], raw: fullName },
1233
- {
1234
- limit: 1,
1235
- sort: "stars",
1236
- json: false,
1237
- verbose: false,
1238
- token: flags.token,
1239
- },
1240
- );
1241
- if (res.repos.length > 0) repos.push(...res.repos);
1242
- else missing.push(fullName);
1258
+ try {
1259
+ repos.push(await resolveRepoFromRef(fullName, flags.token));
1260
+ } catch {
1261
+ missing.push(fullName);
1262
+ }
1243
1263
  }
1244
1264
  if (repos.length < 2) {
1245
1265
  console.error(
@@ -1281,11 +1301,9 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
1281
1301
  }
1282
1302
 
1283
1303
  main().catch((err) => {
1284
- if (err instanceof Error && err.name === "SearchCliError") {
1304
+ if (err instanceof SearchCliError) {
1285
1305
  // ponytail: known user-facing errors already carry a friendly message; skip the report prompt
1286
- console.error(
1287
- err instanceof SearchCliError ? err.userMessage : err.message,
1288
- );
1306
+ console.error(err.userMessage);
1289
1307
  } else {
1290
1308
  console.error(err instanceof Error ? err.message : String(err));
1291
1309
  reportError(err);
package/src/config.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  import type { Config } from "./types";
10
10
  import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
13
 
14
14
  /** Directory for persistent state (history, bookmarks, session). */
15
15
  export function stateDir(): string {
@@ -61,7 +61,17 @@ export function saveConfig(config: Config): void {
61
61
  Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")),
62
62
  );
63
63
  mkdirSync(dir, { recursive: true });
64
- writeFileSync(path, JSON.stringify(config, null, 2));
64
+ // The file holds a plaintext GitHub token — owner-only, and tighten any
65
+ // pre-existing world-readable file too.
66
+ writeFileSync(path, JSON.stringify(config, null, 2), {
67
+ encoding: "utf-8",
68
+ mode: 0o600,
69
+ });
70
+ try {
71
+ chmodSync(path, 0o600);
72
+ } catch {
73
+ // best effort (read-only file / unsupported filesystem)
74
+ }
65
75
  }
66
76
 
67
77
  function mergeWithEnv(config: Config): Config {
package/src/deepdive.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /** Repo deep-dive — fetch and format rich repo details. */
2
2
  import type { Repo } from "./types";
3
+ import { fetchReadme } from "./readme";
3
4
 
4
5
  const USER_AGENT = "ghfind/1.0";
5
6
 
@@ -56,11 +57,29 @@ export function repoFromRef(fullName: string): Repo {
56
57
  };
57
58
  }
58
59
 
60
+ /** Repo payload as returned by GET /repos/{owner}/{repo}. */
61
+ interface GitHubRepoApi {
62
+ description: string | null;
63
+ html_url: string;
64
+ stargazers_count: number;
65
+ forks_count: number;
66
+ subscribers_count?: number;
67
+ language: string | null;
68
+ topics?: string[];
69
+ archived: boolean;
70
+ fork: boolean;
71
+ private?: boolean;
72
+ created_at: string;
73
+ updated_at: string;
74
+ pushed_at: string;
75
+ }
76
+
59
77
  /**
60
78
  * Resolve a repo ref to a real Repo by fetching canonical metadata from the
61
- * GitHub API. Returns a stub via repoFromRef when the metadata fetch fails
62
- * (offline, rate limit) — the ref is still validated first. Deep-dive
63
- * sub-fetches tolerate missing data either way.
79
+ * GitHub API. The ref is validated first. Only transport-level failures
80
+ * (offline, unparseable body) degrade to a stub via repoFromRef — an auth
81
+ * error or a rate limit is surfaced, because a stub with `stars: 0` is
82
+ * indistinguishable from a real repo with no data.
64
83
  */
65
84
  export async function resolveRepoFromRef(
66
85
  fullName: string,
@@ -68,58 +87,50 @@ export async function resolveRepoFromRef(
68
87
  ): Promise<Repo> {
69
88
  const ref = parseRepoRef(fullName); // throws for malformed refs
70
89
  const canonical = `${ref.owner}/${ref.name}`;
90
+
91
+ let res: Response;
71
92
  try {
72
- const res = await fetch(`https://api.github.com/repos/${canonical}`, {
93
+ res = await fetch(`https://api.github.com/repos/${canonical}`, {
73
94
  headers: buildHeaders(token),
74
95
  });
75
- if (res.status === 404) {
76
- throw new Error(`Repo not found: ${canonical}`);
77
- }
78
- if (res.ok) {
79
- const api = (await res.json()) as {
80
- description: string | null;
81
- html_url: string;
82
- stargazers_count: number;
83
- forks_count: number;
84
- subscribers_count?: number;
85
- language: string | null;
86
- topics?: string[];
87
- archived: boolean;
88
- fork: boolean;
89
- created_at: string;
90
- updated_at: string;
91
- pushed_at: string;
92
- };
93
- return {
94
- id: 0,
95
- fullName: canonical,
96
- name: ref.name,
97
- owner: ref.owner,
98
- description: api.description,
99
- url: api.html_url,
100
- stars: api.stargazers_count ?? 0,
101
- forks: api.forks_count ?? 0,
102
- watchers: api.subscribers_count ?? 0,
103
- language: api.language,
104
- topics: api.topics ?? [],
105
- archived: api.archived ?? false,
106
- isFork: api.fork ?? false,
107
- private: false,
108
- createdAt: api.created_at ?? "",
109
- updatedAt: api.updated_at ?? "",
110
- pushedAt: api.pushed_at ?? "",
111
- score: 0,
112
- };
113
- }
114
- } catch (err) {
115
- // Let the not-found error propagate; only network/parse failures degrade
116
- // to the stub below.
117
- if (err instanceof Error && err.message.startsWith("Repo not found")) {
118
- throw err;
119
- }
120
- // Fall through to the stub.
96
+ } catch {
97
+ return repoFromRef(canonical);
98
+ }
99
+
100
+ if (res.status === 404) {
101
+ throw new Error(`Repo not found: ${canonical}`);
102
+ }
103
+ if (!res.ok) {
104
+ throw new Error(`GitHub API error ${res.status} for ${canonical}`);
105
+ }
106
+
107
+ let api: GitHubRepoApi;
108
+ try {
109
+ api = (await res.json()) as GitHubRepoApi;
110
+ } catch {
111
+ return repoFromRef(canonical);
121
112
  }
122
- return repoFromRef(canonical);
113
+
114
+ return {
115
+ id: 0,
116
+ fullName: canonical,
117
+ name: ref.name,
118
+ owner: ref.owner,
119
+ description: api.description,
120
+ url: api.html_url,
121
+ stars: api.stargazers_count ?? 0,
122
+ forks: api.forks_count ?? 0,
123
+ watchers: api.subscribers_count ?? 0,
124
+ language: api.language,
125
+ topics: api.topics ?? [],
126
+ archived: api.archived ?? false,
127
+ isFork: api.fork ?? false,
128
+ private: api.private ?? false,
129
+ createdAt: api.created_at ?? "",
130
+ updatedAt: api.updated_at ?? "",
131
+ pushedAt: api.pushed_at ?? "",
132
+ score: 0,
133
+ };
123
134
  }
124
135
 
125
136
  /** Language breakdown: { [lang]: bytes } */
@@ -176,21 +187,14 @@ export async function fetchDeepDiveRaw(
176
187
  ),
177
188
  ]);
178
189
 
179
- // README with branch fallback: main → master → README.rst
180
- let readmeText = "";
181
- for (const path of [
182
- `${repo.owner}/${repo.name}/main/README.md`,
183
- `${repo.owner}/${repo.name}/master/README.md`,
184
- `${repo.owner}/${repo.name}/main/README.rst`,
185
- ]) {
186
- const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
187
- headers,
188
- });
189
- if (r.ok) {
190
- readmeText = await r.text();
191
- break;
192
- }
193
- }
190
+ // README: main → master → README.rst, probed in parallel by fetchReadme.
191
+ // Public READMEs are served unauthenticated, so the token is only forwarded
192
+ // to raw.githubusercontent.com when the repo actually needs it.
193
+ const readme = await fetchReadme(repo.owner, repo.name, {
194
+ token: repo.private ? token : undefined,
195
+ private: repo.private,
196
+ });
197
+ const readmeText = readme?.text ?? "";
194
198
 
195
199
  const langEntries: [string, number][] = languagesRes.ok
196
200
  ? Object.entries(
package/src/doctor.ts CHANGED
@@ -11,6 +11,7 @@ import { join, dirname } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { homedir, platform, arch } from "node:os";
13
13
  import { loadConfig, configPath, stateDir, cacheDir } from "./config";
14
+ import { maskToken } from "./login";
14
15
 
15
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
17
  const PKG_DIR = join(__dirname, "..");
@@ -212,8 +213,8 @@ function checkGithubToken(): void {
212
213
  const config = loadConfig();
213
214
  const token = config.githubToken || process.env.GITHUB_TOKEN;
214
215
  if (token) {
215
- const preview = token.slice(0, 7);
216
- add("PASS", "GitHub token", `Configured (${preview}…).`);
216
+ const preview = maskToken(token);
217
+ add("PASS", "GitHub token", `Configured (${preview}).`);
217
218
  } else {
218
219
  add(
219
220
  "WARN",