diff --git a/README.md b/README.md index a3e0c1e..a0a99bd 100644 --- a/README.md +++ b/README.md @@ -17,19 +17,18 @@ ## CLI -`prune-github-notifications` can be run on the CLI with an auth token for _notifications_ access specified as a `GH_TOKEN` environment variable: +`prune-github-notifications` can be run on the CLI with an auth token for _notifications_ access: ```shell -GH_TOKEN=$(gh auth token) npx prune-github-notifications +npx prune-github-notifications ``` -### CLI Options +[`get-github-auth-token`](https://github.com/JoshuaKGoldberg/get-github-auth-token) is used to retrieve a GitHub auth token from `process.env.GH_TOKEN` or executing `gh auth token`. -Only `auth` is required, and only if a `GH_TOKEN` isn't available. +### CLI Options | Option | Type | Default | Description | | ------------- | ---------- | -------------------------------- | ------------------------------------------------------------- | -| `--auth` | `string` | `process.env.GH_TOKEN` | GitHub authentication token with _notifications_ access. | | `--bandwidth` | `number` | `6` | Maximum parallel requests to start at once. | | `--reason` | `string[]` | `["subscribed"]` | Notification reason(s) to filter to. | | `--title` | `string` | `"^chore\(deps\): update .+ to"` | Notification title regular expression to filter to. | @@ -38,13 +37,13 @@ Only `auth` is required, and only if a `GH_TOKEN` isn't available. For example, providing all options on the CLI: ```shell -npx prune-github-notifications --auth $(gh auth token) --bandwidth 10 --reason subscribed --title "^chore.+ update .+ to" +npx prune-github-notifications --bandwidth 10 --reason subscribed --title "^chore.+ update .+ to" ``` Running in watch mode to clear notifications every ten seconds: ```shell -npx prune-github-notifications --auth $(gh auth token) --watch 10 +npx prune-github-notifications --watch 10 ``` ## Node.js API diff --git a/package.json b/package.json index da61385..22efdec 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "chalk": "^5.3.0", + "get-github-auth-token": "^0.1.0", "octokit": "^3.1.2", "throttled-queue": "^2.1.4", "zod": "^3.22.4" @@ -76,7 +77,6 @@ "vitest": "^1.3.1", "yaml-eslint-parser": "^1.2.2" }, - "packageManager": "pnpm@9.0.6", "engines": { "node": ">=20" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c942254..0d88c2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: chalk: specifier: ^5.3.0 version: 5.3.0 + get-github-auth-token: + specifier: ^0.1.0 + version: 0.1.0 octokit: specifier: ^3.1.2 version: 3.2.0 @@ -2128,6 +2131,10 @@ packages: get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-github-auth-token@0.1.0: + resolution: {integrity: sha512-ENm+A39AV0X4+Ls1jiCvmqx+C8hSYTv4d5hV9Ks+EL+gx9a0P9pYYpRE1k2ExwRT3EFQabGXF1Rkdp5FIsLkiw==} + engines: {node: '>=18'} + get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -2192,6 +2199,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} @@ -2365,6 +2373,7 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -6307,6 +6316,8 @@ snapshots: get-func-name@2.0.2: {} + get-github-auth-token@0.1.0: {} + get-intrinsic@1.2.4: dependencies: es-errors: 1.3.0 diff --git a/src/cli.test.ts b/src/cli.test.ts index 6c9219d..1928f9e 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -21,28 +21,8 @@ vi.mock("./runInWatch.js", () => ({ })); describe("pruneGitHubNotificationsCLI", () => { - it("throws an error when auth is not available", async () => { - await expect(async () => { - await pruneGitHubNotificationsCLI([]); - }).rejects.toMatchInlineSnapshot(` - [ZodError: [ - { - "code": "invalid_type", - "expected": "string", - "received": "undefined", - "path": [ - "auth" - ], - "message": "--auth is required if a GH_TOKEN environment variable is not specified." - } - ]] - `); - }); - it("passes parsed arguments to pruneGitHubNotifications when they're valid and watch mode is not enabled", async () => { await pruneGitHubNotificationsCLI([ - "--auth", - "abc_def", "--bandwidth", "123", "--reason", @@ -54,7 +34,6 @@ describe("pruneGitHubNotificationsCLI", () => { ]); expect(mockPruneGitHubNotifications).toHaveBeenCalledWith({ - auth: "abc_def", bandwidth: 123, filters: { reason: new Set(["abc", "def"]), @@ -66,8 +45,6 @@ describe("pruneGitHubNotificationsCLI", () => { it("passes parsed arguments to runInWatch when they're valid and watch mode is enabled", async () => { await pruneGitHubNotificationsCLI([ - "--auth", - "abc_def", "--bandwidth", "123", "--reason", @@ -81,7 +58,6 @@ describe("pruneGitHubNotificationsCLI", () => { ]); expect(mockPruneGitHubNotifications).toHaveBeenCalledWith({ - auth: "abc_def", bandwidth: 123, filters: { reason: new Set(["abc", "def"]), diff --git a/src/cli.ts b/src/cli.ts index 6beb371..5eb1463 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,10 +5,6 @@ import { pruneGitHubNotifications } from "./pruneGitHubNotifications.js"; import { runInWatch } from "./runInWatch.js"; const schema = z.object({ - auth: z.string({ - required_error: - "--auth is required if a GH_TOKEN environment variable is not specified.", - }), bandwidth: z.coerce.number().optional(), reason: z .array(z.string()) @@ -46,11 +42,10 @@ export async function pruneGitHubNotificationsCLI(args: string[]) { tokens: true, }); - const { auth, bandwidth, reason, title, watch } = schema.parse(values); + const { bandwidth, reason, title, watch } = schema.parse(values); const action = async () => await pruneGitHubNotifications({ - auth, bandwidth, filters: { reason, diff --git a/src/pruneGitHubNotifications.test.ts b/src/pruneGitHubNotifications.test.ts index 767f1a4..b5590d3 100644 --- a/src/pruneGitHubNotifications.test.ts +++ b/src/pruneGitHubNotifications.test.ts @@ -2,6 +2,14 @@ import { describe, expect, it, vi } from "vitest"; import { pruneGitHubNotifications } from "./pruneGitHubNotifications.js"; +const mockGetGitHubAuthToken = vi.fn(); + +vi.mock("get-github-auth-token", () => ({ + get getGitHubAuthToken() { + return mockGetGitHubAuthToken; + }, +})); + const mockRequest = vi.fn().mockResolvedValue({ data: [ { @@ -43,15 +51,23 @@ vi.mock("octokit", () => ({ describe("pruneGitHubNotifications", () => { it("throws an error when auth is not available", async () => { + mockGetGitHubAuthToken.mockResolvedValue({ + error: "Oh no!", + succeeded: false, + }); + await expect(async () => { await pruneGitHubNotifications(); - }).rejects.toMatchInlineSnapshot( - `[Error: Please provide an auth token (process.env.GH_TOKEN).]`, - ); + }).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); }); it("unsubscribes from threads based on default filters when no filters are provided", async () => { - await pruneGitHubNotifications({ auth: "abc123" }); + mockGetGitHubAuthToken.mockResolvedValue({ + succeeded: true, + token: "abc123", + }); + + await pruneGitHubNotifications(); expect(mockRequest.mock.calls).toMatchInlineSnapshot(` [ @@ -86,8 +102,12 @@ describe("pruneGitHubNotifications", () => { }); it("unsubscribes from threads based on custom filters when custom filters are provided", async () => { + mockGetGitHubAuthToken.mockResolvedValue({ + succeeded: true, + token: "abc123", + }); + await pruneGitHubNotifications({ - auth: "abc123", filters: { reason: new Set(["other-reason"]), title: /other title/ }, }); diff --git a/src/pruneGitHubNotifications.ts b/src/pruneGitHubNotifications.ts index 1191c14..21463e9 100644 --- a/src/pruneGitHubNotifications.ts +++ b/src/pruneGitHubNotifications.ts @@ -1,3 +1,4 @@ +import { getGitHubAuthToken } from "get-github-auth-token"; import { Octokit } from "octokit"; import throttledQueue from "throttled-queue"; @@ -15,16 +16,15 @@ type ThrottledQueue = ( ) => (fn: () => Promise | Return) => Promise; export async function pruneGitHubNotifications({ - auth, bandwidth = defaultOptions.bandwidth, filters, }: PruneGitHubNotificationsOptions = {}): Promise { - auth ??= process.env.GH_TOKEN; - if (!auth) { - throw new Error(`Please provide an auth token (process.env.GH_TOKEN).`); + const auth = await getGitHubAuthToken(); + if (!auth.succeeded) { + throw new Error(auth.error); } - const octokit = new Octokit({ auth }); + const octokit = new Octokit({ auth: auth.token }); const notifications = await octokit.request("GET /notifications", { headers: { "X-GitHub-Api-Version": "2022-11-28", diff --git a/src/runInWatch.test.ts b/src/runInWatch.test.ts index 9fd3fb6..b1c2830 100644 --- a/src/runInWatch.test.ts +++ b/src/runInWatch.test.ts @@ -6,7 +6,7 @@ import { runInWatch } from "./runInWatch.js"; const mockLog = vi.fn(); const mockSetTimeout = vi.fn(); -describe("pruneGitHubNotificationsCLI", () => { +describe("runInWatch", () => { beforeEach(() => { console.log = mockLog; globalThis.setTimeout = mockSetTimeout as unknown as typeof setTimeout; diff --git a/src/types.ts b/src/types.ts index 7ee14c0..d5cef67 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,4 @@ export interface PruneGitHubNotificationsOptions { - auth?: string; bandwidth?: number; filters?: Partial; }