diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index 9ca91cca..903401f6 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -15,7 +15,7 @@ "options": { "denyWarnings": true, "typeAware": true, - "reportUnusedDisableDirectives": "off" + "reportUnusedDisableDirectives": "error" }, "categories": { "correctness": "error", @@ -73,7 +73,12 @@ "hooks/unsupported-syntax": "warn", "hooks/use-memo": "error", "import/no-cycle": "error", - "import/no-unassigned-import": "off", + "import/no-unassigned-import": [ + "error", + { + "allow": ["**/*.css"] + } + ], "jsx-a11y/alt-text": "error", "jsx-a11y/anchor-has-content": "error", "jsx-a11y/anchor-is-valid": "error", @@ -211,7 +216,7 @@ "nextjs/no-typos": "warn", "nextjs/no-unwanted-polyfillio": "warn", "no-array-constructor": "error", - "no-await-in-loop": "off", + "no-await-in-loop": "error", "no-case-declarations": "error", "no-empty": "error", "no-empty-function": "error", @@ -227,11 +232,11 @@ "message": "Read environment variables through an env.ts or env/ module validated with @t3-oss/env-nextjs for Next.js or @t3-oss/env-core for other runtimes." } ], - "no-shadow": "off", + "no-shadow": "error", "no-undef": "off", "no-useless-assignment": "error", "no-var": "error", - "oxc/no-map-spread": "off", + "oxc/no-map-spread": "error", "prefer-const": "error", "prefer-rest-params": "error", "prefer-spread": "error", @@ -242,7 +247,7 @@ "react/jsx-no-duplicate-props": "error", "react/jsx-no-target-blank": "error", "react/jsx-no-undef": "error", - "react/no-array-index-key": "off", + "react/no-array-index-key": "error", "react/no-children-prop": "error", "react/no-danger-with-children": "error", "react/no-direct-mutation-state": "error", @@ -289,7 +294,7 @@ "typescript/class-literal-property-style": "error", "typescript/consistent-generic-constructors": "error", "typescript/consistent-indexed-object-style": "error", - "typescript/consistent-return": "off", + "typescript/consistent-return": "error", "typescript/consistent-type-assertions": "error", "typescript/consistent-type-definitions": "error", "typescript/consistent-type-exports": "error", @@ -360,8 +365,8 @@ "typescript/return-await": ["error", "error-handling-correctness-only"], "typescript/unified-signatures": "error", "typescript/use-unknown-in-catch-callback-variable": "error", - "unicorn/consistent-function-scoping": "off", - "unicorn/no-array-sort": "off", + "unicorn/consistent-function-scoping": "error", + "unicorn/no-array-sort": "error", "unicorn/no-instanceof-builtins": [ "error", { @@ -370,60 +375,6 @@ ] }, "overrides": [ - { - "files": ["**/*.{js,mjs,cjs}"], - "rules": { - "typescript/await-thenable": "off", - "typescript/consistent-type-exports": "off", - "typescript/dot-notation": "off", - "typescript/no-array-delete": "off", - "typescript/no-base-to-string": "off", - "typescript/no-confusing-void-expression": "off", - "typescript/no-deprecated": "off", - "typescript/no-duplicate-type-constituents": "off", - "typescript/no-floating-promises": "off", - "typescript/no-for-in-array": "off", - "typescript/no-implied-eval": "off", - "typescript/no-meaningless-void-operator": "off", - "typescript/no-misused-promises": "off", - "typescript/no-misused-spread": "off", - "typescript/no-mixed-enums": "off", - "typescript/no-redundant-type-constituents": "off", - "typescript/no-unnecessary-boolean-literal-compare": "off", - "typescript/no-unnecessary-condition": "off", - "typescript/no-unnecessary-template-expression": "off", - "typescript/no-unnecessary-type-arguments": "off", - "typescript/no-unnecessary-type-assertion": "off", - "typescript/no-unnecessary-type-conversion": "off", - "typescript/no-unnecessary-type-parameters": "off", - "typescript/no-unsafe-argument": "off", - "typescript/no-unsafe-assignment": "off", - "typescript/no-unsafe-call": "off", - "typescript/no-unsafe-enum-comparison": "off", - "typescript/no-unsafe-member-access": "off", - "typescript/no-unsafe-return": "off", - "typescript/no-unsafe-type-assertion": "off", - "typescript/no-unsafe-unary-minus": "off", - "typescript/no-useless-default-assignment": "off", - "typescript/non-nullable-type-assertion-style": "off", - "typescript/only-throw-error": "off", - "typescript/prefer-find": "off", - "typescript/prefer-includes": "off", - "typescript/prefer-nullish-coalescing": "off", - "typescript/prefer-optional-chain": "off", - "typescript/prefer-promise-reject-errors": "off", - "typescript/prefer-reduce-type-parameter": "off", - "typescript/prefer-regexp-exec": "off", - "typescript/prefer-return-this-type": "off", - "typescript/related-getter-setter-pairs": "off", - "typescript/require-array-sort-compare": "off", - "typescript/restrict-plus-operands": "off", - "typescript/restrict-template-expressions": "off", - "typescript/return-await": "off", - "typescript/unbound-method": "off", - "typescript/use-unknown-in-catch-callback-variable": "off" - } - }, { "files": [ "**/env.{js,cjs,mjs,ts,cts,mts}", @@ -462,41 +413,16 @@ } }, { - // Eve owns this generated Web Chat scaffold. Keep it updateable from the - // framework. - "files": ["src/app/**/*.tsx", "src/components/ai-elements/**/*.tsx"], + // AI Elements uses typography and animation utilities supplied through + // package styles that the Tailwind linter cannot resolve from the app + // entrypoint. Keep this exception limited to the affected components. + "files": [ + "src/components/ai-elements/message.tsx", + "src/components/ai-elements/reasoning.tsx", + "src/components/ai-elements/tool.tsx" + ], "rules": { - "eslint/no-empty-function": "off", - "eslint/no-underscore-dangle": "off", - "hooks/refs": "off", - "hooks/set-state-in-effect": "off", - "hooks/static-components": "off", - "jsx-a11y/anchor-has-content": "off", - "jsx-a11y/control-has-associated-label": "off", - "jsx-a11y/heading-has-content": "off", - "nextjs/no-img-element": "off", - "promise/always-return": "off", - "tailwindcss/enforce-consistent-important-position": "off", - "tailwindcss/enforce-shorthand": "off", - "tailwindcss/no-deprecated-classes": "off", - "tailwindcss/no-unknown-classes": "off", - "typescript/consistent-type-definitions": "off", - "typescript/no-confusing-void-expression": "off", - "typescript/no-deprecated": "off", - "typescript/no-floating-promises": "off", - "typescript/no-misused-promises": "off", - "typescript/no-unnecessary-condition": "off", - "typescript/no-unnecessary-type-assertion": "off", - "typescript/no-unsafe-argument": "off", - "typescript/no-unsafe-assignment": "off", - "typescript/no-unsafe-member-access": "off", - "typescript/no-unsafe-return": "off", - "typescript/no-unsafe-type-assertion": "off", - "typescript/prefer-nullish-coalescing": "off", - "typescript/restrict-template-expressions": "off", - "typescript/return-await": "off", - "typescript/use-unknown-in-catch-callback-variable": "off", - "unicorn/prefer-add-event-listener": "off" + "tailwindcss/no-unknown-classes": "off" } } ] diff --git a/agent/channels/eve.ts b/agent/channels/eve.ts index 8740d424..c3e62e85 100644 --- a/agent/channels/eve.ts +++ b/agent/channels/eve.ts @@ -34,19 +34,19 @@ export default eveChannel({ function sessionIdFromPath(pathname: string) { const match = /^\/eve\/v1\/session\/([^/]+)/.exec(pathname); - if (!match?.[1]) return; + if (!match?.[1]) return undefined; try { return decodeURIComponent(match[1]); } catch { - return; + return undefined; } } async function requestIdentityFromRequest(request: Request) { const session = await getAuthSession(request.headers); - if (!session) return; + if (!session) return undefined; const phoneNumber = z.string().safeParse(session.user.phoneNumber); - if (!phoneNumber.success) return; + if (!phoneNumber.success) return undefined; return { phoneNumber: phoneNumber.data, @@ -55,9 +55,11 @@ async function requestIdentityFromRequest(request: Request) { } async function waitForSessionOwnership(scope: AccessScope, sessionId: string) { + /* oxlint-disable eslint/no-await-in-loop -- Ownership visibility is checked by a bounded sequential retry loop. */ for (let attempt = 0; attempt < 5; attempt += 1) { if (await isSessionOwned(scope, sessionId)) return true; await new Promise((resolve) => setTimeout(resolve, 50)); } + /* oxlint-enable eslint/no-await-in-loop */ return false; } diff --git a/agent/channels/linq.ts b/agent/channels/linq.ts index d70ea078..47e6b88a 100644 --- a/agent/channels/linq.ts +++ b/agent/channels/linq.ts @@ -65,6 +65,7 @@ async function postLinqReply( if (files.length > 0) await thread.post({ files, markdown: "" }); return; } + /* oxlint-disable eslint/no-await-in-loop -- Reply bubbles must be posted in conversational order. */ for (const [index, bubble] of bubbles.entries()) { if (index === bubbles.length - 1 && files.length > 0) { await thread.post({ files, markdown: bubble }); @@ -72,6 +73,7 @@ async function postLinqReply( await thread.post({ markdown: bubble }); } } + /* oxlint-enable eslint/no-await-in-loop */ } const credentials: LinqChannelCredentials = env.LINQ_CONNECTOR diff --git a/agent/lib/google-workspace/calendar.ts b/agent/lib/google-workspace/calendar.ts index a2d0dc44..09d94add 100644 --- a/agent/lib/google-workspace/calendar.ts +++ b/agent/lib/google-workspace/calendar.ts @@ -72,8 +72,8 @@ export function parseCalendarAvailability( value: calendar_v3.Schema$FreeBusyResponse ) { const failures = Object.entries(value.calendars ?? {}).flatMap( - ([calendarId, calendar]) => - (calendar.errors ?? []).map( + ([calendarId, calendarResult]) => + (calendarResult.errors ?? []).map( (error) => `${calendarId}: ${error.reason ?? error.domain ?? "unknown"}` ) ); diff --git a/agent/lib/google-workspace/gmail.ts b/agent/lib/google-workspace/gmail.ts index 0d368f4a..e23ee6f2 100644 --- a/agent/lib/google-workspace/gmail.ts +++ b/agent/lib/google-workspace/gmail.ts @@ -73,11 +73,12 @@ export async function readGmailThread(ctx: ToolContext, threadId: string) { ); return { id: thread.id ?? threadId, - messages: (thread.messages ?? []).slice(-20).map((message) => ({ - ...minimizeMessage(message), - attachments: collectAttachments(message.payload), - body: redactGoogleText(plainText(message.payload)), - })), + messages: (thread.messages ?? []).slice(-20).map((message) => + Object.assign({}, minimizeMessage(message), { + attachments: collectAttachments(message.payload), + body: redactGoogleText(plainText(message.payload)), + }) + ), }; }); } @@ -162,6 +163,7 @@ export function gmailUpdateLabels(action: GmailUpdateAction) { case "unstar": return { addLabelIds: [], removeLabelIds: ["STARRED"] }; } + throw new Error("Unsupported Gmail update action."); } function header(part: GmailPart | undefined, name: string) { diff --git a/agent/lib/linq-browser-image-delivery.ts b/agent/lib/linq-browser-image-delivery.ts index b56c10df..2e0b5a97 100644 --- a/agent/lib/linq-browser-image-delivery.ts +++ b/agent/lib/linq-browser-image-delivery.ts @@ -80,29 +80,31 @@ async function readLinqBrowserImage( !artifact.filename || !artifact.mediaType ) - return; - if (!env.BLOB_STORE_ID && !env.BLOB_READ_WRITE_TOKEN) return; + return undefined; + if (!env.BLOB_STORE_ID && !env.BLOB_READ_WRITE_TOKEN) return undefined; const result = await get(artifact.storagePathname, { access: "private", abortSignal: options.signal, }); - if (result?.statusCode !== 200) return; + if (result?.statusCode !== 200) return undefined; if ( result.blob.size !== artifact.byteSize || result.blob.contentType !== artifact.mediaType ) - return; + return undefined; const reader = result.stream.getReader(); const chunks: Uint8Array[] = []; let total = 0; try { + /* oxlint-disable eslint/no-await-in-loop -- Blob response chunks form an ordered stream. */ for (;;) { const { done, value } = await reader.read(); if (done) break; total += value.byteLength; - if (total > maximumBrowserImageBytes) return; + if (total > maximumBrowserImageBytes) return undefined; chunks.push(value); } + /* oxlint-enable eslint/no-await-in-loop */ } finally { reader.releaseLock(); } @@ -113,7 +115,7 @@ async function readLinqBrowserImage( offset += chunk.byteLength; } if (createHash("sha256").update(bytes).digest("hex") !== artifact.contentHash) - return; + return undefined; return { bytes, filename: artifact.filename, diff --git a/agent/subagents/worker/lib/autofill/native.ts b/agent/subagents/worker/lib/autofill/native.ts index 29f78cf4..d3aa691f 100644 --- a/agent/subagents/worker/lib/autofill/native.ts +++ b/agent/subagents/worker/lib/autofill/native.ts @@ -157,6 +157,7 @@ export async function fillWithKernelNativeAutofill({ } let lastError: unknown; + /* oxlint-disable eslint/no-await-in-loop -- Autofill tries controls in priority order and stops after the first accepted target. */ for (const control of controls) { try { await markNativeAutofilledControls(connection, control); @@ -175,6 +176,7 @@ export async function fillWithKernelNativeAutofill({ } return { filledClaims: claims.length, origin }; } + /* oxlint-enable eslint/no-await-in-loop */ throw new Error( "Chromium could not autofill any visible control. Focus a field in the intended card or address form and retry.", @@ -208,12 +210,14 @@ async function fillNativeLoginControls( ); } + /* oxlint-disable eslint/no-await-in-loop -- Login fields must be filled in DOM order so page validation sees coherent intermediate state. */ for (const { control, value } of fills) { const accepted = await fillNativeLoginControl(connection, control, value); if (!accepted) { throw new Error("The login form rejected secure credential autofill."); } } + /* oxlint-enable eslint/no-await-in-loop */ return fills.length; } @@ -552,6 +556,7 @@ async function withKernelPage( const iframeTargets = targetInfos.filter( ({ targetId, type }) => type === "iframe" && frameIds.has(targetId) ); + /* oxlint-disable eslint/no-await-in-loop -- CDP target attachment mutates one connection and session IDs are collected in target order. */ for (const iframeTarget of iframeTargets) { const attached = attachedTargetSchema.safeParse( await connection @@ -563,6 +568,7 @@ async function withKernelPage( ); if (attached.success) sessionIds.push(attached.data.sessionId); } + /* oxlint-enable eslint/no-await-in-loop */ return await operation({ connection, diff --git a/agent/subagents/worker/lib/autofill/tests/vault-autofill.test.ts b/agent/subagents/worker/lib/autofill/tests/vault-autofill.test.ts index 9dc3f28d..ad6053e9 100644 --- a/agent/subagents/worker/lib/autofill/tests/vault-autofill.test.ts +++ b/agent/subagents/worker/lib/autofill/tests/vault-autofill.test.ts @@ -309,9 +309,9 @@ describe("vault browser autofill", () => { it("lets a vault-owned adapter supply masked suggestions and claims", async () => { const adapter: AutofillVaultAdapter = { - async listSuggestions(_scope, origin, surface) { + async listSuggestions(_scope, origin, targetSurface) { expect(origin).toBe("https://merchant.example"); - expect(surface.kind).toBe("payment-card"); + expect(targetSurface.kind).toBe("payment-card"); return [ { candidateId: "opaque-card", diff --git a/agent/subagents/worker/lib/tests/vault-screenshot-mask.test.ts b/agent/subagents/worker/lib/tests/vault-screenshot-mask.test.ts index 5a8bec35..ed7966f0 100644 --- a/agent/subagents/worker/lib/tests/vault-screenshot-mask.test.ts +++ b/agent/subagents/worker/lib/tests/vault-screenshot-mask.test.ts @@ -1,4 +1,3 @@ -/* oxlint-disable vitest/require-mock-type-parameters -- The hoisted Kernel fake records cleanup request options. */ import { describe, expect, it, vi } from "vitest"; import { withVaultScreenshotMask } from "../vault-screenshot-mask"; diff --git a/agent/subagents/worker/tools/capture_browser_image.ts b/agent/subagents/worker/tools/capture_browser_image.ts index e85ea959..d3fe011e 100644 --- a/agent/subagents/worker/tools/capture_browser_image.ts +++ b/agent/subagents/worker/tools/capture_browser_image.ts @@ -160,6 +160,7 @@ async function captureBrowserImage( }; } } + throw new Error("Unsupported browser image source."); } async function captureImageResource( @@ -342,6 +343,7 @@ async function readBoundedResponse(response: Response) { const chunks: Uint8Array[] = []; let total = 0; try { + /* oxlint-disable eslint/no-await-in-loop -- A response body is an ordered stream and must be read and cancelled sequentially. */ for (;;) { const { done, value } = await reader.read(); if (done) break; @@ -352,6 +354,7 @@ async function readBoundedResponse(response: Response) { } chunks.push(value); } + /* oxlint-enable eslint/no-await-in-loop */ } finally { reader.releaseLock(); } diff --git a/agent/subagents/worker/tools/computer_action.ts b/agent/subagents/worker/tools/computer_action.ts index 5f98e3f3..2719b815 100644 --- a/agent/subagents/worker/tools/computer_action.ts +++ b/agent/subagents/worker/tools/computer_action.ts @@ -126,6 +126,7 @@ export default defineTool({ ); }; + /* oxlint-disable eslint/no-await-in-loop -- Computer actions must execute in user-specified order and batching is flushed at observation boundaries. */ for (const action of input.actions) { const batchAction = toBatchAction(action); if (batchAction) { @@ -184,6 +185,7 @@ export default defineTool({ throw new Error(`Computer action ${action.type} was not batched.`); } } + /* oxlint-enable eslint/no-await-in-loop */ await flushPendingActions(); return outputSchema.parse({ @@ -266,4 +268,5 @@ function toBatchAction( case "write_clipboard": return null; } + throw new Error("Unsupported computer action."); } diff --git a/agent/subagents/worker/tools/manage_browsers.ts b/agent/subagents/worker/tools/manage_browsers.ts index 46d48198..7e1e629d 100644 --- a/agent/subagents/worker/tools/manage_browsers.ts +++ b/agent/subagents/worker/tools/manage_browsers.ts @@ -183,6 +183,7 @@ const manageBrowsers = defineTool({ return "Browser session deleted successfully"; } } + throw new Error("Unsupported browser management action."); }, }); diff --git a/agent/tools/google_workspace_read.ts b/agent/tools/google_workspace_read.ts index df368740..012b307e 100644 --- a/agent/tools/google_workspace_read.ts +++ b/agent/tools/google_workspace_read.ts @@ -73,5 +73,6 @@ export default defineTool({ ...(await searchGoogleContacts(ctx, input.query, input.pageSize)), }; } + throw new Error("Unsupported Google Workspace read action."); }, }); diff --git a/agent/tools/google_workspace_write.ts b/agent/tools/google_workspace_write.ts index bee6f28b..b5c6694d 100644 --- a/agent/tools/google_workspace_write.ts +++ b/agent/tools/google_workspace_write.ts @@ -60,5 +60,6 @@ export default defineTool({ event: await createCalendarEvent(ctx, input), }; } + throw new Error("Unsupported Google Workspace write action."); }, }); diff --git a/db/services/browser-traces.ts b/db/services/browser-traces.ts index 6de1bb8e..794fe935 100644 --- a/db/services/browser-traces.ts +++ b/db/services/browser-traces.ts @@ -87,10 +87,11 @@ export async function listBrowserTraces(scope: AccessScope, cursor?: string) { return { nextCursor: rows.length > page.length && last ? encodeTraceCursor(last) : null, - traces: page.map((row) => ({ - ...row, - domains: domainsByTrace.get(row.sessionId) ?? [], - })), + traces: page.map((row) => + Object.assign({}, row, { + domains: domainsByTrace.get(row.sessionId) ?? [], + }) + ), }; } diff --git a/db/services/vault.ts b/db/services/vault.ts index 9ed0f3c0..dd60bd09 100644 --- a/db/services/vault.ts +++ b/db/services/vault.ts @@ -67,10 +67,11 @@ export async function readVaultItems(scope: AccessScope) { await ensureScope(scope); const records = await listVaultItems(scope); return Promise.all( - records.map(async (record) => ({ - ...record, - hasSecret: await hasVaultSecret(scope, record.id), - })) + records.map(async (record) => + Object.assign({}, record, { + hasSecret: await hasVaultSecret(scope, record.id), + }) + ) ); } @@ -160,6 +161,7 @@ function vaultAccountHint(input: VaultCreateItem) { case "contact": return ""; } + throw new Error("Unsupported vault item kind."); } function encryptVaultSecret( diff --git a/db/tests/database-migration.test.ts b/db/tests/database-migration.test.ts index 7629b934..2399670b 100644 --- a/db/tests/database-migration.test.ts +++ b/db/tests/database-migration.test.ts @@ -267,9 +267,11 @@ async function applyMigration(database: PGlite, name: string) { new URL(`../migrations/${name}`, import.meta.url), "utf8" ); + /* oxlint-disable eslint/no-await-in-loop -- SQL migration statements must execute in file order. */ for (const statement of migration.split("--> statement-breakpoint")) { if (statement.trim()) await database.exec(statement); } + /* oxlint-enable eslint/no-await-in-loop */ } async function pendingConstraintCount(database: PGlite) { diff --git a/db/tests/services.test.ts b/db/tests/services.test.ts index 736e04c3..65285ccd 100644 --- a/db/tests/services.test.ts +++ b/db/tests/services.test.ts @@ -137,8 +137,8 @@ describe("database services", () => { expect(await sessions.isSessionOwned(bob, "session-alice")).toBe(false); await sessions.claimSession(alice, "session-imessage"); - const unindexedChats = (await chats.listChats(alice)).sort((left, right) => - left.sessionId.localeCompare(right.sessionId) + const unindexedChats = (await chats.listChats(alice)).toSorted( + (left, right) => left.sessionId.localeCompare(right.sessionId) ); expect( unindexedChats.map(({ sessionId, title, usage }) => ({ @@ -355,9 +355,11 @@ async function applyInitialMigration(database: PGlite) { new URL("../migrations/0000_fluffy_the_spike.sql", import.meta.url), "utf8" ); + /* oxlint-disable eslint/no-await-in-loop -- SQL migration statements must execute in file order. */ for (const statement of migration.split("--> statement-breakpoint")) { if (statement.trim()) await database.exec(statement); } + /* oxlint-enable eslint/no-await-in-loop */ } async function applyBrowserImageMigration(database: PGlite) { @@ -365,9 +367,11 @@ async function applyBrowserImageMigration(database: PGlite) { new URL("../migrations/0003_unusual_fabian_cortez.sql", import.meta.url), "utf8" ); + /* oxlint-disable eslint/no-await-in-loop -- SQL migration statements must execute in file order. */ for (const statement of migration.split("--> statement-breakpoint")) { if (statement.trim()) await database.exec(statement); } + /* oxlint-enable eslint/no-await-in-loop */ } async function applyBrowserTraceMigration(database: PGlite) { @@ -375,9 +379,11 @@ async function applyBrowserTraceMigration(database: PGlite) { new URL("../migrations/0004_kind_manta.sql", import.meta.url), "utf8" ); + /* oxlint-disable eslint/no-await-in-loop -- SQL migration statements must execute in file order. */ for (const statement of migration.split("--> statement-breakpoint")) { if (statement.trim()) await database.exec(statement); } + /* oxlint-enable eslint/no-await-in-loop */ } async function applyBrowserTraceEventMigration(database: PGlite) { @@ -385,7 +391,9 @@ async function applyBrowserTraceEventMigration(database: PGlite) { new URL("../migrations/0005_brave_kang.sql", import.meta.url), "utf8" ); + /* oxlint-disable eslint/no-await-in-loop -- SQL migration statements must execute in file order. */ for (const statement of migration.split("--> statement-breakpoint")) { if (statement.trim()) await database.exec(statement); } + /* oxlint-enable eslint/no-await-in-loop */ } diff --git a/evals/browser/browser.eval.ts b/evals/browser/browser.eval.ts index 1972b41f..3e289b1c 100644 --- a/evals/browser/browser.eval.ts +++ b/evals/browser/browser.eval.ts @@ -23,6 +23,7 @@ export default browserBenchmarkTasks.flatMap((task) => let session: EveEvalSession | typeof t = t; let completed: EveEvalTurn | null = null; const workerEvents = [...started.events]; + /* oxlint-disable eslint/no-await-in-loop -- Each watch resumes from the stream index produced by the previous turn. */ for (let attempt = 0; attempt < 8 && completed === null; attempt += 1) { const live = t.target.watchTurn(started.sessionId, { startIndex: requireStreamIndex(session), @@ -33,6 +34,7 @@ export default browserBenchmarkTasks.flatMap((task) => if (didFinishWorker(workerEvents)) completed = turn; session = live.session; } + /* oxlint-enable eslint/no-await-in-loop */ await t.require( completed, diff --git a/package.json b/package.json index ba0142a9..ae305dc6 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "deps:check": "taze --no-github-actions --no-node-version --fail-on-outdated", "deploy": "turbo run deploy:app", "deploy:app": "eve deploy", - "dev": "node scripts/dev.mjs", + "dev": "node scripts/dev.ts", "dev:app": "next dev", "eval": "eve eval", "format": "oxfmt .", diff --git a/scripts/dev.mjs b/scripts/dev.ts similarity index 63% rename from scripts/dev.mjs rename to scripts/dev.ts index 6bbf4660..ee858a94 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.ts @@ -1,6 +1,5 @@ -import { spawn } from "node:child_process"; +import { spawn, type ChildProcess } from "node:child_process"; import { createHash } from "node:crypto"; -import { once } from "node:events"; import { fileURLToPath } from "node:url"; const repositoryRoot = fileURLToPath(new URL("..", import.meta.url)); @@ -8,7 +7,7 @@ const composeProject = `open-instinct-${createHash("sha256") .update(repositoryRoot) .digest("hex") .slice(0, 12)}`; -const composeArguments = (...args) => [ +const composeArguments = (...args: string[]) => [ "compose", "--project-name", composeProject, @@ -18,7 +17,7 @@ const composeArguments = (...args) => [ // oxlint-disable-next-line eslint/no-restricted-properties -- the development supervisor must forward the caller's environment to its child processes const inheritedEnvironment = { ...process.env }; -function developmentEnvironment(port) { +function developmentEnvironment(port: string) { const localDatabaseUrl = `postgresql://postgres:postgres@127.0.0.1:${port}/open_instinct`; return { ...inheritedEnvironment, @@ -32,35 +31,47 @@ async function resolvePostgresPort() { "docker", composeArguments("port", "postgres", "5432") ); - if (output === undefined) return; - const port = output.trim().match(/:(\d+)$/)?.[1]; + if (output === undefined) return undefined; + const port = /:(\d+)$/.exec(output.trim())?.[1]; if (!port) { throw new Error("Could not resolve the local PostgreSQL port."); } return port; } -let activeChild; +let activeChild: ChildProcess | undefined; let composeAttempted = false; -let shutdownSignal; +let shutdownSignal: NodeJS.Signals | undefined; -function interrupt(child, signal) { +function interrupt(child: ChildProcess, signal: NodeJS.Signals) { + const childPid = child.pid; try { if (process.platform === "win32") { child.kill(signal); return; } - process.kill(-child.pid, signal); + if (childPid === undefined) { + throw new Error( + "Cannot forward a signal before the child process starts." + ); + } + process.kill(-childPid, signal); } catch (error) { - if (error?.code !== "ESRCH") { - console.error(`Failed to forward ${signal} to ${child.pid}:`, error); + if ( + !(error instanceof Error && "code" in error && error.code === "ESRCH") + ) { + console.error( + `Failed to forward ${signal} to ${String(childPid)}:`, + error + ); process.exitCode = 1; } } } -for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { +const shutdownSignals: NodeJS.Signals[] = ["SIGINT", "SIGTERM", "SIGHUP"]; +for (const signal of shutdownSignals) { process.on(signal, () => { if (shutdownSignal === undefined) { shutdownSignal = signal; @@ -72,9 +83,15 @@ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { } async function run( - command, - args, - { allowInterruption = false, env = inheritedEnvironment } = {} + command: string, + args: string[], + { + allowInterruption = false, + env = inheritedEnvironment, + }: { + allowInterruption?: boolean; + env?: NodeJS.ProcessEnv; + } = {} ) { const child = spawn(command, args, { cwd: repositoryRoot, @@ -85,10 +102,12 @@ async function run( activeChild = child; try { - const [code] = await once(child, "exit"); + const code = await childExitCode(child); if (code !== 0 && !(allowInterruption && shutdownSignal !== undefined)) { - throw new Error(`${command} ${args.join(" ")} exited with ${code}`); + throw new Error( + `${command} ${args.join(" ")} exited with ${String(code)}` + ); } return code === 0 && shutdownSignal === undefined; @@ -99,7 +118,7 @@ async function run( } } -async function runForOutput(command, args) { +async function runForOutput(command: string, args: string[]) { const child = spawn(command, args, { cwd: repositoryRoot, detached: process.platform !== "win32", @@ -109,14 +128,16 @@ async function runForOutput(command, args) { activeChild = child; child.stdout.setEncoding("utf8"); let output = ""; - child.stdout.on("data", (chunk) => { + child.stdout.on("data", (chunk: string) => { output += chunk; }); try { - const [code] = await once(child, "exit"); + const code = await childExitCode(child); if (code !== 0 && shutdownSignal === undefined) { - throw new Error(`${command} ${args.join(" ")} exited with ${code}`); + throw new Error( + `${command} ${args.join(" ")} exited with ${String(code)}` + ); } return shutdownSignal === undefined ? output : undefined; } finally { @@ -126,6 +147,13 @@ async function runForOutput(command, args) { } } +function childExitCode(child: ChildProcess) { + return new Promise((resolve, reject) => { + child.once("error", reject); + child.once("exit", resolve); + }); +} + try { composeAttempted = true; let shouldContinue = await run( @@ -136,9 +164,7 @@ try { if (shouldContinue) { const port = await resolvePostgresPort(); - shouldContinue = port !== undefined; - - if (shouldContinue) { + if (port !== undefined) { const environment = developmentEnvironment(port); shouldContinue = await run("pnpm", ["db:migrate"], { allowInterruption: true, diff --git a/src/app/(authenticated)/(manager)/_components/google-workspace-action.tsx b/src/app/(authenticated)/(manager)/_components/google-workspace-action.tsx index 3d4e8904..bc53e3cf 100644 --- a/src/app/(authenticated)/(manager)/_components/google-workspace-action.tsx +++ b/src/app/(authenticated)/(manager)/_components/google-workspace-action.tsx @@ -9,8 +9,12 @@ export function GoogleWorkspaceAction({ readonly state?: "connected" | "disconnected" | "unavailable"; }) { const update = api.googleWorkspace.update.useMutation({ - onError: () => window.location.assign("/?google=unavailable"), - onSuccess: ({ redirectTo }) => window.location.assign(redirectTo), + onError: () => { + window.location.assign("/?google=unavailable"); + }, + onSuccess: ({ redirectTo }) => { + window.location.assign(redirectTo); + }, }); if (!state) { @@ -26,7 +30,9 @@ export function GoogleWorkspaceAction({ return ( @@ -61,7 +66,9 @@ export function VaultAddresses({ ) : ( <> section.setView("list")} + onClick={() => { + section.setView("list"); + }} title="Addresses" /> @@ -73,7 +80,9 @@ export function VaultAddresses({ section.setView("list")} + onSaved={() => { + section.setView("list"); + }} /> )} diff --git a/src/app/(authenticated)/(manager)/vault/_components/cards/form.tsx b/src/app/(authenticated)/(manager)/vault/_components/cards/form.tsx index 9291cc2d..ff5069b2 100644 --- a/src/app/(authenticated)/(manager)/vault/_components/cards/form.tsx +++ b/src/app/(authenticated)/(manager)/vault/_components/cards/form.tsx @@ -1,6 +1,6 @@ "use client"; -import { type FormEvent, useState } from "react"; +import { type SubmitEvent, useState } from "react"; import { useRouter } from "next/navigation"; import { z } from "zod"; import { Badge } from "@/components/ui/badge"; @@ -64,9 +64,11 @@ export function CardForm({ const cardType = paymentCardType(form.cardNumber); const result = paymentCardFormSchema.safeParse(form); const errors = - attempted && !result.success ? result.error.flatten().fieldErrors : {}; + attempted && !result.success + ? z.flattenError(result.error).fieldErrors + : {}; - const submit = (event: FormEvent) => { + const submit = (event: SubmitEvent) => { event.preventDefault(); setAttempted(true); if (!result.success) return; @@ -103,9 +105,9 @@ export function CardForm({ id="vault-payment-cardholder" label="Name on card" name="cc-name" - onChange={(cardholderName) => - setForm((current) => ({ ...current, cardholderName })) - } + onChange={(cardholderName) => { + setForm((current) => ({ ...current, cardholderName })); + }} value={form.cardholderName} /> - setForm((current) => ({ ...current, nickname })) - } + onChange={(nickname) => { + setForm((current) => ({ ...current, nickname })); + }} placeholder="Personal" value={form.nickname} /> @@ -130,12 +132,12 @@ export function CardForm({ label="Card number" maxLength={23} name="cc-number" - onChange={(value) => + onChange={(value) => { setForm((current) => ({ ...current, cardNumber: formatCardNumber(value), - })) - } + })); + }} placeholder="1234 5678 9012 3456" trailingLabel={cardType?.niceType} value={form.cardNumber} @@ -150,12 +152,12 @@ export function CardForm({ label="Expiration" maxLength={7} name="cc-exp" - onChange={(value) => + onChange={(value) => { setForm((current) => ({ ...current, expiration: formatExpiration(value), - })) - } + })); + }} placeholder="MM / YY" value={form.expiration} /> @@ -167,12 +169,12 @@ export function CardForm({ label="CVC" maxLength={4} name="cc-csc" - onChange={(value) => + onChange={(value) => { setForm((current) => ({ ...current, cvc: value.replaceAll(/\D/gu, "").slice(0, 4), - })) - } + })); + }} placeholder="123" value={form.cvc} /> @@ -184,9 +186,9 @@ export function CardForm({ label="Billing ZIP / postal" maxLength={20} name="postal-code" - onChange={(billingPostalCode) => - setForm((current) => ({ ...current, billingPostalCode })) - } + onChange={(billingPostalCode) => { + setForm((current) => ({ ...current, billingPostalCode })); + }} value={form.billingPostalCode} /> @@ -229,7 +231,9 @@ function CardField({ {...inputProps} aria-invalid={error ? true : undefined} id={id} - onChange={(event) => onChange(event.target.value)} + onChange={(event) => { + onChange(event.target.value); + }} /> diff --git a/src/app/(authenticated)/(manager)/vault/_components/cards/index.tsx b/src/app/(authenticated)/(manager)/vault/_components/cards/index.tsx index 3f5cdf61..629e7650 100644 --- a/src/app/(authenticated)/(manager)/vault/_components/cards/index.tsx +++ b/src/app/(authenticated)/(manager)/vault/_components/cards/index.tsx @@ -52,7 +52,12 @@ export function VaultCards({ title="Cards" />
- @@ -61,7 +66,9 @@ export function VaultCards({ ) : ( <> section.setView("list")} + onClick={() => { + section.setView("list"); + }} title="Cards" /> @@ -73,7 +80,9 @@ export function VaultCards({ section.setView("list")} + onSaved={() => { + section.setView("list"); + }} /> )} diff --git a/src/app/(authenticated)/(manager)/vault/_components/contacts/form.tsx b/src/app/(authenticated)/(manager)/vault/_components/contacts/form.tsx index 74f7a938..7de2d444 100644 --- a/src/app/(authenticated)/(manager)/vault/_components/contacts/form.tsx +++ b/src/app/(authenticated)/(manager)/vault/_components/contacts/form.tsx @@ -1,6 +1,6 @@ "use client"; -import { type FormEvent, useState } from "react"; +import { type SubmitEvent, useState } from "react"; import { useRouter } from "next/navigation"; import { z } from "zod"; import { Button } from "@/components/ui/button"; @@ -57,9 +57,11 @@ export function ContactForm({ }); const result = contactFormSchema.safeParse(form); const errors = - attempted && !result.success ? result.error.flatten().fieldErrors : {}; + attempted && !result.success + ? z.flattenError(result.error).fieldErrors + : {}; - const submit = (event: FormEvent) => { + const submit = (event: SubmitEvent) => { event.preventDefault(); setAttempted(true); if (!result.success) return; @@ -79,8 +81,9 @@ export function ContactForm({ }); }; - const update = (field: keyof typeof form, value: string) => + const update = (field: keyof typeof form, value: string) => { setForm((current) => ({ ...current, [field]: value })); + }; return ( @@ -89,7 +92,9 @@ export function ContactForm({ error={errors.nickname?.[0]} id="vault-contact-label" label="Name" - onChange={(value) => update("nickname", value)} + onChange={(value) => { + update("nickname", value); + }} placeholder="Checkout" value={form.nickname} /> @@ -98,7 +103,9 @@ export function ContactForm({ error={errors.fullName?.[0]} id="vault-contact-name" label="Full name (optional)" - onChange={(value) => update("fullName", value)} + onChange={(value) => { + update("fullName", value); + }} value={form.fullName} /> update("email", value)} + onChange={(value) => { + update("email", value); + }} type="email" value={form.email} /> @@ -115,7 +124,9 @@ export function ContactForm({ error={errors.phone?.[0]} id="vault-contact-phone" label="Phone (optional)" - onChange={(value) => update("phone", value)} + onChange={(value) => { + update("phone", value); + }} type="tel" value={form.phone} /> diff --git a/src/app/(authenticated)/(manager)/vault/_components/contacts/index.tsx b/src/app/(authenticated)/(manager)/vault/_components/contacts/index.tsx index 72a581e4..8328494f 100644 --- a/src/app/(authenticated)/(manager)/vault/_components/contacts/index.tsx +++ b/src/app/(authenticated)/(manager)/vault/_components/contacts/index.tsx @@ -52,7 +52,12 @@ export function VaultContacts({ title="Contact info" />
- @@ -61,7 +66,9 @@ export function VaultContacts({ ) : ( <> section.setView("list")} + onClick={() => { + section.setView("list"); + }} title="Contact info" /> @@ -73,7 +80,9 @@ export function VaultContacts({ section.setView("list")} + onSaved={() => { + section.setView("list"); + }} /> )} diff --git a/src/app/(authenticated)/(manager)/vault/_components/field.tsx b/src/app/(authenticated)/(manager)/vault/_components/field.tsx index 7fd81ffc..5ad34ad4 100644 --- a/src/app/(authenticated)/(manager)/vault/_components/field.tsx +++ b/src/app/(authenticated)/(manager)/vault/_components/field.tsx @@ -21,7 +21,9 @@ export function FormField({ {...inputProps} aria-invalid={error ? true : undefined} id={id} - onChange={(event) => onChange(event.target.value)} + onChange={(event) => { + onChange(event.target.value); + }} /> diff --git a/src/app/(authenticated)/(manager)/vault/_components/logins/form.tsx b/src/app/(authenticated)/(manager)/vault/_components/logins/form.tsx index 8a34e10b..3ad51a3e 100644 --- a/src/app/(authenticated)/(manager)/vault/_components/logins/form.tsx +++ b/src/app/(authenticated)/(manager)/vault/_components/logins/form.tsx @@ -1,6 +1,6 @@ "use client"; -import { type FormEvent, useState } from "react"; +import { type SubmitEvent, useState } from "react"; import { useRouter } from "next/navigation"; import { z } from "zod"; import { Button } from "@/components/ui/button"; @@ -84,9 +84,11 @@ export function LoginForm({ }); const result = loginFormSchema.safeParse(form); const errors = - attempted && !result.success ? result.error.flatten().fieldErrors : {}; + attempted && !result.success + ? z.flattenError(result.error).fieldErrors + : {}; - const submit = (event: FormEvent) => { + const submit = (event: SubmitEvent) => { event.preventDefault(); setAttempted(true); if (!result.success) return; @@ -119,9 +121,9 @@ export function LoginForm({ error={errors.nickname?.[0]} id="vault-login-label" label="Name" - onChange={(nickname) => - setForm((current) => ({ ...current, nickname })) - } + onChange={(nickname) => { + setForm((current) => ({ ...current, nickname })); + }} placeholder="GitHub" value={form.nickname} /> @@ -132,7 +134,9 @@ export function LoginForm({ id="vault-login-origin" inputMode="url" label="Website" - onChange={(origin) => setForm((current) => ({ ...current, origin }))} + onChange={(origin) => { + setForm((current) => ({ ...current, origin })); + }} placeholder="https://www.ubereats.com" type="url" value={form.origin} @@ -178,9 +182,9 @@ export function LoginForm({ error={errors.identifier?.[0]} id="vault-login-identifier" label={identifierLabel(form.identifierType)} - onChange={(identifier) => - setForm((current) => ({ ...current, identifier })) - } + onChange={(identifier) => { + setForm((current) => ({ ...current, identifier })); + }} placeholder={identifierPlaceholder(form.identifierType)} value={form.identifier} /> @@ -193,9 +197,9 @@ export function LoginForm({ error={errors.password?.[0]} id="vault-login-password" label={passwordOptional ? "Password (optional)" : "Password"} - onChange={(password) => - setForm((current) => ({ ...current, password })) - } + onChange={(password) => { + setForm((current) => ({ ...current, password })); + }} type="password" value={form.password} /> diff --git a/src/app/(authenticated)/(manager)/vault/_components/logins/import.tsx b/src/app/(authenticated)/(manager)/vault/_components/logins/import.tsx index 1b5c61fa..5264290c 100644 --- a/src/app/(authenticated)/(manager)/vault/_components/logins/import.tsx +++ b/src/app/(authenticated)/(manager)/vault/_components/logins/import.tsx @@ -113,6 +113,7 @@ export function ChromeImportPanel({ onDone }: { readonly onDone: () => void }) { nativeButton={false} render={
- @@ -74,11 +81,17 @@ export function VaultLogins({ ) : ( <> section.setView("list")} + onClick={() => { + section.setView("list"); + }} title="Logins" /> {section.view === "import" ? ( - section.setView("list")} /> + { + section.setView("list"); + }} + /> ) : ( <> @@ -93,7 +106,9 @@ export function VaultLogins({ initialIdentifierType={initialSetup?.identifierType} initialLabel={initialSetup?.label} initialOrigin={initialSetup?.origin} - onSaved={() => section.setView("list")} + onSaved={() => { + section.setView("list"); + }} /> )} diff --git a/src/app/(authenticated)/(manager)/vault/_components/section.tsx b/src/app/(authenticated)/(manager)/vault/_components/section.tsx index 40a1bd60..03b498bb 100644 --- a/src/app/(authenticated)/(manager)/vault/_components/section.tsx +++ b/src/app/(authenticated)/(manager)/vault/_components/section.tsx @@ -218,7 +218,9 @@ export function VaultItemList({ function VaultItemRow({ item }: { readonly item: VaultItem }) { const router = useRouter(); const remove = api.vault.remove.useMutation({ - onSuccess: () => router.refresh(), + onSuccess: () => { + router.refresh(); + }, }); return ( @@ -235,7 +237,9 @@ function VaultItemRow({ item }: { readonly item: VaultItem }) {