From 0a9f86a102c92aaa14414eaea80487bee817f479 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Sun, 18 Oct 2026 23:50:27 +0900 Subject: [PATCH] Group the sources into aliased modules and lint with the published plugins --- .agents/memory/bot-agents-child-processes.md | 8 +- .../memory/bot-agents-migration-decisions.md | 19 ++-- .agents/memory/bot-agents-tests-root-only.md | 2 +- .agents/memory/imports-aliases-barrels.md | 3 +- AGENTS.md | 13 ++- package-lock.json | 28 ++++++ package.json | 7 ++ scripts/protect-source.sh | 6 +- src/{ => agents}/auth.test.ts | 9 +- src/{ => agents}/auth.ts | 30 +++++-- src/agents/index.ts | 17 ++++ src/{ => agents}/opencode.test.ts | 6 +- src/{ => agents}/opencode.ts | 24 +++-- src/{ => common}/errors.ts | 0 src/common/index.ts | 4 + src/{ => common}/json.ts | 2 +- src/post.test.ts | 30 ++++--- src/post.ts | 16 ++-- src/prepare.ts | 2 +- src/{ => processes}/child.test.ts | 4 +- src/{ => processes}/child.ts | 0 src/processes/index.ts | 4 + src/{ => processes}/isolation.test.ts | 14 +-- src/{ => processes}/isolation.ts | 4 +- src/profiles/index.ts | 12 +++ src/{ => profiles}/profiles.test.ts | 1 + src/{ => profiles}/profiles.ts | 34 ++++--- src/run.ts | 88 ++++++++++--------- src/{ => runtime}/credentials.test.ts | 1 + src/{ => runtime}/credentials.ts | 12 ++- src/runtime/index.ts | 10 +++ src/{ => runtime}/runtime.test.ts | 7 +- src/{ => runtime}/runtime.ts | 32 ++++--- src/testing/index.ts | 2 +- tests/entrypoints.test.ts | 1 - tests/workflow.test.ts | 5 +- vite.config.ts | 15 +++- 37 files changed, 317 insertions(+), 155 deletions(-) rename src/{ => agents}/auth.test.ts (98%) rename src/{ => agents}/auth.ts (88%) create mode 100644 src/agents/index.ts rename src/{ => agents}/opencode.test.ts (97%) rename src/{ => agents}/opencode.ts (84%) rename src/{ => common}/errors.ts (100%) create mode 100644 src/common/index.ts rename src/{ => common}/json.ts (89%) rename src/{ => processes}/child.test.ts (96%) rename src/{ => processes}/child.ts (100%) create mode 100644 src/processes/index.ts rename src/{ => processes}/isolation.test.ts (94%) rename src/{ => processes}/isolation.ts (95%) create mode 100644 src/profiles/index.ts rename src/{ => profiles}/profiles.test.ts (99%) rename src/{ => profiles}/profiles.ts (79%) rename src/{ => runtime}/credentials.test.ts (99%) rename src/{ => runtime}/credentials.ts (95%) create mode 100644 src/runtime/index.ts rename src/{ => runtime}/runtime.test.ts (95%) rename src/{ => runtime}/runtime.ts (87%) diff --git a/.agents/memory/bot-agents-child-processes.md b/.agents/memory/bot-agents-child-processes.md index 65bc1e2..6d70508 100644 --- a/.agents/memory/bot-agents-child-processes.md +++ b/.agents/memory/bot-agents-child-processes.md @@ -1,5 +1,5 @@ --- -description: Child processes use plain node:child_process spawn (explicit stdio, uid/gid, whole env); src/child.ts has exitCode (128+signo) and echo; no execFile maxBuffer +description: Child processes use plain node:child_process spawn (explicit stdio, uid/gid, whole env); src/processes/child.ts has exitCode (128+signo) and echo; no execFile maxBuffer --- Every call site calls `spawn` from `node:child_process` itself, with @@ -15,7 +15,7 @@ everything explicit: - No `execFile`/`exec`: their `maxBuffer` (1 MiB) kills a chatty child. Read piped streams incrementally instead. -`src/child.ts` holds what the call sites share: +`src/processes/child.ts` holds what the call sites share: - **`exitCode(child)`**: call it right after `spawn`. It settles on `exit` (not `close`, which a daemonized grandchild can hold open), maps a signal to @@ -28,9 +28,9 @@ Tests use `spawnSync` for short commands. Other port details: -- **`src/errors.ts`** `isNotFound` checks `ENOENT` (file) or `ESRCH` (process). +- **`src/common/errors.ts`** `isNotFound` checks `ENOENT` (file) or `ESRCH` (process). - Temporary files are an exclusive `wx` create with mode `0600`. -- `src/json.ts` holds type guards used instead of `as` casts on parsed JSON. +- `src/common/json.ts` holds type guards used instead of `as` casts on parsed JSON. - `process` and `Buffer` must come from `node:` imports: `no-restricted-globals` in `scripts/project-lint.ts` enforces it ([[lint-parity-never-shrinks]]). diff --git a/.agents/memory/bot-agents-migration-decisions.md b/.agents/memory/bot-agents-migration-decisions.md index fc74f70..45d16b8 100644 --- a/.agents/memory/bot-agents-migration-decisions.md +++ b/.agents/memory/bot-agents-migration-decisions.md @@ -1,5 +1,5 @@ --- -description: bot-agents migration decisions: live @main action for six repos, JSON profiles, Node 24.2+, no eslint-plugin-* packages needed +description: bot-agents migration decisions: live @main action for six repos, JSON profiles, Node 24.2+, lints with eslint-plugin-import-structure and explicit-types (concrete, absence) --- Decisions for `commons/bot-agents` (Capsulizers Gitea), from the Deno to @@ -18,13 +18,16 @@ Node + Vite+ migration (PR #20, `node-migration`, 2026-10-15/16): - **Node 24.2+**, not 22: `src/post.ts` uses `import.meta.main`. There is no `engines` (user decision 2026-10-18): the `node:24-bookworm` job image decides the version ([[bot-agents-runtime-no-build]]). -- **No `eslint-plugin-*` packages** (`eslint-plugin-explicit-types`, - `eslint-plugin-import-structure`, `eslint-plugin-lit-robust`; unscoped - since 2026-10-18, formerly `@cunarist/...`). The Deno setup enabled no - plugin, so this repo depends on none and CI passes without them being - published. Planned (2026-10-17): once the plugins are on npm, add - `eslint-plugin-import-structure` (rule prefix `import-structure/`) here for - its import checks. +- **Published lint plugins** (2026-10-18, after they reached npm): + `eslint-plugin-import-structure` (`modules` config) and + `eslint-plugin-explicit-types` (`concrete` and `absence` configs) are + devDependencies, enabled in `vite.config.ts`. explicit-types' `naming` + config is left off: its `upper-snake-string-unions` and + `no-upper-snake-object-keys` rules fire on wire tokens this repo does not + name (profile types such as `"claude"`, failure reasons posted to Gitea, + environment variable names), which cannot change without changing + behavior. Open question for the user. `eslint-plugin-lit-robust` does not apply + (no Lit). - **Stricter input checks were accepted** in the migration: Gitea `/user` and comment responses, the runtime manifest, and the outcome's profile type are validated; some type errors became `TypeError` with the same messages. diff --git a/.agents/memory/bot-agents-tests-root-only.md b/.agents/memory/bot-agents-tests-root-only.md index e409af0..c0600a4 100644 --- a/.agents/memory/bot-agents-tests-root-only.md +++ b/.agents/memory/bot-agents-tests-root-only.md @@ -21,5 +21,5 @@ as skipped instead of silently passing. `runCommand`) behind the `#testing` alias. Every test imports it as `#testing`. It imports no source module, so `tests/` (which may not use `../`) never reaches internals. Tests follow - [[tests-layout-vitest]] (unit tests in `src/*.test.ts`, repo-level ones in + [[tests-layout-vitest]] (unit tests next to their code in `src/`, repo-level ones in `tests/`). diff --git a/.agents/memory/imports-aliases-barrels.md b/.agents/memory/imports-aliases-barrels.md index 1456445..a355cbf 100644 --- a/.agents/memory/imports-aliases-barrels.md +++ b/.agents/memory/imports-aliases-barrels.md @@ -97,7 +97,8 @@ It is not a plugin rule. - Every `package.json` `imports` alias points only at the `index.ts` of a direct subfolder of `src/`, e.g. `"#helpers": "./src/helpers/index.ts"`. No alias to a single file, a deeper folder, or anything outside `src/`. - Here the one alias is `"#testing": "./src/testing/index.ts"`. + Here the aliases are `#agents`, `#processes`, `#runtime`, `#profiles`, + `#common`, and `#testing`, in that layer order. - Code outside `src/` (`scripts/`, `tests/`, `vite.config.ts`) gets no alias of its own and imports its own files by `./` relative paths. It reaches `src/` only through the aliases, since `../` is banned. diff --git a/AGENTS.md b/AGENTS.md index 4b08021..a60b245 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ dependency. Keep the sources to erasable syntax and `.ts` imports, which `exactOptionalPropertyTypes` off ([.agents/memory/ts-only-files-and-imports.md](.agents/memory/ts-only-files-and-imports.md)). Checks use Vite+: `vp check` formats, lints, and type-checks, and `vp test` -runs Vitest. Unit tests sit next to the code as `src/*.test.ts`; +runs Vitest. Unit tests sit next to the code as `*.test.ts` in `src/`; repository-level tests live in `tests/`. A `#` alias in `package.json` `imports` may point anywhere and owns its @@ -34,9 +34,14 @@ import is banned in every file, owned or not (`import/no-relative-parent-imports`); runtime file URLs such as `new URL("../prompt.md", import.meta.url)` are paths, not imports, and stay. Files no alias owns need no `index.ts` and no alias, and import `./` siblings -and subfolders freely. The one alias is `#testing` (`./src/testing/index.ts`): -tests that use its helpers import it by alias, and `tests/` imports `src/` -only through it. Local convention, stricter than the general rule above: in +and subfolders freely. The aliases, in layer order, are `#agents` (Codex, +Claude Code and OpenCode specifics), `#processes` (child processes and the +isolated profile users), `#runtime` (the prepared runtime and its +credentials), `#profiles`, `#common` (error and JSON checks), and `#testing` +(test helpers), each a `src//index.ts` barrel. The phase entry points +`src/prepare.ts`, `src/run.ts`, and `src/post.ts` stay at the top of `src/`, +unowned, because `action.yml` runs them by path. Tests that use the test +helpers import `#testing`, and `tests/` imports `src/` only through it. Local convention, stricter than the general rule above: in this repo, aliases point only at a direct subfolder of `src/` (`"#helpers": "./src/helpers/index.ts"`), and code outside `src/` uses `./` relative imports. `scripts/` is flat: one file per tool, no subfolders and no `index.ts`, imported directly by file path diff --git a/package-lock.json b/package-lock.json index e0ecd3b..972a900 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "@eslint/core": "^0.17.0", "@types/node": "^24.19.2", "@typescript-eslint/types": "^8.71.2", + "eslint-plugin-explicit-types": "^0.6.1", + "eslint-plugin-import-structure": "^0.10.2", "vite-plus": "^1.2.0", "yaml": "^2.9.1" } @@ -1887,6 +1889,32 @@ "dev": true, "license": "MIT" }, + "node_modules/eslint-plugin-explicit-types": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-explicit-types/-/eslint-plugin-explicit-types-0.6.1.tgz", + "integrity": "sha512-8JU2Ceoi1aLbhYo4LHwb9Eone/pJuMnzVP1ZlGJn/ETxVzJQFziRnJwOF2dRmInn9dGqvwq05dc3o5tVyTGP2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": ">=22.18" + } + }, + "node_modules/eslint-plugin-import-structure": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-structure/-/eslint-plugin-import-structure-0.10.2.tgz", + "integrity": "sha512-jvx4sccxEdURGXrDtBJZtCHiKqELkHRVlLrMU5bVZIjglN0U464Joopcfn6JvmNB2zyIh2FkolaJQJaKkvy6Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": ">=22.18" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", diff --git a/package.json b/package.json index dd805a7..962022b 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,19 @@ "description": "A Gitea composite action that runs a coding agent for approved issue and pull request automation.", "type": "module", "imports": { + "#agents": "./src/agents/index.ts", + "#processes": "./src/processes/index.ts", + "#runtime": "./src/runtime/index.ts", + "#profiles": "./src/profiles/index.ts", + "#common": "./src/common/index.ts", "#testing": "./src/testing/index.ts" }, "devDependencies": { "@eslint/core": "^0.17.0", "@types/node": "^24.19.2", "@typescript-eslint/types": "^8.71.2", + "eslint-plugin-explicit-types": "^0.6.1", + "eslint-plugin-import-structure": "^0.10.2", "vite-plus": "^1.2.0", "yaml": "^2.9.1" } diff --git a/scripts/protect-source.sh b/scripts/protect-source.sh index 5208c2d..69051d3 100755 --- a/scripts/protect-source.sh +++ b/scripts/protect-source.sh @@ -2,9 +2,9 @@ set -euo pipefail path=$(mktemp -d "${RUNNER_TEMP}/bot-agents-source-XXXXXX") -mkdir "${path}/src" -cp -a "${ACTION_PATH}"/src/*.ts "${path}/src/" -# package.json makes Node load the TypeScript sources as ES modules. +cp -a "${ACTION_PATH}/src" "${path}/" +# package.json makes Node load the TypeScript sources as ES modules and +# resolves their `#` aliases. cp -a "${ACTION_PATH}/package.json" "${ACTION_PATH}/profiles.json" "${ACTION_PATH}/prompt.md" "${path}/" chmod -R go-w "${path}" printf 'path=%s\n' "${path}" >> "${GITHUB_OUTPUT}" diff --git a/src/auth.test.ts b/src/agents/auth.test.ts similarity index 98% rename from src/auth.test.ts rename to src/agents/auth.test.ts index febdb82..3ea92d7 100644 --- a/src/auth.test.ts +++ b/src/agents/auth.test.ts @@ -1,5 +1,7 @@ -import { expect, test } from "vite-plus/test"; import { mkdir, readFile, rm, stat, symlink, writeFile } from "node:fs/promises"; +import { expect, test } from "vite-plus/test"; + +import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "#testing"; import { codexAuthentication, @@ -9,7 +11,6 @@ import { retryInvalidToken, stageCodexAuth, } from "./auth.ts"; -import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "#testing"; test("retries once after an invalid refresh token", async () => { let attempts = 0; @@ -104,7 +105,7 @@ test("re-logs in after the job 9837 revoked-session stderr", async () => { test("recognizes a rejected Claude OAuth token", () => { expect( isClaudeAuthFailure({ - api_error_status: 401, + apiErrorStatus: 401, result: "Failed to authenticate. API Error: 401 OAuth access token is invalid.", }), ).toBe(true); @@ -113,7 +114,7 @@ test("recognizes a rejected Claude OAuth token", () => { result: "OAuth token has expired. Please obtain a new token or refresh your existing token.", }), ).toBe(true); - expect(isClaudeAuthFailure({ api_error_status: 429, result: "API Error: 429" })).toBe(false); + expect(isClaudeAuthFailure({ apiErrorStatus: 429, result: "API Error: 429" })).toBe(false); }); test("selects API-key login only for a configured Codex token", () => { diff --git a/src/auth.ts b/src/agents/auth.ts similarity index 88% rename from src/auth.ts rename to src/agents/auth.ts index d50837d..9d6a2f7 100644 --- a/src/auth.ts +++ b/src/agents/auth.ts @@ -12,10 +12,18 @@ import { } from "node:fs/promises"; import { dirname, join } from "node:path"; -import { isNotFound } from "./errors.ts"; -import { isObject } from "./json.ts"; +import { isNotFound, isObject } from "#common"; -export type CodexAuthentication = { kind: "api-key"; token: string } | { kind: "persistent" }; +interface CodexApiKey { + kind: "api-key"; + token: string; +} + +interface CodexPersistent { + kind: "persistent"; +} + +export type CodexAuthentication = CodexApiKey | CodexPersistent; export function codexAuthentication(token?: string): CodexAuthentication { return token === undefined ? { kind: "persistent" } : { kind: "api-key", token }; @@ -34,21 +42,27 @@ export function isCodexAuthFailure(error: string): boolean { // stream-json result, e.g. "Failed to authenticate. API Error: 401 OAuth // access token is invalid." with api_error_status 401. // The fields of Claude Code's stream-json `result` event that tell a sign-in -// failure apart, under their own names. -export type ClaudeFailureFields = Partial> & { +// failure apart, as `apiErrorStatus` and `result`. +export interface ClaudeFailureFields { + apiErrorStatus?: number; result?: string; -}; +} export function isClaudeAuthFailure(result: ClaudeFailureFields): boolean { return ( - result.api_error_status === 401 || + result.apiErrorStatus === 401 || /failed to authenticate|oauth (?:access )?token (?:is invalid|has expired)|invalid api key|authentication_error|please run \/login|not logged in/iu.test( result.result ?? "", ) ); } -export async function retryInvalidToken( +export interface CommandResult { + code: number; + error: string; +} + +export async function retryInvalidToken( attempt: () => Promise, relogin: () => Promise, ): Promise { diff --git a/src/agents/index.ts b/src/agents/index.ts new file mode 100644 index 0000000..4a37ace --- /dev/null +++ b/src/agents/index.ts @@ -0,0 +1,17 @@ +// Agent command-line specifics: Codex and Claude Code sign-in handling and +// OpenCode's arguments and output. +export { + codexAuthentication, + isClaudeAuthFailure, + isCodexAuthFailure, + persistCodexAuth, + retryInvalidToken, + stageCodexAuth, +} from "./auth.ts"; +export type { ClaudeFailureFields, CodexAuthentication, CommandResult } from "./auth.ts"; +export { + collectOpenCodeOutput, + openCodeArgs, + openCodeError, + parseOpenCodeEvents, +} from "./opencode.ts"; diff --git a/src/opencode.test.ts b/src/agents/opencode.test.ts similarity index 97% rename from src/opencode.test.ts rename to src/agents/opencode.test.ts index 1b2a0cc..70781e7 100644 --- a/src/opencode.test.ts +++ b/src/agents/opencode.test.ts @@ -1,11 +1,13 @@ import { assert, expect, test } from "vite-plus/test"; + +import { AgentAuthError } from "#profiles"; + import { collectOpenCodeOutput, openCodeArgs, openCodeError, parseOpenCodeEvents, } from "./opencode.ts"; -import { AgentAuthError } from "./profiles.ts"; test("builds a non-interactive auto-approved OpenCode command", () => { expect(openCodeArgs("openrouter/qwen/qwen3.8-27b", "Review this")).toStrictEqual([ @@ -99,5 +101,5 @@ test("surfaces an OpenCode error event instead of empty output", () => { expect(() => parseOpenCodeEvents(openRouterError(429, "Rate limit exceeded"))).toThrow( "API error 429", ); - expect(openCodeError('{"type":"text","part":{"text":"ok"}}')).toBe(undefined); + expect(openCodeError('{"type":"text","part":{"text":"ok"}}')).toBeNull(); }); diff --git a/src/opencode.ts b/src/agents/opencode.ts similarity index 84% rename from src/opencode.ts rename to src/agents/opencode.ts index cb911ae..56778ab 100644 --- a/src/opencode.ts +++ b/src/agents/opencode.ts @@ -1,5 +1,5 @@ -import { isObject } from "./json.ts"; -import { AgentAuthError } from "./profiles.ts"; +import { isObject } from "#common"; +import { AgentAuthError } from "#profiles"; export function openCodeArgs(model: string, prompt: string): string[] { return ["run", "--format", "json", "--auto", "--model", model, prompt]; @@ -7,13 +7,21 @@ export function openCodeArgs(model: string, prompt: string): string[] { type ChunkWriter = (chunk: Uint8Array) => void | Promise; +interface OpenCodeOutput { + stdout: string; + stderr: string; +} + export async function collectOpenCodeOutput( stdout: AsyncIterable, stderr: AsyncIterable, writeStdout: ChunkWriter, writeStderr: ChunkWriter, -): Promise<{ stdout: string; stderr: string }> { - const collect = async (stream: AsyncIterable, write: ChunkWriter) => { +): Promise { + const collect = async ( + stream: AsyncIterable, + write: ChunkWriter, + ): Promise => { const chunks: Uint8Array[] = []; for await (const chunk of stream) { chunks.push(chunk); @@ -38,8 +46,8 @@ export async function collectOpenCodeOutput( // OpenCode reports provider failures, including a rejected OpenRouter key // (401 "User not found."), as a JSON error event on stdout and exits 1 with an // empty stderr. -export function openCodeError(output: string): Error | undefined { - let failure: Error | undefined; +export function openCodeError(output: string): Error | null { + let failure: Error | null = null; for (const line of output.split("\n")) { if (line.trim() === "") continue; let event: unknown; @@ -69,7 +77,7 @@ export function openCodeError(output: string): Error | undefined { } export function parseOpenCodeEvents(output: string): string { - let result: string | undefined; + let result: string | null = null; for (const line of output.split("\n")) { if (line.trim() === "") continue; const event: unknown = JSON.parse(line); @@ -79,7 +87,7 @@ export function parseOpenCodeEvents(output: string): string { const { text } = part; if (typeof text === "string" && text.trim() !== "") result = text; } - if (result === undefined) { + if (result === null) { throw openCodeError(output) ?? new Error("OpenCode produced no text output"); } return result; diff --git a/src/errors.ts b/src/common/errors.ts similarity index 100% rename from src/errors.ts rename to src/common/errors.ts diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 0000000..71a7e51 --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1,4 @@ +// Helpers every phase shares: error and JSON value checks. +export { isNotFound } from "./errors.ts"; +export { isMapping, isObject } from "./json.ts"; +export type { JsonObject } from "./json.ts"; diff --git a/src/json.ts b/src/common/json.ts similarity index 89% rename from src/json.ts rename to src/common/json.ts index 20bea5c..a75f091 100644 --- a/src/json.ts +++ b/src/common/json.ts @@ -1,5 +1,5 @@ // Type guards for values read from JSON, which arrive as `unknown`. -export type JsonObject = Record; +export type JsonObject = NodeJS.Dict; // Any non-null object, arrays included: reading a missing key yields undefined. export function isObject(value: unknown): value is JsonObject { diff --git a/src/post.test.ts b/src/post.test.ts index 4a8c914..9103307 100644 --- a/src/post.test.ts +++ b/src/post.test.ts @@ -1,13 +1,12 @@ import { chmod, mkdir, rm, stat, symlink, writeFile } from "node:fs/promises"; - import { assert, expect, test } from "vite-plus/test"; -import { isNotFound } from "./errors.ts"; -import { postRuntime } from "./post.ts"; -import type { PostContext } from "./post.ts"; -import type { RuntimeManifest, RuntimeOutcome } from "./runtime.ts"; +import { isNotFound } from "#common"; +import type { RuntimeFailure, RuntimeManifest, RuntimeOutcome } from "#runtime"; import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "#testing"; +import { type Fetcher, type PostContext, postRuntime } from "./post.ts"; + const context: PostContext = { apiUrl: "https://gitea.example/api/v1", repository: "owner/repo", @@ -23,7 +22,18 @@ interface Fixture { outcomeFile: string; } -type FixtureSource = Extract | { ok: true; body: string }; +interface FixtureSuccess { + ok: true; + body: string; +} + +type FixtureSource = RuntimeFailure | FixtureSuccess; + +// One request the fake Gitea received. +interface RecordedRequest { + url: string; + init: RequestInit; +} async function writePrivate(path: string, text: string): Promise { await writeFile(path, text, { mode: 0o600 }); @@ -32,8 +42,8 @@ async function writePrivate(path: string, text: string): Promise { async function writeArtifacts( outcome: RuntimeOutcome, - review: string | undefined, - anchors: unknown, + review?: string, + anchors?: unknown, ): Promise { if (!outcome.ok) return; if (review !== undefined) await writePrivate(outcome.reviewPath, review); @@ -107,7 +117,7 @@ function requestBody(init?: RequestInit): unknown { } // A fake Gitea that records each request body and answers with `status`. -function recordingFetch(bodies: unknown[], status = 201): typeof fetch { +function recordingFetch(bodies: unknown[], status = 201): Fetcher { return (_url, init) => { bodies.push(requestBody(init)); return Promise.resolve(new Response(null, { status })); @@ -116,7 +126,7 @@ function recordingFetch(bodies: unknown[], status = 201): typeof fetch { test("posts a successful issue result and removes runtime credentials", async () => { const result = await fixture({ ok: true, body: "done" }); - const requests: { url: unknown; init: RequestInit | undefined }[] = []; + const requests: RecordedRequest[] = []; await postRuntime(result.manifestPath, context, (url, init) => { requests.push({ url, init }); diff --git a/src/post.ts b/src/post.ts index 956bdad..d86f52d 100644 --- a/src/post.ts +++ b/src/post.ts @@ -3,12 +3,9 @@ import { lstat, readFile, rm } from "node:fs/promises"; import { join } from "node:path"; import process from "node:process"; -import { isNotFound } from "./errors.ts"; -import { isObject } from "./json.ts"; -import { failureFooter, withProfile } from "./profiles.ts"; -import type { FailureReason, ProfileFailure } from "./profiles.ts"; -import { readRuntimeManifest } from "./runtime.ts"; -import type { RuntimeManifest, RuntimeOutcome } from "./runtime.ts"; +import { isNotFound, isObject } from "#common"; +import { failureFooter, type FailureReason, type ProfileFailure, withProfile } from "#profiles"; +import { readRuntimeManifest, type RuntimeManifest, type RuntimeOutcome } from "#runtime"; export interface PostContext { apiUrl: string; @@ -26,7 +23,7 @@ interface Anchor { const ANSI = new RegExp(`${String.fromCodePoint(27)}\\[[0-9;]*[A-Za-z]`, "gu"); const stripAnsi = (text: string): string => text.replace(ANSI, ""); -const VERDICTS: Record = { +const VERDICTS: NodeJS.Dict<[event: string, mark: string]> = { Approved: ["APPROVED", "โœ…"], "Changes requested": ["REQUEST_CHANGES", "๐Ÿ›‘"], }; @@ -37,6 +34,9 @@ const isFailureReason = (value: unknown): value is FailureReason => FAILURE_REAS const failureComment = (failures: ProfileFailure[]): string => `AI bot failed.\n\n${failureFooter(failures)}`; +// The `fetch` subset the reviewer posts through, which tests replace. +export type Fetcher = (url: string, init: RequestInit) => Promise; + class GiteaRequestError extends Error {} function parseAnchors(text: string, sourcePath: string): Anchor[] { @@ -137,7 +137,7 @@ async function readAgentArtifact( export async function postRuntime( manifestPath: string, context: PostContext, - fetcher: typeof fetch = fetch, + fetcher: Fetcher = fetch, ): Promise { const manifest = await readRuntimeManifest(manifestPath); try { diff --git a/src/prepare.ts b/src/prepare.ts index 9630030..e4fb6f4 100644 --- a/src/prepare.ts +++ b/src/prepare.ts @@ -1,7 +1,7 @@ import { appendFile, rm } from "node:fs/promises"; import process from "node:process"; -import { prepareRuntime } from "./runtime.ts"; +import { prepareRuntime } from "#runtime"; const env = (name: string): string => { const value = process.env[name]; diff --git a/src/child.test.ts b/src/processes/child.test.ts similarity index 96% rename from src/child.test.ts rename to src/processes/child.test.ts index 1b576cf..98056c1 100644 --- a/src/child.test.ts +++ b/src/processes/child.test.ts @@ -1,8 +1,8 @@ import { spawn } from "node:child_process"; - import { expect, test } from "vite-plus/test"; -import { isNotFound } from "./errors.ts"; +import { isNotFound } from "#common"; + import { exitCode } from "./child.ts"; test("reports the exit code", async () => { diff --git a/src/child.ts b/src/processes/child.ts similarity index 100% rename from src/child.ts rename to src/processes/child.ts diff --git a/src/processes/index.ts b/src/processes/index.ts new file mode 100644 index 0000000..bec7169 --- /dev/null +++ b/src/processes/index.ts @@ -0,0 +1,4 @@ +// Child processes: waiting on and echoing them, and the isolated profile +// user's processes and files. +export { echo, exitCode } from "./child.ts"; +export { killProfileProcesses, readProfileFile } from "./isolation.ts"; diff --git a/src/isolation.test.ts b/src/processes/isolation.test.ts similarity index 94% rename from src/isolation.test.ts rename to src/processes/isolation.test.ts index f3d54e8..3493a55 100644 --- a/src/isolation.test.ts +++ b/src/processes/isolation.test.ts @@ -1,14 +1,14 @@ -import { spawnSync } from "node:child_process"; -import type { SpawnSyncReturns } from "node:child_process"; -import { expect, test } from "vite-plus/test"; +import { spawnSync, type SpawnSyncReturns } from "node:child_process"; import { chmod, chown, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; import process from "node:process"; +import { expect, test } from "vite-plus/test"; -import { isNotFound } from "./errors.ts"; -import { isRunnableProfileProcess, killProfileProcesses, readProfileFile } from "./isolation.ts"; -import type { RuntimeProfile } from "./runtime.ts"; +import { isNotFound } from "#common"; +import type { RuntimeProfile } from "#runtime"; import { currentUid, IS_ROOT, IS_WINDOWS, makeTempDir } from "#testing"; +import { isRunnableProfileProcess, killProfileProcesses, readProfileFile } from "./isolation.ts"; + const profile = (uid: number, home: string): RuntimeProfile => ({ index: uid - 20_000, type: "claude", @@ -25,7 +25,7 @@ function runAs( selected: RuntimeProfile, command: string, args: string[], - env?: Record, + env?: NodeJS.ProcessEnv, ): SpawnSyncReturns { return spawnSync(command, args, { cwd: "/", diff --git a/src/isolation.ts b/src/processes/isolation.ts similarity index 95% rename from src/isolation.ts rename to src/processes/isolation.ts index c096155..8cac73c 100644 --- a/src/isolation.ts +++ b/src/processes/isolation.ts @@ -2,8 +2,8 @@ import { lstat, readdir, readFile } from "node:fs/promises"; import process from "node:process"; import { setTimeout as sleep } from "node:timers/promises"; -import { isNotFound } from "./errors.ts"; -import type { RuntimeProfile } from "./runtime.ts"; +import { isNotFound } from "#common"; +import type { RuntimeProfile } from "#runtime"; export function isRunnableProfileProcess(status: string, uid: number): boolean { const owner = /^Uid:\s+(\d+)/mu.exec(status)?.[1]; diff --git a/src/profiles/index.ts b/src/profiles/index.ts new file mode 100644 index 0000000..69ad6a9 --- /dev/null +++ b/src/profiles/index.ts @@ -0,0 +1,12 @@ +// Bot profiles: their footers, failure reports, ordered runs, and the +// environment each agent gets. +export { + AgentAuthError, + agentEnvironment, + failureFooter, + profileFooter, + runProfiles, + sanitizedEnvironment, + withProfile, +} from "./profiles.ts"; +export type { BotProfile, FailureReason, ProfileFailure, ProfileIdentity } from "./profiles.ts"; diff --git a/src/profiles.test.ts b/src/profiles/profiles.test.ts similarity index 99% rename from src/profiles.test.ts rename to src/profiles/profiles.test.ts index 784b077..067c48a 100644 --- a/src/profiles.test.ts +++ b/src/profiles/profiles.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "vite-plus/test"; + import { AgentAuthError, agentEnvironment, diff --git a/src/profiles.ts b/src/profiles/profiles.ts similarity index 79% rename from src/profiles.ts rename to src/profiles/profiles.ts index 8f38d54..80613bb 100644 --- a/src/profiles.ts +++ b/src/profiles/profiles.ts @@ -1,22 +1,32 @@ -export interface BotProfile { +// The agent and model a profile runs, which every report names. +export interface ProfileIdentity { type: "claude" | "codex" | "opencode"; model: string; +} + +export interface BotProfile extends ProfileIdentity { token?: string; } -export function profileFooter(profile: Pick): string { +export function profileFooter(profile: ProfileIdentity): string { return `> Profile: \`${profile.type}\` ยท \`${profile.model}\``; } -export function withProfile(body: string, profile: Pick): string { +export function withProfile(body: string, profile: ProfileIdentity): string { return `${body}\n\n${profileFooter(profile)}`; } export type FailureReason = "sign-in failed" | "usage exhausted" | "failed"; -export type ProfileFailure = Pick & { +export interface ProfileFailure extends ProfileIdentity { reason: FailureReason; -}; +} + +// Where a profile's isolated user keeps its home. +interface ProfileHome { + type: ProfileIdentity["type"]; + home: string; +} // Raised once an agent's credentials are rejected and any re-login is spent. export class AgentAuthError extends Error {} @@ -41,7 +51,7 @@ export function failureFooter(failures: ProfileFailure[]): string { // Tries the profiles from `index` on, moving on only after a sign-in failure // or exhausted usage, and records every profile it tried. -async function attemptFrom>( +async function attemptFrom( profiles: P[], attempt: (profile: P) => Promise, failures: ProfileFailure[], @@ -62,7 +72,7 @@ async function attemptFrom>( } } -export function runProfiles>( +export function runProfiles( profiles: P[], attempt: (profile: P) => Promise, failures: ProfileFailure[] = [], @@ -70,8 +80,8 @@ export function runProfiles>( return attemptFrom(profiles, attempt, failures, 0); } -export function sanitizedEnvironment(environment: Record): Record { - const result: Record = {}; +export function sanitizedEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const result: NodeJS.ProcessEnv = {}; for (const name of [ "PATH", "HOME", @@ -105,10 +115,10 @@ export function sanitizedEnvironment(environment: Record): Recor } export function agentEnvironment( - environment: Record, - profile: Pick & { home: string }, + environment: NodeJS.ProcessEnv, + profile: ProfileHome, token?: string, -): Record { +): NodeJS.ProcessEnv { const result = sanitizedEnvironment(environment); result["HOME"] = profile.home; if (profile.type === "codex") result["CODEX_HOME"] = `${profile.home}/.codex`; diff --git a/src/run.ts b/src/run.ts index 69a410b..3074bec 100644 --- a/src/run.ts +++ b/src/run.ts @@ -10,46 +10,51 @@ import { finished } from "node:stream/promises"; import { setTimeout as sleep } from "node:timers/promises"; import { + type ClaudeFailureFields, + type CodexAuthentication, codexAuthentication, + collectOpenCodeOutput, + type CommandResult, isClaudeAuthFailure, isCodexAuthFailure, - persistCodexAuth, - retryInvalidToken, - stageCodexAuth, -} from "./auth.ts"; -import type { ClaudeFailureFields, CodexAuthentication } from "./auth.ts"; -import { echo, exitCode } from "./child.ts"; -import { killProfileProcesses, readProfileFile } from "./isolation.ts"; -import { isObject } from "./json.ts"; -import { - collectOpenCodeOutput, openCodeArgs, openCodeError, parseOpenCodeEvents, -} from "./opencode.ts"; + persistCodexAuth, + retryInvalidToken, + stageCodexAuth, +} from "#agents"; +import { isObject } from "#common"; +import { echo, exitCode, killProfileProcesses, readProfileFile } from "#processes"; import { AgentAuthError, agentEnvironment, + type ProfileFailure, + type ProfileIdentity, runProfiles, sanitizedEnvironment, withProfile, -} from "./profiles.ts"; -import type { ProfileFailure } from "./profiles.ts"; -import { readRuntimeManifest } from "./runtime.ts"; -import type { RuntimeOutcome, RuntimeProfile } from "./runtime.ts"; +} from "#profiles"; +import { readRuntimeManifest, type RuntimeOutcome, type RuntimeProfile } from "#runtime"; interface AttemptResult { body: string; reviewPath: string; anchorsPath: string; - profile: Pick; + profile: ProfileIdentity; } -// Claude Code's stream-json `result` event, in its own field names. -type ClaudeResult = ClaudeFailureFields & - Partial> & { - subtype: string; - }; +// Claude Code's stream-json `result` event, with camelCase field names. +interface ClaudeResult extends ClaudeFailureFields { + isError?: boolean; + subtype: string; +} + +interface RenderedPrompt { + prompt: string; + reviewPath: string; + anchorsPath: string; +} const env = (name: string): string => { const value = process.env[name]; @@ -57,8 +62,8 @@ const env = (name: string): string => { return value; }; -function definedEnvironment(): Record { - const result: Record = {}; +function definedEnvironment(): NodeJS.ProcessEnv { + const result: NodeJS.ProcessEnv = {}; for (const [name, value] of Object.entries(process.env)) { if (value !== undefined) { result[name] = value; @@ -112,8 +117,8 @@ async function configureGitAuthor(): Promise { await runRoot("git", ["config", "user.email", user["email"]]); } -async function readProfileToken(profile: RuntimeProfile): Promise { - const token = profile.tokenFile === null ? undefined : await readFile(profile.tokenFile, "utf8"); +async function readProfileToken(profile: RuntimeProfile): Promise { + const token = profile.tokenFile === null ? null : await readFile(profile.tokenFile, "utf8"); return token; } @@ -159,9 +164,7 @@ function formatComment(comment: unknown): string { return `## ${String(login)} at ${String(createdAt)}\n\n${String(body)}\n`; } -async function renderPrompt( - profile: RuntimeProfile, -): Promise<{ prompt: string; reviewPath: string; anchorsPath: string }> { +async function renderPrompt(profile: RuntimeProfile): Promise { const reviewPath = await makeProfileFile(profile, "review.md"); const anchorsPath = await makeProfileFile(profile, "anchors.json"); const comments: unknown = await ( @@ -170,7 +173,7 @@ async function renderPrompt( if (!Array.isArray(comments)) { throw new TypeError("comments: unexpected Gitea response"); } - const values: Record = { + const values: NodeJS.ProcessEnv = { COMMENT: env("COMMENT"), ISSUE_COMMENTS: comments.map((comment: unknown) => formatComment(comment)).join("\n"), CODE_RULES: await (await gitea(RULES_PATH)).text(), @@ -217,7 +220,7 @@ async function runClaude(profile: RuntimeProfile, token: string, prompt: string) }, ); const status = exitCode(claude); - let result: ClaudeResult | undefined; + let result: ClaudeResult | null = null; let error = ""; const copyError = (async (): Promise => { claude.stderr.setEncoding("utf8"); @@ -243,18 +246,17 @@ async function runClaude(profile: RuntimeProfile, token: string, prompt: string) if (event["type"] === "result") { const { is_error: isError, result: answer, subtype, api_error_status: apiStatus } = event; result = { - is_error: typeof isError === "boolean" ? isError : undefined, + isError: typeof isError === "boolean" ? isError : undefined, result: typeof answer === "string" ? answer : undefined, subtype: String(subtype), - api_error_status: - typeof apiStatus === "number" || apiStatus === null ? apiStatus : undefined, + apiErrorStatus: typeof apiStatus === "number" ? apiStatus : undefined, }; } } } } const [code] = await Promise.all([status, copyError]); - if (result?.is_error === true) { + if (result?.isError === true) { const failure = new Error(result.result ?? `claude ended with ${result.subtype}`); if (isClaudeAuthFailure(result)) { throw new AgentAuthError("claude sign-in failed", { cause: failure }); @@ -287,7 +289,7 @@ async function pollForInstructions( async function codexDeviceLogin( profile: RuntimeProfile, - environment: Record, + environment: NodeJS.ProcessEnv, ): Promise { const login = spawn("codex", ["login", "--device-auth"], { cwd: WORKSPACE, @@ -316,7 +318,7 @@ async function codexDeviceLogin( async function codexApiLogin( profile: RuntimeProfile, - environment: Record, + environment: NodeJS.ProcessEnv, token: string, ): Promise { const login = spawn("codex", ["login", "--with-api-key"], { @@ -363,7 +365,7 @@ async function runCodex( if ((await exitCode(check)) !== 0) await codexDeviceLogin(profile, environment); } const outputPath = await makeProfileFile(profile, "result.md"); - const attempt = async (): Promise<{ code: number; error: string }> => { + const attempt = async (): Promise => { const codex = spawn( "codex", [ @@ -440,12 +442,12 @@ async function runOpenCode( async function runProfile(profile: RuntimeProfile): Promise { const token = await readProfileToken(profile); - const authentication = profile.type === "codex" ? codexAuthentication(token) : null; + const authentication = profile.type === "codex" ? codexAuthentication(token ?? undefined) : null; let stagedCodexAuth: string | null = null; - let body: string | undefined; + let body: string | null = null; let resultPath: string | null = null; - let reviewPath: string | undefined; - let anchorsPath: string | undefined; + let reviewPath: string | null = null; + let anchorsPath: string | null = null; try { if (authentication?.kind === "persistent") { stagedCodexAuth = await stageCodexAuth(CODEX_AUTH, profile.home, profile.uid, profile.gid); @@ -460,7 +462,7 @@ async function runProfile(profile: RuntimeProfile): Promise { resultPath = await runCodex( profile, rendered.prompt, - authentication ?? codexAuthentication(token), + authentication ?? codexAuthentication(token ?? undefined), ); } else { body = await runOpenCode(profile, token ?? "", rendered.prompt); @@ -472,7 +474,7 @@ async function runProfile(profile: RuntimeProfile): Promise { } } if (resultPath !== null) body = await readProfileFile(profile, resultPath); - if (body === undefined || reviewPath === undefined || anchorsPath === undefined) { + if (body === null || reviewPath === null || anchorsPath === null) { throw new Error("agent attempt produced no result"); } return { diff --git a/src/credentials.test.ts b/src/runtime/credentials.test.ts similarity index 99% rename from src/credentials.test.ts rename to src/runtime/credentials.test.ts index 2b2a8be..9f582c3 100644 --- a/src/credentials.test.ts +++ b/src/runtime/credentials.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "vite-plus/test"; + import { decryptCredential, encryptCredential, parseCredentials } from "./credentials.ts"; const MASTER_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8"; diff --git a/src/credentials.ts b/src/runtime/credentials.ts similarity index 95% rename from src/credentials.ts rename to src/runtime/credentials.ts index 980ec19..09a96d1 100644 --- a/src/credentials.ts +++ b/src/runtime/credentials.ts @@ -1,9 +1,15 @@ import { webcrypto } from "node:crypto"; -import { isMapping } from "./json.ts"; -import type { BotProfile } from "./profiles.ts"; +import { isMapping } from "#common"; +import type { BotProfile } from "#profiles"; -const DEFAULT_MODELS: Record = { +interface DefaultModels { + claude: string; + codex: string; + opencode: string; +} + +const DEFAULT_MODELS: DefaultModels = { claude: "claude-sonnet-5", codex: "gpt-5.6-terra", opencode: "openrouter/qwen/qwen3.8-27b", diff --git a/src/runtime/index.ts b/src/runtime/index.ts new file mode 100644 index 0000000..34dfda1 --- /dev/null +++ b/src/runtime/index.ts @@ -0,0 +1,10 @@ +// The runtime the prepare phase writes for the later phases: decrypted +// credentials laid out per profile, described by a manifest. +export { prepareRuntime, readRuntimeManifest } from "./runtime.ts"; +export type { + RuntimeFailure, + RuntimeManifest, + RuntimeOutcome, + RuntimeProfile, + RuntimeSuccess, +} from "./runtime.ts"; diff --git a/src/runtime.test.ts b/src/runtime/runtime.test.ts similarity index 95% rename from src/runtime.test.ts rename to src/runtime/runtime.test.ts index 364648a..4f282e1 100644 --- a/src/runtime.test.ts +++ b/src/runtime/runtime.test.ts @@ -2,13 +2,12 @@ import { spawnSync } from "node:child_process"; import { chmod, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import process from "node:process"; import { fileURLToPath } from "node:url"; - import { assert, expect, test } from "vite-plus/test"; -import { prepareRuntime } from "./runtime.ts"; -import type { RuntimeProfile } from "./runtime.ts"; import { IS_ROOT, makeTempDir } from "#testing"; +import { prepareRuntime, type RuntimeProfile } from "./runtime.ts"; + const MASTER_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8"; const ENCRYPTED = "v1:ICEiIyQlJicoKSor:oV_FAgnsN3p1FyegJxwGVdyjfyPio6qDeSUwbA"; @@ -82,7 +81,7 @@ test.skipIf(!IS_ROOT)("removes prepared credentials when output publication fail ); const result = spawnSync( process.execPath, - [fileURLToPath(new URL("prepare.ts", import.meta.url))], + [fileURLToPath(new URL("../prepare.ts", import.meta.url))], { env: { ACTION_PATH: action, diff --git a/src/runtime.ts b/src/runtime/runtime.ts similarity index 87% rename from src/runtime.ts rename to src/runtime/runtime.ts index 342cca2..edafa40 100644 --- a/src/runtime.ts +++ b/src/runtime/runtime.ts @@ -1,19 +1,20 @@ import { chmod, chown, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; +import { isMapping } from "#common"; +import type { ProfileFailure, ProfileIdentity } from "#profiles"; + import { parseCredentials } from "./credentials.ts"; -import { isMapping } from "./json.ts"; -import type { BotProfile, ProfileFailure } from "./profiles.ts"; const FIRST_PROFILE_UID = 20_000; -export type RuntimeProfile = Pick & { +export interface RuntimeProfile extends ProfileIdentity { index: number; uid: number; gid: number; home: string; tokenFile: string | null; -}; +} export interface RuntimeManifest { runtimeDir: string; @@ -23,15 +24,20 @@ export interface RuntimeManifest { profiles: RuntimeProfile[]; } -export type RuntimeOutcome = - | { - ok: true; - body: string; - reviewPath: string; - anchorsPath: string; - profile: Pick; - } - | { ok: false; failures: ProfileFailure[] }; +export interface RuntimeSuccess { + ok: true; + body: string; + reviewPath: string; + anchorsPath: string; + profile: ProfileIdentity; +} + +export interface RuntimeFailure { + ok: false; + failures: ProfileFailure[]; +} + +export type RuntimeOutcome = RuntimeSuccess | RuntimeFailure; async function writePrivate(path: string, value: string, uid: number, gid: number): Promise { await writeFile(path, value, { mode: 0o600 }); diff --git a/src/testing/index.ts b/src/testing/index.ts index 1578539..2558862 100644 --- a/src/testing/index.ts +++ b/src/testing/index.ts @@ -38,7 +38,7 @@ export interface CommandResult { export async function runCommand( command: string, args: string[], - env: Record, + env: NodeJS.ProcessEnv, ): Promise { const child = spawn(command, args, { env, stdio: ["ignore", "ignore", "pipe"] }); const [stderr, closed] = await Promise.all([text(child.stderr), once(child, "close")]); diff --git a/tests/entrypoints.test.ts b/tests/entrypoints.test.ts index 06ac374..9f7e3a1 100644 --- a/tests/entrypoints.test.ts +++ b/tests/entrypoints.test.ts @@ -1,6 +1,5 @@ import process from "node:process"; import { fileURLToPath } from "node:url"; - import { expect, test } from "vite-plus/test"; import { runCommand } from "#testing"; diff --git a/tests/workflow.test.ts b/tests/workflow.test.ts index da4625d..363f036 100644 --- a/tests/workflow.test.ts +++ b/tests/workflow.test.ts @@ -1,12 +1,11 @@ import { readdir, readFile, rm, stat } from "node:fs/promises"; import process from "node:process"; - import { assert, expect, test } from "vite-plus/test"; import { parse } from "yaml"; import { makeTempDir, runCommand } from "#testing"; -type JsonObject = Record; +type JsonObject = NodeJS.Dict; const isMapping = (value: unknown): value is JsonObject => typeof value === "object" && value !== null && !Array.isArray(value); @@ -90,6 +89,8 @@ test("the source protection script stages immutable action files", async () => { "src/prepare.ts", "src/run.ts", "src/post.ts", + "src/runtime/index.ts", + "src/agents/auth.ts", "package.json", "profiles.json", "prompt.md", diff --git a/vite.config.ts b/vite.config.ts index 1c48b7c..51c87fc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,5 @@ +import explicitTypes from "eslint-plugin-explicit-types"; +import importStructure from "eslint-plugin-import-structure"; import { defineConfig } from "vite-plus"; import projectLint from "./scripts/project-lint.ts"; @@ -5,9 +7,18 @@ import projectLint from "./scripts/project-lint.ts"; export default defineConfig({ lint: { plugins: ["typescript", "unicorn", "oxc", "import"], - jsPlugins: ["./scripts/project-lint.ts"], + jsPlugins: [ + "eslint-plugin-explicit-types", + "eslint-plugin-import-structure", + "./scripts/project-lint.ts", + ], categories: { correctness: "error", suspicious: "error", pedantic: "error", perf: "error" }, - rules: projectLint.configs.project.rules, + rules: { + ...explicitTypes.configs.concrete.rules, + ...explicitTypes.configs.absence.rules, + ...importStructure.configs.modules.rules, + ...projectLint.configs.project.rules, + }, options: { typeAware: true, typeCheck: true,