From 5e3be5271028a8a30827cdb513c05011cfda4e2a Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 10 Oct 2026 09:08:30 +0100 Subject: [PATCH 1/3] style: add padding lines, block comment style and sorted JSON keys Applies the stylistic autofixes of the newer shared lint config, namely a blank line before return, block form for multi-line comments and alphabetical keys in package.json, tsconfig.json and turbo.json. No behaviour changes. --- package.json | 120 +++++++++--------- scripts/build-sea.ts | 9 +- src/adapters/node/alias-worker.ts | 4 + src/adapters/node/cached-command.ts | 4 + .../forked-alias-process.integration.test.ts | 1 + src/adapters/node/forked-alias-process.ts | 9 ++ .../node/forked-alias-process.unit.test.ts | 7 + src/adapters/node/fs-key-store.ts | 1 + src/adapters/node/fs-registry-store.ts | 2 + src/adapters/node/paths.ts | 10 +- src/adapters/node/ps-proc-info.ts | 1 + src/adapters/node/uds-transport.ts | 5 + src/adapters/node/win-proc-info.ts | 1 + src/alias-pool.ts | 4 + src/alias-pool.unit.test.ts | 13 ++ src/api/schemas.ts | 1 + src/api/server.ts | 12 ++ src/bin/main.ts | 3 + src/cc-peer.ts | 12 ++ src/domain/dedup.ts | 1 + src/domain/envelope.ts | 8 +- src/domain/file-transfer.ts | 3 + src/domain/hop-chain.ts | 2 + src/domain/roster.ts | 2 + src/sea-entry.ts | 12 +- src/test/socket-path.ts | 2 + stryker.config.ts | 12 +- test/api.integration.test.ts | 4 +- test/cli.integration.test.ts | 1 + test/sea.e2e.test.ts | 1 + tsconfig.json | 26 ++-- turbo.json | 43 ++----- vitest.config.ts | 4 +- vitest.e2e.config.ts | 4 +- 34 files changed, 216 insertions(+), 128 deletions(-) diff --git a/package.json b/package.json index e890bab..3f4555e 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,54 @@ { "name": "cc-peer", - "version": "1.6.5", "description": "Communicate with local Claude Code instances over their native cross-session peer messaging: send and receive messages, register as a named discoverable peer, receipts, idle subscriptions, and a REST facade via `npx cc-peer`.", - "type": "module", - "license": "MIT", + "version": "1.6.5", "author": "Joseph Mearman ", - "repository": { - "type": "git", - "url": "git+https://github.com/ExaDev/cc-peer.git" + "bin": { + "cc-peer": "./dist/bin/cc-peer.mjs" }, - "homepage": "https://github.com/ExaDev/cc-peer#readme", "bugs": { "url": "https://github.com/ExaDev/cc-peer/issues" }, - "sideEffects": false, + "dependencies": { + "zod": "4.5.4" + }, + "devDependencies": { + "@anthropic-ai/claude-code": "2.1.261", + "@arethetypeswrong/cli": "0.18.5", + "@commitlint/config-conventional": "21.2.2", + "@commitlint/types": "21.2.0", + "@exadev/eslint-config": "2.12.1", + "@semantic-release/changelog": "7.0.0", + "@semantic-release/commit-analyzer": "13.0.1", + "@semantic-release/exec": "7.1.0", + "@semantic-release/git": "11.0.1", + "@semantic-release/github": "12.0.9", + "@semantic-release/npm": "13.1.5", + "@semantic-release/release-notes-generator": "14.1.1", + "@stryker-mutator/core": "10.0.0", + "@stryker-mutator/vitest-runner": "10.0.0", + "@types/node": "26.4.1", + "@vitest/coverage-v8": "4.1.11", + "commitlint": "21.2.2", + "eslint": "10.10.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.6", + "globals": "17.12.0", + "husky": "9.1.7", + "jiti": "2.7.0", + "lint-staged": "17.5.0", + "prettier": "3.9.6", + "publint": "0.3.24", + "semantic-release": "25.0.9", + "tsdown": "0.23.0", + "tsx": "4.23.13", + "turbo": "2.11.5", + "typescript": "6.0.3", + "vitest": "4.1.11" + }, "engines": { "node": ">=20" }, - "packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c", - "bin": { - "cc-peer": "./dist/bin/cc-peer.mjs" - }, "exports": { ".": { "import": { @@ -45,69 +73,41 @@ "./schemas/*.schema.json": "./schemas/*.schema.json", "./package.json": "./package.json" }, - "main": "./dist/cc-peer.cjs", - "types": "./dist/cc-peer.d.mts", "files": [ "dist", "schemas", "docs" ], + "homepage": "https://github.com/ExaDev/cc-peer#readme", + "license": "MIT", + "main": "./dist/cc-peer.cjs", + "packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c", "publishConfig": { "provenance": true, "access": "public" }, + "repository": { + "type": "git", + "url": "git+https://github.com/ExaDev/cc-peer.git" + }, "scripts": { - "build": "turbo run _build", "_build": "tsdown && tsx scripts/generate-json-schema.ts", - "lint": "turbo run _lint", "_lint": "eslint . --cache --fix", - "typecheck": "turbo run _typecheck", "_typecheck": "tsc --noEmit", + "build": "turbo run _build", + "lint": "turbo run _lint", + "prepare": "husky", + "prepublishOnly": "pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm build && publint && attw --pack --profile node16", "test": "vitest run", - "test:watch": "vitest", - "test:unit": "vitest run --project unit", - "test:integration": "vitest run --project integration", + "test:coverage": "vitest run --coverage", "test:e2e": "vitest run --config vitest.e2e.config.ts", + "test:integration": "vitest run --project integration", "test:mutation": "stryker run stryker.config.ts", - "prepublishOnly": "pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm build && publint && attw --pack --profile node16", - "prepare": "husky", - "test:coverage": "vitest run --coverage" - }, - "dependencies": { - "zod": "4.5.4" + "test:unit": "vitest run --project unit", + "test:watch": "vitest", + "typecheck": "turbo run _typecheck" }, - "devDependencies": { - "@anthropic-ai/claude-code": "2.1.261", - "@arethetypeswrong/cli": "0.18.5", - "@commitlint/config-conventional": "21.2.2", - "@commitlint/types": "21.2.0", - "@exadev/eslint-config": "2.12.1", - "@semantic-release/changelog": "7.0.0", - "@semantic-release/commit-analyzer": "13.0.1", - "@semantic-release/exec": "7.1.0", - "@semantic-release/git": "11.0.1", - "@semantic-release/github": "12.0.9", - "@semantic-release/npm": "13.1.5", - "@semantic-release/release-notes-generator": "14.1.1", - "@stryker-mutator/core": "10.0.0", - "@stryker-mutator/vitest-runner": "10.0.0", - "@types/node": "26.4.1", - "@vitest/coverage-v8": "4.1.11", - "commitlint": "21.2.2", - "eslint": "10.10.0", - "eslint-config-prettier": "10.1.8", - "eslint-plugin-prettier": "5.5.6", - "globals": "17.12.0", - "husky": "9.1.7", - "jiti": "2.7.0", - "lint-staged": "17.5.0", - "prettier": "3.9.6", - "publint": "0.3.24", - "semantic-release": "25.0.9", - "tsdown": "0.23.0", - "tsx": "4.23.13", - "turbo": "2.11.5", - "typescript": "6.0.3", - "vitest": "4.1.11" - } + "sideEffects": false, + "type": "module", + "types": "./dist/cc-peer.d.mts" } diff --git a/scripts/build-sea.ts b/scripts/build-sea.ts index 0888550..24b4b53 100644 --- a/scripts/build-sea.ts +++ b/scripts/build-sea.ts @@ -1,6 +1,6 @@ -// Builds a Node single-executable application (SEA) binary from this repository's tsdown-bundled dist-sea/sea-entry.cjs (see tsdown.config.ts's seaEntryBuildConfig), for whichever platform this script runs on — one call per platform cell of the CI matrix, never cross-compiled: node --build-sea copies the CURRENT running node binary, so the platform this script runs under is the platform the resulting binary targets. Uses the single `node --build-sea` flag (Node 25.5.0+, https://nodejs.org/api/single-executable-applications.html), not the older two-step --experimental-sea-config-then-postject workflow that flag replaced: --build-sea generates the preparation blob and injects it into a copy of the running binary in one internal step, writing the finished executable straight to sea-config.json's own "output" path. -// -// Pattern follows ExaDev/documents.js's .github/scripts/build-sea-binary.ts, which proved each of the non-obvious steps below in real CI runs. +/* Builds a Node single-executable application (SEA) binary from this repository's tsdown-bundled dist-sea/sea-entry.cjs (see tsdown.config.ts's seaEntryBuildConfig), for whichever platform this script runs on — one call per platform cell of the CI matrix, never cross-compiled: node --build-sea copies the CURRENT running node binary, so the platform this script runs under is the platform the resulting binary targets. Uses the single `node --build-sea` flag (Node 25.5.0+, https://nodejs.org/api/single-executable-applications.html), not the older two-step --experimental-sea-config-then-postject workflow that flag replaced: --build-sea generates the preparation blob and injects it into a copy of the running binary in one internal step, writing the finished executable straight to sea-config.json's own "output" path. + + Pattern follows ExaDev/documents.js's .github/scripts/build-sea-binary.ts, which proved each of the non-obvious steps below in real CI runs. */ import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -29,6 +29,7 @@ function binaryFileName( arch: SeaArch, ): string { const suffixed = `${binaryName}-${PLATFORM_LABELS[platform]}-${arch}`; + return platform === "win32" ? `${suffixed}.exe` : suffixed; } @@ -40,6 +41,7 @@ function postBuildCommandsFor( if (platform === "darwin") { return [["codesign", ["--sign", "-", "--force", binaryPath]]]; } + return []; } @@ -57,6 +59,7 @@ function assertBundle(): string { `Expected ${BUNDLE_PATH} to exist — run this repository's own build (which produces dist-sea/) first.`, ); } + return BUNDLE_PATH; } diff --git a/src/adapters/node/alias-worker.ts b/src/adapters/node/alias-worker.ts index 0c3025a..6ba6974 100644 --- a/src/adapters/node/alias-worker.ts +++ b/src/adapters/node/alias-worker.ts @@ -35,10 +35,12 @@ async function handleCommand(raw: unknown): Promise { if (!AliasCommandSchema.is(raw)) return; if (raw.type === "stop") { await shutdown(); + return; } if (raw.type === "send") { await handleSend(raw); + return; } let created: CcPeer; @@ -51,6 +53,7 @@ async function handleCommand(raw: unknown): Promise { }); } catch { process.exit(1); + return; } peer = created; @@ -69,6 +72,7 @@ async function handleSend(command: Readonly): Promise { code: "NOT_STARTED", message: "alias peer has not started", }); + return; } try { diff --git a/src/adapters/node/cached-command.ts b/src/adapters/node/cached-command.ts index 0987642..2b635f9 100644 --- a/src/adapters/node/cached-command.ts +++ b/src/adapters/node/cached-command.ts @@ -11,6 +11,7 @@ export function errnoOf(error: unknown): string { ) { return error.code; } + return ""; } @@ -32,7 +33,9 @@ export class CachedPidCommand { number, { at: number; value: string | undefined } >(); + private static readonly CACHE_MS = 60_000; + private readonly inFlight = new Map>(); async run( @@ -70,6 +73,7 @@ export class CachedPidCommand { this.inFlight.delete(pid); }); this.inFlight.set(pid, promise); + return promise; } } diff --git a/src/adapters/node/forked-alias-process.integration.test.ts b/src/adapters/node/forked-alias-process.integration.test.ts index c4ce9d2..2a29101 100644 --- a/src/adapters/node/forked-alias-process.integration.test.ts +++ b/src/adapters/node/forked-alias-process.integration.test.ts @@ -72,6 +72,7 @@ async function readAliasKey( throw new Error(`no key file found for socket ${socketPath}`); } const raw = await readFile(join(sessionsDir, match), "utf8"); + return JSON.parse(raw) as PeerKeyFile; } diff --git a/src/adapters/node/forked-alias-process.ts b/src/adapters/node/forked-alias-process.ts index 2dc1b4a..1edeba9 100644 --- a/src/adapters/node/forked-alias-process.ts +++ b/src/adapters/node/forked-alias-process.ts @@ -35,9 +35,13 @@ export interface ForkedAliasProcessDeps { */ export class ForkedAliasProcess implements AliasProcess { readonly events = new EventEmitter(); + private readonly forkFn: typeof fork; + private readonly workerPath: string; + private readonly pendingSends = new Map(); + private child: ChildProcess | undefined; constructor(deps: Readonly) { @@ -51,6 +55,7 @@ export class ForkedAliasProcess implements AliasProcess { child.on("message", (raw: unknown) => { if (AliasMessageEventSchema.is(raw)) { this.events.emit("message", toInboundMessage(raw)); + return; } this.settleSend(raw); @@ -102,6 +107,7 @@ export class ForkedAliasProcess implements AliasProcess { throw new AliasSendError("alias worker is not running"); } const requestId = newMsgId(); + return new Promise<{ msgId: string }>((resolve, reject) => { this.pendingSends.set(requestId, { resolve, reject }); child.send({ type: "send", requestId, target, body }); @@ -112,6 +118,7 @@ export class ForkedAliasProcess implements AliasProcess { private settleSend(raw: unknown): void { if (AliasSentEventSchema.is(raw)) { this.takePendingSend(raw.requestId)?.resolve({ msgId: raw.msgId }); + return; } if (AliasSendFailedEventSchema.is(raw)) { @@ -124,6 +131,7 @@ export class ForkedAliasProcess implements AliasProcess { private takePendingSend(requestId: string): PendingSend | undefined { const pending = this.pendingSends.get(requestId); this.pendingSends.delete(requestId); + return pending; } @@ -140,6 +148,7 @@ export class ForkedAliasProcess implements AliasProcess { const child = this.child; if (child === undefined) return undefined; if (child.exitCode !== null || child.signalCode !== null) return undefined; + return child; } diff --git a/src/adapters/node/forked-alias-process.unit.test.ts b/src/adapters/node/forked-alias-process.unit.test.ts index be9a4f6..9edaf53 100644 --- a/src/adapters/node/forked-alias-process.unit.test.ts +++ b/src/adapters/node/forked-alias-process.unit.test.ts @@ -9,11 +9,14 @@ import { AliasSendCommandSchema } from "../../schemas/alias-ipc.js"; /** A fake ChildProcess: just enough of the EventEmitter + send() surface for the adapter's own protocol logic, driven manually by each test. */ class FakeChild extends EventEmitter { sent: unknown[] = []; + exitCode: number | null = null; + signalCode: NodeJS.Signals | null = null; send(message: unknown): boolean { this.sent.push(message); + return true; } @@ -30,8 +33,10 @@ function makeForkedAliasProcess(child: FakeChild): { const forkCalls: unknown[][] = []; const fork = vi.fn((modulePath: string, args: unknown, options: unknown) => { forkCalls.push([modulePath, args, options]); + return child as unknown as ChildProcess; }); + return { process: new ForkedAliasProcess({ fork: fork as never, @@ -203,6 +208,7 @@ function lastSendRequestId(child: FakeChild): string { if (!AliasSendCommandSchema.is(command)) { throw new Error("the last IPC command was not a send command"); } + return command.requestId; } @@ -215,6 +221,7 @@ async function startedProcess(): Promise<{ const pending = proc.start({ name: "alice" }); child.emit("message", { type: "started" }); await pending; + return { child, proc }; } diff --git a/src/adapters/node/fs-key-store.ts b/src/adapters/node/fs-key-store.ts index aeb6532..07b79dc 100644 --- a/src/adapters/node/fs-key-store.ts +++ b/src/adapters/node/fs-key-store.ts @@ -23,6 +23,7 @@ export class FsKeyStore implements KeyStore { return undefined; } const result = PeerKeyFileSchema.safeParse(parsed); + return result.success ? result.data : undefined; } diff --git a/src/adapters/node/fs-registry-store.ts b/src/adapters/node/fs-registry-store.ts index 1c55c3a..ab258e0 100644 --- a/src/adapters/node/fs-registry-store.ts +++ b/src/adapters/node/fs-registry-store.ts @@ -35,6 +35,7 @@ export class FsRegistryStore implements RegistryStore { const entry = await this.read(Number.parseInt(name, 10)); if (entry !== undefined) entries.push(entry); } + return entries; } @@ -52,6 +53,7 @@ export class FsRegistryStore implements RegistryStore { return undefined; } const result = RegistryEntrySchema.safeParse(parsed); + return result.success ? result.data : undefined; } diff --git a/src/adapters/node/paths.ts b/src/adapters/node/paths.ts index bc29fe1..0faff7c 100644 --- a/src/adapters/node/paths.ts +++ b/src/adapters/node/paths.ts @@ -25,9 +25,10 @@ export function socketDirCandidates( config: Readonly = {}, ): [string, ...string[]] { if (config.socketDir !== undefined) return [config.socketDir]; - // /tmp/cc-socks and /private/tmp/cc-socks (its realpath on macOS), plus the - // XDG runtime and Termux variants the reference client also accepts. + /* /tmp/cc-socks and /private/tmp/cc-socks (its realpath on macOS), plus the + XDG runtime and Termux variants the reference client also accepts. */ const runtimeDir = process.env.XDG_RUNTIME_DIR; + return [ "/tmp/cc-socks", "/private/tmp/cc-socks", @@ -64,12 +65,14 @@ export function socketPathForPid( config.socketDir === undefined ? "" : `-${pipeNamespace(config.socketDir)}`; + return `${WINDOWS_PIPE_PREFIX}cc-peer${namespace}-${pid.toString()}`; } // An explicit socketDir always wins on POSIX, matching socketDirCandidates' own precedence rule: it is a full override of automatic path construction, not merely a candidate to prefer. if (config.socketDir !== undefined) { return `${config.socketDir}/${pid.toString()}.sock`; } + return `${socketDirCandidates(config)[0]}/${pid.toString()}.sock`; } @@ -86,6 +89,7 @@ export function keyFilePath( config: Readonly = {}, ): string { const hash = createHash("sha256").update(socketPath).digest("hex"); + return join( sessionsDir(config), `${pidFromSocketPath(socketPath).toString()}.${hash}.key`, @@ -97,11 +101,13 @@ export function pidFromSocketPath(socketPath: string): number { if (socketPath.startsWith(WINDOWS_PIPE_PREFIX)) { const name = socketPath.slice(WINDOWS_PIPE_PREFIX.length); const pid = Number.parseInt(name.slice(name.lastIndexOf("-") + 1), 10); + return Number.isNaN(pid) ? 0 : pid; } // substring after the final slash: split().at(-1) would need an unreachable empty-array fallback. const base = socketPath.substring(socketPath.lastIndexOf("/") + 1); const pid = Number.parseInt(base.replace(/\.sock$/, ""), 10); + return Number.isNaN(pid) ? 0 : pid; } diff --git a/src/adapters/node/ps-proc-info.ts b/src/adapters/node/ps-proc-info.ts index 61544ff..3f3b157 100644 --- a/src/adapters/node/ps-proc-info.ts +++ b/src/adapters/node/ps-proc-info.ts @@ -18,6 +18,7 @@ export class PsProcInfo implements ProcInfo { "-p", pid.toString(), ]); + return stdout?.trim(); } } diff --git a/src/adapters/node/uds-transport.ts b/src/adapters/node/uds-transport.ts index ef1a5a6..a144915 100644 --- a/src/adapters/node/uds-transport.ts +++ b/src/adapters/node/uds-transport.ts @@ -11,9 +11,13 @@ const DEFAULT_LINGER_MS = 150; class NodeInboundConnection implements InboundConnection { private buffer = ""; + private readonly lines: string[] = []; + private readonly waiters: ((line: string | undefined) => void)[] = []; + private ended = false; + /** macOS local-peer pid, captured from the first data chunk's control info. */ private cachedPid: number | undefined; @@ -137,6 +141,7 @@ export class UdsTransport implements Transport { resolve(); }); }); + return { socketPath, close: async () => { diff --git a/src/adapters/node/win-proc-info.ts b/src/adapters/node/win-proc-info.ts index c35a5b2..276d1e8 100644 --- a/src/adapters/node/win-proc-info.ts +++ b/src/adapters/node/win-proc-info.ts @@ -19,6 +19,7 @@ export class WinProcInfo implements ProcInfo { "-Command", script, ]); + return stdout?.trim(); } } diff --git a/src/alias-pool.ts b/src/alias-pool.ts index 8c50446..6063732 100644 --- a/src/alias-pool.ts +++ b/src/alias-pool.ts @@ -44,7 +44,9 @@ interface Deps { */ export class AliasPool extends EventEmitter { private readonly active = new Map(); + private readonly pending = new Map>(); + private readonly log: (message: string) => void; constructor( @@ -79,6 +81,7 @@ export class AliasPool extends EventEmitter { body: string, ): Promise<{ msgId: string }> { const proc = await this.acquire(name); + return proc.send(target, body); } @@ -117,6 +120,7 @@ export class AliasPool extends EventEmitter { }); this.active.set(name, proc); this.log(`alias ${name} active`); + return proc; } diff --git a/src/alias-pool.unit.test.ts b/src/alias-pool.unit.test.ts index 7394295..20f9ce5 100644 --- a/src/alias-pool.unit.test.ts +++ b/src/alias-pool.unit.test.ts @@ -23,17 +23,25 @@ describe("workerExtensionFor", () => { /** A fake AliasProcess whose start()/stop() are externally controllable, so tests can assert ordering and concurrency without a real child process. */ class FakeAliasProcess implements AliasProcess { readonly events = new EventEmitter(); + startCalls: unknown[] = []; + sendCalls: { target: Readonly; body: string }[] = []; + stopCalls = 0; + private resolveStart: (() => void) | undefined; + private rejectStart: ((error: Error) => void) | undefined; + private resolveSend: ((result: Readonly<{ msgId: string }>) => void) | undefined; + private rejectSend: ((error: Error) => void) | undefined; async start(options: unknown): Promise { this.startCalls.push(options); + return new Promise((resolve, reject) => { this.resolveStart = resolve; this.rejectStart = reject; @@ -45,6 +53,7 @@ class FakeAliasProcess implements AliasProcess { body: string, ): Promise<{ msgId: string }> { this.sendCalls.push({ target, body }); + return new Promise<{ msgId: string }>((resolve, reject) => { this.resolveSend = resolve; this.rejectSend = reject; @@ -53,6 +62,7 @@ class FakeAliasProcess implements AliasProcess { async stop(): Promise { this.stopCalls += 1; + return Promise.resolve(); } @@ -84,8 +94,10 @@ function makePool(procs: readonly FakeAliasProcess[]): { if (proc === undefined) { throw new Error("makePool: not enough fake processes provided"); } + return proc; }); + return { pool: new AliasPool({ spawn }), spawnCount: () => spawn.mock.calls.length, @@ -108,6 +120,7 @@ describe("AliasPool.ensure", () => { let index = 0; const spawn = vi.fn(() => { index += 1; + return proc; }); const pool = new AliasPool( diff --git a/src/api/schemas.ts b/src/api/schemas.ts index bf8bcc2..7bfbe5e 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -136,6 +136,7 @@ export function componentSchemasFrom( delete rest.$schema; stripped[name] = rest; } + return stripped; } diff --git a/src/api/server.ts b/src/api/server.ts index 7c1744e..e0c8de0 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -48,6 +48,7 @@ export async function createApiServer( const server = createServer((req, res) => { void handle(peer, req, res, token); }); + return new Promise((resolve) => { server.listen(options.port ?? 0, BIND_HOST, () => { const port = listeningPort(server.address()); @@ -84,12 +85,14 @@ async function handle( const host = hostnameOf(req.headers.host); if (!ALLOWED_HOSTS.has(host)) { finish(HTTP_FORBIDDEN, errorBody("host not allowed")); + return; } if (token !== undefined) { const auth = req.headers.authorization ?? ""; if (auth !== `Bearer ${token}`) { finish(HTTP_UNAUTHORIZED, errorBody("unauthorized")); + return; } } @@ -97,15 +100,18 @@ async function handle( try { if (req.method === "GET" && url.pathname === "/healthz") { finish(HTTP_OK, JSON.stringify({ ok: true })); + return; } if (req.method === "GET" && url.pathname === "/openapi.json") { finish(HTTP_OK, JSON.stringify(openApiDocument())); + return; } if (req.method === "GET" && url.pathname === "/sessions") { const roster = await peer.roster(); finish(HTTP_OK, JSON.stringify({ sessions: roster })); + return; } if (req.method === "POST" && url.pathname === "/messages") { @@ -121,6 +127,7 @@ async function handle( : {}), }); finish(HTTP_ACCEPTED, JSON.stringify(sent)); + return; } if (req.method === "POST" && url.pathname === "/idle-subscriptions") { @@ -129,10 +136,12 @@ async function handle( ); const sent = await peer.subscribeIdle(toPeerRef(request.to)); finish(HTTP_ACCEPTED, JSON.stringify(sent)); + return; } if (req.method === "GET" && url.pathname === "/events") { streamEvents(peer, req, res); + return; } finish(HTTP_NOT_FOUND, errorBody("not found")); @@ -150,6 +159,7 @@ export function listeningPort(address: string | AddressInfo | null): number { "expected the server to report an AddressInfo after listen()", ); } + return address.port; } @@ -159,6 +169,7 @@ export function listeningPort(address: string | AddressInfo | null): number { export function hostnameOf(hostHeader: string | undefined): string { if (hostHeader === undefined) return ""; const colonIndex = hostHeader.indexOf(":"); + return colonIndex === -1 ? hostHeader : hostHeader.slice(0, colonIndex); } @@ -257,6 +268,7 @@ function openApiDocument(): Record { required: true, content: { "application/json": { schema: ref(name) } }, }); + return { openapi: "3.1.0", info: { diff --git a/src/bin/main.ts b/src/bin/main.ts index 2e14354..75a573b 100644 --- a/src/bin/main.ts +++ b/src/bin/main.ts @@ -78,6 +78,7 @@ export function parseArgs(argv: readonly string[]): CliCommand { args.home = value; } } + return { kind: "run", args }; } @@ -91,6 +92,7 @@ export async function main( const command = parseArgs(argv); if (command.kind === "help") { process.stdout.write(USAGE); + return; } if (command.kind === "error") { @@ -98,6 +100,7 @@ export async function main( `[cc-peer] ${command.message}\nRun 'cc-peer --help' for usage.\n`, ); process.exitCode = 2; + return; } const { args } = command; diff --git a/src/cc-peer.ts b/src/cc-peer.ts index 5a2a320..9bd1b7e 100644 --- a/src/cc-peer.ts +++ b/src/cc-peer.ts @@ -99,10 +99,15 @@ const DEFAULT_HEARTBEAT_MS = 15_000; */ export class CcPeer extends EventEmitter { private listening: ListeningSocket | undefined; + private heartbeatTimer: NodeJS.Timeout | undefined; + private ownKey: PeerKeyFile | undefined; + private readonly pacer: Pacer; + private readonly log: (message: string) => void; + private stopped = false; constructor( @@ -123,6 +128,7 @@ export class CcPeer extends EventEmitter { clock: new SystemClock(), }); await peer.start(); + return peer; } @@ -160,6 +166,7 @@ export class CcPeer extends EventEmitter { /** procStart is a required parameter, not read from this.ownKey: start() already guarantees a non-empty value before this is called, so the type checker enforces it rather than a runtime fallback that can never actually fire. */ private buildRegistryEntry(procStart: string): RegistryEntry { const now = this.deps.clock.nowMs(); + return { pid: process.pid, sessionId: this.options.sessionId ?? newMsgId(), @@ -189,6 +196,7 @@ export class CcPeer extends EventEmitter { async roster(): Promise { const entries = await this.deps.registry.list(); + return filterRoster(entries, { transport: this.deps.transport, procInfo: this.deps.procInfo, @@ -251,6 +259,7 @@ export class CcPeer extends EventEmitter { ); } await this.pacedSend(socketPath, [JSON.stringify(auth), line]); + return { msgId }; } @@ -299,6 +308,7 @@ export class CcPeer extends EventEmitter { JSON.stringify(auth), JSON.stringify(frame), ]); + return { msgId }; } @@ -312,6 +322,7 @@ export class CcPeer extends EventEmitter { if (match === undefined) { throw new UnknownPeerError(`no roster entry named ${target.name}`); } + return match.messagingSocketPath; } @@ -334,6 +345,7 @@ export class CcPeer extends EventEmitter { if (!authenticated) { this.log("inbound auth line missing or mismatched (connection closed)"); conn.close(); + return; } } else if (!authenticated) { diff --git a/src/domain/dedup.ts b/src/domain/dedup.ts index fa886a0..b3dff35 100644 --- a/src/domain/dedup.ts +++ b/src/domain/dedup.ts @@ -8,5 +8,6 @@ export const DEDUP_WINDOW_MS = 30_000; */ export function varyBody(body: string, nth: number): string { const zwj = "‍"; + return body + zwj.repeat(nth + 1); } diff --git a/src/domain/envelope.ts b/src/domain/envelope.ts index 3d7bcf1..8373300 100644 --- a/src/domain/envelope.ts +++ b/src/domain/envelope.ts @@ -45,11 +45,13 @@ function serializeAttributes(attrs: EnvelopeAttributes): string { if (attrs.fromMode !== undefined) { parts.push(` from-mode="${attrs.fromMode}"`); } + return parts.join(""); } export function buildEnvelope(attrs: EnvelopeAttributes, body: string): string { const validated = EnvelopeAttributesSchema.parse(attrs); + return `<${TAG}${serializeAttributes(validated)}>\n${escapeBody(body)}\n`; } @@ -66,8 +68,8 @@ export interface ParsedEnvelope { export function parseEnvelope(content: string): ParsedEnvelope | undefined { const match = ENVELOPE_RE.exec(content); if (match === null) return undefined; - // The body is derived positionally from the validated content rather than read from a capture group: attribute grammars bar ">" and newlines, so the first ">\n" is the opening tag's end, and the regex anchor guarantees the "\n" suffix. This avoids an index access that - // noUncheckedIndexedAccess would type string | undefined. + /* The body is derived positionally from the validated content rather than read from a capture group: attribute grammars bar ">" and newlines, so the first ">\n" is the opening tag's end, and the regex anchor guarantees the "\n" suffix. This avoids an index access that + noUncheckedIndexedAccess would type string | undefined. */ const openingEnd = content.indexOf(">\n") + 2; const closingStart = content.length - `\n`.length; const parsed: ParsedEnvelope = { @@ -80,6 +82,7 @@ export function parseEnvelope(content: string): ParsedEnvelope | undefined { if (match[5] === "bypass" || match[5] === "prompting") { parsed.fromMode = match[5]; } + return parsed; } @@ -101,5 +104,6 @@ export function assertRoundTrips(content: string): boolean { }, parsed.body, ); + return rebuilt === content; } diff --git a/src/domain/file-transfer.ts b/src/domain/file-transfer.ts index fd64c0f..f1406ba 100644 --- a/src/domain/file-transfer.ts +++ b/src/domain/file-transfer.ts @@ -54,6 +54,7 @@ export async function stageFile( `${sha256.slice(0, ID_PREFIX_CHARS)}-${randomUUID().slice(0, ID_PREFIX_CHARS)}-${safeName(basename(sourcePath))}`, ); await writeFile(staged, bytes, { mode: 0o600 }); + return { path: staged, file_name: basename(sourcePath), @@ -106,6 +107,7 @@ export async function materialiseAttachment( await mkdir(dirname(dest), { recursive: true, mode: 0o700 }); await copyFile(absolute, dest); await unlink(absolute); + return { uploadPath: dest, mention: `@"${dest}"` }; } @@ -118,6 +120,7 @@ export function capAttachments(attachments: readonly FileAttachment[]): { return { kept: [...attachments], droppedNote: undefined }; } const dropped = attachments.length - MAX_ATTACHMENTS_PER_MESSAGE; + return { kept: attachments.slice(0, MAX_ATTACHMENTS_PER_MESSAGE), droppedNote: `[SendFile: ${dropped.toString()} additional attachment(s) were dropped — max ${MAX_ATTACHMENTS_PER_MESSAGE.toString()} per message]`, diff --git a/src/domain/hop-chain.ts b/src/domain/hop-chain.ts index 23cc6f3..ceca075 100644 --- a/src/domain/hop-chain.ts +++ b/src/domain/hop-chain.ts @@ -13,6 +13,7 @@ export function appendHop( ownId: string, ): string[] { const next = [...(chain ?? []), ownId]; + return next.slice(Math.max(0, next.length - MAX_HOP_CHAIN_ENTRIES)); } @@ -37,5 +38,6 @@ export function checkChain( if (selfHops >= MAX_SELF_HOPS) { return { admitted: false, reason: "hop-loop" }; } + return { admitted: true }; } diff --git a/src/domain/roster.ts b/src/domain/roster.ts index 87786da..1bce97b 100644 --- a/src/domain/roster.ts +++ b/src/domain/roster.ts @@ -29,6 +29,7 @@ export async function filterRoster( const verdicts = await Promise.all( entries.map(async (entry) => checkEntry(entry, probes)), ); + return verdicts.filter((v) => v.admitted).map((v) => v.entry); } @@ -57,5 +58,6 @@ export async function checkEntry( if (!connectable) { return { entry, admitted: false, reason: "socket-dead" }; } + return { entry, admitted: true }; } diff --git a/src/sea-entry.ts b/src/sea-entry.ts index 6a31db5..4c19ebc 100644 --- a/src/sea-entry.ts +++ b/src/sea-entry.ts @@ -1,11 +1,11 @@ import { main } from "./bin/main.js"; -// Node's single-executable application feature runs a CommonJS entry with -// no top-level await (https://nodejs.org/api/single-executable-applications.html): -// this is that entry, built separately from src/bin/cc-peer.ts as a fully -// bundled .cjs with zero external requires, since a SEA embeds no -// node_modules of its own. main stays genuinely async; only this top level -// is synchronous, which is all the CommonJS module system needs. +/* Node's single-executable application feature runs a CommonJS entry with + no top-level await (https://nodejs.org/api/single-executable-applications.html): + this is that entry, built separately from src/bin/cc-peer.ts as a fully + bundled .cjs with zero external requires, since a SEA embeds no + node_modules of its own. main stays genuinely async; only this top level + is synchronous, which is all the CommonJS module system needs. */ main().catch((error: unknown) => { process.stderr.write( `[cc-peer] fatal: ${error instanceof Error ? error.message : String(error)}\n`, diff --git a/src/test/socket-path.ts b/src/test/socket-path.ts index 30e02e5..db9462c 100644 --- a/src/test/socket-path.ts +++ b/src/test/socket-path.ts @@ -15,7 +15,9 @@ export function testSocketPath(home: string, label: string): string { .update(home) .digest("hex") .slice(0, NAMESPACE_HEX_LENGTH); + return `\\\\.\\pipe\\cc-peer-test-${namespace}-${label}`; } + return join(home, `${label}.sock`); } diff --git a/stryker.config.ts b/stryker.config.ts index fe2076c..096521f 100644 --- a/stryker.config.ts +++ b/stryker.config.ts @@ -1,14 +1,14 @@ const options = { testRunner: "vitest", - // Named explicitly rather than left to auto-discovery: a fresh pnpm install's strict, non-flat node_modules layout does not expose - // @stryker-mutator/vitest-runner the way auto-discovery expects, failing - // with "Cannot find TestRunner plugin vitest" on a clean CI checkout even though the identical config resolves it fine against an already-populated local node_modules. + /* Named explicitly rather than left to auto-discovery: a fresh pnpm install's strict, non-flat node_modules layout does not expose + @stryker-mutator/vitest-runner the way auto-discovery expects, failing + with "Cannot find TestRunner plugin vitest" on a clean CI checkout even though the identical config resolves it fine against an already-populated local node_modules. */ plugins: ["@stryker-mutator/vitest-runner"], reporters: ["html", "clear-text", "progress"], coverageAnalysis: "perTest", - // define-schema.ts is excluded, not just left to ignoreStatic: every schema module assigns defineSchema's return value straight to a top-level export const, so a mutant that empties defineSchema's body poisons every module that imports it and crashes test collection across ten-plus files at once. That triggers a still-open - // @stryker-mutator/vitest-runner bug (stryker-mutator/stryker-js#6150): - // the runner's collection-failure path never sets its own failure flag, so a mutant that crashes every test in a file at import time is misreported Survived instead of Killed, confirmed directly by reproducing it manually and watching a real vitest run correctly fail ten files while stryker still reported the mutant as surviving. ignoreStatic was tried first but is too broad a fix: every schema definition in this codebase is itself a top-level const, so it silently zeroes out mutation testing for the entire schemas directory, not just this one function. Excluding only define-schema.ts (a three-line helper already exercised indirectly by every other schema's own tests) keeps the rest of the schema layer's mutation score meaningful. + /* define-schema.ts is excluded, not just left to ignoreStatic: every schema module assigns defineSchema's return value straight to a top-level export const, so a mutant that empties defineSchema's body poisons every module that imports it and crashes test collection across ten-plus files at once. That triggers a still-open + @stryker-mutator/vitest-runner bug (stryker-mutator/stryker-js#6150): + the runner's collection-failure path never sets its own failure flag, so a mutant that crashes every test in a file at import time is misreported Survived instead of Killed, confirmed directly by reproducing it manually and watching a real vitest run correctly fail ten files while stryker still reported the mutant as surviving. ignoreStatic was tried first but is too broad a fix: every schema definition in this codebase is itself a top-level const, so it silently zeroes out mutation testing for the entire schemas directory, not just this one function. Excluding only define-schema.ts (a three-line helper already exercised indirectly by every other schema's own tests) keeps the rest of the schema layer's mutation score meaningful. */ mutate: [ "src/domain/**/*.ts", "src/schemas/**/*.ts", diff --git a/test/api.integration.test.ts b/test/api.integration.test.ts index 9624ee3..2fea6fa 100644 --- a/test/api.integration.test.ts +++ b/test/api.integration.test.ts @@ -50,8 +50,8 @@ describe("REST facade", () => { const unauthorized = await fetch(`${base}/sessions`); expect(unauthorized.status).toBe(401); - // fetch refuses to override the forbidden Host header, so probe the - // DNS-rebinding defence with a raw request that does set it. + /* fetch refuses to override the forbidden Host header, so probe the + DNS-rebinding defence with a raw request that does set it. */ const rebinding = await new Promise((resolve) => { const req = request( `${base}/healthz`, diff --git a/test/cli.integration.test.ts b/test/cli.integration.test.ts index bd9e1b0..b763018 100644 --- a/test/cli.integration.test.ts +++ b/test/cli.integration.test.ts @@ -17,6 +17,7 @@ async function runCli(args: readonly string[]): Promise<{ home: string; }> { const home = await mkdtemp(join(tmpdir(), "cc-peer-cli-")); + return new Promise((resolve, reject) => { const child = spawn(process.execPath, ["--import", "tsx", ENTRY, ...args], { env: { ...process.env, HOME: home, USERPROFILE: home }, diff --git a/test/sea.e2e.test.ts b/test/sea.e2e.test.ts index f79e83d..5b4dbef 100644 --- a/test/sea.e2e.test.ts +++ b/test/sea.e2e.test.ts @@ -27,6 +27,7 @@ async function freePort(): Promise { probe.close(() => { if (address === null || typeof address === "string") { reject(new Error("expected the probe server to report a port")); + return; } resolve(address.port); diff --git a/tsconfig.json b/tsconfig.json index c02c19b..335fa4f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,22 @@ { "compilerOptions": { - "target": "ES2024", + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "incremental": true, + "isolatedModules": true, + "lib": ["ES2024"], "module": "NodeNext", "moduleResolution": "nodenext", - "lib": ["ES2024"], - "types": ["node"], - "strict": true, - "resolveJsonModule": true, - "noUncheckedIndexedAccess": true, - "exactOptionalPropertyTypes": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, "noEmit": true, - "incremental": true, - "tsBuildInfoFile": ".tsbuildinfo", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, "skipLibCheck": true, - "isolatedModules": true, - "forceConsistentCasingInFileNames": true + "strict": true, + "target": "ES2024", + "tsBuildInfoFile": ".tsbuildinfo", + "types": ["node"] }, "include": ["src", "test", "scripts", "*.ts"] } diff --git a/turbo.json b/turbo.json index c264322..4ea4879 100644 --- a/turbo.json +++ b/turbo.json @@ -1,42 +1,21 @@ { "$schema": "https://turborepo.com/schema.json", + "agentGuidance": false, "tasks": { "_build": { - "dependsOn": [ - "^_build" - ], - "inputs": [ - "$TURBO_DEFAULT$", - "!src/**/*.test.ts" - ], - "outputs": [ - "dist/**" - ] + "dependsOn": ["^_build"], + "inputs": ["$TURBO_DEFAULT$", "!src/**/*.test.ts"], + "outputs": ["dist/**"] }, "_lint": { - "dependsOn": [ - "^_lint" - ], - "inputs": [ - "$TURBO_DEFAULT$", - ".eslintcache" - ], - "outputs": [ - ".eslintcache" - ] + "dependsOn": ["^_lint"], + "inputs": ["$TURBO_DEFAULT$", ".eslintcache"], + "outputs": [".eslintcache"] }, "_typecheck": { - "dependsOn": [ - "^_typecheck", - "_build" - ], - "inputs": [ - "$TURBO_DEFAULT$" - ], - "outputs": [ - ".tsbuildinfo" - ] + "dependsOn": ["^_typecheck", "_build"], + "inputs": ["$TURBO_DEFAULT$"], + "outputs": [".tsbuildinfo"] } - }, - "agentGuidance": false + } } diff --git a/vitest.config.ts b/vitest.config.ts index f797363..5ab14c9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,8 @@ import { defineConfig } from "vitest/config"; -// 100% thresholds: the test suite is the contract that every line and branch of src/ is exercised. Test files themselves are excluded from measurement. unit and integration are separate named projects so either tier can be run alone (pnpm test:unit / test:integration) for fast local iteration, but the default pnpm test / test:coverage runs both together with no --project filter — vitest merges their coverage into one map, preserving the same combined 100% gate this repo has always had. Splitting the gate itself is not viable here: cc-peer.ts's own orchestration and the Node adapters it wires together are legitimately proven by integration-tier tests exercising real sockets/processes, not by unit-tier fakes, so a unit-only coverage floor would only push toward over-mocking real component wiring rather than testing it. e2e (the packaged SEA binary) lives in its own vitest.e2e.config.ts entirely, never as a project here: it needs a just-built binary that exists only inside the sea CI job's own context. +/** + * 100% thresholds: the test suite is the contract that every line and branch of src/ is exercised. Test files themselves are excluded from measurement. unit and integration are separate named projects so either tier can be run alone (pnpm test:unit / test:integration) for fast local iteration, but the default pnpm test / test:coverage runs both together with no --project filter — vitest merges their coverage into one map, preserving the same combined 100% gate this repo has always had. Splitting the gate itself is not viable here: cc-peer.ts's own orchestration and the Node adapters it wires together are legitimately proven by integration-tier tests exercising real sockets/processes, not by unit-tier fakes, so a unit-only coverage floor would only push toward over-mocking real component wiring rather than testing it. e2e (the packaged SEA binary) lives in its own vitest.e2e.config.ts entirely, never as a project here: it needs a just-built binary that exists only inside the sea CI job's own context. + */ export default defineConfig({ test: { projects: [ diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts index 8862d3a..80e3cf2 100644 --- a/vitest.e2e.config.ts +++ b/vitest.e2e.config.ts @@ -1,6 +1,8 @@ import { defineConfig } from "vitest/config"; -// Separate root config, not a project under vitest.config.ts: e2e tests need a just-built SEA binary (CC_PEER_SEA_BINARY) that only exists inside the sea CI job's own context, never when pnpm test/test:coverage run, so it must never be picked up by those default invocations. +/** + * Separate root config, not a project under vitest.config.ts: e2e tests need a just-built SEA binary (CC_PEER_SEA_BINARY) that only exists inside the sea CI job's own context, never when pnpm test/test:coverage run, so it must never be picked up by those default invocations. + */ export default defineConfig({ test: { include: ["test/**/*.e2e.test.ts"], From 4f6c7a73cff6f71326fb7d929816db53a7a008b5 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 10 Oct 2026 09:08:48 +0100 Subject: [PATCH 2/3] refactor(pacer): take an options object and tighten internal signatures Pacer is internal and not re-exported, so its constructor takes the existing PacerOptions instead of two trailing optional parameters. defineSchema declares its return type explicitly, and two test helpers stop reassigning shared state across awaits. --- .../node/adapters-extra.integration.test.ts | 10 +++--- src/cc-peer-class.integration.test.ts | 32 ++++++++++++------- src/domain/pacer.ts | 18 ++++++++--- src/domain/units.unit.test.ts | 18 +++++++---- src/schemas/define-schema.ts | 4 ++- 5 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/adapters/node/adapters-extra.integration.test.ts b/src/adapters/node/adapters-extra.integration.test.ts index ef6deaf..d29118e 100644 --- a/src/adapters/node/adapters-extra.integration.test.ts +++ b/src/adapters/node/adapters-extra.integration.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { spawn } from "node:child_process"; import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -48,13 +48,12 @@ describe.skipIf(process.platform === "win32")("PsProcInfo", () => { }); test("a missing ps binary resolves lstart to undefined", async () => { - const realPath = process.env.PATH; - process.env.PATH = "/nonexistent-cc-peer-test-bin"; + vi.stubEnv("PATH", "/nonexistent-cc-peer-test-bin"); try { const fresh = new PsProcInfo(); expect(await fresh.lstart(process.pid)).toBeUndefined(); } finally { - if (realPath !== undefined) process.env.PATH = realPath; + vi.unstubAllEnvs(); } }); }); @@ -81,7 +80,7 @@ describe("UdsTransport connection lifecycle", () => { const home = await tempHome(); const sockPath = testSocketPath(home, "lifecycle"); const transport = new UdsTransport(); - let received: string[] = []; + const received: string[] = []; let iterationEnded = false; const listener = await transport.listen(sockPath, (conn) => { expect(conn.peerPid()).toBeUndefined(); @@ -105,7 +104,6 @@ describe("UdsTransport connection lifecycle", () => { expect(received).toEqual(['{"type":"auth"}', '{"type":"user"}']); expect(iterationEnded).toBe(true); await listener.close(); - received = []; }); test("closing the listener destroys an accepted open connection", async () => { diff --git a/src/cc-peer-class.integration.test.ts b/src/cc-peer-class.integration.test.ts index 4d62716..a34a9b1 100644 --- a/src/cc-peer-class.integration.test.ts +++ b/src/cc-peer-class.integration.test.ts @@ -29,12 +29,17 @@ async function tempHome(): Promise { return mkdtemp(join(tmpdir(), "cc-peer-class-")); } -function peerOptions(home: string, name?: string, sessionId?: string) { +function peerOptions( + home: string, + identity: Readonly<{ name?: string; sessionId?: string }> = {}, +) { return { homeDir: home, socketDir: join(home, "socks"), - ...(name !== undefined ? { name } : {}), - ...(sessionId !== undefined ? { sessionId } : {}), + ...(identity.name !== undefined ? { name: identity.name } : {}), + ...(identity.sessionId !== undefined + ? { sessionId: identity.sessionId } + : {}), }; } @@ -50,7 +55,7 @@ function makePeer( ): CcPeer { return new CcPeer( { - ...peerOptions(home, overrides.name, overrides.sessionId), + ...peerOptions(home, overrides), ...(overrides.heartbeatMs !== undefined ? { heartbeatMs: overrides.heartbeatMs } : {}), @@ -80,6 +85,7 @@ async function rawClient(peer: CcPeer, socketPath?: string): Promise { socketPath ?? socketPathForPid(process.pid, peerOptions(tempHomeOf(peer))); const socket = connect(path); await once(socket, "connect"); + return socket; } @@ -137,6 +143,7 @@ describe("CcPeer dependency-injected construction", () => { transport: { listen: async (path) => { listenedPath = path; + return Promise.resolve({ socketPath: path, close: async () => Promise.resolve(), @@ -397,6 +404,7 @@ async function keyedTarget(home: string): Promise { pidDomain: "darwin", }); targetsToClose.push(listener); + return targetPath; } @@ -406,8 +414,8 @@ describe("CcPeer send happy paths by pid and address", () => { const peer = makePeer(home); await peer.start(); const keys = new FsKeyStore({ homeDir: home }); - // A target pid distinct from our own so the resolved socket path does - // not collide with the sender's own listening socket. + /* A target pid distinct from our own so the resolved socket path does + not collide with the sender's own listening socket. */ const TARGET_PID = 88001; const pidPath = socketPathForPid(TARGET_PID, { socketDir: join(home, "socks"), @@ -475,9 +483,9 @@ describe("CcPeer send happy paths by pid and address", () => { receiver.on("message", (m) => { messages.push(m); }); - // Both peers share one pid, so the single registry file holds whichever - // peer started last: the receiver, which is exactly what the sender's - // name resolution needs to find. + /* Both peers share one pid, so the single registry file holds whichever + peer started last: the receiver, which is exactly what the sender's + name resolution needs to find. */ await sender.send({ name: "opt-receiver" }, "attested default"); await sender.send({ name: "opt-receiver" }, "prompting", { fromMode: "prompting", @@ -511,9 +519,9 @@ describe("CcPeer send happy paths by pid and address", () => { await peer.start(); const target = await keyedTarget(home); const started = Date.now(); - // Concurrent sends exhaust the bucket faster than the 0.5/s refill can - // top it up (serial sends each pay connectWrite's 150ms linger, letting - // the refill mask the exhaustion). + /* Concurrent sends exhaust the bucket faster than the 0.5/s refill can + top it up (serial sends each pay connectWrite's 150ms linger, letting + the refill mask the exhaustion). */ await Promise.all( Array.from({ length: PACER_CAPACITY + 1 }, async (_, i) => peer.send({ address: target }, "burst " + i.toString()), diff --git a/src/domain/pacer.ts b/src/domain/pacer.ts index 73dbe33..16e4933 100644 --- a/src/domain/pacer.ts +++ b/src/domain/pacer.ts @@ -6,8 +6,8 @@ export const DEFAULT_REFILL_PER_SECOND = 0.5; const MS_PER_SECOND = 1_000; export interface PacerOptions { - capacity?: number; - refillPerSecond?: number; + readonly capacity?: number; + readonly refillPerSecond?: number; } /** @@ -15,14 +15,20 @@ export interface PacerOptions { */ export class Pacer { private tokens: number; + private lastRefillMs: number; + private readonly capacity: number; + + private readonly refillPerSecond: number; + constructor( private readonly clock: Readonly, - private readonly capacity: number = DEFAULT_BUCKET_CAPACITY, - private readonly refillPerSecond: number = DEFAULT_REFILL_PER_SECOND, + options: PacerOptions = {}, ) { - this.tokens = capacity; + this.capacity = options.capacity ?? DEFAULT_BUCKET_CAPACITY; + this.refillPerSecond = options.refillPerSecond ?? DEFAULT_REFILL_PER_SECOND; + this.tokens = this.capacity; this.lastRefillMs = clock.nowMs(); } @@ -30,6 +36,7 @@ export class Pacer { msUntilNextToken(): number { this.refill(); const deficit = Math.max(0, 1 - this.tokens); + return Math.ceil((deficit / this.refillPerSecond) * MS_PER_SECOND); } @@ -38,6 +45,7 @@ export class Pacer { this.refill(); if (this.tokens < 1) return false; this.tokens -= 1; + return true; } diff --git a/src/domain/units.unit.test.ts b/src/domain/units.unit.test.ts index a40621e..b0702c5 100644 --- a/src/domain/units.unit.test.ts +++ b/src/domain/units.unit.test.ts @@ -11,9 +11,11 @@ const NO_START = "no-start"; class FakeClock { constructor(private now: number) {} + nowMs(): number { return this.now; } + advance(ms: number): void { this.now += ms; } @@ -22,7 +24,7 @@ class FakeClock { describe("Pacer", () => { test("refills tokens over elapsed time and blocks until one is available", () => { const clock = new FakeClock(1_000_000); - const pacer = new Pacer(clock, 2, 0.5); + const pacer = new Pacer(clock, { capacity: 2, refillPerSecond: 0.5 }); expect(pacer.tryReserve()).toBe(true); expect(pacer.tryReserve()).toBe(true); expect(pacer.tryReserve()).toBe(false); @@ -40,20 +42,23 @@ describe("Pacer", () => { test("capacity caps the refill", () => { const clock = new FakeClock(0); - const pacer = new Pacer(clock, 1, 1); + const pacer = new Pacer(clock, { capacity: 1, refillPerSecond: 1 }); clock.advance(60_000); expect(pacer.tryReserve()).toBe(true); expect(pacer.tryReserve()).toBe(false); }); test("msUntilNextToken is zero while tokens remain", () => { - const pacer = new Pacer(new FakeClock(0), 3, 0.5); + const pacer = new Pacer(new FakeClock(0), { + capacity: 3, + refillPerSecond: 0.5, + }); expect(pacer.msUntilNextToken()).toBe(0); }); test("msUntilNextToken refills from elapsed time on its own, without a prior tryReserve", () => { const clock = new FakeClock(0); - const pacer = new Pacer(clock, 1, 1); + const pacer = new Pacer(clock, { capacity: 1, refillPerSecond: 1 }); expect(pacer.tryReserve()).toBe(true); clock.advance(1_000); expect(pacer.msUntilNextToken()).toBe(0); @@ -61,7 +66,7 @@ describe("Pacer", () => { test("msUntilNextToken reports the exact wait for a fractional deficit", () => { const clock = new FakeClock(0); - const pacer = new Pacer(clock, 1, 1); + const pacer = new Pacer(clock, { capacity: 1, refillPerSecond: 1 }); expect(pacer.tryReserve()).toBe(true); clock.advance(500); expect(pacer.msUntilNextToken()).toBe(500); @@ -69,7 +74,7 @@ describe("Pacer", () => { test("refill ignores a clock that moves backward rather than draining tokens", () => { const clock = new FakeClock(10_000); - const pacer = new Pacer(clock, 1, 1); + const pacer = new Pacer(clock, { capacity: 1, refillPerSecond: 1 }); expect(pacer.tryReserve()).toBe(true); clock.advance(-5_000); expect(pacer.msUntilNextToken()).toBe(1_000); @@ -116,6 +121,7 @@ function probes( Promise.resolve(overrides.alive ?? true); const probe = async (): Promise => Promise.resolve(overrides.probe ?? true); + return { transport: { probe }, procInfo: { alive, lstart }, diff --git a/src/schemas/define-schema.ts b/src/schemas/define-schema.ts index 577ede6..779b9ec 100644 --- a/src/schemas/define-schema.ts +++ b/src/schemas/define-schema.ts @@ -3,7 +3,9 @@ import type { z } from "zod"; /** * Attach an `.is()` type guard to a Zod schema so schema, inferred type, and runtime guard derive from one definition (single source of truth). `Schema.parse()` at JSON boundaries; `Schema.is()` for narrowing. */ -export function defineSchema(schema: T) { +export function defineSchema( + schema: T, +): T & { is: (value: unknown) => value is z.infer } { // Object.assign is required over spread here: a Zod schema is a class instance, and spreading it would drop the prototype (parse, safeParse, refinements), so the guard is deliberately attached to the live instance. The no-object-assign rule is disabled for this file in eslint.config.ts. return Object.assign(schema, { is(value: unknown): value is z.infer { From 60b654dbe6279932ea0e0096c8fd12f7f0413aa7 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 10 Oct 2026 09:09:03 +0100 Subject: [PATCH 3/3] build(deps): bump @exadev/eslint-config to 3.0.0 exadevConfig now takes the options object alone, so the repository blocks are spread after it inside defineConfig. The blocks that register or configure TypeScript rules are scoped to script files, and loops that are sequential by design are exempted with overrides scoped to the file that holds each of them. --- eslint.config.ts | 37 +++++++++- package.json | 2 +- pnpm-lock.yaml | 186 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 213 insertions(+), 12 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index 402426f..ef67c18 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,4 +1,5 @@ import { exadevConfig } from "@exadev/eslint-config"; +import { defineConfig } from "eslint/config"; import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; import globals from "globals"; import type { Rule } from "eslint"; @@ -23,12 +24,13 @@ const noStrykerDisable: Rule.RuleModule = { }); } } + return {}; }, }; -export default exadevConfig( - {}, +export default defineConfig( + ...exadevConfig(), { ignores: [ "dist", @@ -50,6 +52,7 @@ export default exadevConfig( }, }, { + files: ["**/*.{ts,tsx,mts,cts}"], rules: { "@typescript-eslint/consistent-type-imports": [ "error", @@ -58,16 +61,42 @@ export default exadevConfig( }, }, { + files: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"], plugins: { local: { rules: { "no-stryker-disable": noStrykerDisable } }, }, rules: { "local/no-stryker-disable": "error" }, }, /* Test fixtures legitimately encode raw protocol values (16-byte tokens, - 24-hex hop ids, chain lengths); naming them would obscure the fixture. */ + 24-hex hop ids, chain lengths); naming them would obscure the fixture. + Polling for an event and draining a stream are sequential by definition: + each iteration depends on the observation made by the previous one. */ { files: ["**/*.test.ts"], - rules: { "@typescript-eslint/no-magic-numbers": "off" }, + rules: { + "@typescript-eslint/no-magic-numbers": "off", + "no-await-in-loop": "off", + }, + }, + /* readLines is an async generator over a socket: each line is awaited + before the next can be produced, so the loop cannot be parallelised. */ + { + files: ["src/adapters/node/uds-transport.ts"], + rules: { "no-await-in-loop": "off" }, + }, + /* Registry entries are read one at a time so the listing order and the + number of open file handles stay bounded by one, whatever the directory + holds. */ + { + files: ["src/adapters/node/fs-registry-store.ts"], + rules: { "no-await-in-loop": "off" }, + }, + /* The spool sweep stats and unlinks one file at a time so removal order + and open handles stay bounded by one, and the first failed unlink stops + the pass exactly as before. */ + { + files: ["src/domain/file-transfer.ts"], + rules: { "no-await-in-loop": "off" }, }, /* defineSchema attaches a guard to a live Zod class instance; spread would destroy the prototype, so Object.assign is the only correct tool there. */ diff --git a/package.json b/package.json index 3f4555e..861a9b7 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@arethetypeswrong/cli": "0.18.5", "@commitlint/config-conventional": "21.2.2", "@commitlint/types": "21.2.0", - "@exadev/eslint-config": "2.12.1", + "@exadev/eslint-config": "3.0.0", "@semantic-release/changelog": "7.0.0", "@semantic-release/commit-analyzer": "13.0.1", "@semantic-release/exec": "7.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb78c1b..93eddb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,8 +188,8 @@ importers: specifier: 21.2.0 version: 21.2.0 '@exadev/eslint-config': - specifier: 2.12.1 - version: 2.12.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(typescript@6.0.3) + specifier: 3.0.0 + version: 3.0.0(@eslint/json@2.1.1)(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(typescript@6.0.3) '@semantic-release/changelog': specifier: 7.0.0 version: 7.0.0(semantic-release@25.0.9(supports-color@7.2.0)(typescript@6.0.3)) @@ -850,6 +850,10 @@ packages: eslint: optional: true + '@eslint/json@2.1.1': + resolution: {integrity: sha512-ffVzPZALaJzxuo46ggBtJ9c8YBWKLMh/R35aFBHk/dRobmFNortJlHJfBWbHGuydoFeVZbvPy0PLX2wvCI9T9w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/object-schema@3.0.5': resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -858,26 +862,38 @@ packages: resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@exadev/eslint-config@2.12.1': - resolution: {integrity: sha512-b5JKCX7l5onZy0i0kHdIeuiocxDoiuEwnrG6+j+EV8ZWaLpi18BDzNi1iT3tc647DFRUYJtHWDiJrkWT6E2JjQ==} - engines: {node: '>=20'} + '@exadev/eslint-config@3.0.0': + resolution: {integrity: sha512-nvyOjjM1sGKjQo6buZt69vJ5ND0CuE/lxoDvR2oRgv74qj4gJBZr10d6Y/QeTd6LQIPDkPPnH78YYfX/HSzNJA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: + '@eslint/json': ^2.1.0 + '@eslint/markdown': ^8.0.3 '@next/eslint-plugin-next': ^16.3.2 + '@vitest/eslint-plugin': ^1.6.27 eslint: '>=10.0.0' eslint-plugin-jsx-a11y: ^6.10.2 eslint-plugin-react: ^7.37.5 eslint-plugin-react-hooks: ^7.1.1 + eslint-plugin-turbo: ^2.3.1 typescript: '>=4.8.4' typescript-eslint: '>=8.0.0' peerDependenciesMeta: + '@eslint/json': + optional: true + '@eslint/markdown': + optional: true '@next/eslint-plugin-next': optional: true + '@vitest/eslint-plugin': + optional: true eslint-plugin-jsx-a11y: optional: true eslint-plugin-react: optional: true eslint-plugin-react-hooks: optional: true + eslint-plugin-turbo: + optional: true '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} @@ -895,6 +911,10 @@ packages: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} + '@humanwhocodes/momoa@3.3.13': + resolution: {integrity: sha512-AbKq3EW41+HY6pNMJgAOrz68iyiTTf0Nr1JRHHfuG5zwniJ9Dw8J3U7oa1I2ptpqYwmEf1sXKOGK6lbdZ9eqmg==} + engines: {node: '>=18'} + '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} @@ -1064,6 +1084,9 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@microsoft/tsdoc@0.17.1': + resolution: {integrity: sha512-VHyYyyoH63jFoO3shpw9Zfijk5v/9p/VrClW35z0/q6ooK7sSmxpzOkS4iZPxs3vO4XtBW4H38HRAKkAMIK7DA==} + '@octokit/auth-token@6.0.0': resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} engines: {node: '>= 20'} @@ -1348,6 +1371,18 @@ packages: '@stryker-mutator/core': 10.0.0 vitest: '>=2.0.0' + '@stylistic/eslint-plugin@6.0.0-beta.6': + resolution: {integrity: sha512-DqDQZSrV/0AZ/4qLxaQUtNxafk3qWfeG5xOHx64ELjHWSHt2GtELZR6ClrNVvQwo/d13Atf8Hrh/s/ZnuJk57w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^9.0.0 || ^10.0.0 + oxlint: ^1.55.0 + peerDependenciesMeta: + eslint: + optional: true + oxlint: + optional: true + '@turbo/darwin-64@2.11.5': resolution: {integrity: sha512-KdY4y7BBjIisl+jAkb3FFaRjClGqHyaeHCcVcCT8LBzFFwUu6thgfxyRZAx0uL4KoxLmH+iOan8UhVf/aDDcaA==} cpu: [x64] @@ -1432,6 +1467,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.71.0': + resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/scope-manager@8.56.1': resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1440,6 +1481,10 @@ packages: resolution: {integrity: sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.71.0': + resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.56.1': resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1452,6 +1497,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/tsconfig-utils@8.71.0': + resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.69.0': resolution: {integrity: sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1467,6 +1518,10 @@ packages: resolution: {integrity: sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.71.0': + resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.56.1': resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1479,6 +1534,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/typescript-estree@8.71.0': + resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.56.1': resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1493,6 +1554,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.71.0': + resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.56.1': resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1501,6 +1569,10 @@ packages: resolution: {integrity: sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.71.0': + resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/coverage-v8@4.1.11': resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==} peerDependencies: @@ -2109,6 +2181,13 @@ packages: peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + eslint-plugin-json-canonical@2.1.3: + resolution: {integrity: sha512-qAcRVGkXGZvgduOIEBxHLOZhqUf0NdVY+bye/KXbd0oPH5VvoBBBETFEaJ1qa6efLSCH17C4sss7uvr80TfNTQ==} + engines: {node: '>=20'} + peerDependencies: + '@eslint/json': ^2.1.0 + eslint: '>=10.0.0' + eslint-plugin-prettier@5.5.6: resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -3741,6 +3820,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -4350,6 +4434,13 @@ snapshots: optionalDependencies: eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) + '@eslint/json@2.1.1': + dependencies: + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.3 + '@humanwhocodes/momoa': 3.3.13 + natural-compare: 1.4.0 + '@eslint/object-schema@3.0.5': {} '@eslint/plugin-kit@0.7.3': @@ -4357,17 +4448,26 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@exadev/eslint-config@2.12.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(typescript@6.0.3)': + '@exadev/eslint-config@3.0.0(@eslint/json@2.1.1)(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(typescript@6.0.3)': dependencies: '@eslint/js': 10.0.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) - '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@microsoft/tsdoc': 0.17.1 + '@stylistic/eslint-plugin': 6.0.0-beta.6(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) + '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.71.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) eslint-plugin-jsdoc: 64.3.6(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + eslint-plugin-json-canonical: 2.1.3(@eslint/json@2.1.1)(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) eslint-plugin-tsdoc: 0.5.2(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + minimatch: 10.2.6 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 typescript-eslint: 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + yaml: 2.9.1 + optionalDependencies: + '@eslint/json': 2.1.1 transitivePeerDependencies: + - oxlint - supports-color '@humanfs/core@0.19.2': @@ -4384,6 +4484,8 @@ snapshots: '@humanwhocodes/module-importer@1.0.1': {} + '@humanwhocodes/momoa@3.3.13': {} + '@humanwhocodes/retry@0.4.3': {} '@inquirer/ansi@2.0.8': {} @@ -4540,6 +4642,8 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@microsoft/tsdoc@0.17.1': {} + '@octokit/auth-token@6.0.0': {} '@octokit/core@7.0.8': @@ -4873,6 +4977,16 @@ snapshots: tslib: 2.8.1 vitest: 4.1.11(@types/node@26.4.1)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) + '@stylistic/eslint-plugin@6.0.0-beta.6(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/visitor-keys': 8.69.0 + espree: 11.2.0 + picomatch: 4.0.7 + optionalDependencies: + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) + '@turbo/darwin-64@2.11.5': optional: true @@ -4960,6 +5074,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.71.0(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3) + '@typescript-eslint/types': 8.71.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.56.1': dependencies: '@typescript-eslint/types': 8.56.1 @@ -4970,6 +5093,11 @@ snapshots: '@typescript-eslint/types': 8.69.0 '@typescript-eslint/visitor-keys': 8.69.0 + '@typescript-eslint/scope-manager@8.71.0': + dependencies: + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@6.0.3)': dependencies: typescript: 6.0.3 @@ -4978,6 +5106,10 @@ snapshots: dependencies: typescript: 6.0.3 + '@typescript-eslint/tsconfig-utils@8.71.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + '@typescript-eslint/type-utils@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.69.0 @@ -4994,6 +5126,8 @@ snapshots: '@typescript-eslint/types@8.69.0': {} + '@typescript-eslint/types@8.71.0': {} + '@typescript-eslint/typescript-estree@8.56.1(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/project-service': 8.56.1(supports-color@7.2.0)(typescript@6.0.3) @@ -5024,6 +5158,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.71.0(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.71.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3) + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.56.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) @@ -5046,6 +5195,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.71.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.56.1': dependencies: '@typescript-eslint/types': 8.56.1 @@ -5056,6 +5216,11 @@ snapshots: '@typescript-eslint/types': 8.69.0 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.71.0': + dependencies: + '@typescript-eslint/types': 8.71.0 + eslint-visitor-keys: 5.0.1 + '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -5613,6 +5778,11 @@ snapshots: - supports-color - typescript + eslint-plugin-json-canonical@2.1.3(@eslint/json@2.1.1)(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)): + dependencies: + '@eslint/json': 2.1.1 + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(prettier@3.9.6): dependencies: eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) @@ -7061,6 +7231,8 @@ snapshots: yaml@2.9.0: optional: true + yaml@2.9.1: {} + yargs-parser@20.2.9: {} yargs-parser@22.0.0: {}