chore: migrate linting and formatting to Oxc

This commit is contained in:
2026-09-28 20:08:38 +09:00
parent 8085ad2f90
commit 0bab009984
27 changed files with 1550 additions and 1510 deletions
+1
View File
@@ -4,6 +4,7 @@
.env*
!.env.example
.output/
.tanstack/
dist/
node_modules/
playwright-report/
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"ignorePatterns": [".output/**", ".tanstack/**", "src/routeTree.gen.ts"]
}
+200 -3
View File
@@ -1,9 +1,206 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"categories": { "correctness": "off" },
"categories": {
"correctness": "error"
},
"options": {
"typeAware": true,
"typeCheck": true
},
"plugins": [
"eslint",
"typescript",
"unicorn",
"oxc",
"import",
"node",
"promise",
"react",
"react-perf",
"jsx-a11y"
],
"jsPlugins": ["@shadcn/lint"],
"rules": {
"shadcn/no-raw-colors": "error"
"no-console": "error",
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
],
"typescript/consistent-type-exports": "error",
"typescript/consistent-type-imports": [
"error",
{
"disallowTypeAnnotations": false
}
],
"typescript/no-non-null-assertion": "error",
"typescript/no-explicit-any": "error",
"unicorn/no-useless-spread": "error",
"jsx-a11y/no-autofocus": "error",
"jsx-a11y/no-static-element-interactions": "error",
"jsx-a11y/prefer-tag-over-role": "off",
"react/exhaustive-deps": "error",
"react/purity": "error",
"react/rules-of-hooks": "error",
"react/set-state-in-effect": "error",
"shadcn/no-arbitrary-values": [
"error",
{
"allow": ["layout"]
}
],
"shadcn/no-inline-styles": "error",
"shadcn/no-raw-colors": "error",
"shadcn/no-restyle": [
"error",
{
"allow": ["layout"],
"contracts": [
{
"pattern": "^Sidebar$",
"allow": [
"layout",
"border-r",
"transition-none",
"inbox-sidebar",
"inbox-list-panel",
"support-sidebar",
"support-list"
]
},
"ignorePatterns": ["src/routeTree.gen.ts"]
{
"pattern": "^SidebarHeader$",
"allow": ["layout", "py-4", "gap-4", "border-b", "p-5"]
},
{
"pattern": "^SidebarGroup$",
"allow": ["layout", "px-3"]
},
{
"pattern": "^SidebarFooter$",
"allow": ["layout", "px-3", "pb-4"]
},
{
"pattern": "^SidebarMenuButton$",
"allow": ["layout", "p-0"]
},
{
"pattern": "^Label$",
"allow": ["layout", "deck-checkbox"]
},
{
"pattern": "^Textarea$",
"allow": ["layout", "text-sm", "leading-7", "journal-summary"]
},
{
"pattern": "^MessageScroller$",
"allow": ["layout", "research-conversation"]
},
{
"pattern": "^MessageScrollerContent$",
"allow": ["layout", "gap-5", "p-4"]
},
{
"pattern": "^Marker$",
"allow": ["layout", "research-tool-message"]
},
{
"pattern": "^Message$",
"allow": ["layout", "research-message"]
},
{
"pattern": "^Collapsible$",
"allow": ["layout", "research-account-picker"]
},
{
"pattern": "^SidebarProvider$",
"allow": [
"layout",
"workspace",
"home-workspace",
"inbox-workspace",
"vital-workspace",
"support-workspace",
"journal-workspace"
]
},
{
"pattern": "^Button$",
"allow": ["layout", "auth-submit", "inbox-back", "support-back"]
},
{
"pattern": "^SidebarInset$",
"allow": ["layout", "home-main", "inbox-reader", "support-main", "journal-main"]
},
{
"pattern": "^Input$",
"allow": ["layout", "pl-9"]
},
{
"pattern": "^SidebarContent$",
"allow": ["layout", "gap-0"]
},
{
"pattern": "^Badge$",
"allow": ["layout", "deck-source-label"]
}
]
}
],
"shadcn/no-unknown-classes": "error",
"shadcn/require-static-classes": "error"
},
"overrides": [
{
"files": ["scripts/**"],
"rules": {
"no-console": "off"
}
},
{
"files": ["src/components/ui/**"],
"rules": {
"shadcn/no-arbitrary-values": "off",
"shadcn/no-restyle": "off",
"shadcn/require-static-classes": "off"
}
},
{
"files": ["src/**/*.{test,spec}.{ts,tsx}", "tests/**/*.{ts,tsx}"],
"plugins": ["vitest"],
"rules": {
"typescript/unbound-method": "error",
"vitest/expect-expect": "error",
"vitest/no-conditional-expect": "error",
"vitest/no-standalone-expect": "error",
"vitest/require-mock-type-parameters": "error"
}
},
{
"files": ["e2e/**/*.{ts,tsx}"],
"rules": {
"react/rules-of-hooks": "off"
}
}
],
"ignorePatterns": [
"**/.pnpm-store/**",
"**/.direnv/**",
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/coverage/**",
"**/playwright-report/**",
"**/test-results/**",
"**/.vercel/**",
"**/drizzle/**",
"**/*.tsbuildinfo",
".output/**",
".tanstack/**",
"src/routeTree.gen.ts",
"e2e/generated/**",
"src/features/storage/migrations.generated.ts"
]
}
+63 -29
View File
@@ -11,7 +11,7 @@ original language.
## Workspaces and current status
| Workspace | Route | Current implementation |
| --- | --- | --- |
| --------- | ---------- | ---------------------------------------------------------------------------------------------------------------------- |
| Home | `/` | State-derived daily brief, flexible tasks and routines, and natural-language action proposals. Local prototype. |
| Messages | `/support` | Sample conversations, editable replies, scheduling previews, and deferral. Sending is simulated. |
| Journal | `/journal` | Notes, resume points, medication and weight entries, append-only corrections, and summary previews. Local prototype. |
@@ -27,6 +27,21 @@ connections, and chat history use server-side persistence.
See [execution support](docs/execution-support.md) and the
[vitals preview](docs/vitals-preview.md) for prototype scope.
## URL state
Reader search and article selection, Messages views and conversations, Home
previews, account management, Journal review/correction views, and Vitals
navigation are represented in search parameters. Research uses deck, run, and
citation IDs. Reloading or browser Back/Forward restores the view while its
underlying data remains available. Login preserves the requested workspace URL.
Explicit selections add browser history; search input replaces it after a
300 ms debounce using TanStack Pacer. Navigation cancels pending search updates,
and IME composition waits until confirmed. URL navigation never submits prompts,
sends messages, or repeats device actions. Drafts, secrets, results, and task
contents remain outside the URL. Unsaved mock records and manual temporary
decks still disappear on reload; a URL does not persist their contents.
## Direction: personal context and execution
The intended connection with the separate private `life` / Rensheng repository
@@ -52,7 +67,7 @@ or change direction.
- Native X search syntax, Top/Latest ranking, and optional `filter:follows`
- List discovery for the account selected in the column editor
- Column editing, ordering, deletion/undo, refresh, and cursor pagination
- SQLite-backed shared decks, revision conflicts, and device-local selection
- SQLite-backed shared decks, revision conflicts, and URL-based selection
- Temporary views for exploration, with explicit saving
- Server-only encrypted Mastodon credentials and configurable Tailscale owner access
- Read-only post cards with source links, text, media, and quotes
@@ -106,7 +121,8 @@ nix develop -c pnpm dev
nix develop -c pnpm dev:tailscale
nix develop -c pnpm generate:e2e-openapi
nix develop -c pnpm lint
nix develop -c pnpm lint:ui
nix develop -c pnpm format
nix develop -c pnpm format:check
nix develop -c pnpm typecheck
nix develop -c pnpm knip
nix develop -c pnpm test
@@ -116,27 +132,37 @@ nix develop -c pnpm build
nix develop -c pnpm start
nix develop -c pnpm account:setup
nix develop -c pnpm db:generate
nix develop -c pnpm codex:serve
```
## Home Codex research prototype
Run `codex login` as the host user, then keep `pnpm codex:serve` running separately
from the web server. It starts a resident app-server at `ws://127.0.0.1:4500`
using that user's existing Codex login. The launcher passes a small environment
allowlist, without the app's database, relay or SNS secret settings. Configured
MCP servers are disabled for this process without changing the user's settings.
The prototype was developed against Codex CLI 0.156.1; its WebSocket and dynamic
tool APIs are experimental.
Run `codex login` as the host user. The backend uses Vercel AI SDK 7 and
`ai-sdk-provider-codex-cli` 2.3 to manage a local Codex app-server over stdio.
A separate WebSocket service is no longer required. Use Codex CLI 0.156.0 or
newer; set an explicit executable path if it is not on PATH.
The child receives an environment allowlist; inherited MCP servers, hooks,
web search and multi-agent execution are disabled for these research turns.
The user's Codex settings are not changed.
Configure the web app with these values in `.env.local` (absolute report path):
Configure the web app in `.env.local`:
```dotenv
TWITTER_LITE_CODEX_URL=ws://127.0.0.1:4500
TWITTER_LITE_CODEX_MODEL=gpt-6-astra
TWITTER_LITE_REPORT_ROOT=/absolute/path/to/twitter-lite/.data/research
# Optional:
# TWITTER_LITE_CODEX_PATH=/absolute/path/to/codex
```
This provider does not register AI SDK `tools` as Codex dynamic tools.
The existing validated, account-scoped research functions are exposed through
its in-process `createSdkMcpServer` bridge instead. Streaming uses `streamText`
and `smoothStream` with 15 ms pacing and Japanese-aware chunking:
`/[\u3040-\u309F\u30A0-\u30FF]|\S+\s+/`.
Streamdown renders the Markdown and animates new text only while the final
assistant message is live; completed history is immediate and reduced-motion
preferences are respected. `@shadcn/helpers/ai-sdk` supplies deterministic
streaming fixtures for renderer tests.
Use the chat on the left while browsing horizontally scrollable deck columns on
the right. Messages continue the same Codex thread and include the currently
open deck as context. The account selector controls which connections Codex can
@@ -152,15 +178,15 @@ Manual deck creation and column editing remain
available. The backend handles tools even when the browser is closed.
Use **New chat** to leave the current conversation and start a fresh Codex
thread with your next message. The open deck and host reports remain available.
If a turn is running, stop it first. The conversation reset is shared across
open devices. **Chat history** reopens saved conversations with their account
A running turn continues in the background when you open another chat.
Selection belongs to the current URL; another browser does not change it. **Chat history** reopens saved conversations with their account
selection, deck and citations; the next message resumes the same Codex thread.
SQLite stores conversation snapshots and the active conversation selection.
SQLite stores conversation snapshots. Active selection is not stored globally.
Each turn is limited to 6 columns, 12 upstream requests shared between list
discovery and post retrieval (at most 20 posts per fetch), and 15 minutes.
The prototype accepts up to 100 messages per backend process.
SSE pushes conversation messages, tool activity and generated deck updates to
open browsers, without polling.
SSE pushes messages, tool activity and deck updates for the requested conversation
ID, without polling. Separate conversations have independent executions.
Codex replies render as Markdown, including tables, lists and code. Clicking a
cited post link scrolls to its card and briefly highlights it. Loaded cards take
priority; otherwise the app shows the post captured during research in a column
@@ -183,12 +209,11 @@ not independently verified. Report contents remain on the host filesystem;
the app DB stores the conversation, thread ID, deck context and citation metadata.
Browser disconnection does not cancel research; reconnecting restores the latest
state through `/api/research/events`, under the same owner access checks as the
state through `/api/research/events?run=<id>`, under the same owner access checks as the
rest of the app. After a backend restart, unfinished turns appear as interrupted;
sending another message resumes the same thread. Before starting that message,
the backend checks the resumed thread and interrupts any orphaned active turn.
sending another message explicitly resumes the same thread through the provider.
It does not automatically rerun interrupted requests. Files already written remain.
Cancellation is explicit. The launcher and report directory must
Cancellation is explicit. The backend and report directory must
run under a user able to write those files. NixOS DynamicUser service integration
and dedicated service credentials are outside this prototype.
@@ -216,22 +241,31 @@ editing, ordering, and deletion; refresh stays available directly in each header
Shared UI components live in `src/components/ui`; `components.json` configures
the registry and theme entrypoint. Chat uses MessageScroller, Message, Bubble,
Marker and InputGroup while retaining the existing Codex transport and saved
conversation model. The icon rail switches between chat and deck management;
Marker and InputGroup with Streamdown, reconnectable snapshot SSE, and saved
conversations. The icon rail switches between chat and deck management;
closing the mobile sheet or crossing the desktop breakpoint preserves chat
drafts, selected accounts and live deck updates. Theme colors are declared in
`src/ui.css`.
`src/ui.css`. The shared entrypoint `src/styles.css` loads the theme, tokens,
base styles and feature styles in that order, so the UI lint rules can resolve
application classes through the same stylesheet used by the app.
`pnpm lint` runs Biome and the UI checks. `pnpm lint:ui` runs
[@shadcn/lint](https://github.com/shadcn-ui/lint) through Oxlint. The
`pnpm lint` runs Oxlint with type-aware linting and TypeScript checking through
`oxlint-tsgolint`, including the
[@shadcn/lint](https://github.com/shadcn-ui/lint) UI checks. The
`shadcn/no-raw-colors` rule in `.oxlintrc.json` requires theme colors in JSX and
class helpers, including shared UI components. Layout values remain available
for the sidebar and deck. Biome continues to handle formatting and general
code checks. Knip checks application usage while retaining the public exports
for the sidebar and deck. Component contracts allow only the existing workspace
styles; Next.js-specific rules are omitted because this app uses TanStack Start.
`pnpm format` formats files with Oxfmt defaults, and
`pnpm format:check` checks formatting without changing files. Knip checks application usage while retaining the public exports
of registry components for composition. Additional design rules can be configured
in `.oxlintrc.json`;
see the [available rules](https://github.com/shadcn-ui/lint#rules).
The database and OpenAPI generators format their output with Oxfmt. TanStack
Router owns the formatting of `src/routeTree.gen.ts`, which is excluded from
formatting; `pnpm check:routes` verifies that regeneration leaves it unchanged.
Create a deck for an investigation, then add columns for each perspective.
Choose a connected account and one of its supported sources. Account
changes affect only the edited column. Matching source conditions and accounts
-32
View File
@@ -1,32 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
"files": {
"includes": [
"**",
"!src/routeTree.gen.ts",
"!!**/.output",
"!!**/test-results"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "asNeeded"
}
},
"css": {
"parser": {
"tailwindDirectives": true
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
"tsx": true,
"tailwind": {
"config": "",
"css": "src/ui.css",
"css": "src/styles.css",
"baseColor": "neutral",
"cssVariables": true
},
@@ -35,7 +35,7 @@ flowchart LR
SQLiteを採用する案。端末が増えても、各端末がSQLを実行するのではなく、同じアプリサーバーへアクセスする。デッキ設定とOAuth情報の小さな更新には単一サーバーのSQLiteで始められると判断する。DBファイルを端末間コピーしたり、NAS上のファイルを複数サーバーから直接開いたりしない。
| 候補 | 今回の評価 |
| --- | --- |
| -------------------- | ------------------------------------------------------------------------------------------------------------ |
| SQLite | 推奨。別DBサービス不要。ローカルディスク、短いトランザクション、マイグレーション、復元試験を用意する |
| PostgreSQL | 複数アプリサーバーや大量の並列収集へ進む時に再評価。現時点では運用対象が増える |
| ブラウザDB+同期基盤 | 採用しない。今回必要なのはオンラインで同じサーバー状態を読むこと。オフライン同期エンジンを導入する必要はない |
@@ -53,7 +53,7 @@ OAuth callbackは固定のHTTPS URLにする。認可後に戻る主体はブラ
## 保存モデル案
| 保存対象 | 主な内容 |
| --- | --- |
| ------------------------ | ---------------------------------------------------------------------------------------- |
| `decks` | ID、名前、revision、更新日時 |
| `deck_columns` | ID、deck ID、並び順、connection ID、名前、platform固有のsource JSON |
| `connections` | ID、platform、接続先origin、外部アカウントIDまたはrelay profile参照、表示名、接続状態 |
@@ -70,7 +70,7 @@ DBには秘密を暗号化して保存し、暗号鍵はDB外の実行時credent
## PR単位のタスク
| ID | タスク | 依存 | 完了条件 |
| --- | --- | --- | --- |
| --- | -------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| T0 | 実行環境とアクセス経路を固定 | なし | 本番HTTPS origin/callback候補を決め、PC・スマホから同じ利用者として接続。Serve以外からのヘッダー偽装を許さない構成を確認。対象Mastodonのバージョン・OAuthメタデータも調べる |
| T1 | SQLiteと永続ディレクトリを導入 | T0 | 現行Node/Nixで動くdriver・migration方式を実証。`StateDirectory`等でDBを永続化。再起動・アプリ更新後も残り、バックアップから復元できる。依存追加時はflakeのpnpm hash更新まで行う |
| T2 | connectionモデルへTwitterを移行 | T1 | relay profile一覧から接続を作り、カラム・取得・キャッシュ・ページ送りがconnection IDを使う。2つのTwitter接続を混ぜずに並列表示。不明・削除済みの接続はエラーとして残す |
+4
View File
@@ -1,5 +1,9 @@
# shadcn workspace migration
> Tooling update: Biome has since been replaced by Oxlint for linting (including
> the shadcn rules) and Oxfmt for formatting. The checks recorded below describe
> the tooling used at the time of this migration; see README for current commands.
## Target
Replace the custom workspace UI with the Base UI version of shadcn sidebar-09
+1 -1
View File
@@ -10,7 +10,7 @@ tailnet HTTPS address after Mastodon authorization.
## Runtime configuration
| Variable | Value |
| --- | --- |
| ---------------------------------- | --------------------------------------------------------- |
| `TWITTER_LITE_ORIGIN` | Exact Serve HTTPS origin, without a trailing slash |
| `TWITTER_LITE_ALLOWED_LOGIN` | Owner's Tailscale login |
| `TWITTER_LITE_DB_PATH` | Absolute path to the SQLite database on local disk |
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -89,14 +89,14 @@ The server input has two valid states:
```ts
type InitialThreadInput = {
tweetId: string
}
tweetId: string;
};
type ContinuedThreadInput = {
tweetId: string
conversationId: string
cursor: string
}
tweetId: string;
conversationId: string;
cursor: string;
};
```
All IDs must be decimal strings. Cursor values are opaque non-empty strings.
@@ -106,11 +106,11 @@ The successful page shape is:
```ts
type ThreadPage = {
tweets: Post[]
focalPost?: Post
conversationId: string
nextCursor?: string
}
tweets: Post[];
focalPost?: Post;
conversationId: string;
nextCursor?: string;
};
```
`focalPost` is present only on the initial page. All returned posts pass through
@@ -149,7 +149,10 @@ continuation requests.
Extend the existing feed request union with:
```ts
{ kind: 'thread'; tweetId: string }
{
kind: "thread";
tweetId: string;
}
```
The thread query key includes the focal tweet ID. Its initial page parameter is
@@ -68,16 +68,16 @@ Work spans two repositories and the Gitea package registry:
Bird currently hard-codes `product: 'Latest'` in SearchTimeline requests. Add the following public contract:
```ts
export type SearchProduct = 'Top' | 'Latest'
export type SearchProduct = "Top" | "Latest";
export interface SearchFetchOptions {
includeRaw?: boolean
product?: SearchProduct
includeRaw?: boolean;
product?: SearchProduct;
}
export interface SearchPaginationOptions extends SearchFetchOptions {
maxPages?: number
cursor?: string
maxPages?: number;
cursor?: string;
}
```
@@ -126,7 +126,7 @@ new TwitterClient({
relayBaseUrl: process.env.TWITTER_RELAY_BASE_URL,
profileName: process.env.BIRD_PROFILE_NAME,
timeoutMs: 20_000,
})
});
```
`TWITTER_RELAY_BASE_URL` is required. `BIRD_PROFILE_NAME` is optional but must be passed explicitly because the Bird library does not read that environment variable itself.
+5 -5
View File
@@ -1,7 +1,7 @@
import { defineConfig } from 'drizzle-kit'
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: 'sqlite',
schema: './src/features/storage/schema.ts',
out: './drizzle',
})
dialect: "sqlite",
schema: "./src/features/storage/schema.ts",
out: "./drizzle",
});
+1 -1
View File
@@ -22,7 +22,7 @@
inherit (finalAttrs) pname version src;
pnpm = pkgs.pnpm_11;
fetcherVersion = 4;
hash = "sha256-+DqNt+58TrI0W8SU//JNdYYJtCdKNIG7gfWRxno36QQ=";
hash = "sha256-60nQAoDWoO15P7TFLhABqGNYpJy/R+w+Zz3oo7qD8dM=";
};
nativeBuildInputs = with pkgs; [
+3 -3
View File
@@ -1,6 +1,6 @@
import type { KnipConfig } from 'knip'
import type { KnipConfig } from "knip";
export default {
entry: ['playwright.config.ts', 'e2e/**/*.test.ts', 'e2e/mock-relay.mjs'],
entry: ["playwright.config.ts", "e2e/**/*.test.ts", "e2e/mock-relay.mjs"],
playwright: false,
} satisfies KnipConfig
} satisfies KnipConfig;
+30 -33
View File
@@ -1,64 +1,61 @@
import { defineConfig, defineTransformer } from 'orval'
import { defineConfig, defineTransformer } from "orval";
const selectedTag = 'twitter-lite-e2e'
const selectedTag = "twitter-lite-e2e";
const selectedOperationIds = new Set([
'getUserByScreenName',
'getUserTweets',
'getTweetDetail',
'getSearchTimeline',
'getListLatestTweetsTimeline',
])
"getUserByScreenName",
"getUserTweets",
"getTweetDetail",
"getSearchTimeline",
"getListLatestTweetsTimeline",
]);
const selectE2eOperations = defineTransformer((document) => {
const foundOperationIds = new Set<string>()
const foundOperationIds = new Set<string>();
for (const pathItem of Object.values(document.paths ?? {})) {
if (!pathItem || typeof pathItem !== 'object') continue
if (!pathItem || typeof pathItem !== "object") continue;
for (const candidate of Object.values(pathItem)) {
if (
!candidate ||
typeof candidate !== 'object' ||
typeof candidate !== "object" ||
Array.isArray(candidate) ||
!('operationId' in candidate)
!("operationId" in candidate)
) {
continue
continue;
}
const operation = candidate as {
operationId?: string
tags?: string[]
}
operationId?: string;
tags?: string[];
};
if (
operation.operationId &&
selectedOperationIds.has(operation.operationId)
) {
foundOperationIds.add(operation.operationId)
operation.tags = [...(operation.tags ?? []), selectedTag]
if (operation.operationId && selectedOperationIds.has(operation.operationId)) {
foundOperationIds.add(operation.operationId);
operation.tags = [...(operation.tags ?? []), selectedTag];
}
}
}
const missingOperationIds = [...selectedOperationIds].filter(
(operationId) => !foundOperationIds.has(operationId),
)
);
if (missingOperationIds.length > 0) {
throw new Error(
`twitter-openapi is missing selected operations: ${missingOperationIds.join(', ')}`,
)
`twitter-openapi is missing selected operations: ${missingOperationIds.join(", ")}`,
);
}
return document
})
return document;
});
export default defineConfig({
twitterOpenApi: {
input: {
target:
'https://raw.githubusercontent.com/fa0311/twitter-openapi/590dae5c9f8575abc91d3774946bfe6f23960aba/dist/docs/openapi-3.0.yaml',
"https://raw.githubusercontent.com/fa0311/twitter-openapi/590dae5c9f8575abc91d3774946bfe6f23960aba/dist/docs/openapi-3.0.yaml",
filters: {
mode: 'include',
mode: "include",
tags: [selectedTag],
},
override: {
@@ -67,9 +64,9 @@ export default defineConfig({
},
output: {
clean: true,
client: 'fetch',
mode: 'single',
target: './e2e/generated/twitter-openapi/client.ts',
client: "fetch",
mode: "single",
target: "./e2e/generated/twitter-openapi/client.ts",
},
},
})
});
+5 -9
View File
@@ -1,13 +1,9 @@
import { getSetupToken, readAuthState } from '../src/features/auth/auth.server'
import { getSetupToken, readAuthState } from "../src/features/auth/auth.server";
if (!readAuthState().needsSetup) {
console.error(
'This workspace already has an owner. Sign in with your existing account.',
)
process.exitCode = 1
console.error("This workspace already has an owner. Sign in with your existing account.");
process.exitCode = 1;
} else {
console.log(
'Open /setup in your workspace and enter this one-time setup code:',
)
console.log(getSetupToken())
console.log("Open /setup in your workspace and enter this one-time setup code:");
console.log(getSetupToken());
}
+7 -9
View File
@@ -1,13 +1,11 @@
import { backupDatabase } from '../src/features/storage/backup.server'
import { backupDatabase } from "../src/features/storage/backup.server";
const source = process.env.TWITTER_LITE_DB_PATH
const destination = process.argv[2]
const source = process.env.TWITTER_LITE_DB_PATH;
const destination = process.argv[2];
if (!source || !destination || process.argv.length !== 3) {
console.error(
'Usage: TWITTER_LITE_DB_PATH=/absolute/app.db pnpm db:backup /absolute/backup.db',
)
process.exitCode = 1
console.error("Usage: TWITTER_LITE_DB_PATH=/absolute/app.db pnpm db:backup /absolute/backup.db");
process.exitCode = 1;
} else {
await backupDatabase(source, destination)
console.log(`Backup verified: ${destination}`)
await backupDatabase(source, destination);
console.log(`Backup verified: ${destination}`);
}
+5 -5
View File
@@ -1,13 +1,13 @@
import { build } from 'vite'
import { build } from "vite";
for (const entry of ['backup-database', 'account-setup']) {
for (const entry of ["backup-database", "account-setup"]) {
await build({
configFile: false,
build: {
ssr: `scripts/${entry}.ts`,
outDir: '.output/server/tools',
outDir: ".output/server/tools",
emptyOutDir: false,
rollupOptions: { external: ['better-sqlite3'] },
rollupOptions: { external: ["better-sqlite3"] },
},
})
});
}
+6 -6
View File
@@ -1,10 +1,10 @@
import { writeFileSync } from 'node:fs'
import { readMigrationFiles } from 'drizzle-orm/migrator'
import { writeFileSync } from "node:fs";
import { readMigrationFiles } from "drizzle-orm/migrator";
const migrations = readMigrationFiles({
migrationsFolder: new URL('../drizzle', import.meta.url).pathname,
})
migrationsFolder: new URL("../drizzle", import.meta.url).pathname,
});
writeFileSync(
new URL('../src/features/storage/migrations.generated.ts', import.meta.url),
new URL("../src/features/storage/migrations.generated.ts", import.meta.url),
`// Generated by pnpm db:generate. Do not edit.\nexport const migrations = ${JSON.stringify(migrations, null, 2)}\n`,
)
);
+4 -4
View File
@@ -1,9 +1,9 @@
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { createServer } from 'vite'
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import { createServer } from "vite";
const server = await createServer({
configFile: false,
plugins: [tanstackStart()],
server: { middlewareMode: true },
})
await server.close()
});
await server.close();
+12 -17
View File
@@ -1,28 +1,23 @@
// @vitest-environment node
import { TwitterClient } from '@yuta/bird'
import { describe, expect, it } from 'vitest'
import { TwitterClient } from "@yuta/bird";
import { describe, expect, it } from "vitest";
describe.runIf(process.env.TWITTER_LITE_LIVE === '1')(
'configured relay',
() => {
it('performs one read-only Top search', async () => {
const relayBaseUrl = process.env.TWITTER_RELAY_BASE_URL
describe.runIf(process.env.TWITTER_LITE_LIVE === "1")("configured relay", () => {
it("performs one read-only Top search", async () => {
const relayBaseUrl = process.env.TWITTER_RELAY_BASE_URL;
if (!relayBaseUrl) {
throw new Error(
'TWITTER_RELAY_BASE_URL is required for the opt-in live smoke test',
)
throw new Error("TWITTER_RELAY_BASE_URL is required for the opt-in live smoke test");
}
const client = new TwitterClient({
relayBaseUrl,
profileName: process.env.BIRD_PROFILE_NAME,
timeoutMs: 20_000,
})
const query = process.env.TWITTER_LITE_LIVE_QUERY ?? 'OpenAI'
const result = await client.search(query, 1, { product: 'Top' })
});
const query = process.env.TWITTER_LITE_LIVE_QUERY ?? "OpenAI";
const result = await client.search(query, 1, { product: "Top" });
expect(result.success).toBe(true)
}, 30_000)
},
)
expect(result.success).toBe(true);
}, 30_000);
});
+4 -4
View File
@@ -1,5 +1,5 @@
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
afterEach(cleanup)
afterEach(cleanup);
+1 -2
View File
@@ -19,8 +19,7 @@
--color-shadow: oklch(10% 0.015 250 / 45%);
--color-overlay: oklch(10% 0.015 250 / 70%);
--font-body:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans",
"Noto Sans JP", sans-serif;
-apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", "Noto Sans JP", sans-serif;
--font-display: var(--font-body);
--font-mono: ui-monospace, "SFMono-Regular", Consolas, monospace;
--space-3xs: 0.125rem;
+6 -6
View File
@@ -1,9 +1,9 @@
import tailwindcss from '@tailwindcss/vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import { nitro } from 'nitro/vite'
import { defineConfig } from 'vite'
import tailwindcss from "@tailwindcss/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { nitro } from "nitro/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss(), nitro(), tanstackStart(), viteReact()],
})
});
+7 -7
View File
@@ -1,11 +1,11 @@
import path from 'node:path'
import { configDefaults, defineConfig } from 'vitest/config'
import path from "node:path";
import { configDefaults, defineConfig } from "vitest/config";
export default defineConfig({
resolve: { alias: { '#': path.resolve(import.meta.dirname, 'src') } },
resolve: { alias: { "#": path.resolve(import.meta.dirname, "src") } },
test: {
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
exclude: [...configDefaults.exclude, 'e2e/**'],
environment: "jsdom",
setupFiles: ["./tests/setup.ts"],
exclude: [...configDefaults.exclude, "e2e/**"],
},
})
});