diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2f3b5f5 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +CODEX_SWITCH_CODEX_HOME= +CODEX_SWITCH_VAULT_ROOT= +CODEX_SWITCH_CODEX_BIN= +CODEX_SWITCH_SKIP_LAUNCH= +CODEX_SWITCH_QUOTA_TTL_MS= +CODEX_SWITCH_LOG_LEVEL= +CODEX_HOME= +OPENAI_API_KEY= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..38fa956 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + validate: + name: Validate (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.33.1 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm typecheck + + - name: Test + run: pnpm test + + - name: Lint + run: pnpm lint + + - name: Build + run: pnpm build + + - name: Pack dry run + run: npm pack --dry-run diff --git a/.gitignore b/.gitignore index c02cfcf..0b9a940 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist/ out/ coverage/ .turbo/ +output/ .DS_Store *.log *.sqlite @@ -12,6 +13,6 @@ coverage/ *.tgz .env .env.* +!.env.example .codex-switch/ -packages/dashboard/.next/ plans/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f829df..0f6ea10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Changelog -## 0.1.0 +## 1.0.7 -- Added the pnpm workspace with shared, CLI, and dashboard packages -- Added vault-backed auth switching, TUI picker, log ingestion, and local dashboard flows -- Added standalone packaging, publish bins, and repo validation scripts -- Fixed session lock exclusivity, atomic auth replacement, watcher attribution, local day buckets, and add-page poll cancellation +- Added the pnpm workspace with shared and CLI packages. +- Added vault-backed auth switching, TUI picker, and CLI quota flows. +- Added deterministic `cs ls` quota bars for 5h and 7d limits. +- Removed the local browser UI package, browser launch command, browser dependencies, standalone UI packaging, stale screenshot artifacts, and unused session-history code. +- Added publish bins and repo validation scripts. +- Fixed session lock exclusivity and atomic auth replacement. diff --git a/README.md b/README.md index 27afa14..551c416 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # codex-switch -`codex-switch` keeps native Codex behavior intact while letting you switch between multiple OAuth accounts. +`codex-switch` keeps native Codex behavior intact while letting you switch between multiple OAuth accounts from the terminal. It does not proxy API traffic and it does not split `~/.codex/` into per-account homes. Instead it snapshots each account's `auth.json` into a vault at `~/.codex-switch/accounts/` and swaps the active snapshot into `~/.codex/auth.json` before launching Codex. ## What ships -- `cs` / `codex-switch`: CLI wrapper for add, use, run, sync, remove, dashboard launch -- Next.js dashboard on localhost for list, switch, history, and add-account flow +- `cs` / `codex-switch`: CLI wrapper for add, use, run, sync, remove, current, and list +- bare `cs` TUI account picker - local SQLite state at `~/.codex-switch/state.sqlite` -- read-only session ingestion plus on-demand quota probing with a 2-minute cache +- on-demand quota probing with a 2-minute cache +- terminal quota bars in `cs ls` ## Requirements @@ -38,10 +39,22 @@ Use an account: pnpm --filter @codex-switch/cli exec node dist/index.js use personal ``` -Launch the dashboard: +List accounts and cached quota: ```bash -pnpm --filter @codex-switch/cli exec node dist/index.js dash +pnpm --filter @codex-switch/cli exec node dist/index.js ls +``` + +Force-refresh cached quota: + +```bash +pnpm --filter @codex-switch/cli exec node dist/index.js ls --refresh +``` + +Machine-readable list output: + +```bash +pnpm --filter @codex-switch/cli exec node dist/index.js ls --json ``` ## Global install flow @@ -52,15 +65,24 @@ After packaging, the intended user flow is: npm i -g codex-switch cs add --name personal cs use personal -cs dash +cs ls --refresh ``` -Force-refresh cached quota: +## Quota Display -```bash -pnpm --filter @codex-switch/cli exec node dist/index.js ls --refresh +`cs ls` prints a three-column quota table: + +```text +╭───────────────────────────────┬──────────────────────────────┬──────────────────────────────────────╮ +│ Account (1) │ 5h Limit │ Weekly Limit │ +├───────────────────────────────┼──────────────────────────────┼──────────────────────────────────────┤ +│ * personal │ [████████░░░░░░░░░░░░] 42% │ [█████████████████░░░] 84% │ +│ (Pro / user@example.com) │ (resets 16:52) │ (resets 13:04 on 29 Apr) │ +╰───────────────────────────────┴──────────────────────────────┴──────────────────────────────────────╯ ``` +The display uses a monochrome ink palette: bold for emphasis and dim text for secondary/used quota cells. Columns are auto-aligned by visible terminal width, ignoring ANSI style codes. Bars clamp to 0-100%; `█` means quota left and `░` means quota used/unavailable. Unknown quota renders as a fully shaded bar with `--`. Re-auth and stale states are shown in the account cell instead of fake quota. + ## Recommended shell aliases PowerShell: @@ -92,16 +114,10 @@ alias codex='cs run' ## Troubleshooting - `Could not locate the bindings file`: - Run `pnpm install` again after allowing native builds for `better-sqlite3` and `sharp`. + Run `pnpm install` again after allowing native builds for `better-sqlite3`. - `No active account recorded`: Add or use an account first. - `Close the running Codex session first`: Another `cs use` / `cs run` process still owns the runtime lock. - No quota data yet: - Quota is now fetched on demand from ChatGPT when the dashboard or `cs ls --refresh` asks for it. If refresh fails, the UI shows the last cached value or a re-auth banner instead of parsing Codex logs. - -## Developer utilities - -```bash -pnpm inspect:codex-logs -``` + Quota is fetched on demand from ChatGPT when `cs ls --refresh` asks for it. If refresh fails, the CLI shows the last cached value or a re-auth status. diff --git a/docs/code-standards.md b/docs/code-standards.md index f407d5e..d289943 100644 --- a/docs/code-standards.md +++ b/docs/code-standards.md @@ -3,8 +3,8 @@ ## Monorepo Boundaries - Keep shared filesystem paths, SQL schema, and cross-package types in `packages/shared`. -- Keep auth mutation, vault writes, swapping, lock handling, and process launching in `packages/cli`. -- Keep dashboard UI and local HTTP routes in `packages/dashboard`. +- Keep auth mutation, vault writes, swapping, lock handling, process launching, and CLI display in `packages/cli`. +- Do not reintroduce browser UI or local HTTP routes without a new plan. ## TypeScript and Module Rules @@ -19,7 +19,6 @@ - `pnpm typecheck` - `pnpm lint` - `pnpm test` -- `pnpm inspect:codex-logs` Run validation after code edits that affect runtime behavior. @@ -28,18 +27,15 @@ Run validation after code edits that affect runtime behavior. - Commands are registered in `packages/cli/src/index.ts`. - Mutating commands should flow through the same swap/vault helpers rather than duplicate auth logic. - Bare `cs` should remain a shortcut to the interactive picker. - -## Dashboard Conventions - -- App routes live under `packages/dashboard/app`. -- Read-only database access stays in `packages/dashboard/lib/db.ts`. -- Mutation routes should guard against cross-origin requests and call the CLI rather than reimplement auth changes inside the dashboard. +- Keep `cs ls --json` stable for scripts. +- Put deterministic terminal formatting in small helpers with focused tests. +- Use ASCII for quota bars so output is width-stable across terminals. ## SQLite Rules -- `state.sqlite` is the source of truth for dashboard reads. -- Writes go through the CLI-side DB helpers. -- Dashboard DB access must stay read-only with `query_only`. +- `state.sqlite` is the source of truth for local account and quota state. +- Writes go through CLI-side DB helpers. +- Read-only paths should open SQLite with readonly/query-only options when practical. ## File Safety Rules @@ -59,20 +55,15 @@ Run validation after code edits that affect runtime behavior. - `CODEX_SWITCH_VAULT_ROOT` - `CODEX_SWITCH_CODEX_BIN` - `CODEX_SWITCH_SKIP_LAUNCH` -- `CODEX_SWITCH_NO_OPEN` -- `CODEX_SWITCH_FORCE_DASH_BUILD` - `CODEX_SWITCH_QUOTA_TTL_MS` -- `CODEX_SWITCH_POLL_DISABLED` - `CODEX_SWITCH_LOG_LEVEL` -- `CS_CLI_ENTRY` -- `CS_NODE_BINARY` ## Generated Artifact Policy - Publish bins from `bin/`. - Build CLI and shared packages into `dist/`. -- Keep dashboard standalone output under `.next/standalone`. -- `prepack` must rebuild and copy dashboard static/public assets into the standalone tree. +- `prepack` must rebuild shared before CLI. +- Package files should not include removed UI artifacts. ## Documentation Sync Rules diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index 998312c..cff290f 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -2,18 +2,16 @@ ## Repo Shape -- `packages/shared`: shared types, paths, SQL schema, SQL queries -- `packages/cli`: commander-based CLI, vault logic, swap flow, TUI, watcher -- `packages/dashboard`: Next.js app, local API routes, charts, add/history pages +- `packages/shared`: shared types, paths, SQL schema +- `packages/cli`: commander-based CLI, vault logic, swap flow, TUI, quota display - `bin`: publish-time bin shims - `scripts`: packaging helpers - `plans`: brainstorm + implementation plan artifacts ## Package Summary -- `@codex-switch/shared`: exports path helpers for `~/.codex` and `~/.codex-switch`, plus SQLite schema and query strings. -- `@codex-switch/cli`: owns account add/use/run/sync/remove flows, session locking, atomic writes, and the dashboard launcher. -- `@codex-switch/dashboard`: reads the shared SQLite state read-only and exposes a local dashboard plus local mutation routes that shell out to the CLI. +- `@codex-switch/shared`: exports path helpers for `~/.codex` and `~/.codex-switch`, plus SQLite schema and shared runtime types. +- `@codex-switch/cli`: owns account add/use/run/sync/remove/current/list flows, session locking, atomic writes, quota cache/probe orchestration, and terminal quota formatting. ## Primary Commands @@ -21,18 +19,14 @@ - `pnpm typecheck` - `pnpm lint` - `pnpm test` -- `pnpm inspect:codex-logs` ## Key Data Stores - `~/.codex/auth.json`: active live auth used by native Codex -- `~/.codex/history.jsonl`: session history source -- `~/.codex/logs_2.sqlite`: structured log source - `~/.codex-switch/accounts/*.json`: vault snapshots -- `~/.codex-switch/state.sqlite`: account/session/quota state +- `~/.codex-switch/state.sqlite`: account and quota state ## Main Runtime Paths - CLI entry: `packages/cli/dist/index.js` -- Dashboard standalone server: `packages/dashboard/.next/standalone/packages/dashboard/server.js` - Publish bins: `bin/codex-switch.js`, `bin/cs.js` diff --git a/docs/design-guidelines.md b/docs/design-guidelines.md new file mode 100644 index 0000000..0f2380d --- /dev/null +++ b/docs/design-guidelines.md @@ -0,0 +1,28 @@ +# Design Guidelines + +## Terminal Account List + +- Keep `cs ls` scan-friendly with three columns: account/type/email, 5h limit, weekly limit. +- Use terminal block bars: `█` for quota left and `░` for quota used/unavailable. +- Use rounded Unicode table borders for the default terminal table. +- Auto-align columns by visible width and ignore ANSI color codes during width calculation. +- Use a monochrome ink palette only: bold for emphasis, dim for secondary text and used/unavailable quota cells. Avoid colorful status or quota palettes. +- Clamp quota percentages to 0-100% and round to whole numbers. +- Render unknown quota as a fully shaded `░` bar with `--`. +- Show re-auth and stale states as text. Do not invent quota values. +- Keep `--json` output for machines; terminal formatting is for humans. + +## Text And Status + +- Active account marker is `*`. +- Account names should be bold in supported terminals, with plan/email on the next line. +- Quota cells should put the bar and percent on the first line, then reset text on the next line. +- Terminal output should omit low-value metadata such as source and last-used when there is no stale/error state. +- Stale and re-auth status text should render only when it requires user attention, inside the account column. +- Do not print auth payloads, tokens, or raw vault file content. + +## Layout Rules + +- Use a text table for quota bars so accounts can be scanned row-by-row. +- Keep each line short enough for typical terminals. +- Use deterministic formatting so tests can assert exact bar output. diff --git a/docs/development-roadmap.md b/docs/development-roadmap.md index 82846f9..3a0426b 100644 --- a/docs/development-roadmap.md +++ b/docs/development-roadmap.md @@ -2,7 +2,7 @@ ## Current Status Summary -Implementation is materially complete for the planned v1 surface. Local validation is green. Remaining work is mostly live-account acceptance, packaging confidence, and publish readiness. +The product scope is now CLI-only. Core account switching, TUI picker, and on-demand quota probing are implemented. The removed browser UI scope is superseded by terminal quota bars in `cs ls`. ## Phase Status @@ -10,19 +10,19 @@ Implementation is materially complete for the planned v1 surface. Local validati | --- | --- | --- | | 1. Monorepo Setup | Completed | Workspace, packages, build/lint/typecheck wiring are in place. | | 2. Vault and Core CLI Swap | In Progress | Core flows are implemented; native live-account smoke is still manual. | -| 3. TUI Picker and Quota Capture | Completed | Picker is implemented and quota now comes from on-demand backend probing instead of log parsing. | -| 4. Dashboard Core | Completed | Home/API/switch flows exist with cache-backed quota gauges; live UI smoke remains open. | -| 5. History Charts and OAuth Add | In Progress | Charts and add flow exist; real interaction smoke remains open. | -| 6. Packaging and Distribution | In Progress | Build/prepack/tarball checks pass; install and publish readiness still need manual proof. | +| 3. TUI Picker and Quota Capture | Completed | Picker is implemented and quota comes from on-demand backend probing. | +| 4. Terminal Quota Visibility | Completed | `cs ls` shows deterministic 5h and 7d quota bars plus stale/reauth status. | +| 5. Removed Browser UI Scope | Completed | The local browser UI package and command were removed from current product scope. | +| 6. Packaging and Distribution | In Progress | CLI-only package and temp global install smoke pass on Windows; publish readiness still needs live-account and cross-platform proof. | ## Completed Scope - CLI command surface and TUI picker - vault snapshot storage and active auth swap -- local SQLite schema and query layer -- history ingestion helpers plus on-demand quota probing -- local dashboard home/history/add pages -- standalone dashboard packaging +- local SQLite schema and quota cache helpers +- on-demand quota probing +- terminal quota bars for account list output +- CLI/shared-only package build path ## Validation Still Pending @@ -30,17 +30,15 @@ Implementation is materially complete for the planned v1 surface. Local validati - live `cs use` / `cs run` smoke against real accounts - native Codex tool fidelity checks after switch - repeated swap durability testing -- global install smoke on Windows and one non-Windows platform +- global install smoke on one non-Windows platform ## Immediate Backlog 1. Perform native end-to-end smoke with real Codex auth. 2. Validate packaged install from tarball on another platform. -3. Decide whether to suppress or isolate the Next NFT tracing warning. -4. Add broader dashboard/API integration tests if the project continues beyond v1. +3. Consider `cs quota [account]` only if `cs ls` lacks enough detail in real use. ## Risks and Dependencies -- The project depends on Codex’s auth snapshot shape and the undocumented ChatGPT quota endpoint remaining usable. -- Packaging confidence depends on standalone Next output continuing to tolerate local terminal spawn helpers. +- The project depends on Codex's auth snapshot shape and the undocumented ChatGPT quota endpoint remaining usable. - Real publish should wait for live-account smoke, not just local unit/integration-style checks. diff --git a/docs/journals/2026-04-25-cli-package-install-smoke.md b/docs/journals/2026-04-25-cli-package-install-smoke.md new file mode 100644 index 0000000..2d2813b --- /dev/null +++ b/docs/journals/2026-04-25-cli-package-install-smoke.md @@ -0,0 +1,38 @@ +# CLI Package Install Smoke Caught Missing Runtime Dependencies + +**Date**: 2026-04-25 11:52 +**Severity**: High +**Component**: Packaging +**Status**: Resolved + +## What Happened + +The CLI-only pivot passed build, tests, lint, and `npm pack --dry-run`, but code review caught that the packed root package was not actually installable. The root `package.json` shipped `bin/`, `packages/cli/dist/`, and `packages/shared/dist/`, but it did not declare the CLI runtime dependencies. + +## The Brutal Truth + +`npm pack --dry-run` looked useful but was not enough. It verified file inclusion, not whether `npm i -g codex-switch` would start. That is exactly the flow the README advertises, so missing this would have shipped a broken first command. + +## Technical Details + +Temp install failed with `ERR_MODULE_NOT_FOUND` for runtime packages such as `commander`. The compiled CLI also imported the workspace-only `@codex-switch/shared` package, which is not published as a separate package in this plan. + +## What We Tried + +- Removed browser UI package and stale `dash` dist artifacts. +- Added root runtime dependencies for the published package. +- Rewrote compiled CLI imports during `prepack` from `@codex-switch/shared` to relative `packages/shared/dist` imports. +- Added temp global install/bin smoke from a packed tarball. + +## Root Cause Analysis + +The repo is a workspace during development but publishes the root package as the user-facing CLI. Package dry-run did not exercise Node module resolution in the installed layout. + +## Lessons Learned + +Package validation must include install-and-run smoke, not just tarball inspection. For this repo, `npm pack` plus temp `npm install -g` and `cs --help` should stay in release validation. + +## Next Steps + +- Keep temp install/bin smoke in release readiness checks. +- Still run live `cs add`, `cs use`, `cs run`, and cross-platform install smoke before publish. diff --git a/docs/project-changelog.md b/docs/project-changelog.md index b664910..3e02750 100644 --- a/docs/project-changelog.md +++ b/docs/project-changelog.md @@ -1,44 +1,30 @@ # Project Changelog -## 2026-04-23 +## 2026-04-25 ### Added -- pnpm monorepo with `shared`, `cli`, and `dashboard` packages -- vault-backed auth snapshot storage and active-account tracking -- CLI commands for add, use, run, remove, list, current, sync, and dashboard launch -- TUI account picker with quota context -- local SQLite schema for accounts, sessions, quota samples, and active state -- Next.js dashboard with home, history, and add-account flows -- standalone dashboard packaging and publish-time bin shims -- developer utility for inspecting observed Codex log shapes +- terminal `cs ls` quota table with 5h and weekly quota bars +- formatter tests for healthy, partial, unavailable, stale, and re-auth list output +- CLI-first packaging path for root global installs ### Changed -- fixed session-lock acquisition to use exclusive file creation -- fixed file replacement to avoid deleting the destination before rename -- fixed log-watcher attribution to honor the active account switch cutoff -- fixed request-per-day bucket generation to use local-day keys -- fixed add-account polling so navigation/unmount can abort the wait loop -- moved quota collection from `logs_2.sqlite` polling to on-demand ChatGPT backend probing with a per-account TTL cache -- rewired dashboard quota UI to show 5h and 7d gauges with refresh/re-auth states +- product scope is CLI-only +- package scripts and prepack build shared + CLI only +- docs describe terminal quota workflows and current package shape only +- local SQLite state now tracks accounts, active account, quota cache, and auth state ### Removed -- `log-watcher` quota polling from `logs_2.sqlite` -- the history token-per-week chart that depended on background log parsing +- local browser UI package and launch command +- browser UI dependencies and standalone package artifacts +- stale dashboard screenshot/output artifacts +- historical dashboard design spec +- unused session-history watcher, queries, API export, tests, types, schema tables, and inspect script ### Validation -- `pnpm build` -- `pnpm typecheck` -- `pnpm lint` -- `pnpm test` -- `pnpm inspect:codex-logs` -- `npm pack --dry-run` - -### Known Limitations - -- native Codex smoke is still manual -- one Next standalone NFT tracing warning remains during build -- publish readiness depends on cross-platform install validation +- passed after cleanup: `pnpm install --lockfile-only`, `pnpm typecheck`, `pnpm test`, `pnpm lint`, `pnpm build`, `npm pack --dry-run`, docs validation, stale reference scans +- passed before cleanup: temp global install/bin smoke +- manual pending: live `cs add`, `cs use`, `cs run`, `cs ls --refresh`, and cross-platform global install smoke diff --git a/docs/project-overview-pdr.md b/docs/project-overview-pdr.md index d7fdb79..121d248 100644 --- a/docs/project-overview-pdr.md +++ b/docs/project-overview-pdr.md @@ -8,14 +8,14 @@ - Switch between multiple Codex OAuth accounts without forking the Codex runtime. - Keep image generation, web browsing, MCP tools, and other native Codex capabilities intact. -- Provide both CLI and local dashboard workflows. -- Persist account metadata, session history, and quota cache state in a local SQLite database. +- Provide terminal-first account management and quota visibility. +- Persist account metadata and quota cache state in local SQLite. ## Non-Goals - No API proxying. - No per-account `CODEX_HOME` for normal runtime sessions. -- No remote sync, hosted dashboard, or shared multi-user service. +- No remote sync, hosted UI, or shared multi-user service. - No billing estimation or quota prediction in v1. ## Primary User Flows @@ -24,13 +24,11 @@ 2. `cs use ` syncs the current active auth back into the vault, swaps the target snapshot into `~/.codex/auth.json`, and launches Codex. 3. `cs run ...` reuses the same swap-and-launch path for shell aliases such as `alias codex='cs run'`. 4. `cs` with no subcommand opens a TUI picker with account and cached quota context. -5. `cs dash` starts the local Next.js dashboard, opens a browser, and runs the local session watcher. -6. The dashboard supports home, history, and add-account flows through local API routes only. +5. `cs ls` lists vault accounts with compact 5h and 7d quota bars. +6. `cs ls --refresh` probes quota before printing; `cs ls --json` keeps machine-readable output. ## Implemented Feature Scope -### CLI - - `add` - `use` - `run` @@ -38,37 +36,25 @@ - `ls` - `current` - `sync` -- `dash` - bare `cs` TUI picker -### Dashboard - -- `/` account grid -- `/history` usage charts and sessions table -- `/add` account onboarding flow -- `/api/accounts` -- `/api/accounts/[name]` -- `/api/active` -- `/api/add` -- `/api/sessions` -- `/api/switch` -- `/api/usage` - ## Functional Requirements - Persist account snapshots under `~/.codex-switch/accounts/*.json`. - Maintain local state in `~/.codex-switch/state.sqlite`. - Keep swaps local and filesystem-based. - Block conflicting runtime switches with a session lock. -- Use read-only ingestion of `~/.codex/history.jsonl` for sessions and on-demand ChatGPT backend probing for quota. -- Cache quota per account and degrade safely when the backend endpoint or stored auth becomes invalid. +- Use on-demand ChatGPT backend probing for quota. +- Cache quota per account and degrade safely when the endpoint or stored auth becomes invalid. +- Show terminal quota bars for 5h and 7d quota windows. +- Keep re-auth and stale states visible without inventing quota values. ## Non-Functional Requirements - Local-only data handling. - Safe file replacement for vault/auth writes. - Typecheck, lint, build, and automated test coverage for core helpers. -- Standalone dashboard packaging for global install use. +- Publish package contains CLI/shared artifacts only. ## Local Storage Layout @@ -86,24 +72,26 @@ - Vault files and `~/.codex/auth.json` are sensitive secrets. - Logger redacts token-shaped fields and `OPENAI_API_KEY`. -- Dashboard mutations reject cross-origin requests. -- The log watcher is read-only against Codex-owned files and quota probing never logs raw tokens. +- The log watcher is read-only against Codex-owned files. +- Quota probing never logs raw tokens. ## Acceptance Criteria -- `pnpm build`, `pnpm typecheck`, `pnpm lint`, `pnpm test`, and `pnpm inspect:codex-logs` pass locally. +- `pnpm build`, `pnpm typecheck`, `pnpm lint`, and `pnpm test` pass locally, or unrelated failures are documented. - CLI command surface is callable from built artifacts. -- Dashboard builds as standalone output and can be launched from `cs dash`. +- `cs --help` has no removed commands. +- `cs ls` renders 5h and 7d quota bars. +- Packed install exposes working `cs`/`codex-switch` bins. +- Packed artifacts contain no removed UI files. ## Known Limitations -- Native smoke for real `add`, `use`, `run`, and `dash` against live Codex auth is still manual. +- Native smoke for real `add`, `use`, and `run` against live Codex auth is still manual. - Cross-platform global-install validation is not complete. -- Next standalone build still emits an NFT tracing warning around terminal-spawn code. - Quota uses undocumented ChatGPT backend endpoints and may need a fallback patch if OpenAI changes them. ## Deferred Work - Cross-platform install smoke on a non-Windows platform. -- End-to-end UI tests and native Codex fidelity smoke. -- Stronger packaging verification before publish. +- End-to-end native Codex fidelity smoke. +- Optional `cs quota [account]` if users need more detail than `cs ls`. diff --git a/docs/system-architecture.md b/docs/system-architecture.md index 894185c..be5fd0e 100644 --- a/docs/system-architecture.md +++ b/docs/system-architecture.md @@ -2,27 +2,25 @@ ## System Context -`codex-switch` sits beside the native Codex CLI. It does not intercept Codex network traffic. Instead it manages local auth snapshots, local state, and a local dashboard. +`codex-switch` sits beside the native Codex CLI. It does not intercept Codex network traffic. Instead it manages local auth snapshots, local state, and terminal account workflows. ## Package Responsibilities -- `packages/shared`: common path helpers, SQL schema, SQL queries, shared types -- `packages/cli`: auth parsing, OAuth refresh, quota cache/probe orchestration, vault IO, atomic writes, session lock, Codex launch, TUI, session watcher -- `packages/dashboard`: local UI, read-only state queries, local API routes, terminal spawn helpers +- `packages/shared`: common path helpers, SQL schema, shared types +- `packages/cli`: auth parsing, OAuth refresh, quota cache/probe orchestration, vault IO, atomic writes, session lock, Codex launch, TUI, list formatting ## Filesystem Layout - Native live auth: `~/.codex/auth.json` -- Native log inputs: `~/.codex/history.jsonl`, `~/.codex/logs_2.sqlite`, `~/.codex/session_index.jsonl` - Vault root: `~/.codex-switch/` -- Publish/runtime assets: `packages/dashboard/.next/standalone/`, `packages/dashboard/.next/static/` +- Publish assets: `bin/`, `packages/cli/dist/`, `packages/shared/dist/` ## Vault and Active Auth Flow 1. `add` runs `codex login` in a temporary `CODEX_HOME`. 2. The resulting temp `auth.json` is validated and written to `~/.codex-switch/accounts/.json`. 3. `use` syncs the current live auth back into the previously active vault entry when needed. -4. `use` then copies the selected vault snapshot into `~/.codex/auth.json`. +4. `use` copies the selected vault snapshot into `~/.codex/auth.json`. 5. The CLI records the active account and launch timestamp in `state.sqlite`. ## CLI Launch and Locking Flow @@ -30,26 +28,18 @@ - A session lock file under `~/.codex-switch/.lock` prevents overlapping swap flows. - Lock acquisition uses exclusive create semantics. - The release path verifies lock ownership with a per-lock token. -- `launchCodex()` shells out to the real `codex` binary and forwards termination signals. +- The `launchCodex` helper shells out to the real `codex` binary and forwards termination signals. -## Dashboard Runtime Model +## CLI Command Surface -- `cs dash` builds the dashboard if needed. -- It copies static/public assets into the standalone tree. -- It starts the CLI-side log watcher. -- It launches the standalone Next server and opens a browser unless disabled. - -## Local API Inventory - -- `GET /api/accounts` -- `DELETE /api/accounts/:name` -- `GET /api/active` -- `POST /api/add` -- `GET /api/sessions` -- `POST /api/switch` -- `GET /api/usage` (cache-backed quota state, optional refresh) - -All mutations stay local and call back into the CLI. +- `add`: capture a new account auth snapshot. +- `use`: switch to an account and launch Codex. +- `run`: switch/sync wrapper for shell aliases. +- `rm`: remove a vault account and related local state. +- `ls`: list accounts, active marker, metadata, quota bars, and stale/reauth status. +- `current`: print the active account. +- `sync`: persist the current live auth back to its active vault entry. +- bare `cs`: open the account picker. ## SQLite Schema @@ -57,25 +47,20 @@ Tables in `state.sqlite`: - `accounts` - `quota_cache` -- `quota_samples` - `account_auth_state` -- `sessions` - `active` Shared schema lives in `packages/shared/src/schema.sql`. -## Quota and Session Ingestion +## Quota Probing -- `history.jsonl` contributes session-level rows. - Quota is fetched on demand from `https://chatgpt.com/backend-api/wham/usage` with `/backend-api/codex/usage` fallback. - Successful quota probes are cached per account in `quota_cache` with a default 2-minute TTL. -- Invalid or expired stored auth is surfaced through `account_auth_state` so the CLI/dashboard can show a re-auth banner instead of crashing. -- Rows older than the current account’s `switched_at` cutoff are skipped so late watcher startup does not misattribute prior activity. +- Invalid or expired stored auth is surfaced through `account_auth_state` so CLI output can show re-auth status instead of crashing. +- `cs ls` maps cached/probed quota into deterministic ASCII bars and status text. ## Failure Modes and Degradation -- Missing or unknown Codex log files do not block switching. -- Cross-origin dashboard mutations are rejected. -- If no vault state exists yet, dashboard readers return empty snapshots rather than crashing. -- If quota probing fails, the dashboard and CLI fall back to the last cached quota row when available. -- Standalone build currently emits a non-failing NFT tracing warning due terminal spawn code in dashboard routes. +- If quota probing fails, the CLI falls back to the last cached quota row when available. +- Missing, invalid, or expired vault snapshots are shown as re-auth states. +- Unknown quota renders as empty bars with `--`, not fabricated percentages. diff --git a/package.json b/package.json index e3140d2..1f4b6d8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codex-switch", - "version": "0.1.0", - "description": "Native Codex account switching with a vault-backed CLI and dashboard.", + "version": "1.0.7", + "description": "Native Codex account switching with a vault-backed CLI.", "type": "module", "packageManager": "pnpm@10.33.1", "engines": { @@ -12,32 +12,43 @@ ], "scripts": { "build": "pnpm -r build", - "typecheck": "pnpm -r typecheck", + "typecheck": "pnpm --filter @codex-switch/shared build && pnpm -r typecheck", "lint": "pnpm exec eslint .", - "test": "pnpm --filter @codex-switch/shared test && pnpm --filter @codex-switch/cli test && pnpm --filter @codex-switch/dashboard test", - "dev:dashboard": "pnpm --filter @codex-switch/dashboard dev", + "test": "pnpm --filter @codex-switch/shared build && pnpm --filter @codex-switch/shared test && pnpm --filter @codex-switch/cli test", "dev:cli": "pnpm --filter @codex-switch/cli dev", - "inspect:codex-logs": "pnpm --filter @codex-switch/cli exec tsx scripts/inspect-codex-logs.ts", "prepack": "node ./scripts/prepack.mjs" }, "files": [ "bin/", "packages/cli/dist/", "packages/shared/dist/", - "packages/dashboard/.next/standalone/", - "packages/dashboard/.next/static/", - "packages/dashboard/public/", "README.md", "CHANGELOG.md" ], "pnpm": { - "onlyBuiltDependencies": ["better-sqlite3", "esbuild", "sharp"] + "onlyBuiltDependencies": [ + "better-sqlite3", + "esbuild" + ] }, "bin": { - "codex-switch": "./bin/codex-switch.js", - "cs": "./bin/cs.js" + "codex-switch": "bin/codex-switch.js", + "cs": "bin/cs.js" + }, + "keywords": [ + "codex", + "cli", + "oauth", + "account-switching" + ], + "dependencies": { + "@clack/prompts": "1.2.0", + "better-sqlite3": "12.9.0", + "commander": "14.0.3", + "execa": "9.6.1", + "pino": "10.3.1", + "zod": "4.3.6" }, - "keywords": ["codex", "cli", "oauth", "account-switching", "nextjs"], "devDependencies": { "@eslint/js": "9.39.1", "@types/node": "24.10.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 735adfe..a11e32b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -6,10 +6,6 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" - }, - "./api": { - "types": "./dist/api.d.ts", - "default": "./dist/api.js" } }, "files": [ @@ -30,11 +26,8 @@ "@clack/prompts": "1.2.0", "@codex-switch/shared": "workspace:*", "better-sqlite3": "12.9.0", - "chokidar": "5.0.0", "commander": "14.0.3", "execa": "9.6.1", - "get-port": "7.1.0", - "open": "11.0.0", "pino": "10.3.1", "zod": "4.3.6" }, diff --git a/packages/cli/scripts/inspect-codex-logs.ts b/packages/cli/scripts/inspect-codex-logs.ts deleted file mode 100644 index 4cf317c..0000000 --- a/packages/cli/scripts/inspect-codex-logs.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - codexHistoryFile, - codexLogsDatabaseFile, - sessionIndexFile, -} from '@codex-switch/shared'; -import { existsSync, readFileSync } from 'node:fs'; -import Database from 'better-sqlite3'; - -console.log('session_index.jsonl'); -if (existsSync(sessionIndexFile())) { - const lines = readFileSync(sessionIndexFile(), 'utf8') - .split(/\r?\n/) - .filter(Boolean); - const last = lines.at(-1); - if (last) { - console.log(JSON.stringify(Object.keys(JSON.parse(last)).sort(), null, 2)); - } -} else { - console.log('not found'); -} - -console.log('\nhistory.jsonl'); -if (existsSync(codexHistoryFile())) { - const lines = readFileSync(codexHistoryFile(), 'utf8') - .split(/\r?\n/) - .filter(Boolean); - const last = lines.at(-1); - if (last) { - console.log(JSON.stringify(Object.keys(JSON.parse(last)).sort(), null, 2)); - } -} else { - console.log('not found'); -} - -console.log('\nlogs_2.sqlite'); -if (existsSync(codexLogsDatabaseFile())) { - const db = new Database(codexLogsDatabaseFile(), { - readonly: true, - fileMustExist: true, - }); - - try { - db.pragma('query_only = ON'); - const tables = db - .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name`) - .all() as Array<{ name: string }>; - - for (const table of tables) { - const columns = db.prepare(`PRAGMA table_info(${table.name})`).all() as Array<{ - name: string; - }>; - console.log(table.name, columns.map((column) => column.name)); - } - } finally { - db.close(); - } -} else { - console.log('not found'); -} diff --git a/packages/cli/src/api.ts b/packages/cli/src/api.ts deleted file mode 100644 index a2bbf81..0000000 --- a/packages/cli/src/api.ts +++ /dev/null @@ -1,41 +0,0 @@ -export { - fetchQuotaWithCache, - type FetchQuotaReason, - type FetchQuotaWithCacheOptions, - type FetchQuotaWithCacheResult, -} from './core/quota-orchestrator.js'; - -import { vaultStateFile } from '@codex-switch/shared'; -import { existsSync } from 'node:fs'; -import { listAccounts, openStateDatabase } from './core/db.js'; - -export function listKnownAccounts() { - if (!existsSync(vaultStateFile())) { - return []; - } - - try { - const db = openStateDatabase({ readonly: true }); - - try { - return listAccounts(db); - } finally { - db.close(); - } - } catch (error) { - if (isMissingStateDatabaseError(error)) { - return []; - } - - throw error; - } -} - -function isMissingStateDatabaseError(error: unknown) { - return ( - error instanceof Error && - 'code' in error && - typeof error.code === 'string' && - error.code === 'SQLITE_CANTOPEN' - ); -} diff --git a/packages/cli/src/commands/dash.ts b/packages/cli/src/commands/dash.ts deleted file mode 100644 index faf30a4..0000000 --- a/packages/cli/src/commands/dash.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { createLogWatcher } from '../core/log-watcher.js'; -import { Command } from 'commander'; -import { cp, mkdir } from 'node:fs/promises'; -import { existsSync } from 'node:fs'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { execa } from 'execa'; -import getPort from 'get-port'; -import open from 'open'; - -export function registerDashCommand(program: Command) { - program - .command('dash') - .description('Start the local dashboard and open it in a browser.') - .action(async () => { - const runtime = resolveRuntimePaths(); - await ensureDashboardBuild(runtime); - await copyStandaloneAssets(runtime); - - const port = await getPort(); - const watcher = createLogWatcher(); - await watcher.start(); - - const child = execa(process.execPath, [runtime.serverEntry], { - cwd: runtime.standaloneRoot, - stdio: 'inherit', - env: { - ...process.env, - HOSTNAME: '127.0.0.1', - PORT: String(port), - CS_CLI_ENTRY: runtime.cliEntry, - CS_NODE_BINARY: process.execPath, - }, - }); - - const cleanup = async () => { - child.kill('SIGTERM'); - await watcher.stop(); - }; - - const onSigint = () => { - void cleanup(); - }; - const onSigterm = () => { - void cleanup(); - }; - - process.once('SIGINT', onSigint); - process.once('SIGTERM', onSigterm); - - if (process.env.CODEX_SWITCH_NO_OPEN !== '1') { - await open(`http://127.0.0.1:${port}`); - } - - try { - await child; - } finally { - process.removeListener('SIGINT', onSigint); - process.removeListener('SIGTERM', onSigterm); - await watcher.stop(); - } - }); -} - -function resolveRuntimePaths() { - const commandDir = dirname(fileURLToPath(import.meta.url)); - const cliPackageDir = resolve(commandDir, '..', '..'); - const workspacePackagesDir = resolve(cliPackageDir, '..'); - const workspaceRoot = resolve(workspacePackagesDir, '..'); - const dashboardPackageDir = join(workspacePackagesDir, 'dashboard'); - const sharedPackageDir = join(workspacePackagesDir, 'shared'); - const standaloneRoot = join(dashboardPackageDir, '.next', 'standalone'); - - return { - cliPackageDir, - workspaceRoot, - sharedPackageDir, - dashboardPackageDir, - standaloneRoot, - serverEntry: join(standaloneRoot, 'packages', 'dashboard', 'server.js'), - staticSource: join(dashboardPackageDir, '.next', 'static'), - publicSource: join(dashboardPackageDir, 'public'), - staticDestination: join( - standaloneRoot, - 'packages', - 'dashboard', - '.next', - 'static', - ), - publicDestination: join(standaloneRoot, 'packages', 'dashboard', 'public'), - cliEntry: join(cliPackageDir, 'dist', 'index.js'), - cliApiEntry: join(cliPackageDir, 'dist', 'api.js'), - sharedEntry: join(sharedPackageDir, 'dist', 'index.js'), - typescriptBin: join(workspaceRoot, 'node_modules', 'typescript', 'bin', 'tsc'), - }; -} - -async function ensureDashboardBuild(runtime: ReturnType) { - if ( - existsSync(runtime.serverEntry) && - existsSync(runtime.cliEntry) && - existsSync(runtime.cliApiEntry) && - existsSync(runtime.sharedEntry) && - process.env.CODEX_SWITCH_FORCE_DASH_BUILD !== '1' - ) { - return; - } - - // Invoke Next's binary directly via node to avoid pnpm engine checks - // scanning unrelated system package.json files (e.g. ERR_PNPM_UNSUPPORTED_ENGINE - // from globally resolved packages like `openai-codex-read`). - const nextBin = join( - runtime.dashboardPackageDir, - 'node_modules', - 'next', - 'dist', - 'bin', - 'next', - ); - - if (!existsSync(nextBin)) { - throw new Error( - `next binary not found at ${nextBin}. Run "pnpm install" at the workspace root first.`, - ); - } - - if (!existsSync(runtime.typescriptBin)) { - throw new Error( - `TypeScript binary not found at ${runtime.typescriptBin}. Run "pnpm install" at the workspace root first.`, - ); - } - - await buildTypeScriptPackage(runtime.typescriptBin, runtime.sharedPackageDir); - await buildTypeScriptPackage(runtime.typescriptBin, runtime.cliPackageDir); - - await execa(process.execPath, [nextBin, 'build'], { - cwd: runtime.dashboardPackageDir, - stdio: 'inherit', - }); -} - -async function buildTypeScriptPackage(typescriptBin: string, packageDir: string) { - await execa(process.execPath, [typescriptBin, '-p', 'tsconfig.json'], { - cwd: packageDir, - stdio: 'inherit', - }); -} - -async function copyStandaloneAssets(runtime: ReturnType) { - await mkdir(dirname(runtime.staticDestination), { recursive: true }); - if (existsSync(runtime.staticSource)) { - await cp(runtime.staticSource, runtime.staticDestination, { - recursive: true, - force: true, - }); - } - - if (existsSync(runtime.publicSource)) { - await cp(runtime.publicSource, runtime.publicDestination, { - recursive: true, - force: true, - }); - } -} diff --git a/packages/cli/src/commands/ls.ts b/packages/cli/src/commands/ls.ts index 65733a7..fe7da6b 100644 --- a/packages/cli/src/commands/ls.ts +++ b/packages/cli/src/commands/ls.ts @@ -5,6 +5,7 @@ import { fetchQuotaWithCache, type FetchQuotaWithCacheResult, } from '../core/quota-orchestrator.js'; +import { formatListRows, type ListDisplayRow } from './quota-display.js'; export function registerListCommand(program: Command) { program @@ -18,7 +19,7 @@ export function registerListCommand(program: Command) { try { const active = getActiveAccount(db); const accounts = listAccounts(db); - const rows = []; + const rows: ListDisplayRow[] = []; for (const account of accounts) { const quota = await readQuotaState(account.name, options.refresh ?? false); @@ -28,13 +29,14 @@ export function registerListCommand(program: Command) { isActive: active?.account === account.name, latestQuota: quota.row, requiresReauth: quota.requiresReauth, + quotaReason: quota.reason, quotaSource: quota.source, quotaError: quota.error, }); } if (options.json) { - console.log(JSON.stringify(rows, null, 2)); + console.log(JSON.stringify(rows.map(toJsonRow), null, 2)); return; } @@ -43,22 +45,19 @@ export function registerListCommand(program: Command) { return; } - console.table( - rows.map((row) => ({ - active: row.isActive ? '*' : '', - name: row.name, - email: row.email ?? '', - plan: row.plan ?? '', - quota: row.requiresReauth ? 'reauth required' : formatQuotaCell(row.latestQuota), - lastUsed: row.lastUsedAt ? new Date(row.lastUsedAt).toISOString() : '', - })), - ); + console.log(formatListRows(rows)); } finally { db.close(); } }); } +function toJsonRow(row: ListDisplayRow) { + const jsonRow: Partial = { ...row }; + delete jsonRow.quotaReason; + return jsonRow; +} + async function readQuotaState( account: string, force: boolean, @@ -79,33 +78,6 @@ async function readQuotaState( } } -function formatQuotaCell( - quota: { - fiveHourPercent: number | null; - weeklyPercent: number | null; - staleReason: string | null; - } | null, -) { - if (!quota) { - return 'quota unavailable'; - } - - const pieces = []; - if (quota.fiveHourPercent !== null) { - pieces.push(`5h ${Math.round(quota.fiveHourPercent)}%`); - } - - if (quota.weeklyPercent !== null) { - pieces.push(`7d ${Math.round(quota.weeklyPercent)}%`); - } - - if (quota.staleReason && quota.staleReason !== 'requires_reauth') { - pieces.push(`stale:${quota.staleReason}`); - } - - return pieces.join(' · ') || 'quota unavailable'; -} - function toQuotaErrorSummary(code: string, error: unknown): QuotaErrorSummary { return { code, diff --git a/packages/cli/src/commands/quota-display.ts b/packages/cli/src/commands/quota-display.ts new file mode 100644 index 0000000..ea36678 --- /dev/null +++ b/packages/cli/src/commands/quota-display.ts @@ -0,0 +1,178 @@ +import type { + AccountRecord, + QuotaCacheRow, + QuotaDisplaySource, + QuotaErrorSummary, +} from '@codex-switch/shared'; +import type { FetchQuotaReason } from '../core/quota-orchestrator.js'; + +const BAR_WIDTH = 20; +const EMPTY_BAR_CELL = '░'; +const FILLED_BAR_CELL = '█'; +const ANSI_BOLD = '\u001B[1m'; +const ANSI_BOLD_OFF = '\u001B[22m'; +const ANSI_DIM = '\u001B[2m'; +const ANSI_RESET_INTENSITY = '\u001B[22m'; +const ANSI_PATTERN = new RegExp(`${ANSI_BOLD.slice(0, 2).replace('[', '\\[')}[0-9;]*m`, 'g'); + +export interface ListDisplayRow extends AccountRecord { + isActive: boolean; + latestQuota: QuotaCacheRow | null; + requiresReauth: boolean; + quotaReason: FetchQuotaReason; + quotaSource: QuotaDisplaySource | null; + quotaError: QuotaErrorSummary | null; +} + +export function formatListRows(rows: ListDisplayRow[]) { + return formatTable(rows.map(formatListRow)); +} + +function formatListRow(row: ListDisplayRow) { + const marker = row.isActive ? '*' : ' '; + const identity = [row.plan, row.email].filter(Boolean).join(' / ') || 'no plan/email'; + const quota = row.latestQuota; + const status = formatQuotaStatus(row); + const account = [ + formatBold(`${marker} ${row.name}`), + formatDim(`(${identity})`), + status, + ] + .filter(Boolean) + .join('\n'); + + return [ + account, + formatQuotaLimit(quota?.fiveHourPercent ?? null, quota?.fiveHourResetAt ?? null), + formatQuotaLimit(quota?.weeklyPercent ?? null, quota?.weeklyResetAt ?? null), + ]; +} + +export function formatQuotaBar(label: string, percent: number | null) { + return `${label} ${formatBarWithPercent(percent)}`; +} + +function formatQuotaLimit(percent: number | null, resetAt: number | null) { + const lines = [formatBarWithPercent(percent)]; + if (resetAt) { + lines.push(`(resets ${formatResetAt(resetAt)})`); + } + + return lines.join('\n'); +} + +function formatBarWithPercent(percent: number | null) { + if (percent === null || !Number.isFinite(percent)) { + return `[${formatDim(EMPTY_BAR_CELL.repeat(BAR_WIDTH))}] ${formatDim('--')}`; + } + + const clamped = Math.max(0, Math.min(100, Math.round(percent))); + const filled = Math.round((clamped / 100) * BAR_WIDTH); + const filledBar = FILLED_BAR_CELL.repeat(filled); + const emptyBar = formatDim(EMPTY_BAR_CELL.repeat(BAR_WIDTH - filled)); + + return `[${filledBar}${emptyBar}] ${formatBold(`${clamped}%`)}`; +} + +function formatQuotaStatus(row: ListDisplayRow) { + const pieces = []; + + if (row.requiresReauth) { + pieces.push(formatBold('reauth required')); + } + + if (row.latestQuota?.staleReason && row.latestQuota.staleReason !== 'requires_reauth') { + pieces.push(formatBold(`stale:${row.latestQuota.staleReason}`)); + } + + if (row.quotaError) { + pieces.push(formatBold(`${row.quotaError.code}: ${row.quotaError.message}`)); + } + + return pieces.join(' | '); +} + +function formatResetAt(value: number) { + const reset = new Date(value); + const now = new Date(); + const time = `${String(reset.getHours()).padStart(2, '0')}:${String(reset.getMinutes()).padStart(2, '0')}`; + + if (isSameLocalDay(reset, now)) { + return time; + } + + return `${time} on ${reset.getDate()} ${formatMonth(reset)}`; +} + +function formatBold(value: string) { + return `${ANSI_BOLD}${value}${ANSI_BOLD_OFF}`; +} + +function formatDim(value: string) { + return value ? `${ANSI_DIM}${value}${ANSI_RESET_INTENSITY}` : ''; +} + +function isSameLocalDay(left: Date, right: Date) { + return ( + left.getFullYear() === right.getFullYear() && + left.getMonth() === right.getMonth() && + left.getDate() === right.getDate() + ); +} + +function formatMonth(value: Date) { + return ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][ + value.getMonth() + ] ?? ''; +} + +function formatTable(rows: string[][]) { + const header = [`Account (${rows.length})`, '5h Limit', 'Weekly Limit'].map((value) => + formatBold(value), + ); + const table = [header, ...rows]; + const widths = header.map((_, index) => + Math.max(...table.map((row) => maxVisibleLineLength(row[index] ?? ''))), + ); + const topBorder = formatBorder('╭', '┬', '╮', widths); + const separator = formatBorder('├', '┼', '┤', widths); + const bottomBorder = formatBorder('╰', '┴', '╯', widths); + const renderedRows = table.map((row, index) => { + const rendered = formatTableRow(row, widths); + return index === 0 ? `${rendered}\n${separator}` : rendered; + }); + + return [topBorder, ...renderedRows, bottomBorder].join('\n'); +} + +function formatTableRow(row: string[], widths: number[]) { + const lines = row.map((cell) => cell.split('\n')); + const height = Math.max(...lines.map((cellLines) => cellLines.length)); + const renderedLines = []; + + for (let lineIndex = 0; lineIndex < height; lineIndex += 1) { + renderedLines.push( + lines + .map((cellLines, cellIndex) => ` ${padVisible(cellLines[lineIndex] ?? '', widths[cellIndex] ?? 0)} `) + .join('│'), + ); + } + + return renderedLines.map((line) => `│${line}│`).join('\n'); +} + +function formatBorder(left: string, middle: string, right: string, widths: number[]) { + return `${left}${widths.map((width) => '─'.repeat(width + 2)).join(middle)}${right}`; +} + +function padVisible(value: string, width: number) { + return `${value}${' '.repeat(Math.max(0, width - visibleLength(value)))}`; +} + +function maxVisibleLineLength(value: string) { + return Math.max(...value.split('\n').map(visibleLength)); +} + +function visibleLength(value: string) { + return value.replace(ANSI_PATTERN, '').length; +} diff --git a/packages/cli/src/core/db.ts b/packages/cli/src/core/db.ts index f9b82f3..8518f31 100644 --- a/packages/cli/src/core/db.ts +++ b/packages/cli/src/core/db.ts @@ -1,8 +1,4 @@ -import type { - AccountRecord, - ActiveAccountState, - SessionRow, -} from '@codex-switch/shared'; +import type { AccountRecord, ActiveAccountState } from '@codex-switch/shared'; import { STATE_SCHEMA_SQL, vaultStateFile } from '@codex-switch/shared'; import Database from 'better-sqlite3'; import { mkdirSync } from 'node:fs'; @@ -103,30 +99,5 @@ export function touchAccountLastUsed(db: StateDatabase, name: string) { export function removeAccount(db: StateDatabase, name: string) { db.prepare('DELETE FROM accounts WHERE name = ?').run(name); db.prepare('DELETE FROM quota_cache WHERE account = ?').run(name); - db.prepare('DELETE FROM quota_samples WHERE account = ?').run(name); db.prepare('DELETE FROM account_auth_state WHERE account = ?').run(name); - db.prepare('DELETE FROM sessions WHERE account = ?').run(name); -} - -export function upsertSession(db: StateDatabase, session: SessionRow) { - db.prepare( - `INSERT INTO sessions - (session_id, account, started_at, ended_at, request_count, token_in, token_out) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(session_id) DO UPDATE SET - account = excluded.account, - started_at = MIN(sessions.started_at, excluded.started_at), - ended_at = COALESCE(excluded.ended_at, sessions.ended_at), - request_count = MAX(sessions.request_count, excluded.request_count), - token_in = COALESCE(excluded.token_in, sessions.token_in), - token_out = COALESCE(excluded.token_out, sessions.token_out)`, - ).run( - session.sessionId, - session.account, - session.startedAt, - session.endedAt, - session.requestCount, - session.tokenIn, - session.tokenOut, - ); } diff --git a/packages/cli/src/core/log-watcher-helpers.ts b/packages/cli/src/core/log-watcher-helpers.ts deleted file mode 100644 index a751822..0000000 --- a/packages/cli/src/core/log-watcher-helpers.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { SessionRow } from '@codex-switch/shared'; - -interface HistoryRowShape { - session_id?: unknown; - ts?: unknown; -} - -export function parseHistoryChunk(raw: string, account: string, switchedAt: number) { - const sessions: SessionRow[] = []; - - for (const line of raw.split(/\r?\n/)) { - if (!line.trim()) { - continue; - } - - const row = safeParseJson(line) as HistoryRowShape | null; - if (!row || typeof row.session_id !== 'string' || typeof row.ts !== 'number') { - continue; - } - - const timestamp = row.ts * 1000; - if (timestamp < switchedAt) { - continue; - } - - sessions.push({ - account, - sessionId: row.session_id, - startedAt: timestamp, - endedAt: timestamp, - requestCount: 1, - tokenIn: null, - tokenOut: null, - }); - } - - return sessions; -} - -function safeParseJson(raw: string) { - try { - return JSON.parse(raw) as Record; - } catch { - return null; - } -} diff --git a/packages/cli/src/core/log-watcher.ts b/packages/cli/src/core/log-watcher.ts deleted file mode 100644 index b8649e3..0000000 --- a/packages/cli/src/core/log-watcher.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { codexHistoryFile } from '@codex-switch/shared'; -import chokidar from 'chokidar'; -import { readFile } from 'node:fs/promises'; -import { getActiveAccount, openStateDatabase, upsertSession } from './db.js'; -import { parseHistoryChunk } from './log-watcher-helpers.js'; -import { logger } from '../util/logger.js'; - -export function createLogWatcher() { - let historyOffset = 0; - const stateDb = openStateDatabase(); - const historyWatcher = chokidar.watch(codexHistoryFile(), { - ignoreInitial: false, - }); - - async function start() { - await backfillHistory(); - - historyWatcher.on('add', () => void processHistoryAppend()); - historyWatcher.on('change', () => void processHistoryAppend()); - } - - async function stop() { - await historyWatcher.close(); - stateDb.close(); - } - - async function backfillHistory() { - try { - const active = getActiveAccount(stateDb); - if (!active) { - return; - } - - const content = await tryReadFile(codexHistoryFile()); - historyOffset = Buffer.byteLength(content); - - for (const session of parseHistoryChunk(content, active.account, active.switchedAt)) { - upsertSession(stateDb, session); - } - } catch (error) { - logger.debug({ error }, 'History backfill skipped.'); - } - } - - async function processHistoryAppend() { - try { - const active = getActiveAccount(stateDb); - if (!active) { - return; - } - - const content = await tryReadFile(codexHistoryFile()); - const allBytes = Buffer.from(content); - const nextChunk = allBytes.subarray(historyOffset).toString('utf8'); - historyOffset = allBytes.length; - - for (const session of parseHistoryChunk(nextChunk, active.account, active.switchedAt)) { - upsertSession(stateDb, session); - } - } catch (error) { - logger.debug({ error }, 'History append parse failed.'); - } - } - - return { - start, - stop, - }; -} - -async function tryReadFile(filePath: string) { - try { - return await readFile(filePath, 'utf8'); - } catch { - return ''; - } -} diff --git a/packages/cli/src/core/quota-cache.ts b/packages/cli/src/core/quota-cache.ts index e823eca..17ede8a 100644 --- a/packages/cli/src/core/quota-cache.ts +++ b/packages/cli/src/core/quota-cache.ts @@ -21,29 +21,6 @@ export function getQuotaCache(db: StateDatabase, account: string) { return row ? mapQuotaCacheRow(row) : null; } -export function getAllQuotaCache(db: StateDatabase) { - const rows = db - .prepare( - `SELECT - account, - captured_at, - five_hour_percent, - five_hour_reset_at, - weekly_percent, - weekly_reset_at, - source, - stale_reason - FROM quota_cache - ORDER BY captured_at DESC, account ASC`, - ) - .all() as DatabaseQuotaCacheRow[]; - - return Object.fromEntries(rows.map((row) => [row.account, mapQuotaCacheRow(row)])) as Record< - string, - QuotaCacheRow - >; -} - export function upsertQuotaCache( db: StateDatabase, account: string, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 507a67c..73e9af9 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,7 +1,6 @@ import { Command } from 'commander'; import { registerAddCommand } from './commands/add.js'; import { registerCurrentCommand } from './commands/current.js'; -import { registerDashCommand } from './commands/dash.js'; import { registerListCommand } from './commands/ls.js'; import { registerRemoveCommand } from './commands/rm.js'; import { registerRunCommand } from './commands/run.js'; @@ -24,7 +23,6 @@ registerAddCommand(program); registerRemoveCommand(program); registerListCommand(program); registerCurrentCommand(program); -registerDashCommand(program); registerSyncCommand(program); program.action(async () => { diff --git a/packages/cli/test/api.test.ts b/packages/cli/test/api.test.ts deleted file mode 100644 index 799d106..0000000 --- a/packages/cli/test/api.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import assert from 'node:assert/strict'; -import os from 'node:os'; -import path from 'node:path'; -import { mkdtemp, rm } from 'node:fs/promises'; -import test from 'node:test'; -import { listKnownAccounts } from '../src/api.js'; - -test('listKnownAccounts returns an empty list before state.sqlite exists', async () => { - const cleanup = await withTempVaultRoot(); - - try { - assert.deepEqual(listKnownAccounts(), []); - } finally { - await cleanup(); - } -}); - -async function withTempVaultRoot() { - const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-switch-api-')); - const previous = process.env.CODEX_SWITCH_VAULT_ROOT; - process.env.CODEX_SWITCH_VAULT_ROOT = tempRoot; - - return async () => { - if (previous === undefined) { - delete process.env.CODEX_SWITCH_VAULT_ROOT; - } else { - process.env.CODEX_SWITCH_VAULT_ROOT = previous; - } - - await rm(tempRoot, { recursive: true, force: true }); - }; -} diff --git a/packages/cli/test/list-command-formatting.test.ts b/packages/cli/test/list-command-formatting.test.ts new file mode 100644 index 0000000..f31799b --- /dev/null +++ b/packages/cli/test/list-command-formatting.test.ts @@ -0,0 +1,138 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { formatListRows, formatQuotaBar, type ListDisplayRow } from '../src/commands/quota-display.js'; + +const BOLD = '\u001B[1m'; +const ANSI_PATTERN = new RegExp(`${BOLD.slice(0, 2).replace('[', '\\[')}[0-9;]*m`, 'g'); +const COLOR_PATTERN = new RegExp(`${BOLD.slice(0, 2).replace('[', '\\[')}(31|32|33|36)m`); + +test('formatQuotaBar renders deterministic block bars', () => { + assert.equal(stripAnsi(formatQuotaBar('5h', 42)), '5h [████████░░░░░░░░░░░░] 42%'); + assert.equal(stripAnsi(formatQuotaBar('7d', 84)), '7d [█████████████████░░░] 84%'); + assert.match(formatQuotaBar('5h', 42), new RegExp(escapeRegExp(BOLD))); + assert.doesNotMatch(formatQuotaBar('5h', 42), COLOR_PATTERN); +}); + +test('formatQuotaBar clamps unavailable and out-of-range values', () => { + assert.equal(stripAnsi(formatQuotaBar('5h', null)), '5h [░░░░░░░░░░░░░░░░░░░░] --'); + assert.equal(stripAnsi(formatQuotaBar('5h', -20)), '5h [░░░░░░░░░░░░░░░░░░░░] 0%'); + assert.equal(stripAnsi(formatQuotaBar('5h', 120)), '5h [████████████████████] 100%'); +}); + +test('formatListRows renders healthy probe rows', () => { + const output = formatListRows([ + row({ + quotaReason: 'probed', + quotaSource: 'wham', + latestQuota: quota({ + fiveHourPercent: 42, + fiveHourResetAt: todayAt(16, 52), + weeklyPercent: 84, + weeklyResetAt: localDate(2026, 4, 29, 13, 4), + }), + }), + ]); + const cleanOutput = stripAnsi(output); + + assert.match(cleanOutput, /^╭─+┬─+┬─+╮/m); + assert.match(cleanOutput, /│ Account \(1\)\s+│ 5h Limit\s+│ Weekly Limit\s+│/); + assert.match(cleanOutput, /\* personal/); + assert.match(cleanOutput, /\(Pro \/ user@example.com\)/); + assert.match(cleanOutput, /│ \[████████░{12}\] 42%\s+│/); + assert.match(cleanOutput, /│ \(resets 16:52\)\s+│/); + assert.match(cleanOutput, /│ \[█████████████████░{3}\] 84%/); + assert.match(cleanOutput, /│ \(resets 13:04 on 29 Apr\)/); + assert.match(output, new RegExp(escapeRegExp(BOLD))); + assert.doesNotMatch(output, COLOR_PATTERN); + assert.doesNotMatch(cleanOutput, /source probe:wham/); + assert.doesNotMatch(cleanOutput, /last used/); +}); + +test('formatListRows renders partial and stale cache rows', () => { + const output = formatListRows([ + row({ + quotaReason: 'stale_network', + quotaSource: 'cache', + latestQuota: quota({ + fiveHourPercent: 30, + weeklyPercent: null, + staleReason: 'network', + }), + }), + ]); + const cleanOutput = stripAnsi(output); + + assert.match(cleanOutput, /│ \[██████░{14}\] 30%\s+│/); + assert.match(cleanOutput, /│ \[░{20}\] --/); + assert.match(cleanOutput, /stale:network/); + assert.doesNotMatch(cleanOutput, /source cache/); +}); + +test('formatListRows renders unavailable reauth rows', () => { + const output = formatListRows([ + row({ + latestQuota: null, + quotaReason: 'requires_reauth', + requiresReauth: true, + quotaError: { + code: 'vault_missing', + message: 'Vault snapshot is missing for this account.', + }, + }), + ]); + const cleanOutput = stripAnsi(output); + + assert.match(cleanOutput, /│ \[░{20}\] --\s+│/); + assert.match(cleanOutput, /│ \[░{20}\] --/); + assert.match(cleanOutput, /reauth required/); + assert.match(cleanOutput, /vault_missing/); +}); + +function row(overrides: Partial): ListDisplayRow { + return { + name: 'personal', + email: 'user@example.com', + plan: 'Pro', + addedAt: 1, + lastUsedAt: 1_800_000_000_000, + notes: null, + isActive: true, + latestQuota: null, + requiresReauth: false, + quotaReason: 'cache', + quotaSource: null, + quotaError: null, + ...overrides, + }; +} + +function todayAt(hour: number, minute: number) { + const now = new Date(); + return new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute).getTime(); +} + +function localDate(year: number, month: number, day: number, hour: number, minute: number) { + return new Date(year, month - 1, day, hour, minute).getTime(); +} + +function quota(overrides: Partial>) { + return { + account: 'personal', + capturedAt: 1_800_000_000_000, + fiveHourPercent: null, + fiveHourResetAt: null, + weeklyPercent: null, + weeklyResetAt: null, + source: 'wham' as const, + staleReason: null, + ...overrides, + }; +} + +function escapeRegExp(value: string) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function stripAnsi(value: string) { + return value.replace(ANSI_PATTERN, ''); +} diff --git a/packages/cli/test/log-watcher.test.ts b/packages/cli/test/log-watcher.test.ts deleted file mode 100644 index 2cfb522..0000000 --- a/packages/cli/test/log-watcher.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { parseHistoryChunk } from '../src/core/log-watcher-helpers.js'; - -test('parseHistoryChunk ignores sessions from before the active switch', () => { - const rows = parseHistoryChunk( - [ - JSON.stringify({ session_id: 'before', ts: 100 }), - JSON.stringify({ session_id: 'after', ts: 200 }), - ].join('\n'), - 'beta', - 150_000, - ); - - assert.deepEqual( - rows.map((row) => row.sessionId), - ['after'], - ); - assert.equal(rows[0]?.account, 'beta'); -}); diff --git a/packages/dashboard/app/add/add-form.tsx b/packages/dashboard/app/add/add-form.tsx deleted file mode 100644 index e035291..0000000 --- a/packages/dashboard/app/add/add-form.tsx +++ /dev/null @@ -1,94 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import { useEffect, useRef, useState, useTransition } from 'react'; -import { toast } from 'sonner'; -import { waitForAccountReady } from '../../lib/account-availability'; - -export function AddForm() { - const router = useRouter(); - const [name, setName] = useState(''); - const [pending, startTransition] = useTransition(); - const [status, setStatus] = useState(''); - const pollAbortRef = useRef(null); - - useEffect(() => { - return () => { - pollAbortRef.current?.abort(); - }; - }, []); - - return ( -
{ - event.preventDefault(); - - startTransition(async () => { - const response = await fetch('/api/add', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name }), - }); - const payload = (await response.json().catch(() => ({}))) as { error?: string }; - - if (!response.ok) { - toast.error(payload.error ?? 'Unable to open OAuth terminal.'); - return; - } - - setStatus('Waiting for login to complete in the new terminal...'); - pollAbortRef.current?.abort(); - const controller = new AbortController(); - pollAbortRef.current = controller; - - try { - const isReady = await waitForAccountReady({ - name, - signal: controller.signal, - }); - - if (isReady) { - toast.success(`Account "${name}" is ready.`); - router.push('/'); - router.refresh(); - return; - } - - toast.error('Timed out waiting for OAuth completion.'); - } catch (error) { - if (!(error instanceof Error) || error.name !== 'AbortError') { - toast.error('Unable to confirm OAuth completion.'); - } - } finally { - if (pollAbortRef.current === controller) { - pollAbortRef.current = null; - } - } - }); - }} - > - -

- The terminal flow runs `codex login` with a temporary `CODEX_HOME`, then stores the - resulting auth snapshot in your vault. -

- {status ?

{status}

: null} - -
- ); -} diff --git a/packages/dashboard/app/add/page.tsx b/packages/dashboard/app/add/page.tsx deleted file mode 100644 index e844ec8..0000000 --- a/packages/dashboard/app/add/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { AppShell } from '../components/app-shell'; -import { AddForm } from './add-form'; - -export default function AddPage() { - return ( - - - - ); -} diff --git a/packages/dashboard/app/api/accounts/[name]/route.ts b/packages/dashboard/app/api/accounts/[name]/route.ts deleted file mode 100644 index b3b785d..0000000 --- a/packages/dashboard/app/api/accounts/[name]/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NextResponse } from 'next/server'; -import { validateAccountName } from '../../../../lib/account-name'; -import { runCli } from '../../../../lib/cli-spawn'; -import { rejectCrossOrigin } from '../../../../lib/request-guard'; - -export async function DELETE( - request: Request, - context: { params: Promise> }, -) { - const rejection = rejectCrossOrigin(request); - if (rejection) { - return rejection; - } - - const params = await context.params; - const name = validateAccountName(params.name); - if (!name) { - return NextResponse.json({ error: 'Invalid account name.' }, { status: 400 }); - } - - const result = await runCli(['rm', name]); - if (result.code !== 0) { - return NextResponse.json( - { error: result.stderr || result.stdout || 'Unable to remove account.' }, - { status: /active/i.test(result.stderr) ? 409 : 500 }, - ); - } - - return NextResponse.json({ ok: true }); -} diff --git a/packages/dashboard/app/api/accounts/route.ts b/packages/dashboard/app/api/accounts/route.ts deleted file mode 100644 index 947faa2..0000000 --- a/packages/dashboard/app/api/accounts/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { NextResponse } from 'next/server'; -import { readAccountsSnapshot } from '../../../lib/db'; - -export async function GET() { - return NextResponse.json({ accounts: readAccountsSnapshot() }); -} diff --git a/packages/dashboard/app/api/active/route.ts b/packages/dashboard/app/api/active/route.ts deleted file mode 100644 index c497077..0000000 --- a/packages/dashboard/app/api/active/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextResponse } from 'next/server'; -import { readAccountsSnapshot } from '../../../lib/db'; - -export async function GET() { - const active = readAccountsSnapshot().find((account) => account.isActive) ?? null; - return NextResponse.json({ active: active?.name ?? null }); -} diff --git a/packages/dashboard/app/api/add/route.ts b/packages/dashboard/app/api/add/route.ts deleted file mode 100644 index 6bf8aa1..0000000 --- a/packages/dashboard/app/api/add/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NextResponse } from 'next/server'; -import { validateAccountName } from '../../../lib/account-name'; -import { openCliInNewTerminal } from '../../../lib/cli-spawn'; -import { readAccountsSnapshot } from '../../../lib/db'; -import { rejectCrossOrigin } from '../../../lib/request-guard'; - -export async function POST(request: Request) { - const rejection = rejectCrossOrigin(request); - if (rejection) { - return rejection; - } - - const body = (await request.json().catch(() => ({}))) as { name?: string }; - const name = validateAccountName(body.name); - if (!name) { - return NextResponse.json({ error: 'Invalid account name.' }, { status: 400 }); - } - - if (readAccountsSnapshot().some((account) => account.name === name)) { - return NextResponse.json({ error: 'That account name already exists.' }, { status: 409 }); - } - - openCliInNewTerminal(['add', '--name', name]); - return NextResponse.json({ ok: true }, { status: 202 }); -} diff --git a/packages/dashboard/app/api/sessions/route.ts b/packages/dashboard/app/api/sessions/route.ts deleted file mode 100644 index 68c2ab6..0000000 --- a/packages/dashboard/app/api/sessions/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NextResponse } from 'next/server'; -import { readRecentSessions } from '../../../lib/db'; - -export async function GET(request: Request) { - const url = new URL(request.url); - const limit = Math.max(1, Math.min(200, Number(url.searchParams.get('limit') ?? '50'))); - const offset = Math.max(0, Number(url.searchParams.get('offset') ?? '0')); - - return NextResponse.json(readRecentSessions(limit, offset)); -} diff --git a/packages/dashboard/app/api/switch/route.ts b/packages/dashboard/app/api/switch/route.ts deleted file mode 100644 index 62d5e0c..0000000 --- a/packages/dashboard/app/api/switch/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NextResponse } from 'next/server'; -import { validateAccountName } from '../../../lib/account-name'; -import { openCliInNewTerminal } from '../../../lib/cli-spawn'; -import { readActiveLock } from '../../../lib/lock-probe'; -import { rejectCrossOrigin } from '../../../lib/request-guard'; - -export async function POST(request: Request) { - const rejection = rejectCrossOrigin(request); - if (rejection) { - return rejection; - } - - const body = (await request.json().catch(() => ({}))) as { name?: string }; - const name = validateAccountName(body.name); - if (!name) { - return NextResponse.json({ error: 'Invalid account name.' }, { status: 400 }); - } - - const activeLock = readActiveLock(); - if (activeLock) { - return NextResponse.json( - { error: `Close the running Codex session for "${activeLock.account}" first.` }, - { status: 409 }, - ); - } - - openCliInNewTerminal(['use', name]); - return NextResponse.json({ ok: true }, { status: 202 }); -} diff --git a/packages/dashboard/app/api/usage/route.ts b/packages/dashboard/app/api/usage/route.ts deleted file mode 100644 index 31c5ef2..0000000 --- a/packages/dashboard/app/api/usage/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { UsageResponse } from '@codex-switch/shared'; -import { listKnownAccounts, fetchQuotaWithCache } from '@codex-switch/cli/api'; -import { NextResponse } from 'next/server'; -import { buildUsageResponse } from '../../../lib/usage-response'; - -export async function GET(request: Request) { - const url = new URL(request.url); - const force = url.searchParams.get('refresh') === '1'; - const targetAccount = url.searchParams.get('account'); - const accounts = listKnownAccounts() - .map((account) => account.name) - .filter((name) => !targetAccount || name === targetAccount); - const ttlMs = Number(process.env.CODEX_SWITCH_QUOTA_TTL_MS ?? 120_000); - const payload: UsageResponse = await buildUsageResponse(accounts, { - ttlMs, - fetchQuota: (account) => - fetchQuotaWithCache(account, { - force, - ttlMs, - }), - }); - - return NextResponse.json(payload, { - headers: { - 'Cache-Control': 'no-store', - }, - }); -} diff --git a/packages/dashboard/app/components/account-card.tsx b/packages/dashboard/app/components/account-card.tsx deleted file mode 100644 index 1103b20..0000000 --- a/packages/dashboard/app/components/account-card.tsx +++ /dev/null @@ -1,128 +0,0 @@ -'use client'; - -import type { AccountSummary, AccountUsageSnapshot } from '@codex-switch/shared'; -import { useState, useTransition } from 'react'; -import { toast } from 'sonner'; -import { QuotaGauge } from './quota-gauge'; -import { ReauthBanner } from './reauth-banner'; -import { SwitchButton } from './switch-button'; - -export function AccountCard({ - account, - usage, - onDone, - onRefreshUsage, -}: Readonly<{ - account: AccountSummary; - usage: AccountUsageSnapshot | null; - onDone: () => void; - onRefreshUsage: (account: string) => Promise | void; -}>) { - const [pending, startTransition] = useTransition(); - const [refreshing, setRefreshing] = useState(false); - const currentUsage = usage ?? account.latestQuota; - - return ( -
-
-
-
-

{account.name}

- {account.isActive ? ( - - Active - - ) : null} -
-

{account.email ?? 'Email unknown'}

-

{account.plan ?? 'Plan unknown'}

-
- -
- -
- {currentUsage?.requiresReauth ? ( - - ) : ( -
- - -
- )} -
- -
-

- Updated{' '} - {currentUsage?.ageMs !== null && currentUsage?.ageMs !== undefined - ? formatAge(currentUsage.ageMs) - : 'never'} -

- {currentUsage?.error ?

{currentUsage.error.message}

: null} -

- Last used{' '} - {account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleString() : 'not yet tracked'} -

-
- -
- - -
-
- ); -} - -function formatAge(ageMs: number) { - const diffMinutes = Math.round(Math.max(0, ageMs) / 60_000); - if (diffMinutes < 1) { - return 'just now'; - } - - if (diffMinutes < 60) { - return `${diffMinutes}m ago`; - } - - const diffHours = Math.round(diffMinutes / 60); - if (diffHours < 24) { - return `${diffHours}h ago`; - } - - return `${Math.round(diffHours / 24)}d ago`; -} diff --git a/packages/dashboard/app/components/account-grid-client.tsx b/packages/dashboard/app/components/account-grid-client.tsx deleted file mode 100644 index d776d47..0000000 --- a/packages/dashboard/app/components/account-grid-client.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import type { AccountSummary } from '@codex-switch/shared'; -import { startTransition, useState } from 'react'; -import { AccountCard } from './account-card'; -import { useUsagePolling } from '../../lib/usage-hooks'; - -export function AccountGridClient({ - initialAccounts, - pollingEnabled, -}: Readonly<{ - initialAccounts: AccountSummary[]; - pollingEnabled: boolean; -}>) { - const [accounts, setAccounts] = useState(initialAccounts); - const { usage, refresh } = useUsagePolling( - Object.fromEntries(initialAccounts.map((account) => [account.name, account.latestQuota])), - 60_000, - pollingEnabled, - ); - - async function refreshAccounts() { - const response = await fetch('/api/accounts', { cache: 'no-store' }); - if (!response.ok) { - return; - } - - const payload = (await response.json()) as { accounts: AccountSummary[] }; - startTransition(() => { - setAccounts(payload.accounts); - }); - } - - if (accounts.length === 0) { - return ( -
- No accounts yet. Open the Add Account screen to ingest a new Codex login. -
- ); - } - - return ( -
- {accounts.map((account) => ( - void refreshAccounts()} - onRefreshUsage={(name) => refresh(name)} - /> - ))} -
- ); -} diff --git a/packages/dashboard/app/components/app-shell.tsx b/packages/dashboard/app/components/app-shell.tsx deleted file mode 100644 index e1976d3..0000000 --- a/packages/dashboard/app/components/app-shell.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import Link from 'next/link'; - -export function AppShell({ - title, - description, - children, -}: Readonly<{ - title: string; - description: string; - children: React.ReactNode; -}>) { - return ( -
-
-
-
-

- codex-switch -

-

- {title} -

-

- {description} -

-
- -
-
{children}
-
-
- ); -} - -function NavLink({ href, children }: Readonly<{ href: string; children: React.ReactNode }>) { - return ( - - {children} - - ); -} diff --git a/packages/dashboard/app/components/quota-gauge.tsx b/packages/dashboard/app/components/quota-gauge.tsx deleted file mode 100644 index 2a52e79..0000000 --- a/packages/dashboard/app/components/quota-gauge.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client'; - -import type { QuotaWindow } from '@codex-switch/shared'; -import { useEffect, useState } from 'react'; - -export function QuotaGauge({ - label, - quota, -}: Readonly<{ - label: string; - quota: QuotaWindow | null; -}>) { - const [now, setNow] = useState(() => Date.now()); - - useEffect(() => { - const timer = window.setInterval(() => { - setNow(Date.now()); - }, 30_000); - - return () => { - window.clearInterval(timer); - }; - }, []); - - const percent = quota?.percentLeft ?? null; - const tone = - percent === null - ? 'bg-[var(--card-border)]' - : percent <= 10 - ? 'bg-[var(--danger)]' - : percent <= 25 - ? 'bg-[var(--warning)]' - : 'bg-[var(--success)]'; - - return ( -
-
- {label} - {percent === null ? 'No data' : `${Math.round(percent)}% left`} -
-
-
-
-

- {quota?.resetAt ? `Resets in ${formatRemaining(quota.resetAt, now)}` : 'Reset unavailable'} -

-
- ); -} - -function formatRemaining(timestamp: number, now: number) { - const diffMs = Math.max(0, timestamp - now); - const diffMinutes = Math.round(diffMs / 60_000); - const hours = Math.floor(diffMinutes / 60); - const minutes = diffMinutes % 60; - - if (hours > 0) { - return `${hours}h ${minutes}m`; - } - - return `${minutes}m`; -} diff --git a/packages/dashboard/app/components/reauth-banner.tsx b/packages/dashboard/app/components/reauth-banner.tsx deleted file mode 100644 index 8834b30..0000000 --- a/packages/dashboard/app/components/reauth-banner.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export function ReauthBanner({ account }: Readonly<{ account: string }>) { - return ( -
-

Re-authentication required

-

- Run cs add --name {account} to - refresh the stored OAuth snapshot for this account. -

-
- ); -} diff --git a/packages/dashboard/app/components/switch-button.tsx b/packages/dashboard/app/components/switch-button.tsx deleted file mode 100644 index 5788ce6..0000000 --- a/packages/dashboard/app/components/switch-button.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client'; - -import { useTransition } from 'react'; -import { toast } from 'sonner'; - -export function SwitchButton({ - name, - disabled, - onDone, -}: Readonly<{ - name: string; - disabled?: boolean; - onDone: () => void; -}>) { - const [pending, startTransition] = useTransition(); - - return ( - - ); -} diff --git a/packages/dashboard/app/globals.css b/packages/dashboard/app/globals.css deleted file mode 100644 index 0abf49c..0000000 --- a/packages/dashboard/app/globals.css +++ /dev/null @@ -1,24 +0,0 @@ -@import 'tailwindcss'; - -:root { - color-scheme: light; - --background: #f5f7fb; - --foreground: #182033; - --card: rgba(255, 255, 255, 0.9); - --card-border: rgba(24, 32, 51, 0.1); - --muted: #5b6476; - --accent: #1f6feb; - --accent-soft: rgba(31, 111, 235, 0.12); - --success: #0f9b68; - --warning: #d68c00; - --danger: #c74141; -} - -body { - background: - radial-gradient(circle at top left, rgba(31, 111, 235, 0.16), transparent 28%), - linear-gradient(180deg, #f8fbff 0%, #eef3fb 100%); - color: var(--foreground); - font-family: 'Segoe UI', system-ui, sans-serif; - min-height: 100vh; -} diff --git a/packages/dashboard/app/history/components/requests-per-day-chart.tsx b/packages/dashboard/app/history/components/requests-per-day-chart.tsx deleted file mode 100644 index 8786e12..0000000 --- a/packages/dashboard/app/history/components/requests-per-day-chart.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; - -import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; - -export function RequestsPerDayChart({ - data, - accounts, -}: Readonly<{ - data: Array>; - accounts: string[]; -}>) { - return ( -
-

Requests per day

-
- - - - - - {accounts.map((account, index) => ( - - ))} - - -
-
- ); -} - -const palette = ['#1f6feb', '#0f9b68', '#d68c00', '#c74141', '#6f42c1', '#14746f']; - -function pickColor(index: number) { - return palette[index % palette.length] || '#1f6feb'; -} diff --git a/packages/dashboard/app/history/components/sessions-table.tsx b/packages/dashboard/app/history/components/sessions-table.tsx deleted file mode 100644 index 4b8a786..0000000 --- a/packages/dashboard/app/history/components/sessions-table.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import Link from 'next/link'; -import type { SessionRow } from '@codex-switch/shared'; - -export function SessionsTable({ - rows, - page, - pageSize, - total, -}: Readonly<{ - rows: SessionRow[]; - page: number; - pageSize: number; - total: number; -}>) { - const totalPages = Math.max(1, Math.ceil(total / pageSize)); - - return ( -
-
-

Recent sessions

-

- Page {page} of {totalPages} -

-
- -
- - - - - - - - - - - - - {rows.map((row) => ( - - - - - - - - - ))} - -
AccountStartedDurationRequestsToken inToken out
{row.account}{new Date(row.startedAt).toLocaleString()}{formatDuration(row.startedAt, row.endedAt)}{row.requestCount}{row.tokenIn ?? 'n/a'}{row.tokenOut ?? 'n/a'}
-
- -
- - Previous - - = totalPages} href={`/history?page=${page + 1}`}> - Next - -
-
- ); -} - -function PagerLink({ - href, - disabled, - children, -}: Readonly<{ href: string; disabled: boolean; children: React.ReactNode }>) { - if (disabled) { - return ( - - {children} - - ); - } - - return ( - - {children} - - ); -} - -function formatDuration(startedAt: number, endedAt: number | null) { - if (!endedAt) { - return 'active'; - } - - const minutes = Math.max(1, Math.round((endedAt - startedAt) / 60_000)); - return `${minutes} min`; -} diff --git a/packages/dashboard/app/history/page.tsx b/packages/dashboard/app/history/page.tsx deleted file mode 100644 index 7c09d22..0000000 --- a/packages/dashboard/app/history/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { AppShell } from '../components/app-shell'; -import { readRecentSessions, readUsageSnapshot } from '../../lib/db'; -import { RequestsPerDayChart } from './components/requests-per-day-chart'; -import { SessionsTable } from './components/sessions-table'; - -const PAGE_SIZE = 20; - -export default async function HistoryPage({ - searchParams, -}: Readonly<{ - searchParams?: Promise>; -}>) { - const resolvedSearchParams = - (searchParams ? await searchParams : undefined) ?? {}; - const page = Math.max(1, Number(resolvedSearchParams.page ?? '1')); - const to = Date.now(); - const from = to - 30 * 24 * 60 * 60 * 1000; - const usage = readUsageSnapshot(from, to); - const sessions = readRecentSessions(PAGE_SIZE, (page - 1) * PAGE_SIZE); - - return ( - -
- -
-
- -
-
- ); -} diff --git a/packages/dashboard/app/layout.tsx b/packages/dashboard/app/layout.tsx deleted file mode 100644 index d9416c6..0000000 --- a/packages/dashboard/app/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Metadata } from 'next'; -import { Toaster } from 'sonner'; -import './globals.css'; - -export const metadata: Metadata = { - title: 'codex-switch', - description: 'Native Codex account switcher dashboard', -}; - -export default function RootLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { - return ( - - - {children} - - - - ); -} diff --git a/packages/dashboard/app/page.tsx b/packages/dashboard/app/page.tsx deleted file mode 100644 index 6d37395..0000000 --- a/packages/dashboard/app/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { AppShell } from './components/app-shell'; -import { AccountGridClient } from './components/account-grid-client'; -import { readAccountsSnapshot } from '../lib/db'; - -export default function HomePage() { - const accounts = readAccountsSnapshot(); - const pollingEnabled = process.env.CODEX_SWITCH_POLL_DISABLED !== '1'; - - return ( - - - - ); -} diff --git a/packages/dashboard/lib/account-availability.ts b/packages/dashboard/lib/account-availability.ts deleted file mode 100644 index c0eeb48..0000000 --- a/packages/dashboard/lib/account-availability.ts +++ /dev/null @@ -1,79 +0,0 @@ -export interface WaitForAccountReadyOptions { - name: string; - signal?: AbortSignal; - timeoutMs?: number; - intervalMs?: number; - fetchAccountNames?: (signal?: AbortSignal) => Promise; - now?: () => number; - sleep?: (ms: number, signal?: AbortSignal) => Promise; -} - -export async function waitForAccountReady(options: WaitForAccountReadyOptions) { - const fetchAccountNames = options.fetchAccountNames ?? readAccountNames; - const now = options.now ?? Date.now; - const sleep = options.sleep ?? delay; - const startedAt = now(); - - while (now() - startedAt < (options.timeoutMs ?? 5 * 60_000)) { - assertNotAborted(options.signal); - await sleep(options.intervalMs ?? 2000, options.signal); - assertNotAborted(options.signal); - const names = await fetchAccountNames(options.signal); - if (names.includes(options.name)) { - return true; - } - } - - return false; -} - -async function readAccountNames(signal?: AbortSignal) { - const response = await fetch('/api/accounts', { - cache: 'no-store', - ...(signal ? { signal } : {}), - }); - const payload = (await response.json()) as { - accounts?: Array<{ name?: string }>; - }; - - return (payload.accounts ?? []) - .map((account) => account.name) - .filter((name): name is string => typeof name === 'string' && name.length > 0); -} - -function delay(ms: number, signal?: AbortSignal) { - if (signal?.aborted) { - return Promise.reject(createAbortError()); - } - - return new Promise((resolve, reject) => { - const timer = window.setTimeout(() => { - cleanup(); - resolve(); - }, ms); - - const onAbort = () => { - cleanup(); - reject(createAbortError()); - }; - - const cleanup = () => { - window.clearTimeout(timer); - signal?.removeEventListener('abort', onAbort); - }; - - signal?.addEventListener('abort', onAbort, { once: true }); - }); -} - -function createAbortError() { - return Object.assign(new Error('The wait operation was aborted.'), { - name: 'AbortError', - }); -} - -function assertNotAborted(signal?: AbortSignal) { - if (signal?.aborted) { - throw createAbortError(); - } -} diff --git a/packages/dashboard/lib/account-name.ts b/packages/dashboard/lib/account-name.ts deleted file mode 100644 index 3246ef4..0000000 --- a/packages/dashboard/lib/account-name.ts +++ /dev/null @@ -1,9 +0,0 @@ -const accountNamePattern = /^[a-z0-9_-]{1,32}$/i; - -export function validateAccountName(value: unknown) { - if (typeof value !== 'string' || !accountNamePattern.test(value.trim())) { - return null; - } - - return value.trim(); -} diff --git a/packages/dashboard/lib/cli-spawn.ts b/packages/dashboard/lib/cli-spawn.ts deleted file mode 100644 index 00b1a0b..0000000 --- a/packages/dashboard/lib/cli-spawn.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { spawn } from 'node:child_process'; -import process from 'node:process'; - -export async function runCli(args: string[]) { - const invocation = resolveCliInvocation(args); - - return new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => { - const child = spawn(invocation.command, invocation.args, { - env: process.env, - stdio: ['ignore', 'pipe', 'pipe'], - windowsHide: true, - }); - - let stdout = ''; - let stderr = ''; - child.stdout.on('data', (chunk) => { - stdout += String(chunk); - }); - child.stderr.on('data', (chunk) => { - stderr += String(chunk); - }); - child.on('close', (code) => { - resolve({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }); - }); - }); -} - -export function openCliInNewTerminal(args: string[]) { - const invocation = resolveCliInvocation(args); - - if (process.platform === 'win32') { - const commandLine = [invocation.command, ...invocation.args].map(quoteWindows).join(' '); - spawn('cmd.exe', ['/d', '/s', '/c', `start "codex-switch" ${commandLine}`], { - detached: true, - stdio: 'ignore', - windowsHide: false, - }).unref(); - return; - } - - if (process.platform === 'darwin') { - const commandLine = [invocation.command, ...invocation.args].map(quotePosix).join(' '); - spawn( - 'osascript', - ['-e', `tell application "Terminal" to do script ${quoteAppleScript(commandLine)}`], - { detached: true, stdio: 'ignore' }, - ).unref(); - return; - } - - spawn('x-terminal-emulator', ['-e', invocation.command, ...invocation.args], { - detached: true, - stdio: 'ignore', - }).unref(); -} - -function resolveCliInvocation(args: string[]) { - const entry = process.env.CS_CLI_ENTRY; - if (entry) { - return { - command: process.env.CS_NODE_BINARY ?? process.execPath, - args: [entry, ...args], - }; - } - - return { - command: 'cs', - args, - }; -} - -function quoteWindows(value: string) { - if (!/[ \t"]/u.test(value)) { - return value; - } - - return JSON.stringify(value); -} - -function quotePosix(value: string) { - const escapedSingleQuote = String.raw`'"'"'`; - return `'${value.replaceAll("'", escapedSingleQuote)}'`; -} - -function quoteAppleScript(value: string) { - return JSON.stringify(value); -} diff --git a/packages/dashboard/lib/day-buckets.ts b/packages/dashboard/lib/day-buckets.ts deleted file mode 100644 index e9a97d9..0000000 --- a/packages/dashboard/lib/day-buckets.ts +++ /dev/null @@ -1,22 +0,0 @@ -export function formatLocalDayBucket(value: number | Date) { - const date = value instanceof Date ? new Date(value) : new Date(value); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; -} - -export function eachLocalDayBucket(from: number, to: number) { - const buckets: string[] = []; - const cursor = new Date(from); - cursor.setHours(0, 0, 0, 0); - const end = new Date(to); - end.setHours(0, 0, 0, 0); - - while (cursor <= end) { - buckets.push(formatLocalDayBucket(cursor)); - cursor.setDate(cursor.getDate() + 1); - } - - return buckets; -} diff --git a/packages/dashboard/lib/db.ts b/packages/dashboard/lib/db.ts deleted file mode 100644 index 6254e35..0000000 --- a/packages/dashboard/lib/db.ts +++ /dev/null @@ -1,254 +0,0 @@ -import type { - AccountSummary, - AccountUsageSnapshot, - SessionRow, -} from '@codex-switch/shared'; -import { - formatQuotaErrorMessage, - recentSessionsSql, - requestsPerDaySql, - vaultStateFile, -} from '@codex-switch/shared'; -import Database from 'better-sqlite3'; -import { existsSync } from 'node:fs'; -import { eachLocalDayBucket } from './day-buckets'; - -type UsagePoint = Record; - -export interface UsageSnapshot { - accounts: string[]; - requestsPerDay: UsagePoint[]; -} - -export function readAccountsSnapshot(): AccountSummary[] { - return withDatabase((db) => { - const active = db - .prepare('SELECT account FROM active WHERE id = 1') - .get() as { account: string } | undefined; - const latestQuotaRows = safeAll<{ - account: string; - captured_at: number; - five_hour_percent: number | null; - five_hour_reset_at: number | null; - weekly_percent: number | null; - weekly_reset_at: number | null; - source: 'wham' | 'codex'; - stale_reason: string | null; - }>( - db, - `SELECT - account, - captured_at, - five_hour_percent, - five_hour_reset_at, - weekly_percent, - weekly_reset_at, - source, - stale_reason - FROM quota_cache - ORDER BY captured_at DESC, account ASC`, - ); - const quotaMap = new Map( - latestQuotaRows.map((row) => [ - row.account, - { - fiveHour: { - percentLeft: row.five_hour_percent, - resetAt: row.five_hour_reset_at, - }, - weekly: { - percentLeft: row.weekly_percent, - resetAt: row.weekly_reset_at, - }, - capturedAt: row.captured_at, - ageMs: Math.max(0, Date.now() - row.captured_at), - requiresReauth: row.stale_reason === 'requires_reauth', - source: 'cache', - error: row.stale_reason - ? { - code: row.stale_reason, - message: formatQuotaError(row.stale_reason), - } - : null, - }, - ]), - ); - const authStateRows = safeAll<{ - account: string; - requires_reauth: number; - last_error: string | null; - }>( - db, - `SELECT account, requires_reauth, last_error - FROM account_auth_state`, - ); - const authStateMap = new Map( - authStateRows.map((row) => [ - row.account, - { - requiresReauth: row.requires_reauth === 1, - lastError: row.last_error, - }, - ]), - ); - - const accounts = db.prepare( - `SELECT name, email, plan, added_at, last_used_at, notes - FROM accounts - ORDER BY COALESCE(last_used_at, added_at) DESC, name ASC`, - ).all() as Array<{ - name: string; - email: string | null; - plan: string | null; - added_at: number; - last_used_at: number | null; - notes: string | null; - }>; - - return accounts.map((account) => ({ - name: account.name, - email: account.email, - plan: account.plan, - addedAt: account.added_at, - lastUsedAt: account.last_used_at, - notes: account.notes, - isActive: active?.account === account.name, - latestQuota: mergeUsageState( - quotaMap.get(account.name) ?? null, - authStateMap.get(account.name), - ), - })); - }, []); -} - -export function readUsageSnapshot(from: number, to: number): UsageSnapshot { - return withDatabase((db) => { - const requestRows = db.prepare(requestsPerDaySql).all(from, to) as Array<{ - account: string; - bucket: string; - request_count: number; - }>; - const accountSet = new Set(); - - requestRows.forEach((row) => accountSet.add(row.account)); - - const accounts = Array.from(accountSet).sort(); - const requestBuckets = new Map(); - - for (const bucket of eachLocalDayBucket(from, to)) { - requestBuckets.set(bucket, { bucket }); - } - - for (const row of requestRows) { - const entry = requestBuckets.get(row.bucket) ?? { bucket: row.bucket }; - entry[row.account] = row.request_count; - requestBuckets.set(row.bucket, entry); - } - - return { - accounts, - requestsPerDay: Array.from(requestBuckets.values()), - }; - }, { accounts: [], requestsPerDay: [] }); -} - -export function readRecentSessions(limit: number, offset: number) { - return withDatabase( - (db) => { - const rows = db.prepare(recentSessionsSql).all(limit, offset) as Array<{ - session_id: string; - account: string; - started_at: number; - ended_at: number | null; - request_count: number; - token_in: number | null; - token_out: number | null; - }>; - const total = db.prepare('SELECT COUNT(*) AS total FROM sessions').get() as { - total: number; - }; - - return { - rows: rows.map( - (row) => - ({ - sessionId: row.session_id, - account: row.account, - startedAt: row.started_at, - endedAt: row.ended_at, - requestCount: row.request_count, - tokenIn: row.token_in, - tokenOut: row.token_out, - }) satisfies SessionRow, - ), - total: total.total, - }; - }, - { rows: [], total: 0 }, - ); -} - -function withDatabase(run: (db: Database.Database) => T, fallback: T) { - if (!existsSync(vaultStateFile())) { - return fallback; - } - - const db = new Database(vaultStateFile(), { - readonly: true, - fileMustExist: true, - }); - - try { - db.pragma('query_only = ON'); - return run(db); - } catch { - return fallback; - } finally { - db.close(); - } -} - -function safeAll(db: Database.Database, sql: string, ...params: unknown[]) { - try { - return db.prepare(sql).all(...params) as T[]; - } catch { - return []; - } -} - -function mergeUsageState( - usage: AccountUsageSnapshot | null, - authState: - | { - requiresReauth: boolean; - lastError: string | null; - } - | undefined, -) { - if (!usage && !authState) { - return null; - } - - if (!authState) { - return usage; - } - - return { - fiveHour: usage?.fiveHour ?? null, - weekly: usage?.weekly ?? null, - capturedAt: usage?.capturedAt ?? null, - ageMs: usage?.ageMs ?? null, - requiresReauth: authState.requiresReauth, - source: usage?.source ?? null, - error: authState.lastError - ? { - code: authState.lastError, - message: formatQuotaError(authState.lastError), - } - : usage?.error ?? null, - } satisfies AccountUsageSnapshot; -} - -function formatQuotaError(code: string) { - return formatQuotaErrorMessage(code); -} diff --git a/packages/dashboard/lib/lock-probe.ts b/packages/dashboard/lib/lock-probe.ts deleted file mode 100644 index 2c5a9c0..0000000 --- a/packages/dashboard/lib/lock-probe.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { vaultLockFile } from '@codex-switch/shared'; -import { existsSync, readFileSync, rmSync } from 'node:fs'; - -interface LockState { - pid: number; - account: string; - startedAt: number; -} - -export function readActiveLock() { - if (!existsSync(vaultLockFile())) { - return null; - } - - try { - const parsed = JSON.parse(readFileSync(vaultLockFile(), 'utf8')) as LockState; - if (isProcessAlive(parsed.pid)) { - return parsed; - } - - rmSync(vaultLockFile(), { force: true }); - return null; - } catch { - return null; - } -} - -function isProcessAlive(pid: number) { - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} diff --git a/packages/dashboard/lib/request-guard.ts b/packages/dashboard/lib/request-guard.ts deleted file mode 100644 index 8563166..0000000 --- a/packages/dashboard/lib/request-guard.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextResponse } from 'next/server'; - -export function rejectCrossOrigin(request: Request) { - const origin = request.headers.get('origin'); - const host = request.headers.get('host'); - - if (!origin || !host) { - return null; - } - - try { - if (new URL(origin).host === host) { - return null; - } - } catch { - return NextResponse.json({ error: 'Invalid origin header.' }, { status: 400 }); - } - - return NextResponse.json({ error: 'Cross-origin requests are blocked.' }, { status: 403 }); -} diff --git a/packages/dashboard/lib/usage-hooks.ts b/packages/dashboard/lib/usage-hooks.ts deleted file mode 100644 index fcce7ec..0000000 --- a/packages/dashboard/lib/usage-hooks.ts +++ /dev/null @@ -1,91 +0,0 @@ -'use client'; - -import type { AccountUsageSnapshot, UsageResponse } from '@codex-switch/shared'; -import { startTransition, useEffect, useEffectEvent, useState } from 'react'; - -type UsageMap = Record; - -export function useUsagePolling(initialUsage: UsageMap, ttlMs: number, enabled: boolean) { - const [usage, setUsage] = useState(initialUsage); - const [pending, setPending] = useState(false); - - const fetchUsage = useEffectEvent(async (options?: { account?: string; force?: boolean }) => { - const params = new URLSearchParams(); - if (options?.account) { - params.set('account', options.account); - } - if (options?.force) { - params.set('refresh', '1'); - } - - const suffix = params.size > 0 ? `?${params.toString()}` : ''; - setPending(true); - - try { - const response = await fetch(`/api/usage${suffix}`, { - cache: 'no-store', - }); - if (!response.ok) { - return; - } - - const payload = (await response.json()) as UsageResponse; - startTransition(() => { - setUsage((current) => ({ - ...current, - ...payload.accounts, - })); - }); - } finally { - setPending(false); - } - }); - - useEffect(() => { - if (!enabled) { - return; - } - - let timer: number | null = null; - - const schedule = () => { - if (timer !== null) { - window.clearInterval(timer); - timer = null; - } - - if (document.visibilityState === 'visible') { - timer = window.setInterval(() => { - void fetchUsage(); - }, Math.max(60_000, ttlMs)); - } - }; - - const onVisibilityChange = () => { - if (document.visibilityState === 'visible') { - void fetchUsage(); - } - - schedule(); - }; - - void fetchUsage(); - schedule(); - document.addEventListener('visibilitychange', onVisibilityChange); - - return () => { - if (timer !== null) { - window.clearInterval(timer); - } - document.removeEventListener('visibilitychange', onVisibilityChange); - }; - }, [enabled, ttlMs]); - - return { - usage, - pending, - refresh(account?: string) { - return fetchUsage(account ? { account, force: true } : { force: true }); - }, - }; -} diff --git a/packages/dashboard/lib/usage-response.ts b/packages/dashboard/lib/usage-response.ts deleted file mode 100644 index f742952..0000000 --- a/packages/dashboard/lib/usage-response.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { FetchQuotaWithCacheResult } from '@codex-switch/cli/api'; -import { - formatQuotaErrorMessage, - type AccountUsageSnapshot, - type QuotaErrorSummary, - type UsageResponse, -} from '@codex-switch/shared'; - -export async function buildUsageResponse( - accounts: string[], - options: { - ttlMs: number; - fetchQuota: (account: string) => Promise; - now?: () => number; - }, -): Promise { - const now = options.now ?? Date.now; - const payload: UsageResponse = { - accounts: {}, - ttlMs: options.ttlMs, - }; - - for (const account of accounts) { - try { - const result = await options.fetchQuota(account); - payload.accounts[account] = quotaResultToUsageSnapshot(result, now()); - } catch (error) { - payload.accounts[account] = quotaErrorToUsageSnapshot(error); - } - } - - return payload; -} - -export function quotaResultToUsageSnapshot( - result: FetchQuotaWithCacheResult, - now = Date.now(), -): AccountUsageSnapshot { - return { - fiveHour: result.row - ? { - percentLeft: result.row.fiveHourPercent, - resetAt: result.row.fiveHourResetAt, - } - : null, - weekly: result.row - ? { - percentLeft: result.row.weeklyPercent, - resetAt: result.row.weeklyResetAt, - } - : null, - capturedAt: result.row?.capturedAt ?? null, - ageMs: result.row ? Math.max(0, now - result.row.capturedAt) : null, - requiresReauth: result.requiresReauth, - source: result.source, - error: result.error, - }; -} - -export function quotaErrorToUsageSnapshot(error: unknown): AccountUsageSnapshot { - const code = readQuotaErrorCode(error); - - return { - fiveHour: null, - weekly: null, - capturedAt: null, - ageMs: null, - requiresReauth: isReauthErrorCode(code), - source: null, - error: toQuotaErrorSummary(code, error), - }; -} - -function toQuotaErrorSummary(code: string, error: unknown): QuotaErrorSummary { - return { - code, - message: - code === 'unknown' && error instanceof Error && error.message - ? error.message - : formatQuotaErrorMessage(code), - }; -} - -function readQuotaErrorCode(error: unknown) { - return ( - (error instanceof Error && - 'code' in error && - typeof error.code === 'string' && - error.code) || - 'unknown' - ); -} - -function isReauthErrorCode(code: string) { - return ( - code === 'invalid_grant' || - code === 'requires_reauth' || - code === 'unauthorized' || - code === 'vault_missing' || - code === 'vault_invalid' - ); -} diff --git a/packages/dashboard/next-env.d.ts b/packages/dashboard/next-env.d.ts deleted file mode 100644 index 9edff1c..0000000 --- a/packages/dashboard/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -import "./.next/types/routes.d.ts"; - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/packages/dashboard/next.config.ts b/packages/dashboard/next.config.ts deleted file mode 100644 index 61c56c2..0000000 --- a/packages/dashboard/next.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { NextConfig } from 'next'; - -const nextConfig: NextConfig = { - output: 'standalone', - serverExternalPackages: ['better-sqlite3'], -}; - -export default nextConfig; diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json deleted file mode 100644 index 1e79e58..0000000 --- a/packages/dashboard/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@codex-switch/dashboard", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "build": "pnpm exec next build", - "dev": "pnpm exec next dev", - "start": "pnpm exec next start", - "typecheck": "pnpm exec tsc -p tsconfig.json --noEmit", - "test": "pnpm exec tsx --test test/**/*.test.ts" - }, - "dependencies": { - "@codex-switch/cli": "workspace:*", - "@codex-switch/shared": "workspace:*", - "better-sqlite3": "12.9.0", - "next": "16.2.4", - "react": "19.2.0", - "react-dom": "19.2.0", - "recharts": "3.8.1", - "sonner": "2.0.7" - }, - "devDependencies": { - "@tailwindcss/postcss": "4.2.4", - "@types/better-sqlite3": "7.6.13", - "@types/react": "19.2.2", - "@types/react-dom": "19.2.2", - "tailwindcss": "4.2.4" - } -} diff --git a/packages/dashboard/postcss.config.mjs b/packages/dashboard/postcss.config.mjs deleted file mode 100644 index a34a3d5..0000000 --- a/packages/dashboard/postcss.config.mjs +++ /dev/null @@ -1,5 +0,0 @@ -export default { - plugins: { - '@tailwindcss/postcss': {}, - }, -}; diff --git a/packages/dashboard/test/account-availability.test.ts b/packages/dashboard/test/account-availability.test.ts deleted file mode 100644 index 72e4874..0000000 --- a/packages/dashboard/test/account-availability.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { waitForAccountReady } from '../lib/account-availability'; - -test('waitForAccountReady resolves when the account appears', async () => { - let now = 0; - let attempts = 0; - - const ready = await waitForAccountReady({ - name: 'personal', - timeoutMs: 10, - intervalMs: 1, - now: () => now, - sleep: async () => { - now += 1; - }, - fetchAccountNames: async () => { - attempts += 1; - return attempts >= 2 ? ['personal'] : []; - }, - }); - - assert.equal(ready, true); -}); - -test('waitForAccountReady rejects with AbortError when aborted', async () => { - const controller = new AbortController(); - controller.abort(); - - await assert.rejects( - () => - waitForAccountReady({ - name: 'personal', - signal: controller.signal, - fetchAccountNames: async () => [], - sleep: async () => undefined, - }), - (error: unknown) => error instanceof Error && error.name === 'AbortError', - ); -}); diff --git a/packages/dashboard/test/day-buckets.test.ts b/packages/dashboard/test/day-buckets.test.ts deleted file mode 100644 index 076cb4c..0000000 --- a/packages/dashboard/test/day-buckets.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { eachLocalDayBucket, formatLocalDayBucket } from '../lib/day-buckets'; - -test('formatLocalDayBucket keeps the local calendar day', () => { - const timestamp = new Date(2026, 3, 23, 0, 30, 0, 0).getTime(); - assert.equal(formatLocalDayBucket(timestamp), '2026-04-23'); -}); - -test('eachLocalDayBucket fills inclusive local-day buckets', () => { - const from = new Date(2026, 3, 23, 8, 0, 0, 0).getTime(); - const to = new Date(2026, 3, 25, 18, 0, 0, 0).getTime(); - - assert.deepEqual(eachLocalDayBucket(from, to), [ - '2026-04-23', - '2026-04-24', - '2026-04-25', - ]); -}); diff --git a/packages/dashboard/test/usage-response.test.ts b/packages/dashboard/test/usage-response.test.ts deleted file mode 100644 index feebb13..0000000 --- a/packages/dashboard/test/usage-response.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { - buildUsageResponse, - quotaResultToUsageSnapshot, -} from '../lib/usage-response'; - -test('quotaResultToUsageSnapshot maps cached quota rows', () => { - const snapshot = quotaResultToUsageSnapshot( - { - row: { - account: 'personal', - capturedAt: 1_000, - fiveHourPercent: 42, - fiveHourResetAt: 2_000, - weeklyPercent: null, - weeklyResetAt: null, - source: 'wham', - staleReason: null, - }, - fresh: false, - reason: 'cache', - requiresReauth: false, - source: 'cache', - error: null, - }, - 1_600, - ); - - assert.deepEqual(snapshot, { - fiveHour: { - percentLeft: 42, - resetAt: 2_000, - }, - weekly: { - percentLeft: null, - resetAt: null, - }, - capturedAt: 1_000, - ageMs: 600, - requiresReauth: false, - source: 'cache', - error: null, - }); -}); - -test('buildUsageResponse isolates per-account failures', async () => { - const payload = await buildUsageResponse(['personal', 'work'], { - ttlMs: 120_000, - now: () => 2_000, - fetchQuota: async (account) => { - if (account === 'personal') { - return { - row: { - account: 'personal', - capturedAt: 1_000, - fiveHourPercent: 70, - fiveHourResetAt: 2_500, - weeklyPercent: 85, - weeklyResetAt: 8_000, - source: 'wham', - staleReason: null, - }, - fresh: true, - reason: 'probed', - requiresReauth: false, - source: 'wham', - error: null, - }; - } - - throw Object.assign(new Error('Vault snapshot is broken.'), { - code: 'vault_invalid', - }); - }, - }); - - assert.equal(payload.ttlMs, 120_000); - assert.equal(payload.accounts.personal?.fiveHour?.percentLeft, 70); - assert.equal(payload.accounts.personal?.weekly?.percentLeft, 85); - assert.equal(payload.accounts.work?.requiresReauth, true); - assert.deepEqual(payload.accounts.work?.error, { - code: 'vault_invalid', - message: 'Vault snapshot could not be parsed.', - }); -}); diff --git a/packages/dashboard/tsconfig.json b/packages/dashboard/tsconfig.json deleted file mode 100644 index a1f7c6a..0000000 --- a/packages/dashboard/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "baseUrl": ".", - "jsx": "preserve", - "lib": ["dom", "dom.iterable", "es2022"], - "module": "ESNext", - "moduleResolution": "Bundler", - "noEmit": true, - "plugins": [ - { - "name": "next" - } - ] - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] -} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 684b515..52a7e6a 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,7 +1,6 @@ export * from './auth-schema.js'; export * from './codex-oauth.js'; export * from './paths.js'; -export * from './queries.js'; export * from './quota-errors.js'; export * from './quota-probe.js'; export * from './schema.js'; diff --git a/packages/shared/src/paths.ts b/packages/shared/src/paths.ts index cdee7ab..63cfba8 100644 --- a/packages/shared/src/paths.ts +++ b/packages/shared/src/paths.ts @@ -17,18 +17,6 @@ export function authFile() { return path.join(codexHome(), 'auth.json'); } -export function codexHistoryFile() { - return path.join(codexHome(), 'history.jsonl'); -} - -export function codexLogsDatabaseFile() { - return path.join(codexHome(), 'logs_2.sqlite'); -} - -export function sessionIndexFile() { - return path.join(codexHome(), 'session_index.jsonl'); -} - export function vaultAccountsDir() { return path.join(vaultRoot(), 'accounts'); } diff --git a/packages/shared/src/queries.ts b/packages/shared/src/queries.ts deleted file mode 100644 index a31ccf4..0000000 --- a/packages/shared/src/queries.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const requestsPerDaySql = ` - SELECT - account, - strftime('%Y-%m-%d', started_at / 1000, 'unixepoch', 'localtime') AS bucket, - SUM(request_count) AS request_count - FROM sessions - WHERE started_at BETWEEN ? AND ? - GROUP BY account, bucket - ORDER BY bucket ASC, account ASC -`; - -export const recentSessionsSql = ` - SELECT - session_id, - account, - started_at, - ended_at, - request_count, - token_in, - token_out - FROM sessions - ORDER BY started_at DESC - LIMIT ? OFFSET ? -`; diff --git a/packages/shared/src/schema.sql b/packages/shared/src/schema.sql index 081ba70..b63ea44 100644 --- a/packages/shared/src/schema.sql +++ b/packages/shared/src/schema.sql @@ -7,18 +7,6 @@ CREATE TABLE IF NOT EXISTS accounts ( notes TEXT ); -CREATE TABLE IF NOT EXISTS quota_samples ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - account TEXT NOT NULL, - captured_at INTEGER NOT NULL, - limit_kind TEXT NOT NULL, - used INTEGER, - remaining INTEGER, - reset_at INTEGER, - source TEXT NOT NULL, - UNIQUE(account, captured_at, limit_kind, source) -); - CREATE TABLE IF NOT EXISTS quota_cache ( account TEXT PRIMARY KEY, captured_at INTEGER NOT NULL, @@ -37,27 +25,11 @@ CREATE TABLE IF NOT EXISTS account_auth_state ( last_error_at INTEGER ); -CREATE TABLE IF NOT EXISTS sessions ( - session_id TEXT PRIMARY KEY, - account TEXT NOT NULL, - started_at INTEGER NOT NULL, - ended_at INTEGER, - request_count INTEGER NOT NULL DEFAULT 0, - token_in INTEGER, - token_out INTEGER -); - CREATE TABLE IF NOT EXISTS active ( id INTEGER PRIMARY KEY CHECK (id = 1), account TEXT NOT NULL, switched_at INTEGER NOT NULL ); -CREATE INDEX IF NOT EXISTS idx_quota_samples_account_captured_at - ON quota_samples(account, captured_at DESC); - CREATE INDEX IF NOT EXISTS idx_quota_cache_captured_at ON quota_cache(captured_at DESC); - -CREATE INDEX IF NOT EXISTS idx_sessions_started_at - ON sessions(started_at DESC); diff --git a/packages/shared/src/schema.ts b/packages/shared/src/schema.ts index f58798b..c3a439e 100644 --- a/packages/shared/src/schema.ts +++ b/packages/shared/src/schema.ts @@ -8,18 +8,6 @@ CREATE TABLE IF NOT EXISTS accounts ( notes TEXT ); -CREATE TABLE IF NOT EXISTS quota_samples ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - account TEXT NOT NULL, - captured_at INTEGER NOT NULL, - limit_kind TEXT NOT NULL, - used INTEGER, - remaining INTEGER, - reset_at INTEGER, - source TEXT NOT NULL, - UNIQUE(account, captured_at, limit_kind, source) -); - CREATE TABLE IF NOT EXISTS quota_cache ( account TEXT PRIMARY KEY, captured_at INTEGER NOT NULL, @@ -38,28 +26,12 @@ CREATE TABLE IF NOT EXISTS account_auth_state ( last_error_at INTEGER ); -CREATE TABLE IF NOT EXISTS sessions ( - session_id TEXT PRIMARY KEY, - account TEXT NOT NULL, - started_at INTEGER NOT NULL, - ended_at INTEGER, - request_count INTEGER NOT NULL DEFAULT 0, - token_in INTEGER, - token_out INTEGER -); - CREATE TABLE IF NOT EXISTS active ( id INTEGER PRIMARY KEY CHECK (id = 1), account TEXT NOT NULL, switched_at INTEGER NOT NULL ); -CREATE INDEX IF NOT EXISTS idx_quota_samples_account_captured_at - ON quota_samples(account, captured_at DESC); - CREATE INDEX IF NOT EXISTS idx_quota_cache_captured_at ON quota_cache(captured_at DESC); - -CREATE INDEX IF NOT EXISTS idx_sessions_started_at - ON sessions(started_at DESC); `; diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 8d432eb..96c7b91 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -7,16 +7,6 @@ export interface AccountRecord { notes: string | null; } -export interface QuotaSample { - account: string; - capturedAt: number; - limitKind: string; - used: number | null; - remaining: number | null; - resetAt: number | null; - source: string; -} - export interface QuotaWindow { percentLeft: number | null; resetAt: number | null; @@ -55,42 +45,11 @@ export interface AccountAuthState { lastErrorAt: number | null; } -export interface AccountUsageSnapshot { - fiveHour: QuotaWindow | null; - weekly: QuotaWindow | null; - capturedAt: number | null; - ageMs: number | null; - requiresReauth: boolean; - source: QuotaDisplaySource | null; - error: QuotaErrorSummary | null; -} - -export interface UsageResponse { - accounts: Record; - ttlMs: number; -} - -export interface SessionRow { - account: string; - sessionId: string; - startedAt: number; - endedAt: number | null; - requestCount: number; - tokenIn: number | null; - tokenOut: number | null; -} - export interface ActiveAccountState { account: string; switchedAt: number; } -export interface VaultConfig { - port: number | null; - theme: 'system' | 'light' | 'dark'; - defaultAccount: string | null; -} - export interface AuthTokenBundle { id_token?: string | null; access_token?: string | null; @@ -106,8 +65,3 @@ export interface AuthJson { last_refresh?: string | null; [key: string]: unknown; } - -export interface AccountSummary extends AccountRecord { - isActive: boolean; - latestQuota: AccountUsageSnapshot | null; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 814546a..d0036f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,25 @@ settings: importers: .: + dependencies: + '@clack/prompts': + specifier: 1.2.0 + version: 1.2.0 + better-sqlite3: + specifier: 12.9.0 + version: 12.9.0 + commander: + specifier: 14.0.3 + version: 14.0.3 + execa: + specifier: 9.6.1 + version: 9.6.1 + pino: + specifier: 10.3.1 + version: 10.3.1 + zod: + specifier: 4.3.6 + version: 4.3.6 devDependencies: '@eslint/js': specifier: 9.39.1 @@ -44,21 +63,12 @@ importers: better-sqlite3: specifier: 12.9.0 version: 12.9.0 - chokidar: - specifier: 5.0.0 - version: 5.0.0 commander: specifier: 14.0.3 version: 14.0.3 execa: specifier: 9.6.1 version: 9.6.1 - get-port: - specifier: 7.1.0 - version: 7.1.0 - open: - specifier: 11.0.0 - version: 11.0.0 pino: specifier: 10.3.1 version: 10.3.1 @@ -70,49 +80,6 @@ importers: specifier: 7.6.13 version: 7.6.13 - packages/dashboard: - dependencies: - '@codex-switch/cli': - specifier: workspace:* - version: link:../cli - '@codex-switch/shared': - specifier: workspace:* - version: link:../shared - better-sqlite3: - specifier: 12.9.0 - version: 12.9.0 - next: - specifier: 16.2.4 - version: 16.2.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: - specifier: 19.2.0 - version: 19.2.0 - react-dom: - specifier: 19.2.0 - version: 19.2.0(react@19.2.0) - recharts: - specifier: 3.8.1 - version: 3.8.1(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react-is@19.2.5)(react@19.2.0)(redux@5.0.1) - sonner: - specifier: 2.0.7 - version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - devDependencies: - '@tailwindcss/postcss': - specifier: 4.2.4 - version: 4.2.4 - '@types/better-sqlite3': - specifier: 7.6.13 - version: 7.6.13 - '@types/react': - specifier: 19.2.2 - version: 19.2.2 - '@types/react-dom': - specifier: 19.2.2 - version: 19.2.2(@types/react@19.2.2) - tailwindcss: - specifier: 4.2.4 - version: 4.2.4 - packages/shared: dependencies: zod: @@ -121,19 +88,12 @@ importers: packages: - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - '@clack/core@1.2.0': resolution: {integrity: sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==} '@clack/prompts@1.2.0': resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -348,230 +308,6 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@img/colour@1.1.0': - resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} - engines: {node: '>=18'} - - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@next/env@16.2.4': - resolution: {integrity: sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==} - - '@next/swc-darwin-arm64@16.2.4': - resolution: {integrity: sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@next/swc-darwin-x64@16.2.4': - resolution: {integrity: sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@next/swc-linux-arm64-gnu@16.2.4': - resolution: {integrity: sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@next/swc-linux-arm64-musl@16.2.4': - resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@next/swc-linux-x64-gnu@16.2.4': - resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@next/swc-linux-x64-musl@16.2.4': - resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@next/swc-win32-arm64-msvc@16.2.4': - resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@next/swc-win32-x64-msvc@16.2.4': - resolution: {integrity: sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -587,17 +323,6 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} - '@reduxjs/toolkit@2.11.2': - resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 || ^19 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -605,137 +330,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@standard-schema/utils@0.3.0': - resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - - '@tailwindcss/node@4.2.4': - resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==} - - '@tailwindcss/oxide-android-arm64@4.2.4': - resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.2.4': - resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.2.4': - resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==} - engines: {node: '>= 20'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.2.4': - resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': - resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==} - engines: {node: '>= 20'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': - resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-arm64-musl@4.2.4': - resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-linux-x64-gnu@4.2.4': - resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-x64-musl@4.2.4': - resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-wasm32-wasi@4.2.4': - resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': - resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.2.4': - resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.2.4': - resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} - engines: {node: '>= 20'} - - '@tailwindcss/postcss@4.2.4': - resolution: {integrity: sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==} - '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} - '@types/d3-array@3.2.2': - resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} - - '@types/d3-color@3.1.3': - resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - - '@types/d3-ease@3.0.2': - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - - '@types/d3-interpolate@3.0.4': - resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - - '@types/d3-path@3.1.1': - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - - '@types/d3-scale@4.0.9': - resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - - '@types/d3-shape@3.1.8': - resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} - - '@types/d3-time@3.0.4': - resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} - - '@types/d3-timer@3.0.2': - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -745,17 +342,6 @@ packages: '@types/node@24.10.1': resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} - '@types/react-dom@19.2.2': - resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.2': - resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} - - '@types/use-sync-external-store@0.0.6': - resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} - '@typescript-eslint/eslint-plugin@8.46.2': resolution: {integrity: sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -845,11 +431,6 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.21: - resolution: {integrity: sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==} - engines: {node: '>=6.0.0'} - hasBin: true - better-sqlite3@12.9.0: resolution: {integrity: sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} @@ -873,35 +454,17 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - bundle-name@4.1.0: - resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} - engines: {node: '>=18'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001790: - resolution: {integrity: sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chokidar@5.0.0: - resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} - engines: {node: '>= 20.19.0'} - chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -920,53 +483,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - - d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - - d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - - d3-format@3.1.2: - resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} - engines: {node: '>=12'} - - d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} - - d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - - d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} - - d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} - - d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} - - d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} - - d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -976,9 +492,6 @@ packages: supports-color: optional: true - decimal.js-light@2.5.1: - resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} - decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -990,18 +503,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - default-browser-id@5.0.1: - resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} - engines: {node: '>=18'} - - default-browser@5.5.0: - resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} - engines: {node: '>=18'} - - define-lazy-prop@3.0.0: - resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} - engines: {node: '>=12'} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1009,13 +510,6 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} - engines: {node: '>=10.13.0'} - - es-toolkit@1.46.0: - resolution: {integrity: sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA==} - esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -1067,9 +561,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - execa@9.6.1: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} @@ -1137,10 +628,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - get-port@7.1.0: - resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} - engines: {node: '>=16'} - get-stream@9.0.1: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} @@ -1167,9 +654,6 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -1192,12 +676,6 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - immer@10.2.0: - resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} - - immer@11.1.4: - resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1212,15 +690,6 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - - is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1229,15 +698,6 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-in-ssh@1.0.0: - resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} - engines: {node: '>=20'} - - is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1254,10 +714,6 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} - is-wsl@3.1.1: - resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} - engines: {node: '>=16'} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1285,80 +741,6 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1366,9 +748,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1397,38 +776,12 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@16.2.4: - resolution: {integrity: sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==} - engines: {node: '>=20.9.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.51.1 - babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - node-abi@3.89.0: resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} engines: {node: '>=10'} @@ -1444,10 +797,6 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - open@11.0.0: - resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} - engines: {node: '>=20'} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1480,9 +829,6 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.2: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} @@ -1497,18 +843,6 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true - postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} - - postcss@8.5.10: - resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} - engines: {node: ^10 || ^12 || >=14} - - powershell-utils@0.1.0: - resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} - engines: {node: '>=20'} - prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -1548,61 +882,14 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - react-dom@19.2.0: - resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} - peerDependencies: - react: ^19.2.0 - - react-is@19.2.5: - resolution: {integrity: sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==} - - react-redux@9.2.0: - resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} - peerDependencies: - '@types/react': ^18.2.25 || ^19 - react: ^18.0 || ^19 - redux: ^5.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - redux: - optional: true - - react@19.2.0: - resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} - engines: {node: '>=0.10.0'} - readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} - engines: {node: '>= 20.19.0'} - real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} - recharts@3.8.1: - resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} - engines: {node: '>=18'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - redux-thunk@3.1.0: - resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} - peerDependencies: - redux: ^5.0.0 - - redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} - - reselect@5.1.1: - resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1614,10 +901,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - run-applescript@7.1.0: - resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} - engines: {node: '>=18'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1628,18 +911,11 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1664,16 +940,6 @@ packages: sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} - sonner@2.0.7: - resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} - peerDependencies: - react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc - react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -1693,30 +959,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - styled-jsx@5.1.6: - resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - tailwindcss@4.2.4: - resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} - - tapable@2.3.3: - resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} - engines: {node: '>=6'} - tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -1728,9 +974,6 @@ packages: resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} engines: {node: '>=20'} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1741,9 +984,6 @@ packages: peerDependencies: typescript: '>=4.8.4' - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.20.6: resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} engines: {node: '>=18.0.0'} @@ -1778,17 +1018,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - victory-vendor@37.3.6: - resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1801,10 +1033,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - wsl-utils@0.3.1: - resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} - engines: {node: '>=20'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -1818,8 +1046,6 @@ packages: snapshots: - '@alloc/quick-lru@5.2.0': {} - '@clack/core@1.2.0': dependencies: fast-wrap-ansi: 0.1.6 @@ -1832,11 +1058,6 @@ snapshots: fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild/aix-ppc64@0.25.12': optional: true @@ -1873,251 +1094,109 @@ snapshots: '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-mips64el@0.25.12': - optional: true - - '@esbuild/linux-ppc64@0.25.12': - optional: true - - '@esbuild/linux-riscv64@0.25.12': - optional: true - - '@esbuild/linux-s390x@0.25.12': - optional: true - - '@esbuild/linux-x64@0.25.12': - optional: true - - '@esbuild/netbsd-arm64@0.25.12': - optional: true - - '@esbuild/netbsd-x64@0.25.12': - optional: true - - '@esbuild/openbsd-arm64@0.25.12': - optional: true - - '@esbuild/openbsd-x64@0.25.12': - optional: true - - '@esbuild/openharmony-arm64@0.25.12': - optional: true - - '@esbuild/sunos-x64@0.25.12': - optional: true - - '@esbuild/win32-arm64@0.25.12': - optional: true - - '@esbuild/win32-ia32@0.25.12': - optional: true - - '@esbuild/win32-x64@0.25.12': - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.1(jiti@2.6.1))': - dependencies: - eslint: 9.39.1(jiti@2.6.1) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.21.2': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3 - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 - - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.5': - dependencies: - ajv: 6.14.0 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.1': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 - - '@humanfs/core@0.19.2': - dependencies: - '@humanfs/types': 0.15.0 - - '@humanfs/node@0.16.8': - dependencies: - '@humanfs/core': 0.19.2 - '@humanfs/types': 0.15.0 - '@humanwhocodes/retry': 0.4.3 - - '@humanfs/types@0.15.0': {} - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@img/colour@1.1.0': - optional: true - - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@esbuild/linux-riscv64@0.25.12': optional: true - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@esbuild/linux-s390x@0.25.12': optional: true - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@esbuild/linux-x64@0.25.12': optional: true - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@esbuild/netbsd-x64@0.25.12': optional: true - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@esbuild/openbsd-x64@0.25.12': optional: true - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@img/sharp-wasm32@0.34.5': - dependencies: - '@emnapi/runtime': 1.10.0 + '@esbuild/sunos-x64@0.25.12': optional: true - '@img/sharp-win32-arm64@0.34.5': + '@esbuild/win32-arm64@0.25.12': optional: true - '@img/sharp-win32-ia32@0.34.5': + '@esbuild/win32-ia32@0.25.12': optional: true - '@img/sharp-win32-x64@0.34.5': + '@esbuild/win32-x64@0.25.12': optional: true - '@jridgewell/gen-mapping@0.3.13': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.1(jiti@2.6.1))': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + eslint: 9.39.1(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + '@eslint-community/regexpp@4.12.2': {} - '@jridgewell/resolve-uri@3.1.2': {} + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color - '@jridgewell/sourcemap-codec@1.5.5': {} + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 - '@jridgewell/trace-mapping@0.3.31': + '@eslint/core@0.17.0': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@types/json-schema': 7.0.15 - '@next/env@16.2.4': {} + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color - '@next/swc-darwin-arm64@16.2.4': - optional: true + '@eslint/js@9.39.1': {} - '@next/swc-darwin-x64@16.2.4': - optional: true + '@eslint/object-schema@2.1.7': {} - '@next/swc-linux-arm64-gnu@16.2.4': - optional: true + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 - '@next/swc-linux-arm64-musl@16.2.4': - optional: true + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 - '@next/swc-linux-x64-gnu@16.2.4': - optional: true + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 - '@next/swc-linux-x64-musl@16.2.4': - optional: true + '@humanfs/types@0.15.0': {} - '@next/swc-win32-arm64-msvc@16.2.4': - optional: true + '@humanwhocodes/module-importer@1.0.1': {} - '@next/swc-win32-x64-msvc@16.2.4': - optional: true + '@humanwhocodes/retry@0.4.3': {} '@nodelib/fs.scandir@2.1.5': dependencies: @@ -2133,127 +1212,14 @@ snapshots: '@pinojs/redact@0.4.0': {} - '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1))(react@19.2.0)': - dependencies: - '@standard-schema/spec': 1.1.0 - '@standard-schema/utils': 0.3.0 - immer: 11.1.4 - redux: 5.0.1 - redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.1.1 - optionalDependencies: - react: 19.2.0 - react-redux: 9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1) - '@sec-ant/readable-stream@0.4.1': {} '@sindresorhus/merge-streams@4.0.0': {} - '@standard-schema/spec@1.1.0': {} - - '@standard-schema/utils@0.3.0': {} - - '@swc/helpers@0.5.15': - dependencies: - tslib: 2.8.1 - - '@tailwindcss/node@4.2.4': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.20.1 - jiti: 2.6.1 - lightningcss: 1.32.0 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.2.4 - - '@tailwindcss/oxide-android-arm64@4.2.4': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.2.4': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.2.4': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.2.4': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.2.4': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.2.4': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.2.4': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.2.4': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.2.4': - optional: true - - '@tailwindcss/oxide@4.2.4': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.4 - '@tailwindcss/oxide-darwin-arm64': 4.2.4 - '@tailwindcss/oxide-darwin-x64': 4.2.4 - '@tailwindcss/oxide-freebsd-x64': 4.2.4 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.4 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.4 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.4 - '@tailwindcss/oxide-linux-x64-musl': 4.2.4 - '@tailwindcss/oxide-wasm32-wasi': 4.2.4 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 - - '@tailwindcss/postcss@4.2.4': - dependencies: - '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.2.4 - '@tailwindcss/oxide': 4.2.4 - postcss: 8.5.10 - tailwindcss: 4.2.4 - '@types/better-sqlite3@7.6.13': dependencies: '@types/node': 24.10.1 - '@types/d3-array@3.2.2': {} - - '@types/d3-color@3.1.3': {} - - '@types/d3-ease@3.0.2': {} - - '@types/d3-interpolate@3.0.4': - dependencies: - '@types/d3-color': 3.1.3 - - '@types/d3-path@3.1.1': {} - - '@types/d3-scale@4.0.9': - dependencies: - '@types/d3-time': 3.0.4 - - '@types/d3-shape@3.1.8': - dependencies: - '@types/d3-path': 3.1.1 - - '@types/d3-time@3.0.4': {} - - '@types/d3-timer@3.0.2': {} - '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -2262,16 +1228,6 @@ snapshots: dependencies: undici-types: 7.16.0 - '@types/react-dom@19.2.2(@types/react@19.2.2)': - dependencies: - '@types/react': 19.2.2 - - '@types/react@19.2.2': - dependencies: - csstype: 3.2.3 - - '@types/use-sync-external-store@0.0.6': {} - '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2390,8 +1346,6 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.21: {} - better-sqlite3@12.9.0: dependencies: bindings: 1.5.0 @@ -2425,29 +1379,15 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bundle-name@4.1.0: - dependencies: - run-applescript: 7.1.0 - callsites@3.1.0: {} - caniuse-lite@1.0.30001790: {} - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - chokidar@5.0.0: - dependencies: - readdirp: 5.0.0 - chownr@1.1.4: {} - client-only@0.0.1: {} - - clsx@2.1.1: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2464,52 +1404,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - csstype@3.2.3: {} - - d3-array@3.2.4: - dependencies: - internmap: 2.0.3 - - d3-color@3.1.0: {} - - d3-ease@3.0.1: {} - - d3-format@3.1.2: {} - - d3-interpolate@3.0.1: - dependencies: - d3-color: 3.1.0 - - d3-path@3.1.0: {} - - d3-scale@4.0.2: - dependencies: - d3-array: 3.2.4 - d3-format: 3.1.2 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - - d3-shape@3.2.0: - dependencies: - d3-path: 3.1.0 - - d3-time-format@4.1.0: - dependencies: - d3-time: 3.1.0 - - d3-time@3.1.0: - dependencies: - d3-array: 3.2.4 - - d3-timer@3.0.1: {} - debug@4.4.3: dependencies: ms: 2.1.3 - decimal.js-light@2.5.1: {} - decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -2518,28 +1416,12 @@ snapshots: deep-is@0.1.4: {} - default-browser-id@5.0.1: {} - - default-browser@5.5.0: - dependencies: - bundle-name: 4.1.0 - default-browser-id: 5.0.1 - - define-lazy-prop@3.0.0: {} - detect-libc@2.1.2: {} end-of-stream@1.4.5: dependencies: once: 1.4.0 - enhanced-resolve@5.20.1: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - - es-toolkit@1.46.0: {} - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -2639,8 +1521,6 @@ snapshots: esutils@2.0.3: {} - eventemitter3@5.0.4: {} - execa@9.6.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -2717,8 +1597,6 @@ snapshots: fsevents@2.3.3: optional: true - get-port@7.1.0: {} - get-stream@9.0.1: dependencies: '@sec-ant/readable-stream': 0.4.1 @@ -2742,8 +1620,6 @@ snapshots: globals@16.5.0: {} - graceful-fs@4.2.11: {} - graphemer@1.4.0: {} has-flag@4.0.0: {} @@ -2756,10 +1632,6 @@ snapshots: ignore@7.0.5: {} - immer@10.2.0: {} - - immer@11.1.4: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -2771,22 +1643,12 @@ snapshots: ini@1.3.8: {} - internmap@2.0.3: {} - - is-docker@3.0.0: {} - is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - is-in-ssh@1.0.0: {} - - is-inside-container@1.0.0: - dependencies: - is-docker: 3.0.0 - is-number@7.0.0: {} is-plain-obj@4.1.0: {} @@ -2795,13 +1657,10 @@ snapshots: is-unicode-supported@2.1.0: {} - is-wsl@3.1.1: - dependencies: - is-inside-container: 1.0.0 - isexe@2.0.0: {} - jiti@2.6.1: {} + jiti@2.6.1: + optional: true js-yaml@4.1.1: dependencies: @@ -2822,65 +1681,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - locate-path@6.0.0: dependencies: p-locate: 5.0.0 lodash.merge@4.6.2: {} - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - merge2@1.4.1: {} micromatch@4.0.8: @@ -2904,36 +1710,10 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.11: {} - napi-build-utils@2.0.0: {} natural-compare@1.4.0: {} - next@16.2.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0): - dependencies: - '@next/env': 16.2.4 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.21 - caniuse-lite: 1.0.30001790 - postcss: 8.4.31 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - styled-jsx: 5.1.6(react@19.2.0) - optionalDependencies: - '@next/swc-darwin-arm64': 16.2.4 - '@next/swc-darwin-x64': 16.2.4 - '@next/swc-linux-arm64-gnu': 16.2.4 - '@next/swc-linux-arm64-musl': 16.2.4 - '@next/swc-linux-x64-gnu': 16.2.4 - '@next/swc-linux-x64-musl': 16.2.4 - '@next/swc-win32-arm64-msvc': 16.2.4 - '@next/swc-win32-x64-msvc': 16.2.4 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - node-abi@3.89.0: dependencies: semver: 7.7.4 @@ -2949,15 +1729,6 @@ snapshots: dependencies: wrappy: 1.0.2 - open@11.0.0: - dependencies: - default-browser: 5.5.0 - define-lazy-prop: 3.0.0 - is-in-ssh: 1.0.0 - is-inside-container: 1.0.0 - powershell-utils: 0.1.0 - wsl-utils: 0.3.1 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2987,8 +1758,6 @@ snapshots: path-key@4.0.0: {} - picocolors@1.1.1: {} - picomatch@2.3.2: {} pino-abstract-transport@3.0.0: @@ -3011,20 +1780,6 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 4.0.0 - postcss@8.4.31: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postcss@8.5.10: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - powershell-utils@0.1.0: {} - prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -3068,70 +1823,20 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-dom@19.2.0(react@19.2.0): - dependencies: - react: 19.2.0 - scheduler: 0.27.0 - - react-is@19.2.5: {} - - react-redux@9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1): - dependencies: - '@types/use-sync-external-store': 0.0.6 - react: 19.2.0 - use-sync-external-store: 1.6.0(react@19.2.0) - optionalDependencies: - '@types/react': 19.2.2 - redux: 5.0.1 - - react@19.2.0: {} - readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - readdirp@5.0.0: {} - real-require@0.2.0: {} - recharts@3.8.1(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react-is@19.2.5)(react@19.2.0)(redux@5.0.1): - dependencies: - '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1))(react@19.2.0) - clsx: 2.1.1 - decimal.js-light: 2.5.1 - es-toolkit: 1.46.0 - eventemitter3: 5.0.4 - immer: 10.2.0 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-is: 19.2.5 - react-redux: 9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1) - reselect: 5.1.1 - tiny-invariant: 1.3.3 - use-sync-external-store: 1.6.0(react@19.2.0) - victory-vendor: 37.3.6 - transitivePeerDependencies: - - '@types/react' - - redux - - redux-thunk@3.1.0(redux@5.0.1): - dependencies: - redux: 5.0.1 - - redux@5.0.1: {} - - reselect@5.1.1: {} - resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} reusify@1.1.0: {} - run-applescript@7.1.0: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -3140,42 +1845,8 @@ snapshots: safe-stable-stringify@2.5.0: {} - scheduler@0.27.0: {} - semver@7.7.4: {} - sharp@0.34.5: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.7.4 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - optional: true - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3198,13 +1869,6 @@ snapshots: dependencies: atomic-sleep: 1.0.0 - sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0): - dependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - - source-map-js@1.2.1: {} - split2@4.2.0: {} string_decoder@1.3.0: @@ -3217,19 +1881,10 @@ snapshots: strip-json-comments@3.1.1: {} - styled-jsx@5.1.6(react@19.2.0): - dependencies: - client-only: 0.0.1 - react: 19.2.0 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 - tailwindcss@4.2.4: {} - - tapable@2.3.3: {} - tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -3249,8 +1904,6 @@ snapshots: dependencies: real-require: 0.2.0 - tiny-invariant@1.3.3: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -3259,8 +1912,6 @@ snapshots: dependencies: typescript: 5.9.3 - tslib@2.8.1: {} - tsx@4.20.6: dependencies: esbuild: 0.25.12 @@ -3297,29 +1948,8 @@ snapshots: dependencies: punycode: 2.3.1 - use-sync-external-store@1.6.0(react@19.2.0): - dependencies: - react: 19.2.0 - util-deprecate@1.0.2: {} - victory-vendor@37.3.6: - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-ease': 3.0.2 - '@types/d3-interpolate': 3.0.4 - '@types/d3-scale': 4.0.9 - '@types/d3-shape': 3.1.8 - '@types/d3-time': 3.0.4 - '@types/d3-timer': 3.0.2 - d3-array: 3.2.4 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-scale: 4.0.2 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-timer: 3.0.1 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -3328,11 +1958,6 @@ snapshots: wrappy@1.0.2: {} - wsl-utils@0.3.1: - dependencies: - is-wsl: 3.1.1 - powershell-utils: 0.1.0 - yocto-queue@0.1.0: {} yoctocolors@2.1.2: {} diff --git a/scripts/prepack.mjs b/scripts/prepack.mjs index 6bb8dd2..d85fcd5 100644 --- a/scripts/prepack.mjs +++ b/scripts/prepack.mjs @@ -1,48 +1,31 @@ -import { cp, mkdir } from 'node:fs/promises'; -import { existsSync } from 'node:fs'; -import path from 'node:path'; import process from 'node:process'; import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { readdir, readFile, rm, writeFile } from 'node:fs/promises'; const rootDir = process.cwd(); +const cliDistDir = path.join(rootDir, 'packages', 'cli', 'dist'); +const sharedDistEntry = path.join(rootDir, 'packages', 'shared', 'dist', 'index.js'); -await runCommand('pnpm', ['-r', 'build']); - -const standaloneDir = path.join( - rootDir, - 'packages', - 'dashboard', - '.next', - 'standalone', -); -const staticDir = path.join(rootDir, 'packages', 'dashboard', '.next', 'static'); -const publicDir = path.join(rootDir, 'packages', 'dashboard', 'public'); - -if (existsSync(standaloneDir)) { - const standaloneDashboardDir = path.join(standaloneDir, 'packages', 'dashboard'); - await mkdir(path.join(standaloneDashboardDir, '.next'), { recursive: true }); - - if (existsSync(staticDir)) { - await cp(staticDir, path.join(standaloneDashboardDir, '.next', 'static'), { - recursive: true, - force: true, - }); - } +await rm(new URL('../packages/shared/dist/', import.meta.url), { + recursive: true, + force: true, +}); +await rm(new URL('../packages/cli/dist/', import.meta.url), { + recursive: true, + force: true, +}); - if (existsSync(publicDir)) { - await cp(publicDir, path.join(standaloneDashboardDir, 'public'), { - recursive: true, - force: true, - }); - } -} +await runCommand('pnpm', ['--filter', '@codex-switch/shared', 'build']); +await runCommand('pnpm', ['--filter', '@codex-switch/cli', 'build']); +await rewriteSharedPackageImports(); function runCommand(command, args) { return new Promise((resolve, reject) => { - const child = spawn(command, args, { + const [executable, commandArgs] = resolveCommand(command, args); + const child = spawn(executable, commandArgs, { cwd: rootDir, stdio: 'inherit', - shell: process.platform === 'win32', }); child.on('exit', (code) => { @@ -55,3 +38,55 @@ function runCommand(command, args) { }); }); } + +function resolveCommand(command, args) { + if (process.platform !== 'win32') { + return [command, args]; + } + + return [ + process.env.ComSpec ?? 'cmd.exe', + ['/d', '/s', '/c', [command, ...args].join(' ')], + ]; +} + +async function rewriteSharedPackageImports() { + const files = await listFiles(cliDistDir); + + await Promise.all( + files + .filter((file) => file.endsWith('.js') || file.endsWith('.d.ts')) + .map(async (file) => { + const input = await readFile(file, 'utf8'); + if (!input.includes('@codex-switch/shared')) { + return; + } + + const relativeImport = toModuleSpecifier( + path.relative(path.dirname(file), sharedDistEntry), + ); + const output = input + .replaceAll("'@codex-switch/shared'", `'${relativeImport}'`) + .replaceAll('"@codex-switch/shared"', `"${relativeImport}"`); + + await writeFile(file, output, 'utf8'); + }), + ); +} + +async function listFiles(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = await Promise.all( + entries.map((entry) => { + const fullPath = path.join(directory, entry.name); + return entry.isDirectory() ? listFiles(fullPath) : fullPath; + }), + ); + + return files.flat(); +} + +function toModuleSpecifier(relativePath) { + const normalized = relativePath.replaceAll(path.sep, '/'); + return normalized.startsWith('.') ? normalized : `./${normalized}`; +}