Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ jobs:

- run: bun install --frozen-lockfile

# Lint first — cheapest check. Biome covers formatting + general
# lint; ESLint catches layering violations via eslint-plugin-boundaries.
- run: bun run lint

# Typecheck before tests: `bun test` strips types and won't catch
# type errors on its own. Running tsc first short-circuits the job
# on type failures instead of wasting the test-run time.
Expand Down
41 changes: 41 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.12/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"includes": [
"src/**",
"scripts/**",
"*.{json,ts,js}",
"!src/shared/bitbucket-http/generated.d.ts"
]
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noNonNullAssertion": "off"
},
"suspicious": {
"noExplicitAny": "off",
"noControlCharactersInRegex": "off"
}
}
},
"javascript": {
"formatter": { "quoteStyle": "double" }
},
"assist": {
"enabled": true,
"actions": { "source": { "organizeImports": "on" } }
}
}
240 changes: 236 additions & 4 deletions bun.lock

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Intentionally narrow: ESLint is here only to enforce the
// commands → backend → shared dependency rule via
// eslint-plugin-boundaries. Formatting and general lint are Biome's job.
//
// See BBC2-34 for the rationale.

import tsParser from "@typescript-eslint/parser";
import boundaries from "eslint-plugin-boundaries";

export default [
{
files: ["src/**/*.ts"],
ignores: ["src/shared/bitbucket-http/generated.d.ts", "src/**/*.test.ts"],
languageOptions: {
parser: tsParser,
ecmaVersion: "latest",
sourceType: "module",
},
plugins: { boundaries },
settings: {
"boundaries/elements": [
{ type: "commands", pattern: "src/commands/*", mode: "folder" },
{ type: "backend", pattern: "src/backend/*", mode: "folder" },
{ type: "shared", pattern: "src/shared/*", mode: "folder" },
],
},
rules: {
"boundaries/dependencies": [
"error",
{
default: "disallow",
rules: [
{ from: "commands", allow: ["commands", "backend", "shared"] },
{
from: "backend",
allow: [
["backend", { elementName: "{{from.elementName}}" }],
"shared",
],
},
{ from: "shared", allow: ["shared"] },
],
},
],
},
},
];
54 changes: 30 additions & 24 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
{
"name": "bbcli",
"type": "module",
"private": true,
"bin": {
"bb": "./src/index.ts"
},
"scripts": {
"generate:api": "bun run scripts/generate-api.ts"
},
"devDependencies": {
"@types/bun": "latest",
"msw": "^2.13.3",
"openapi-typescript": "^7.13.0"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"cli-table3": "^0.6.5",
"commander": "^14.0.3",
"openapi-fetch": "^0.17.0",
"picocolors": "^1.1.1",
"zod": "^4.3.6"
}
"name": "bbcli",
"type": "module",
"private": true,
"bin": {
"bb": "./src/index.ts"
},
"scripts": {
"generate:api": "bun run scripts/generate-api.ts",
"format": "biome format --write .",
"lint": "biome check . && eslint src"
},
"devDependencies": {
"@biomejs/biome": "2.4.12",
"@types/bun": "latest",
"@typescript-eslint/parser": "^8.58.2",
"eslint": "^10.2.0",
"eslint-plugin-boundaries": "^6.0.2",
"msw": "^2.13.3",
"openapi-typescript": "^7.13.0"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"cli-table3": "^0.6.5",
"commander": "^14.0.3",
"openapi-fetch": "^0.17.0",
"picocolors": "^1.1.1",
"zod": "^4.3.6"
}
}
105 changes: 59 additions & 46 deletions scripts/generate-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@
* Usage: bun run generate:api
*/
import openapiTS, { astToString, type OpenAPI3 } from "openapi-typescript";
import { overlay, type OperationOverlay, type OverlayParameter } from "./openapi-overlay.ts";
import {
type OperationOverlay,
type OverlayParameter,
overlay,
} from "./openapi-overlay.ts";

const SPEC_URL = "https://dac-static.atlassian.com/cloud/bitbucket/swagger.v3.json";
const OUTPUT_PATH = new URL("../src/shared/bitbucket-http/generated.d.ts", import.meta.url);
const SPEC_URL =
"https://dac-static.atlassian.com/cloud/bitbucket/swagger.v3.json";
const OUTPUT_PATH = new URL(
"../src/shared/bitbucket-http/generated.d.ts",
import.meta.url,
);

const HEADER = `/**
* AUTO-GENERATED — do not edit by hand.
Expand All @@ -29,7 +37,7 @@ const HEADER = `/**
console.log(`Fetching OpenAPI spec from ${SPEC_URL}...`);
const response = await fetch(SPEC_URL);
if (!response.ok) {
throw new Error(`Failed to fetch spec: HTTP ${response.status}`);
throw new Error(`Failed to fetch spec: HTTP ${response.status}`);
}
const spec = (await response.json()) as Record<string, any>;

Expand All @@ -41,51 +49,56 @@ const ast = await openapiTS(spec as OpenAPI3);
const contents = HEADER + astToString(ast);

await Bun.write(OUTPUT_PATH, contents);
console.log(`Wrote ${contents.length.toLocaleString()} bytes to ${OUTPUT_PATH.pathname}`);
console.log(
`Wrote ${contents.length.toLocaleString()} bytes to ${OUTPUT_PATH.pathname}`,
);

function applyOverlay(
spec: Record<string, any>,
overlay: Record<string, Record<string, OperationOverlay>>,
spec: Record<string, any>,
overlay: Record<string, Record<string, OperationOverlay>>,
): void {
const paths = spec["paths"] as Record<string, any> | undefined;
if (!paths) throw new Error("Spec has no `paths` object — refusing to apply overlay.");
const paths = spec.paths as Record<string, any> | undefined;
if (!paths)
throw new Error("Spec has no `paths` object — refusing to apply overlay.");

for (const [path, methods] of Object.entries(overlay)) {
const pathItem = paths[path];
if (!pathItem) {
// Loud warning: an overlay entry that doesn't match the spec is
// either a typo or a sign Bitbucket renamed something. Either way
// the maintainer needs to know.
console.warn(` ! overlay path not found in spec: ${path}`);
continue;
}
for (const [method, mods] of Object.entries(methods)) {
const op = pathItem[method];
if (!op) {
console.warn(` ! overlay method not found: ${method.toUpperCase()} ${path}`);
continue;
}
op.parameters ??= [];
const params = op.parameters as OverlayParameter[];
for (const [path, methods] of Object.entries(overlay)) {
const pathItem = paths[path];
if (!pathItem) {
// Loud warning: an overlay entry that doesn't match the spec is
// either a typo or a sign Bitbucket renamed something. Either way
// the maintainer needs to know.
console.warn(` ! overlay path not found in spec: ${path}`);
continue;
}
for (const [method, mods] of Object.entries(methods)) {
const op = pathItem[method];
if (!op) {
console.warn(
` ! overlay method not found: ${method.toUpperCase()} ${path}`,
);
continue;
}
op.parameters ??= [];
const params = op.parameters as OverlayParameter[];

if (mods.replaceParameters) {
for (const replacement of mods.replaceParameters) {
const idx = params.findIndex(
(p) => p.name === replacement.name && p.in === replacement.in,
);
if (idx >= 0) params[idx] = replacement;
else params.push(replacement);
}
}
if (mods.addParameters) {
for (const addition of mods.addParameters) {
const exists = params.some(
(p) => p.name === addition.name && p.in === addition.in,
);
if (!exists) params.push(addition);
}
}
console.log(` ✓ overlaid ${method.toUpperCase()} ${path}`);
}
}
if (mods.replaceParameters) {
for (const replacement of mods.replaceParameters) {
const idx = params.findIndex(
(p) => p.name === replacement.name && p.in === replacement.in,
);
if (idx >= 0) params[idx] = replacement;
else params.push(replacement);
}
}
if (mods.addParameters) {
for (const addition of mods.addParameters) {
const exists = params.some(
(p) => p.name === addition.name && p.in === addition.in,
);
if (!exists) params.push(addition);
}
}
console.log(` ✓ overlaid ${method.toUpperCase()} ${path}`);
}
}
}
Loading
Loading