| author | |
| committer | |
| log | 8ebd2479cf976a6ef79843b97d477777efb7cb6a |
| tree | 85e8cea2dc57ab2e1c59cc74576ff027c37464d0 |
| parent | e7c4292d271f14ace547c135089245825b82e17b |
| signature | Signed by PGP key B5690EEEBB952194 |
* feat: scaffold apps/server package and WebSocket protocol types
- Create apps/server with package.json, tsconfig, tsup config
- Copy codexAppServerManager, providerManager, fixPath from desktop (no Electron deps)
- Add WebSocket message protocol schemas (WsRequest, WsResponse, WsPush) to @acme/contracts
- Define WS_METHODS and WS_CHANNELS constants for type-safe routing
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* feat: implement WebSocket + HTTP server and CLI entry point
- wsServer.ts: WebSocket server routes NativeApi methods, serves static files
- Provider events pushed to all connected clients
- SPA fallback for client-side routing
- Server welcome message with cwd/projectName on connect
- index.ts: CLI entry point
- Auto-detects available port (default 3773)
- Resolves renderer dist for static serving
- Opens browser on startup via 'open' package
- Graceful shutdown on SIGINT/SIGTERM
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* feat: WebSocket transport and NativeApi implementation for browser
- wsTransport.ts: WebSocket client with request/response, push subscriptions,
auto-reconnect with exponential backoff
- wsNativeApi.ts: Full NativeApi implementation over WebSocket transport
- All provider methods routed through WS_METHODS
- Provider events streamed via WS_CHANNELS.providerEvent
- Todos/terminal/agent stubbed (not needed for web mode)
- dialogs.pickFolder returns null (text input fallback in UI)
- session-logic.ts: readNativeApi() now auto-detects environment
- Prefers Electron preload bridge if available (window.nativeApi)
- Falls back to WebSocket transport for browser mode
- onServerWelcome() hook for renderer to receive cwd on connect
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* feat: adapt renderer for browser mode
- App.tsx: Add AutoProjectBootstrap that listens for server welcome
and auto-creates project + thread from server's cwd
- Sidebar: Conditionalize drag region (Electron only), hide Browse button in browser
- ChatView: Conditionalize drag region, adjust padding for browser
- DiffPanel: Same drag region treatment
- index.html: Update title to 'CodeThing'
- env.ts: isElectron detection utility
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* feat: monorepo & CLI integration
- Root package.json: Add dev, dev:server, dev:web, start scripts
- dev runs both server and renderer in parallel via turbo
- VITE_WS_URL injected so renderer connects to server in dev mode
- turbo.json: Add WS env vars to globalEnv
- vite.config.ts: Inject VITE_WS_URL at build time for WS server discovery
- wsTransport.ts: Use VITE_WS_URL env var in dev, fall back to page URL in prod
- Server dev script: Uses tsx for direct TypeScript execution with VITE_DEV_SERVER_URL
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* feat: remove Electron desktop app, clean up contracts and docs
- Delete apps/desktop/ entirely (replaced by apps/server)
- Remove IPC_CHANNELS constant from contracts (no longer needed)
- Keep NativeApi interface (still the renderer's API contract)
- Keep EDITORS constant (used by both server and renderer)
- Update README.md with new architecture diagram and scripts
- Update AGENTS.md with new package roles and architecture
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* test: add WebSocket server integration tests
- Tests welcome message on connect with cwd/projectName
- Tests server.getConfig method
- Tests providers.listSessions returns empty array
- Tests error response for unknown methods
- Tests graceful handling of invalid JSON
- All 73 tests pass across 12 test files
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* fix: address lint warnings in new files
- wsTransport.ts: Use addEventListener instead of on-function assignments
- wsServer.ts: Replace filter(Boolean).pop() with direct array access
- wsServer.test.ts: Use addEventListener for WebSocket events
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* fix: race condition in auto-project bootstrap + cleanup
- wsNativeApi: Cache last welcome payload and replay to late subscribers.
Fixes race where WS connect + welcome arrives before React effect fires.
- App.tsx: Add bootstrappedRef to prevent duplicate project creation,
skip bootstrap in Electron mode
- wsTransport: Use standard import.meta.env for VITE_WS_URL access
- CLAUDE.md: Fix header (was incorrectly '# AGENTS.md')
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* fix: use tsx command directly in server dev script
Use 'tsx src/index.ts' instead of 'node --import tsx' for simpler dev execution
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* chore: clean up stale Electron references and config
- Remove smoke-test script (was desktop-only)
- Remove dist-electron from turbo outputs and .gitignore
- Rename ELECTRON_RENDERER_PORT → PORT in vite.config.ts
- Clean up turbo globalEnv (remove ELECTRON_RENDERER_PORT, WS_SERVER_PORT)
- Update CI workflow to use bun 1.3.9 (matches package.json engines)
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* feat: make t3 package publishable for npx
- Remove private:true so package can be published to npm
- Add files field to limit published files to dist/
- Move @acme/contracts to devDependencies (bundled by tsup)
- Add @acme/renderer devDependency for turbo build ordering
- Add bundle-client.mjs: copies renderer build into dist/client/
- Update resolveStaticDir to check dist/client/ first (npx scenario),
then fall back to monorepo layout (../../renderer/dist)
- Full build pipeline: contracts → renderer → server + bundle client
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
* fix tests
* turborepo and better logging
* bring back electron
* fix
* Promise catching fixes
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>55 files changed, 3679 insertions(+), 2436 deletions(-)
.github/workflows/ci.yml+10-2| ... | @@ -8,7 +8,7 @@ on: | ... | @@ -8,7 +8,7 @@ on: |
| 8 | 8 | ||
| 9 | jobs: | 9 | jobs: |
| 10 | quality: | 10 | quality: |
| 11 | name: Lint, Typecheck, Test | 11 | name: Lint, Typecheck, Test, Build |
| 12 | runs-on: ubuntu-latest | 12 | runs-on: ubuntu-latest |
| 13 | steps: | 13 | steps: |
| 14 | - name: Checkout | 14 | - name: Checkout |
| ... | @@ -17,7 +17,7 @@ jobs: | ... | @@ -17,7 +17,7 @@ jobs: |
| 17 | - name: Setup Bun | 17 | - name: Setup Bun |
| 18 | uses: oven-sh/setup-bun@v2 | 18 | uses: oven-sh/setup-bun@v2 |
| 19 | with: | 19 | with: |
| 20 | bun-version: 1.1.38 | 20 | bun-version: 1.3.9 |
| 21 | 21 | ||
| 22 | - name: Cache Bun and Turbo | 22 | - name: Cache Bun and Turbo |
| 23 | uses: actions/cache@v4 | 23 | uses: actions/cache@v4 |
| ... | @@ -40,3 +40,11 @@ jobs: | ... | @@ -40,3 +40,11 @@ jobs: |
| 40 | 40 | ||
| 41 | - name: Test | 41 | - name: Test |
| 42 | run: bun run test | 42 | run: bun run test |
| 43 | |||
| 44 | - name: Build desktop pipeline | ||
| 45 | run: bun run build:desktop | ||
| 46 | |||
| 47 | - name: Verify preload bundle output | ||
| 48 | run: | | ||
| 49 | test -f apps/desktop/dist-electron/preload.js | ||
| 50 | grep -nE "desktopBridge|getWsUrl|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.js |
.gitignore-1| ... | @@ -5,7 +5,6 @@ node_modules | ... | @@ -5,7 +5,6 @@ node_modules |
| 5 | *.log | 5 | *.log |
| 6 | *.tsbuildinfo | 6 | *.tsbuildinfo |
| 7 | apps/*/dist | 7 | apps/*/dist |
| 8 | apps/*/dist-electron | ||
| 9 | packages/*/dist | 8 | packages/*/dist |
| 10 | .env | 9 | .env |
| 11 | .env.local | 10 | .env.local |
AGENTS.md+10-9| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | # AGENTS.md | 1 | # CLAUDE.md |
| 2 | 2 | ||
| 3 | ## Project Snapshot | 3 | ## Project Snapshot |
| 4 | CodeThing is a minimal GUI for using code agents like Codex and Claude Code (coming soon). | 4 | CodeThing is a minimal web GUI for using code agents like Codex and Claude Code (coming soon). |
| 5 | 5 | ||
| 6 | This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged. | 6 | This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged. |
| 7 | 7 | ||
| ... | @@ -13,17 +13,18 @@ This repository is a VERY EARLY WIP. Proposing sweeping changes that improve lon | ... | @@ -13,17 +13,18 @@ This repository is a VERY EARLY WIP. Proposing sweeping changes that improve lon |
| 13 | If a tradeoff is required, choose correctness and robustness over short-term convenience. | 13 | If a tradeoff is required, choose correctness and robustness over short-term convenience. |
| 14 | 14 | ||
| 15 | ## Package Roles | 15 | ## Package Roles |
| 16 | - `apps/desktop`: Electron main/preload runtime. Owns provider orchestration, process/session lifecycle, and native IPC boundaries. | 16 | - `apps/server`: Node.js WebSocket server. Wraps Codex app-server (JSON-RPC over stdio), serves the React web app, and manages provider sessions. |
| 17 | - `apps/renderer`: React/Vite UI. Owns session UX, conversation/event rendering, and client-side state. | 17 | - `apps/renderer`: React/Vite UI. Owns session UX, conversation/event rendering, and client-side state. Connects to the server via WebSocket. |
| 18 | - `packages/contracts`: Shared Zod schemas and TypeScript contracts for provider events, IPC payloads, and model/session types. | 18 | - `packages/contracts`: Shared Zod schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types. |
| 19 | 19 | ||
| 20 | ## Codex App Server (Important) | 20 | ## Codex App Server (Important) |
| 21 | CodeThing is currently Codex-first. The desktop app starts `codex app-server` (JSON-RPC over stdio) per provider session, then streams structured events into the renderer through the provider APIs. | 21 | CodeThing is currently Codex-first. The server starts `codex app-server` (JSON-RPC over stdio) per provider session, then streams structured events to the browser through WebSocket push messages. |
| 22 | 22 | ||
| 23 | How we use it in this codebase: | 23 | How we use it in this codebase: |
| 24 | - Session startup/resume and turn lifecycle are brokered in `apps/desktop/src/codexAppServerManager.ts`. | 24 | - Session startup/resume and turn lifecycle are brokered in `apps/server/src/codexAppServerManager.ts`. |
| 25 | - Provider dispatch and thread event logging are coordinated in `apps/desktop/src/providerManager.ts`. | 25 | - Provider dispatch and thread event logging are coordinated in `apps/server/src/providerManager.ts`. |
| 26 | - Renderer consumes provider event streams via `nativeApi.providers.onEvent`. | 26 | - WebSocket server routes NativeApi methods in `apps/server/src/wsServer.ts`. |
| 27 | - Renderer consumes provider event streams via WebSocket push on channel `providers.event`. | ||
| 27 | 28 | ||
| 28 | Docs: | 29 | Docs: |
| 29 | - Codex App Server docs: https://developers.openai.com/codex/sdk/#app-server | 30 | - Codex App Server docs: https://developers.openai.com/codex/sdk/#app-server |
README.md+68-41| ... | @@ -1,16 +1,38 @@ | ... | @@ -1,16 +1,38 @@ |
| 1 | # CodeThing (Electron + Vite + Bun) | 1 | # CodeThing |
| 2 | 2 | ||
| 3 | CodeThing is a desktop shell for coding agents. This first implementation is: | 3 | CodeThing is a minimal web GUI for coding agents. Currently Codex-first, with Claude Code support coming soon. |
| 4 | 4 | ||
| 5 | 1. Codex-first: connects to `codex app-server` and streams turn/item events. | 5 | Run `npx t3` in any project directory to launch the web interface. |
| 6 | 2. Provider-ready: renderer speaks a provider abstraction so Claude Code can plug in later. | 6 | Run `bun run dev:desktop` to launch the Electron desktop app in this monorepo. |
| 7 | 3. Typed end-to-end: contracts validate payloads at preload/main boundaries. | 7 | |
| 8 | ## Architecture | ||
| 9 | |||
| 10 | CodeThing runs as a **Node.js WebSocket server** that wraps `codex app-server` (JSON-RPC over stdio) and serves a React web app. | ||
| 11 | |||
| 12 | ``` | ||
| 13 | ┌─────────────────────────────────┐ | ||
| 14 | │ Browser (React + Vite) │ | ||
| 15 | │ Connected via WebSocket │ | ||
| 16 | └──────────┬──────────────────────┘ | ||
| 17 | │ ws://localhost:3773 | ||
| 18 | ┌──────────▼──────────────────────┐ | ||
| 19 | │ apps/server (Node.js) │ | ||
| 20 | │ WebSocket + HTTP static server │ | ||
| 21 | │ ProviderManager │ | ||
| 22 | │ CodexAppServerManager │ | ||
| 23 | └──────────┬──────────────────────┘ | ||
| 24 | │ JSON-RPC over stdio | ||
| 25 | ┌──────────▼──────────────────────┐ | ||
| 26 | │ codex app-server │ | ||
| 27 | └─────────────────────────────────┘ | ||
| 28 | ``` | ||
| 8 | 29 | ||
| 9 | ## Workspace layout | 30 | ## Workspace layout |
| 10 | 31 | ||
| 11 | - `/apps/desktop`: Electron main + preload process, includes provider and Codex session managers. | 32 | - `/apps/server`: Node.js WebSocket server. Wraps Codex app-server, serves the built renderer, and opens the browser on start. |
| 12 | - `/apps/renderer`: React + Vite UI for session control, conversation, and protocol event stream. | 33 | - `/apps/renderer`: React + Vite UI. Session control, conversation, and provider event rendering. Connects to the server via WebSocket. |
| 13 | - `/packages/contracts`: shared Zod schemas + TypeScript types for IPC and provider events. | 34 | - `/apps/desktop`: Electron shell. Spawns a desktop-scoped `t3` backend process and loads the shared renderer. |
| 35 | - `/packages/contracts`: Shared Zod schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types. | ||
| 14 | 36 | ||
| 15 | ## Codex prerequisites | 37 | ## Codex prerequisites |
| 16 | 38 | ||
| ... | @@ -18,50 +40,55 @@ CodeThing is a desktop shell for coding agents. This first implementation is: | ... | @@ -18,50 +40,55 @@ CodeThing is a desktop shell for coding agents. This first implementation is: |
| 18 | - Authenticate Codex before running CodeThing (for example via API key or ChatGPT auth supported by Codex). | 40 | - Authenticate Codex before running CodeThing (for example via API key or ChatGPT auth supported by Codex). |
| 19 | - CodeThing starts the server via `codex app-server` per session. | 41 | - CodeThing starts the server via `codex app-server` per session. |
| 20 | 42 | ||
| 21 | ## Security and boundary model | 43 | ## Quick start |
| 22 | 44 | ||
| 23 | - `nodeIntegration: false` | 45 | ```bash |
| 24 | - `contextIsolation: true` | 46 | # Development (with hot reload) |
| 25 | - `sandbox: true` | 47 | bun run dev |
| 26 | - Renderer talks only to `window.nativeApi` exposed by preload. | ||
| 27 | - Preload and main both validate inputs using shared Zod schemas. | ||
| 28 | 48 | ||
| 29 | `sandbox: true` above is Electron renderer sandboxing. It is separate from Codex execution sandbox policy (`read-only`, `workspace-write`, `danger-full-access`) used when starting provider sessions. | 49 | # Desktop development |
| 50 | bun run dev:desktop | ||
| 30 | 51 | ||
| 31 | ## Runtime modes | 52 | # Production |
| 53 | bun run build | ||
| 54 | bun run start | ||
| 32 | 55 | ||
| 33 | CodeThing has a global runtime mode switch in the sidebar: | 56 | # Or from any project directory after publishing: |
| 57 | npx t3 | ||
| 58 | ``` | ||
| 34 | 59 | ||
| 35 | - `Full access` (default): starts new sessions with `approvalPolicy: never` and `sandboxMode: danger-full-access`. | 60 | ## Scripts |
| 36 | - `Approval required`: starts new sessions with `approvalPolicy: on-request` and `sandboxMode: workspace-write`, then prompts in-app for command/file approvals. | ||
| 37 | 61 | ||
| 38 | Mode changes apply across all threads. Existing live sessions are restarted so old and new threads use the selected mode. | 62 | - `bun run dev` — Starts contracts, server, and web dev tasks via Turborepo's parallel task runner. |
| 63 | - `bun run dev:server` — Starts just the WebSocket server (uses tsx for TS execution). | ||
| 64 | - `bun run dev:web` — Starts just the Vite dev server for the renderer. | ||
| 65 | - `bun run start` — Runs the production server (serves built renderer as static files). | ||
| 66 | - `bun run build` — Builds contracts, renderer, and server through Turbo. | ||
| 67 | - `bun run typecheck` — Strict TypeScript checks for all packages. | ||
| 68 | - `bun run test` — Runs workspace tests. | ||
| 39 | 69 | ||
| 40 | ## Scripts | 70 | ## Runtime modes |
| 41 | 71 | ||
| 42 | - `bun run dev`: starts contract build/watch, renderer dev server, and Electron process. | 72 | CodeThing has a global runtime mode switch in the chat toolbar: |
| 43 | - `bun run build`: builds contracts, renderer, and desktop bundles through Turbo. | ||
| 44 | - `bun run typecheck`: strict TypeScript checks for all packages. | ||
| 45 | - `bun run test`: runs workspace tests. | ||
| 46 | 73 | ||
| 47 | ## CI quality gates | 74 | - **Full access** (default): starts sessions with `approvalPolicy: never` and `sandboxMode: danger-full-access`. |
| 75 | - **Supervised**: starts sessions with `approvalPolicy: on-request` and `sandboxMode: workspace-write`, then prompts in-app for command/file approvals. | ||
| 48 | 76 | ||
| 49 | - `.github/workflows/ci.yml` runs `bun run lint`, `bun run typecheck`, and `bun run test` on pull requests and pushes to `main`. | 77 | ## Provider architecture |
| 50 | 78 | ||
| 51 | Optional: | 79 | The renderer communicates with the server via WebSocket using a simple JSON-RPC-style protocol: |
| 52 | 80 | ||
| 53 | - `ELECTRON_RENDERER_PORT=5180 bun run dev` if `5173` is already in use. | 81 | - **Request/Response**: `{ id, method, params }` → `{ id, result }` or `{ id, error }` |
| 82 | - **Push events**: `{ type: "push", channel, data }` for streaming provider events | ||
| 54 | 83 | ||
| 55 | ## Provider architecture | 84 | Methods mirror the `NativeApi` interface defined in `@acme/contracts`: |
| 56 | 85 | ||
| 57 | The renderer now depends on `nativeApi.providers.*`: | 86 | - `providers.startSession`, `providers.sendTurn`, `providers.interruptTurn` |
| 87 | - `providers.respondToRequest`, `providers.stopSession`, `providers.listSessions` | ||
| 88 | - `shell.openInEditor`, `server.getConfig` | ||
| 58 | 89 | ||
| 59 | 1. `startSession` | 90 | Codex is the only implemented provider. `claudeCode` is reserved in contracts/UI. |
| 60 | 2. `sendTurn` | ||
| 61 | 3. `interruptTurn` | ||
| 62 | 4. `respondToRequest` | ||
| 63 | 5. `stopSession` | ||
| 64 | 6. `listSessions` | ||
| 65 | 7. `onEvent` | ||
| 66 | 91 | ||
| 67 | Codex is the only implemented provider right now. `claudeCode` is reserved in contracts/UI but returns a not-implemented error in main-process dispatch. | 92 | ## CI quality gates |
| 93 | |||
| 94 | - `.github/workflows/ci.yml` runs `bun run lint`, `bun run typecheck`, and `bun run test` on pull requests and pushes to `main`. |
apps/desktop/.gitignore created+1| ... | @@ -0,0 +1 @@ | ||
| 1 | dist-electron/ | ||
apps/desktop/package.json+5-8| ... | @@ -4,28 +4,25 @@ | ... | @@ -4,28 +4,25 @@ |
| 4 | "private": true, | 4 | "private": true, |
| 5 | "main": "dist-electron/main.js", | 5 | "main": "dist-electron/main.js", |
| 6 | "scripts": { | 6 | "scripts": { |
| 7 | "dev": "concurrently -k -n BUNDLE,ELECTRON \"bun run dev:bundle\" \"bun run dev:electron\"", | 7 | "dev": "bun run --cwd ../server build && concurrently -k -n BUNDLE,RENDERER,ELECTRON \"bun run dev:bundle\" \"bun run --cwd ../renderer dev\" \"bun run dev:electron\"", |
| 8 | "dev:bundle": "tsup --watch", | 8 | "dev:bundle": "tsup --watch", |
| 9 | "dev:electron": "bun run scripts/dev-electron.mjs", | 9 | "dev:electron": "bun run scripts/dev-electron.mjs", |
| 10 | "build": "tsup", | 10 | "build": "tsup", |
| 11 | "start": "electron dist-electron/main.js", | 11 | "start": "electron dist-electron/main.js", |
| 12 | "postinstall": "electron-rebuild", | ||
| 13 | "typecheck": "tsc --noEmit", | 12 | "typecheck": "tsc --noEmit", |
| 14 | "test": "vitest run", | 13 | "test": "vitest run --passWithNoTests", |
| 15 | "smoke-test": "node scripts/smoke-test.mjs" | 14 | "smoke-test": "node scripts/smoke-test.mjs" |
| 16 | }, | 15 | }, |
| 17 | "dependencies": { | 16 | "dependencies": { |
| 18 | "@acme/contracts": "workspace:*", | 17 | "electron": "33.4.11" |
| 19 | "electron": "33.4.11", | ||
| 20 | "node-pty": "^1.0.0" | ||
| 21 | }, | 18 | }, |
| 22 | "devDependencies": { | 19 | "devDependencies": { |
| 23 | "concurrently": "^9.1.2", | ||
| 24 | "@electron/rebuild": "^3.7.0", | ||
| 25 | "@types/node": "^22.10.2", | 20 | "@types/node": "^22.10.2", |
| 21 | "concurrently": "^9.1.2", | ||
| 26 | "electronmon": "^2.0.2", | 22 | "electronmon": "^2.0.2", |
| 27 | "tsup": "^8.3.5", | 23 | "tsup": "^8.3.5", |
| 28 | "typescript": "^5.7.3", | 24 | "typescript": "^5.7.3", |
| 25 | "vitest": "^3.0.0", | ||
| 29 | "wait-on": "^8.0.2" | 26 | "wait-on": "^8.0.2" |
| 30 | } | 27 | } |
| 31 | } | 28 | } |
apps/desktop/scripts/dev-electron.mjs+2-2| ... | @@ -10,11 +10,11 @@ await waitOn({ | ... | @@ -10,11 +10,11 @@ await waitOn({ |
| 10 | `tcp:${port}`, | 10 | `tcp:${port}`, |
| 11 | "file:dist-electron/main.js", | 11 | "file:dist-electron/main.js", |
| 12 | "file:dist-electron/preload.js", | 12 | "file:dist-electron/preload.js", |
| 13 | "file:../server/dist/index.js", | ||
| 13 | ], | 14 | ], |
| 14 | }); | 15 | }); |
| 15 | 16 | ||
| 16 | const command = | 17 | const command = process.platform === "win32" ? "electronmon.cmd" : "electronmon"; |
| 17 | process.platform === "win32" ? "electronmon.cmd" : "electronmon"; | ||
| 18 | const child = spawn(command, ["dist-electron/main.js"], { | 18 | const child = spawn(command, ["dist-electron/main.js"], { |
| 19 | stdio: "inherit", | 19 | stdio: "inherit", |
| 20 | env: { | 20 | env: { |
apps/desktop/scripts/smoke-test.mjs+19-42| ... | @@ -1,84 +1,61 @@ | ... | @@ -1,84 +1,61 @@ |
| 1 | /** | 1 | import { execSync, spawn } from "node:child_process"; |
| 2 | * Smoke test: builds desktop + renderer, launches Electron against the | ||
| 3 | * production bundle, waits for the renderer to confirm it loaded, then exits. | ||
| 4 | * | ||
| 5 | * Catches the two categories of regression we've hit: | ||
| 6 | * 1. Module resolution failures (preload can't find @acme/contracts, etc.) | ||
| 7 | * 2. CSP / script blocking (React fails to mount) | ||
| 8 | * | ||
| 9 | * The test works by injecting a tiny check via ELECTRON_ENABLE_LOGGING — | ||
| 10 | * Electron forwards renderer console.log to the main process stdout when | ||
| 11 | * that env var is set. We look for React's "Download the React DevTools" | ||
| 12 | * message as proof that React successfully mounted (it only fires after | ||
| 13 | * the first render). For production mode (no React DevTools message), we | ||
| 14 | * instead check that no fatal errors appeared. | ||
| 15 | */ | ||
| 16 | import { spawn, execSync } from "node:child_process"; | ||
| 17 | import { dirname, resolve } from "node:path"; | 2 | import { dirname, resolve } from "node:path"; |
| 18 | import { fileURLToPath } from "node:url"; | 3 | import { fileURLToPath } from "node:url"; |
| 19 | 4 | ||
| 20 | const __dirname = dirname(fileURLToPath(import.meta.url)); | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 21 | const root = resolve(__dirname, "../../.."); | ||
| 22 | const desktopDir = resolve(__dirname, ".."); | 6 | const desktopDir = resolve(__dirname, ".."); |
| 7 | const rootDir = resolve(__dirname, "../../.."); | ||
| 23 | const electronBin = resolve(desktopDir, "node_modules/.bin/electron"); | 8 | const electronBin = resolve(desktopDir, "node_modules/.bin/electron"); |
| 24 | const mainJs = resolve(desktopDir, "dist-electron/main.js"); | 9 | const mainJs = resolve(desktopDir, "dist-electron/main.js"); |
| 25 | 10 | ||
| 26 | // ── Build first ────────────────────────────────────────────────────── | 11 | console.log("Building contracts + renderer + server + desktop..."); |
| 27 | console.log("Building contracts + renderer + desktop..."); | 12 | execSync("bun run build", { cwd: rootDir, stdio: "inherit" }); |
| 28 | execSync("bun run build", { cwd: root, stdio: "inherit" }); | ||
| 29 | 13 | ||
| 30 | // ── Launch Electron (production mode — no VITE_DEV_SERVER_URL) ────── | 14 | console.log("\nLaunching Electron smoke test..."); |
| 31 | console.log("\nLaunching Electron (production mode)..."); | ||
| 32 | 15 | ||
| 33 | const child = spawn(electronBin, [mainJs], { | 16 | const child = spawn(electronBin, [mainJs], { |
| 34 | stdio: ["pipe", "pipe", "pipe"], | 17 | stdio: ["pipe", "pipe", "pipe"], |
| 35 | env: { | 18 | env: { |
| 36 | ...process.env, | 19 | ...process.env, |
| 37 | VITE_DEV_SERVER_URL: "", // ensure production path | 20 | VITE_DEV_SERVER_URL: "", |
| 38 | ELECTRON_ENABLE_LOGGING: "1", | 21 | ELECTRON_ENABLE_LOGGING: "1", |
| 39 | }, | 22 | }, |
| 40 | }); | 23 | }); |
| 41 | 24 | ||
| 42 | let output = ""; | 25 | let output = ""; |
| 43 | 26 | child.stdout.on("data", (chunk) => { | |
| 44 | child.stdout.on("data", (d) => { | 27 | output += chunk.toString(); |
| 45 | output += d.toString(); | ||
| 46 | }); | 28 | }); |
| 47 | child.stderr.on("data", (d) => { | 29 | child.stderr.on("data", (chunk) => { |
| 48 | output += d.toString(); | 30 | output += chunk.toString(); |
| 49 | }); | 31 | }); |
| 50 | 32 | ||
| 51 | const TIMEOUT_MS = 8_000; | 33 | const timeout = setTimeout(() => { |
| 52 | |||
| 53 | const timer = setTimeout(() => { | ||
| 54 | child.kill(); | 34 | child.kill(); |
| 55 | }, TIMEOUT_MS); | 35 | }, 8_000); |
| 56 | 36 | ||
| 57 | child.on("exit", () => { | 37 | child.on("exit", () => { |
| 58 | clearTimeout(timer); | 38 | clearTimeout(timeout); |
| 59 | 39 | ||
| 60 | // Fatal patterns that indicate broken builds | ||
| 61 | const fatalPatterns = [ | 40 | const fatalPatterns = [ |
| 62 | "Cannot find module", | 41 | "Cannot find module", |
| 63 | "MODULE_NOT_FOUND", | 42 | "MODULE_NOT_FOUND", |
| 64 | "Refused to execute", // CSP blocking scripts | 43 | "Refused to execute", |
| 65 | "can't detect preamble", // @vitejs/plugin-react failure | ||
| 66 | "Uncaught Error", | 44 | "Uncaught Error", |
| 67 | "Uncaught TypeError", | 45 | "Uncaught TypeError", |
| 68 | "Uncaught ReferenceError", | 46 | "Uncaught ReferenceError", |
| 69 | ]; | 47 | ]; |
| 70 | 48 | const failures = fatalPatterns.filter((pattern) => output.includes(pattern)); | |
| 71 | const failures = fatalPatterns.filter((p) => output.includes(p)); | ||
| 72 | 49 | ||
| 73 | if (failures.length > 0) { | 50 | if (failures.length > 0) { |
| 74 | console.error("\n❌ Smoke test FAILED. Matched fatal patterns:"); | 51 | console.error("\nDesktop smoke test failed:"); |
| 75 | for (const f of failures) { | 52 | for (const failure of failures) { |
| 76 | console.error(` • ${f}`); | 53 | console.error(` - ${failure}`); |
| 77 | } | 54 | } |
| 78 | console.error("\nFull output:\n" + output); | 55 | console.error("\nFull output:\n" + output); |
| 79 | process.exit(1); | 56 | process.exit(1); |
| 80 | } | 57 | } |
| 81 | 58 | ||
| 82 | console.log("✅ Smoke test passed — no fatal errors detected"); | 59 | console.log("Desktop smoke test passed."); |
| 83 | process.exit(0); | 60 | process.exit(0); |
| 84 | }); | 61 | }); |
apps/desktop/src/codexAppServerManager.test.ts deleted-81| ... | @@ -1,81 +0,0 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | classifyCodexStderrLine, | ||
| 5 | isRecoverableThreadResumeError, | ||
| 6 | normalizeCodexModelSlug, | ||
| 7 | } from "./codexAppServerManager"; | ||
| 8 | |||
| 9 | describe("classifyCodexStderrLine", () => { | ||
| 10 | it("ignores empty lines", () => { | ||
| 11 | expect(classifyCodexStderrLine(" ")).toBeNull(); | ||
| 12 | }); | ||
| 13 | |||
| 14 | it("ignores non-error structured codex logs", () => { | ||
| 15 | const line = | ||
| 16 | "2026-02-08T04:24:19.241256Z WARN codex_core::features: unknown feature key in config: skills"; | ||
| 17 | expect(classifyCodexStderrLine(line)).toBeNull(); | ||
| 18 | }); | ||
| 19 | |||
| 20 | it("ignores known benign rollout path errors", () => { | ||
| 21 | const line = | ||
| 22 | "\u001b[2m2026-02-08T04:24:20.085687Z\u001b[0m \u001b[31mERROR\u001b[0m \u001b[2mcodex_core::rollout::list\u001b[0m: state db missing rollout path for thread 019c3b6c-46b8-7b70-ad23-82f824d161fb"; | ||
| 23 | expect(classifyCodexStderrLine(line)).toBeNull(); | ||
| 24 | }); | ||
| 25 | |||
| 26 | it("keeps unknown structured errors", () => { | ||
| 27 | const line = "2026-02-08T04:24:20.085687Z ERROR codex_core::runtime: unrecoverable failure"; | ||
| 28 | expect(classifyCodexStderrLine(line)).toEqual({ | ||
| 29 | message: line, | ||
| 30 | }); | ||
| 31 | }); | ||
| 32 | |||
| 33 | it("keeps plain stderr messages", () => { | ||
| 34 | const line = "fatal: permission denied"; | ||
| 35 | expect(classifyCodexStderrLine(line)).toEqual({ | ||
| 36 | message: line, | ||
| 37 | }); | ||
| 38 | }); | ||
| 39 | }); | ||
| 40 | |||
| 41 | describe("normalizeCodexModelSlug", () => { | ||
| 42 | it("maps 5.3 aliases to gpt-5.3-codex", () => { | ||
| 43 | expect(normalizeCodexModelSlug("5.3")).toBe("gpt-5.3-codex"); | ||
| 44 | expect(normalizeCodexModelSlug("gpt-5.3")).toBe("gpt-5.3-codex"); | ||
| 45 | }); | ||
| 46 | |||
| 47 | it("prefers codex id when model differs", () => { | ||
| 48 | expect(normalizeCodexModelSlug("gpt-5.3", "gpt-5.3-codex")).toBe("gpt-5.3-codex"); | ||
| 49 | }); | ||
| 50 | |||
| 51 | it("keeps non-aliased models as-is", () => { | ||
| 52 | expect(normalizeCodexModelSlug("gpt-5.2-codex")).toBe("gpt-5.2-codex"); | ||
| 53 | expect(normalizeCodexModelSlug("gpt-5.2")).toBe("gpt-5.2"); | ||
| 54 | }); | ||
| 55 | }); | ||
| 56 | |||
| 57 | describe("isRecoverableThreadResumeError", () => { | ||
| 58 | it("matches not-found resume errors", () => { | ||
| 59 | expect( | ||
| 60 | isRecoverableThreadResumeError( | ||
| 61 | new Error("thread/resume failed: thread not found"), | ||
| 62 | ), | ||
| 63 | ).toBe(true); | ||
| 64 | }); | ||
| 65 | |||
| 66 | it("ignores non-resume errors", () => { | ||
| 67 | expect( | ||
| 68 | isRecoverableThreadResumeError( | ||
| 69 | new Error("thread/start failed: permission denied"), | ||
| 70 | ), | ||
| 71 | ).toBe(false); | ||
| 72 | }); | ||
| 73 | |||
| 74 | it("ignores non-recoverable resume errors", () => { | ||
| 75 | expect( | ||
| 76 | isRecoverableThreadResumeError( | ||
| 77 | new Error("thread/resume failed: timed out waiting for server"), | ||
| 78 | ), | ||
| 79 | ).toBe(false); | ||
| 80 | }); | ||
| 81 | }); | ||
apps/desktop/src/codexAppServerManager.ts deleted-847| ... | @@ -1,847 +0,0 @@ | ||
| 1 | import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; | ||
| 2 | import { randomUUID } from "node:crypto"; | ||
| 3 | import { EventEmitter } from "node:events"; | ||
| 4 | import readline from "node:readline"; | ||
| 5 | |||
| 6 | import type { | ||
| 7 | ProviderApprovalDecision, | ||
| 8 | ProviderEvent, | ||
| 9 | ProviderRequestKind, | ||
| 10 | ProviderSendTurnInput, | ||
| 11 | ProviderSession, | ||
| 12 | ProviderSessionStartInput, | ||
| 13 | ProviderTurnStartResult, | ||
| 14 | } from "@acme/contracts"; | ||
| 15 | import { normalizeModelSlug } from "@acme/contracts"; | ||
| 16 | |||
| 17 | type PendingRequestKey = string; | ||
| 18 | |||
| 19 | interface PendingRequest { | ||
| 20 | method: string; | ||
| 21 | timeout: ReturnType<typeof setTimeout>; | ||
| 22 | resolve: (value: unknown) => void; | ||
| 23 | reject: (error: Error) => void; | ||
| 24 | } | ||
| 25 | |||
| 26 | interface PendingApprovalRequest { | ||
| 27 | requestId: string; | ||
| 28 | jsonRpcId: string | number; | ||
| 29 | method: | ||
| 30 | | "item/commandExecution/requestApproval" | ||
| 31 | | "item/fileChange/requestApproval"; | ||
| 32 | requestKind: ProviderRequestKind; | ||
| 33 | threadId?: string; | ||
| 34 | turnId?: string; | ||
| 35 | itemId?: string; | ||
| 36 | } | ||
| 37 | |||
| 38 | interface CodexSessionContext { | ||
| 39 | session: ProviderSession; | ||
| 40 | child: ChildProcessWithoutNullStreams; | ||
| 41 | output: readline.Interface; | ||
| 42 | pending: Map<PendingRequestKey, PendingRequest>; | ||
| 43 | pendingApprovals: Map<string, PendingApprovalRequest>; | ||
| 44 | nextRequestId: number; | ||
| 45 | stopping: boolean; | ||
| 46 | } | ||
| 47 | |||
| 48 | interface JsonRpcError { | ||
| 49 | code?: number; | ||
| 50 | message?: string; | ||
| 51 | } | ||
| 52 | |||
| 53 | interface JsonRpcRequest { | ||
| 54 | id: string | number; | ||
| 55 | method: string; | ||
| 56 | params?: unknown; | ||
| 57 | } | ||
| 58 | |||
| 59 | interface JsonRpcResponse { | ||
| 60 | id: string | number; | ||
| 61 | result?: unknown; | ||
| 62 | error?: JsonRpcError; | ||
| 63 | } | ||
| 64 | |||
| 65 | interface JsonRpcNotification { | ||
| 66 | method: string; | ||
| 67 | params?: unknown; | ||
| 68 | } | ||
| 69 | |||
| 70 | const ANSI_ESCAPE_CHAR = String.fromCharCode(27); | ||
| 71 | const ANSI_ESCAPE_REGEX = new RegExp(`${ANSI_ESCAPE_CHAR}\\[[0-9;]*m`, "g"); | ||
| 72 | const CODEX_STDERR_LOG_REGEX = | ||
| 73 | /^\d{4}-\d{2}-\d{2}T\S+\s+(TRACE|DEBUG|INFO|WARN|ERROR)\s+\S+:\s+(.*)$/; | ||
| 74 | const BENIGN_ERROR_LOG_SNIPPETS = [ | ||
| 75 | "state db missing rollout path for thread", | ||
| 76 | "state db record_discrepancy: find_thread_path_by_id_str_in_subdir, falling_back", | ||
| 77 | ]; | ||
| 78 | const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [ | ||
| 79 | "not found", | ||
| 80 | "missing thread", | ||
| 81 | "no such thread", | ||
| 82 | "unknown thread", | ||
| 83 | "does not exist", | ||
| 84 | ]; | ||
| 85 | |||
| 86 | export function normalizeCodexModelSlug( | ||
| 87 | model: string | undefined | null, | ||
| 88 | preferredId?: string, | ||
| 89 | ): string | undefined { | ||
| 90 | const normalized = normalizeModelSlug(model); | ||
| 91 | if (!normalized) { | ||
| 92 | return undefined; | ||
| 93 | } | ||
| 94 | |||
| 95 | if (preferredId?.endsWith("-codex") && preferredId !== normalized) { | ||
| 96 | return preferredId; | ||
| 97 | } | ||
| 98 | |||
| 99 | return normalized; | ||
| 100 | } | ||
| 101 | |||
| 102 | export function classifyCodexStderrLine(rawLine: string): { message: string } | null { | ||
| 103 | const line = rawLine.replaceAll(ANSI_ESCAPE_REGEX, "").trim(); | ||
| 104 | if (!line) { | ||
| 105 | return null; | ||
| 106 | } | ||
| 107 | |||
| 108 | const match = line.match(CODEX_STDERR_LOG_REGEX); | ||
| 109 | if (match) { | ||
| 110 | const level = match[1]; | ||
| 111 | if (level && level !== "ERROR") { | ||
| 112 | return null; | ||
| 113 | } | ||
| 114 | |||
| 115 | const isBenignError = BENIGN_ERROR_LOG_SNIPPETS.some((snippet) => line.includes(snippet)); | ||
| 116 | if (isBenignError) { | ||
| 117 | return null; | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | return { message: line }; | ||
| 122 | } | ||
| 123 | |||
| 124 | export function isRecoverableThreadResumeError(error: unknown): boolean { | ||
| 125 | const message = ( | ||
| 126 | error instanceof Error ? error.message : String(error) | ||
| 127 | ).toLowerCase(); | ||
| 128 | if (!message.includes("thread/resume")) { | ||
| 129 | return false; | ||
| 130 | } | ||
| 131 | |||
| 132 | return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => | ||
| 133 | message.includes(snippet), | ||
| 134 | ); | ||
| 135 | } | ||
| 136 | |||
| 137 | export interface CodexAppServerManagerEvents { | ||
| 138 | event: [event: ProviderEvent]; | ||
| 139 | } | ||
| 140 | |||
| 141 | export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEvents> { | ||
| 142 | private readonly sessions = new Map<string, CodexSessionContext>(); | ||
| 143 | |||
| 144 | async startSession(input: ProviderSessionStartInput): Promise<ProviderSession> { | ||
| 145 | const sessionId = randomUUID(); | ||
| 146 | const now = new Date().toISOString(); | ||
| 147 | |||
| 148 | const session: ProviderSession = { | ||
| 149 | sessionId, | ||
| 150 | provider: "codex", | ||
| 151 | status: "connecting", | ||
| 152 | model: normalizeCodexModelSlug(input.model), | ||
| 153 | cwd: input.cwd, | ||
| 154 | createdAt: now, | ||
| 155 | updatedAt: now, | ||
| 156 | }; | ||
| 157 | |||
| 158 | const child = spawn("codex", ["app-server"], { | ||
| 159 | cwd: input.cwd, | ||
| 160 | env: process.env, | ||
| 161 | stdio: ["pipe", "pipe", "pipe"], | ||
| 162 | }); | ||
| 163 | const output = readline.createInterface({ input: child.stdout }); | ||
| 164 | |||
| 165 | const context: CodexSessionContext = { | ||
| 166 | session, | ||
| 167 | child, | ||
| 168 | output, | ||
| 169 | pending: new Map(), | ||
| 170 | pendingApprovals: new Map(), | ||
| 171 | nextRequestId: 1, | ||
| 172 | stopping: false, | ||
| 173 | }; | ||
| 174 | |||
| 175 | this.sessions.set(sessionId, context); | ||
| 176 | this.attachProcessListeners(context); | ||
| 177 | |||
| 178 | this.emitLifecycleEvent(context, "session/connecting", "Starting codex app-server"); | ||
| 179 | |||
| 180 | try { | ||
| 181 | await this.sendRequest(context, "initialize", { | ||
| 182 | clientInfo: { | ||
| 183 | name: "codething_desktop", | ||
| 184 | title: "CodeThing Desktop", | ||
| 185 | version: "0.1.0", | ||
| 186 | }, | ||
| 187 | capabilities: { | ||
| 188 | experimentalApi: false, | ||
| 189 | }, | ||
| 190 | }); | ||
| 191 | |||
| 192 | this.writeMessage(context, { method: "initialized" }); | ||
| 193 | |||
| 194 | const normalizedModel = normalizeCodexModelSlug(input.model); | ||
| 195 | const sessionOverrides = { | ||
| 196 | model: normalizedModel ?? null, | ||
| 197 | cwd: input.cwd ?? null, | ||
| 198 | approvalPolicy: input.approvalPolicy, | ||
| 199 | sandbox: input.sandboxMode, | ||
| 200 | }; | ||
| 201 | |||
| 202 | const threadStartParams = { | ||
| 203 | ...sessionOverrides, | ||
| 204 | experimentalRawEvents: false, | ||
| 205 | }; | ||
| 206 | |||
| 207 | let threadOpenMethod: "thread/start" | "thread/resume" = "thread/start"; | ||
| 208 | let threadOpenResponse: unknown; | ||
| 209 | if (input.resumeThreadId) { | ||
| 210 | try { | ||
| 211 | threadOpenMethod = "thread/resume"; | ||
| 212 | threadOpenResponse = await this.sendRequest( | ||
| 213 | context, | ||
| 214 | "thread/resume", | ||
| 215 | { | ||
| 216 | ...sessionOverrides, | ||
| 217 | threadId: input.resumeThreadId, | ||
| 218 | }, | ||
| 219 | ); | ||
| 220 | } catch (error) { | ||
| 221 | if (!isRecoverableThreadResumeError(error)) { | ||
| 222 | throw error; | ||
| 223 | } | ||
| 224 | |||
| 225 | threadOpenMethod = "thread/start"; | ||
| 226 | this.emitLifecycleEvent( | ||
| 227 | context, | ||
| 228 | "session/threadResumeFallback", | ||
| 229 | `Could not resume thread ${input.resumeThreadId}; started a new thread instead.`, | ||
| 230 | ); | ||
| 231 | threadOpenResponse = await this.sendRequest( | ||
| 232 | context, | ||
| 233 | "thread/start", | ||
| 234 | threadStartParams, | ||
| 235 | ); | ||
| 236 | } | ||
| 237 | } else { | ||
| 238 | threadOpenMethod = "thread/start"; | ||
| 239 | threadOpenResponse = await this.sendRequest( | ||
| 240 | context, | ||
| 241 | "thread/start", | ||
| 242 | threadStartParams, | ||
| 243 | ); | ||
| 244 | } | ||
| 245 | |||
| 246 | const threadOpenRecord = this.readObject(threadOpenResponse); | ||
| 247 | const threadId = | ||
| 248 | this.readString(this.readObject(threadOpenRecord, "thread"), "id") ?? | ||
| 249 | this.readString(threadOpenRecord, "threadId"); | ||
| 250 | if (!threadId) { | ||
| 251 | throw new Error( | ||
| 252 | `${threadOpenMethod} response did not include a thread id.`, | ||
| 253 | ); | ||
| 254 | } | ||
| 255 | |||
| 256 | this.updateSession(context, { | ||
| 257 | status: "ready", | ||
| 258 | threadId, | ||
| 259 | }); | ||
| 260 | this.emitLifecycleEvent(context, "session/ready", `Connected to thread ${threadId}`); | ||
| 261 | return { ...context.session }; | ||
| 262 | } catch (error) { | ||
| 263 | const message = error instanceof Error ? error.message : "Failed to start Codex session."; | ||
| 264 | this.updateSession(context, { | ||
| 265 | status: "error", | ||
| 266 | lastError: message, | ||
| 267 | }); | ||
| 268 | this.emitErrorEvent(context, "session/startFailed", message); | ||
| 269 | this.stopSession(sessionId); | ||
| 270 | throw new Error(message, { cause: error }); | ||
| 271 | } | ||
| 272 | } | ||
| 273 | |||
| 274 | async sendTurn(input: ProviderSendTurnInput): Promise<ProviderTurnStartResult> { | ||
| 275 | const context = this.requireSession(input.sessionId); | ||
| 276 | if (!context.session.threadId) { | ||
| 277 | throw new Error("Session is missing a thread id."); | ||
| 278 | } | ||
| 279 | |||
| 280 | const turnStartParams: { | ||
| 281 | threadId: string; | ||
| 282 | input: Array<{ type: "text"; text: string; text_elements: [] }>; | ||
| 283 | model?: string; | ||
| 284 | effort?: string; | ||
| 285 | } = { | ||
| 286 | threadId: context.session.threadId, | ||
| 287 | input: [ | ||
| 288 | { | ||
| 289 | type: "text", | ||
| 290 | text: input.input, | ||
| 291 | text_elements: [], | ||
| 292 | }, | ||
| 293 | ], | ||
| 294 | }; | ||
| 295 | const normalizedModel = normalizeCodexModelSlug(input.model); | ||
| 296 | if (normalizedModel) { | ||
| 297 | turnStartParams.model = normalizedModel; | ||
| 298 | } | ||
| 299 | if (input.effort) { | ||
| 300 | turnStartParams.effort = input.effort; | ||
| 301 | } | ||
| 302 | |||
| 303 | const response = await this.sendRequest(context, "turn/start", turnStartParams); | ||
| 304 | |||
| 305 | const turn = this.readObject(this.readObject(response), "turn"); | ||
| 306 | const turnId = this.readString(turn, "id"); | ||
| 307 | if (!turnId) { | ||
| 308 | throw new Error("turn/start response did not include a turn id."); | ||
| 309 | } | ||
| 310 | |||
| 311 | this.updateSession(context, { | ||
| 312 | status: "running", | ||
| 313 | activeTurnId: turnId, | ||
| 314 | }); | ||
| 315 | |||
| 316 | return { | ||
| 317 | threadId: context.session.threadId, | ||
| 318 | turnId, | ||
| 319 | }; | ||
| 320 | } | ||
| 321 | |||
| 322 | async interruptTurn(sessionId: string, turnId?: string): Promise<void> { | ||
| 323 | const context = this.requireSession(sessionId); | ||
| 324 | const effectiveTurnId = turnId ?? context.session.activeTurnId; | ||
| 325 | |||
| 326 | if (!effectiveTurnId || !context.session.threadId) { | ||
| 327 | return; | ||
| 328 | } | ||
| 329 | |||
| 330 | await this.sendRequest(context, "turn/interrupt", { | ||
| 331 | threadId: context.session.threadId, | ||
| 332 | turnId: effectiveTurnId, | ||
| 333 | }); | ||
| 334 | } | ||
| 335 | |||
| 336 | async respondToRequest( | ||
| 337 | sessionId: string, | ||
| 338 | requestId: string, | ||
| 339 | decision: ProviderApprovalDecision, | ||
| 340 | ): Promise<void> { | ||
| 341 | const context = this.requireSession(sessionId); | ||
| 342 | const pendingRequest = context.pendingApprovals.get(requestId); | ||
| 343 | if (!pendingRequest) { | ||
| 344 | throw new Error(`Unknown pending approval request: ${requestId}`); | ||
| 345 | } | ||
| 346 | |||
| 347 | context.pendingApprovals.delete(requestId); | ||
| 348 | this.writeMessage(context, { | ||
| 349 | id: pendingRequest.jsonRpcId, | ||
| 350 | result: { | ||
| 351 | decision, | ||
| 352 | }, | ||
| 353 | }); | ||
| 354 | |||
| 355 | this.emitEvent({ | ||
| 356 | id: randomUUID(), | ||
| 357 | kind: "notification", | ||
| 358 | provider: "codex", | ||
| 359 | sessionId: context.session.sessionId, | ||
| 360 | createdAt: new Date().toISOString(), | ||
| 361 | method: "item/requestApproval/decision", | ||
| 362 | threadId: pendingRequest.threadId, | ||
| 363 | turnId: pendingRequest.turnId, | ||
| 364 | itemId: pendingRequest.itemId, | ||
| 365 | requestId: pendingRequest.requestId, | ||
| 366 | requestKind: pendingRequest.requestKind, | ||
| 367 | payload: { | ||
| 368 | requestId: pendingRequest.requestId, | ||
| 369 | requestKind: pendingRequest.requestKind, | ||
| 370 | decision, | ||
| 371 | }, | ||
| 372 | }); | ||
| 373 | } | ||
| 374 | |||
| 375 | stopSession(sessionId: string): void { | ||
| 376 | const context = this.sessions.get(sessionId); | ||
| 377 | if (!context) { | ||
| 378 | return; | ||
| 379 | } | ||
| 380 | |||
| 381 | context.stopping = true; | ||
| 382 | |||
| 383 | for (const pending of context.pending.values()) { | ||
| 384 | clearTimeout(pending.timeout); | ||
| 385 | pending.reject(new Error("Session stopped before request completed.")); | ||
| 386 | } | ||
| 387 | context.pending.clear(); | ||
| 388 | context.pendingApprovals.clear(); | ||
| 389 | |||
| 390 | context.output.close(); | ||
| 391 | |||
| 392 | if (!context.child.killed) { | ||
| 393 | context.child.kill(); | ||
| 394 | } | ||
| 395 | |||
| 396 | this.updateSession(context, { | ||
| 397 | status: "closed", | ||
| 398 | activeTurnId: undefined, | ||
| 399 | }); | ||
| 400 | this.emitLifecycleEvent(context, "session/closed", "Session stopped"); | ||
| 401 | this.sessions.delete(sessionId); | ||
| 402 | } | ||
| 403 | |||
| 404 | listSessions(): ProviderSession[] { | ||
| 405 | return Array.from(this.sessions.values(), ({ session }) => ({ | ||
| 406 | ...session, | ||
| 407 | })); | ||
| 408 | } | ||
| 409 | |||
| 410 | hasSession(sessionId: string): boolean { | ||
| 411 | return this.sessions.has(sessionId); | ||
| 412 | } | ||
| 413 | |||
| 414 | stopAll(): void { | ||
| 415 | for (const sessionId of this.sessions.keys()) { | ||
| 416 | this.stopSession(sessionId); | ||
| 417 | } | ||
| 418 | } | ||
| 419 | |||
| 420 | private requireSession(sessionId: string): CodexSessionContext { | ||
| 421 | const context = this.sessions.get(sessionId); | ||
| 422 | if (!context) { | ||
| 423 | throw new Error(`Unknown session: ${sessionId}`); | ||
| 424 | } | ||
| 425 | |||
| 426 | if (context.session.status === "closed") { | ||
| 427 | throw new Error(`Session is closed: ${sessionId}`); | ||
| 428 | } | ||
| 429 | |||
| 430 | return context; | ||
| 431 | } | ||
| 432 | |||
| 433 | private attachProcessListeners(context: CodexSessionContext): void { | ||
| 434 | context.output.on("line", (line) => { | ||
| 435 | this.handleStdoutLine(context, line); | ||
| 436 | }); | ||
| 437 | |||
| 438 | context.child.stderr.on("data", (chunk: Buffer) => { | ||
| 439 | const raw = chunk.toString(); | ||
| 440 | const lines = raw.split(/\r?\n/g); | ||
| 441 | for (const rawLine of lines) { | ||
| 442 | const classified = classifyCodexStderrLine(rawLine); | ||
| 443 | if (!classified) { | ||
| 444 | continue; | ||
| 445 | } | ||
| 446 | |||
| 447 | this.emitErrorEvent(context, "process/stderr", classified.message); | ||
| 448 | } | ||
| 449 | }); | ||
| 450 | |||
| 451 | context.child.on("error", (error) => { | ||
| 452 | const message = error.message || "codex app-server process errored."; | ||
| 453 | this.updateSession(context, { | ||
| 454 | status: "error", | ||
| 455 | lastError: message, | ||
| 456 | }); | ||
| 457 | this.emitErrorEvent(context, "process/error", message); | ||
| 458 | }); | ||
| 459 | |||
| 460 | context.child.on("exit", (code, signal) => { | ||
| 461 | if (context.stopping) { | ||
| 462 | return; | ||
| 463 | } | ||
| 464 | |||
| 465 | const message = `codex app-server exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`; | ||
| 466 | this.updateSession(context, { | ||
| 467 | status: "closed", | ||
| 468 | activeTurnId: undefined, | ||
| 469 | lastError: code === 0 ? context.session.lastError : message, | ||
| 470 | }); | ||
| 471 | this.emitLifecycleEvent(context, "session/exited", message); | ||
| 472 | this.sessions.delete(context.session.sessionId); | ||
| 473 | }); | ||
| 474 | } | ||
| 475 | |||
| 476 | private handleStdoutLine(context: CodexSessionContext, line: string): void { | ||
| 477 | let parsed: unknown; | ||
| 478 | try { | ||
| 479 | parsed = JSON.parse(line); | ||
| 480 | } catch { | ||
| 481 | this.emitErrorEvent( | ||
| 482 | context, | ||
| 483 | "protocol/parseError", | ||
| 484 | "Received invalid JSON from codex app-server.", | ||
| 485 | ); | ||
| 486 | return; | ||
| 487 | } | ||
| 488 | |||
| 489 | if (!parsed || typeof parsed !== "object") { | ||
| 490 | this.emitErrorEvent( | ||
| 491 | context, | ||
| 492 | "protocol/invalidMessage", | ||
| 493 | "Received non-object protocol message.", | ||
| 494 | ); | ||
| 495 | return; | ||
| 496 | } | ||
| 497 | |||
| 498 | if (this.isServerRequest(parsed)) { | ||
| 499 | this.handleServerRequest(context, parsed); | ||
| 500 | return; | ||
| 501 | } | ||
| 502 | |||
| 503 | if (this.isServerNotification(parsed)) { | ||
| 504 | this.handleServerNotification(context, parsed); | ||
| 505 | return; | ||
| 506 | } | ||
| 507 | |||
| 508 | if (this.isResponse(parsed)) { | ||
| 509 | this.handleResponse(context, parsed); | ||
| 510 | return; | ||
| 511 | } | ||
| 512 | |||
| 513 | this.emitErrorEvent( | ||
| 514 | context, | ||
| 515 | "protocol/unrecognizedMessage", | ||
| 516 | "Received protocol message in an unknown shape.", | ||
| 517 | ); | ||
| 518 | } | ||
| 519 | |||
| 520 | private handleServerNotification( | ||
| 521 | context: CodexSessionContext, | ||
| 522 | notification: JsonRpcNotification, | ||
| 523 | ): void { | ||
| 524 | const route = this.readRouteFields(notification.params); | ||
| 525 | const textDelta = | ||
| 526 | notification.method === "item/agentMessage/delta" | ||
| 527 | ? this.readString(notification.params, "delta") | ||
| 528 | : undefined; | ||
| 529 | |||
| 530 | this.emitEvent({ | ||
| 531 | id: randomUUID(), | ||
| 532 | kind: "notification", | ||
| 533 | provider: "codex", | ||
| 534 | sessionId: context.session.sessionId, | ||
| 535 | createdAt: new Date().toISOString(), | ||
| 536 | method: notification.method, | ||
| 537 | threadId: route.threadId, | ||
| 538 | turnId: route.turnId, | ||
| 539 | itemId: route.itemId, | ||
| 540 | textDelta, | ||
| 541 | payload: notification.params, | ||
| 542 | }); | ||
| 543 | |||
| 544 | if (notification.method === "thread/started") { | ||
| 545 | const threadId = this.readString(this.readObject(notification.params)?.thread, "id"); | ||
| 546 | if (threadId) { | ||
| 547 | this.updateSession(context, { threadId }); | ||
| 548 | } | ||
| 549 | return; | ||
| 550 | } | ||
| 551 | |||
| 552 | if (notification.method === "turn/started") { | ||
| 553 | const turnId = this.readString(this.readObject(notification.params)?.turn, "id"); | ||
| 554 | this.updateSession(context, { | ||
| 555 | status: "running", | ||
| 556 | activeTurnId: turnId, | ||
| 557 | }); | ||
| 558 | return; | ||
| 559 | } | ||
| 560 | |||
| 561 | if (notification.method === "turn/completed") { | ||
| 562 | const turn = this.readObject(notification.params, "turn"); | ||
| 563 | const status = this.readString(turn, "status"); | ||
| 564 | const errorMessage = this.readString(this.readObject(turn, "error"), "message"); | ||
| 565 | this.updateSession(context, { | ||
| 566 | status: status === "failed" ? "error" : "ready", | ||
| 567 | activeTurnId: undefined, | ||
| 568 | lastError: errorMessage ?? context.session.lastError, | ||
| 569 | }); | ||
| 570 | return; | ||
| 571 | } | ||
| 572 | |||
| 573 | if (notification.method === "error") { | ||
| 574 | const message = this.readString(this.readObject(notification.params)?.error, "message"); | ||
| 575 | const willRetry = this.readBoolean(notification.params, "willRetry"); | ||
| 576 | |||
| 577 | this.updateSession(context, { | ||
| 578 | status: willRetry ? "running" : "error", | ||
| 579 | lastError: message ?? context.session.lastError, | ||
| 580 | }); | ||
| 581 | } | ||
| 582 | } | ||
| 583 | |||
| 584 | private handleServerRequest(context: CodexSessionContext, request: JsonRpcRequest): void { | ||
| 585 | const route = this.readRouteFields(request.params); | ||
| 586 | const requestKind = this.requestKindForMethod(request.method); | ||
| 587 | let requestId: string | undefined; | ||
| 588 | if (requestKind) { | ||
| 589 | requestId = randomUUID(); | ||
| 590 | const pendingRequest: PendingApprovalRequest = { | ||
| 591 | requestId, | ||
| 592 | jsonRpcId: request.id, | ||
| 593 | method: | ||
| 594 | requestKind === "command" | ||
| 595 | ? "item/commandExecution/requestApproval" | ||
| 596 | : "item/fileChange/requestApproval", | ||
| 597 | requestKind, | ||
| 598 | ...(route.threadId ? { threadId: route.threadId } : {}), | ||
| 599 | ...(route.turnId ? { turnId: route.turnId } : {}), | ||
| 600 | ...(route.itemId ? { itemId: route.itemId } : {}), | ||
| 601 | }; | ||
| 602 | context.pendingApprovals.set(requestId, pendingRequest); | ||
| 603 | } | ||
| 604 | |||
| 605 | this.emitEvent({ | ||
| 606 | id: randomUUID(), | ||
| 607 | kind: "request", | ||
| 608 | provider: "codex", | ||
| 609 | sessionId: context.session.sessionId, | ||
| 610 | createdAt: new Date().toISOString(), | ||
| 611 | method: request.method, | ||
| 612 | threadId: route.threadId, | ||
| 613 | turnId: route.turnId, | ||
| 614 | itemId: route.itemId, | ||
| 615 | requestId, | ||
| 616 | requestKind, | ||
| 617 | payload: request.params, | ||
| 618 | }); | ||
| 619 | |||
| 620 | if (requestKind) { | ||
| 621 | return; | ||
| 622 | } | ||
| 623 | |||
| 624 | if (request.method === "item/tool/requestUserInput") { | ||
| 625 | this.writeMessage(context, { | ||
| 626 | id: request.id, | ||
| 627 | result: { answers: {} }, | ||
| 628 | }); | ||
| 629 | return; | ||
| 630 | } | ||
| 631 | |||
| 632 | this.writeMessage(context, { | ||
| 633 | id: request.id, | ||
| 634 | error: { | ||
| 635 | code: -32601, | ||
| 636 | message: `Unsupported server request: ${request.method}`, | ||
| 637 | }, | ||
| 638 | }); | ||
| 639 | } | ||
| 640 | |||
| 641 | private handleResponse(context: CodexSessionContext, response: JsonRpcResponse): void { | ||
| 642 | const key = String(response.id); | ||
| 643 | const pending = context.pending.get(key); | ||
| 644 | if (!pending) { | ||
| 645 | return; | ||
| 646 | } | ||
| 647 | |||
| 648 | clearTimeout(pending.timeout); | ||
| 649 | context.pending.delete(key); | ||
| 650 | |||
| 651 | if (response.error?.message) { | ||
| 652 | pending.reject(new Error(`${pending.method} failed: ${String(response.error.message)}`)); | ||
| 653 | return; | ||
| 654 | } | ||
| 655 | |||
| 656 | pending.resolve(response.result); | ||
| 657 | } | ||
| 658 | |||
| 659 | private async sendRequest<TResponse>( | ||
| 660 | context: CodexSessionContext, | ||
| 661 | method: string, | ||
| 662 | params: unknown, | ||
| 663 | timeoutMs = 20_000, | ||
| 664 | ): Promise<TResponse> { | ||
| 665 | const id = context.nextRequestId; | ||
| 666 | context.nextRequestId += 1; | ||
| 667 | |||
| 668 | const result = await new Promise<unknown>((resolve, reject) => { | ||
| 669 | const timeout = setTimeout(() => { | ||
| 670 | context.pending.delete(String(id)); | ||
| 671 | reject(new Error(`Timed out waiting for ${method}.`)); | ||
| 672 | }, timeoutMs); | ||
| 673 | |||
| 674 | context.pending.set(String(id), { | ||
| 675 | method, | ||
| 676 | timeout, | ||
| 677 | resolve, | ||
| 678 | reject, | ||
| 679 | }); | ||
| 680 | this.writeMessage(context, { | ||
| 681 | method, | ||
| 682 | id, | ||
| 683 | params, | ||
| 684 | }); | ||
| 685 | }); | ||
| 686 | |||
| 687 | return result as TResponse; | ||
| 688 | } | ||
| 689 | |||
| 690 | private writeMessage(context: CodexSessionContext, message: unknown): void { | ||
| 691 | const encoded = JSON.stringify(message); | ||
| 692 | if (!context.child.stdin.writable) { | ||
| 693 | throw new Error("Cannot write to codex app-server stdin."); | ||
| 694 | } | ||
| 695 | |||
| 696 | context.child.stdin.write(`${encoded}\n`); | ||
| 697 | } | ||
| 698 | |||
| 699 | private emitLifecycleEvent(context: CodexSessionContext, method: string, message: string): void { | ||
| 700 | this.emitEvent({ | ||
| 701 | id: randomUUID(), | ||
| 702 | kind: "session", | ||
| 703 | provider: "codex", | ||
| 704 | sessionId: context.session.sessionId, | ||
| 705 | createdAt: new Date().toISOString(), | ||
| 706 | method, | ||
| 707 | message, | ||
| 708 | }); | ||
| 709 | } | ||
| 710 | |||
| 711 | private emitErrorEvent(context: CodexSessionContext, method: string, message: string): void { | ||
| 712 | this.emitEvent({ | ||
| 713 | id: randomUUID(), | ||
| 714 | kind: "error", | ||
| 715 | provider: "codex", | ||
| 716 | sessionId: context.session.sessionId, | ||
| 717 | createdAt: new Date().toISOString(), | ||
| 718 | method, | ||
| 719 | message, | ||
| 720 | }); | ||
| 721 | } | ||
| 722 | |||
| 723 | private emitEvent(event: ProviderEvent): void { | ||
| 724 | this.emit("event", event); | ||
| 725 | } | ||
| 726 | |||
| 727 | private updateSession(context: CodexSessionContext, updates: Partial<ProviderSession>): void { | ||
| 728 | context.session = { | ||
| 729 | ...context.session, | ||
| 730 | ...updates, | ||
| 731 | updatedAt: new Date().toISOString(), | ||
| 732 | }; | ||
| 733 | } | ||
| 734 | |||
| 735 | private requestKindForMethod( | ||
| 736 | method: string, | ||
| 737 | ): ProviderRequestKind | undefined { | ||
| 738 | if (method === "item/commandExecution/requestApproval") { | ||
| 739 | return "command"; | ||
| 740 | } | ||
| 741 | |||
| 742 | if (method === "item/fileChange/requestApproval") { | ||
| 743 | return "file-change"; | ||
| 744 | } | ||
| 745 | |||
| 746 | return undefined; | ||
| 747 | } | ||
| 748 | |||
| 749 | private isServerRequest(value: unknown): value is JsonRpcRequest { | ||
| 750 | if (!value || typeof value !== "object") { | ||
| 751 | return false; | ||
| 752 | } | ||
| 753 | |||
| 754 | const candidate = value as Record<string, unknown>; | ||
| 755 | return ( | ||
| 756 | typeof candidate.method === "string" && | ||
| 757 | (typeof candidate.id === "string" || typeof candidate.id === "number") | ||
| 758 | ); | ||
| 759 | } | ||
| 760 | |||
| 761 | private isServerNotification(value: unknown): value is JsonRpcNotification { | ||
| 762 | if (!value || typeof value !== "object") { | ||
| 763 | return false; | ||
| 764 | } | ||
| 765 | |||
| 766 | const candidate = value as Record<string, unknown>; | ||
| 767 | return typeof candidate.method === "string" && !("id" in candidate); | ||
| 768 | } | ||
| 769 | |||
| 770 | private isResponse(value: unknown): value is JsonRpcResponse { | ||
| 771 | if (!value || typeof value !== "object") { | ||
| 772 | return false; | ||
| 773 | } | ||
| 774 | |||
| 775 | const candidate = value as Record<string, unknown>; | ||
| 776 | const hasId = typeof candidate.id === "string" || typeof candidate.id === "number"; | ||
| 777 | const hasMethod = typeof candidate.method === "string"; | ||
| 778 | return hasId && !hasMethod; | ||
| 779 | } | ||
| 780 | |||
| 781 | private readRouteFields(params: unknown): { | ||
| 782 | threadId?: string; | ||
| 783 | turnId?: string; | ||
| 784 | itemId?: string; | ||
| 785 | } { | ||
| 786 | const route: { | ||
| 787 | threadId?: string; | ||
| 788 | turnId?: string; | ||
| 789 | itemId?: string; | ||
| 790 | } = {}; | ||
| 791 | |||
| 792 | const threadId = | ||
| 793 | this.readString(params, "threadId") ?? | ||
| 794 | this.readString(this.readObject(params, "thread"), "id"); | ||
| 795 | const turnId = | ||
| 796 | this.readString(params, "turnId") ?? this.readString(this.readObject(params, "turn"), "id"); | ||
| 797 | const itemId = | ||
| 798 | this.readString(params, "itemId") ?? this.readString(this.readObject(params, "item"), "id"); | ||
| 799 | |||
| 800 | if (threadId) { | ||
| 801 | route.threadId = threadId; | ||
| 802 | } | ||
| 803 | |||
| 804 | if (turnId) { | ||
| 805 | route.turnId = turnId; | ||
| 806 | } | ||
| 807 | |||
| 808 | if (itemId) { | ||
| 809 | route.itemId = itemId; | ||
| 810 | } | ||
| 811 | |||
| 812 | return route; | ||
| 813 | } | ||
| 814 | |||
| 815 | private readObject(value: unknown, key?: string): Record<string, unknown> | undefined { | ||
| 816 | const target = | ||
| 817 | key === undefined | ||
| 818 | ? value | ||
| 819 | : value && typeof value === "object" | ||
| 820 | ? (value as Record<string, unknown>)[key] | ||
| 821 | : undefined; | ||
| 822 | |||
| 823 | if (!target || typeof target !== "object") { | ||
| 824 | return undefined; | ||
| 825 | } | ||
| 826 | |||
| 827 | return target as Record<string, unknown>; | ||
| 828 | } | ||
| 829 | |||
| 830 | private readString(value: unknown, key: string): string | undefined { | ||
| 831 | if (!value || typeof value !== "object") { | ||
| 832 | return undefined; | ||
| 833 | } | ||
| 834 | |||
| 835 | const candidate = (value as Record<string, unknown>)[key]; | ||
| 836 | return typeof candidate === "string" ? candidate : undefined; | ||
| 837 | } | ||
| 838 | |||
| 839 | private readBoolean(value: unknown, key: string): boolean | undefined { | ||
| 840 | if (!value || typeof value !== "object") { | ||
| 841 | return undefined; | ||
| 842 | } | ||
| 843 | |||
| 844 | const candidate = (value as Record<string, unknown>)[key]; | ||
| 845 | return typeof candidate === "boolean" ? candidate : undefined; | ||
| 846 | } | ||
| 847 | } | ||
apps/desktop/src/fixPath.ts+1-1| ... | @@ -13,6 +13,6 @@ export function fixPath(): void { | ... | @@ -13,6 +13,6 @@ export function fixPath(): void { |
| 13 | process.env.PATH = result; | 13 | process.env.PATH = result; |
| 14 | } | 14 | } |
| 15 | } catch { | 15 | } catch { |
| 16 | // Silently ignore — keep default PATH | 16 | // Keep inherited PATH if shell lookup fails. |
| 17 | } | 17 | } |
| 18 | } | 18 | } |
apps/desktop/src/ipcHelpers.test.ts deleted-104| ... | @@ -1,104 +0,0 @@ | ||
| 1 | import { describe, expect, it, vi } from "vitest"; | ||
| 2 | |||
| 3 | import { withParsedArgs, withParsedPayload } from "./ipcHelpers"; | ||
| 4 | |||
| 5 | describe("withParsedPayload", () => { | ||
| 6 | it("parses payload and passes typed input to handler", async () => { | ||
| 7 | const handler = vi.fn(async (_event: unknown, payload: { value: string }) => | ||
| 8 | payload.value.toUpperCase(), | ||
| 9 | ); | ||
| 10 | const wrapped = withParsedPayload( | ||
| 11 | { | ||
| 12 | parse(payload: unknown): { value: string } { | ||
| 13 | if ( | ||
| 14 | !payload || | ||
| 15 | typeof payload !== "object" || | ||
| 16 | typeof (payload as { value?: unknown }).value !== "string" | ||
| 17 | ) { | ||
| 18 | throw new Error("Invalid payload"); | ||
| 19 | } | ||
| 20 | |||
| 21 | return { value: (payload as { value: string }).value }; | ||
| 22 | }, | ||
| 23 | }, | ||
| 24 | handler, | ||
| 25 | ); | ||
| 26 | |||
| 27 | const result = await wrapped({}, { value: "hello" }); | ||
| 28 | expect(result).toBe("HELLO"); | ||
| 29 | expect(handler).toHaveBeenCalledWith({}, { value: "hello" }); | ||
| 30 | }); | ||
| 31 | |||
| 32 | it("throws and does not call handler on invalid payload", async () => { | ||
| 33 | const handler = vi.fn(async () => "ok"); | ||
| 34 | const wrapped = withParsedPayload( | ||
| 35 | { | ||
| 36 | parse(payload: unknown): { value: string } { | ||
| 37 | if ( | ||
| 38 | !payload || | ||
| 39 | typeof payload !== "object" || | ||
| 40 | typeof (payload as { value?: unknown }).value !== "string" | ||
| 41 | ) { | ||
| 42 | throw new Error("Invalid payload"); | ||
| 43 | } | ||
| 44 | |||
| 45 | return { value: (payload as { value: string }).value }; | ||
| 46 | }, | ||
| 47 | }, | ||
| 48 | handler, | ||
| 49 | ); | ||
| 50 | |||
| 51 | expect(() => wrapped({}, { value: 123 })).toThrow(); | ||
| 52 | expect(handler).not.toHaveBeenCalled(); | ||
| 53 | }); | ||
| 54 | }); | ||
| 55 | |||
| 56 | describe("withParsedArgs", () => { | ||
| 57 | it("parses tuple arguments before invoking handler", () => { | ||
| 58 | const handler = vi.fn((_event: unknown, sessionId: string, data: string) => { | ||
| 59 | return `${sessionId}:${data}`; | ||
| 60 | }); | ||
| 61 | const wrapped = withParsedArgs( | ||
| 62 | { | ||
| 63 | parse(args: unknown[]): [string, string] { | ||
| 64 | const [sessionId, data] = args; | ||
| 65 | if (typeof sessionId !== "string" || sessionId.length === 0) { | ||
| 66 | throw new Error("Invalid sessionId"); | ||
| 67 | } | ||
| 68 | if (typeof data !== "string") { | ||
| 69 | throw new Error("Invalid data"); | ||
| 70 | } | ||
| 71 | |||
| 72 | return [sessionId, data]; | ||
| 73 | }, | ||
| 74 | }, | ||
| 75 | handler, | ||
| 76 | ); | ||
| 77 | |||
| 78 | expect(wrapped({}, "abc", "input")).toBe("abc:input"); | ||
| 79 | expect(handler).toHaveBeenCalledWith({}, "abc", "input"); | ||
| 80 | }); | ||
| 81 | |||
| 82 | it("throws and does not call handler when args are invalid", () => { | ||
| 83 | const handler = vi.fn(); | ||
| 84 | const wrapped = withParsedArgs( | ||
| 85 | { | ||
| 86 | parse(args: unknown[]): [string, string] { | ||
| 87 | const [sessionId, data] = args; | ||
| 88 | if (typeof sessionId !== "string" || sessionId.length === 0) { | ||
| 89 | throw new Error("Invalid sessionId"); | ||
| 90 | } | ||
| 91 | if (typeof data !== "string") { | ||
| 92 | throw new Error("Invalid data"); | ||
| 93 | } | ||
| 94 | |||
| 95 | return [sessionId, data]; | ||
| 96 | }, | ||
| 97 | }, | ||
| 98 | handler, | ||
| 99 | ); | ||
| 100 | |||
| 101 | expect(() => wrapped({}, 123, "input")).toThrow(); | ||
| 102 | expect(handler).not.toHaveBeenCalled(); | ||
| 103 | }); | ||
| 104 | }); | ||
apps/desktop/src/ipcHelpers.ts deleted-24| ... | @@ -1,24 +0,0 @@ | ||
| 1 | type MaybePromise<T> = T | Promise<T>; | ||
| 2 | type Parser<T> = { | ||
| 3 | parse: (value: unknown) => T; | ||
| 4 | }; | ||
| 5 | type ArgsParser<T extends unknown[]> = { | ||
| 6 | parse: (value: unknown[]) => T; | ||
| 7 | }; | ||
| 8 | |||
| 9 | export function withParsedPayload<TPayload, TResult>( | ||
| 10 | schema: Parser<TPayload>, | ||
| 11 | handler: (event: unknown, payload: TPayload) => MaybePromise<TResult>, | ||
| 12 | ): (event: unknown, payload: unknown) => MaybePromise<TResult> { | ||
| 13 | return (event, payload) => handler(event, schema.parse(payload)); | ||
| 14 | } | ||
| 15 | |||
| 16 | export function withParsedArgs<TArgs extends unknown[], TResult>( | ||
| 17 | schema: ArgsParser<TArgs>, | ||
| 18 | handler: (event: unknown, ...args: TArgs) => MaybePromise<TResult>, | ||
| 19 | ): (event: unknown, ...args: unknown[]) => MaybePromise<TResult> { | ||
| 20 | return (event, ...args) => { | ||
| 21 | const parsedArgs = schema.parse(args); | ||
| 22 | return handler(event, ...parsedArgs); | ||
| 23 | }; | ||
| 24 | } | ||
apps/desktop/src/main.ts+156-290| ... | @@ -1,111 +1,134 @@ | ... | @@ -1,111 +1,134 @@ |
| 1 | import { fixPath } from "./fixPath"; | 1 | import { spawn, type ChildProcess } from "node:child_process"; |
| 2 | fixPath(); | 2 | import { randomBytes } from "node:crypto"; |
| 3 | 3 | import fs from "node:fs"; | |
| 4 | import { spawn } from "node:child_process"; | 4 | import net from "node:net"; |
| 5 | import os from "node:os"; | ||
| 5 | import path from "node:path"; | 6 | import path from "node:path"; |
| 6 | import { BrowserWindow, app, dialog, ipcMain, session, shell } from "electron"; | ||
| 7 | 7 | ||
| 8 | import { | 8 | import { app, BrowserWindow, dialog, ipcMain } from "electron"; |
| 9 | EDITORS, | ||
| 10 | IPC_CHANNELS, | ||
| 11 | type TerminalCommandInput, | ||
| 12 | type TerminalCommandResult, | ||
| 13 | agentConfigSchema, | ||
| 14 | agentSessionIdSchema, | ||
| 15 | newTodoInputSchema, | ||
| 16 | providerInterruptTurnInputSchema, | ||
| 17 | providerRespondToRequestInputSchema, | ||
| 18 | providerSendTurnInputSchema, | ||
| 19 | providerSessionStartInputSchema, | ||
| 20 | providerStopSessionInputSchema, | ||
| 21 | terminalCommandInputSchema, | ||
| 22 | todoIdSchema, | ||
| 23 | } from "@acme/contracts"; | ||
| 24 | import { withParsedArgs, withParsedPayload } from "./ipcHelpers"; | ||
| 25 | import { ProcessManager } from "./processManager"; | ||
| 26 | import { ProviderManager } from "./providerManager"; | ||
| 27 | import { TodoStore } from "./todoStore"; | ||
| 28 | 9 | ||
| 29 | const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); | 10 | import { fixPath } from "./fixPath"; |
| 30 | 11 | ||
| 31 | let todoStore: TodoStore; | 12 | fixPath(); |
| 32 | const processManager = new ProcessManager(); | ||
| 33 | const providerManager = new ProviderManager(); | ||
| 34 | const agentWriteArgsParser = { | ||
| 35 | parse(args: unknown[]): [string, string] { | ||
| 36 | const [sessionId, data] = args; | ||
| 37 | if (typeof data !== "string") { | ||
| 38 | throw new Error("agent:write data must be a string"); | ||
| 39 | } | ||
| 40 | 13 | ||
| 41 | return [agentSessionIdSchema.parse(sessionId), data]; | 14 | const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; |
| 42 | }, | 15 | const ROOT_DIR = path.resolve(__dirname, "../../.."); |
| 43 | }; | 16 | const BACKEND_ENTRY = path.join(ROOT_DIR, "apps/server/dist/index.js"); |
| 17 | const RENDERER_ENTRY = path.join(ROOT_DIR, "apps/renderer/dist/index.html"); | ||
| 18 | const STATE_DIR = path.join(os.homedir(), ".t3", "userdata"); | ||
| 19 | const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); | ||
| 44 | 20 | ||
| 45 | function createWindow(): BrowserWindow { | 21 | let mainWindow: BrowserWindow | null = null; |
| 46 | const window = new BrowserWindow({ | 22 | let backendProcess: ChildProcess | null = null; |
| 47 | width: 1100, | 23 | let backendPort = 0; |
| 48 | height: 780, | 24 | let backendAuthToken = ""; |
| 49 | minWidth: 840, | 25 | let backendWsUrl = ""; |
| 50 | minHeight: 620, | 26 | let restartAttempt = 0; |
| 51 | show: false, | 27 | let restartTimer: ReturnType<typeof setTimeout> | null = null; |
| 52 | autoHideMenuBar: true, | 28 | let isQuitting = false; |
| 53 | titleBarStyle: "hiddenInset", | 29 | |
| 54 | trafficLightPosition: { x: 16, y: 18 }, | 30 | async function reserveLoopbackPort(): Promise<number> { |
| 55 | webPreferences: { | 31 | return new Promise((resolve, reject) => { |
| 56 | preload: path.join(__dirname, "preload.js"), | 32 | const probe = net.createServer(); |
| 57 | contextIsolation: true, | 33 | probe.listen(0, "127.0.0.1", () => { |
| 58 | nodeIntegration: false, | 34 | const address = probe.address(); |
| 59 | sandbox: true, | 35 | const port = typeof address === "object" && address !== null ? address.port : 0; |
| 60 | }, | 36 | probe.close(() => { |
| 37 | if (port > 0) { | ||
| 38 | resolve(port); | ||
| 39 | return; | ||
| 40 | } | ||
| 41 | reject(new Error("Failed to reserve backend port")); | ||
| 42 | }); | ||
| 43 | }); | ||
| 44 | probe.on("error", reject); | ||
| 61 | }); | 45 | }); |
| 46 | } | ||
| 62 | 47 | ||
| 63 | window.webContents.setWindowOpenHandler(() => ({ action: "deny" })); | 48 | function backendEnv(): NodeJS.ProcessEnv { |
| 49 | return { | ||
| 50 | ...process.env, | ||
| 51 | CODETHING_MODE: "desktop", | ||
| 52 | CODETHING_NO_BROWSER: "1", | ||
| 53 | CODETHING_PORT: String(backendPort), | ||
| 54 | CODETHING_STATE_DIR: STATE_DIR, | ||
| 55 | CODETHING_AUTH_TOKEN: backendAuthToken, | ||
| 56 | }; | ||
| 57 | } | ||
| 64 | 58 | ||
| 65 | window.once("ready-to-show", () => { | 59 | function scheduleBackendRestart(reason: string): void { |
| 66 | window.show(); | 60 | if (isQuitting || restartTimer) return; |
| 67 | }); | ||
| 68 | 61 | ||
| 69 | setupEventForwarding(window); | 62 | const delayMs = Math.min(500 * 2 ** restartAttempt, 10_000); |
| 63 | restartAttempt += 1; | ||
| 64 | console.error(`[desktop] backend exited unexpectedly (${reason}); restarting in ${delayMs}ms`); | ||
| 70 | 65 | ||
| 71 | if (isDevelopment) { | 66 | restartTimer = setTimeout(() => { |
| 72 | void window.loadURL(process.env.VITE_DEV_SERVER_URL as string); | 67 | restartTimer = null; |
| 73 | return window; | 68 | startBackend(); |
| 69 | }, delayMs); | ||
| 70 | } | ||
| 71 | |||
| 72 | function startBackend(): void { | ||
| 73 | if (isQuitting || backendProcess) return; | ||
| 74 | |||
| 75 | if (!fs.existsSync(BACKEND_ENTRY)) { | ||
| 76 | scheduleBackendRestart(`missing server entry at ${BACKEND_ENTRY}`); | ||
| 77 | return; | ||
| 74 | } | 78 | } |
| 75 | 79 | ||
| 76 | void window.loadFile(path.join(__dirname, "../../renderer/dist/index.html")); | 80 | const child = spawn(process.execPath, [BACKEND_ENTRY], { |
| 77 | return window; | 81 | cwd: ROOT_DIR, |
| 78 | } | 82 | env: backendEnv(), |
| 83 | stdio: "inherit", | ||
| 84 | }); | ||
| 85 | backendProcess = child; | ||
| 79 | 86 | ||
| 80 | function registerIpcHandlers(): void { | 87 | child.once("spawn", () => { |
| 81 | // Todo handlers | 88 | restartAttempt = 0; |
| 82 | ipcMain.handle(IPC_CHANNELS.todosList, async () => { | 89 | }); |
| 83 | return todoStore.list(); | 90 | |
| 91 | child.on("error", (error) => { | ||
| 92 | if (backendProcess === child) { | ||
| 93 | backendProcess = null; | ||
| 94 | } | ||
| 95 | scheduleBackendRestart(error.message); | ||
| 96 | }); | ||
| 97 | |||
| 98 | child.on("exit", (code, signal) => { | ||
| 99 | if (backendProcess === child) { | ||
| 100 | backendProcess = null; | ||
| 101 | } | ||
| 102 | if (isQuitting) return; | ||
| 103 | const reason = `code=${code ?? "null"} signal=${signal ?? "null"}`; | ||
| 104 | scheduleBackendRestart(reason); | ||
| 84 | }); | 105 | }); |
| 106 | } | ||
| 85 | 107 | ||
| 86 | ipcMain.handle( | 108 | function stopBackend(): void { |
| 87 | IPC_CHANNELS.todosAdd, | 109 | if (restartTimer) { |
| 88 | withParsedPayload(newTodoInputSchema, async (_event, payload) => { | 110 | clearTimeout(restartTimer); |
| 89 | return todoStore.add(payload); | 111 | restartTimer = null; |
| 90 | }), | 112 | } |
| 91 | ); | ||
| 92 | 113 | ||
| 93 | ipcMain.handle( | 114 | const child = backendProcess; |
| 94 | IPC_CHANNELS.todosToggle, | 115 | backendProcess = null; |
| 95 | withParsedPayload(todoIdSchema, async (_event, id) => { | 116 | if (!child) return; |
| 96 | return todoStore.toggle(id); | ||
| 97 | }), | ||
| 98 | ); | ||
| 99 | 117 | ||
| 100 | ipcMain.handle( | 118 | if (!child.killed) { |
| 101 | IPC_CHANNELS.todosRemove, | 119 | child.kill("SIGTERM"); |
| 102 | withParsedPayload(todoIdSchema, async (_event, id) => { | 120 | setTimeout(() => { |
| 103 | return todoStore.remove(id); | 121 | if (!child.killed) { |
| 104 | }), | 122 | child.kill("SIGKILL"); |
| 105 | ); | 123 | } |
| 124 | }, 2_000).unref(); | ||
| 125 | } | ||
| 126 | } | ||
| 106 | 127 | ||
| 107 | ipcMain.handle(IPC_CHANNELS.dialogPickFolder, async () => { | 128 | function registerIpcHandlers(): void { |
| 108 | const owner = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0]; | 129 | ipcMain.removeHandler(PICK_FOLDER_CHANNEL); |
| 130 | ipcMain.handle(PICK_FOLDER_CHANNEL, async () => { | ||
| 131 | const owner = BrowserWindow.getFocusedWindow() ?? mainWindow; | ||
| 109 | const result = owner | 132 | const result = owner |
| 110 | ? await dialog.showOpenDialog(owner, { | 133 | ? await dialog.showOpenDialog(owner, { |
| 111 | properties: ["openDirectory", "createDirectory"], | 134 | properties: ["openDirectory", "createDirectory"], |
| ... | @@ -113,233 +136,76 @@ function registerIpcHandlers(): void { | ... | @@ -113,233 +136,76 @@ function registerIpcHandlers(): void { |
| 113 | : await dialog.showOpenDialog({ | 136 | : await dialog.showOpenDialog({ |
| 114 | properties: ["openDirectory", "createDirectory"], | 137 | properties: ["openDirectory", "createDirectory"], |
| 115 | }); | 138 | }); |
| 116 | |||
| 117 | if (result.canceled) return null; | 139 | if (result.canceled) return null; |
| 118 | return result.filePaths[0] ?? null; | 140 | return result.filePaths[0] ?? null; |
| 119 | }); | 141 | }); |
| 142 | } | ||
| 120 | 143 | ||
| 121 | // Terminal handlers | 144 | function createWindow(): BrowserWindow { |
| 122 | ipcMain.handle( | 145 | const window = new BrowserWindow({ |
| 123 | IPC_CHANNELS.terminalRun, | 146 | width: 1100, |
| 124 | withParsedPayload(terminalCommandInputSchema, async (_event, payload) => { | 147 | height: 780, |
| 125 | return runTerminalCommand(payload); | 148 | minWidth: 840, |
| 126 | }), | 149 | minHeight: 620, |
| 127 | ); | 150 | show: false, |
| 128 | 151 | autoHideMenuBar: true, | |
| 129 | // Shell handlers | 152 | titleBarStyle: "hiddenInset", |
| 130 | ipcMain.handle( | 153 | trafficLightPosition: { x: 16, y: 18 }, |
| 131 | IPC_CHANNELS.shellOpenInEditor, | 154 | webPreferences: { |
| 132 | async (_event, cwd: string, editor: string) => { | 155 | preload: path.join(__dirname, "preload.js"), |
| 133 | if (!cwd) throw new Error("cwd is required"); | 156 | contextIsolation: true, |
| 134 | const editorDef = EDITORS.find((e) => e.id === editor); | 157 | nodeIntegration: false, |
| 135 | if (!editorDef) throw new Error(`Unknown editor: ${editor}`); | 158 | sandbox: true, |
| 136 | if (!editorDef.command) { | ||
| 137 | const error = await shell.openPath(cwd); | ||
| 138 | if (error) throw new Error(error); | ||
| 139 | return; | ||
| 140 | } | ||
| 141 | const child = spawn(editorDef.command, [cwd], { | ||
| 142 | detached: true, | ||
| 143 | stdio: "ignore", | ||
| 144 | }); | ||
| 145 | child.on("error", () => { | ||
| 146 | /* ignore spawn failures for detached editors */ | ||
| 147 | }); | ||
| 148 | child.unref(); | ||
| 149 | }, | 159 | }, |
| 150 | ); | ||
| 151 | |||
| 152 | // Agent handlers | ||
| 153 | ipcMain.handle( | ||
| 154 | IPC_CHANNELS.agentSpawn, | ||
| 155 | withParsedPayload(agentConfigSchema, async (_event, config) => { | ||
| 156 | return processManager.spawn(config); | ||
| 157 | }), | ||
| 158 | ); | ||
| 159 | |||
| 160 | ipcMain.handle( | ||
| 161 | IPC_CHANNELS.agentKill, | ||
| 162 | withParsedPayload(agentSessionIdSchema, async (_event, sessionId) => { | ||
| 163 | processManager.kill(sessionId); | ||
| 164 | }), | ||
| 165 | ); | ||
| 166 | |||
| 167 | ipcMain.handle( | ||
| 168 | IPC_CHANNELS.agentWrite, | ||
| 169 | withParsedArgs(agentWriteArgsParser, async (_event, sessionId, data) => { | ||
| 170 | processManager.write(sessionId, data); | ||
| 171 | }), | ||
| 172 | ); | ||
| 173 | |||
| 174 | // Provider handlers | ||
| 175 | ipcMain.handle( | ||
| 176 | IPC_CHANNELS.providerSessionStart, | ||
| 177 | withParsedPayload(providerSessionStartInputSchema, async (_event, payload) => { | ||
| 178 | return providerManager.startSession(payload); | ||
| 179 | }), | ||
| 180 | ); | ||
| 181 | |||
| 182 | ipcMain.handle( | ||
| 183 | IPC_CHANNELS.providerTurnStart, | ||
| 184 | withParsedPayload(providerSendTurnInputSchema, async (_event, payload) => { | ||
| 185 | return providerManager.sendTurn(payload); | ||
| 186 | }), | ||
| 187 | ); | ||
| 188 | |||
| 189 | ipcMain.handle( | ||
| 190 | IPC_CHANNELS.providerTurnInterrupt, | ||
| 191 | withParsedPayload(providerInterruptTurnInputSchema, async (_event, payload) => { | ||
| 192 | await providerManager.interruptTurn(payload); | ||
| 193 | }), | ||
| 194 | ); | ||
| 195 | |||
| 196 | ipcMain.handle( | ||
| 197 | IPC_CHANNELS.providerRequestRespond, | ||
| 198 | withParsedPayload( | ||
| 199 | providerRespondToRequestInputSchema, | ||
| 200 | async (_event, payload) => { | ||
| 201 | await providerManager.respondToRequest(payload); | ||
| 202 | }, | ||
| 203 | ), | ||
| 204 | ); | ||
| 205 | |||
| 206 | ipcMain.handle( | ||
| 207 | IPC_CHANNELS.providerSessionStop, | ||
| 208 | withParsedPayload(providerStopSessionInputSchema, async (_event, payload) => { | ||
| 209 | providerManager.stopSession(payload); | ||
| 210 | }), | ||
| 211 | ); | ||
| 212 | |||
| 213 | ipcMain.handle(IPC_CHANNELS.providerSessionList, async () => { | ||
| 214 | return providerManager.listSessions(); | ||
| 215 | }); | 160 | }); |
| 216 | } | ||
| 217 | |||
| 218 | async function runTerminalCommand(input: TerminalCommandInput): Promise<TerminalCommandResult> { | ||
| 219 | const shellPath = | ||
| 220 | process.platform === "win32" | ||
| 221 | ? (process.env.ComSpec ?? "cmd.exe") | ||
| 222 | : (process.env.SHELL ?? "/bin/sh"); | ||
| 223 | |||
| 224 | const args = | ||
| 225 | process.platform === "win32" ? ["/d", "/s", "/c", input.command] : ["-lc", input.command]; | ||
| 226 | |||
| 227 | return new Promise((resolve, reject) => { | ||
| 228 | const child = spawn(shellPath, args, { | ||
| 229 | cwd: input.cwd, | ||
| 230 | env: process.env, | ||
| 231 | stdio: ["ignore", "pipe", "pipe"], | ||
| 232 | }); | ||
| 233 | |||
| 234 | let stdout = ""; | ||
| 235 | let stderr = ""; | ||
| 236 | let timedOut = false; | ||
| 237 | |||
| 238 | const timeout = setTimeout(() => { | ||
| 239 | timedOut = true; | ||
| 240 | child.kill("SIGTERM"); | ||
| 241 | setTimeout(() => { | ||
| 242 | if (!child.killed) { | ||
| 243 | child.kill("SIGKILL"); | ||
| 244 | } | ||
| 245 | }, 1_000).unref(); | ||
| 246 | }, input.timeoutMs ?? 30_000); | ||
| 247 | |||
| 248 | child.stdout?.on("data", (chunk: Buffer) => { | ||
| 249 | stdout += chunk.toString(); | ||
| 250 | }); | ||
| 251 | |||
| 252 | child.stderr?.on("data", (chunk: Buffer) => { | ||
| 253 | stderr += chunk.toString(); | ||
| 254 | }); | ||
| 255 | |||
| 256 | child.on("error", (error) => { | ||
| 257 | clearTimeout(timeout); | ||
| 258 | reject(error); | ||
| 259 | }); | ||
| 260 | 161 | ||
| 261 | child.on("close", (code, signal) => { | 162 | window.webContents.setWindowOpenHandler(() => ({ action: "deny" })); |
| 262 | clearTimeout(timeout); | 163 | window.once("ready-to-show", () => { |
| 263 | resolve({ | 164 | window.show(); |
| 264 | stdout, | ||
| 265 | stderr, | ||
| 266 | code: code ?? null, | ||
| 267 | signal: signal ?? null, | ||
| 268 | timedOut, | ||
| 269 | }); | ||
| 270 | }); | ||
| 271 | }); | 165 | }); |
| 272 | } | ||
| 273 | |||
| 274 | function setupEventForwarding(window: BrowserWindow): void { | ||
| 275 | const onOutput = (chunk: unknown) => { | ||
| 276 | if (!window.isDestroyed()) { | ||
| 277 | window.webContents.send(IPC_CHANNELS.agentOutput, chunk); | ||
| 278 | } | ||
| 279 | }; | ||
| 280 | 166 | ||
| 281 | const onExit = (exit: unknown) => { | 167 | if (isDevelopment) { |
| 282 | if (!window.isDestroyed()) { | 168 | void window.loadURL(process.env.VITE_DEV_SERVER_URL as string); |
| 283 | window.webContents.send(IPC_CHANNELS.agentExit, exit); | 169 | } else { |
| 284 | } | 170 | if (!fs.existsSync(RENDERER_ENTRY)) { |
| 285 | }; | 171 | throw new Error(`Renderer bundle missing at ${RENDERER_ENTRY}`); |
| 286 | |||
| 287 | const onProviderEvent = (event: unknown) => { | ||
| 288 | if (!window.isDestroyed()) { | ||
| 289 | window.webContents.send(IPC_CHANNELS.providerEvent, event); | ||
| 290 | } | 172 | } |
| 291 | }; | 173 | void window.loadFile(RENDERER_ENTRY); |
| 292 | 174 | } | |
| 293 | processManager.on("output", onOutput); | ||
| 294 | processManager.on("exit", onExit); | ||
| 295 | providerManager.on("event", onProviderEvent); | ||
| 296 | 175 | ||
| 297 | window.on("closed", () => { | 176 | window.on("closed", () => { |
| 298 | processManager.off("output", onOutput); | 177 | if (mainWindow === window) { |
| 299 | processManager.off("exit", onExit); | 178 | mainWindow = null; |
| 300 | providerManager.off("event", onProviderEvent); | 179 | } |
| 301 | }); | 180 | }); |
| 302 | } | ||
| 303 | |||
| 304 | function setupCSP(): void { | ||
| 305 | session.defaultSession.webRequest.onHeadersReceived((details, callback) => { | ||
| 306 | const csp = isDevelopment | ||
| 307 | ? "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://localhost:* http://localhost:*" | ||
| 308 | : "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"; | ||
| 309 | 181 | ||
| 310 | callback({ | 182 | return window; |
| 311 | responseHeaders: { | ||
| 312 | ...details.responseHeaders, | ||
| 313 | "Content-Security-Policy": [csp], | ||
| 314 | }, | ||
| 315 | }); | ||
| 316 | }); | ||
| 317 | } | 183 | } |
| 318 | 184 | ||
| 319 | async function bootstrap(): Promise<void> { | 185 | async function bootstrap(): Promise<void> { |
| 320 | setupCSP(); | 186 | backendPort = await reserveLoopbackPort(); |
| 321 | 187 | backendAuthToken = randomBytes(24).toString("hex"); | |
| 322 | todoStore = new TodoStore(path.join(app.getPath("userData"), "todos.json")); | 188 | backendWsUrl = `ws://127.0.0.1:${backendPort}/?token=${encodeURIComponent(backendAuthToken)}`; |
| 323 | await todoStore.init(); | 189 | process.env.CODETHING_DESKTOP_WS_URL = backendWsUrl; |
| 324 | 190 | ||
| 325 | registerIpcHandlers(); | 191 | registerIpcHandlers(); |
| 326 | createWindow(); | 192 | startBackend(); |
| 327 | 193 | mainWindow = createWindow(); | |
| 328 | app.on("activate", () => { | ||
| 329 | if (BrowserWindow.getAllWindows().length === 0) { | ||
| 330 | createWindow(); | ||
| 331 | } | ||
| 332 | }); | ||
| 333 | } | 194 | } |
| 334 | 195 | ||
| 335 | app.on("before-quit", () => { | 196 | app.on("before-quit", () => { |
| 336 | processManager.killAll(); | 197 | isQuitting = true; |
| 337 | providerManager.stopAll(); | 198 | stopBackend(); |
| 338 | providerManager.dispose(); | ||
| 339 | }); | 199 | }); |
| 340 | 200 | ||
| 341 | app.whenReady().then(() => { | 201 | app.whenReady().then(() => { |
| 342 | void bootstrap(); | 202 | void bootstrap(); |
| 203 | |||
| 204 | app.on("activate", () => { | ||
| 205 | if (BrowserWindow.getAllWindows().length === 0) { | ||
| 206 | mainWindow = createWindow(); | ||
| 207 | } | ||
| 208 | }); | ||
| 343 | }); | 209 | }); |
| 344 | 210 | ||
| 345 | app.on("window-all-closed", () => { | 211 | app.on("window-all-closed", () => { |
apps/desktop/src/preload.ts+6-52| ... | @@ -1,55 +1,9 @@ | ... | @@ -1,55 +1,9 @@ |
| 1 | import { contextBridge, ipcRenderer } from "electron"; | 1 | import { contextBridge, ipcRenderer } from "electron"; |
| 2 | 2 | ||
| 3 | import { IPC_CHANNELS, type NativeApi } from "@acme/contracts"; | 3 | const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; |
| 4 | const wsUrl = process.env.CODETHING_DESKTOP_WS_URL ?? null; | ||
| 4 | 5 | ||
| 5 | const nativeApi: NativeApi = { | 6 | contextBridge.exposeInMainWorld("desktopBridge", { |
| 6 | todos: { | 7 | getWsUrl: () => wsUrl, |
| 7 | list: () => ipcRenderer.invoke(IPC_CHANNELS.todosList), | 8 | pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL) as Promise<string | null>, |
| 8 | add: (input) => ipcRenderer.invoke(IPC_CHANNELS.todosAdd, input), | 9 | }); |
| 9 | toggle: (id) => ipcRenderer.invoke(IPC_CHANNELS.todosToggle, id), | ||
| 10 | remove: (id) => ipcRenderer.invoke(IPC_CHANNELS.todosRemove, id), | ||
| 11 | }, | ||
| 12 | dialogs: { | ||
| 13 | pickFolder: () => ipcRenderer.invoke(IPC_CHANNELS.dialogPickFolder), | ||
| 14 | }, | ||
| 15 | terminal: { | ||
| 16 | run: (input) => ipcRenderer.invoke(IPC_CHANNELS.terminalRun, input), | ||
| 17 | }, | ||
| 18 | agent: { | ||
| 19 | spawn: (config) => ipcRenderer.invoke(IPC_CHANNELS.agentSpawn, config), | ||
| 20 | kill: (sessionId) => ipcRenderer.invoke(IPC_CHANNELS.agentKill, sessionId), | ||
| 21 | write: (sessionId, data) => ipcRenderer.invoke(IPC_CHANNELS.agentWrite, sessionId, data), | ||
| 22 | onOutput: (callback) => { | ||
| 23 | const listener = (_event: Electron.IpcRendererEvent, chunk: unknown) => | ||
| 24 | callback(chunk as Parameters<typeof callback>[0]); | ||
| 25 | ipcRenderer.on(IPC_CHANNELS.agentOutput, listener); | ||
| 26 | return () => ipcRenderer.removeListener(IPC_CHANNELS.agentOutput, listener); | ||
| 27 | }, | ||
| 28 | onExit: (callback) => { | ||
| 29 | const listener = (_event: Electron.IpcRendererEvent, exit: unknown) => | ||
| 30 | callback(exit as Parameters<typeof callback>[0]); | ||
| 31 | ipcRenderer.on(IPC_CHANNELS.agentExit, listener); | ||
| 32 | return () => ipcRenderer.removeListener(IPC_CHANNELS.agentExit, listener); | ||
| 33 | }, | ||
| 34 | }, | ||
| 35 | providers: { | ||
| 36 | startSession: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerSessionStart, input), | ||
| 37 | sendTurn: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerTurnStart, input), | ||
| 38 | interruptTurn: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerTurnInterrupt, input), | ||
| 39 | respondToRequest: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerRequestRespond, input), | ||
| 40 | stopSession: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerSessionStop, input), | ||
| 41 | listSessions: () => ipcRenderer.invoke(IPC_CHANNELS.providerSessionList), | ||
| 42 | onEvent: (callback) => { | ||
| 43 | const listener = (_event: Electron.IpcRendererEvent, payload: unknown) => | ||
| 44 | callback(payload as Parameters<typeof callback>[0]); | ||
| 45 | ipcRenderer.on(IPC_CHANNELS.providerEvent, listener); | ||
| 46 | return () => ipcRenderer.removeListener(IPC_CHANNELS.providerEvent, listener); | ||
| 47 | }, | ||
| 48 | }, | ||
| 49 | shell: { | ||
| 50 | openInEditor: (cwd: string, editor: string) => | ||
| 51 | ipcRenderer.invoke(IPC_CHANNELS.shellOpenInEditor, cwd, editor), | ||
| 52 | }, | ||
| 53 | }; | ||
| 54 | |||
| 55 | contextBridge.exposeInMainWorld("nativeApi", nativeApi); |
apps/desktop/src/processManager.test.ts deleted-48| ... | @@ -1,48 +0,0 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { ProcessManager } from "./processManager"; | ||
| 4 | |||
| 5 | describe("ProcessManager", () => { | ||
| 6 | it("can be instantiated", () => { | ||
| 7 | const pm = new ProcessManager(); | ||
| 8 | expect(pm).toBeInstanceOf(ProcessManager); | ||
| 9 | }); | ||
| 10 | |||
| 11 | it("spawns a process and receives output", async () => { | ||
| 12 | const pm = new ProcessManager(); | ||
| 13 | const chunks: string[] = []; | ||
| 14 | |||
| 15 | const done = new Promise<void>((resolve) => { | ||
| 16 | pm.on("output", (chunk) => { | ||
| 17 | chunks.push(chunk.data); | ||
| 18 | }); | ||
| 19 | pm.on("exit", () => { | ||
| 20 | resolve(); | ||
| 21 | }); | ||
| 22 | }); | ||
| 23 | |||
| 24 | const sessionId = pm.spawn({ command: "echo", args: ["hello"] }); | ||
| 25 | expect(typeof sessionId).toBe("string"); | ||
| 26 | expect(sessionId.length).toBeGreaterThan(0); | ||
| 27 | |||
| 28 | await done; | ||
| 29 | |||
| 30 | expect(chunks.join("").trim()).toBe("hello"); | ||
| 31 | }); | ||
| 32 | |||
| 33 | it("can kill a spawned process", async () => { | ||
| 34 | const pm = new ProcessManager(); | ||
| 35 | |||
| 36 | const exited = new Promise<number | null>((resolve) => { | ||
| 37 | pm.on("exit", (exit) => { | ||
| 38 | resolve(exit.code); | ||
| 39 | }); | ||
| 40 | }); | ||
| 41 | |||
| 42 | const sessionId = pm.spawn({ command: "sleep", args: ["10"] }); | ||
| 43 | pm.kill(sessionId); | ||
| 44 | |||
| 45 | const code = await exited; | ||
| 46 | expect(code).not.toBe(0); | ||
| 47 | }); | ||
| 48 | }); | ||
apps/desktop/src/processManager.ts deleted-144| ... | @@ -1,144 +0,0 @@ | ||
| 1 | import { type ChildProcess, spawn } from "node:child_process"; | ||
| 2 | import { randomUUID } from "node:crypto"; | ||
| 3 | import { EventEmitter } from "node:events"; | ||
| 4 | |||
| 5 | import { | ||
| 6 | type AgentConfig, | ||
| 7 | type AgentExit, | ||
| 8 | type OutputChunk, | ||
| 9 | agentConfigSchema, | ||
| 10 | } from "@acme/contracts"; | ||
| 11 | import type { IPty } from "node-pty"; | ||
| 12 | |||
| 13 | export interface ProcessManagerEvents { | ||
| 14 | output: [chunk: OutputChunk]; | ||
| 15 | exit: [exit: AgentExit]; | ||
| 16 | } | ||
| 17 | |||
| 18 | export class ProcessManager extends EventEmitter<ProcessManagerEvents> { | ||
| 19 | private sessions = new Map<string, ChildProcess>(); | ||
| 20 | private ptySessions = new Map<string, IPty>(); | ||
| 21 | |||
| 22 | spawn(raw: AgentConfig): string { | ||
| 23 | const config = agentConfigSchema.parse(raw); | ||
| 24 | const sessionId = randomUUID(); | ||
| 25 | |||
| 26 | if (config.usePty) { | ||
| 27 | return this.spawnPty(sessionId, config); | ||
| 28 | } | ||
| 29 | |||
| 30 | return this.spawnProcess(sessionId, config); | ||
| 31 | } | ||
| 32 | |||
| 33 | private spawnProcess(sessionId: string, config: AgentConfig): string { | ||
| 34 | const child = spawn(config.command, config.args, { | ||
| 35 | cwd: config.cwd, | ||
| 36 | env: config.env ? { ...process.env, ...config.env } : process.env, | ||
| 37 | stdio: ["pipe", "pipe", "pipe"], | ||
| 38 | }); | ||
| 39 | |||
| 40 | this.sessions.set(sessionId, child); | ||
| 41 | |||
| 42 | child.stdout?.on("data", (data: Buffer) => { | ||
| 43 | this.emit("output", { | ||
| 44 | sessionId, | ||
| 45 | stream: "stdout", | ||
| 46 | data: data.toString(), | ||
| 47 | }); | ||
| 48 | }); | ||
| 49 | |||
| 50 | child.stderr?.on("data", (data: Buffer) => { | ||
| 51 | this.emit("output", { | ||
| 52 | sessionId, | ||
| 53 | stream: "stderr", | ||
| 54 | data: data.toString(), | ||
| 55 | }); | ||
| 56 | }); | ||
| 57 | |||
| 58 | child.on("exit", (code, signal) => { | ||
| 59 | this.sessions.delete(sessionId); | ||
| 60 | this.emit("exit", { | ||
| 61 | sessionId, | ||
| 62 | code: code ?? null, | ||
| 63 | signal: signal ?? null, | ||
| 64 | }); | ||
| 65 | }); | ||
| 66 | |||
| 67 | return sessionId; | ||
| 68 | } | ||
| 69 | |||
| 70 | private spawnPty(sessionId: string, config: AgentConfig): string { | ||
| 71 | const pty = require("node-pty") as typeof import("node-pty"); | ||
| 72 | |||
| 73 | const ptyProcess = pty.spawn(config.command, config.args, { | ||
| 74 | name: "xterm-256color", | ||
| 75 | cols: 120, | ||
| 76 | rows: 30, | ||
| 77 | cwd: config.cwd ?? process.cwd(), | ||
| 78 | env: (config.env ? { ...process.env, ...config.env } : process.env) as Record<string, string>, | ||
| 79 | }); | ||
| 80 | |||
| 81 | this.ptySessions.set(sessionId, ptyProcess); | ||
| 82 | |||
| 83 | ptyProcess.onData((data) => { | ||
| 84 | this.emit("output", { | ||
| 85 | sessionId, | ||
| 86 | stream: "stdout", | ||
| 87 | data, | ||
| 88 | }); | ||
| 89 | }); | ||
| 90 | |||
| 91 | ptyProcess.onExit(({ exitCode, signal }) => { | ||
| 92 | this.ptySessions.delete(sessionId); | ||
| 93 | this.emit("exit", { | ||
| 94 | sessionId, | ||
| 95 | code: exitCode, | ||
| 96 | signal: signal !== undefined ? String(signal) : null, | ||
| 97 | }); | ||
| 98 | }); | ||
| 99 | |||
| 100 | return sessionId; | ||
| 101 | } | ||
| 102 | |||
| 103 | write(sessionId: string, data: string): void { | ||
| 104 | const child = this.sessions.get(sessionId); | ||
| 105 | if (child) { | ||
| 106 | child.stdin?.write(data); | ||
| 107 | return; | ||
| 108 | } | ||
| 109 | |||
| 110 | const pty = this.ptySessions.get(sessionId); | ||
| 111 | if (pty) { | ||
| 112 | pty.write(data); | ||
| 113 | return; | ||
| 114 | } | ||
| 115 | |||
| 116 | throw new Error(`No session: ${sessionId}`); | ||
| 117 | } | ||
| 118 | |||
| 119 | kill(sessionId: string): void { | ||
| 120 | const child = this.sessions.get(sessionId); | ||
| 121 | if (child) { | ||
| 122 | child.kill(); | ||
| 123 | return; | ||
| 124 | } | ||
| 125 | |||
| 126 | const pty = this.ptySessions.get(sessionId); | ||
| 127 | if (pty) { | ||
| 128 | pty.kill(); | ||
| 129 | return; | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | killAll(): void { | ||
| 134 | for (const child of this.sessions.values()) { | ||
| 135 | child.kill(); | ||
| 136 | } | ||
| 137 | this.sessions.clear(); | ||
| 138 | |||
| 139 | for (const pty of this.ptySessions.values()) { | ||
| 140 | pty.kill(); | ||
| 141 | } | ||
| 142 | this.ptySessions.clear(); | ||
| 143 | } | ||
| 144 | } | ||
apps/desktop/src/providerManager.test.ts deleted-65| ... | @@ -1,65 +0,0 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { ProviderManager } from "./providerManager"; | ||
| 4 | |||
| 5 | describe("ProviderManager", () => { | ||
| 6 | it("detaches provider event listener and ends thread log streams on dispose", () => { | ||
| 7 | const manager = new ProviderManager(); | ||
| 8 | const internals = manager as unknown as { | ||
| 9 | codex: { listenerCount: (event: string) => number }; | ||
| 10 | onCodexEvent: (event: { | ||
| 11 | id: string; | ||
| 12 | kind: "notification"; | ||
| 13 | provider: "codex"; | ||
| 14 | sessionId: string; | ||
| 15 | createdAt: string; | ||
| 16 | method: string; | ||
| 17 | threadId: string; | ||
| 18 | }) => void; | ||
| 19 | threadLogStreams: Map< | ||
| 20 | string, | ||
| 21 | { writableEnded: boolean; destroyed: boolean } | ||
| 22 | >; | ||
| 23 | }; | ||
| 24 | internals.onCodexEvent({ | ||
| 25 | id: "evt-1", | ||
| 26 | kind: "notification", | ||
| 27 | provider: "codex", | ||
| 28 | sessionId: "sess-1", | ||
| 29 | createdAt: new Date().toISOString(), | ||
| 30 | method: "thread/started", | ||
| 31 | threadId: "thread-1", | ||
| 32 | }); | ||
| 33 | const stream = internals.threadLogStreams.get("thread-1"); | ||
| 34 | expect(stream).toBeDefined(); | ||
| 35 | if (!stream) return; | ||
| 36 | |||
| 37 | expect(internals.codex.listenerCount("event")).toBe(1); | ||
| 38 | manager.dispose(); | ||
| 39 | |||
| 40 | expect(internals.codex.listenerCount("event")).toBe(0); | ||
| 41 | expect(stream.writableEnded || stream.destroyed).toBe(true); | ||
| 42 | expect(internals.threadLogStreams.size).toBe(0); | ||
| 43 | }); | ||
| 44 | |||
| 45 | it("allows multiple dispose calls", () => { | ||
| 46 | const manager = new ProviderManager(); | ||
| 47 | |||
| 48 | manager.dispose(); | ||
| 49 | expect(() => manager.dispose()).not.toThrow(); | ||
| 50 | }); | ||
| 51 | |||
| 52 | it("rejects request responses for unknown sessions", async () => { | ||
| 53 | const manager = new ProviderManager(); | ||
| 54 | |||
| 55 | await expect( | ||
| 56 | manager.respondToRequest({ | ||
| 57 | sessionId: "missing-session", | ||
| 58 | requestId: "req-1", | ||
| 59 | decision: "accept", | ||
| 60 | }), | ||
| 61 | ).rejects.toThrow("Unknown provider session: missing-session"); | ||
| 62 | |||
| 63 | manager.dispose(); | ||
| 64 | }); | ||
| 65 | }); | ||
apps/desktop/src/providerManager.ts deleted-209| ... | @@ -1,209 +0,0 @@ | ||
| 1 | import { EventEmitter } from "node:events"; | ||
| 2 | import fs from "node:fs"; | ||
| 3 | import path from "node:path"; | ||
| 4 | |||
| 5 | import { | ||
| 6 | type ProviderEvent, | ||
| 7 | type ProviderInterruptTurnInput, | ||
| 8 | type ProviderRespondToRequestInput, | ||
| 9 | type ProviderSendTurnInput, | ||
| 10 | type ProviderSession, | ||
| 11 | type ProviderSessionStartInput, | ||
| 12 | type ProviderStopSessionInput, | ||
| 13 | type ProviderTurnStartResult, | ||
| 14 | providerInterruptTurnInputSchema, | ||
| 15 | providerRespondToRequestInputSchema, | ||
| 16 | providerSendTurnInputSchema, | ||
| 17 | providerSessionStartInputSchema, | ||
| 18 | providerStopSessionInputSchema, | ||
| 19 | } from "@acme/contracts"; | ||
| 20 | import { CodexAppServerManager } from "./codexAppServerManager"; | ||
| 21 | |||
| 22 | export interface ProviderManagerEvents { | ||
| 23 | event: [event: ProviderEvent]; | ||
| 24 | } | ||
| 25 | |||
| 26 | export class ProviderManager extends EventEmitter<ProviderManagerEvents> { | ||
| 27 | private readonly codex = new CodexAppServerManager(); | ||
| 28 | private readonly threadLogsDir: string; | ||
| 29 | private readonly threadLogStreams = new Map<string, fs.WriteStream>(); | ||
| 30 | private readonly sessionThreadIds = new Map<string, string>(); | ||
| 31 | private readonly pendingEventsBySession = new Map<string, ProviderEvent[]>(); | ||
| 32 | private disposed = false; | ||
| 33 | private readonly onCodexEvent = (event: ProviderEvent) => { | ||
| 34 | if (this.disposed) { | ||
| 35 | return; | ||
| 36 | } | ||
| 37 | |||
| 38 | this.routeEventToThreadLog(event); | ||
| 39 | this.emit("event", event); | ||
| 40 | }; | ||
| 41 | |||
| 42 | constructor() { | ||
| 43 | super(); | ||
| 44 | |||
| 45 | const logsDir = path.resolve(process.cwd(), ".logs"); | ||
| 46 | this.threadLogsDir = path.join(logsDir, "threads"); | ||
| 47 | fs.mkdirSync(this.threadLogsDir, { recursive: true }); | ||
| 48 | |||
| 49 | this.codex.on("event", this.onCodexEvent); | ||
| 50 | } | ||
| 51 | |||
| 52 | async startSession(raw: ProviderSessionStartInput): Promise<ProviderSession> { | ||
| 53 | const input = providerSessionStartInputSchema.parse(raw); | ||
| 54 | if (input.provider !== "codex") { | ||
| 55 | throw new Error(`Provider '${input.provider}' is not implemented yet.`); | ||
| 56 | } | ||
| 57 | |||
| 58 | const session = await this.codex.startSession(input); | ||
| 59 | if (session.threadId) { | ||
| 60 | this.sessionThreadIds.set(session.sessionId, session.threadId); | ||
| 61 | this.flushPendingSessionEvents(session.sessionId, session.threadId); | ||
| 62 | } | ||
| 63 | return session; | ||
| 64 | } | ||
| 65 | |||
| 66 | async sendTurn(raw: ProviderSendTurnInput): Promise<ProviderTurnStartResult> { | ||
| 67 | const input = providerSendTurnInputSchema.parse(raw); | ||
| 68 | if (!this.codex.hasSession(input.sessionId)) { | ||
| 69 | throw new Error(`Unknown provider session: ${input.sessionId}`); | ||
| 70 | } | ||
| 71 | |||
| 72 | return this.codex.sendTurn(input); | ||
| 73 | } | ||
| 74 | |||
| 75 | async interruptTurn(raw: ProviderInterruptTurnInput): Promise<void> { | ||
| 76 | const input = providerInterruptTurnInputSchema.parse(raw); | ||
| 77 | if (!this.codex.hasSession(input.sessionId)) { | ||
| 78 | throw new Error(`Unknown provider session: ${input.sessionId}`); | ||
| 79 | } | ||
| 80 | |||
| 81 | await this.codex.interruptTurn(input.sessionId, input.turnId); | ||
| 82 | } | ||
| 83 | |||
| 84 | async respondToRequest(raw: ProviderRespondToRequestInput): Promise<void> { | ||
| 85 | const input = providerRespondToRequestInputSchema.parse(raw); | ||
| 86 | if (!this.codex.hasSession(input.sessionId)) { | ||
| 87 | throw new Error(`Unknown provider session: ${input.sessionId}`); | ||
| 88 | } | ||
| 89 | |||
| 90 | await this.codex.respondToRequest( | ||
| 91 | input.sessionId, | ||
| 92 | input.requestId, | ||
| 93 | input.decision, | ||
| 94 | ); | ||
| 95 | } | ||
| 96 | |||
| 97 | stopSession(raw: ProviderStopSessionInput): void { | ||
| 98 | const input = providerStopSessionInputSchema.parse(raw); | ||
| 99 | this.codex.stopSession(input.sessionId); | ||
| 100 | this.sessionThreadIds.delete(input.sessionId); | ||
| 101 | this.pendingEventsBySession.delete(input.sessionId); | ||
| 102 | } | ||
| 103 | |||
| 104 | listSessions(): ProviderSession[] { | ||
| 105 | return this.codex.listSessions(); | ||
| 106 | } | ||
| 107 | |||
| 108 | stopAll(): void { | ||
| 109 | this.codex.stopAll(); | ||
| 110 | } | ||
| 111 | |||
| 112 | dispose(): void { | ||
| 113 | if (this.disposed) { | ||
| 114 | return; | ||
| 115 | } | ||
| 116 | |||
| 117 | this.disposed = true; | ||
| 118 | this.codex.off("event", this.onCodexEvent); | ||
| 119 | for (const stream of this.threadLogStreams.values()) { | ||
| 120 | stream.end(); | ||
| 121 | } | ||
| 122 | this.threadLogStreams.clear(); | ||
| 123 | this.sessionThreadIds.clear(); | ||
| 124 | this.pendingEventsBySession.clear(); | ||
| 125 | } | ||
| 126 | |||
| 127 | private routeEventToThreadLog(event: ProviderEvent): void { | ||
| 128 | const threadId = this.resolveThreadId(event); | ||
| 129 | if (!threadId) { | ||
| 130 | const pending = this.pendingEventsBySession.get(event.sessionId) ?? []; | ||
| 131 | pending.push(event); | ||
| 132 | this.pendingEventsBySession.set(event.sessionId, pending.slice(-100)); | ||
| 133 | return; | ||
| 134 | } | ||
| 135 | |||
| 136 | this.flushPendingSessionEvents(event.sessionId, threadId); | ||
| 137 | this.writeThreadEvent(threadId, event); | ||
| 138 | } | ||
| 139 | |||
| 140 | private flushPendingSessionEvents(sessionId: string, threadId: string): void { | ||
| 141 | const pending = this.pendingEventsBySession.get(sessionId); | ||
| 142 | if (!pending || pending.length === 0) { | ||
| 143 | return; | ||
| 144 | } | ||
| 145 | |||
| 146 | for (const event of pending) { | ||
| 147 | this.writeThreadEvent(threadId, event); | ||
| 148 | } | ||
| 149 | this.pendingEventsBySession.delete(sessionId); | ||
| 150 | } | ||
| 151 | |||
| 152 | private resolveThreadId(event: ProviderEvent): string | undefined { | ||
| 153 | const fromPayload = this.readThreadIdFromPayload(event.payload); | ||
| 154 | const threadId = | ||
| 155 | event.threadId ?? | ||
| 156 | fromPayload ?? | ||
| 157 | this.sessionThreadIds.get(event.sessionId); | ||
| 158 | |||
| 159 | if (threadId) { | ||
| 160 | this.sessionThreadIds.set(event.sessionId, threadId); | ||
| 161 | } | ||
| 162 | |||
| 163 | return threadId; | ||
| 164 | } | ||
| 165 | |||
| 166 | private readThreadIdFromPayload(payload: unknown): string | undefined { | ||
| 167 | if (!payload || typeof payload !== "object") { | ||
| 168 | return undefined; | ||
| 169 | } | ||
| 170 | |||
| 171 | const record = payload as Record<string, unknown>; | ||
| 172 | const direct = record.threadId; | ||
| 173 | if (typeof direct === "string" && direct.length > 0) { | ||
| 174 | return direct; | ||
| 175 | } | ||
| 176 | |||
| 177 | const thread = record.thread; | ||
| 178 | if (!thread || typeof thread !== "object") { | ||
| 179 | return undefined; | ||
| 180 | } | ||
| 181 | |||
| 182 | const nested = (thread as Record<string, unknown>).id; | ||
| 183 | if (typeof nested === "string" && nested.length > 0) { | ||
| 184 | return nested; | ||
| 185 | } | ||
| 186 | |||
| 187 | return undefined; | ||
| 188 | } | ||
| 189 | |||
| 190 | private writeThreadEvent(threadId: string, event: ProviderEvent): void { | ||
| 191 | const stream = this.getOrCreateThreadLogStream(threadId); | ||
| 192 | stream.write(`${JSON.stringify(event)}\n`); | ||
| 193 | } | ||
| 194 | |||
| 195 | private getOrCreateThreadLogStream(threadId: string): fs.WriteStream { | ||
| 196 | const cached = this.threadLogStreams.get(threadId); | ||
| 197 | if (cached) { | ||
| 198 | return cached; | ||
| 199 | } | ||
| 200 | |||
| 201 | const safeThreadId = threadId.replace(/[^a-zA-Z0-9._-]/g, "_"); | ||
| 202 | const stream = fs.createWriteStream( | ||
| 203 | path.join(this.threadLogsDir, `${safeThreadId}.events.ndjson`), | ||
| 204 | { flags: "a" }, | ||
| 205 | ); | ||
| 206 | this.threadLogStreams.set(threadId, stream); | ||
| 207 | return stream; | ||
| 208 | } | ||
| 209 | } | ||
apps/desktop/src/todoStore.ts deleted-106| ... | @@ -1,106 +0,0 @@ | ||
| 1 | import { randomUUID } from "node:crypto"; | ||
| 2 | import { promises as fs } from "node:fs"; | ||
| 3 | import path from "node:path"; | ||
| 4 | |||
| 5 | import { | ||
| 6 | type NewTodoInput, | ||
| 7 | type Todo, | ||
| 8 | newTodoInputSchema, | ||
| 9 | todoIdSchema, | ||
| 10 | todoListSchema, | ||
| 11 | } from "@acme/contracts"; | ||
| 12 | |||
| 13 | export class TodoStore { | ||
| 14 | private todos: Todo[] = []; | ||
| 15 | private queue: Promise<void> = Promise.resolve(); | ||
| 16 | |||
| 17 | public constructor(private readonly filePath: string) {} | ||
| 18 | |||
| 19 | public async init(): Promise<void> { | ||
| 20 | return this.runExclusive(async () => { | ||
| 21 | await fs.mkdir(path.dirname(this.filePath), { recursive: true }); | ||
| 22 | |||
| 23 | try { | ||
| 24 | const raw = await fs.readFile(this.filePath, "utf8"); | ||
| 25 | const parsed = todoListSchema.safeParse(JSON.parse(raw)); | ||
| 26 | |||
| 27 | if (parsed.success) { | ||
| 28 | this.todos = parsed.data; | ||
| 29 | return; | ||
| 30 | } | ||
| 31 | } catch (error) { | ||
| 32 | if (!isNotFoundError(error)) { | ||
| 33 | throw error; | ||
| 34 | } | ||
| 35 | } | ||
| 36 | |||
| 37 | this.todos = []; | ||
| 38 | await this.persist(); | ||
| 39 | }); | ||
| 40 | } | ||
| 41 | |||
| 42 | public async list(): Promise<Todo[]> { | ||
| 43 | return this.runExclusive(async () => [...this.todos]); | ||
| 44 | } | ||
| 45 | |||
| 46 | public async add(input: NewTodoInput): Promise<Todo[]> { | ||
| 47 | return this.runExclusive(async () => { | ||
| 48 | const { title } = newTodoInputSchema.parse(input); | ||
| 49 | const todo: Todo = { | ||
| 50 | id: randomUUID(), | ||
| 51 | title, | ||
| 52 | completed: false, | ||
| 53 | createdAt: new Date().toISOString(), | ||
| 54 | }; | ||
| 55 | |||
| 56 | this.todos = [todo, ...this.todos]; | ||
| 57 | await this.persist(); | ||
| 58 | return [...this.todos]; | ||
| 59 | }); | ||
| 60 | } | ||
| 61 | |||
| 62 | public async toggle(id: string): Promise<Todo[]> { | ||
| 63 | return this.runExclusive(async () => { | ||
| 64 | const parsedId = todoIdSchema.parse(id); | ||
| 65 | this.todos = this.todos.map((todo) => | ||
| 66 | todo.id === parsedId ? { ...todo, completed: !todo.completed } : todo, | ||
| 67 | ); | ||
| 68 | |||
| 69 | await this.persist(); | ||
| 70 | return [...this.todos]; | ||
| 71 | }); | ||
| 72 | } | ||
| 73 | |||
| 74 | public async remove(id: string): Promise<Todo[]> { | ||
| 75 | return this.runExclusive(async () => { | ||
| 76 | const parsedId = todoIdSchema.parse(id); | ||
| 77 | this.todos = this.todos.filter((todo) => todo.id !== parsedId); | ||
| 78 | |||
| 79 | await this.persist(); | ||
| 80 | return [...this.todos]; | ||
| 81 | }); | ||
| 82 | } | ||
| 83 | |||
| 84 | private async persist(): Promise<void> { | ||
| 85 | await fs.writeFile(this.filePath, JSON.stringify(this.todos, null, 2), "utf8"); | ||
| 86 | } | ||
| 87 | |||
| 88 | private async runExclusive<T>(operation: () => Promise<T>): Promise<T> { | ||
| 89 | const next = this.queue.then(operation, operation); | ||
| 90 | this.queue = next.then( | ||
| 91 | () => undefined, | ||
| 92 | () => undefined, | ||
| 93 | ); | ||
| 94 | |||
| 95 | return next; | ||
| 96 | } | ||
| 97 | } | ||
| 98 | |||
| 99 | function isNotFoundError(error: unknown): boolean { | ||
| 100 | return ( | ||
| 101 | typeof error === "object" && | ||
| 102 | error !== null && | ||
| 103 | "code" in error && | ||
| 104 | (error as { code?: string }).code === "ENOENT" | ||
| 105 | ); | ||
| 106 | } | ||
apps/renderer/index.html+1-1| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | <head> | 3 | <head> |
| 4 | <meta charset="UTF-8" /> | 4 | <meta charset="UTF-8" /> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 6 | <title>Electron Todo</title> | 6 | <title>CodeThing</title> |
| 7 | </head> | 7 | </head> |
| 8 | <body> | 8 | <body> |
| 9 | <div id="root"></div> | 9 | <div id="root"></div> |
apps/renderer/src/App.tsx+118-4| ... | @@ -3,8 +3,11 @@ import { useEffect, useMemo, useRef } from "react"; | ... | @@ -3,8 +3,11 @@ import { useEffect, useMemo, useRef } from "react"; |
| 3 | import ChatView from "./components/ChatView"; | 3 | import ChatView from "./components/ChatView"; |
| 4 | import DiffPanel from "./components/DiffPanel"; | 4 | import DiffPanel from "./components/DiffPanel"; |
| 5 | import Sidebar from "./components/Sidebar"; | 5 | import Sidebar from "./components/Sidebar"; |
| 6 | import { isElectron } from "./env"; | ||
| 7 | import { DEFAULT_MODEL } from "./model-logic"; | ||
| 6 | import { readNativeApi } from "./session-logic"; | 8 | import { readNativeApi } from "./session-logic"; |
| 7 | import { StoreProvider, useStore } from "./store"; | 9 | import { StoreProvider, useStore } from "./store"; |
| 10 | import { onServerWelcome } from "./wsNativeApi"; | ||
| 8 | 11 | ||
| 9 | function EventRouter() { | 12 | function EventRouter() { |
| 10 | const api = useMemo(() => readNativeApi(), []); | 13 | const api = useMemo(() => readNativeApi(), []); |
| ... | @@ -25,6 +28,118 @@ function EventRouter() { | ... | @@ -25,6 +28,118 @@ function EventRouter() { |
| 25 | return null; | 28 | return null; |
| 26 | } | 29 | } |
| 27 | 30 | ||
| 31 | function AutoProjectBootstrap() { | ||
| 32 | const { state, dispatch } = useStore(); | ||
| 33 | const bootstrappedRef = useRef(false); | ||
| 34 | |||
| 35 | useEffect(() => { | ||
| 36 | // Browser mode bootstraps from server welcome. | ||
| 37 | // Electron bootstraps from persisted projects via DesktopProjectBootstrap. | ||
| 38 | if (isElectron) return; | ||
| 39 | |||
| 40 | return onServerWelcome((payload) => { | ||
| 41 | if (bootstrappedRef.current) return; | ||
| 42 | |||
| 43 | // Don't create duplicate projects for the same cwd | ||
| 44 | const existing = state.projects.find((p) => p.cwd === payload.cwd); | ||
| 45 | if (existing) { | ||
| 46 | bootstrappedRef.current = true; | ||
| 47 | // Ensure a thread is active | ||
| 48 | const existingThread = state.threads.find((t) => t.projectId === existing.id); | ||
| 49 | if (existingThread && !state.activeThreadId) { | ||
| 50 | dispatch({ | ||
| 51 | type: "SET_ACTIVE_THREAD", | ||
| 52 | threadId: existingThread.id, | ||
| 53 | }); | ||
| 54 | } | ||
| 55 | return; | ||
| 56 | } | ||
| 57 | |||
| 58 | bootstrappedRef.current = true; | ||
| 59 | |||
| 60 | // Create project + thread from server cwd | ||
| 61 | const projectId = crypto.randomUUID(); | ||
| 62 | dispatch({ | ||
| 63 | type: "ADD_PROJECT", | ||
| 64 | project: { | ||
| 65 | id: projectId, | ||
| 66 | name: payload.projectName, | ||
| 67 | cwd: payload.cwd, | ||
| 68 | model: DEFAULT_MODEL, | ||
| 69 | expanded: true, | ||
| 70 | }, | ||
| 71 | }); | ||
| 72 | dispatch({ | ||
| 73 | type: "ADD_THREAD", | ||
| 74 | thread: { | ||
| 75 | id: crypto.randomUUID(), | ||
| 76 | codexThreadId: null, | ||
| 77 | projectId, | ||
| 78 | title: "New thread", | ||
| 79 | model: DEFAULT_MODEL, | ||
| 80 | session: null, | ||
| 81 | messages: [], | ||
| 82 | events: [], | ||
| 83 | error: null, | ||
| 84 | createdAt: new Date().toISOString(), | ||
| 85 | }, | ||
| 86 | }); | ||
| 87 | }); | ||
| 88 | }, [state.projects, state.threads, state.activeThreadId, dispatch]); | ||
| 89 | |||
| 90 | return null; | ||
| 91 | } | ||
| 92 | |||
| 93 | function DesktopProjectBootstrap() { | ||
| 94 | const api = useMemo(() => readNativeApi(), []); | ||
| 95 | const { dispatch } = useStore(); | ||
| 96 | const bootstrappedRef = useRef(false); | ||
| 97 | |||
| 98 | useEffect(() => { | ||
| 99 | if (!isElectron || !api || bootstrappedRef.current) return; | ||
| 100 | |||
| 101 | let disposed = false; | ||
| 102 | let retryDelayMs = 500; | ||
| 103 | let retryTimer: ReturnType<typeof setTimeout> | null = null; | ||
| 104 | |||
| 105 | const attemptBootstrap = async () => { | ||
| 106 | try { | ||
| 107 | const projects = await api.projects.list(); | ||
| 108 | if (disposed) return; | ||
| 109 | dispatch({ | ||
| 110 | type: "SYNC_PROJECTS", | ||
| 111 | projects: projects.map((project) => ({ | ||
| 112 | id: project.id, | ||
| 113 | name: project.name, | ||
| 114 | cwd: project.cwd, | ||
| 115 | model: DEFAULT_MODEL, | ||
| 116 | expanded: true, | ||
| 117 | })), | ||
| 118 | }); | ||
| 119 | bootstrappedRef.current = true; | ||
| 120 | } catch { | ||
| 121 | if (disposed) return; | ||
| 122 | retryTimer = setTimeout(() => { | ||
| 123 | retryTimer = null; | ||
| 124 | void attemptBootstrap(); | ||
| 125 | }, retryDelayMs); | ||
| 126 | retryDelayMs = Math.min(retryDelayMs * 2, 5_000); | ||
| 127 | } | ||
| 128 | }; | ||
| 129 | |||
| 130 | void attemptBootstrap(); | ||
| 131 | |||
| 132 | return () => { | ||
| 133 | disposed = true; | ||
| 134 | if (retryTimer) { | ||
| 135 | clearTimeout(retryTimer); | ||
| 136 | } | ||
| 137 | }; | ||
| 138 | }, [api, dispatch]); | ||
| 139 | |||
| 140 | return null; | ||
| 141 | } | ||
| 142 | |||
| 28 | function Layout() { | 143 | function Layout() { |
| 29 | const api = useMemo(() => readNativeApi(), []); | 144 | const api = useMemo(() => readNativeApi(), []); |
| 30 | const { state } = useStore(); | 145 | const { state } = useStore(); |
| ... | @@ -32,11 +147,8 @@ function Layout() { | ... | @@ -32,11 +147,8 @@ function Layout() { |
| 32 | if (!api) { | 147 | if (!api) { |
| 33 | return ( | 148 | return ( |
| 34 | <div className="flex h-screen flex-col bg-background text-foreground"> | 149 | <div className="flex h-screen flex-col bg-background text-foreground"> |
| 35 | <div className="drag-region h-[52px] shrink-0" /> | ||
| 36 | <div className="flex flex-1 items-center justify-center"> | 150 | <div className="flex flex-1 items-center justify-center"> |
| 37 | <p className="text-sm text-muted-foreground"> | 151 | <p className="text-sm text-muted-foreground">Connecting to CodeThing server...</p> |
| 38 | Native bridge unavailable. Launch through Electron. | ||
| 39 | </p> | ||
| 40 | </div> | 152 | </div> |
| 41 | </div> | 153 | </div> |
| 42 | ); | 154 | ); |
| ... | @@ -45,6 +157,8 @@ function Layout() { | ... | @@ -45,6 +157,8 @@ function Layout() { |
| 45 | return ( | 157 | return ( |
| 46 | <div className="flex h-screen overflow-hidden bg-background text-foreground"> | 158 | <div className="flex h-screen overflow-hidden bg-background text-foreground"> |
| 47 | <EventRouter /> | 159 | <EventRouter /> |
| 160 | <AutoProjectBootstrap /> | ||
| 161 | <DesktopProjectBootstrap /> | ||
| 48 | <Sidebar /> | 162 | <Sidebar /> |
| 49 | <ChatView /> | 163 | <ChatView /> |
| 50 | {state.diffOpen && <DiffPanel />} | 164 | {state.diffOpen && <DiffPanel />} |
apps/renderer/src/components/ChatView.tsx+3-2| ... | @@ -14,6 +14,7 @@ import { | ... | @@ -14,6 +14,7 @@ import { |
| 14 | } from "react"; | 14 | } from "react"; |
| 15 | 15 | ||
| 16 | import { EDITORS, type EditorId } from "@acme/contracts"; | 16 | import { EDITORS, type EditorId } from "@acme/contracts"; |
| 17 | import { isElectron } from "../env"; | ||
| 17 | import { buildBootstrapInput } from "../historyBootstrap"; | 18 | import { buildBootstrapInput } from "../historyBootstrap"; |
| 18 | import { | 19 | import { |
| 19 | DEFAULT_MODEL, | 20 | DEFAULT_MODEL, |
| ... | @@ -563,7 +564,7 @@ export default function ChatView() { | ... | @@ -563,7 +564,7 @@ export default function ChatView() { |
| 563 | if (!activeThread) { | 564 | if (!activeThread) { |
| 564 | return ( | 565 | return ( |
| 565 | <div className="flex flex-1 flex-col bg-background text-muted-foreground/40"> | 566 | <div className="flex flex-1 flex-col bg-background text-muted-foreground/40"> |
| 566 | <div className="drag-region h-[52px] shrink-0" /> | 567 | {isElectron && <div className="drag-region h-[52px] shrink-0" />} |
| 567 | <div className="flex flex-1 items-center justify-center"> | 568 | <div className="flex flex-1 items-center justify-center"> |
| 568 | <div className="text-center"> | 569 | <div className="text-center"> |
| 569 | <p className="text-sm">Select a thread or create a new one to get started.</p> | 570 | <p className="text-sm">Select a thread or create a new one to get started.</p> |
| ... | @@ -576,7 +577,7 @@ export default function ChatView() { | ... | @@ -576,7 +577,7 @@ export default function ChatView() { |
| 576 | return ( | 577 | return ( |
| 577 | <div className="flex flex-1 flex-col bg-background"> | 578 | <div className="flex flex-1 flex-col bg-background"> |
| 578 | {/* Top bar */} | 579 | {/* Top bar */} |
| 579 | <header className="drag-region flex items-center justify-between border-b border-border px-5 pt-[28px] pb-3"> | 580 | <header className={`flex items-center justify-between border-b border-border px-5 pb-3 ${isElectron ? "drag-region pt-[28px]" : "pt-3"}`}> |
| 580 | <div className="flex items-center gap-3"> | 581 | <div className="flex items-center gap-3"> |
| 581 | <h2 className="text-sm font-medium text-foreground"> | 582 | <h2 className="text-sm font-medium text-foreground"> |
| 582 | {activeThread.title} | 583 | {activeThread.title} |
apps/renderer/src/components/DiffPanel.tsx+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { isElectron } from "../env"; | ||
| 1 | import { useStore } from "../store"; | 2 | import { useStore } from "../store"; |
| 2 | 3 | ||
| 3 | export default function DiffPanel() { | 4 | export default function DiffPanel() { |
| ... | @@ -6,7 +7,7 @@ export default function DiffPanel() { | ... | @@ -6,7 +7,7 @@ export default function DiffPanel() { |
| 6 | return ( | 7 | return ( |
| 7 | <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-border bg-card"> | 8 | <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-border bg-card"> |
| 8 | {/* Header */} | 9 | {/* Header */} |
| 9 | <div className="drag-region flex items-center justify-between border-b border-border px-4 pt-[28px] pb-3"> | 10 | <div className={`flex items-center justify-between border-b border-border px-4 pb-3 ${isElectron ? "drag-region pt-[28px]" : "pt-3"}`}> |
| 10 | <h3 className="text-xs font-medium text-foreground">Uncommitted changes</h3> | 11 | <h3 className="text-xs font-medium text-foreground">Uncommitted changes</h3> |
| 11 | <button | 12 | <button |
| 12 | type="button" | 13 | type="button" |
apps/renderer/src/components/Sidebar.tsx+112-43| ... | @@ -1,5 +1,6 @@ | ... | @@ -1,5 +1,6 @@ |
| 1 | import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react"; | 1 | import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react"; |
| 2 | import { useCallback, useEffect, useMemo, useState } from "react"; | 2 | import { useCallback, useEffect, useMemo, useState } from "react"; |
| 3 | import { isElectron } from "../env"; | ||
| 3 | import { useTheme } from "../hooks/useTheme"; | 4 | import { useTheme } from "../hooks/useTheme"; |
| 4 | import { DEFAULT_MODEL } from "../model-logic"; | 5 | import { DEFAULT_MODEL } from "../model-logic"; |
| 5 | import { readNativeApi } from "../session-logic"; | 6 | import { readNativeApi } from "../session-logic"; |
| ... | @@ -17,6 +18,13 @@ function formatRelativeTime(iso: string): string { | ... | @@ -17,6 +18,13 @@ function formatRelativeTime(iso: string): string { |
| 17 | return `${Math.floor(hours / 24)}d ago`; | 18 | return `${Math.floor(hours / 24)}d ago`; |
| 18 | } | 19 | } |
| 19 | 20 | ||
| 21 | function inferProjectName(cwd: string): string { | ||
| 22 | const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, ""); | ||
| 23 | const parts = normalized.split("/"); | ||
| 24 | const last = parts[parts.length - 1]; | ||
| 25 | return last && last.length > 0 ? last : "project"; | ||
| 26 | } | ||
| 27 | |||
| 20 | function threadStatusLabel( | 28 | function threadStatusLabel( |
| 21 | status: "connecting" | "ready" | "running" | "error" | "closed" | undefined, | 29 | status: "connecting" | "ready" | "running" | "error" | "closed" | undefined, |
| 22 | ): "Working" | "Connecting" | null { | 30 | ): "Working" | "Connecting" | null { |
| ... | @@ -32,22 +40,7 @@ export default function Sidebar() { | ... | @@ -32,22 +40,7 @@ export default function Sidebar() { |
| 32 | const [addingProject, setAddingProject] = useState(false); | 40 | const [addingProject, setAddingProject] = useState(false); |
| 33 | const [newCwd, setNewCwd] = useState(""); | 41 | const [newCwd, setNewCwd] = useState(""); |
| 34 | const [isPickingFolder, setIsPickingFolder] = useState(false); | 42 | const [isPickingFolder, setIsPickingFolder] = useState(false); |
| 35 | 43 | const [isAddingProject, setIsAddingProject] = useState(false); | |
| 36 | const handleAddProject = () => { | ||
| 37 | const cwd = newCwd.trim(); | ||
| 38 | if (!cwd) return; | ||
| 39 | const name = cwd.split("/").filter(Boolean).pop() ?? "project"; | ||
| 40 | const project: Project = { | ||
| 41 | id: crypto.randomUUID(), | ||
| 42 | name, | ||
| 43 | cwd, | ||
| 44 | model: DEFAULT_MODEL, | ||
| 45 | expanded: true, | ||
| 46 | }; | ||
| 47 | dispatch({ type: "ADD_PROJECT", project }); | ||
| 48 | setNewCwd(""); | ||
| 49 | setAddingProject(false); | ||
| 50 | }; | ||
| 51 | 44 | ||
| 52 | const handleNewThread = useCallback( | 45 | const handleNewThread = useCallback( |
| 53 | (projectId: string) => { | 46 | (projectId: string) => { |
| ... | @@ -58,9 +51,7 @@ export default function Sidebar() { | ... | @@ -58,9 +51,7 @@ export default function Sidebar() { |
| 58 | codexThreadId: null, | 51 | codexThreadId: null, |
| 59 | projectId, | 52 | projectId, |
| 60 | title: "New thread", | 53 | title: "New thread", |
| 61 | model: | 54 | model: state.projects.find((p) => p.id === projectId)?.model ?? DEFAULT_MODEL, |
| 62 | state.projects.find((p) => p.id === projectId)?.model ?? | ||
| 63 | DEFAULT_MODEL, | ||
| 64 | session: null, | 55 | session: null, |
| 65 | messages: [], | 56 | messages: [], |
| 66 | events: [], | 57 | events: [], |
| ... | @@ -72,13 +63,98 @@ export default function Sidebar() { | ... | @@ -72,13 +63,98 @@ export default function Sidebar() { |
| 72 | [dispatch, state.projects], | 63 | [dispatch, state.projects], |
| 73 | ); | 64 | ); |
| 74 | 65 | ||
| 66 | const focusMostRecentThreadForProject = useCallback( | ||
| 67 | (projectId: string) => { | ||
| 68 | const latestThread = state.threads | ||
| 69 | .filter((thread) => thread.projectId === projectId) | ||
| 70 | .toSorted((a, b) => { | ||
| 71 | const byDate = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); | ||
| 72 | if (byDate !== 0) return byDate; | ||
| 73 | return b.id.localeCompare(a.id); | ||
| 74 | })[0]; | ||
| 75 | if (!latestThread) return; | ||
| 76 | |||
| 77 | dispatch({ | ||
| 78 | type: "SET_ACTIVE_THREAD", | ||
| 79 | threadId: latestThread.id, | ||
| 80 | }); | ||
| 81 | }, | ||
| 82 | [dispatch, state.threads], | ||
| 83 | ); | ||
| 84 | |||
| 85 | const addProjectFromPath = useCallback( | ||
| 86 | async (rawCwd: string) => { | ||
| 87 | const cwd = rawCwd.trim(); | ||
| 88 | if (!cwd || isAddingProject) return; | ||
| 89 | |||
| 90 | setIsAddingProject(true); | ||
| 91 | try { | ||
| 92 | if (isElectron && api) { | ||
| 93 | const result = await api.projects.add({ cwd }); | ||
| 94 | const project: Project = { | ||
| 95 | id: result.project.id, | ||
| 96 | name: result.project.name, | ||
| 97 | cwd: result.project.cwd, | ||
| 98 | model: DEFAULT_MODEL, | ||
| 99 | expanded: true, | ||
| 100 | }; | ||
| 101 | const existingById = state.projects.find((p) => p.id === project.id); | ||
| 102 | const existingByCwd = state.projects.find((p) => p.cwd === project.cwd); | ||
| 103 | if (!existingById && !existingByCwd) { | ||
| 104 | dispatch({ type: "ADD_PROJECT", project }); | ||
| 105 | } | ||
| 106 | const resolvedProjectId = existingByCwd?.id ?? project.id; | ||
| 107 | |||
| 108 | if (result.created) { | ||
| 109 | handleNewThread(resolvedProjectId); | ||
| 110 | } else { | ||
| 111 | focusMostRecentThreadForProject(resolvedProjectId); | ||
| 112 | } | ||
| 113 | } else { | ||
| 114 | const existing = state.projects.find((project) => project.cwd === cwd); | ||
| 115 | if (existing) { | ||
| 116 | focusMostRecentThreadForProject(existing.id); | ||
| 117 | return; | ||
| 118 | } | ||
| 119 | |||
| 120 | const name = inferProjectName(cwd); | ||
| 121 | const project: Project = { | ||
| 122 | id: crypto.randomUUID(), | ||
| 123 | name, | ||
| 124 | cwd, | ||
| 125 | model: DEFAULT_MODEL, | ||
| 126 | expanded: true, | ||
| 127 | }; | ||
| 128 | dispatch({ type: "ADD_PROJECT", project }); | ||
| 129 | handleNewThread(project.id); | ||
| 130 | } | ||
| 131 | } finally { | ||
| 132 | setIsAddingProject(false); | ||
| 133 | setNewCwd(""); | ||
| 134 | setAddingProject(false); | ||
| 135 | } | ||
| 136 | }, | ||
| 137 | [ | ||
| 138 | api, | ||
| 139 | dispatch, | ||
| 140 | focusMostRecentThreadForProject, | ||
| 141 | handleNewThread, | ||
| 142 | isAddingProject, | ||
| 143 | state.projects, | ||
| 144 | ], | ||
| 145 | ); | ||
| 146 | |||
| 147 | const handleAddProject = () => { | ||
| 148 | void addProjectFromPath(newCwd); | ||
| 149 | }; | ||
| 150 | |||
| 75 | const handlePickFolder = async () => { | 151 | const handlePickFolder = async () => { |
| 76 | if (!api || isPickingFolder) return; | 152 | if (!api || isPickingFolder) return; |
| 77 | setIsPickingFolder(true); | 153 | setIsPickingFolder(true); |
| 78 | try { | 154 | try { |
| 79 | const pickedPath = await api.dialogs.pickFolder(); | 155 | const pickedPath = await api.dialogs.pickFolder(); |
| 80 | if (!pickedPath) return; | 156 | if (!pickedPath) return; |
| 81 | setNewCwd(pickedPath); | 157 | await addProjectFromPath(pickedPath); |
| 82 | } finally { | 158 | } finally { |
| 83 | setIsPickingFolder(false); | 159 | setIsPickingFolder(false); |
| 84 | } | 160 | } |
| ... | @@ -107,9 +183,7 @@ export default function Sidebar() { | ... | @@ -107,9 +183,7 @@ export default function Sidebar() { |
| 107 | } | 183 | } |
| 108 | } | 184 | } |
| 109 | 185 | ||
| 110 | const activeThread = state.threads.find( | 186 | const activeThread = state.threads.find((t) => t.id === state.activeThreadId); |
| 111 | (t) => t.id === state.activeThreadId, | ||
| 112 | ); | ||
| 113 | const projectId = activeThread?.projectId ?? state.projects[0]?.id; | 187 | const projectId = activeThread?.projectId ?? state.projects[0]?.id; |
| 114 | if (!projectId) return; | 188 | if (!projectId) return; |
| 115 | 189 | ||
| ... | @@ -125,10 +199,10 @@ export default function Sidebar() { | ... | @@ -125,10 +199,10 @@ export default function Sidebar() { |
| 125 | 199 | ||
| 126 | return ( | 200 | return ( |
| 127 | <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-border bg-card"> | 201 | <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-border bg-card"> |
| 128 | {/* Drag region / traffic light space */} | 202 | {/* Drag region / traffic light space (Electron only) */} |
| 129 | <div className="drag-region h-[52px] shrink-0" /> | 203 | {isElectron && <div className="drag-region h-[52px] shrink-0" />} |
| 130 | {/* Branding */} | 204 | {/* Branding */} |
| 131 | <div className="flex items-center gap-2 px-4 pb-4"> | 205 | <div className={`flex items-center gap-2 px-4 pb-4 ${isElectron ? "" : "pt-4"}`}> |
| 132 | <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-xs font-bold text-primary-foreground"> | 206 | <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-xs font-bold text-primary-foreground"> |
| 133 | CT | 207 | CT |
| 134 | </div> | 208 | </div> |
| ... | @@ -176,10 +250,8 @@ export default function Sidebar() { | ... | @@ -176,10 +250,8 @@ export default function Sidebar() { |
| 176 | {state.projects.map((project) => { | 250 | {state.projects.map((project) => { |
| 177 | const threads = state.threads | 251 | const threads = state.threads |
| 178 | .filter((t) => t.projectId === project.id) | 252 | .filter((t) => t.projectId === project.id) |
| 179 | .sort((a, b) => { | 253 | .toSorted((a, b) => { |
| 180 | const byDate = | 254 | const byDate = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); |
| 181 | new Date(b.createdAt).getTime() - | ||
| 182 | new Date(a.createdAt).getTime(); | ||
| 183 | if (byDate !== 0) return byDate; | 255 | if (byDate !== 0) return byDate; |
| 184 | return b.id.localeCompare(a.id); | 256 | return b.id.localeCompare(a.id); |
| 185 | }); | 257 | }); |
| ... | @@ -205,15 +277,15 @@ export default function Sidebar() { | ... | @@ -205,15 +277,15 @@ export default function Sidebar() { |
| 205 | <div className="ml-2 border-l border-border/80 pl-2"> | 277 | <div className="ml-2 border-l border-border/80 pl-2"> |
| 206 | {threads.map((thread) => { | 278 | {threads.map((thread) => { |
| 207 | const isActive = state.activeThreadId === thread.id; | 279 | const isActive = state.activeThreadId === thread.id; |
| 208 | const threadStatus = threadStatusLabel( | 280 | const threadStatus = threadStatusLabel(thread.session?.status); |
| 209 | thread.session?.status, | ||
| 210 | ); | ||
| 211 | return ( | 281 | return ( |
| 212 | <button | 282 | <button |
| 213 | key={thread.id} | 283 | key={thread.id} |
| 214 | type="button" | 284 | type="button" |
| 215 | className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 ${ | 285 | className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 ${ |
| 216 | isActive ? "bg-accent text-foreground" : "text-muted-foreground hover:bg-secondary" | 286 | isActive |
| 287 | ? "bg-accent text-foreground" | ||
| 288 | : "text-muted-foreground hover:bg-secondary" | ||
| 217 | }`} | 289 | }`} |
| 218 | onClick={() => | 290 | onClick={() => |
| 219 | dispatch({ | 291 | dispatch({ |
| ... | @@ -226,14 +298,10 @@ export default function Sidebar() { | ... | @@ -226,14 +298,10 @@ export default function Sidebar() { |
| 226 | {threadStatus && ( | 298 | {threadStatus && ( |
| 227 | <span className="inline-flex items-center gap-1 text-[10px] text-sky-600 dark:text-sky-300/80"> | 299 | <span className="inline-flex items-center gap-1 text-[10px] text-sky-600 dark:text-sky-300/80"> |
| 228 | <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-500 dark:bg-sky-300/80" /> | 300 | <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-500 dark:bg-sky-300/80" /> |
| 229 | <span className="hidden xl:inline"> | 301 | <span className="hidden xl:inline">{threadStatus}</span> |
| 230 | {threadStatus} | ||
| 231 | </span> | ||
| 232 | </span> | 302 | </span> |
| 233 | )} | 303 | )} |
| 234 | <span className="min-w-0 flex-1 truncate text-xs"> | 304 | <span className="min-w-0 flex-1 truncate text-xs">{thread.title}</span> |
| 235 | {thread.title} | ||
| 236 | </span> | ||
| 237 | </div> | 305 | </div> |
| 238 | <span className="shrink-0 text-[10px] text-muted-foreground/40"> | 306 | <span className="shrink-0 text-[10px] text-muted-foreground/40"> |
| 239 | {formatRelativeTime(thread.createdAt)} | 307 | {formatRelativeTime(thread.createdAt)} |
| ... | @@ -281,12 +349,12 @@ export default function Sidebar() { | ... | @@ -281,12 +349,12 @@ export default function Sidebar() { |
| 281 | if (e.key === "Escape") setAddingProject(false); | 349 | if (e.key === "Escape") setAddingProject(false); |
| 282 | }} | 350 | }} |
| 283 | /> | 351 | /> |
| 284 | {api && ( | 352 | {isElectron && api && ( |
| 285 | <button | 353 | <button |
| 286 | type="button" | 354 | type="button" |
| 287 | className="mb-2 flex w-full items-center justify-center rounded-md border border-border px-2 py-1.5 text-xs text-muted-foreground transition-colors duration-150 hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60" | 355 | className="mb-2 flex w-full items-center justify-center rounded-md border border-border px-2 py-1.5 text-xs text-muted-foreground transition-colors duration-150 hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60" |
| 288 | onClick={() => void handlePickFolder()} | 356 | onClick={() => void handlePickFolder()} |
| 289 | disabled={isPickingFolder} | 357 | disabled={isPickingFolder || isAddingProject} |
| 290 | > | 358 | > |
| 291 | {isPickingFolder ? "Picking folder..." : "Browse for folder"} | 359 | {isPickingFolder ? "Picking folder..." : "Browse for folder"} |
| 292 | </button> | 360 | </button> |
| ... | @@ -296,8 +364,9 @@ export default function Sidebar() { | ... | @@ -296,8 +364,9 @@ export default function Sidebar() { |
| 296 | type="button" | 364 | type="button" |
| 297 | className="flex-1 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90" | 365 | className="flex-1 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90" |
| 298 | onClick={handleAddProject} | 366 | onClick={handleAddProject} |
| 367 | disabled={isAddingProject} | ||
| 299 | > | 368 | > |
| 300 | Add | 369 | {isAddingProject ? "Adding..." : "Add"} |
| 301 | </button> | 370 | </button> |
| 302 | <button | 371 | <button |
| 303 | type="button" | 372 | type="button" |
apps/renderer/src/env.ts created+8| ... | @@ -0,0 +1,8 @@ | ||
| 1 | /** | ||
| 2 | * True when running inside the Electron preload bridge, false in a regular browser. | ||
| 3 | * The preload script sets window.nativeApi via contextBridge before any renderer | ||
| 4 | * code executes, so this is reliable at module load time. | ||
| 5 | */ | ||
| 6 | export const isElectron = | ||
| 7 | typeof window !== "undefined" && | ||
| 8 | (window.desktopBridge !== undefined || window.nativeApi !== undefined); | ||
apps/renderer/src/session-logic.ts+16-3| ... | @@ -1,5 +1,6 @@ | ... | @@ -1,5 +1,6 @@ |
| 1 | import type { ProviderEvent, ProviderKind, ProviderSession } from "@acme/contracts"; | 1 | import type { NativeApi, ProviderEvent, ProviderKind, ProviderSession } from "@acme/contracts"; |
| 2 | import type { ChatMessage, SessionPhase } from "./types"; | 2 | import type { ChatMessage, SessionPhase } from "./types"; |
| 3 | import { createWsNativeApi } from "./wsNativeApi"; | ||
| 3 | 4 | ||
| 4 | export const PROVIDER_OPTIONS: Array<{ | 5 | export const PROVIDER_OPTIONS: Array<{ |
| 5 | value: ProviderKind; | 6 | value: ProviderKind; |
| ... | @@ -10,9 +11,21 @@ export const PROVIDER_OPTIONS: Array<{ | ... | @@ -10,9 +11,21 @@ export const PROVIDER_OPTIONS: Array<{ |
| 10 | { value: "claudeCode", label: "Claude Code (soon)", available: false }, | 11 | { value: "claudeCode", label: "Claude Code (soon)", available: false }, |
| 11 | ]; | 12 | ]; |
| 12 | 13 | ||
| 13 | export function readNativeApi() { | 14 | let cachedApi: NativeApi | undefined; |
| 15 | |||
| 16 | export function readNativeApi(): NativeApi | undefined { | ||
| 14 | if (typeof window === "undefined") return undefined; | 17 | if (typeof window === "undefined") return undefined; |
| 15 | return window.nativeApi; | 18 | if (cachedApi) return cachedApi; |
| 19 | |||
| 20 | // Prefer Electron preload bridge if available | ||
| 21 | if (window.nativeApi) { | ||
| 22 | cachedApi = window.nativeApi; | ||
| 23 | return cachedApi; | ||
| 24 | } | ||
| 25 | |||
| 26 | // Fall back to WebSocket transport | ||
| 27 | cachedApi = createWsNativeApi(); | ||
| 28 | return cachedApi; | ||
| 16 | } | 29 | } |
| 17 | 30 | ||
| 18 | export function asObject(value: unknown): Record<string, unknown> | undefined { | 31 | export function asObject(value: unknown): Record<string, unknown> | undefined { |
apps/renderer/src/store.test.ts+57-3| ... | @@ -4,9 +4,7 @@ import { describe, expect, it } from "vitest"; | ... | @@ -4,9 +4,7 @@ import { describe, expect, it } from "vitest"; |
| 4 | import { type AppState, reducer } from "./store"; | 4 | import { type AppState, reducer } from "./store"; |
| 5 | import type { Thread } from "./types"; | 5 | import type { Thread } from "./types"; |
| 6 | 6 | ||
| 7 | function makeSession( | 7 | function makeSession(overrides: Partial<ProviderSession> = {}): ProviderSession { |
| 8 | overrides: Partial<ProviderSession> = {}, | ||
| 9 | ): ProviderSession { | ||
| 10 | return { | 8 | return { |
| 11 | sessionId: "sess-1", | 9 | sessionId: "sess-1", |
| 12 | provider: "codex", | 10 | provider: "codex", |
| ... | @@ -131,4 +129,60 @@ describe("store reducer thread continuity", () => { | ... | @@ -131,4 +129,60 @@ describe("store reducer thread continuity", () => { |
| 131 | expect(next.threads[0]?.codexThreadId).toBe("thr_new"); | 129 | expect(next.threads[0]?.codexThreadId).toBe("thr_new"); |
| 132 | expect(next.threads[0]?.session?.threadId).toBe("thr_new"); | 130 | expect(next.threads[0]?.session?.threadId).toBe("thr_new"); |
| 133 | }); | 131 | }); |
| 132 | |||
| 133 | it("reconciles project ids by cwd when syncing backend projects", () => { | ||
| 134 | const state: AppState = { | ||
| 135 | projects: [ | ||
| 136 | { | ||
| 137 | id: "project-old-a", | ||
| 138 | name: "A", | ||
| 139 | cwd: "/tmp/a", | ||
| 140 | model: "gpt-5.3-codex", | ||
| 141 | expanded: false, | ||
| 142 | }, | ||
| 143 | { | ||
| 144 | id: "project-old-b", | ||
| 145 | name: "B", | ||
| 146 | cwd: "/tmp/b", | ||
| 147 | model: "gpt-5.3-codex", | ||
| 148 | expanded: true, | ||
| 149 | }, | ||
| 150 | ], | ||
| 151 | threads: [ | ||
| 152 | makeThread({ | ||
| 153 | id: "thread-a", | ||
| 154 | projectId: "project-old-a", | ||
| 155 | }), | ||
| 156 | makeThread({ | ||
| 157 | id: "thread-b", | ||
| 158 | projectId: "project-old-b", | ||
| 159 | }), | ||
| 160 | ], | ||
| 161 | activeThreadId: "thread-b", | ||
| 162 | runtimeMode: "full-access", | ||
| 163 | diffOpen: false, | ||
| 164 | }; | ||
| 165 | |||
| 166 | const next = reducer(state, { | ||
| 167 | type: "SYNC_PROJECTS", | ||
| 168 | projects: [ | ||
| 169 | { | ||
| 170 | id: "project-new-a", | ||
| 171 | name: "A", | ||
| 172 | cwd: "/tmp/a", | ||
| 173 | model: "gpt-5.3-codex", | ||
| 174 | expanded: true, | ||
| 175 | }, | ||
| 176 | ], | ||
| 177 | }); | ||
| 178 | |||
| 179 | expect(next.projects).toHaveLength(1); | ||
| 180 | expect(next.projects[0]?.id).toBe("project-new-a"); | ||
| 181 | // Preserve existing project UI preferences by cwd | ||
| 182 | expect(next.projects[0]?.expanded).toBe(false); | ||
| 183 | expect(next.threads).toHaveLength(1); | ||
| 184 | expect(next.threads[0]?.id).toBe("thread-a"); | ||
| 185 | expect(next.threads[0]?.projectId).toBe("project-new-a"); | ||
| 186 | expect(next.activeThreadId).toBe("thread-a"); | ||
| 187 | }); | ||
| 134 | }); | 188 | }); |
apps/renderer/src/store.ts+57-40| ... | @@ -11,23 +11,14 @@ import { | ... | @@ -11,23 +11,14 @@ import { |
| 11 | import type { ProviderEvent, ProviderSession } from "@acme/contracts"; | 11 | import type { ProviderEvent, ProviderSession } from "@acme/contracts"; |
| 12 | import { resolveModelSlug } from "./model-logic"; | 12 | import { resolveModelSlug } from "./model-logic"; |
| 13 | import { hydratePersistedState, toPersistedState } from "./persistenceSchema"; | 13 | import { hydratePersistedState, toPersistedState } from "./persistenceSchema"; |
| 14 | import { | 14 | import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic"; |
| 15 | applyEventToMessages, | 15 | import { DEFAULT_RUNTIME_MODE, type Project, type RuntimeMode, type Thread } from "./types"; |
| 16 | asObject, | ||
| 17 | asString, | ||
| 18 | evolveSession, | ||
| 19 | } from "./session-logic"; | ||
| 20 | import { | ||
| 21 | DEFAULT_RUNTIME_MODE, | ||
| 22 | type Project, | ||
| 23 | type RuntimeMode, | ||
| 24 | type Thread, | ||
| 25 | } from "./types"; | ||
| 26 | 16 | ||
| 27 | // ── Actions ────────────────────────────────────────────────────────── | 17 | // ── Actions ────────────────────────────────────────────────────────── |
| 28 | 18 | ||
| 29 | type Action = | 19 | type Action = |
| 30 | | { type: "ADD_PROJECT"; project: Project } | 20 | | { type: "ADD_PROJECT"; project: Project } |
| 21 | | { type: "SYNC_PROJECTS"; projects: Project[] } | ||
| 31 | | { type: "TOGGLE_PROJECT"; projectId: string } | 22 | | { type: "TOGGLE_PROJECT"; projectId: string } |
| 32 | | { type: "ADD_THREAD"; thread: Thread } | 23 | | { type: "ADD_THREAD"; thread: Thread } |
| 33 | | { type: "SET_ACTIVE_THREAD"; threadId: string } | 24 | | { type: "SET_ACTIVE_THREAD"; threadId: string } |
| ... | @@ -98,10 +89,7 @@ function persistState(state: AppState): void { | ... | @@ -98,10 +89,7 @@ function persistState(state: AppState): void { |
| 98 | if (typeof window === "undefined") return; | 89 | if (typeof window === "undefined") return; |
| 99 | 90 | ||
| 100 | try { | 91 | try { |
| 101 | window.localStorage.setItem( | 92 | window.localStorage.setItem(PERSISTED_STATE_KEY, JSON.stringify(toPersistedState(state))); |
| 102 | PERSISTED_STATE_KEY, | ||
| 103 | JSON.stringify(toPersistedState(state)), | ||
| 104 | ); | ||
| 105 | for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) { | 93 | for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) { |
| 106 | window.localStorage.removeItem(legacyKey); | 94 | window.localStorage.removeItem(legacyKey); |
| 107 | } | 95 | } |
| ... | @@ -143,10 +131,7 @@ function getEventThreadId(event: ProviderEvent): string | undefined { | ... | @@ -143,10 +131,7 @@ function getEventThreadId(event: ProviderEvent): string | undefined { |
| 143 | ); | 131 | ); |
| 144 | } | 132 | } |
| 145 | 133 | ||
| 146 | function shouldIgnoreForeignThreadEvent( | 134 | function shouldIgnoreForeignThreadEvent(thread: Thread, event: ProviderEvent): boolean { |
| 147 | thread: Thread, | ||
| 148 | event: ProviderEvent, | ||
| 149 | ): boolean { | ||
| 150 | const eventThreadId = getEventThreadId(event); | 135 | const eventThreadId = getEventThreadId(event); |
| 151 | if (!eventThreadId) { | 136 | if (!eventThreadId) { |
| 152 | return false; | 137 | return false; |
| ... | @@ -175,10 +160,7 @@ function durationMs(startIso: string, endIso: string): number | undefined { | ... | @@ -175,10 +160,7 @@ function durationMs(startIso: string, endIso: string): number | undefined { |
| 175 | return end - start; | 160 | return end - start; |
| 176 | } | 161 | } |
| 177 | 162 | ||
| 178 | function updateTurnFields( | 163 | function updateTurnFields(thread: Thread, event: ProviderEvent): Partial<Thread> { |
| 179 | thread: Thread, | ||
| 180 | event: ProviderEvent, | ||
| 181 | ): Partial<Thread> { | ||
| 182 | if (event.method === "turn/started") { | 164 | if (event.method === "turn/started") { |
| 183 | return { | 165 | return { |
| 184 | latestTurnId: getEventTurnId(event) ?? thread.latestTurnId, | 166 | latestTurnId: getEventTurnId(event) ?? thread.latestTurnId, |
| ... | @@ -195,9 +177,7 @@ function updateTurnFields( | ... | @@ -195,9 +177,7 @@ function updateTurnFields( |
| 195 | ? thread.latestTurnStartedAt | 177 | ? thread.latestTurnStartedAt |
| 196 | : undefined; | 178 | : undefined; |
| 197 | const elapsed = | 179 | const elapsed = |
| 198 | startedAt && startedAt.length > 0 | 180 | startedAt && startedAt.length > 0 ? durationMs(startedAt, event.createdAt) : undefined; |
| 199 | ? durationMs(startedAt, event.createdAt) | ||
| 200 | : undefined; | ||
| 201 | 181 | ||
| 202 | return { | 182 | return { |
| 203 | latestTurnId: completedTurnId ?? thread.latestTurnId, | 183 | latestTurnId: completedTurnId ?? thread.latestTurnId, |
| ... | @@ -214,6 +194,9 @@ function updateTurnFields( | ... | @@ -214,6 +194,9 @@ function updateTurnFields( |
| 214 | export function reducer(state: AppState, action: Action): AppState { | 194 | export function reducer(state: AppState, action: Action): AppState { |
| 215 | switch (action.type) { | 195 | switch (action.type) { |
| 216 | case "ADD_PROJECT": | 196 | case "ADD_PROJECT": |
| 197 | if (state.projects.some((project) => project.cwd === action.project.cwd)) { | ||
| 198 | return state; | ||
| 199 | } | ||
| 217 | return { | 200 | return { |
| 218 | ...state, | 201 | ...state, |
| 219 | projects: [ | 202 | projects: [ |
| ... | @@ -225,6 +208,48 @@ export function reducer(state: AppState, action: Action): AppState { | ... | @@ -225,6 +208,48 @@ export function reducer(state: AppState, action: Action): AppState { |
| 225 | ], | 208 | ], |
| 226 | }; | 209 | }; |
| 227 | 210 | ||
| 211 | case "SYNC_PROJECTS": { | ||
| 212 | const previousByCwd = new Map( | ||
| 213 | state.projects.map((project) => [project.cwd, project] as const), | ||
| 214 | ); | ||
| 215 | const nextProjects = action.projects.map((project) => { | ||
| 216 | const previous = previousByCwd.get(project.cwd); | ||
| 217 | return { | ||
| 218 | ...project, | ||
| 219 | model: resolveModelSlug(previous?.model ?? project.model), | ||
| 220 | expanded: previous?.expanded ?? project.expanded, | ||
| 221 | }; | ||
| 222 | }); | ||
| 223 | const previousProjectById = new Map( | ||
| 224 | state.projects.map((project) => [project.id, project] as const), | ||
| 225 | ); | ||
| 226 | const nextProjectIdByCwd = new Map( | ||
| 227 | nextProjects.map((project) => [project.cwd, project.id] as const), | ||
| 228 | ); | ||
| 229 | const nextThreads = state.threads | ||
| 230 | .map((thread) => { | ||
| 231 | const previousProject = previousProjectById.get(thread.projectId); | ||
| 232 | if (!previousProject) return null; | ||
| 233 | const mappedProjectId = nextProjectIdByCwd.get(previousProject.cwd); | ||
| 234 | if (!mappedProjectId) return null; | ||
| 235 | return { | ||
| 236 | ...thread, | ||
| 237 | projectId: mappedProjectId, | ||
| 238 | }; | ||
| 239 | }) | ||
| 240 | .filter((thread): thread is Thread => thread !== null); | ||
| 241 | const activeThreadId = nextThreads.some((thread) => thread.id === state.activeThreadId) | ||
| 242 | ? state.activeThreadId | ||
| 243 | : (nextThreads[0]?.id ?? null); | ||
| 244 | |||
| 245 | return { | ||
| 246 | ...state, | ||
| 247 | projects: nextProjects, | ||
| 248 | threads: nextThreads, | ||
| 249 | activeThreadId, | ||
| 250 | }; | ||
| 251 | } | ||
| 252 | |||
| 228 | case "TOGGLE_PROJECT": | 253 | case "TOGGLE_PROJECT": |
| 229 | return { | 254 | return { |
| 230 | ...state, | 255 | ...state, |
| ... | @@ -265,24 +290,16 @@ export function reducer(state: AppState, action: Action): AppState { | ... | @@ -265,24 +290,16 @@ export function reducer(state: AppState, action: Action): AppState { |
| 265 | ...(() => { | 290 | ...(() => { |
| 266 | const eventThreadId = getEventThreadId(event); | 291 | const eventThreadId = getEventThreadId(event); |
| 267 | const shouldRebindIdentity = | 292 | const shouldRebindIdentity = |
| 268 | event.method === "thread/started" && | 293 | event.method === "thread/started" && t.session?.status === "connecting"; |
| 269 | t.session?.status === "connecting"; | ||
| 270 | return { | 294 | return { |
| 271 | codexThreadId: shouldRebindIdentity | 295 | codexThreadId: shouldRebindIdentity |
| 272 | ? eventThreadId ?? t.codexThreadId | 296 | ? (eventThreadId ?? t.codexThreadId) |
| 273 | : t.codexThreadId ?? eventThreadId ?? null, | 297 | : (t.codexThreadId ?? eventThreadId ?? null), |
| 274 | error: | 298 | error: event.kind === "error" && event.message ? event.message : t.error, |
| 275 | (event.kind === "error" && event.message | ||
| 276 | ? event.message | ||
| 277 | : t.error), | ||
| 278 | }; | 299 | }; |
| 279 | })(), | 300 | })(), |
| 280 | session: t.session ? evolveSession(t.session, event) : t.session, | 301 | session: t.session ? evolveSession(t.session, event) : t.session, |
| 281 | messages: applyEventToMessages( | 302 | messages: applyEventToMessages(t.messages, event, activeAssistantItemRef), |
| 282 | t.messages, | ||
| 283 | event, | ||
| 284 | activeAssistantItemRef, | ||
| 285 | ), | ||
| 286 | events: [event, ...t.events], | 303 | events: [event, ...t.events], |
| 287 | ...updateTurnFields(t, event), | 304 | ...updateTurnFields(t, event), |
| 288 | })), | 305 | })), |
apps/renderer/src/vite-env.d.ts+6| ... | @@ -2,8 +2,14 @@ | ... | @@ -2,8 +2,14 @@ |
| 2 | 2 | ||
| 3 | import type { NativeApi } from "@acme/contracts"; | 3 | import type { NativeApi } from "@acme/contracts"; |
| 4 | 4 | ||
| 5 | interface DesktopBridge { | ||
| 6 | getWsUrl: () => string | null; | ||
| 7 | pickFolder: () => Promise<string | null>; | ||
| 8 | } | ||
| 9 | |||
| 5 | declare global { | 10 | declare global { |
| 6 | interface Window { | 11 | interface Window { |
| 7 | nativeApi?: NativeApi; | 12 | nativeApi?: NativeApi; |
| 13 | desktopBridge?: DesktopBridge; | ||
| 8 | } | 14 | } |
| 9 | } | 15 | } |
apps/renderer/src/wsNativeApi.ts created+102| ... | @@ -0,0 +1,102 @@ | ||
| 1 | import { type NativeApi, WS_CHANNELS, WS_METHODS, type WsWelcomePayload } from "@acme/contracts"; | ||
| 2 | |||
| 3 | import { WsTransport } from "./wsTransport"; | ||
| 4 | |||
| 5 | let instance: { api: NativeApi; transport: WsTransport } | null = null; | ||
| 6 | const welcomeListeners = new Set<(payload: WsWelcomePayload) => void>(); | ||
| 7 | let lastWelcome: WsWelcomePayload | null = null; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Subscribe to the server welcome message. If a welcome was already received | ||
| 11 | * before this call, the listener fires synchronously with the cached payload. | ||
| 12 | * This avoids the race between WebSocket connect and React effect registration. | ||
| 13 | */ | ||
| 14 | export function onServerWelcome(listener: (payload: WsWelcomePayload) => void): () => void { | ||
| 15 | welcomeListeners.add(listener); | ||
| 16 | |||
| 17 | // Replay cached welcome for late subscribers | ||
| 18 | if (lastWelcome) { | ||
| 19 | try { | ||
| 20 | listener(lastWelcome); | ||
| 21 | } catch { | ||
| 22 | // Swallow listener errors | ||
| 23 | } | ||
| 24 | } | ||
| 25 | |||
| 26 | return () => { | ||
| 27 | welcomeListeners.delete(listener); | ||
| 28 | }; | ||
| 29 | } | ||
| 30 | |||
| 31 | export function createWsNativeApi(): NativeApi { | ||
| 32 | if (instance) return instance.api; | ||
| 33 | |||
| 34 | const transport = new WsTransport(); | ||
| 35 | |||
| 36 | // Listen for server welcome and forward to registered listeners. | ||
| 37 | // Also cache it so late subscribers (React effects) get it immediately. | ||
| 38 | transport.subscribe(WS_CHANNELS.serverWelcome, (data) => { | ||
| 39 | const payload = data as WsWelcomePayload; | ||
| 40 | lastWelcome = payload; | ||
| 41 | for (const listener of welcomeListeners) { | ||
| 42 | try { | ||
| 43 | listener(payload); | ||
| 44 | } catch { | ||
| 45 | // Swallow listener errors | ||
| 46 | } | ||
| 47 | } | ||
| 48 | }); | ||
| 49 | |||
| 50 | const api: NativeApi = { | ||
| 51 | todos: { | ||
| 52 | list: async () => [], | ||
| 53 | add: async () => [], | ||
| 54 | toggle: async () => [], | ||
| 55 | remove: async () => [], | ||
| 56 | }, | ||
| 57 | dialogs: { | ||
| 58 | pickFolder: async () => { | ||
| 59 | if (!window.desktopBridge) return null; | ||
| 60 | return window.desktopBridge.pickFolder(); | ||
| 61 | }, | ||
| 62 | }, | ||
| 63 | terminal: { | ||
| 64 | run: async () => ({ | ||
| 65 | stdout: "", | ||
| 66 | stderr: "Terminal not available in web mode", | ||
| 67 | code: 1, | ||
| 68 | signal: null, | ||
| 69 | timedOut: false, | ||
| 70 | }), | ||
| 71 | }, | ||
| 72 | agent: { | ||
| 73 | spawn: async () => "", | ||
| 74 | kill: async () => {}, | ||
| 75 | write: async () => {}, | ||
| 76 | onOutput: () => () => {}, | ||
| 77 | onExit: () => () => {}, | ||
| 78 | }, | ||
| 79 | providers: { | ||
| 80 | startSession: (input) => transport.request(WS_METHODS.providersStartSession, input), | ||
| 81 | sendTurn: (input) => transport.request(WS_METHODS.providersSendTurn, input), | ||
| 82 | interruptTurn: (input) => transport.request(WS_METHODS.providersInterruptTurn, input), | ||
| 83 | respondToRequest: (input) => transport.request(WS_METHODS.providersRespondToRequest, input), | ||
| 84 | stopSession: (input) => transport.request(WS_METHODS.providersStopSession, input), | ||
| 85 | listSessions: () => transport.request(WS_METHODS.providersListSessions), | ||
| 86 | onEvent: (callback) => | ||
| 87 | transport.subscribe(WS_CHANNELS.providerEvent, callback as (data: unknown) => void), | ||
| 88 | }, | ||
| 89 | projects: { | ||
| 90 | list: () => transport.request(WS_METHODS.projectsList), | ||
| 91 | add: (input) => transport.request(WS_METHODS.projectsAdd, input), | ||
| 92 | remove: (input) => transport.request(WS_METHODS.projectsRemove, input), | ||
| 93 | }, | ||
| 94 | shell: { | ||
| 95 | openInEditor: (cwd, editor) => | ||
| 96 | transport.request(WS_METHODS.shellOpenInEditor, { cwd, editor }), | ||
| 97 | }, | ||
| 98 | }; | ||
| 99 | |||
| 100 | instance = { api, transport }; | ||
| 101 | return api; | ||
| 102 | } | ||
apps/renderer/src/wsTransport.ts created+197| ... | @@ -0,0 +1,197 @@ | ||
| 1 | import type { WsPush, WsRequest, WsResponse } from "@acme/contracts"; | ||
| 2 | |||
| 3 | type PushListener = (data: unknown) => void; | ||
| 4 | |||
| 5 | interface PendingRequest { | ||
| 6 | resolve: (result: unknown) => void; | ||
| 7 | reject: (error: Error) => void; | ||
| 8 | timeout: ReturnType<typeof setTimeout>; | ||
| 9 | } | ||
| 10 | |||
| 11 | const REQUEST_TIMEOUT_MS = 60_000; | ||
| 12 | const RECONNECT_DELAYS_MS = [500, 1_000, 2_000, 4_000, 8_000]; | ||
| 13 | |||
| 14 | export class WsTransport { | ||
| 15 | private ws: WebSocket | null = null; | ||
| 16 | private nextId = 1; | ||
| 17 | private readonly pending = new Map<string, PendingRequest>(); | ||
| 18 | private readonly listeners = new Map<string, Set<PushListener>>(); | ||
| 19 | private reconnectAttempt = 0; | ||
| 20 | private reconnectTimer: ReturnType<typeof setTimeout> | null = null; | ||
| 21 | private disposed = false; | ||
| 22 | private readonly url: string; | ||
| 23 | |||
| 24 | constructor(url?: string) { | ||
| 25 | const bridgeUrl = window.desktopBridge?.getWsUrl(); | ||
| 26 | // In dev mode, VITE_WS_URL points to the server's WebSocket endpoint. | ||
| 27 | // In production, the page is served by the WS server on the same host:port. | ||
| 28 | const envUrl = import.meta.env.VITE_WS_URL as string | undefined; | ||
| 29 | this.url = | ||
| 30 | url ?? | ||
| 31 | (bridgeUrl && bridgeUrl.length > 0 | ||
| 32 | ? bridgeUrl | ||
| 33 | : envUrl && envUrl.length > 0 | ||
| 34 | ? envUrl | ||
| 35 | : `ws://${window.location.hostname}:${window.location.port}`); | ||
| 36 | this.connect(); | ||
| 37 | } | ||
| 38 | |||
| 39 | async request<T = unknown>(method: string, params?: unknown): Promise<T> { | ||
| 40 | const id = String(this.nextId++); | ||
| 41 | const message: WsRequest = { id, method, ...(params !== undefined ? { params } : {}) }; | ||
| 42 | |||
| 43 | return new Promise<T>((resolve, reject) => { | ||
| 44 | const timeout = setTimeout(() => { | ||
| 45 | this.pending.delete(id); | ||
| 46 | reject(new Error(`Request timed out: ${method}`)); | ||
| 47 | }, REQUEST_TIMEOUT_MS); | ||
| 48 | |||
| 49 | this.pending.set(id, { | ||
| 50 | resolve: resolve as (result: unknown) => void, | ||
| 51 | reject, | ||
| 52 | timeout, | ||
| 53 | }); | ||
| 54 | |||
| 55 | this.send(message); | ||
| 56 | }); | ||
| 57 | } | ||
| 58 | |||
| 59 | subscribe(channel: string, listener: PushListener): () => void { | ||
| 60 | let channelListeners = this.listeners.get(channel); | ||
| 61 | if (!channelListeners) { | ||
| 62 | channelListeners = new Set(); | ||
| 63 | this.listeners.set(channel, channelListeners); | ||
| 64 | } | ||
| 65 | channelListeners.add(listener); | ||
| 66 | |||
| 67 | return () => { | ||
| 68 | channelListeners!.delete(listener); | ||
| 69 | if (channelListeners!.size === 0) { | ||
| 70 | this.listeners.delete(channel); | ||
| 71 | } | ||
| 72 | }; | ||
| 73 | } | ||
| 74 | |||
| 75 | dispose() { | ||
| 76 | this.disposed = true; | ||
| 77 | if (this.reconnectTimer !== null) { | ||
| 78 | clearTimeout(this.reconnectTimer); | ||
| 79 | this.reconnectTimer = null; | ||
| 80 | } | ||
| 81 | for (const pending of this.pending.values()) { | ||
| 82 | clearTimeout(pending.timeout); | ||
| 83 | pending.reject(new Error("Transport disposed")); | ||
| 84 | } | ||
| 85 | this.pending.clear(); | ||
| 86 | this.ws?.close(); | ||
| 87 | this.ws = null; | ||
| 88 | } | ||
| 89 | |||
| 90 | private connect() { | ||
| 91 | if (this.disposed) return; | ||
| 92 | |||
| 93 | const ws = new WebSocket(this.url); | ||
| 94 | |||
| 95 | ws.addEventListener("open", () => { | ||
| 96 | this.ws = ws; | ||
| 97 | this.reconnectAttempt = 0; | ||
| 98 | }); | ||
| 99 | |||
| 100 | ws.addEventListener("message", (event) => { | ||
| 101 | this.handleMessage(event.data); | ||
| 102 | }); | ||
| 103 | |||
| 104 | ws.addEventListener("close", () => { | ||
| 105 | this.ws = null; | ||
| 106 | this.scheduleReconnect(); | ||
| 107 | }); | ||
| 108 | |||
| 109 | ws.addEventListener("error", () => { | ||
| 110 | // close event will fire after error | ||
| 111 | }); | ||
| 112 | } | ||
| 113 | |||
| 114 | private handleMessage(raw: unknown) { | ||
| 115 | let parsed: unknown; | ||
| 116 | try { | ||
| 117 | parsed = JSON.parse(String(raw)); | ||
| 118 | } catch { | ||
| 119 | return; | ||
| 120 | } | ||
| 121 | |||
| 122 | if (!parsed || typeof parsed !== "object") return; | ||
| 123 | |||
| 124 | const message = parsed as Record<string, unknown>; | ||
| 125 | |||
| 126 | // Push event | ||
| 127 | if (message.type === "push") { | ||
| 128 | const push = message as unknown as WsPush; | ||
| 129 | const channelListeners = this.listeners.get(push.channel); | ||
| 130 | if (channelListeners) { | ||
| 131 | for (const listener of channelListeners) { | ||
| 132 | try { | ||
| 133 | listener(push.data); | ||
| 134 | } catch { | ||
| 135 | // Swallow listener errors | ||
| 136 | } | ||
| 137 | } | ||
| 138 | } | ||
| 139 | return; | ||
| 140 | } | ||
| 141 | |||
| 142 | // Response to a request | ||
| 143 | if (typeof message.id === "string") { | ||
| 144 | const response = message as unknown as WsResponse; | ||
| 145 | const pending = this.pending.get(response.id); | ||
| 146 | if (!pending) return; | ||
| 147 | |||
| 148 | clearTimeout(pending.timeout); | ||
| 149 | this.pending.delete(response.id); | ||
| 150 | |||
| 151 | if (response.error) { | ||
| 152 | pending.reject(new Error(response.error.message)); | ||
| 153 | } else { | ||
| 154 | pending.resolve(response.result); | ||
| 155 | } | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | private send(message: WsRequest) { | ||
| 160 | if (this.ws?.readyState === WebSocket.OPEN) { | ||
| 161 | this.ws.send(JSON.stringify(message)); | ||
| 162 | return; | ||
| 163 | } | ||
| 164 | |||
| 165 | // If not connected, wait for connection | ||
| 166 | const waitForOpen = () => { | ||
| 167 | const check = setInterval(() => { | ||
| 168 | if (this.disposed) { | ||
| 169 | clearInterval(check); | ||
| 170 | return; | ||
| 171 | } | ||
| 172 | if (this.ws?.readyState === WebSocket.OPEN) { | ||
| 173 | clearInterval(check); | ||
| 174 | this.ws.send(JSON.stringify(message)); | ||
| 175 | } | ||
| 176 | }, 50); | ||
| 177 | |||
| 178 | // Give up after timeout (the pending request will time out on its own) | ||
| 179 | setTimeout(() => clearInterval(check), REQUEST_TIMEOUT_MS); | ||
| 180 | }; | ||
| 181 | waitForOpen(); | ||
| 182 | } | ||
| 183 | |||
| 184 | private scheduleReconnect() { | ||
| 185 | if (this.disposed) return; | ||
| 186 | |||
| 187 | const delay = | ||
| 188 | RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)] ?? | ||
| 189 | RECONNECT_DELAYS_MS[0]!; | ||
| 190 | |||
| 191 | this.reconnectAttempt++; | ||
| 192 | this.reconnectTimer = setTimeout(() => { | ||
| 193 | this.reconnectTimer = null; | ||
| 194 | this.connect(); | ||
| 195 | }, delay); | ||
| 196 | } | ||
| 197 | } | ||
apps/renderer/vite.config.ts+7-1| ... | @@ -11,8 +11,14 @@ export default defineConfig({ | ... | @@ -11,8 +11,14 @@ export default defineConfig({ |
| 11 | }), | 11 | }), |
| 12 | tailwindcss(), | 12 | tailwindcss(), |
| 13 | ], | 13 | ], |
| 14 | define: { | ||
| 15 | // In dev mode, tell the renderer where the WebSocket server lives | ||
| 16 | "import.meta.env.VITE_WS_URL": JSON.stringify( | ||
| 17 | process.env.VITE_WS_URL ?? "", | ||
| 18 | ), | ||
| 19 | }, | ||
| 14 | server: { | 20 | server: { |
| 15 | port: Number(process.env.ELECTRON_RENDERER_PORT ?? 5173), | 21 | port: Number(process.env.PORT ?? 5173), |
| 16 | strictPort: true, | 22 | strictPort: true, |
| 17 | }, | 23 | }, |
| 18 | build: { | 24 | build: { |
apps/server/package.json created+34| ... | @@ -0,0 +1,34 @@ | ||
| 1 | { | ||
| 2 | "name": "t3", | ||
| 3 | "version": "0.1.0", | ||
| 4 | "bin": { | ||
| 5 | "t3": "./dist/index.js" | ||
| 6 | }, | ||
| 7 | "files": [ | ||
| 8 | "dist" | ||
| 9 | ], | ||
| 10 | "type": "module", | ||
| 11 | "main": "./dist/index.js", | ||
| 12 | "scripts": { | ||
| 13 | "dev": "VITE_DEV_SERVER_URL=http://localhost:5173 tsx src/index.ts", | ||
| 14 | "dev:desktop": "CODETHING_MODE=desktop CODETHING_NO_BROWSER=1 tsx src/index.ts", | ||
| 15 | "build": "tsup && node scripts/bundle-client.mjs", | ||
| 16 | "start": "node dist/index.js", | ||
| 17 | "start:desktop": "CODETHING_MODE=desktop CODETHING_NO_BROWSER=1 node dist/index.js", | ||
| 18 | "typecheck": "tsc --noEmit", | ||
| 19 | "test": "vitest run" | ||
| 20 | }, | ||
| 21 | "dependencies": { | ||
| 22 | "open": "^10.1.0", | ||
| 23 | "ws": "^8.18.0" | ||
| 24 | }, | ||
| 25 | "devDependencies": { | ||
| 26 | "@acme/contracts": "workspace:*", | ||
| 27 | "@acme/renderer": "workspace:*", | ||
| 28 | "@types/node": "^22.10.2", | ||
| 29 | "@types/ws": "^8.5.13", | ||
| 30 | "tsup": "^8.3.5", | ||
| 31 | "tsx": "^4.19.0", | ||
| 32 | "typescript": "^5.7.3" | ||
| 33 | } | ||
| 34 | } | ||
apps/server/scripts/bundle-client.mjs created+21| ... | @@ -0,0 +1,21 @@ | ||
| 1 | /** | ||
| 2 | * Copies the built renderer into dist/client/ so the published npm package | ||
| 3 | * includes the web UI. This runs as a post-build step. | ||
| 4 | */ | ||
| 5 | import fs from "node:fs"; | ||
| 6 | import path from "node:path"; | ||
| 7 | import { fileURLToPath } from "node:url"; | ||
| 8 | |||
| 9 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
| 10 | const rendererDist = path.resolve(__dirname, "../../renderer/dist"); | ||
| 11 | const target = path.resolve(__dirname, "../dist/client"); | ||
| 12 | |||
| 13 | if (!fs.existsSync(rendererDist)) { | ||
| 14 | console.log( | ||
| 15 | "⚠ Renderer dist not found — skipping client bundle. Run `bun run --cwd apps/renderer build` first.", | ||
| 16 | ); | ||
| 17 | process.exit(0); | ||
| 18 | } | ||
| 19 | |||
| 20 | fs.cpSync(rendererDist, target, { recursive: true }); | ||
| 21 | console.log("✓ Bundled renderer into dist/client"); | ||
apps/server/src/codexAppServerManager.test.ts created+81| ... | @@ -0,0 +1,81 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | classifyCodexStderrLine, | ||
| 5 | isRecoverableThreadResumeError, | ||
| 6 | normalizeCodexModelSlug, | ||
| 7 | } from "./codexAppServerManager"; | ||
| 8 | |||
| 9 | describe("classifyCodexStderrLine", () => { | ||
| 10 | it("ignores empty lines", () => { | ||
| 11 | expect(classifyCodexStderrLine(" ")).toBeNull(); | ||
| 12 | }); | ||
| 13 | |||
| 14 | it("ignores non-error structured codex logs", () => { | ||
| 15 | const line = | ||
| 16 | "2026-02-08T04:24:19.241256Z WARN codex_core::features: unknown feature key in config: skills"; | ||
| 17 | expect(classifyCodexStderrLine(line)).toBeNull(); | ||
| 18 | }); | ||
| 19 | |||
| 20 | it("ignores known benign rollout path errors", () => { | ||
| 21 | const line = | ||
| 22 | "\u001b[2m2026-02-08T04:24:20.085687Z\u001b[0m \u001b[31mERROR\u001b[0m \u001b[2mcodex_core::rollout::list\u001b[0m: state db missing rollout path for thread 019c3b6c-46b8-7b70-ad23-82f824d161fb"; | ||
| 23 | expect(classifyCodexStderrLine(line)).toBeNull(); | ||
| 24 | }); | ||
| 25 | |||
| 26 | it("keeps unknown structured errors", () => { | ||
| 27 | const line = "2026-02-08T04:24:20.085687Z ERROR codex_core::runtime: unrecoverable failure"; | ||
| 28 | expect(classifyCodexStderrLine(line)).toEqual({ | ||
| 29 | message: line, | ||
| 30 | }); | ||
| 31 | }); | ||
| 32 | |||
| 33 | it("keeps plain stderr messages", () => { | ||
| 34 | const line = "fatal: permission denied"; | ||
| 35 | expect(classifyCodexStderrLine(line)).toEqual({ | ||
| 36 | message: line, | ||
| 37 | }); | ||
| 38 | }); | ||
| 39 | }); | ||
| 40 | |||
| 41 | describe("normalizeCodexModelSlug", () => { | ||
| 42 | it("maps 5.3 aliases to gpt-5.3-codex", () => { | ||
| 43 | expect(normalizeCodexModelSlug("5.3")).toBe("gpt-5.3-codex"); | ||
| 44 | expect(normalizeCodexModelSlug("gpt-5.3")).toBe("gpt-5.3-codex"); | ||
| 45 | }); | ||
| 46 | |||
| 47 | it("prefers codex id when model differs", () => { | ||
| 48 | expect(normalizeCodexModelSlug("gpt-5.3", "gpt-5.3-codex")).toBe("gpt-5.3-codex"); | ||
| 49 | }); | ||
| 50 | |||
| 51 | it("keeps non-aliased models as-is", () => { | ||
| 52 | expect(normalizeCodexModelSlug("gpt-5.2-codex")).toBe("gpt-5.2-codex"); | ||
| 53 | expect(normalizeCodexModelSlug("gpt-5.2")).toBe("gpt-5.2"); | ||
| 54 | }); | ||
| 55 | }); | ||
| 56 | |||
| 57 | describe("isRecoverableThreadResumeError", () => { | ||
| 58 | it("matches not-found resume errors", () => { | ||
| 59 | expect( | ||
| 60 | isRecoverableThreadResumeError( | ||
| 61 | new Error("thread/resume failed: thread not found"), | ||
| 62 | ), | ||
| 63 | ).toBe(true); | ||
| 64 | }); | ||
| 65 | |||
| 66 | it("ignores non-resume errors", () => { | ||
| 67 | expect( | ||
| 68 | isRecoverableThreadResumeError( | ||
| 69 | new Error("thread/start failed: permission denied"), | ||
| 70 | ), | ||
| 71 | ).toBe(false); | ||
| 72 | }); | ||
| 73 | |||
| 74 | it("ignores non-recoverable resume errors", () => { | ||
| 75 | expect( | ||
| 76 | isRecoverableThreadResumeError( | ||
| 77 | new Error("thread/resume failed: timed out waiting for server"), | ||
| 78 | ), | ||
| 79 | ).toBe(false); | ||
| 80 | }); | ||
| 81 | }); | ||
apps/server/src/codexAppServerManager.ts created+847| ... | @@ -0,0 +1,847 @@ | ||
| 1 | import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; | ||
| 2 | import { randomUUID } from "node:crypto"; | ||
| 3 | import { EventEmitter } from "node:events"; | ||
| 4 | import readline from "node:readline"; | ||
| 5 | |||
| 6 | import type { | ||
| 7 | ProviderApprovalDecision, | ||
| 8 | ProviderEvent, | ||
| 9 | ProviderRequestKind, | ||
| 10 | ProviderSendTurnInput, | ||
| 11 | ProviderSession, | ||
| 12 | ProviderSessionStartInput, | ||
| 13 | ProviderTurnStartResult, | ||
| 14 | } from "@acme/contracts"; | ||
| 15 | import { normalizeModelSlug } from "@acme/contracts"; | ||
| 16 | |||
| 17 | type PendingRequestKey = string; | ||
| 18 | |||
| 19 | interface PendingRequest { | ||
| 20 | method: string; | ||
| 21 | timeout: ReturnType<typeof setTimeout>; | ||
| 22 | resolve: (value: unknown) => void; | ||
| 23 | reject: (error: Error) => void; | ||
| 24 | } | ||
| 25 | |||
| 26 | interface PendingApprovalRequest { | ||
| 27 | requestId: string; | ||
| 28 | jsonRpcId: string | number; | ||
| 29 | method: | ||
| 30 | | "item/commandExecution/requestApproval" | ||
| 31 | | "item/fileChange/requestApproval"; | ||
| 32 | requestKind: ProviderRequestKind; | ||
| 33 | threadId?: string; | ||
| 34 | turnId?: string; | ||
| 35 | itemId?: string; | ||
| 36 | } | ||
| 37 | |||
| 38 | interface CodexSessionContext { | ||
| 39 | session: ProviderSession; | ||
| 40 | child: ChildProcessWithoutNullStreams; | ||
| 41 | output: readline.Interface; | ||
| 42 | pending: Map<PendingRequestKey, PendingRequest>; | ||
| 43 | pendingApprovals: Map<string, PendingApprovalRequest>; | ||
| 44 | nextRequestId: number; | ||
| 45 | stopping: boolean; | ||
| 46 | } | ||
| 47 | |||
| 48 | interface JsonRpcError { | ||
| 49 | code?: number; | ||
| 50 | message?: string; | ||
| 51 | } | ||
| 52 | |||
| 53 | interface JsonRpcRequest { | ||
| 54 | id: string | number; | ||
| 55 | method: string; | ||
| 56 | params?: unknown; | ||
| 57 | } | ||
| 58 | |||
| 59 | interface JsonRpcResponse { | ||
| 60 | id: string | number; | ||
| 61 | result?: unknown; | ||
| 62 | error?: JsonRpcError; | ||
| 63 | } | ||
| 64 | |||
| 65 | interface JsonRpcNotification { | ||
| 66 | method: string; | ||
| 67 | params?: unknown; | ||
| 68 | } | ||
| 69 | |||
| 70 | const ANSI_ESCAPE_CHAR = String.fromCharCode(27); | ||
| 71 | const ANSI_ESCAPE_REGEX = new RegExp(`${ANSI_ESCAPE_CHAR}\\[[0-9;]*m`, "g"); | ||
| 72 | const CODEX_STDERR_LOG_REGEX = | ||
| 73 | /^\d{4}-\d{2}-\d{2}T\S+\s+(TRACE|DEBUG|INFO|WARN|ERROR)\s+\S+:\s+(.*)$/; | ||
| 74 | const BENIGN_ERROR_LOG_SNIPPETS = [ | ||
| 75 | "state db missing rollout path for thread", | ||
| 76 | "state db record_discrepancy: find_thread_path_by_id_str_in_subdir, falling_back", | ||
| 77 | ]; | ||
| 78 | const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [ | ||
| 79 | "not found", | ||
| 80 | "missing thread", | ||
| 81 | "no such thread", | ||
| 82 | "unknown thread", | ||
| 83 | "does not exist", | ||
| 84 | ]; | ||
| 85 | |||
| 86 | export function normalizeCodexModelSlug( | ||
| 87 | model: string | undefined | null, | ||
| 88 | preferredId?: string, | ||
| 89 | ): string | undefined { | ||
| 90 | const normalized = normalizeModelSlug(model); | ||
| 91 | if (!normalized) { | ||
| 92 | return undefined; | ||
| 93 | } | ||
| 94 | |||
| 95 | if (preferredId?.endsWith("-codex") && preferredId !== normalized) { | ||
| 96 | return preferredId; | ||
| 97 | } | ||
| 98 | |||
| 99 | return normalized; | ||
| 100 | } | ||
| 101 | |||
| 102 | export function classifyCodexStderrLine(rawLine: string): { message: string } | null { | ||
| 103 | const line = rawLine.replaceAll(ANSI_ESCAPE_REGEX, "").trim(); | ||
| 104 | if (!line) { | ||
| 105 | return null; | ||
| 106 | } | ||
| 107 | |||
| 108 | const match = line.match(CODEX_STDERR_LOG_REGEX); | ||
| 109 | if (match) { | ||
| 110 | const level = match[1]; | ||
| 111 | if (level && level !== "ERROR") { | ||
| 112 | return null; | ||
| 113 | } | ||
| 114 | |||
| 115 | const isBenignError = BENIGN_ERROR_LOG_SNIPPETS.some((snippet) => line.includes(snippet)); | ||
| 116 | if (isBenignError) { | ||
| 117 | return null; | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | return { message: line }; | ||
| 122 | } | ||
| 123 | |||
| 124 | export function isRecoverableThreadResumeError(error: unknown): boolean { | ||
| 125 | const message = ( | ||
| 126 | error instanceof Error ? error.message : String(error) | ||
| 127 | ).toLowerCase(); | ||
| 128 | if (!message.includes("thread/resume")) { | ||
| 129 | return false; | ||
| 130 | } | ||
| 131 | |||
| 132 | return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => | ||
| 133 | message.includes(snippet), | ||
| 134 | ); | ||
| 135 | } | ||
| 136 | |||
| 137 | export interface CodexAppServerManagerEvents { | ||
| 138 | event: [event: ProviderEvent]; | ||
| 139 | } | ||
| 140 | |||
| 141 | export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEvents> { | ||
| 142 | private readonly sessions = new Map<string, CodexSessionContext>(); | ||
| 143 | |||
| 144 | async startSession(input: ProviderSessionStartInput): Promise<ProviderSession> { | ||
| 145 | const sessionId = randomUUID(); | ||
| 146 | const now = new Date().toISOString(); | ||
| 147 | |||
| 148 | const session: ProviderSession = { | ||
| 149 | sessionId, | ||
| 150 | provider: "codex", | ||
| 151 | status: "connecting", | ||
| 152 | model: normalizeCodexModelSlug(input.model), | ||
| 153 | cwd: input.cwd, | ||
| 154 | createdAt: now, | ||
| 155 | updatedAt: now, | ||
| 156 | }; | ||
| 157 | |||
| 158 | const child = spawn("codex", ["app-server"], { | ||
| 159 | cwd: input.cwd, | ||
| 160 | env: process.env, | ||
| 161 | stdio: ["pipe", "pipe", "pipe"], | ||
| 162 | }); | ||
| 163 | const output = readline.createInterface({ input: child.stdout }); | ||
| 164 | |||
| 165 | const context: CodexSessionContext = { | ||
| 166 | session, | ||
| 167 | child, | ||
| 168 | output, | ||
| 169 | pending: new Map(), | ||
| 170 | pendingApprovals: new Map(), | ||
| 171 | nextRequestId: 1, | ||
| 172 | stopping: false, | ||
| 173 | }; | ||
| 174 | |||
| 175 | this.sessions.set(sessionId, context); | ||
| 176 | this.attachProcessListeners(context); | ||
| 177 | |||
| 178 | this.emitLifecycleEvent(context, "session/connecting", "Starting codex app-server"); | ||
| 179 | |||
| 180 | try { | ||
| 181 | await this.sendRequest(context, "initialize", { | ||
| 182 | clientInfo: { | ||
| 183 | name: "codething_desktop", | ||
| 184 | title: "CodeThing Desktop", | ||
| 185 | version: "0.1.0", | ||
| 186 | }, | ||
| 187 | capabilities: { | ||
| 188 | experimentalApi: false, | ||
| 189 | }, | ||
| 190 | }); | ||
| 191 | |||
| 192 | this.writeMessage(context, { method: "initialized" }); | ||
| 193 | |||
| 194 | const normalizedModel = normalizeCodexModelSlug(input.model); | ||
| 195 | const sessionOverrides = { | ||
| 196 | model: normalizedModel ?? null, | ||
| 197 | cwd: input.cwd ?? null, | ||
| 198 | approvalPolicy: input.approvalPolicy, | ||
| 199 | sandbox: input.sandboxMode, | ||
| 200 | }; | ||
| 201 | |||
| 202 | const threadStartParams = { | ||
| 203 | ...sessionOverrides, | ||
| 204 | experimentalRawEvents: false, | ||
| 205 | }; | ||
| 206 | |||
| 207 | let threadOpenMethod: "thread/start" | "thread/resume" = "thread/start"; | ||
| 208 | let threadOpenResponse: unknown; | ||
| 209 | if (input.resumeThreadId) { | ||
| 210 | try { | ||
| 211 | threadOpenMethod = "thread/resume"; | ||
| 212 | threadOpenResponse = await this.sendRequest( | ||
| 213 | context, | ||
| 214 | "thread/resume", | ||
| 215 | { | ||
| 216 | ...sessionOverrides, | ||
| 217 | threadId: input.resumeThreadId, | ||
| 218 | }, | ||
| 219 | ); | ||
| 220 | } catch (error) { | ||
| 221 | if (!isRecoverableThreadResumeError(error)) { | ||
| 222 | throw error; | ||
| 223 | } | ||
| 224 | |||
| 225 | threadOpenMethod = "thread/start"; | ||
| 226 | this.emitLifecycleEvent( | ||
| 227 | context, | ||
| 228 | "session/threadResumeFallback", | ||
| 229 | `Could not resume thread ${input.resumeThreadId}; started a new thread instead.`, | ||
| 230 | ); | ||
| 231 | threadOpenResponse = await this.sendRequest( | ||
| 232 | context, | ||
| 233 | "thread/start", | ||
| 234 | threadStartParams, | ||
| 235 | ); | ||
| 236 | } | ||
| 237 | } else { | ||
| 238 | threadOpenMethod = "thread/start"; | ||
| 239 | threadOpenResponse = await this.sendRequest( | ||
| 240 | context, | ||
| 241 | "thread/start", | ||
| 242 | threadStartParams, | ||
| 243 | ); | ||
| 244 | } | ||
| 245 | |||
| 246 | const threadOpenRecord = this.readObject(threadOpenResponse); | ||
| 247 | const threadId = | ||
| 248 | this.readString(this.readObject(threadOpenRecord, "thread"), "id") ?? | ||
| 249 | this.readString(threadOpenRecord, "threadId"); | ||
| 250 | if (!threadId) { | ||
| 251 | throw new Error( | ||
| 252 | `${threadOpenMethod} response did not include a thread id.`, | ||
| 253 | ); | ||
| 254 | } | ||
| 255 | |||
| 256 | this.updateSession(context, { | ||
| 257 | status: "ready", | ||
| 258 | threadId, | ||
| 259 | }); | ||
| 260 | this.emitLifecycleEvent(context, "session/ready", `Connected to thread ${threadId}`); | ||
| 261 | return { ...context.session }; | ||
| 262 | } catch (error) { | ||
| 263 | const message = error instanceof Error ? error.message : "Failed to start Codex session."; | ||
| 264 | this.updateSession(context, { | ||
| 265 | status: "error", | ||
| 266 | lastError: message, | ||
| 267 | }); | ||
| 268 | this.emitErrorEvent(context, "session/startFailed", message); | ||
| 269 | this.stopSession(sessionId); | ||
| 270 | throw new Error(message, { cause: error }); | ||
| 271 | } | ||
| 272 | } | ||
| 273 | |||
| 274 | async sendTurn(input: ProviderSendTurnInput): Promise<ProviderTurnStartResult> { | ||
| 275 | const context = this.requireSession(input.sessionId); | ||
| 276 | if (!context.session.threadId) { | ||
| 277 | throw new Error("Session is missing a thread id."); | ||
| 278 | } | ||
| 279 | |||
| 280 | const turnStartParams: { | ||
| 281 | threadId: string; | ||
| 282 | input: Array<{ type: "text"; text: string; text_elements: [] }>; | ||
| 283 | model?: string; | ||
| 284 | effort?: string; | ||
| 285 | } = { | ||
| 286 | threadId: context.session.threadId, | ||
| 287 | input: [ | ||
| 288 | { | ||
| 289 | type: "text", | ||
| 290 | text: input.input, | ||
| 291 | text_elements: [], | ||
| 292 | }, | ||
| 293 | ], | ||
| 294 | }; | ||
| 295 | const normalizedModel = normalizeCodexModelSlug(input.model); | ||
| 296 | if (normalizedModel) { | ||
| 297 | turnStartParams.model = normalizedModel; | ||
| 298 | } | ||
| 299 | if (input.effort) { | ||
| 300 | turnStartParams.effort = input.effort; | ||
| 301 | } | ||
| 302 | |||
| 303 | const response = await this.sendRequest(context, "turn/start", turnStartParams); | ||
| 304 | |||
| 305 | const turn = this.readObject(this.readObject(response), "turn"); | ||
| 306 | const turnId = this.readString(turn, "id"); | ||
| 307 | if (!turnId) { | ||
| 308 | throw new Error("turn/start response did not include a turn id."); | ||
| 309 | } | ||
| 310 | |||
| 311 | this.updateSession(context, { | ||
| 312 | status: "running", | ||
| 313 | activeTurnId: turnId, | ||
| 314 | }); | ||
| 315 | |||
| 316 | return { | ||
| 317 | threadId: context.session.threadId, | ||
| 318 | turnId, | ||
| 319 | }; | ||
| 320 | } | ||
| 321 | |||
| 322 | async interruptTurn(sessionId: string, turnId?: string): Promise<void> { | ||
| 323 | const context = this.requireSession(sessionId); | ||
| 324 | const effectiveTurnId = turnId ?? context.session.activeTurnId; | ||
| 325 | |||
| 326 | if (!effectiveTurnId || !context.session.threadId) { | ||
| 327 | return; | ||
| 328 | } | ||
| 329 | |||
| 330 | await this.sendRequest(context, "turn/interrupt", { | ||
| 331 | threadId: context.session.threadId, | ||
| 332 | turnId: effectiveTurnId, | ||
| 333 | }); | ||
| 334 | } | ||
| 335 | |||
| 336 | async respondToRequest( | ||
| 337 | sessionId: string, | ||
| 338 | requestId: string, | ||
| 339 | decision: ProviderApprovalDecision, | ||
| 340 | ): Promise<void> { | ||
| 341 | const context = this.requireSession(sessionId); | ||
| 342 | const pendingRequest = context.pendingApprovals.get(requestId); | ||
| 343 | if (!pendingRequest) { | ||
| 344 | throw new Error(`Unknown pending approval request: ${requestId}`); | ||
| 345 | } | ||
| 346 | |||
| 347 | context.pendingApprovals.delete(requestId); | ||
| 348 | this.writeMessage(context, { | ||
| 349 | id: pendingRequest.jsonRpcId, | ||
| 350 | result: { | ||
| 351 | decision, | ||
| 352 | }, | ||
| 353 | }); | ||
| 354 | |||
| 355 | this.emitEvent({ | ||
| 356 | id: randomUUID(), | ||
| 357 | kind: "notification", | ||
| 358 | provider: "codex", | ||
| 359 | sessionId: context.session.sessionId, | ||
| 360 | createdAt: new Date().toISOString(), | ||
| 361 | method: "item/requestApproval/decision", | ||
| 362 | threadId: pendingRequest.threadId, | ||
| 363 | turnId: pendingRequest.turnId, | ||
| 364 | itemId: pendingRequest.itemId, | ||
| 365 | requestId: pendingRequest.requestId, | ||
| 366 | requestKind: pendingRequest.requestKind, | ||
| 367 | payload: { | ||
| 368 | requestId: pendingRequest.requestId, | ||
| 369 | requestKind: pendingRequest.requestKind, | ||
| 370 | decision, | ||
| 371 | }, | ||
| 372 | }); | ||
| 373 | } | ||
| 374 | |||
| 375 | stopSession(sessionId: string): void { | ||
| 376 | const context = this.sessions.get(sessionId); | ||
| 377 | if (!context) { | ||
| 378 | return; | ||
| 379 | } | ||
| 380 | |||
| 381 | context.stopping = true; | ||
| 382 | |||
| 383 | for (const pending of context.pending.values()) { | ||
| 384 | clearTimeout(pending.timeout); | ||
| 385 | pending.reject(new Error("Session stopped before request completed.")); | ||
| 386 | } | ||
| 387 | context.pending.clear(); | ||
| 388 | context.pendingApprovals.clear(); | ||
| 389 | |||
| 390 | context.output.close(); | ||
| 391 | |||
| 392 | if (!context.child.killed) { | ||
| 393 | context.child.kill(); | ||
| 394 | } | ||
| 395 | |||
| 396 | this.updateSession(context, { | ||
| 397 | status: "closed", | ||
| 398 | activeTurnId: undefined, | ||
| 399 | }); | ||
| 400 | this.emitLifecycleEvent(context, "session/closed", "Session stopped"); | ||
| 401 | this.sessions.delete(sessionId); | ||
| 402 | } | ||
| 403 | |||
| 404 | listSessions(): ProviderSession[] { | ||
| 405 | return Array.from(this.sessions.values(), ({ session }) => ({ | ||
| 406 | ...session, | ||
| 407 | })); | ||
| 408 | } | ||
| 409 | |||
| 410 | hasSession(sessionId: string): boolean { | ||
| 411 | return this.sessions.has(sessionId); | ||
| 412 | } | ||
| 413 | |||
| 414 | stopAll(): void { | ||
| 415 | for (const sessionId of this.sessions.keys()) { | ||
| 416 | this.stopSession(sessionId); | ||
| 417 | } | ||
| 418 | } | ||
| 419 | |||
| 420 | private requireSession(sessionId: string): CodexSessionContext { | ||
| 421 | const context = this.sessions.get(sessionId); | ||
| 422 | if (!context) { | ||
| 423 | throw new Error(`Unknown session: ${sessionId}`); | ||
| 424 | } | ||
| 425 | |||
| 426 | if (context.session.status === "closed") { | ||
| 427 | throw new Error(`Session is closed: ${sessionId}`); | ||
| 428 | } | ||
| 429 | |||
| 430 | return context; | ||
| 431 | } | ||
| 432 | |||
| 433 | private attachProcessListeners(context: CodexSessionContext): void { | ||
| 434 | context.output.on("line", (line) => { | ||
| 435 | this.handleStdoutLine(context, line); | ||
| 436 | }); | ||
| 437 | |||
| 438 | context.child.stderr.on("data", (chunk: Buffer) => { | ||
| 439 | const raw = chunk.toString(); | ||
| 440 | const lines = raw.split(/\r?\n/g); | ||
| 441 | for (const rawLine of lines) { | ||
| 442 | const classified = classifyCodexStderrLine(rawLine); | ||
| 443 | if (!classified) { | ||
| 444 | continue; | ||
| 445 | } | ||
| 446 | |||
| 447 | this.emitErrorEvent(context, "process/stderr", classified.message); | ||
| 448 | } | ||
| 449 | }); | ||
| 450 | |||
| 451 | context.child.on("error", (error) => { | ||
| 452 | const message = error.message || "codex app-server process errored."; | ||
| 453 | this.updateSession(context, { | ||
| 454 | status: "error", | ||
| 455 | lastError: message, | ||
| 456 | }); | ||
| 457 | this.emitErrorEvent(context, "process/error", message); | ||
| 458 | }); | ||
| 459 | |||
| 460 | context.child.on("exit", (code, signal) => { | ||
| 461 | if (context.stopping) { | ||
| 462 | return; | ||
| 463 | } | ||
| 464 | |||
| 465 | const message = `codex app-server exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`; | ||
| 466 | this.updateSession(context, { | ||
| 467 | status: "closed", | ||
| 468 | activeTurnId: undefined, | ||
| 469 | lastError: code === 0 ? context.session.lastError : message, | ||
| 470 | }); | ||
| 471 | this.emitLifecycleEvent(context, "session/exited", message); | ||
| 472 | this.sessions.delete(context.session.sessionId); | ||
| 473 | }); | ||
| 474 | } | ||
| 475 | |||
| 476 | private handleStdoutLine(context: CodexSessionContext, line: string): void { | ||
| 477 | let parsed: unknown; | ||
| 478 | try { | ||
| 479 | parsed = JSON.parse(line); | ||
| 480 | } catch { | ||
| 481 | this.emitErrorEvent( | ||
| 482 | context, | ||
| 483 | "protocol/parseError", | ||
| 484 | "Received invalid JSON from codex app-server.", | ||
| 485 | ); | ||
| 486 | return; | ||
| 487 | } | ||
| 488 | |||
| 489 | if (!parsed || typeof parsed !== "object") { | ||
| 490 | this.emitErrorEvent( | ||
| 491 | context, | ||
| 492 | "protocol/invalidMessage", | ||
| 493 | "Received non-object protocol message.", | ||
| 494 | ); | ||
| 495 | return; | ||
| 496 | } | ||
| 497 | |||
| 498 | if (this.isServerRequest(parsed)) { | ||
| 499 | this.handleServerRequest(context, parsed); | ||
| 500 | return; | ||
| 501 | } | ||
| 502 | |||
| 503 | if (this.isServerNotification(parsed)) { | ||
| 504 | this.handleServerNotification(context, parsed); | ||
| 505 | return; | ||
| 506 | } | ||
| 507 | |||
| 508 | if (this.isResponse(parsed)) { | ||
| 509 | this.handleResponse(context, parsed); | ||
| 510 | return; | ||
| 511 | } | ||
| 512 | |||
| 513 | this.emitErrorEvent( | ||
| 514 | context, | ||
| 515 | "protocol/unrecognizedMessage", | ||
| 516 | "Received protocol message in an unknown shape.", | ||
| 517 | ); | ||
| 518 | } | ||
| 519 | |||
| 520 | private handleServerNotification( | ||
| 521 | context: CodexSessionContext, | ||
| 522 | notification: JsonRpcNotification, | ||
| 523 | ): void { | ||
| 524 | const route = this.readRouteFields(notification.params); | ||
| 525 | const textDelta = | ||
| 526 | notification.method === "item/agentMessage/delta" | ||
| 527 | ? this.readString(notification.params, "delta") | ||
| 528 | : undefined; | ||
| 529 | |||
| 530 | this.emitEvent({ | ||
| 531 | id: randomUUID(), | ||
| 532 | kind: "notification", | ||
| 533 | provider: "codex", | ||
| 534 | sessionId: context.session.sessionId, | ||
| 535 | createdAt: new Date().toISOString(), | ||
| 536 | method: notification.method, | ||
| 537 | threadId: route.threadId, | ||
| 538 | turnId: route.turnId, | ||
| 539 | itemId: route.itemId, | ||
| 540 | textDelta, | ||
| 541 | payload: notification.params, | ||
| 542 | }); | ||
| 543 | |||
| 544 | if (notification.method === "thread/started") { | ||
| 545 | const threadId = this.readString(this.readObject(notification.params)?.thread, "id"); | ||
| 546 | if (threadId) { | ||
| 547 | this.updateSession(context, { threadId }); | ||
| 548 | } | ||
| 549 | return; | ||
| 550 | } | ||
| 551 | |||
| 552 | if (notification.method === "turn/started") { | ||
| 553 | const turnId = this.readString(this.readObject(notification.params)?.turn, "id"); | ||
| 554 | this.updateSession(context, { | ||
| 555 | status: "running", | ||
| 556 | activeTurnId: turnId, | ||
| 557 | }); | ||
| 558 | return; | ||
| 559 | } | ||
| 560 | |||
| 561 | if (notification.method === "turn/completed") { | ||
| 562 | const turn = this.readObject(notification.params, "turn"); | ||
| 563 | const status = this.readString(turn, "status"); | ||
| 564 | const errorMessage = this.readString(this.readObject(turn, "error"), "message"); | ||
| 565 | this.updateSession(context, { | ||
| 566 | status: status === "failed" ? "error" : "ready", | ||
| 567 | activeTurnId: undefined, | ||
| 568 | lastError: errorMessage ?? context.session.lastError, | ||
| 569 | }); | ||
| 570 | return; | ||
| 571 | } | ||
| 572 | |||
| 573 | if (notification.method === "error") { | ||
| 574 | const message = this.readString(this.readObject(notification.params)?.error, "message"); | ||
| 575 | const willRetry = this.readBoolean(notification.params, "willRetry"); | ||
| 576 | |||
| 577 | this.updateSession(context, { | ||
| 578 | status: willRetry ? "running" : "error", | ||
| 579 | lastError: message ?? context.session.lastError, | ||
| 580 | }); | ||
| 581 | } | ||
| 582 | } | ||
| 583 | |||
| 584 | private handleServerRequest(context: CodexSessionContext, request: JsonRpcRequest): void { | ||
| 585 | const route = this.readRouteFields(request.params); | ||
| 586 | const requestKind = this.requestKindForMethod(request.method); | ||
| 587 | let requestId: string | undefined; | ||
| 588 | if (requestKind) { | ||
| 589 | requestId = randomUUID(); | ||
| 590 | const pendingRequest: PendingApprovalRequest = { | ||
| 591 | requestId, | ||
| 592 | jsonRpcId: request.id, | ||
| 593 | method: | ||
| 594 | requestKind === "command" | ||
| 595 | ? "item/commandExecution/requestApproval" | ||
| 596 | : "item/fileChange/requestApproval", | ||
| 597 | requestKind, | ||
| 598 | ...(route.threadId ? { threadId: route.threadId } : {}), | ||
| 599 | ...(route.turnId ? { turnId: route.turnId } : {}), | ||
| 600 | ...(route.itemId ? { itemId: route.itemId } : {}), | ||
| 601 | }; | ||
| 602 | context.pendingApprovals.set(requestId, pendingRequest); | ||
| 603 | } | ||
| 604 | |||
| 605 | this.emitEvent({ | ||
| 606 | id: randomUUID(), | ||
| 607 | kind: "request", | ||
| 608 | provider: "codex", | ||
| 609 | sessionId: context.session.sessionId, | ||
| 610 | createdAt: new Date().toISOString(), | ||
| 611 | method: request.method, | ||
| 612 | threadId: route.threadId, | ||
| 613 | turnId: route.turnId, | ||
| 614 | itemId: route.itemId, | ||
| 615 | requestId, | ||
| 616 | requestKind, | ||
| 617 | payload: request.params, | ||
| 618 | }); | ||
| 619 | |||
| 620 | if (requestKind) { | ||
| 621 | return; | ||
| 622 | } | ||
| 623 | |||
| 624 | if (request.method === "item/tool/requestUserInput") { | ||
| 625 | this.writeMessage(context, { | ||
| 626 | id: request.id, | ||
| 627 | result: { answers: {} }, | ||
| 628 | }); | ||
| 629 | return; | ||
| 630 | } | ||
| 631 | |||
| 632 | this.writeMessage(context, { | ||
| 633 | id: request.id, | ||
| 634 | error: { | ||
| 635 | code: -32601, | ||
| 636 | message: `Unsupported server request: ${request.method}`, | ||
| 637 | }, | ||
| 638 | }); | ||
| 639 | } | ||
| 640 | |||
| 641 | private handleResponse(context: CodexSessionContext, response: JsonRpcResponse): void { | ||
| 642 | const key = String(response.id); | ||
| 643 | const pending = context.pending.get(key); | ||
| 644 | if (!pending) { | ||
| 645 | return; | ||
| 646 | } | ||
| 647 | |||
| 648 | clearTimeout(pending.timeout); | ||
| 649 | context.pending.delete(key); | ||
| 650 | |||
| 651 | if (response.error?.message) { | ||
| 652 | pending.reject(new Error(`${pending.method} failed: ${String(response.error.message)}`)); | ||
| 653 | return; | ||
| 654 | } | ||
| 655 | |||
| 656 | pending.resolve(response.result); | ||
| 657 | } | ||
| 658 | |||
| 659 | private async sendRequest<TResponse>( | ||
| 660 | context: CodexSessionContext, | ||
| 661 | method: string, | ||
| 662 | params: unknown, | ||
| 663 | timeoutMs = 20_000, | ||
| 664 | ): Promise<TResponse> { | ||
| 665 | const id = context.nextRequestId; | ||
| 666 | context.nextRequestId += 1; | ||
| 667 | |||
| 668 | const result = await new Promise<unknown>((resolve, reject) => { | ||
| 669 | const timeout = setTimeout(() => { | ||
| 670 | context.pending.delete(String(id)); | ||
| 671 | reject(new Error(`Timed out waiting for ${method}.`)); | ||
| 672 | }, timeoutMs); | ||
| 673 | |||
| 674 | context.pending.set(String(id), { | ||
| 675 | method, | ||
| 676 | timeout, | ||
| 677 | resolve, | ||
| 678 | reject, | ||
| 679 | }); | ||
| 680 | this.writeMessage(context, { | ||
| 681 | method, | ||
| 682 | id, | ||
| 683 | params, | ||
| 684 | }); | ||
| 685 | }); | ||
| 686 | |||
| 687 | return result as TResponse; | ||
| 688 | } | ||
| 689 | |||
| 690 | private writeMessage(context: CodexSessionContext, message: unknown): void { | ||
| 691 | const encoded = JSON.stringify(message); | ||
| 692 | if (!context.child.stdin.writable) { | ||
| 693 | throw new Error("Cannot write to codex app-server stdin."); | ||
| 694 | } | ||
| 695 | |||
| 696 | context.child.stdin.write(`${encoded}\n`); | ||
| 697 | } | ||
| 698 | |||
| 699 | private emitLifecycleEvent(context: CodexSessionContext, method: string, message: string): void { | ||
| 700 | this.emitEvent({ | ||
| 701 | id: randomUUID(), | ||
| 702 | kind: "session", | ||
| 703 | provider: "codex", | ||
| 704 | sessionId: context.session.sessionId, | ||
| 705 | createdAt: new Date().toISOString(), | ||
| 706 | method, | ||
| 707 | message, | ||
| 708 | }); | ||
| 709 | } | ||
| 710 | |||
| 711 | private emitErrorEvent(context: CodexSessionContext, method: string, message: string): void { | ||
| 712 | this.emitEvent({ | ||
| 713 | id: randomUUID(), | ||
| 714 | kind: "error", | ||
| 715 | provider: "codex", | ||
| 716 | sessionId: context.session.sessionId, | ||
| 717 | createdAt: new Date().toISOString(), | ||
| 718 | method, | ||
| 719 | message, | ||
| 720 | }); | ||
| 721 | } | ||
| 722 | |||
| 723 | private emitEvent(event: ProviderEvent): void { | ||
| 724 | this.emit("event", event); | ||
| 725 | } | ||
| 726 | |||
| 727 | private updateSession(context: CodexSessionContext, updates: Partial<ProviderSession>): void { | ||
| 728 | context.session = { | ||
| 729 | ...context.session, | ||
| 730 | ...updates, | ||
| 731 | updatedAt: new Date().toISOString(), | ||
| 732 | }; | ||
| 733 | } | ||
| 734 | |||
| 735 | private requestKindForMethod( | ||
| 736 | method: string, | ||
| 737 | ): ProviderRequestKind | undefined { | ||
| 738 | if (method === "item/commandExecution/requestApproval") { | ||
| 739 | return "command"; | ||
| 740 | } | ||
| 741 | |||
| 742 | if (method === "item/fileChange/requestApproval") { | ||
| 743 | return "file-change"; | ||
| 744 | } | ||
| 745 | |||
| 746 | return undefined; | ||
| 747 | } | ||
| 748 | |||
| 749 | private isServerRequest(value: unknown): value is JsonRpcRequest { | ||
| 750 | if (!value || typeof value !== "object") { | ||
| 751 | return false; | ||
| 752 | } | ||
| 753 | |||
| 754 | const candidate = value as Record<string, unknown>; | ||
| 755 | return ( | ||
| 756 | typeof candidate.method === "string" && | ||
| 757 | (typeof candidate.id === "string" || typeof candidate.id === "number") | ||
| 758 | ); | ||
| 759 | } | ||
| 760 | |||
| 761 | private isServerNotification(value: unknown): value is JsonRpcNotification { | ||
| 762 | if (!value || typeof value !== "object") { | ||
| 763 | return false; | ||
| 764 | } | ||
| 765 | |||
| 766 | const candidate = value as Record<string, unknown>; | ||
| 767 | return typeof candidate.method === "string" && !("id" in candidate); | ||
| 768 | } | ||
| 769 | |||
| 770 | private isResponse(value: unknown): value is JsonRpcResponse { | ||
| 771 | if (!value || typeof value !== "object") { | ||
| 772 | return false; | ||
| 773 | } | ||
| 774 | |||
| 775 | const candidate = value as Record<string, unknown>; | ||
| 776 | const hasId = typeof candidate.id === "string" || typeof candidate.id === "number"; | ||
| 777 | const hasMethod = typeof candidate.method === "string"; | ||
| 778 | return hasId && !hasMethod; | ||
| 779 | } | ||
| 780 | |||
| 781 | private readRouteFields(params: unknown): { | ||
| 782 | threadId?: string; | ||
| 783 | turnId?: string; | ||
| 784 | itemId?: string; | ||
| 785 | } { | ||
| 786 | const route: { | ||
| 787 | threadId?: string; | ||
| 788 | turnId?: string; | ||
| 789 | itemId?: string; | ||
| 790 | } = {}; | ||
| 791 | |||
| 792 | const threadId = | ||
| 793 | this.readString(params, "threadId") ?? | ||
| 794 | this.readString(this.readObject(params, "thread"), "id"); | ||
| 795 | const turnId = | ||
| 796 | this.readString(params, "turnId") ?? this.readString(this.readObject(params, "turn"), "id"); | ||
| 797 | const itemId = | ||
| 798 | this.readString(params, "itemId") ?? this.readString(this.readObject(params, "item"), "id"); | ||
| 799 | |||
| 800 | if (threadId) { | ||
| 801 | route.threadId = threadId; | ||
| 802 | } | ||
| 803 | |||
| 804 | if (turnId) { | ||
| 805 | route.turnId = turnId; | ||
| 806 | } | ||
| 807 | |||
| 808 | if (itemId) { | ||
| 809 | route.itemId = itemId; | ||
| 810 | } | ||
| 811 | |||
| 812 | return route; | ||
| 813 | } | ||
| 814 | |||
| 815 | private readObject(value: unknown, key?: string): Record<string, unknown> | undefined { | ||
| 816 | const target = | ||
| 817 | key === undefined | ||
| 818 | ? value | ||
| 819 | : value && typeof value === "object" | ||
| 820 | ? (value as Record<string, unknown>)[key] | ||
| 821 | : undefined; | ||
| 822 | |||
| 823 | if (!target || typeof target !== "object") { | ||
| 824 | return undefined; | ||
| 825 | } | ||
| 826 | |||
| 827 | return target as Record<string, unknown>; | ||
| 828 | } | ||
| 829 | |||
| 830 | private readString(value: unknown, key: string): string | undefined { | ||
| 831 | if (!value || typeof value !== "object") { | ||
| 832 | return undefined; | ||
| 833 | } | ||
| 834 | |||
| 835 | const candidate = (value as Record<string, unknown>)[key]; | ||
| 836 | return typeof candidate === "string" ? candidate : undefined; | ||
| 837 | } | ||
| 838 | |||
| 839 | private readBoolean(value: unknown, key: string): boolean | undefined { | ||
| 840 | if (!value || typeof value !== "object") { | ||
| 841 | return undefined; | ||
| 842 | } | ||
| 843 | |||
| 844 | const candidate = (value as Record<string, unknown>)[key]; | ||
| 845 | return typeof candidate === "boolean" ? candidate : undefined; | ||
| 846 | } | ||
| 847 | } | ||
apps/server/src/fixPath.ts created+18| ... | @@ -0,0 +1,18 @@ | ||
| 1 | import { execFileSync } from "node:child_process"; | ||
| 2 | |||
| 3 | export function fixPath(): void { | ||
| 4 | if (process.platform !== "darwin") return; | ||
| 5 | |||
| 6 | try { | ||
| 7 | const shell = process.env.SHELL ?? "/bin/zsh"; | ||
| 8 | const result = execFileSync(shell, ["-ilc", "echo -n $PATH"], { | ||
| 9 | encoding: "utf8", | ||
| 10 | timeout: 5000, | ||
| 11 | }); | ||
| 12 | if (result) { | ||
| 13 | process.env.PATH = result; | ||
| 14 | } | ||
| 15 | } catch { | ||
| 16 | // Silently ignore — keep default PATH | ||
| 17 | } | ||
| 18 | } | ||
apps/server/src/index.ts created+176| ... | @@ -0,0 +1,176 @@ | ||
| 1 | import fs from "node:fs"; | ||
| 2 | import net from "node:net"; | ||
| 3 | import os from "node:os"; | ||
| 4 | import path from "node:path"; | ||
| 5 | import { fileURLToPath } from "node:url"; | ||
| 6 | |||
| 7 | import { fixPath } from "./fixPath"; | ||
| 8 | import { createLogger } from "./logger"; | ||
| 9 | import { ProjectRegistry } from "./projectRegistry"; | ||
| 10 | import { createServer } from "./wsServer"; | ||
| 11 | |||
| 12 | fixPath(); | ||
| 13 | |||
| 14 | const DEFAULT_PORT = 3773; | ||
| 15 | const cwd = process.cwd(); | ||
| 16 | const logger = createLogger("server"); | ||
| 17 | |||
| 18 | type RuntimeMode = "web" | "desktop"; | ||
| 19 | |||
| 20 | function parseBooleanEnv(value: string | undefined): boolean | undefined { | ||
| 21 | if (value === undefined) return undefined; | ||
| 22 | const normalized = value.trim().toLowerCase(); | ||
| 23 | if (["1", "true", "yes", "on"].includes(normalized)) return true; | ||
| 24 | if (["0", "false", "no", "off"].includes(normalized)) return false; | ||
| 25 | return undefined; | ||
| 26 | } | ||
| 27 | |||
| 28 | function parsePort(value: string | undefined): number | undefined { | ||
| 29 | if (!value) return undefined; | ||
| 30 | const parsed = Number(value); | ||
| 31 | if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) { | ||
| 32 | throw new Error(`Invalid CODETHING_PORT: ${value}`); | ||
| 33 | } | ||
| 34 | return parsed; | ||
| 35 | } | ||
| 36 | |||
| 37 | function expandHomePath(input: string): string { | ||
| 38 | if (input === "~") return os.homedir(); | ||
| 39 | if (input.startsWith("~/")) { | ||
| 40 | return path.join(os.homedir(), input.slice(2)); | ||
| 41 | } | ||
| 42 | return input; | ||
| 43 | } | ||
| 44 | |||
| 45 | function resolveStateDir(raw: string | undefined): string { | ||
| 46 | if (!raw || raw.trim().length === 0) { | ||
| 47 | return path.join(os.homedir(), ".t3", "userdata"); | ||
| 48 | } | ||
| 49 | return path.resolve(expandHomePath(raw.trim())); | ||
| 50 | } | ||
| 51 | |||
| 52 | async function findAvailablePort(preferred: number): Promise<number> { | ||
| 53 | return new Promise((resolve, reject) => { | ||
| 54 | const server = net.createServer(); | ||
| 55 | server.listen(preferred, () => { | ||
| 56 | server.close(() => resolve(preferred)); | ||
| 57 | }); | ||
| 58 | server.on("error", () => { | ||
| 59 | // Preferred port busy — let the OS pick one | ||
| 60 | const fallback = net.createServer(); | ||
| 61 | fallback.listen(0, () => { | ||
| 62 | const addr = fallback.address(); | ||
| 63 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | ||
| 64 | fallback.close(() => { | ||
| 65 | if (port > 0) resolve(port); | ||
| 66 | else reject(new Error("Could not find an available port.")); | ||
| 67 | }); | ||
| 68 | }); | ||
| 69 | fallback.on("error", reject); | ||
| 70 | }); | ||
| 71 | }); | ||
| 72 | } | ||
| 73 | |||
| 74 | function resolveStaticDir(): string | undefined { | ||
| 75 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
| 76 | |||
| 77 | // Check for bundled client (npm publish / npx scenario): | ||
| 78 | // dist/client/ lives alongside dist/index.js | ||
| 79 | const bundledClient = path.resolve(__dirname, "client"); | ||
| 80 | try { | ||
| 81 | const stat = fs.statSync(path.join(bundledClient, "index.html")); | ||
| 82 | if (stat.isFile()) return bundledClient; | ||
| 83 | } catch { | ||
| 84 | // Not bundled — check monorepo layout | ||
| 85 | } | ||
| 86 | |||
| 87 | // Monorepo layout: apps/server/dist/index.js → apps/renderer/dist/ | ||
| 88 | const monorepoClient = path.resolve(__dirname, "../../renderer/dist"); | ||
| 89 | try { | ||
| 90 | const stat = fs.statSync(path.join(monorepoClient, "index.html")); | ||
| 91 | if (stat.isFile()) return monorepoClient; | ||
| 92 | } catch { | ||
| 93 | // Not found — probably dev mode, Vite will serve | ||
| 94 | } | ||
| 95 | |||
| 96 | return undefined; | ||
| 97 | } | ||
| 98 | |||
| 99 | async function main() { | ||
| 100 | const mode: RuntimeMode = process.env.CODETHING_MODE === "desktop" ? "desktop" : "web"; | ||
| 101 | const requestedPort = parsePort(process.env.CODETHING_PORT); | ||
| 102 | const port = | ||
| 103 | requestedPort ?? (mode === "desktop" ? DEFAULT_PORT : await findAvailablePort(DEFAULT_PORT)); | ||
| 104 | const stateDir = resolveStateDir(process.env.CODETHING_STATE_DIR); | ||
| 105 | const projectRegistry = new ProjectRegistry(stateDir); | ||
| 106 | const devUrl = process.env.VITE_DEV_SERVER_URL; | ||
| 107 | const noBrowser = parseBooleanEnv(process.env.CODETHING_NO_BROWSER) ?? mode === "desktop"; | ||
| 108 | const authToken = process.env.CODETHING_AUTH_TOKEN; | ||
| 109 | const staticDir = devUrl ? undefined : resolveStaticDir(); | ||
| 110 | |||
| 111 | if (!devUrl && !staticDir) { | ||
| 112 | logger.warn("renderer bundle missing and no VITE_DEV_SERVER_URL; web UI unavailable", { | ||
| 113 | hint: "Run `bun run --cwd apps/renderer build` or set VITE_DEV_SERVER_URL for dev mode.", | ||
| 114 | }); | ||
| 115 | } | ||
| 116 | |||
| 117 | const server = createServer({ | ||
| 118 | port, | ||
| 119 | host: mode === "desktop" ? "127.0.0.1" : undefined, | ||
| 120 | cwd, | ||
| 121 | staticDir, | ||
| 122 | devUrl, | ||
| 123 | projectRegistry, | ||
| 124 | authToken, | ||
| 125 | }); | ||
| 126 | await server.start(); | ||
| 127 | |||
| 128 | const url = `http://localhost:${port}`; | ||
| 129 | logger.info("CodeThing running", { | ||
| 130 | url, | ||
| 131 | cwd, | ||
| 132 | mode, | ||
| 133 | stateDir, | ||
| 134 | authEnabled: Boolean(authToken), | ||
| 135 | }); | ||
| 136 | |||
| 137 | // Open browser (dynamic import because `open` is ESM-only) | ||
| 138 | if (!noBrowser) { | ||
| 139 | try { | ||
| 140 | const open = await import("open"); | ||
| 141 | const target = devUrl ?? url; | ||
| 142 | await open.default(target); | ||
| 143 | } catch { | ||
| 144 | logger.info("browser auto-open unavailable", { | ||
| 145 | hint: `Open ${devUrl ?? url} in your browser.`, | ||
| 146 | }); | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | // Graceful shutdown | ||
| 151 | let shuttingDown = false; | ||
| 152 | async function shutdown() { | ||
| 153 | if (shuttingDown) return; | ||
| 154 | shuttingDown = true; | ||
| 155 | logger.info("shutting down"); | ||
| 156 | try { | ||
| 157 | await server.stop(); | ||
| 158 | process.exit(0); | ||
| 159 | } catch (error) { | ||
| 160 | logger.error("failed to stop server cleanly", { error }); | ||
| 161 | process.exit(1); | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | process.on("SIGINT", () => { | ||
| 166 | void shutdown(); | ||
| 167 | }); | ||
| 168 | process.on("SIGTERM", () => { | ||
| 169 | void shutdown(); | ||
| 170 | }); | ||
| 171 | } | ||
| 172 | |||
| 173 | main().catch((err) => { | ||
| 174 | logger.error("failed to start CodeThing", { error: err }); | ||
| 175 | process.exit(1); | ||
| 176 | }); | ||
apps/server/src/logger.ts created+103| ... | @@ -0,0 +1,103 @@ | ||
| 1 | import util from "node:util"; | ||
| 2 | |||
| 3 | type LogLevel = "info" | "warn" | "error" | "event"; | ||
| 4 | |||
| 5 | type LogContext = Record<string, unknown>; | ||
| 6 | |||
| 7 | const ANSI = { | ||
| 8 | reset: "\u001b[0m", | ||
| 9 | dim: "\u001b[2m", | ||
| 10 | cyan: "\u001b[36m", | ||
| 11 | yellow: "\u001b[33m", | ||
| 12 | red: "\u001b[31m", | ||
| 13 | magenta: "\u001b[35m", | ||
| 14 | } as const; | ||
| 15 | |||
| 16 | const LEVEL_LABEL: Record<LogLevel, string> = { | ||
| 17 | info: "INFO", | ||
| 18 | warn: "WARN", | ||
| 19 | error: "ERROR", | ||
| 20 | event: "EVENT", | ||
| 21 | }; | ||
| 22 | |||
| 23 | const LEVEL_COLOR: Record<LogLevel, string> = { | ||
| 24 | info: ANSI.cyan, | ||
| 25 | warn: ANSI.yellow, | ||
| 26 | error: ANSI.red, | ||
| 27 | event: ANSI.magenta, | ||
| 28 | }; | ||
| 29 | |||
| 30 | function useColors() { | ||
| 31 | return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined; | ||
| 32 | } | ||
| 33 | |||
| 34 | function colorize(value: string, color: string, enabled: boolean) { | ||
| 35 | return enabled ? `${color}${value}${ANSI.reset}` : value; | ||
| 36 | } | ||
| 37 | |||
| 38 | function timeStamp() { | ||
| 39 | return new Date().toISOString().slice(11, 23); | ||
| 40 | } | ||
| 41 | |||
| 42 | function formatValue(value: unknown) { | ||
| 43 | if (typeof value === "string") { | ||
| 44 | return JSON.stringify(value); | ||
| 45 | } | ||
| 46 | if ( | ||
| 47 | typeof value === "number" || | ||
| 48 | typeof value === "boolean" || | ||
| 49 | value === null || | ||
| 50 | value === undefined | ||
| 51 | ) { | ||
| 52 | return String(value); | ||
| 53 | } | ||
| 54 | return util.inspect(value, { | ||
| 55 | depth: 4, | ||
| 56 | breakLength: Infinity, | ||
| 57 | compact: true, | ||
| 58 | maxArrayLength: 25, | ||
| 59 | maxStringLength: 320, | ||
| 60 | }); | ||
| 61 | } | ||
| 62 | |||
| 63 | function formatContext(context: LogContext | undefined) { | ||
| 64 | if (!context) return ""; | ||
| 65 | const entries = Object.entries(context).filter(([, value]) => value !== undefined); | ||
| 66 | if (entries.length === 0) return ""; | ||
| 67 | return entries.map(([key, value]) => `${key}=${formatValue(value)}`).join(" "); | ||
| 68 | } | ||
| 69 | |||
| 70 | function write(level: LogLevel, scope: string, message: string, context?: LogContext) { | ||
| 71 | const colorEnabled = useColors(); | ||
| 72 | const ts = colorize(timeStamp(), ANSI.dim, colorEnabled); | ||
| 73 | const levelLabel = colorize(LEVEL_LABEL[level], LEVEL_COLOR[level], colorEnabled); | ||
| 74 | const contextText = formatContext(context); | ||
| 75 | const line = `${ts} ${levelLabel} [${scope}] ${message}${contextText ? ` ${contextText}` : ""}`; | ||
| 76 | |||
| 77 | if (level === "warn") { | ||
| 78 | console.warn(line); | ||
| 79 | return; | ||
| 80 | } | ||
| 81 | if (level === "error") { | ||
| 82 | console.error(line); | ||
| 83 | return; | ||
| 84 | } | ||
| 85 | console.log(line); | ||
| 86 | } | ||
| 87 | |||
| 88 | export function createLogger(scope: string) { | ||
| 89 | return { | ||
| 90 | info(message: string, context?: LogContext) { | ||
| 91 | write("info", scope, message, context); | ||
| 92 | }, | ||
| 93 | warn(message: string, context?: LogContext) { | ||
| 94 | write("warn", scope, message, context); | ||
| 95 | }, | ||
| 96 | error(message: string, context?: LogContext) { | ||
| 97 | write("error", scope, message, context); | ||
| 98 | }, | ||
| 99 | event(message: string, context?: LogContext) { | ||
| 100 | write("event", scope, message, context); | ||
| 101 | }, | ||
| 102 | }; | ||
| 103 | } | ||
apps/server/src/projectRegistry.ts created+172| ... | @@ -0,0 +1,172 @@ | ||
| 1 | import { randomUUID } from "node:crypto"; | ||
| 2 | import fs from "node:fs"; | ||
| 3 | import path from "node:path"; | ||
| 4 | |||
| 5 | import { | ||
| 6 | type ProjectAddInput, | ||
| 7 | type ProjectAddResult, | ||
| 8 | type ProjectListResult, | ||
| 9 | type ProjectRecord, | ||
| 10 | type ProjectRemoveInput, | ||
| 11 | projectAddInputSchema, | ||
| 12 | projectRecordSchema, | ||
| 13 | projectRemoveInputSchema, | ||
| 14 | } from "@acme/contracts"; | ||
| 15 | |||
| 16 | function normalizeCwd(rawCwd: string): string { | ||
| 17 | const resolved = path.resolve(rawCwd.trim()); | ||
| 18 | const normalized = path.normalize(resolved); | ||
| 19 | if (process.platform === "win32") { | ||
| 20 | return normalized.toLowerCase(); | ||
| 21 | } | ||
| 22 | return normalized; | ||
| 23 | } | ||
| 24 | |||
| 25 | function isDirectory(cwd: string): boolean { | ||
| 26 | try { | ||
| 27 | return fs.statSync(cwd).isDirectory(); | ||
| 28 | } catch { | ||
| 29 | return false; | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | function inferProjectName(cwd: string): string { | ||
| 34 | const name = path.basename(cwd); | ||
| 35 | return name.length > 0 ? name : "project"; | ||
| 36 | } | ||
| 37 | |||
| 38 | export class ProjectRegistry { | ||
| 39 | private readonly stateDir: string; | ||
| 40 | private readonly filePath: string; | ||
| 41 | private projects: ProjectRecord[]; | ||
| 42 | |||
| 43 | constructor(stateDir: string) { | ||
| 44 | this.stateDir = stateDir; | ||
| 45 | this.filePath = path.join(this.stateDir, "projects.json"); | ||
| 46 | fs.mkdirSync(this.stateDir, { recursive: true }); | ||
| 47 | this.projects = this.loadFromDisk(); | ||
| 48 | } | ||
| 49 | |||
| 50 | list(): ProjectListResult { | ||
| 51 | return this.projects.map((project) => ({ ...project })); | ||
| 52 | } | ||
| 53 | |||
| 54 | add(raw: ProjectAddInput): ProjectAddResult { | ||
| 55 | const input = projectAddInputSchema.parse(raw); | ||
| 56 | const normalizedCwd = normalizeCwd(input.cwd); | ||
| 57 | if (!isDirectory(normalizedCwd)) { | ||
| 58 | throw new Error(`Project path does not exist: ${normalizedCwd}`); | ||
| 59 | } | ||
| 60 | |||
| 61 | const existing = this.projects.find((project) => normalizeCwd(project.cwd) === normalizedCwd); | ||
| 62 | if (existing) { | ||
| 63 | return { project: { ...existing }, created: false }; | ||
| 64 | } | ||
| 65 | |||
| 66 | const now = new Date().toISOString(); | ||
| 67 | const project: ProjectRecord = { | ||
| 68 | id: randomUUID(), | ||
| 69 | cwd: normalizedCwd, | ||
| 70 | name: inferProjectName(normalizedCwd), | ||
| 71 | createdAt: now, | ||
| 72 | updatedAt: now, | ||
| 73 | }; | ||
| 74 | |||
| 75 | this.projects = [project, ...this.projects]; | ||
| 76 | this.persist(); | ||
| 77 | return { project: { ...project }, created: true }; | ||
| 78 | } | ||
| 79 | |||
| 80 | remove(raw: ProjectRemoveInput): void { | ||
| 81 | const input = projectRemoveInputSchema.parse(raw); | ||
| 82 | const next = this.projects.filter((project) => project.id !== input.id); | ||
| 83 | if (next.length === this.projects.length) { | ||
| 84 | return; | ||
| 85 | } | ||
| 86 | |||
| 87 | this.projects = next; | ||
| 88 | this.persist(); | ||
| 89 | } | ||
| 90 | |||
| 91 | private loadFromDisk(): ProjectRecord[] { | ||
| 92 | const loaded = this.readPersistedProjects(); | ||
| 93 | const deduped = new Map<string, ProjectRecord>(); | ||
| 94 | const normalizedProjects: ProjectRecord[] = []; | ||
| 95 | |||
| 96 | for (const project of loaded) { | ||
| 97 | const normalizedCwd = normalizeCwd(project.cwd); | ||
| 98 | if (!isDirectory(normalizedCwd)) { | ||
| 99 | continue; | ||
| 100 | } | ||
| 101 | if (deduped.has(normalizedCwd)) { | ||
| 102 | continue; | ||
| 103 | } | ||
| 104 | |||
| 105 | const normalizedProject: ProjectRecord = { | ||
| 106 | ...project, | ||
| 107 | cwd: normalizedCwd, | ||
| 108 | name: | ||
| 109 | project.name.trim().length > 0 ? project.name.trim() : inferProjectName(normalizedCwd), | ||
| 110 | }; | ||
| 111 | deduped.set(normalizedCwd, normalizedProject); | ||
| 112 | normalizedProjects.push(normalizedProject); | ||
| 113 | } | ||
| 114 | |||
| 115 | const changed = | ||
| 116 | normalizedProjects.length !== loaded.length || | ||
| 117 | normalizedProjects.some((project, index) => { | ||
| 118 | const source = loaded[index]; | ||
| 119 | if (!source) return true; | ||
| 120 | return ( | ||
| 121 | source.id !== project.id || | ||
| 122 | source.cwd !== project.cwd || | ||
| 123 | source.name !== project.name || | ||
| 124 | source.createdAt !== project.createdAt || | ||
| 125 | source.updatedAt !== project.updatedAt | ||
| 126 | ); | ||
| 127 | }); | ||
| 128 | |||
| 129 | if (changed) { | ||
| 130 | this.projects = normalizedProjects; | ||
| 131 | this.persist(); | ||
| 132 | } | ||
| 133 | |||
| 134 | return normalizedProjects; | ||
| 135 | } | ||
| 136 | |||
| 137 | private readPersistedProjects(): ProjectRecord[] { | ||
| 138 | try { | ||
| 139 | const raw = fs.readFileSync(this.filePath, "utf8"); | ||
| 140 | const parsed = JSON.parse(raw) as { projects?: unknown }; | ||
| 141 | if (!parsed || !Array.isArray(parsed.projects)) { | ||
| 142 | return []; | ||
| 143 | } | ||
| 144 | |||
| 145 | const projects: ProjectRecord[] = []; | ||
| 146 | for (const candidate of parsed.projects) { | ||
| 147 | const row = projectRecordSchema.safeParse(candidate); | ||
| 148 | if (!row.success) { | ||
| 149 | continue; | ||
| 150 | } | ||
| 151 | projects.push(row.data); | ||
| 152 | } | ||
| 153 | return projects; | ||
| 154 | } catch { | ||
| 155 | return []; | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | private persist(): void { | ||
| 160 | const payload = JSON.stringify( | ||
| 161 | { | ||
| 162 | version: 1, | ||
| 163 | projects: this.projects, | ||
| 164 | }, | ||
| 165 | null, | ||
| 166 | 2, | ||
| 167 | ); | ||
| 168 | const tempFile = `${this.filePath}.tmp-${process.pid}-${Date.now()}`; | ||
| 169 | fs.writeFileSync(tempFile, payload); | ||
| 170 | fs.renameSync(tempFile, this.filePath); | ||
| 171 | } | ||
| 172 | } | ||
apps/server/src/providerManager.test.ts created+65| ... | @@ -0,0 +1,65 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { ProviderManager } from "./providerManager"; | ||
| 4 | |||
| 5 | describe("ProviderManager", () => { | ||
| 6 | it("detaches provider event listener and ends thread log streams on dispose", () => { | ||
| 7 | const manager = new ProviderManager(); | ||
| 8 | const internals = manager as unknown as { | ||
| 9 | codex: { listenerCount: (event: string) => number }; | ||
| 10 | onCodexEvent: (event: { | ||
| 11 | id: string; | ||
| 12 | kind: "notification"; | ||
| 13 | provider: "codex"; | ||
| 14 | sessionId: string; | ||
| 15 | createdAt: string; | ||
| 16 | method: string; | ||
| 17 | threadId: string; | ||
| 18 | }) => void; | ||
| 19 | threadLogStreams: Map< | ||
| 20 | string, | ||
| 21 | { writableEnded: boolean; destroyed: boolean } | ||
| 22 | >; | ||
| 23 | }; | ||
| 24 | internals.onCodexEvent({ | ||
| 25 | id: "evt-1", | ||
| 26 | kind: "notification", | ||
| 27 | provider: "codex", | ||
| 28 | sessionId: "sess-1", | ||
| 29 | createdAt: new Date().toISOString(), | ||
| 30 | method: "thread/started", | ||
| 31 | threadId: "thread-1", | ||
| 32 | }); | ||
| 33 | const stream = internals.threadLogStreams.get("thread-1"); | ||
| 34 | expect(stream).toBeDefined(); | ||
| 35 | if (!stream) return; | ||
| 36 | |||
| 37 | expect(internals.codex.listenerCount("event")).toBe(1); | ||
| 38 | manager.dispose(); | ||
| 39 | |||
| 40 | expect(internals.codex.listenerCount("event")).toBe(0); | ||
| 41 | expect(stream.writableEnded || stream.destroyed).toBe(true); | ||
| 42 | expect(internals.threadLogStreams.size).toBe(0); | ||
| 43 | }); | ||
| 44 | |||
| 45 | it("allows multiple dispose calls", () => { | ||
| 46 | const manager = new ProviderManager(); | ||
| 47 | |||
| 48 | manager.dispose(); | ||
| 49 | expect(() => manager.dispose()).not.toThrow(); | ||
| 50 | }); | ||
| 51 | |||
| 52 | it("rejects request responses for unknown sessions", async () => { | ||
| 53 | const manager = new ProviderManager(); | ||
| 54 | |||
| 55 | await expect( | ||
| 56 | manager.respondToRequest({ | ||
| 57 | sessionId: "missing-session", | ||
| 58 | requestId: "req-1", | ||
| 59 | decision: "accept", | ||
| 60 | }), | ||
| 61 | ).rejects.toThrow("Unknown provider session: missing-session"); | ||
| 62 | |||
| 63 | manager.dispose(); | ||
| 64 | }); | ||
| 65 | }); | ||
apps/server/src/providerManager.ts created+209| ... | @@ -0,0 +1,209 @@ | ||
| 1 | import { EventEmitter } from "node:events"; | ||
| 2 | import fs from "node:fs"; | ||
| 3 | import path from "node:path"; | ||
| 4 | |||
| 5 | import { | ||
| 6 | type ProviderEvent, | ||
| 7 | type ProviderInterruptTurnInput, | ||
| 8 | type ProviderRespondToRequestInput, | ||
| 9 | type ProviderSendTurnInput, | ||
| 10 | type ProviderSession, | ||
| 11 | type ProviderSessionStartInput, | ||
| 12 | type ProviderStopSessionInput, | ||
| 13 | type ProviderTurnStartResult, | ||
| 14 | providerInterruptTurnInputSchema, | ||
| 15 | providerRespondToRequestInputSchema, | ||
| 16 | providerSendTurnInputSchema, | ||
| 17 | providerSessionStartInputSchema, | ||
| 18 | providerStopSessionInputSchema, | ||
| 19 | } from "@acme/contracts"; | ||
| 20 | import { CodexAppServerManager } from "./codexAppServerManager"; | ||
| 21 | |||
| 22 | export interface ProviderManagerEvents { | ||
| 23 | event: [event: ProviderEvent]; | ||
| 24 | } | ||
| 25 | |||
| 26 | export class ProviderManager extends EventEmitter<ProviderManagerEvents> { | ||
| 27 | private readonly codex = new CodexAppServerManager(); | ||
| 28 | private readonly threadLogsDir: string; | ||
| 29 | private readonly threadLogStreams = new Map<string, fs.WriteStream>(); | ||
| 30 | private readonly sessionThreadIds = new Map<string, string>(); | ||
| 31 | private readonly pendingEventsBySession = new Map<string, ProviderEvent[]>(); | ||
| 32 | private disposed = false; | ||
| 33 | private readonly onCodexEvent = (event: ProviderEvent) => { | ||
| 34 | if (this.disposed) { | ||
| 35 | return; | ||
| 36 | } | ||
| 37 | |||
| 38 | this.routeEventToThreadLog(event); | ||
| 39 | this.emit("event", event); | ||
| 40 | }; | ||
| 41 | |||
| 42 | constructor() { | ||
| 43 | super(); | ||
| 44 | |||
| 45 | const logsDir = path.resolve(process.cwd(), ".logs"); | ||
| 46 | this.threadLogsDir = path.join(logsDir, "threads"); | ||
| 47 | fs.mkdirSync(this.threadLogsDir, { recursive: true }); | ||
| 48 | |||
| 49 | this.codex.on("event", this.onCodexEvent); | ||
| 50 | } | ||
| 51 | |||
| 52 | async startSession(raw: ProviderSessionStartInput): Promise<ProviderSession> { | ||
| 53 | const input = providerSessionStartInputSchema.parse(raw); | ||
| 54 | if (input.provider !== "codex") { | ||
| 55 | throw new Error(`Provider '${input.provider}' is not implemented yet.`); | ||
| 56 | } | ||
| 57 | |||
| 58 | const session = await this.codex.startSession(input); | ||
| 59 | if (session.threadId) { | ||
| 60 | this.sessionThreadIds.set(session.sessionId, session.threadId); | ||
| 61 | this.flushPendingSessionEvents(session.sessionId, session.threadId); | ||
| 62 | } | ||
| 63 | return session; | ||
| 64 | } | ||
| 65 | |||
| 66 | async sendTurn(raw: ProviderSendTurnInput): Promise<ProviderTurnStartResult> { | ||
| 67 | const input = providerSendTurnInputSchema.parse(raw); | ||
| 68 | if (!this.codex.hasSession(input.sessionId)) { | ||
| 69 | throw new Error(`Unknown provider session: ${input.sessionId}`); | ||
| 70 | } | ||
| 71 | |||
| 72 | return this.codex.sendTurn(input); | ||
| 73 | } | ||
| 74 | |||
| 75 | async interruptTurn(raw: ProviderInterruptTurnInput): Promise<void> { | ||
| 76 | const input = providerInterruptTurnInputSchema.parse(raw); | ||
| 77 | if (!this.codex.hasSession(input.sessionId)) { | ||
| 78 | throw new Error(`Unknown provider session: ${input.sessionId}`); | ||
| 79 | } | ||
| 80 | |||
| 81 | await this.codex.interruptTurn(input.sessionId, input.turnId); | ||
| 82 | } | ||
| 83 | |||
| 84 | async respondToRequest(raw: ProviderRespondToRequestInput): Promise<void> { | ||
| 85 | const input = providerRespondToRequestInputSchema.parse(raw); | ||
| 86 | if (!this.codex.hasSession(input.sessionId)) { | ||
| 87 | throw new Error(`Unknown provider session: ${input.sessionId}`); | ||
| 88 | } | ||
| 89 | |||
| 90 | await this.codex.respondToRequest( | ||
| 91 | input.sessionId, | ||
| 92 | input.requestId, | ||
| 93 | input.decision, | ||
| 94 | ); | ||
| 95 | } | ||
| 96 | |||
| 97 | stopSession(raw: ProviderStopSessionInput): void { | ||
| 98 | const input = providerStopSessionInputSchema.parse(raw); | ||
| 99 | this.codex.stopSession(input.sessionId); | ||
| 100 | this.sessionThreadIds.delete(input.sessionId); | ||
| 101 | this.pendingEventsBySession.delete(input.sessionId); | ||
| 102 | } | ||
| 103 | |||
| 104 | listSessions(): ProviderSession[] { | ||
| 105 | return this.codex.listSessions(); | ||
| 106 | } | ||
| 107 | |||
| 108 | stopAll(): void { | ||
| 109 | this.codex.stopAll(); | ||
| 110 | } | ||
| 111 | |||
| 112 | dispose(): void { | ||
| 113 | if (this.disposed) { | ||
| 114 | return; | ||
| 115 | } | ||
| 116 | |||
| 117 | this.disposed = true; | ||
| 118 | this.codex.off("event", this.onCodexEvent); | ||
| 119 | for (const stream of this.threadLogStreams.values()) { | ||
| 120 | stream.end(); | ||
| 121 | } | ||
| 122 | this.threadLogStreams.clear(); | ||
| 123 | this.sessionThreadIds.clear(); | ||
| 124 | this.pendingEventsBySession.clear(); | ||
| 125 | } | ||
| 126 | |||
| 127 | private routeEventToThreadLog(event: ProviderEvent): void { | ||
| 128 | const threadId = this.resolveThreadId(event); | ||
| 129 | if (!threadId) { | ||
| 130 | const pending = this.pendingEventsBySession.get(event.sessionId) ?? []; | ||
| 131 | pending.push(event); | ||
| 132 | this.pendingEventsBySession.set(event.sessionId, pending.slice(-100)); | ||
| 133 | return; | ||
| 134 | } | ||
| 135 | |||
| 136 | this.flushPendingSessionEvents(event.sessionId, threadId); | ||
| 137 | this.writeThreadEvent(threadId, event); | ||
| 138 | } | ||
| 139 | |||
| 140 | private flushPendingSessionEvents(sessionId: string, threadId: string): void { | ||
| 141 | const pending = this.pendingEventsBySession.get(sessionId); | ||
| 142 | if (!pending || pending.length === 0) { | ||
| 143 | return; | ||
| 144 | } | ||
| 145 | |||
| 146 | for (const event of pending) { | ||
| 147 | this.writeThreadEvent(threadId, event); | ||
| 148 | } | ||
| 149 | this.pendingEventsBySession.delete(sessionId); | ||
| 150 | } | ||
| 151 | |||
| 152 | private resolveThreadId(event: ProviderEvent): string | undefined { | ||
| 153 | const fromPayload = this.readThreadIdFromPayload(event.payload); | ||
| 154 | const threadId = | ||
| 155 | event.threadId ?? | ||
| 156 | fromPayload ?? | ||
| 157 | this.sessionThreadIds.get(event.sessionId); | ||
| 158 | |||
| 159 | if (threadId) { | ||
| 160 | this.sessionThreadIds.set(event.sessionId, threadId); | ||
| 161 | } | ||
| 162 | |||
| 163 | return threadId; | ||
| 164 | } | ||
| 165 | |||
| 166 | private readThreadIdFromPayload(payload: unknown): string | undefined { | ||
| 167 | if (!payload || typeof payload !== "object") { | ||
| 168 | return undefined; | ||
| 169 | } | ||
| 170 | |||
| 171 | const record = payload as Record<string, unknown>; | ||
| 172 | const direct = record.threadId; | ||
| 173 | if (typeof direct === "string" && direct.length > 0) { | ||
| 174 | return direct; | ||
| 175 | } | ||
| 176 | |||
| 177 | const thread = record.thread; | ||
| 178 | if (!thread || typeof thread !== "object") { | ||
| 179 | return undefined; | ||
| 180 | } | ||
| 181 | |||
| 182 | const nested = (thread as Record<string, unknown>).id; | ||
| 183 | if (typeof nested === "string" && nested.length > 0) { | ||
| 184 | return nested; | ||
| 185 | } | ||
| 186 | |||
| 187 | return undefined; | ||
| 188 | } | ||
| 189 | |||
| 190 | private writeThreadEvent(threadId: string, event: ProviderEvent): void { | ||
| 191 | const stream = this.getOrCreateThreadLogStream(threadId); | ||
| 192 | stream.write(`${JSON.stringify(event)}\n`); | ||
| 193 | } | ||
| 194 | |||
| 195 | private getOrCreateThreadLogStream(threadId: string): fs.WriteStream { | ||
| 196 | const cached = this.threadLogStreams.get(threadId); | ||
| 197 | if (cached) { | ||
| 198 | return cached; | ||
| 199 | } | ||
| 200 | |||
| 201 | const safeThreadId = threadId.replace(/[^a-zA-Z0-9._-]/g, "_"); | ||
| 202 | const stream = fs.createWriteStream( | ||
| 203 | path.join(this.threadLogsDir, `${safeThreadId}.events.ndjson`), | ||
| 204 | { flags: "a" }, | ||
| 205 | ); | ||
| 206 | this.threadLogStreams.set(threadId, stream); | ||
| 207 | return stream; | ||
| 208 | } | ||
| 209 | } | ||
apps/server/src/wsServer.test.ts created+347| ... | @@ -0,0 +1,347 @@ | ||
| 1 | import fs from "node:fs"; | ||
| 2 | import os from "node:os"; | ||
| 3 | import path from "node:path"; | ||
| 4 | |||
| 5 | import { describe, expect, it, afterEach, vi } from "vitest"; | ||
| 6 | import { createServer } from "./wsServer"; | ||
| 7 | import WebSocket from "ws"; | ||
| 8 | |||
| 9 | import { WS_CHANNELS, WS_METHODS, type WsPush, type WsResponse } from "@acme/contracts"; | ||
| 10 | import { ProjectRegistry } from "./projectRegistry"; | ||
| 11 | |||
| 12 | interface PendingMessages { | ||
| 13 | queue: unknown[]; | ||
| 14 | waiters: Array<(message: unknown) => void>; | ||
| 15 | } | ||
| 16 | |||
| 17 | const pendingBySocket = new WeakMap<WebSocket, PendingMessages>(); | ||
| 18 | |||
| 19 | function connectWs(port: number, token?: string): Promise<WebSocket> { | ||
| 20 | return new Promise((resolve, reject) => { | ||
| 21 | const query = token ? `?token=${encodeURIComponent(token)}` : ""; | ||
| 22 | const ws = new WebSocket(`ws://127.0.0.1:${port}/${query}`); | ||
| 23 | const pending: PendingMessages = { queue: [], waiters: [] }; | ||
| 24 | pendingBySocket.set(ws, pending); | ||
| 25 | |||
| 26 | ws.on("message", (raw) => { | ||
| 27 | const parsed = JSON.parse(String(raw)); | ||
| 28 | const waiter = pending.waiters.shift(); | ||
| 29 | if (waiter) { | ||
| 30 | waiter(parsed); | ||
| 31 | return; | ||
| 32 | } | ||
| 33 | pending.queue.push(parsed); | ||
| 34 | }); | ||
| 35 | |||
| 36 | ws.once("open", () => resolve(ws)); | ||
| 37 | ws.once("error", () => reject(new Error("WebSocket connection failed"))); | ||
| 38 | }); | ||
| 39 | } | ||
| 40 | |||
| 41 | function waitForMessage(ws: WebSocket): Promise<unknown> { | ||
| 42 | const pending = pendingBySocket.get(ws); | ||
| 43 | if (!pending) { | ||
| 44 | return Promise.reject(new Error("WebSocket not initialized")); | ||
| 45 | } | ||
| 46 | |||
| 47 | const queued = pending.queue.shift(); | ||
| 48 | if (queued !== undefined) { | ||
| 49 | return Promise.resolve(queued); | ||
| 50 | } | ||
| 51 | |||
| 52 | return new Promise((resolve) => { | ||
| 53 | pending.waiters.push(resolve); | ||
| 54 | }); | ||
| 55 | } | ||
| 56 | |||
| 57 | async function sendRequest(ws: WebSocket, method: string, params?: unknown): Promise<WsResponse> { | ||
| 58 | const id = crypto.randomUUID(); | ||
| 59 | const message = JSON.stringify({ id, method, ...(params !== undefined ? { params } : {}) }); | ||
| 60 | ws.send(message); | ||
| 61 | |||
| 62 | // Wait for response with matching id | ||
| 63 | while (true) { | ||
| 64 | const parsed = (await waitForMessage(ws)) as Record<string, unknown>; | ||
| 65 | if (parsed.id === id) { | ||
| 66 | return parsed as WsResponse; | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | describe("WebSocket Server", () => { | ||
| 72 | let server: ReturnType<typeof createServer> | null = null; | ||
| 73 | const connections: WebSocket[] = []; | ||
| 74 | const tempDirs: string[] = []; | ||
| 75 | |||
| 76 | function makeTempDir(prefix: string): string { | ||
| 77 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); | ||
| 78 | tempDirs.push(dir); | ||
| 79 | return dir; | ||
| 80 | } | ||
| 81 | |||
| 82 | function createTestServer( | ||
| 83 | options: { | ||
| 84 | cwd?: string; | ||
| 85 | devUrl?: string; | ||
| 86 | authToken?: string; | ||
| 87 | stateDir?: string; | ||
| 88 | } = {}, | ||
| 89 | ): ReturnType<typeof createServer> { | ||
| 90 | const stateDir = options.stateDir ?? makeTempDir("codething-ws-state-"); | ||
| 91 | return createServer({ | ||
| 92 | port: 0, | ||
| 93 | cwd: options.cwd ?? "/test/project", | ||
| 94 | ...(options.devUrl ? { devUrl: options.devUrl } : {}), | ||
| 95 | ...(options.authToken ? { authToken: options.authToken } : {}), | ||
| 96 | projectRegistry: new ProjectRegistry(stateDir), | ||
| 97 | }); | ||
| 98 | } | ||
| 99 | |||
| 100 | afterEach(async () => { | ||
| 101 | for (const ws of connections) { | ||
| 102 | ws.close(); | ||
| 103 | } | ||
| 104 | connections.length = 0; | ||
| 105 | if (server) { | ||
| 106 | await server.stop(); | ||
| 107 | } | ||
| 108 | server = null; | ||
| 109 | for (const dir of tempDirs.splice(0, tempDirs.length)) { | ||
| 110 | fs.rmSync(dir, { recursive: true, force: true }); | ||
| 111 | } | ||
| 112 | vi.restoreAllMocks(); | ||
| 113 | }); | ||
| 114 | |||
| 115 | it("sends welcome message on connect", async () => { | ||
| 116 | server = createTestServer({ cwd: "/test/project" }); | ||
| 117 | // Get the actual port after listen | ||
| 118 | await server.start(); | ||
| 119 | const addr = server.httpServer.address(); | ||
| 120 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | ||
| 121 | expect(port).toBeGreaterThan(0); | ||
| 122 | |||
| 123 | const ws = await connectWs(port); | ||
| 124 | connections.push(ws); | ||
| 125 | |||
| 126 | const message = (await waitForMessage(ws)) as WsPush; | ||
| 127 | expect(message.type).toBe("push"); | ||
| 128 | expect(message.channel).toBe(WS_CHANNELS.serverWelcome); | ||
| 129 | expect(message.data).toEqual({ | ||
| 130 | cwd: "/test/project", | ||
| 131 | projectName: "project", | ||
| 132 | }); | ||
| 133 | }); | ||
| 134 | |||
| 135 | it("logs outbound websocket push events in dev mode", async () => { | ||
| 136 | const logSpy = vi.spyOn(console, "log").mockImplementation(() => { | ||
| 137 | // Keep test output clean while verifying websocket logs. | ||
| 138 | }); | ||
| 139 | |||
| 140 | server = createTestServer({ | ||
| 141 | cwd: "/test/project", | ||
| 142 | devUrl: "http://localhost:5173", | ||
| 143 | }); | ||
| 144 | await server.start(); | ||
| 145 | const addr = server.httpServer.address(); | ||
| 146 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | ||
| 147 | expect(port).toBeGreaterThan(0); | ||
| 148 | |||
| 149 | const ws = await connectWs(port); | ||
| 150 | connections.push(ws); | ||
| 151 | await waitForMessage(ws); | ||
| 152 | |||
| 153 | expect( | ||
| 154 | logSpy.mock.calls.some(([message]) => { | ||
| 155 | if (typeof message !== "string") return false; | ||
| 156 | return ( | ||
| 157 | message.includes("[ws]") && | ||
| 158 | message.includes("outgoing push") && | ||
| 159 | message.includes(`channel="${WS_CHANNELS.serverWelcome}"`) | ||
| 160 | ); | ||
| 161 | }), | ||
| 162 | ).toBe(true); | ||
| 163 | }); | ||
| 164 | |||
| 165 | it("responds to server.getConfig", async () => { | ||
| 166 | server = createTestServer({ cwd: "/my/workspace" }); | ||
| 167 | await server.start(); | ||
| 168 | const addr = server.httpServer.address(); | ||
| 169 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | ||
| 170 | |||
| 171 | const ws = await connectWs(port); | ||
| 172 | connections.push(ws); | ||
| 173 | |||
| 174 | // Consume welcome message | ||
| 175 | await waitForMessage(ws); | ||
| 176 | |||
| 177 | const response = await sendRequest(ws, WS_METHODS.serverGetConfig); | ||
| 178 | expect(response.error).toBeUndefined(); | ||
| 179 | expect(response.result).toEqual({ cwd: "/my/workspace" }); | ||
| 180 | }); | ||
| 181 | |||
| 182 | it("returns error for unknown methods", async () => { | ||
| 183 | server = createTestServer({ cwd: "/test" }); | ||
| 184 | await server.start(); | ||
| 185 | const addr = server.httpServer.address(); | ||
| 186 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | ||
| 187 | |||
| 188 | const ws = await connectWs(port); | ||
| 189 | connections.push(ws); | ||
| 190 | |||
| 191 | // Consume welcome | ||
| 192 | await waitForMessage(ws); | ||
| 193 | |||
| 194 | const response = await sendRequest(ws, "nonexistent.method"); | ||
| 195 | expect(response.error).toBeDefined(); | ||
| 196 | expect(response.error!.message).toContain("Unknown method"); | ||
| 197 | }); | ||
| 198 | |||
| 199 | it("responds to providers.listSessions with empty array", async () => { | ||
| 200 | server = createTestServer({ cwd: "/test" }); | ||
| 201 | await server.start(); | ||
| 202 | const addr = server.httpServer.address(); | ||
| 203 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | ||
| 204 | |||
| 205 | const ws = await connectWs(port); | ||
| 206 | connections.push(ws); | ||
| 207 | |||
| 208 | // Consume welcome | ||
| 209 | await waitForMessage(ws); | ||
| 210 | |||
| 211 | const response = await sendRequest(ws, WS_METHODS.providersListSessions); | ||
| 212 | expect(response.error).toBeUndefined(); | ||
| 213 | expect(response.result).toEqual([]); | ||
| 214 | }); | ||
| 215 | |||
| 216 | it("handles invalid JSON gracefully", async () => { | ||
| 217 | server = createTestServer({ cwd: "/test" }); | ||
| 218 | await server.start(); | ||
| 219 | const addr = server.httpServer.address(); | ||
| 220 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | ||
| 221 | |||
| 222 | const ws = await connectWs(port); | ||
| 223 | connections.push(ws); | ||
| 224 | |||
| 225 | // Consume welcome | ||
| 226 | await waitForMessage(ws); | ||
| 227 | |||
| 228 | // Send garbage | ||
| 229 | ws.send("not json at all"); | ||
| 230 | |||
| 231 | const response = (await waitForMessage(ws)) as WsResponse; | ||
| 232 | expect(response.error).toBeDefined(); | ||
| 233 | expect(response.error!.message).toContain("Invalid request format"); | ||
| 234 | }); | ||
| 235 | |||
| 236 | it("supports projects list/add/dedupe/remove", async () => { | ||
| 237 | const stateDir = makeTempDir("codething-ws-projects-state-"); | ||
| 238 | const firstProjectCwd = makeTempDir("codething-ws-project-a-"); | ||
| 239 | |||
| 240 | server = createTestServer({ stateDir, cwd: "/test" }); | ||
| 241 | await server.start(); | ||
| 242 | const addr = server.httpServer.address(); | ||
| 243 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | ||
| 244 | |||
| 245 | const ws = await connectWs(port); | ||
| 246 | connections.push(ws); | ||
| 247 | await waitForMessage(ws); | ||
| 248 | |||
| 249 | const emptyList = await sendRequest(ws, WS_METHODS.projectsList); | ||
| 250 | expect(emptyList.error).toBeUndefined(); | ||
| 251 | expect(emptyList.result).toEqual([]); | ||
| 252 | |||
| 253 | const created = await sendRequest(ws, WS_METHODS.projectsAdd, { | ||
| 254 | cwd: firstProjectCwd, | ||
| 255 | }); | ||
| 256 | expect(created.error).toBeUndefined(); | ||
| 257 | expect((created.result as { created: boolean }).created).toBe(true); | ||
| 258 | |||
| 259 | const duplicate = await sendRequest(ws, WS_METHODS.projectsAdd, { | ||
| 260 | cwd: firstProjectCwd, | ||
| 261 | }); | ||
| 262 | expect(duplicate.error).toBeUndefined(); | ||
| 263 | expect((duplicate.result as { created: boolean }).created).toBe(false); | ||
| 264 | |||
| 265 | const listed = await sendRequest(ws, WS_METHODS.projectsList); | ||
| 266 | expect(listed.error).toBeUndefined(); | ||
| 267 | const listedProjects = listed.result as Array<{ id: string; cwd: string }>; | ||
| 268 | expect(listedProjects).toHaveLength(1); | ||
| 269 | |||
| 270 | const projectId = listedProjects[0]?.id; | ||
| 271 | expect(projectId).toBeTruthy(); | ||
| 272 | if (!projectId) return; | ||
| 273 | |||
| 274 | const removed = await sendRequest(ws, WS_METHODS.projectsRemove, { | ||
| 275 | id: projectId, | ||
| 276 | }); | ||
| 277 | expect(removed.error).toBeUndefined(); | ||
| 278 | |||
| 279 | const afterRemove = await sendRequest(ws, WS_METHODS.projectsList); | ||
| 280 | expect(afterRemove.error).toBeUndefined(); | ||
| 281 | expect(afterRemove.result).toEqual([]); | ||
| 282 | }); | ||
| 283 | |||
| 284 | it("prunes missing projects on startup", async () => { | ||
| 285 | const stateDir = makeTempDir("codething-ws-prune-state-"); | ||
| 286 | const existing = makeTempDir("codething-ws-existing-project-"); | ||
| 287 | const missing = path.join(stateDir, "definitely-missing"); | ||
| 288 | const now = new Date().toISOString(); | ||
| 289 | const projectsFile = path.join(stateDir, "projects.json"); | ||
| 290 | |||
| 291 | fs.writeFileSync( | ||
| 292 | projectsFile, | ||
| 293 | JSON.stringify( | ||
| 294 | { | ||
| 295 | version: 1, | ||
| 296 | projects: [ | ||
| 297 | { | ||
| 298 | id: "project-existing", | ||
| 299 | cwd: existing, | ||
| 300 | name: "existing", | ||
| 301 | createdAt: now, | ||
| 302 | updatedAt: now, | ||
| 303 | }, | ||
| 304 | { | ||
| 305 | id: "project-missing", | ||
| 306 | cwd: missing, | ||
| 307 | name: "missing", | ||
| 308 | createdAt: now, | ||
| 309 | updatedAt: now, | ||
| 310 | }, | ||
| 311 | ], | ||
| 312 | }, | ||
| 313 | null, | ||
| 314 | 2, | ||
| 315 | ), | ||
| 316 | ); | ||
| 317 | |||
| 318 | server = createTestServer({ stateDir, cwd: "/test" }); | ||
| 319 | await server.start(); | ||
| 320 | const addr = server.httpServer.address(); | ||
| 321 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | ||
| 322 | |||
| 323 | const ws = await connectWs(port); | ||
| 324 | connections.push(ws); | ||
| 325 | await waitForMessage(ws); | ||
| 326 | |||
| 327 | const response = await sendRequest(ws, WS_METHODS.projectsList); | ||
| 328 | expect(response.error).toBeUndefined(); | ||
| 329 | const listed = response.result as Array<{ id: string }>; | ||
| 330 | expect(listed).toHaveLength(1); | ||
| 331 | expect(listed[0]?.id).toBe("project-existing"); | ||
| 332 | }); | ||
| 333 | |||
| 334 | it("rejects websocket connections without a valid auth token", async () => { | ||
| 335 | server = createTestServer({ cwd: "/test", authToken: "secret-token" }); | ||
| 336 | await server.start(); | ||
| 337 | const addr = server.httpServer.address(); | ||
| 338 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | ||
| 339 | |||
| 340 | await expect(connectWs(port)).rejects.toThrow("WebSocket connection failed"); | ||
| 341 | |||
| 342 | const authorizedWs = await connectWs(port, "secret-token"); | ||
| 343 | connections.push(authorizedWs); | ||
| 344 | const welcome = (await waitForMessage(authorizedWs)) as WsPush; | ||
| 345 | expect(welcome.channel).toBe(WS_CHANNELS.serverWelcome); | ||
| 346 | }); | ||
| 347 | }); | ||
apps/server/src/wsServer.ts created+373| ... | @@ -0,0 +1,373 @@ | ||
| 1 | import { spawn } from "node:child_process"; | ||
| 2 | import fs from "node:fs"; | ||
| 3 | import http from "node:http"; | ||
| 4 | import os from "node:os"; | ||
| 5 | import path from "node:path"; | ||
| 6 | import type { Duplex } from "node:stream"; | ||
| 7 | |||
| 8 | import { | ||
| 9 | EDITORS, | ||
| 10 | WS_CHANNELS, | ||
| 11 | WS_METHODS, | ||
| 12 | type WsPush, | ||
| 13 | type WsRequest, | ||
| 14 | type WsResponse, | ||
| 15 | wsRequestSchema, | ||
| 16 | } from "@acme/contracts"; | ||
| 17 | import { WebSocketServer, type WebSocket } from "ws"; | ||
| 18 | |||
| 19 | import { createLogger } from "./logger"; | ||
| 20 | import { ProjectRegistry } from "./projectRegistry"; | ||
| 21 | import { ProviderManager } from "./providerManager"; | ||
| 22 | |||
| 23 | const MIME_TYPES: Record<string, string> = { | ||
| 24 | ".html": "text/html; charset=utf-8", | ||
| 25 | ".js": "application/javascript; charset=utf-8", | ||
| 26 | ".css": "text/css; charset=utf-8", | ||
| 27 | ".json": "application/json; charset=utf-8", | ||
| 28 | ".png": "image/png", | ||
| 29 | ".jpg": "image/jpeg", | ||
| 30 | ".svg": "image/svg+xml", | ||
| 31 | ".ico": "image/x-icon", | ||
| 32 | ".woff": "font/woff", | ||
| 33 | ".woff2": "font/woff2", | ||
| 34 | ".ttf": "font/ttf", | ||
| 35 | ".map": "application/json", | ||
| 36 | }; | ||
| 37 | |||
| 38 | export interface ServerOptions { | ||
| 39 | port: number; | ||
| 40 | host?: string | undefined; | ||
| 41 | cwd: string; | ||
| 42 | staticDir?: string | undefined; | ||
| 43 | devUrl?: string | undefined; | ||
| 44 | logWebSocketEvents?: boolean | undefined; | ||
| 45 | projectRegistry?: ProjectRegistry | undefined; | ||
| 46 | authToken?: string | undefined; | ||
| 47 | } | ||
| 48 | |||
| 49 | function parseBooleanEnv(value: string | undefined): boolean | undefined { | ||
| 50 | if (value === undefined) return undefined; | ||
| 51 | const normalized = value.trim().toLowerCase(); | ||
| 52 | if (["1", "true", "yes", "on"].includes(normalized)) return true; | ||
| 53 | if (["0", "false", "no", "off"].includes(normalized)) return false; | ||
| 54 | return undefined; | ||
| 55 | } | ||
| 56 | |||
| 57 | export function createServer(options: ServerOptions) { | ||
| 58 | const { | ||
| 59 | port, | ||
| 60 | host, | ||
| 61 | cwd, | ||
| 62 | staticDir, | ||
| 63 | devUrl, | ||
| 64 | logWebSocketEvents: explicitLogWsEvents, | ||
| 65 | projectRegistry: providedRegistry, | ||
| 66 | authToken, | ||
| 67 | } = options; | ||
| 68 | const providerManager = new ProviderManager(); | ||
| 69 | const projectRegistry = | ||
| 70 | providedRegistry ?? new ProjectRegistry(path.join(os.homedir(), ".t3", "userdata")); | ||
| 71 | const clients = new Set<WebSocket>(); | ||
| 72 | const logger = createLogger("ws"); | ||
| 73 | const logWebSocketEvents = | ||
| 74 | explicitLogWsEvents ?? parseBooleanEnv(process.env.CODETHING_LOG_WS_EVENTS) ?? Boolean(devUrl); | ||
| 75 | |||
| 76 | function logOutgoingPush(push: WsPush, recipients: number) { | ||
| 77 | if (!logWebSocketEvents) return; | ||
| 78 | logger.event("outgoing push", { | ||
| 79 | channel: push.channel, | ||
| 80 | recipients, | ||
| 81 | payload: push.data, | ||
| 82 | }); | ||
| 83 | } | ||
| 84 | |||
| 85 | // Forward provider events to all connected WebSocket clients | ||
| 86 | providerManager.on("event", (event) => { | ||
| 87 | const push: WsPush = { | ||
| 88 | type: "push", | ||
| 89 | channel: WS_CHANNELS.providerEvent, | ||
| 90 | data: event, | ||
| 91 | }; | ||
| 92 | const message = JSON.stringify(push); | ||
| 93 | let recipients = 0; | ||
| 94 | for (const client of clients) { | ||
| 95 | if (client.readyState === client.OPEN) { | ||
| 96 | client.send(message); | ||
| 97 | recipients += 1; | ||
| 98 | } | ||
| 99 | } | ||
| 100 | logOutgoingPush(push, recipients); | ||
| 101 | }); | ||
| 102 | |||
| 103 | // HTTP server — serves static files or redirects to Vite dev server | ||
| 104 | const httpServer = http.createServer((req, res) => { | ||
| 105 | // In dev mode, redirect to Vite dev server | ||
| 106 | if (devUrl) { | ||
| 107 | res.writeHead(302, { Location: devUrl }); | ||
| 108 | res.end(); | ||
| 109 | return; | ||
| 110 | } | ||
| 111 | |||
| 112 | // Serve static files from the renderer build | ||
| 113 | if (!staticDir) { | ||
| 114 | res.writeHead(503, { "Content-Type": "text/plain" }); | ||
| 115 | res.end("No static directory configured and no dev URL set."); | ||
| 116 | return; | ||
| 117 | } | ||
| 118 | |||
| 119 | const url = new URL(req.url ?? "/", `http://localhost:${port}`); | ||
| 120 | let filePath = path.join(staticDir, url.pathname); | ||
| 121 | |||
| 122 | // SPA fallback: if no file extension and not found, serve index.html | ||
| 123 | const ext = path.extname(filePath); | ||
| 124 | if (!ext) { | ||
| 125 | filePath = path.join(filePath, "index.html"); | ||
| 126 | } | ||
| 127 | |||
| 128 | fs.stat(filePath, (err, stats) => { | ||
| 129 | if (err || !stats?.isFile()) { | ||
| 130 | // SPA fallback | ||
| 131 | const indexPath = path.join(staticDir, "index.html"); | ||
| 132 | fs.readFile(indexPath, (readErr, data) => { | ||
| 133 | if (readErr) { | ||
| 134 | res.writeHead(404, { "Content-Type": "text/plain" }); | ||
| 135 | res.end("Not Found"); | ||
| 136 | return; | ||
| 137 | } | ||
| 138 | res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); | ||
| 139 | res.end(data); | ||
| 140 | }); | ||
| 141 | return; | ||
| 142 | } | ||
| 143 | |||
| 144 | const fileExt = path.extname(filePath); | ||
| 145 | const contentType = MIME_TYPES[fileExt] ?? "application/octet-stream"; | ||
| 146 | |||
| 147 | fs.readFile(filePath, (readErr, data) => { | ||
| 148 | if (readErr) { | ||
| 149 | res.writeHead(500, { "Content-Type": "text/plain" }); | ||
| 150 | res.end("Internal Server Error"); | ||
| 151 | return; | ||
| 152 | } | ||
| 153 | res.writeHead(200, { "Content-Type": contentType }); | ||
| 154 | res.end(data); | ||
| 155 | }); | ||
| 156 | }); | ||
| 157 | }); | ||
| 158 | |||
| 159 | // WebSocket server — upgrades from the HTTP server | ||
| 160 | const wss = new WebSocketServer({ noServer: true }); | ||
| 161 | |||
| 162 | function rejectUpgrade(socket: Duplex, statusCode: number, message: string): void { | ||
| 163 | socket.write( | ||
| 164 | `HTTP/1.1 ${statusCode} ${statusCode === 401 ? "Unauthorized" : "Bad Request"}\r\n` + | ||
| 165 | "Connection: close\r\n" + | ||
| 166 | "Content-Type: text/plain\r\n" + | ||
| 167 | `Content-Length: ${Buffer.byteLength(message)}\r\n` + | ||
| 168 | "\r\n" + | ||
| 169 | message, | ||
| 170 | ); | ||
| 171 | socket.destroy(); | ||
| 172 | } | ||
| 173 | |||
| 174 | httpServer.on("upgrade", (request, socket, head) => { | ||
| 175 | if (authToken) { | ||
| 176 | let providedToken: string | null = null; | ||
| 177 | try { | ||
| 178 | const url = new URL(request.url ?? "/", `http://localhost:${port}`); | ||
| 179 | providedToken = url.searchParams.get("token"); | ||
| 180 | } catch { | ||
| 181 | rejectUpgrade(socket, 400, "Invalid WebSocket URL"); | ||
| 182 | return; | ||
| 183 | } | ||
| 184 | |||
| 185 | if (providedToken !== authToken) { | ||
| 186 | rejectUpgrade(socket, 401, "Unauthorized WebSocket connection"); | ||
| 187 | return; | ||
| 188 | } | ||
| 189 | } | ||
| 190 | |||
| 191 | wss.handleUpgrade(request, socket, head, (ws) => { | ||
| 192 | wss.emit("connection", ws, request); | ||
| 193 | }); | ||
| 194 | }); | ||
| 195 | |||
| 196 | wss.on("connection", (ws) => { | ||
| 197 | clients.add(ws); | ||
| 198 | |||
| 199 | // Send welcome message with project info | ||
| 200 | const segments = cwd.split(/[/\\]/).filter(Boolean); | ||
| 201 | const projectName = segments[segments.length - 1] ?? "project"; | ||
| 202 | |||
| 203 | const welcome: WsPush = { | ||
| 204 | type: "push", | ||
| 205 | channel: WS_CHANNELS.serverWelcome, | ||
| 206 | data: { cwd, projectName }, | ||
| 207 | }; | ||
| 208 | logOutgoingPush(welcome, 1); | ||
| 209 | ws.send(JSON.stringify(welcome)); | ||
| 210 | |||
| 211 | ws.on("message", (raw) => { | ||
| 212 | void handleMessage(ws, raw); | ||
| 213 | }); | ||
| 214 | |||
| 215 | ws.on("close", () => { | ||
| 216 | clients.delete(ws); | ||
| 217 | }); | ||
| 218 | |||
| 219 | ws.on("error", () => { | ||
| 220 | clients.delete(ws); | ||
| 221 | }); | ||
| 222 | }); | ||
| 223 | |||
| 224 | async function handleMessage(ws: WebSocket, raw: unknown) { | ||
| 225 | let request: WsRequest; | ||
| 226 | try { | ||
| 227 | const parsed = JSON.parse(String(raw)); | ||
| 228 | request = wsRequestSchema.parse(parsed); | ||
| 229 | } catch { | ||
| 230 | const errorResponse: WsResponse = { | ||
| 231 | id: "unknown", | ||
| 232 | error: { message: "Invalid request format" }, | ||
| 233 | }; | ||
| 234 | ws.send(JSON.stringify(errorResponse)); | ||
| 235 | return; | ||
| 236 | } | ||
| 237 | |||
| 238 | try { | ||
| 239 | const result = await routeRequest(request); | ||
| 240 | const response: WsResponse = { id: request.id, result }; | ||
| 241 | ws.send(JSON.stringify(response)); | ||
| 242 | } catch (err) { | ||
| 243 | const message = err instanceof Error ? err.message : "Unknown server error"; | ||
| 244 | const response: WsResponse = { | ||
| 245 | id: request.id, | ||
| 246 | error: { message }, | ||
| 247 | }; | ||
| 248 | ws.send(JSON.stringify(response)); | ||
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 252 | async function routeRequest(request: WsRequest): Promise<unknown> { | ||
| 253 | switch (request.method) { | ||
| 254 | case WS_METHODS.providersStartSession: | ||
| 255 | return providerManager.startSession(request.params as never); | ||
| 256 | |||
| 257 | case WS_METHODS.providersSendTurn: | ||
| 258 | return providerManager.sendTurn(request.params as never); | ||
| 259 | |||
| 260 | case WS_METHODS.providersInterruptTurn: | ||
| 261 | return providerManager.interruptTurn(request.params as never); | ||
| 262 | |||
| 263 | case WS_METHODS.providersRespondToRequest: | ||
| 264 | return providerManager.respondToRequest(request.params as never); | ||
| 265 | |||
| 266 | case WS_METHODS.providersStopSession: { | ||
| 267 | providerManager.stopSession(request.params as never); | ||
| 268 | return undefined; | ||
| 269 | } | ||
| 270 | |||
| 271 | case WS_METHODS.providersListSessions: | ||
| 272 | return providerManager.listSessions(); | ||
| 273 | |||
| 274 | case WS_METHODS.projectsList: | ||
| 275 | return projectRegistry.list(); | ||
| 276 | |||
| 277 | case WS_METHODS.projectsAdd: | ||
| 278 | return projectRegistry.add(request.params as never); | ||
| 279 | |||
| 280 | case WS_METHODS.projectsRemove: | ||
| 281 | projectRegistry.remove(request.params as never); | ||
| 282 | return undefined; | ||
| 283 | |||
| 284 | case WS_METHODS.shellOpenInEditor: { | ||
| 285 | const params = request.params as { | ||
| 286 | cwd: string; | ||
| 287 | editor: string; | ||
| 288 | }; | ||
| 289 | if (!params?.cwd) throw new Error("cwd is required"); | ||
| 290 | const editorDef = EDITORS.find((e) => e.id === params.editor); | ||
| 291 | if (!editorDef) throw new Error(`Unknown editor: ${params.editor}`); | ||
| 292 | if (!editorDef.command) { | ||
| 293 | // Can't open file manager programmatically from server, just succeed silently | ||
| 294 | return undefined; | ||
| 295 | } | ||
| 296 | const child = spawn(editorDef.command, [params.cwd], { | ||
| 297 | detached: true, | ||
| 298 | stdio: "ignore", | ||
| 299 | }); | ||
| 300 | child.on("error", () => { | ||
| 301 | /* ignore spawn failures for detached editors */ | ||
| 302 | }); | ||
| 303 | child.unref(); | ||
| 304 | return undefined; | ||
| 305 | } | ||
| 306 | |||
| 307 | case WS_METHODS.serverGetConfig: | ||
| 308 | return { cwd }; | ||
| 309 | |||
| 310 | default: | ||
| 311 | throw new Error(`Unknown method: ${request.method}`); | ||
| 312 | } | ||
| 313 | } | ||
| 314 | |||
| 315 | function start() { | ||
| 316 | return new Promise<void>((resolve, reject) => { | ||
| 317 | const onError = (error: Error) => { | ||
| 318 | httpServer.off("error", onError); | ||
| 319 | reject(error); | ||
| 320 | }; | ||
| 321 | httpServer.once("error", onError); | ||
| 322 | const onListening = () => { | ||
| 323 | httpServer.off("error", onError); | ||
| 324 | resolve(); | ||
| 325 | }; | ||
| 326 | if (host) { | ||
| 327 | httpServer.listen(port, host, onListening); | ||
| 328 | return; | ||
| 329 | } | ||
| 330 | httpServer.listen(port, onListening); | ||
| 331 | }); | ||
| 332 | } | ||
| 333 | |||
| 334 | async function stop(): Promise<void> { | ||
| 335 | providerManager.stopAll(); | ||
| 336 | providerManager.dispose(); | ||
| 337 | |||
| 338 | for (const client of clients) { | ||
| 339 | client.close(); | ||
| 340 | } | ||
| 341 | clients.clear(); | ||
| 342 | |||
| 343 | const isServerNotRunningError = (error: unknown): boolean => { | ||
| 344 | if (!(error instanceof Error)) return false; | ||
| 345 | const maybeCode = (error as NodeJS.ErrnoException).code; | ||
| 346 | return maybeCode === "ERR_SERVER_NOT_RUNNING" || error.message.toLowerCase().includes("not running"); | ||
| 347 | }; | ||
| 348 | |||
| 349 | const closeWebSocketServer = new Promise<void>((resolve, reject) => { | ||
| 350 | wss.close((error) => { | ||
| 351 | if (error && !isServerNotRunningError(error)) { | ||
| 352 | reject(error); | ||
| 353 | return; | ||
| 354 | } | ||
| 355 | resolve(); | ||
| 356 | }); | ||
| 357 | }); | ||
| 358 | |||
| 359 | const closeHttpServer = new Promise<void>((resolve, reject) => { | ||
| 360 | httpServer.close((error) => { | ||
| 361 | if (error && !isServerNotRunningError(error)) { | ||
| 362 | reject(error); | ||
| 363 | return; | ||
| 364 | } | ||
| 365 | resolve(); | ||
| 366 | }); | ||
| 367 | }); | ||
| 368 | |||
| 369 | await Promise.all([closeWebSocketServer, closeHttpServer]); | ||
| 370 | } | ||
| 371 | |||
| 372 | return { start, stop, httpServer }; | ||
| 373 | } | ||
apps/server/tsconfig.json created+9| ... | @@ -0,0 +1,9 @@ | ||
| 1 | { | ||
| 2 | "extends": "../../tsconfig.base.json", | ||
| 3 | "compilerOptions": { | ||
| 4 | "composite": true, | ||
| 5 | "types": ["node"], | ||
| 6 | "lib": ["ES2023"] | ||
| 7 | }, | ||
| 8 | "include": ["src", "tsup.config.ts"] | ||
| 9 | } | ||
apps/server/tsup.config.ts created+13| ... | @@ -0,0 +1,13 @@ | ||
| 1 | import { defineConfig } from "tsup"; | ||
| 2 | |||
| 3 | export default defineConfig({ | ||
| 4 | entry: ["src/index.ts"], | ||
| 5 | format: "esm", | ||
| 6 | outDir: "dist", | ||
| 7 | sourcemap: true, | ||
| 8 | clean: true, | ||
| 9 | noExternal: ["@acme/contracts"], | ||
| 10 | banner: { | ||
| 11 | js: '#!/usr/bin/env node\n', | ||
| 12 | }, | ||
| 13 | }); | ||
bun.lock+56-232| ... | @@ -16,17 +16,15 @@ | ... | @@ -16,17 +16,15 @@ |
| 16 | "name": "@acme/desktop", | 16 | "name": "@acme/desktop", |
| 17 | "version": "0.0.0", | 17 | "version": "0.0.0", |
| 18 | "dependencies": { | 18 | "dependencies": { |
| 19 | "@acme/contracts": "workspace:*", | ||
| 20 | "electron": "33.4.11", | 19 | "electron": "33.4.11", |
| 21 | "node-pty": "^1.0.0", | ||
| 22 | }, | 20 | }, |
| 23 | "devDependencies": { | 21 | "devDependencies": { |
| 24 | "@electron/rebuild": "^3.7.0", | ||
| 25 | "@types/node": "^22.10.2", | 22 | "@types/node": "^22.10.2", |
| 26 | "concurrently": "^9.1.2", | 23 | "concurrently": "^9.1.2", |
| 27 | "electronmon": "^2.0.2", | 24 | "electronmon": "^2.0.2", |
| 28 | "tsup": "^8.3.5", | 25 | "tsup": "^8.3.5", |
| 29 | "typescript": "^5.7.3", | 26 | "typescript": "^5.7.3", |
| 27 | "vitest": "^3.0.0", | ||
| 30 | "wait-on": "^8.0.2", | 28 | "wait-on": "^8.0.2", |
| 31 | }, | 29 | }, |
| 32 | }, | 30 | }, |
| ... | @@ -55,6 +53,26 @@ | ... | @@ -55,6 +53,26 @@ |
| 55 | "vite": "^6.0.5", | 53 | "vite": "^6.0.5", |
| 56 | }, | 54 | }, |
| 57 | }, | 55 | }, |
| 56 | "apps/server": { | ||
| 57 | "name": "t3", | ||
| 58 | "version": "0.1.0", | ||
| 59 | "bin": { | ||
| 60 | "t3": "./dist/index.js", | ||
| 61 | }, | ||
| 62 | "dependencies": { | ||
| 63 | "open": "^10.1.0", | ||
| 64 | "ws": "^8.18.0", | ||
| 65 | }, | ||
| 66 | "devDependencies": { | ||
| 67 | "@acme/contracts": "workspace:*", | ||
| 68 | "@acme/renderer": "workspace:*", | ||
| 69 | "@types/node": "^22.10.2", | ||
| 70 | "@types/ws": "^8.5.13", | ||
| 71 | "tsup": "^8.3.5", | ||
| 72 | "tsx": "^4.19.0", | ||
| 73 | "typescript": "^5.7.3", | ||
| 74 | }, | ||
| 75 | }, | ||
| 58 | "packages/contracts": { | 76 | "packages/contracts": { |
| 59 | "name": "@acme/contracts", | 77 | "name": "@acme/contracts", |
| 60 | "version": "0.0.0", | 78 | "version": "0.0.0", |
| ... | @@ -114,10 +132,6 @@ | ... | @@ -114,10 +132,6 @@ |
| 114 | 132 | ||
| 115 | "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], | 133 | "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], |
| 116 | 134 | ||
| 117 | "@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"], | ||
| 118 | |||
| 119 | "@electron/rebuild": ["@electron/rebuild@3.7.2", "", { "dependencies": { "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "fs-extra": "^10.0.0", "got": "^11.7.0", "node-abi": "^3.45.0", "node-api-version": "^0.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^6.0.5", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg=="], | ||
| 120 | |||
| 121 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], | 135 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], |
| 122 | 136 | ||
| 123 | "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], | 137 | "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], |
| ... | @@ -170,8 +184,6 @@ | ... | @@ -170,8 +184,6 @@ |
| 170 | 184 | ||
| 171 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], | 185 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], |
| 172 | 186 | ||
| 173 | "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], | ||
| 174 | |||
| 175 | "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="], | 187 | "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="], |
| 176 | 188 | ||
| 177 | "@hapi/formula": ["@hapi/formula@3.0.2", "", {}, "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw=="], | 189 | "@hapi/formula": ["@hapi/formula@3.0.2", "", {}, "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw=="], |
| ... | @@ -194,12 +206,6 @@ | ... | @@ -194,12 +206,6 @@ |
| 194 | 206 | ||
| 195 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], | 207 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], |
| 196 | 208 | ||
| 197 | "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], | ||
| 198 | |||
| 199 | "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="], | ||
| 200 | |||
| 201 | "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="], | ||
| 202 | |||
| 203 | "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww=="], | 209 | "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww=="], |
| 204 | 210 | ||
| 205 | "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-S6vlV8S7jbjzJOSjfVg2CimUC0r7/aHDLdUm/3+/B/SU/s1jV7ivqWkMv1/8EB43d1BBwT9JQ60ZMTkBqeXSFA=="], | 211 | "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-S6vlV8S7jbjzJOSjfVg2CimUC0r7/aHDLdUm/3+/B/SU/s1jV7ivqWkMv1/8EB43d1BBwT9JQ60ZMTkBqeXSFA=="], |
| ... | @@ -320,8 +326,6 @@ | ... | @@ -320,8 +326,6 @@ |
| 320 | 326 | ||
| 321 | "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], | 327 | "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], |
| 322 | 328 | ||
| 323 | "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], | ||
| 324 | |||
| 325 | "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], | 329 | "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], |
| 326 | 330 | ||
| 327 | "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], | 331 | "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], |
| ... | @@ -362,6 +366,8 @@ | ... | @@ -362,6 +366,8 @@ |
| 362 | 366 | ||
| 363 | "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], | 367 | "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], |
| 364 | 368 | ||
| 369 | "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], | ||
| 370 | |||
| 365 | "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], | 371 | "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], |
| 366 | 372 | ||
| 367 | "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], | 373 | "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], |
| ... | @@ -382,16 +388,8 @@ | ... | @@ -382,16 +388,8 @@ |
| 382 | 388 | ||
| 383 | "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], | 389 | "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], |
| 384 | 390 | ||
| 385 | "abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="], | ||
| 386 | |||
| 387 | "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], | 391 | "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], |
| 388 | 392 | ||
| 389 | "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], | ||
| 390 | |||
| 391 | "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], | ||
| 392 | |||
| 393 | "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], | ||
| 394 | |||
| 395 | "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], | 393 | "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], |
| 396 | 394 | ||
| 397 | "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], | 395 | "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], |
| ... | @@ -402,38 +400,28 @@ | ... | @@ -402,38 +400,28 @@ |
| 402 | 400 | ||
| 403 | "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], | 401 | "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], |
| 404 | 402 | ||
| 405 | "axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="], | 403 | "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], |
| 406 | 404 | ||
| 407 | "babel-plugin-react-compiler": ["babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-q84bNR9JG1crykAlJUt5Ud0/5BUyMFuQww/mrwIQDFBaxsikqBDj3f/FNDsVd2iR26A1HvXKWPEIfgJDv8/V2g=="], | 405 | "babel-plugin-react-compiler": ["babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-q84bNR9JG1crykAlJUt5Ud0/5BUyMFuQww/mrwIQDFBaxsikqBDj3f/FNDsVd2iR26A1HvXKWPEIfgJDv8/V2g=="], |
| 408 | 406 | ||
| 409 | "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], | 407 | "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], |
| 410 | 408 | ||
| 411 | "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], | ||
| 412 | |||
| 413 | "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], | ||
| 414 | |||
| 415 | "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], | 409 | "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], |
| 416 | 410 | ||
| 417 | "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], | ||
| 418 | |||
| 419 | "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], | 411 | "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], |
| 420 | 412 | ||
| 421 | "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], | ||
| 422 | |||
| 423 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], | 413 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], |
| 424 | 414 | ||
| 425 | "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], | 415 | "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], |
| 426 | 416 | ||
| 427 | "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], | ||
| 428 | |||
| 429 | "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], | 417 | "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], |
| 430 | 418 | ||
| 419 | "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], | ||
| 420 | |||
| 431 | "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], | 421 | "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], |
| 432 | 422 | ||
| 433 | "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], | 423 | "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], |
| 434 | 424 | ||
| 435 | "cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], | ||
| 436 | |||
| 437 | "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], | 425 | "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], |
| 438 | 426 | ||
| 439 | "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], | 427 | "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], |
| ... | @@ -460,18 +448,8 @@ | ... | @@ -460,18 +448,8 @@ |
| 460 | 448 | ||
| 461 | "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], | 449 | "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], |
| 462 | 450 | ||
| 463 | "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], | ||
| 464 | |||
| 465 | "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], | ||
| 466 | |||
| 467 | "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], | ||
| 468 | |||
| 469 | "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], | ||
| 470 | |||
| 471 | "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], | 451 | "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], |
| 472 | 452 | ||
| 473 | "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], | ||
| 474 | |||
| 475 | "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], | 453 | "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], |
| 476 | 454 | ||
| 477 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], | 455 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], |
| ... | @@ -484,8 +462,6 @@ | ... | @@ -484,8 +462,6 @@ |
| 484 | 462 | ||
| 485 | "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], | 463 | "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], |
| 486 | 464 | ||
| 487 | "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], | ||
| 488 | |||
| 489 | "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], | 465 | "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], |
| 490 | 466 | ||
| 491 | "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], | 467 | "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], |
| ... | @@ -494,8 +470,6 @@ | ... | @@ -494,8 +470,6 @@ |
| 494 | 470 | ||
| 495 | "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], | 471 | "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], |
| 496 | 472 | ||
| 497 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], | ||
| 498 | |||
| 499 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], | 473 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], |
| 500 | 474 | ||
| 501 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], | 475 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], |
| ... | @@ -506,12 +480,16 @@ | ... | @@ -506,12 +480,16 @@ |
| 506 | 480 | ||
| 507 | "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], | 481 | "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], |
| 508 | 482 | ||
| 509 | "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], | 483 | "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], |
| 484 | |||
| 485 | "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], | ||
| 510 | 486 | ||
| 511 | "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], | 487 | "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], |
| 512 | 488 | ||
| 513 | "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], | 489 | "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], |
| 514 | 490 | ||
| 491 | "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], | ||
| 492 | |||
| 515 | "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], | 493 | "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], |
| 516 | 494 | ||
| 517 | "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], | 495 | "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], |
| ... | @@ -534,16 +512,12 @@ | ... | @@ -534,16 +512,12 @@ |
| 534 | 512 | ||
| 535 | "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], | 513 | "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], |
| 536 | 514 | ||
| 537 | "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], | ||
| 538 | |||
| 539 | "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], | 515 | "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], |
| 540 | 516 | ||
| 541 | "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], | 517 | "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], |
| 542 | 518 | ||
| 543 | "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], | 519 | "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], |
| 544 | 520 | ||
| 545 | "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], | ||
| 546 | |||
| 547 | "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], | 521 | "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], |
| 548 | 522 | ||
| 549 | "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], | 523 | "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], |
| ... | @@ -568,8 +542,6 @@ | ... | @@ -568,8 +542,6 @@ |
| 568 | 542 | ||
| 569 | "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], | 543 | "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], |
| 570 | 544 | ||
| 571 | "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], | ||
| 572 | |||
| 573 | "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], | 545 | "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], |
| 574 | 546 | ||
| 575 | "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], | 547 | "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], |
| ... | @@ -586,11 +558,7 @@ | ... | @@ -586,11 +558,7 @@ |
| 586 | 558 | ||
| 587 | "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], | 559 | "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], |
| 588 | 560 | ||
| 589 | "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], | 561 | "fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], |
| 590 | |||
| 591 | "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], | ||
| 592 | |||
| 593 | "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], | ||
| 594 | 562 | ||
| 595 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | 563 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], |
| 596 | 564 | ||
| ... | @@ -606,7 +574,7 @@ | ... | @@ -606,7 +574,7 @@ |
| 606 | 574 | ||
| 607 | "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], | 575 | "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], |
| 608 | 576 | ||
| 609 | "glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], | 577 | "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], |
| 610 | 578 | ||
| 611 | "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], | 579 | "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], |
| 612 | 580 | ||
| ... | @@ -642,55 +610,31 @@ | ... | @@ -642,55 +610,31 @@ |
| 642 | 610 | ||
| 643 | "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], | 611 | "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], |
| 644 | 612 | ||
| 645 | "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], | ||
| 646 | |||
| 647 | "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], | 613 | "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], |
| 648 | 614 | ||
| 649 | "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], | ||
| 650 | |||
| 651 | "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], | ||
| 652 | |||
| 653 | "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], | ||
| 654 | |||
| 655 | "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], | ||
| 656 | |||
| 657 | "import-from": ["import-from@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ=="], | 615 | "import-from": ["import-from@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ=="], |
| 658 | 616 | ||
| 659 | "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], | ||
| 660 | |||
| 661 | "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], | ||
| 662 | |||
| 663 | "infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="], | ||
| 664 | |||
| 665 | "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], | ||
| 666 | |||
| 667 | "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], | ||
| 668 | |||
| 669 | "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], | 617 | "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], |
| 670 | 618 | ||
| 671 | "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], | ||
| 672 | |||
| 673 | "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], | 619 | "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], |
| 674 | 620 | ||
| 675 | "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], | 621 | "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], |
| 676 | 622 | ||
| 677 | "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], | 623 | "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], |
| 678 | 624 | ||
| 625 | "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], | ||
| 626 | |||
| 679 | "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], | 627 | "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], |
| 680 | 628 | ||
| 681 | "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], | 629 | "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], |
| 682 | 630 | ||
| 683 | "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], | 631 | "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], |
| 684 | |||
| 685 | "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="], | ||
| 686 | 632 | ||
| 687 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], | 633 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], |
| 688 | 634 | ||
| 689 | "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], | 635 | "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], |
| 690 | 636 | ||
| 691 | "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], | 637 | "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], |
| 692 | |||
| 693 | "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], | ||
| 694 | 638 | ||
| 695 | "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], | 639 | "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], |
| 696 | 640 | ||
| ... | @@ -708,7 +652,7 @@ | ... | @@ -708,7 +652,7 @@ |
| 708 | 652 | ||
| 709 | "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], | 653 | "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], |
| 710 | 654 | ||
| 711 | "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], | 655 | "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], |
| 712 | 656 | ||
| 713 | "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], | 657 | "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], |
| 714 | 658 | ||
| ... | @@ -746,8 +690,6 @@ | ... | @@ -746,8 +690,6 @@ |
| 746 | 690 | ||
| 747 | "lodash.difference": ["lodash.difference@4.5.0", "", {}, "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="], | 691 | "lodash.difference": ["lodash.difference@4.5.0", "", {}, "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="], |
| 748 | 692 | ||
| 749 | "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], | ||
| 750 | |||
| 751 | "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], | 693 | "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], |
| 752 | 694 | ||
| 753 | "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], | 695 | "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], |
| ... | @@ -756,14 +698,12 @@ | ... | @@ -756,14 +698,12 @@ |
| 756 | 698 | ||
| 757 | "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="], | 699 | "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="], |
| 758 | 700 | ||
| 759 | "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], | 701 | "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], |
| 760 | 702 | ||
| 761 | "lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="], | 703 | "lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="], |
| 762 | 704 | ||
| 763 | "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], | 705 | "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], |
| 764 | 706 | ||
| 765 | "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], | ||
| 766 | |||
| 767 | "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], | 707 | "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], |
| 768 | 708 | ||
| 769 | "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], | 709 | "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], |
| ... | @@ -862,30 +802,10 @@ | ... | @@ -862,30 +802,10 @@ |
| 862 | 802 | ||
| 863 | "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], | 803 | "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], |
| 864 | 804 | ||
| 865 | "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], | ||
| 866 | |||
| 867 | "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], | 805 | "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], |
| 868 | 806 | ||
| 869 | "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], | ||
| 870 | |||
| 871 | "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], | 807 | "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], |
| 872 | 808 | ||
| 873 | "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], | ||
| 874 | |||
| 875 | "minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="], | ||
| 876 | |||
| 877 | "minipass-fetch": ["minipass-fetch@2.1.2", "", { "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA=="], | ||
| 878 | |||
| 879 | "minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="], | ||
| 880 | |||
| 881 | "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], | ||
| 882 | |||
| 883 | "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], | ||
| 884 | |||
| 885 | "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], | ||
| 886 | |||
| 887 | "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], | ||
| 888 | |||
| 889 | "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], | 809 | "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], |
| 890 | 810 | ||
| 891 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], | 811 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], |
| ... | @@ -894,20 +814,8 @@ | ... | @@ -894,20 +814,8 @@ |
| 894 | 814 | ||
| 895 | "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], | 815 | "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], |
| 896 | 816 | ||
| 897 | "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], | ||
| 898 | |||
| 899 | "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], | ||
| 900 | |||
| 901 | "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], | ||
| 902 | |||
| 903 | "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], | ||
| 904 | |||
| 905 | "node-pty": ["node-pty@1.1.0", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg=="], | ||
| 906 | |||
| 907 | "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], | 817 | "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], |
| 908 | 818 | ||
| 909 | "nopt": ["nopt@6.0.0", "", { "dependencies": { "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g=="], | ||
| 910 | |||
| 911 | "normalize-path": ["normalize-path@2.1.1", "", { "dependencies": { "remove-trailing-separator": "^1.0.1" } }, "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w=="], | 819 | "normalize-path": ["normalize-path@2.1.1", "", { "dependencies": { "remove-trailing-separator": "^1.0.1" } }, "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w=="], |
| 912 | 820 | ||
| 913 | "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], | 821 | "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], |
| ... | @@ -918,9 +826,7 @@ | ... | @@ -918,9 +826,7 @@ |
| 918 | 826 | ||
| 919 | "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], | 827 | "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], |
| 920 | 828 | ||
| 921 | "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], | 829 | "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], |
| 922 | |||
| 923 | "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], | ||
| 924 | 830 | ||
| 925 | "oxfmt": ["oxfmt@0.28.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.28.0", "@oxfmt/darwin-x64": "0.28.0", "@oxfmt/linux-arm64-gnu": "0.28.0", "@oxfmt/linux-arm64-musl": "0.28.0", "@oxfmt/linux-x64-gnu": "0.28.0", "@oxfmt/linux-x64-musl": "0.28.0", "@oxfmt/win32-arm64": "0.28.0", "@oxfmt/win32-x64": "0.28.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-3+hhBqPE6Kp22KfJmnstrZbl+KdOVSEu1V0ABaFIg1rYLtrMgrupx9znnHgHLqKxAVHebjTdiCJDk30CXOt6cw=="], | 831 | "oxfmt": ["oxfmt@0.28.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.28.0", "@oxfmt/darwin-x64": "0.28.0", "@oxfmt/linux-arm64-gnu": "0.28.0", "@oxfmt/linux-arm64-musl": "0.28.0", "@oxfmt/linux-x64-gnu": "0.28.0", "@oxfmt/linux-x64-musl": "0.28.0", "@oxfmt/win32-arm64": "0.28.0", "@oxfmt/win32-x64": "0.28.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-3+hhBqPE6Kp22KfJmnstrZbl+KdOVSEu1V0ABaFIg1rYLtrMgrupx9znnHgHLqKxAVHebjTdiCJDk30CXOt6cw=="], |
| 926 | 832 | ||
| ... | @@ -928,14 +834,8 @@ | ... | @@ -928,14 +834,8 @@ |
| 928 | 834 | ||
| 929 | "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], | 835 | "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], |
| 930 | 836 | ||
| 931 | "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], | ||
| 932 | |||
| 933 | "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], | 837 | "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], |
| 934 | 838 | ||
| 935 | "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], | ||
| 936 | |||
| 937 | "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], | ||
| 938 | |||
| 939 | "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], | 839 | "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], |
| 940 | 840 | ||
| 941 | "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], | 841 | "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], |
| ... | @@ -956,14 +856,8 @@ | ... | @@ -956,14 +856,8 @@ |
| 956 | 856 | ||
| 957 | "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], | 857 | "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], |
| 958 | 858 | ||
| 959 | "proc-log": ["proc-log@2.0.1", "", {}, "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw=="], | ||
| 960 | |||
| 961 | "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], | 859 | "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], |
| 962 | 860 | ||
| 963 | "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="], | ||
| 964 | |||
| 965 | "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], | ||
| 966 | |||
| 967 | "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], | 861 | "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], |
| 968 | 862 | ||
| 969 | "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], | 863 | "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], |
| ... | @@ -980,10 +874,6 @@ | ... | @@ -980,10 +874,6 @@ |
| 980 | 874 | ||
| 981 | "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], | 875 | "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], |
| 982 | 876 | ||
| 983 | "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], | ||
| 984 | |||
| 985 | "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], | ||
| 986 | |||
| 987 | "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], | 877 | "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], |
| 988 | 878 | ||
| 989 | "rehype-highlight": ["rehype-highlight@7.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="], | 879 | "rehype-highlight": ["rehype-highlight@7.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="], |
| ... | @@ -1004,50 +894,32 @@ | ... | @@ -1004,50 +894,32 @@ |
| 1004 | 894 | ||
| 1005 | "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], | 895 | "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], |
| 1006 | 896 | ||
| 1007 | "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], | 897 | "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], |
| 1008 | |||
| 1009 | "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], | ||
| 1010 | |||
| 1011 | "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], | ||
| 1012 | 898 | ||
| 1013 | "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], | 899 | "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], |
| 1014 | 900 | ||
| 1015 | "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], | 901 | "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], |
| 1016 | 902 | ||
| 1017 | "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], | 903 | "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], |
| 1018 | 904 | ||
| 905 | "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], | ||
| 906 | |||
| 1019 | "runtime-required": ["runtime-required@1.1.0", "", {}, "sha512-yX97f5E0WfNpcQnfVjap6vzQcvErkYYCx6eTK4siqGEdC8lglwypUFgZVTX7ShvIlgfkC4XGFl9O1KTYcff0pw=="], | 907 | "runtime-required": ["runtime-required@1.1.0", "", {}, "sha512-yX97f5E0WfNpcQnfVjap6vzQcvErkYYCx6eTK4siqGEdC8lglwypUFgZVTX7ShvIlgfkC4XGFl9O1KTYcff0pw=="], |
| 1020 | 908 | ||
| 1021 | "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], | 909 | "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], |
| 1022 | 910 | ||
| 1023 | "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], | ||
| 1024 | |||
| 1025 | "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], | ||
| 1026 | |||
| 1027 | "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], | 911 | "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], |
| 1028 | 912 | ||
| 1029 | "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], | 913 | "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], |
| 1030 | 914 | ||
| 1031 | "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], | 915 | "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], |
| 1032 | 916 | ||
| 1033 | "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], | 917 | "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], |
| 1034 | 918 | ||
| 1035 | "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], | ||
| 1036 | |||
| 1037 | "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], | ||
| 1038 | |||
| 1039 | "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], | 919 | "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], |
| 1040 | 920 | ||
| 1041 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], | 921 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], |
| 1042 | 922 | ||
| 1043 | "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], | ||
| 1044 | |||
| 1045 | "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], | ||
| 1046 | |||
| 1047 | "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], | ||
| 1048 | |||
| 1049 | "socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="], | ||
| 1050 | |||
| 1051 | "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], | 923 | "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], |
| 1052 | 924 | ||
| 1053 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], | 925 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], |
| ... | @@ -1056,16 +928,12 @@ | ... | @@ -1056,16 +928,12 @@ |
| 1056 | 928 | ||
| 1057 | "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], | 929 | "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], |
| 1058 | 930 | ||
| 1059 | "ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="], | ||
| 1060 | |||
| 1061 | "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], | 931 | "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], |
| 1062 | 932 | ||
| 1063 | "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], | 933 | "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], |
| 1064 | 934 | ||
| 1065 | "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], | 935 | "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], |
| 1066 | 936 | ||
| 1067 | "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], | ||
| 1068 | |||
| 1069 | "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], | 937 | "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], |
| 1070 | 938 | ||
| 1071 | "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], | 939 | "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], |
| ... | @@ -1082,12 +950,12 @@ | ... | @@ -1082,12 +950,12 @@ |
| 1082 | 950 | ||
| 1083 | "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], | 951 | "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], |
| 1084 | 952 | ||
| 953 | "t3": ["t3@workspace:apps/server"], | ||
| 954 | |||
| 1085 | "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], | 955 | "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], |
| 1086 | 956 | ||
| 1087 | "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], | 957 | "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], |
| 1088 | 958 | ||
| 1089 | "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], | ||
| 1090 | |||
| 1091 | "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], | 959 | "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], |
| 1092 | 960 | ||
| 1093 | "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], | 961 | "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], |
| ... | @@ -1118,6 +986,8 @@ | ... | @@ -1118,6 +986,8 @@ |
| 1118 | 986 | ||
| 1119 | "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], | 987 | "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], |
| 1120 | 988 | ||
| 989 | "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], | ||
| 990 | |||
| 1121 | "turbo": ["turbo@2.8.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.3", "turbo-darwin-arm64": "2.8.3", "turbo-linux-64": "2.8.3", "turbo-linux-arm64": "2.8.3", "turbo-windows-64": "2.8.3", "turbo-windows-arm64": "2.8.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-8Osxz5Tu/Dw2kb31EAY+nhq/YZ3wzmQSmYa1nIArqxgCAldxv9TPlrAiaBUDVnKA4aiPn0OFBD1ACcpc5VFOAQ=="], | 991 | "turbo": ["turbo@2.8.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.3", "turbo-darwin-arm64": "2.8.3", "turbo-linux-64": "2.8.3", "turbo-linux-arm64": "2.8.3", "turbo-windows-64": "2.8.3", "turbo-windows-arm64": "2.8.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-8Osxz5Tu/Dw2kb31EAY+nhq/YZ3wzmQSmYa1nIArqxgCAldxv9TPlrAiaBUDVnKA4aiPn0OFBD1ACcpc5VFOAQ=="], |
| 1122 | 992 | ||
| 1123 | "turbo-darwin-64": ["turbo-darwin-64@2.8.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kXRLfcygLOeNcP6JquqRLmGB/ATjjfehiojL2dJkL7GFm3SPSXbq7oNj8UbD8XriYQ5hPaSuz59iF1ijPHkTw=="], | 993 | "turbo-darwin-64": ["turbo-darwin-64@2.8.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kXRLfcygLOeNcP6JquqRLmGB/ATjjfehiojL2dJkL7GFm3SPSXbq7oNj8UbD8XriYQ5hPaSuz59iF1ijPHkTw=="], |
| ... | @@ -1142,10 +1012,6 @@ | ... | @@ -1142,10 +1012,6 @@ |
| 1142 | 1012 | ||
| 1143 | "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], | 1013 | "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], |
| 1144 | 1014 | ||
| 1145 | "unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="], | ||
| 1146 | |||
| 1147 | "unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="], | ||
| 1148 | |||
| 1149 | "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], | 1015 | "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], |
| 1150 | 1016 | ||
| 1151 | "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], | 1017 | "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], |
| ... | @@ -1158,14 +1024,12 @@ | ... | @@ -1158,14 +1024,12 @@ |
| 1158 | 1024 | ||
| 1159 | "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], | 1025 | "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], |
| 1160 | 1026 | ||
| 1161 | "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], | 1027 | "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], |
| 1162 | 1028 | ||
| 1163 | "unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="], | 1029 | "unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="], |
| 1164 | 1030 | ||
| 1165 | "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], | 1031 | "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], |
| 1166 | 1032 | ||
| 1167 | "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], | ||
| 1168 | |||
| 1169 | "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], | 1033 | "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], |
| 1170 | 1034 | ||
| 1171 | "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], | 1035 | "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], |
| ... | @@ -1180,19 +1044,19 @@ | ... | @@ -1180,19 +1044,19 @@ |
| 1180 | 1044 | ||
| 1181 | "watchboy": ["watchboy@0.4.3", "", { "dependencies": { "lodash.difference": "^4.5.0", "micromatch": "^4.0.2", "pify": "^4.0.1", "unixify": "^1.0.0" } }, "sha512-GHs1HxwvxSMBsqd/WfTOZhj5gBdMqf5HQpfgtKxDfZRxrlYPDdVLRB61LCeRzJaWANmvSIMlfmRVDwVmJFgAKA=="], | 1045 | "watchboy": ["watchboy@0.4.3", "", { "dependencies": { "lodash.difference": "^4.5.0", "micromatch": "^4.0.2", "pify": "^4.0.1", "unixify": "^1.0.0" } }, "sha512-GHs1HxwvxSMBsqd/WfTOZhj5gBdMqf5HQpfgtKxDfZRxrlYPDdVLRB61LCeRzJaWANmvSIMlfmRVDwVmJFgAKA=="], |
| 1182 | 1046 | ||
| 1183 | "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], | ||
| 1184 | |||
| 1185 | "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], | ||
| 1186 | |||
| 1187 | "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], | 1047 | "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], |
| 1188 | 1048 | ||
| 1189 | "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], | 1049 | "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], |
| 1190 | 1050 | ||
| 1191 | "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], | 1051 | "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], |
| 1192 | 1052 | ||
| 1053 | "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], | ||
| 1054 | |||
| 1055 | "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], | ||
| 1056 | |||
| 1193 | "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], | 1057 | "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], |
| 1194 | 1058 | ||
| 1195 | "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], | 1059 | "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], |
| 1196 | 1060 | ||
| 1197 | "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], | 1061 | "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], |
| 1198 | 1062 | ||
| ... | @@ -1206,16 +1070,6 @@ | ... | @@ -1206,16 +1070,6 @@ |
| 1206 | 1070 | ||
| 1207 | "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], | 1071 | "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], |
| 1208 | 1072 | ||
| 1209 | "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], | ||
| 1210 | |||
| 1211 | "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], | ||
| 1212 | |||
| 1213 | "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], | ||
| 1214 | |||
| 1215 | "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], | ||
| 1216 | |||
| 1217 | "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], | ||
| 1218 | |||
| 1219 | "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], | 1073 | "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], |
| 1220 | 1074 | ||
| 1221 | "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], | 1075 | "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], |
| ... | @@ -1230,8 +1084,6 @@ | ... | @@ -1230,8 +1084,6 @@ |
| 1230 | 1084 | ||
| 1231 | "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | 1085 | "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], |
| 1232 | 1086 | ||
| 1233 | "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], | ||
| 1234 | |||
| 1235 | "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], | 1087 | "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], |
| 1236 | 1088 | ||
| 1237 | "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], | 1089 | "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], |
| ... | @@ -1240,46 +1092,20 @@ | ... | @@ -1240,46 +1092,20 @@ |
| 1240 | 1092 | ||
| 1241 | "electronmon/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], | 1093 | "electronmon/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], |
| 1242 | 1094 | ||
| 1243 | "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], | 1095 | "global-agent/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], |
| 1244 | |||
| 1245 | "make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], | ||
| 1246 | 1096 | ||
| 1247 | "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], | 1097 | "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], |
| 1248 | 1098 | ||
| 1249 | "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], | 1099 | "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |
| 1250 | 1100 | ||
| 1251 | "minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], | ||
| 1252 | |||
| 1253 | "minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], | ||
| 1254 | |||
| 1255 | "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], | ||
| 1256 | |||
| 1257 | "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], | ||
| 1258 | |||
| 1259 | "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], | ||
| 1260 | |||
| 1261 | "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], | ||
| 1262 | |||
| 1263 | "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], | 1101 | "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], |
| 1264 | 1102 | ||
| 1265 | "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], | ||
| 1266 | |||
| 1267 | "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], | ||
| 1268 | |||
| 1269 | "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], | 1103 | "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], |
| 1270 | 1104 | ||
| 1271 | "vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], | 1105 | "vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], |
| 1272 | 1106 | ||
| 1273 | "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], | ||
| 1274 | |||
| 1275 | "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], | ||
| 1276 | |||
| 1277 | "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], | ||
| 1278 | |||
| 1279 | "electronmon/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], | 1107 | "electronmon/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], |
| 1280 | 1108 | ||
| 1281 | "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], | ||
| 1282 | |||
| 1283 | "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], | 1109 | "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], |
| 1284 | 1110 | ||
| 1285 | "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], | 1111 | "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], |
| ... | @@ -1331,7 +1157,5 @@ | ... | @@ -1331,7 +1157,5 @@ |
| 1331 | "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], | 1157 | "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], |
| 1332 | 1158 | ||
| 1333 | "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], | 1159 | "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], |
| 1334 | |||
| 1335 | "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], | ||
| 1336 | } | 1160 | } |
| 1337 | } | 1161 | } |
package.json+8-2| ... | @@ -6,13 +6,19 @@ | ... | @@ -6,13 +6,19 @@ |
| 6 | "packages/*" | 6 | "packages/*" |
| 7 | ], | 7 | ], |
| 8 | "scripts": { | 8 | "scripts": { |
| 9 | "dev": "bun run build:contracts && turbo run dev --parallel", | 9 | "dev": "CODETHING_LOG_WS_EVENTS=1 VITE_WS_URL=ws://localhost:3773 turbo run dev --ui=tui --filter=@acme/contracts --filter=@acme/renderer --filter=t3 --parallel", |
| 10 | "dev:server": "bun run build:contracts && bun run --cwd apps/server dev", | ||
| 11 | "dev:web": "VITE_WS_URL=ws://localhost:3773 bun run --cwd apps/renderer dev", | ||
| 12 | "dev:desktop": "bun run --cwd apps/desktop dev", | ||
| 13 | "start": "node apps/server/dist/index.js", | ||
| 14 | "start:desktop": "bun run --cwd apps/desktop start", | ||
| 10 | "build": "turbo run build", | 15 | "build": "turbo run build", |
| 16 | "build:desktop": "bun run build:contracts && bun run --cwd apps/renderer build && bun run --cwd apps/server build && bun run --cwd apps/desktop build", | ||
| 11 | "typecheck": "turbo run typecheck", | 17 | "typecheck": "turbo run typecheck", |
| 12 | "lint": "oxlint --report-unused-disable-directives", | 18 | "lint": "oxlint --report-unused-disable-directives", |
| 13 | "test": "turbo run test", | 19 | "test": "turbo run test", |
| 20 | "test:desktop-smoke": "bun run --cwd apps/desktop smoke-test", | ||
| 14 | "fmt": "oxfmt", | 21 | "fmt": "oxfmt", |
| 15 | "smoke-test": "turbo run smoke-test", | ||
| 16 | "build:contracts": "bun run --cwd packages/contracts build" | 22 | "build:contracts": "bun run --cwd packages/contracts build" |
| 17 | }, | 23 | }, |
| 18 | "devDependencies": { | 24 | "devDependencies": { |
packages/contracts/src/index.ts+2| ... | @@ -4,3 +4,5 @@ export * from "./agent"; | ... | @@ -4,3 +4,5 @@ export * from "./agent"; |
| 4 | export * from "./terminal"; | 4 | export * from "./terminal"; |
| 5 | export * from "./provider"; | 5 | export * from "./provider"; |
| 6 | export * from "./model"; | 6 | export * from "./model"; |
| 7 | export * from "./ws"; | ||
| 8 | export * from "./project"; |
packages/contracts/src/ipc.ts+11-22| ... | @@ -9,6 +9,12 @@ import type { | ... | @@ -9,6 +9,12 @@ import type { |
| 9 | ProviderStopSessionInput, | 9 | ProviderStopSessionInput, |
| 10 | ProviderTurnStartResult, | 10 | ProviderTurnStartResult, |
| 11 | } from "./provider"; | 11 | } from "./provider"; |
| 12 | import type { | ||
| 13 | ProjectAddInput, | ||
| 14 | ProjectAddResult, | ||
| 15 | ProjectListResult, | ||
| 16 | ProjectRemoveInput, | ||
| 17 | } from "./project"; | ||
| 12 | import type { TerminalCommandInput, TerminalCommandResult } from "./terminal"; | 18 | import type { TerminalCommandInput, TerminalCommandResult } from "./terminal"; |
| 13 | import type { NewTodoInput, Todo } from "./todo"; | 19 | import type { NewTodoInput, Todo } from "./todo"; |
| 14 | 20 | ||
| ... | @@ -19,28 +25,6 @@ export const EDITORS = [ | ... | @@ -19,28 +25,6 @@ export const EDITORS = [ |
| 19 | 25 | ||
| 20 | export type EditorId = (typeof EDITORS)[number]["id"]; | 26 | export type EditorId = (typeof EDITORS)[number]["id"]; |
| 21 | 27 | ||
| 22 | export const IPC_CHANNELS = { | ||
| 23 | todosList: "todos:list", | ||
| 24 | todosAdd: "todos:add", | ||
| 25 | todosToggle: "todos:toggle", | ||
| 26 | todosRemove: "todos:remove", | ||
| 27 | dialogPickFolder: "dialog:pick-folder", | ||
| 28 | terminalRun: "terminal:run", | ||
| 29 | agentSpawn: "agent:spawn", | ||
| 30 | agentKill: "agent:kill", | ||
| 31 | agentWrite: "agent:write", | ||
| 32 | agentOutput: "agent:output", | ||
| 33 | agentExit: "agent:exit", | ||
| 34 | providerSessionStart: "provider:session:start", | ||
| 35 | providerTurnStart: "provider:turn:start", | ||
| 36 | providerTurnInterrupt: "provider:turn:interrupt", | ||
| 37 | providerSessionStop: "provider:session:stop", | ||
| 38 | providerSessionList: "provider:session:list", | ||
| 39 | providerRequestRespond: "provider:request:respond", | ||
| 40 | providerEvent: "provider:event", | ||
| 41 | shellOpenInEditor: "shell:open-in-editor", | ||
| 42 | } as const; | ||
| 43 | |||
| 44 | export interface NativeApi { | 28 | export interface NativeApi { |
| 45 | todos: { | 29 | todos: { |
| 46 | list: () => Promise<Todo[]>; | 30 | list: () => Promise<Todo[]>; |
| ... | @@ -70,6 +54,11 @@ export interface NativeApi { | ... | @@ -70,6 +54,11 @@ export interface NativeApi { |
| 70 | listSessions: () => Promise<ProviderSession[]>; | 54 | listSessions: () => Promise<ProviderSession[]>; |
| 71 | onEvent: (callback: (event: ProviderEvent) => void) => () => void; | 55 | onEvent: (callback: (event: ProviderEvent) => void) => () => void; |
| 72 | }; | 56 | }; |
| 57 | projects: { | ||
| 58 | list: () => Promise<ProjectListResult>; | ||
| 59 | add: (input: ProjectAddInput) => Promise<ProjectAddResult>; | ||
| 60 | remove: (input: ProjectRemoveInput) => Promise<void>; | ||
| 61 | }; | ||
| 73 | shell: { | 62 | shell: { |
| 74 | openInEditor: (cwd: string, editor: EditorId) => Promise<void>; | 63 | openInEditor: (cwd: string, editor: EditorId) => Promise<void>; |
| 75 | }; | 64 | }; |
packages/contracts/src/project.test.ts created+49| ... | @@ -0,0 +1,49 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | projectAddInputSchema, | ||
| 5 | projectAddResultSchema, | ||
| 6 | projectListResultSchema, | ||
| 7 | projectRemoveInputSchema, | ||
| 8 | } from "./project"; | ||
| 9 | |||
| 10 | describe("project contracts", () => { | ||
| 11 | it("parses project list result", () => { | ||
| 12 | const result = projectListResultSchema.parse([ | ||
| 13 | { | ||
| 14 | id: "project-1", | ||
| 15 | cwd: "/tmp/project", | ||
| 16 | name: "project", | ||
| 17 | createdAt: "2026-02-10T00:00:00.000Z", | ||
| 18 | updatedAt: "2026-02-10T00:00:00.000Z", | ||
| 19 | }, | ||
| 20 | ]); | ||
| 21 | |||
| 22 | expect(result).toHaveLength(1); | ||
| 23 | expect(result[0]?.id).toBe("project-1"); | ||
| 24 | }); | ||
| 25 | |||
| 26 | it("trims add input cwd", () => { | ||
| 27 | const parsed = projectAddInputSchema.parse({ cwd: " /tmp/project " }); | ||
| 28 | expect(parsed.cwd).toBe("/tmp/project"); | ||
| 29 | }); | ||
| 30 | |||
| 31 | it("requires add result created flag", () => { | ||
| 32 | const parsed = projectAddResultSchema.parse({ | ||
| 33 | project: { | ||
| 34 | id: "project-1", | ||
| 35 | cwd: "/tmp/project", | ||
| 36 | name: "project", | ||
| 37 | createdAt: "2026-02-10T00:00:00.000Z", | ||
| 38 | updatedAt: "2026-02-10T00:00:00.000Z", | ||
| 39 | }, | ||
| 40 | created: true, | ||
| 41 | }); | ||
| 42 | expect(parsed.created).toBe(true); | ||
| 43 | }); | ||
| 44 | |||
| 45 | it("parses remove input", () => { | ||
| 46 | const parsed = projectRemoveInputSchema.parse({ id: "project-1" }); | ||
| 47 | expect(parsed.id).toBe("project-1"); | ||
| 48 | }); | ||
| 49 | }); | ||
packages/contracts/src/project.ts created+30| ... | @@ -0,0 +1,30 @@ | ||
| 1 | import { z } from "zod"; | ||
| 2 | |||
| 3 | export const projectRecordSchema = z.object({ | ||
| 4 | id: z.string().min(1), | ||
| 5 | cwd: z.string().min(1), | ||
| 6 | name: z.string().min(1), | ||
| 7 | createdAt: z.string().datetime(), | ||
| 8 | updatedAt: z.string().datetime(), | ||
| 9 | }); | ||
| 10 | |||
| 11 | export const projectListResultSchema = z.array(projectRecordSchema); | ||
| 12 | |||
| 13 | export const projectAddInputSchema = z.object({ | ||
| 14 | cwd: z.string().trim().min(1), | ||
| 15 | }); | ||
| 16 | |||
| 17 | export const projectAddResultSchema = z.object({ | ||
| 18 | project: projectRecordSchema, | ||
| 19 | created: z.boolean(), | ||
| 20 | }); | ||
| 21 | |||
| 22 | export const projectRemoveInputSchema = z.object({ | ||
| 23 | id: z.string().min(1), | ||
| 24 | }); | ||
| 25 | |||
| 26 | export type ProjectRecord = z.infer<typeof projectRecordSchema>; | ||
| 27 | export type ProjectListResult = z.infer<typeof projectListResultSchema>; | ||
| 28 | export type ProjectAddInput = z.input<typeof projectAddInputSchema>; | ||
| 29 | export type ProjectAddResult = z.infer<typeof projectAddResultSchema>; | ||
| 30 | export type ProjectRemoveInput = z.input<typeof projectRemoveInputSchema>; | ||
packages/contracts/src/ws.ts created+78| ... | @@ -0,0 +1,78 @@ | ||
| 1 | import { z } from "zod"; | ||
| 2 | |||
| 3 | // ── WebSocket RPC Method Names ─────────────────────────────────────── | ||
| 4 | |||
| 5 | export const WS_METHODS = { | ||
| 6 | // Provider methods (mirrors NativeApi.providers) | ||
| 7 | providersStartSession: "providers.startSession", | ||
| 8 | providersSendTurn: "providers.sendTurn", | ||
| 9 | providersInterruptTurn: "providers.interruptTurn", | ||
| 10 | providersRespondToRequest: "providers.respondToRequest", | ||
| 11 | providersStopSession: "providers.stopSession", | ||
| 12 | providersListSessions: "providers.listSessions", | ||
| 13 | |||
| 14 | // Project registry methods | ||
| 15 | projectsList: "projects.list", | ||
| 16 | projectsAdd: "projects.add", | ||
| 17 | projectsRemove: "projects.remove", | ||
| 18 | |||
| 19 | // Shell methods | ||
| 20 | shellOpenInEditor: "shell.openInEditor", | ||
| 21 | |||
| 22 | // Server meta | ||
| 23 | serverGetConfig: "server.getConfig", | ||
| 24 | } as const; | ||
| 25 | |||
| 26 | // ── Push Event Channels ────────────────────────────────────────────── | ||
| 27 | |||
| 28 | export const WS_CHANNELS = { | ||
| 29 | providerEvent: "providers.event", | ||
| 30 | serverWelcome: "server.welcome", | ||
| 31 | } as const; | ||
| 32 | |||
| 33 | // ── Client → Server (request) ──────────────────────────────────────── | ||
| 34 | |||
| 35 | export const wsRequestSchema = z.object({ | ||
| 36 | id: z.string().min(1), | ||
| 37 | method: z.string().min(1), | ||
| 38 | params: z.unknown().optional(), | ||
| 39 | }); | ||
| 40 | |||
| 41 | export type WsRequest = z.infer<typeof wsRequestSchema>; | ||
| 42 | |||
| 43 | // ── Server → Client (response to request) ──────────────────────────── | ||
| 44 | |||
| 45 | export const wsResponseSchema = z.object({ | ||
| 46 | id: z.string().min(1), | ||
| 47 | result: z.unknown().optional(), | ||
| 48 | error: z | ||
| 49 | .object({ | ||
| 50 | message: z.string(), | ||
| 51 | }) | ||
| 52 | .optional(), | ||
| 53 | }); | ||
| 54 | |||
| 55 | export type WsResponse = z.infer<typeof wsResponseSchema>; | ||
| 56 | |||
| 57 | // ── Server → Client (push event) ───────────────────────────────────── | ||
| 58 | |||
| 59 | export const wsPushSchema = z.object({ | ||
| 60 | type: z.literal("push"), | ||
| 61 | channel: z.string().min(1), | ||
| 62 | data: z.unknown(), | ||
| 63 | }); | ||
| 64 | |||
| 65 | export type WsPush = z.infer<typeof wsPushSchema>; | ||
| 66 | |||
| 67 | // ── Union of all server → client messages ───────────────────────────── | ||
| 68 | |||
| 69 | export type WsServerMessage = WsResponse | WsPush; | ||
| 70 | |||
| 71 | // ── Server welcome payload ─────────────────────────────────────────── | ||
| 72 | |||
| 73 | export const wsWelcomePayloadSchema = z.object({ | ||
| 74 | cwd: z.string().min(1), | ||
| 75 | projectName: z.string().min(1), | ||
| 76 | }); | ||
| 77 | |||
| 78 | export type WsWelcomePayload = z.infer<typeof wsWelcomePayloadSchema>; | ||
turbo.json+13-6| ... | @@ -1,12 +1,24 @@ | ... | @@ -1,12 +1,24 @@ |
| 1 | { | 1 | { |
| 2 | "$schema": "https://turbo.build/schema.json", | 2 | "$schema": "https://turbo.build/schema.json", |
| 3 | "globalEnv": ["ELECTRON_RENDERER_PORT"], | 3 | "globalEnv": [ |
| 4 | "PORT", | ||
| 5 | "VITE_WS_URL", | ||
| 6 | "VITE_DEV_SERVER_URL", | ||
| 7 | "CODETHING_LOG_WS_EVENTS", | ||
| 8 | "CODETHING_MODE", | ||
| 9 | "CODETHING_PORT", | ||
| 10 | "CODETHING_NO_BROWSER", | ||
| 11 | "CODETHING_STATE_DIR", | ||
| 12 | "CODETHING_AUTH_TOKEN", | ||
| 13 | "CODETHING_DESKTOP_WS_URL" | ||
| 14 | ], | ||
| 4 | "tasks": { | 15 | "tasks": { |
| 5 | "build": { | 16 | "build": { |
| 6 | "dependsOn": ["^build"], | 17 | "dependsOn": ["^build"], |
| 7 | "outputs": ["dist/**", "dist-electron/**"] | 18 | "outputs": ["dist/**", "dist-electron/**"] |
| 8 | }, | 19 | }, |
| 9 | "dev": { | 20 | "dev": { |
| 21 | "dependsOn": ["@acme/contracts#build"], | ||
| 10 | "cache": false, | 22 | "cache": false, |
| 11 | "persistent": true | 23 | "persistent": true |
| 12 | }, | 24 | }, |
| ... | @@ -17,11 +29,6 @@ | ... | @@ -17,11 +29,6 @@ |
| 17 | "test": { | 29 | "test": { |
| 18 | "dependsOn": ["^build"], | 30 | "dependsOn": ["^build"], |
| 19 | "outputs": [] | 31 | "outputs": [] |
| 20 | }, | ||
| 21 | "smoke-test": { | ||
| 22 | "dependsOn": ["build"], | ||
| 23 | "cache": false, | ||
| 24 | "outputs": [] | ||
| 25 | } | 32 | } |
| 26 | } | 33 | } |
| 27 | } | 34 | } |