diff --git a/src/cli.js b/src/cli.js index 99837c6..2fdceab 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,14 +1,14 @@ -// repo-releases CLI: `list [--fixture path] [--limit N]`. +// repo-releases CLI: `list [--fixture path] [--limit N] [--json]`. // Uses a local cache over the releases seam; with --fixture it reads a canned // upstream response instead of the network (the fast/offline path). import { readFileSync } from "node:fs"; import { rawReleases, parseReleases } from "./github.js"; import { ReleaseCache } from "./cache.js"; -import { formatTags } from "./format.js"; +import { formatTags, formatJson } from "./format.js"; const cache = new ReleaseCache({ ttlMs: 60_000 }); -async function list(slug, { fixture, limit }) { +async function list(slug, { fixture, limit, json }) { const [owner, repo] = String(slug).split("/"); if (!owner || !repo) throw new Error(`expected , got "${slug}"`); const key = `${owner}/${repo}`; @@ -20,7 +20,7 @@ async function list(slug, { fixture, limit }) { parsed = parseReleases(raw); cache.put(key, parsed); } - return formatTags(parsed, limit); + return json ? formatJson(parsed, limit) : formatTags(parsed, limit); } function parseArgs(argv) { @@ -29,13 +29,14 @@ function parseArgs(argv) { for (let i = 0; i < rest.length; i++) { if (rest[i] === "--fixture") opts.fixture = rest[++i]; else if (rest[i] === "--limit") opts.limit = Number(rest[++i]); + else if (rest[i] === "--json") opts.json = true; } return { cmd, slug, opts }; } const { cmd, slug, opts } = parseArgs(process.argv.slice(2)); if (cmd !== "list" || !slug) { - process.stderr.write("usage: repo-releases list [--fixture path] [--limit N]\n"); + process.stderr.write("usage: repo-releases list [--fixture path] [--limit N] [--json]\n"); process.exit(2); } list(slug, opts) diff --git a/src/format.js b/src/format.js index e09ea3b..059d50b 100644 --- a/src/format.js +++ b/src/format.js @@ -1,3 +1,8 @@ +// Render domain releases as a JSON array, newest first, top N (machine-readable). +export function formatJson(releases, limit = 10) { + return JSON.stringify(releases.slice(0, limit), null, 2); +} + // Render domain releases as aligned "tag name date" lines, newest first, top N. export function formatTags(releases, limit = 10) { const rows = releases.slice(0, limit); diff --git a/tests/e2e/json.test.js b/tests/e2e/json.test.js new file mode 100644 index 0000000..48e7cf8 --- /dev/null +++ b/tests/e2e/json.test.js @@ -0,0 +1,10 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; + +test("e2e: `list --json` emits valid JSON with the newest tag", () => { + const r = spawnSync("node", ["src/cli.js", "list", "cli/cli", "--json", "--fixture", "fixtures/releases.cli-cli.json"], { encoding: "utf8" }); + assert.equal(r.status, 0, "cli exits 0"); + const arr = JSON.parse(r.stdout); + assert.equal(arr[0].tag, "v2.62.0", "newest tag first"); +}); diff --git a/tests/format.test.js b/tests/format.test.js index 04e979e..e23b8fa 100644 --- a/tests/format.test.js +++ b/tests/format.test.js @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { parseReleases } from "../src/github.js"; -import { formatTags } from "../src/format.js"; +import { formatTags, formatJson } from "../src/format.js"; test("parse maps real API fields and drops drafts", () => { const parsed = parseReleases([ @@ -15,3 +15,8 @@ test("format lists tag and name", () => { const out = formatTags([{ tag: "v2", name: "two", publishedAt: "2024-02-01T00:00:00Z" }], 5); assert.match(out, /v2\s+two/); }); + +test("formatJson emits a JSON array of {tag,name,publishedAt}", () => { + const out = formatJson([{ tag: "v2", name: "two", publishedAt: "2024-02-01T00:00:00Z" }], 5); + assert.deepEqual(JSON.parse(out), [{ tag: "v2", name: "two", publishedAt: "2024-02-01T00:00:00Z" }]); +});