From b57eaa6f9a0e48a427c63a4eae80e5134e958ca1 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Sun, 3 May 2026 09:54:11 -0600 Subject: [PATCH] ci: add CI workflow + fix tsc strict union narrowing in api.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI workflow runs `bun install --frozen-lockfile`, `bunx tsc --noEmit`, and shellcheck on bin/. The api handlers used `c.req.json().catch(() => ({}))` which produces a `T | {}` union — strict tsc rejects property access on the empty object. Annotated each `body` with the explicit `T` type so `{}` (which is structurally assignable to a type whose props are all optional) lands on the right branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++++++++ src/api.ts | 13 ++++++++----- 2 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ab92704 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + typecheck: + name: Typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install --frozen-lockfile + - run: bunx tsc --noEmit + + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run shellcheck + run: | + shopt -s nullglob + fail=0 + for f in bin/*; do + if head -n1 "$f" | grep -qE '^#!.*\b(bash|sh)\b'; then + echo "▶ shellcheck $f" + shellcheck "$f" || fail=1 + fi + done + exit "$fail" diff --git a/src/api.ts b/src/api.ts index c96fef2..94d5101 100644 --- a/src/api.ts +++ b/src/api.ts @@ -44,7 +44,7 @@ api.get("/project", async (c) => { }); api.post("/project/run-task", async (c) => { - const body = await c.req.json<{ task?: string }>().catch(() => ({})); + const body: { task?: string } = await c.req.json<{ task?: string }>().catch(() => ({})); const task = body.task; if (!task || !/^[A-Za-z0-9_-]+$/.test(task)) { return c.json({ error: "invalid task name" }, 400); @@ -60,7 +60,7 @@ api.post("/project/run-task", async (c) => { }); api.post("/project/run-lane", async (c) => { - const body = await c.req.json<{ lane?: string }>().catch(() => ({})); + const body: { lane?: string } = await c.req.json<{ lane?: string }>().catch(() => ({})); const lane = body.lane; if (!lane || !/^[A-Za-z0-9_-]+$/.test(lane)) { return c.json({ error: "invalid lane name" }, 400); @@ -103,7 +103,10 @@ api.get("/plugins/search", async (c) => { }); api.post("/plugins/install", async (c) => { - const body = await c.req.json<{ source?: string; force?: boolean }>().catch(() => ({})); + const body: { source?: string; force?: boolean } = await c + .req + .json<{ source?: string; force?: boolean }>() + .catch(() => ({})); if (!body.source) return c.json({ error: "source required" }, 400); // --yes is required because FLEDGE_NON_INTERACTIVE is set in the env and // install would otherwise bail on the trust-prompt. @@ -119,7 +122,7 @@ api.post("/plugins/install", async (c) => { }); api.post("/plugins/remove", async (c) => { - const body = await c.req.json<{ name?: string }>().catch(() => ({})); + const body: { name?: string } = await c.req.json<{ name?: string }>().catch(() => ({})); if (!body.name) return c.json({ error: "name required" }, 400); const result = await fledge(["plugins", "remove", body.name, "--json"]); return c.json({ @@ -131,7 +134,7 @@ api.post("/plugins/remove", async (c) => { }); api.post("/plugins/update", async (c) => { - const body = await c.req.json<{ name?: string }>().catch(() => ({})); + const body: { name?: string } = await c.req.json<{ name?: string }>().catch(() => ({})); const args = ["plugins", "update"]; if (body.name) args.push(body.name); args.push("--json");