Show the remaining GreenPT API credit balance in the OpenCode TUI sidebar. The key is resolved from the ``apiKey`` option, the ``GREENPT_API_KEY`` environment variable, or the active OpenCode ``auth.json`` (provider ``greenpt``); the section is hidden when no key is present, so the plugin stays portable across machines and profiles. The balance is read from the ``x-credits-remaining`` header returned by a free ``HEAD /v1/chat/completions`` probe (HTTP 400 before any credit is consumed), which avoids a billable call per refresh. Tests cover the credential resolution, the header parsing and the live API. Distribute via git clone; there is no npm publish.
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest"
|
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
|
import { tmpdir } from "node:os"
|
|
import { join } from "node:path"
|
|
import { credentialSearchDirs, resolveGreenptKey } from "../src/credential"
|
|
|
|
const created: string[] = []
|
|
|
|
function tempDir(): string {
|
|
const dir = mkdtempSync(join(tmpdir(), "greenpt-cred-"))
|
|
created.push(dir)
|
|
return dir
|
|
}
|
|
|
|
function writeAuth(dir: string, content: unknown): void {
|
|
writeFileSync(join(dir, "auth.json"), JSON.stringify(content))
|
|
}
|
|
|
|
afterEach(() => {
|
|
while (created.length > 0) {
|
|
const dir = created.pop()
|
|
if (dir) rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
describe("resolveGreenptKey", () => {
|
|
it("prefers the explicit apiKey option", () => {
|
|
const dir = tempDir()
|
|
writeAuth(dir, { greenpt: { type: "api", key: "from-auth" } })
|
|
const result = resolveGreenptKey({ apiKey: "from-option" }, dir, { GREENPT_API_KEY: "from-env" })
|
|
expect(result).toEqual({ ok: true, key: "from-option" })
|
|
})
|
|
|
|
it("prefers the environment variable over the auth store", () => {
|
|
const dir = tempDir()
|
|
writeAuth(dir, { greenpt: { type: "api", key: "from-auth" } })
|
|
const result = resolveGreenptKey({}, dir, { GREENPT_API_KEY: "from-env" })
|
|
expect(result).toEqual({ ok: true, key: "from-env" })
|
|
})
|
|
|
|
it("reads the active auth store when nothing else is set", () => {
|
|
const dir = tempDir()
|
|
writeAuth(dir, { greenpt: { type: "api", key: "from-auth" } })
|
|
const result = resolveGreenptKey({}, dir, {})
|
|
expect(result).toEqual({ ok: true, key: "from-auth" })
|
|
})
|
|
|
|
it("ignores OAuth entries", () => {
|
|
const dir = tempDir()
|
|
writeAuth(dir, { greenpt: { type: "oauth", access: "token" } })
|
|
const result = resolveGreenptKey({}, dir, {})
|
|
expect(result).toEqual({ ok: false, reason: "missing" })
|
|
})
|
|
|
|
it("ignores empty keys", () => {
|
|
const dir = tempDir()
|
|
writeAuth(dir, { greenpt: { type: "api", key: "" } })
|
|
const result = resolveGreenptKey({}, dir, {})
|
|
expect(result).toEqual({ ok: false, reason: "missing" })
|
|
})
|
|
|
|
it("returns missing when no credential is available", () => {
|
|
const dir = tempDir()
|
|
const result = resolveGreenptKey({}, dir, {})
|
|
expect(result).toEqual({ ok: false, reason: "missing" })
|
|
})
|
|
|
|
it("falls back to XDG data home", () => {
|
|
const xdg = tempDir()
|
|
const stateDir = join(xdg, "opencode")
|
|
mkdirSync(stateDir)
|
|
writeAuth(stateDir, { greenpt: { type: "api", key: "from-xdg" } })
|
|
const result = resolveGreenptKey({}, undefined, { XDG_DATA_HOME: xdg })
|
|
expect(result).toEqual({ ok: true, key: "from-xdg" })
|
|
})
|
|
})
|
|
|
|
describe("credentialSearchDirs", () => {
|
|
it("puts the state dir first and de-duplicates", () => {
|
|
const dirs = credentialSearchDirs("/state", {
|
|
XDG_DATA_HOME: "/xdg",
|
|
OPENCODE_DATA_DIR: "/state",
|
|
})
|
|
expect(dirs[0]).toBe("/state")
|
|
expect(dirs).toContain(join("/xdg", "opencode"))
|
|
expect(new Set(dirs).size).toBe(dirs.length)
|
|
})
|
|
|
|
it("falls back to ~/.local/share/opencode", () => {
|
|
const dirs = credentialSearchDirs(undefined, { HOME: "/home/tester" })
|
|
expect(dirs).toContain(join("/home/tester", ".local", "share", "opencode"))
|
|
})
|
|
})
|