* 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 ---------
Unified · Split
| @@ -8,7 +8,7 @@ on: · 7 unmodified lines | ||
| 8 | 8 | |
| 9 | 9 | jobs: |
| 10 | 10 | quality: |
| 11 | − name: Lint, Typecheck, Test | |
| 11 | + name: Lint, Typecheck, Test, Build | |
| 12 | 12 | runs-on: ubuntu-latest |
| 13 | 13 | steps: |
| 14 | 14 | - name: Checkout |
| @@ -17,7 +17,7 @@ jobs: · 2 unmodified lines | ||
| 17 | 17 | - name: Setup Bun |
| 18 | 18 | uses: oven-sh/setup-bun@v2 |
| 19 | 19 | with: |
| 20 | − bun-version: 1.1.38 | |
| 20 | + bun-version: 1.3.9 | |
| 21 | 21 | |
| 22 | 22 | - name: Cache Bun and Turbo |
| 23 | 23 | uses: actions/cache@v4 |
| @@ -40,3 +40,11 @@ jobs: · 16 unmodified lines | ||
| 40 | 40 | |
| 41 | 41 | - name: Test |
| 42 | 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 | |
| @@ -5,7 +5,6 @@ node_modules · 4 unmodified lines | ||
| 5 | 5 | *.log |
| 6 | 6 | *.tsbuildinfo |
| 7 | 7 | apps/*/dist |
| 8 | −apps/*/dist-electron | |
| 9 | 8 | packages/*/dist |
| 10 | 9 | .env |
| 11 | 10 | .env.local |
| @@ -1,7 +1,7 @@ | ||
| 1 | −# AGENTS.md | |
| 1 | +# CLAUDE.md | |
| 2 | 2 | |
| 3 | 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 | 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 · 5 unmodified lines | ||
| 13 | 13 | If a tradeoff is required, choose correctness and robustness over short-term convenience. |
| 14 | 14 | |
| 15 | 15 | ## Package Roles |
| 16 | −- `apps/desktop`: Electron main/preload runtime. Owns provider orchestration, process/session lifecycle, and native IPC boundaries. | |
| 17 | −- `apps/renderer`: React/Vite UI. Owns session UX, conversation/event rendering, and client-side state. | |
| 18 | −- `packages/contracts`: Shared Zod schemas and TypeScript contracts for provider events, IPC payloads, and model/session types. | |
| 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. Connects to the server via WebSocket. | |
| 18 | +- `packages/contracts`: Shared Zod schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types. | |
| 19 | 19 | |
| 20 | 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 | 23 | How we use it in this codebase: |
| 24 | −- Session startup/resume and turn lifecycle are brokered in `apps/desktop/src/codexAppServerManager.ts`. | |
| 25 | −- Provider dispatch and thread event logging are coordinated in `apps/desktop/src/providerManager.ts`. | |
| 26 | −- Renderer consumes provider event streams via `nativeApi.providers.onEvent`. | |
| 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/server/src/providerManager.ts`. | |
| 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 | 29 | Docs: |
| 29 | 30 | - Codex App Server docs: https://developers.openai.com/codex/sdk/#app-server |
| @@ -1,16 +1,38 @@ | ||
| 1 | −# CodeThing (Electron + Vite + Bun) | |
| 2 | − | |
| 3 | −CodeThing is a desktop shell for coding agents. This first implementation is: | |
| 4 | − | |
| 5 | −1. Codex-first: connects to `codex app-server` and streams turn/item events. | |
| 6 | −2. Provider-ready: renderer speaks a provider abstraction so Claude Code can plug in later. | |
| 7 | −3. Typed end-to-end: contracts validate payloads at preload/main boundaries. | |
| 1 | +# CodeThing | |
| 2 | + | |
| 3 | +CodeThing is a minimal web GUI for coding agents. Currently Codex-first, with Claude Code support coming soon. | |
| 4 | + | |
| 5 | +Run `npx t3` in any project directory to launch the web interface. | |
| 6 | +Run `bun run dev:desktop` to launch the Electron desktop app in this monorepo. | |
| 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 | 30 | ## Workspace layout |
| 10 | 31 | |
| 11 | −- `/apps/desktop`: Electron main + preload process, includes provider and Codex session managers. | |
| 12 | −- `/apps/renderer`: React + Vite UI for session control, conversation, and protocol event stream. | |
| 13 | −- `/packages/contracts`: shared Zod schemas + TypeScript types for IPC and provider events. | |
| 32 | +- `/apps/server`: Node.js WebSocket server. Wraps Codex app-server, serves the built renderer, and opens the browser on start. | |
| 33 | +- `/apps/renderer`: React + Vite UI. Session control, conversation, and provider event rendering. Connects to the server via WebSocket. | |
| 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 | 37 | ## Codex prerequisites |
| 16 | 38 | |
| @@ -18,50 +40,55 @@ CodeThing is a desktop shell for coding agents. This first implementation is: · 1 unmodified line | ||
| 18 | 40 | - Authenticate Codex before running CodeThing (for example via API key or ChatGPT auth supported by Codex). |
| 19 | 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` | |
| 24 | −- `contextIsolation: true` | |
| 25 | −- `sandbox: true` | |
| 26 | −- Renderer talks only to `window.nativeApi` exposed by preload. | |
| 27 | −- Preload and main both validate inputs using shared Zod schemas. | |
| 45 | +```bash | |
| 46 | +# Development (with hot reload) | |
| 47 | +bun run dev | |
| 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`. | |
| 36 | −- `Approval required`: starts new sessions with `approvalPolicy: on-request` and `sandboxMode: workspace-write`, then prompts in-app for command/file approvals. | |
| 60 | +## Scripts | |
| 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. | |
| 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. | |
| 72 | +CodeThing has a global runtime mode switch in the chat toolbar: | |
| 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` | |
| 60 | −2. `sendTurn` | |
| 61 | −3. `interruptTurn` | |
| 62 | −4. `respondToRequest` | |
| 63 | −5. `stopSession` | |
| 64 | −6. `listSessions` | |
| 65 | −7. `onEvent` | |
| 90 | +Codex is the only implemented provider. `claudeCode` is reserved in contracts/UI. | |
| 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`. | |
| @@ -0,0 +1 @@ | ||
| 1 | +dist-electron/ | |
| @@ -4,28 +4,25 @@ · 3 unmodified lines | ||
| 4 | 4 | "private": true, |
| 5 | 5 | "main": "dist-electron/main.js", |
| 6 | 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 | 8 | "dev:bundle": "tsup --watch", |
| 9 | 9 | "dev:electron": "bun run scripts/dev-electron.mjs", |
| 10 | 10 | "build": "tsup", |
| 11 | 11 | "start": "electron dist-electron/main.js", |
| 12 | − "postinstall": "electron-rebuild", | |
| 13 | 12 | "typecheck": "tsc --noEmit", |
| 14 | − "test": "vitest run", | |
| 13 | + "test": "vitest run --passWithNoTests", | |
| 15 | 14 | "smoke-test": "node scripts/smoke-test.mjs" |
| 16 | 15 | }, |
| 17 | 16 | "dependencies": { |
| 18 | − "@acme/contracts": "workspace:*", | |
| 19 | − "electron": "33.4.11", | |
| 20 | − "node-pty": "^1.0.0" | |
| 17 | + "electron": "33.4.11" | |
| 21 | 18 | }, |
| 22 | 19 | "devDependencies": { |
| 23 | − "concurrently": "^9.1.2", | |
| 24 | − "@electron/rebuild": "^3.7.0", | |
| 25 | 20 | "@types/node": "^22.10.2", |
| 21 | + "concurrently": "^9.1.2", | |
| 26 | 22 | "electronmon": "^2.0.2", |
| 27 | 23 | "tsup": "^8.3.5", |
| 28 | 24 | "typescript": "^5.7.3", |
| 25 | + "vitest": "^3.0.0", | |
| 29 | 26 | "wait-on": "^8.0.2" |
| 30 | 27 | } |
| 31 | 28 | } |
| @@ -10,11 +10,11 @@ await waitOn({ · 9 unmodified lines | ||
| 10 | 10 | `tcp:${port}`, |
| 11 | 11 | "file:dist-electron/main.js", |
| 12 | 12 | "file:dist-electron/preload.js", |
| 13 | + "file:../server/dist/index.js", | |
| 13 | 14 | ], |
| 14 | 15 | }); |
| 15 | 16 | |
| 16 | −const command = | |
| 17 | − process.platform === "win32" ? "electronmon.cmd" : "electronmon"; | |
| 17 | +const command = process.platform === "win32" ? "electronmon.cmd" : "electronmon"; | |
| 18 | 18 | const child = spawn(command, ["dist-electron/main.js"], { |
| 19 | 19 | stdio: "inherit", |
| 20 | 20 | env: { |
| @@ -1,84 +1,61 @@ | ||
| 1 | −/** | |
| 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"; | |
| 1 | +import { execSync, spawn } from "node:child_process"; | |
| 17 | 2 | import { dirname, resolve } from "node:path"; |
| 18 | 3 | import { fileURLToPath } from "node:url"; |
| 19 | 4 | |
| 20 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 21 | −const root = resolve(__dirname, "../../.."); | |
| 22 | 6 | const desktopDir = resolve(__dirname, ".."); |
| 7 | +const rootDir = resolve(__dirname, "../../.."); | |
| 23 | 8 | const electronBin = resolve(desktopDir, "node_modules/.bin/electron"); |
| 24 | 9 | const mainJs = resolve(desktopDir, "dist-electron/main.js"); |
| 25 | 10 | |
| 26 | −// ── Build first ────────────────────────────────────────────────────── | |
| 27 | −console.log("Building contracts + renderer + desktop..."); | |
| 28 | −execSync("bun run build", { cwd: root, stdio: "inherit" }); | |
| 11 | +console.log("Building contracts + renderer + server + desktop..."); | |
| 12 | +execSync("bun run build", { cwd: rootDir, stdio: "inherit" }); | |
| 29 | 13 | |
| 30 | −// ── Launch Electron (production mode — no VITE_DEV_SERVER_URL) ────── | |
| 31 | −console.log("\nLaunching Electron (production mode)..."); | |
| 14 | +console.log("\nLaunching Electron smoke test..."); | |
| 32 | 15 | |
| 33 | 16 | const child = spawn(electronBin, [mainJs], { |
| 34 | 17 | stdio: ["pipe", "pipe", "pipe"], |
| 35 | 18 | env: { |
| 36 | 19 | ...process.env, |
| 37 | − VITE_DEV_SERVER_URL: "", // ensure production path | |
| 20 | + VITE_DEV_SERVER_URL: "", | |
| 38 | 21 | ELECTRON_ENABLE_LOGGING: "1", |
| 39 | 22 | }, |
| 40 | 23 | }); |
| 41 | 24 | |
| 42 | 25 | let output = ""; |
| 43 | − | |
| 44 | −child.stdout.on("data", (d) => { | |
| 45 | − output += d.toString(); | |
| 26 | +child.stdout.on("data", (chunk) => { | |
| 27 | + output += chunk.toString(); | |
| 46 | 28 | }); |
| 47 | −child.stderr.on("data", (d) => { | |
| 48 | − output += d.toString(); | |
| 29 | +child.stderr.on("data", (chunk) => { | |
| 30 | + output += chunk.toString(); | |
| 49 | 31 | }); |
| 50 | 32 | |
| 51 | −const TIMEOUT_MS = 8_000; | |
| 52 | − | |
| 53 | −const timer = setTimeout(() => { | |
| 33 | +const timeout = setTimeout(() => { | |
| 54 | 34 | child.kill(); |
| 55 | −}, TIMEOUT_MS); | |
| 35 | +}, 8_000); | |
| 56 | 36 | |
| 57 | 37 | child.on("exit", () => { |
| 58 | − clearTimeout(timer); | |
| 38 | + clearTimeout(timeout); | |
| 59 | 39 | |
| 60 | − // Fatal patterns that indicate broken builds | |
| 61 | 40 | const fatalPatterns = [ |
| 62 | 41 | "Cannot find module", |
| 63 | 42 | "MODULE_NOT_FOUND", |
| 64 | − "Refused to execute", // CSP blocking scripts | |
| 65 | − "can't detect preamble", // @vitejs/plugin-react failure | |
| 43 | + "Refused to execute", | |
| 66 | 44 | "Uncaught Error", |
| 67 | 45 | "Uncaught TypeError", |
| 68 | 46 | "Uncaught ReferenceError", |
| 69 | 47 | ]; |
| 70 | − | |
| 71 | − const failures = fatalPatterns.filter((p) => output.includes(p)); | |
| 48 | + const failures = fatalPatterns.filter((pattern) => output.includes(pattern)); | |
| 72 | 49 | |
| 73 | 50 | if (failures.length > 0) { |
| 74 | − console.error("\n❌ Smoke test FAILED. Matched fatal patterns:"); | |
| 75 | − for (const f of failures) { | |
| 76 | − console.error(` • ${f}`); | |
| 51 | + console.error("\nDesktop smoke test failed:"); | |
| 52 | + for (const failure of failures) { | |
| 53 | + console.error(` - ${failure}`); | |
| 77 | 54 | } |
| 78 | 55 | console.error("\nFull output:\n" + output); |
| 79 | 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 | 60 | process.exit(0); |
| 84 | 61 | }); |
| @@ -13,6 +13,6 @@ export function fixPath(): void { · 12 unmodified lines | ||
| 13 | 13 | process.env.PATH = result; |
| 14 | 14 | } |
| 15 | 15 | } catch { |
| 16 | − // Silently ignore — keep default PATH | |
| 16 | + // Keep inherited PATH if shell lookup fails. | |
| 17 | 17 | } |
| 18 | 18 | } |
| @@ -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 | −}); | |
| @@ -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 | −} | |
| @@ -1,111 +1,134 @@ | ||
| 1 | −import { fixPath } from "./fixPath"; | |
| 2 | −fixPath(); | |
| 3 | − | |
| 4 | −import { spawn } from "node:child_process"; | |
| 1 | +import { spawn, type ChildProcess } from "node:child_process"; | |
| 2 | +import { randomBytes } from "node:crypto"; | |
| 3 | +import fs from "node:fs"; | |
| 4 | +import net from "node:net"; | |
| 5 | +import os from "node:os"; | |
| 5 | 6 | import path from "node:path"; |
| 6 | −import { BrowserWindow, app, dialog, ipcMain, session, shell } from "electron"; | |
| 7 | 7 | |
| 8 | −import { | |
| 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"; | |
| 8 | +import { app, BrowserWindow, dialog, ipcMain } from "electron"; | |
| 28 | 9 | |
| 29 | −const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); | |
| 10 | +import { fixPath } from "./fixPath"; | |
| 30 | 11 | |
| 31 | −let todoStore: TodoStore; | |
| 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 | − } | |
| 12 | +fixPath(); | |
| 40 | 13 | |
| 41 | − return [agentSessionIdSchema.parse(sessionId), data]; | |
| 42 | − }, | |
| 43 | −}; | |
| 14 | +const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; | |
| 15 | +const ROOT_DIR = path.resolve(__dirname, "../../.."); | |
| 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 { | |
| 46 | − const window = new BrowserWindow({ | |
| 47 | − width: 1100, | |
| 48 | − height: 780, | |
| 49 | − minWidth: 840, | |
| 50 | − minHeight: 620, | |
| 51 | − show: false, | |
| 52 | − autoHideMenuBar: true, | |
| 53 | − titleBarStyle: "hiddenInset", | |
| 54 | − trafficLightPosition: { x: 16, y: 18 }, | |
| 55 | − webPreferences: { | |
| 56 | − preload: path.join(__dirname, "preload.js"), | |
| 57 | − contextIsolation: true, | |
| 58 | − nodeIntegration: false, | |
| 59 | − sandbox: true, | |
| 60 | − }, | |
| 21 | +let mainWindow: BrowserWindow | null = null; | |
| 22 | +let backendProcess: ChildProcess | null = null; | |
| 23 | +let backendPort = 0; | |
| 24 | +let backendAuthToken = ""; | |
| 25 | +let backendWsUrl = ""; | |
| 26 | +let restartAttempt = 0; | |
| 27 | +let restartTimer: ReturnType<typeof setTimeout> | null = null; | |
| 28 | +let isQuitting = false; | |
| 29 | + | |
| 30 | +async function reserveLoopbackPort(): Promise<number> { | |
| 31 | + return new Promise((resolve, reject) => { | |
| 32 | + const probe = net.createServer(); | |
| 33 | + probe.listen(0, "127.0.0.1", () => { | |
| 34 | + const address = probe.address(); | |
| 35 | + const port = typeof address === "object" && address !== null ? address.port : 0; | |
| 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", () => { | |
| 66 | − window.show(); | |
| 67 | − }); | |
| 59 | +function scheduleBackendRestart(reason: string): void { | |
| 60 | + if (isQuitting || restartTimer) return; | |
| 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) { | |
| 72 | − void window.loadURL(process.env.VITE_DEV_SERVER_URL as string); | |
| 73 | − return window; | |
| 66 | + restartTimer = setTimeout(() => { | |
| 67 | + restartTimer = null; | |
| 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")); | |
| 77 | − return window; | |
| 78 | −} | |
| 80 | + const child = spawn(process.execPath, [BACKEND_ENTRY], { | |
| 81 | + cwd: ROOT_DIR, | |
| 82 | + env: backendEnv(), | |
| 83 | + stdio: "inherit", | |
| 84 | + }); | |
| 85 | + backendProcess = child; | |
| 79 | 86 | |
| 80 | −function registerIpcHandlers(): void { | |
| 81 | − // Todo handlers | |
| 82 | − ipcMain.handle(IPC_CHANNELS.todosList, async () => { | |
| 83 | − return todoStore.list(); | |
| 87 | + child.once("spawn", () => { | |
| 88 | + restartAttempt = 0; | |
| 89 | + }); | |
| 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( | |
| 87 | − IPC_CHANNELS.todosAdd, | |
| 88 | − withParsedPayload(newTodoInputSchema, async (_event, payload) => { | |
| 89 | − return todoStore.add(payload); | |
| 90 | − }), | |
| 91 | − ); | |
| 108 | +function stopBackend(): void { | |
| 109 | + if (restartTimer) { | |
| 110 | + clearTimeout(restartTimer); | |
| 111 | + restartTimer = null; | |
| 112 | + } | |
| 92 | 113 | |
| 93 | − ipcMain.handle( | |
| 94 | − IPC_CHANNELS.todosToggle, | |
| 95 | − withParsedPayload(todoIdSchema, async (_event, id) => { | |
| 96 | − return todoStore.toggle(id); | |
| 97 | − }), | |
| 98 | − ); | |
| 114 | + const child = backendProcess; | |
| 115 | + backendProcess = null; | |
| 116 | + if (!child) return; | |
| 99 | 117 | |
| 100 | − ipcMain.handle( | |
| 101 | − IPC_CHANNELS.todosRemove, | |
| 102 | − withParsedPayload(todoIdSchema, async (_event, id) => { | |
| 103 | − return todoStore.remove(id); | |
| 104 | − }), | |
| 105 | − ); | |
| 118 | + if (!child.killed) { | |
| 119 | + child.kill("SIGTERM"); | |
| 120 | + setTimeout(() => { | |
| 121 | + if (!child.killed) { | |
| 122 | + child.kill("SIGKILL"); | |
| 123 | + } | |
| 124 | + }, 2_000).unref(); | |
| 125 | + } | |
| 126 | +} | |
| 106 | 127 | |
| 107 | − ipcMain.handle(IPC_CHANNELS.dialogPickFolder, async () => { | |
| 108 | − const owner = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0]; | |
| 128 | +function registerIpcHandlers(): void { | |
| 129 | + ipcMain.removeHandler(PICK_FOLDER_CHANNEL); | |
| 130 | + ipcMain.handle(PICK_FOLDER_CHANNEL, async () => { | |
| 131 | + const owner = BrowserWindow.getFocusedWindow() ?? mainWindow; | |
| 109 | 132 | const result = owner |
| 110 | 133 | ? await dialog.showOpenDialog(owner, { |
| 111 | 134 | properties: ["openDirectory", "createDirectory"], |
| @@ -113,233 +136,76 @@ function registerIpcHandlers(): void { · 1 unmodified line | ||
| 113 | 136 | : await dialog.showOpenDialog({ |
| 114 | 137 | properties: ["openDirectory", "createDirectory"], |
| 115 | 138 | }); |
| 116 | − | |
| 117 | 139 | if (result.canceled) return null; |
| 118 | 140 | return result.filePaths[0] ?? null; |
| 119 | 141 | }); |
| 142 | +} | |
| 120 | 143 | |
| 121 | − // Terminal handlers | |
| 122 | − ipcMain.handle( | |
| 123 | − IPC_CHANNELS.terminalRun, | |
| 124 | − withParsedPayload(terminalCommandInputSchema, async (_event, payload) => { | |
| 125 | − return runTerminalCommand(payload); | |
| 126 | − }), | |
| 127 | − ); | |
| 128 | − | |
| 129 | − // Shell handlers | |
| 130 | − ipcMain.handle( | |
| 131 | − IPC_CHANNELS.shellOpenInEditor, | |
| 132 | − async (_event, cwd: string, editor: string) => { | |
| 133 | − if (!cwd) throw new Error("cwd is required"); | |
| 134 | − const editorDef = EDITORS.find((e) => e.id === editor); | |
| 135 | − if (!editorDef) throw new Error(`Unknown editor: ${editor}`); | |
| 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(); | |
| 144 | +function createWindow(): BrowserWindow { | |
| 145 | + const window = new BrowserWindow({ | |
| 146 | + width: 1100, | |
| 147 | + height: 780, | |
| 148 | + minWidth: 840, | |
| 149 | + minHeight: 620, | |
| 150 | + show: false, | |
| 151 | + autoHideMenuBar: true, | |
| 152 | + titleBarStyle: "hiddenInset", | |
| 153 | + trafficLightPosition: { x: 16, y: 18 }, | |
| 154 | + webPreferences: { | |
| 155 | + preload: path.join(__dirname, "preload.js"), | |
| 156 | + contextIsolation: true, | |
| 157 | + nodeIntegration: false, | |
| 158 | + sandbox: true, | |
| 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) => { | |
| 262 | − clearTimeout(timeout); | |
| 263 | − resolve({ | |
| 264 | − stdout, | |
| 265 | − stderr, | |
| 266 | − code: code ?? null, | |
| 267 | − signal: signal ?? null, | |
| 268 | − timedOut, | |
| 269 | − }); | |
| 270 | − }); | |
| 162 | + window.webContents.setWindowOpenHandler(() => ({ action: "deny" })); | |
| 163 | + window.once("ready-to-show", () => { | |
| 164 | + window.show(); | |
| 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) => { | |
| 282 | − if (!window.isDestroyed()) { | |
| 283 | − window.webContents.send(IPC_CHANNELS.agentExit, exit); | |
| 284 | − } | |
| 285 | − }; | |
| 286 | − | |
| 287 | − const onProviderEvent = (event: unknown) => { | |
| 288 | − if (!window.isDestroyed()) { | |
| 289 | − window.webContents.send(IPC_CHANNELS.providerEvent, event); | |
| 167 | + if (isDevelopment) { | |
| 168 | + void window.loadURL(process.env.VITE_DEV_SERVER_URL as string); | |
| 169 | + } else { | |
| 170 | + if (!fs.existsSync(RENDERER_ENTRY)) { | |
| 171 | + throw new Error(`Renderer bundle missing at ${RENDERER_ENTRY}`); | |
| 290 | 172 | } |
| 291 | − }; | |
| 292 | − | |
| 293 | − processManager.on("output", onOutput); | |
| 294 | − processManager.on("exit", onExit); | |
| 295 | − providerManager.on("event", onProviderEvent); | |
| 173 | + void window.loadFile(RENDERER_ENTRY); | |
| 174 | + } | |
| 296 | 175 | |
| 297 | 176 | window.on("closed", () => { |
| 298 | − processManager.off("output", onOutput); | |
| 299 | − processManager.off("exit", onExit); | |
| 300 | − providerManager.off("event", onProviderEvent); | |
| 177 | + if (mainWindow === window) { | |
| 178 | + mainWindow = null; | |
| 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({ | |
| 311 | − responseHeaders: { | |
| 312 | − ...details.responseHeaders, | |
| 313 | − "Content-Security-Policy": [csp], | |
| 314 | − }, | |
| 315 | − }); | |
| 316 | − }); | |
| 182 | + return window; | |
| 317 | 183 | } |
| 318 | 184 | |
| 319 | 185 | async function bootstrap(): Promise<void> { |
| 320 | − setupCSP(); | |
| 321 | − | |
| 322 | − todoStore = new TodoStore(path.join(app.getPath("userData"), "todos.json")); | |
| 323 | − await todoStore.init(); | |
| 186 | + backendPort = await reserveLoopbackPort(); | |
| 187 | + backendAuthToken = randomBytes(24).toString("hex"); | |
| 188 | + backendWsUrl = `ws://127.0.0.1:${backendPort}/?token=${encodeURIComponent(backendAuthToken)}`; | |
| 189 | + process.env.CODETHING_DESKTOP_WS_URL = backendWsUrl; | |
| 324 | 190 | |
| 325 | 191 | registerIpcHandlers(); |
| 326 | − createWindow(); | |
| 327 | − | |
| 328 | − app.on("activate", () => { | |
| 329 | − if (BrowserWindow.getAllWindows().length === 0) { | |
| 330 | − createWindow(); | |
| 331 | − } | |
| 332 | − }); | |
| 192 | + startBackend(); | |
| 193 | + mainWindow = createWindow(); | |
| 333 | 194 | } |
| 334 | 195 | |
| 335 | 196 | app.on("before-quit", () => { |
| 336 | − processManager.killAll(); | |
| 337 | − providerManager.stopAll(); | |
| 338 | − providerManager.dispose(); | |
| 197 | + isQuitting = true; | |
| 198 | + stopBackend(); | |
| 339 | 199 | }); |
| 340 | 200 | |
| 341 | 201 | app.whenReady().then(() => { |
| 342 | 202 | void bootstrap(); |
| 203 | + | |
| 204 | + app.on("activate", () => { | |
| 205 | + if (BrowserWindow.getAllWindows().length === 0) { | |
| 206 | + mainWindow = createWindow(); | |
| 207 | + } | |
| 208 | + }); | |
| 343 | 209 | }); |
| 344 | 210 | |
| 345 | 211 | app.on("window-all-closed", () => { |
| @@ -1,55 +1,9 @@ | ||
| 1 | 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 | − todos: { | |
| 7 | − list: () => ipcRenderer.invoke(IPC_CHANNELS.todosList), | |
| 8 | − add: (input) => ipcRenderer.invoke(IPC_CHANNELS.todosAdd, input), | |
| 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); | |
| 6 | +contextBridge.exposeInMainWorld("desktopBridge", { | |
| 7 | + getWsUrl: () => wsUrl, | |
| 8 | + pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL) as Promise<string | null>, | |
| 9 | +}); | |
| @@ -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 | −}); | |
| @@ -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 | −} | |
| @@ -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 | −} | |
| @@ -3,7 +3,7 @@ · 2 unmodified lines | ||
| 3 | 3 | <head> |
| 4 | 4 | <meta charset="UTF-8" /> |
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 6 | − <title>Electron Todo</title> | |
| 6 | + <title>CodeThing</title> | |
| 7 | 7 | </head> |
| 8 | 8 | <body> |
| 9 | 9 | <div id="root"></div> |
| @@ -3,8 +3,11 @@ import { useEffect, useMemo, useRef } from "react"; · 2 unmodified lines | ||
| 3 | 3 | import ChatView from "./components/ChatView"; |
| 4 | 4 | import DiffPanel from "./components/DiffPanel"; |
| 5 | 5 | import Sidebar from "./components/Sidebar"; |
| 6 | +import { isElectron } from "./env"; | |
| 7 | +import { DEFAULT_MODEL } from "./model-logic"; | |
| 6 | 8 | import { readNativeApi } from "./session-logic"; |
| 7 | 9 | import { StoreProvider, useStore } from "./store"; |
| 10 | +import { onServerWelcome } from "./wsNativeApi"; | |
| 8 | 11 | |
| 9 | 12 | function EventRouter() { |
| 10 | 13 | const api = useMemo(() => readNativeApi(), []); |
| @@ -25,6 +28,118 @@ function EventRouter() { · 14 unmodified lines | ||
| 25 | 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 | 143 | function Layout() { |
| 29 | 144 | const api = useMemo(() => readNativeApi(), []); |
| 30 | 145 | const { state } = useStore(); |
| @@ -32,11 +147,8 @@ function Layout() { · 1 unmodified line | ||
| 32 | 147 | if (!api) { |
| 33 | 148 | return ( |
| 34 | 149 | <div className="flex h-screen flex-col bg-background text-foreground"> |
| 35 | − <div className="drag-region h-[52px] shrink-0" /> | |
| 36 | 150 | <div className="flex flex-1 items-center justify-center"> |
| 37 | − <p className="text-sm text-muted-foreground"> | |
| 38 | − Native bridge unavailable. Launch through Electron. | |
| 39 | − </p> | |
| 151 | + <p className="text-sm text-muted-foreground">Connecting to CodeThing server...</p> | |
| 40 | 152 | </div> |
| 41 | 153 | </div> |
| 42 | 154 | ); |
| @@ -45,6 +157,8 @@ function Layout() { · 2 unmodified lines | ||
| 45 | 157 | return ( |
| 46 | 158 | <div className="flex h-screen overflow-hidden bg-background text-foreground"> |
| 47 | 159 | <EventRouter /> |
| 160 | + <AutoProjectBootstrap /> | |
| 161 | + <DesktopProjectBootstrap /> | |
| 48 | 162 | <Sidebar /> |
| 49 | 163 | <ChatView /> |
| 50 | 164 | {state.diffOpen && <DiffPanel />} |
| @@ -14,6 +14,7 @@ import { · 13 unmodified lines | ||
| 14 | 14 | } from "react"; |
| 15 | 15 | |
| 16 | 16 | import { EDITORS, type EditorId } from "@acme/contracts"; |
| 17 | +import { isElectron } from "../env"; | |
| 17 | 18 | import { buildBootstrapInput } from "../historyBootstrap"; |
| 18 | 19 | import { |
| 19 | 20 | DEFAULT_MODEL, |
| @@ -563,7 +564,7 @@ export default function ChatView() { · 543 unmodified lines | ||
| 563 | 564 | if (!activeThread) { |
| 564 | 565 | return ( |
| 565 | 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 | 568 | <div className="flex flex-1 items-center justify-center"> |
| 568 | 569 | <div className="text-center"> |
| 569 | 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() { · 6 unmodified lines | ||
| 576 | 577 | return ( |
| 577 | 578 | <div className="flex flex-1 flex-col bg-background"> |
| 578 | 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 | 581 | <div className="flex items-center gap-3"> |
| 581 | 582 | <h2 className="text-sm font-medium text-foreground"> |
| 582 | 583 | {activeThread.title} |
| @@ -1,3 +1,4 @@ | ||
| 1 | +import { isElectron } from "../env"; | |
| 1 | 2 | import { useStore } from "../store"; |
| 2 | 3 | |
| 3 | 4 | export default function DiffPanel() { |
| @@ -6,7 +7,7 @@ export default function DiffPanel() { · 2 unmodified lines | ||
| 6 | 7 | return ( |
| 7 | 8 | <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-border bg-card"> |
| 8 | 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 | 11 | <h3 className="text-xs font-medium text-foreground">Uncommitted changes</h3> |
| 11 | 12 | <button |
| 12 | 13 | type="button" |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react"; |
| 2 | 2 | import { useCallback, useEffect, useMemo, useState } from "react"; |
| 3 | +import { isElectron } from "../env"; | |
| 3 | 4 | import { useTheme } from "../hooks/useTheme"; |
| 4 | 5 | import { DEFAULT_MODEL } from "../model-logic"; |
| 5 | 6 | import { readNativeApi } from "../session-logic"; |
| @@ -17,6 +18,13 @@ function formatRelativeTime(iso: string): string { · 11 unmodified lines | ||
| 17 | 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 | 28 | function threadStatusLabel( |
| 21 | 29 | status: "connecting" | "ready" | "running" | "error" | "closed" | undefined, |
| 22 | 30 | ): "Working" | "Connecting" | null { |
| @@ -32,22 +40,7 @@ export default function Sidebar() { · 9 unmodified lines | ||
| 32 | 40 | const [addingProject, setAddingProject] = useState(false); |
| 33 | 41 | const [newCwd, setNewCwd] = useState(""); |
| 34 | 42 | const [isPickingFolder, setIsPickingFolder] = useState(false); |
| 35 | − | |
| 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 | − }; | |
| 43 | + const [isAddingProject, setIsAddingProject] = useState(false); | |
| 51 | 44 | |
| 52 | 45 | const handleNewThread = useCallback( |
| 53 | 46 | (projectId: string) => { |
| @@ -58,9 +51,7 @@ export default function Sidebar() { · 4 unmodified lines | ||
| 58 | 51 | codexThreadId: null, |
| 59 | 52 | projectId, |
| 60 | 53 | title: "New thread", |
| 61 | − model: | |
| 62 | − state.projects.find((p) => p.id === projectId)?.model ?? | |
| 63 | − DEFAULT_MODEL, | |
| 54 | + model: state.projects.find((p) => p.id === projectId)?.model ?? DEFAULT_MODEL, | |
| 64 | 55 | session: null, |
| 65 | 56 | messages: [], |
| 66 | 57 | events: [], |
| @@ -72,13 +63,98 @@ export default function Sidebar() { · 5 unmodified lines | ||
| 72 | 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 | 151 | const handlePickFolder = async () => { |
| 76 | 152 | if (!api || isPickingFolder) return; |
| 77 | 153 | setIsPickingFolder(true); |
| 78 | 154 | try { |
| 79 | 155 | const pickedPath = await api.dialogs.pickFolder(); |
| 80 | 156 | if (!pickedPath) return; |
| 81 | − setNewCwd(pickedPath); | |
| 157 | + await addProjectFromPath(pickedPath); | |
| 82 | 158 | } finally { |
| 83 | 159 | setIsPickingFolder(false); |
| 84 | 160 | } |
| @@ -107,9 +183,7 @@ export default function Sidebar() { · 22 unmodified lines | ||
| 107 | 183 | } |
| 108 | 184 | } |
| 109 | 185 | |
| 110 | − const activeThread = state.threads.find( | |
| 111 | − (t) => t.id === state.activeThreadId, | |
| 112 | − ); | |
| 186 | + const activeThread = state.threads.find((t) => t.id === state.activeThreadId); | |
| 113 | 187 | const projectId = activeThread?.projectId ?? state.projects[0]?.id; |
| 114 | 188 | if (!projectId) return; |
| 115 | 189 | |
| @@ -125,10 +199,10 @@ export default function Sidebar() { · 9 unmodified lines | ||
| 125 | 199 | |
| 126 | 200 | return ( |
| 127 | 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 */} | |
| 129 | − <div className="drag-region h-[52px] shrink-0" /> | |
| 202 | + {/* Drag region / traffic light space (Electron only) */} | |
| 203 | + {isElectron && <div className="drag-region h-[52px] shrink-0" />} | |
| 130 | 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 | 206 | <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-xs font-bold text-primary-foreground"> |
| 133 | 207 | CT |
| 134 | 208 | </div> |
| @@ -176,10 +250,8 @@ export default function Sidebar() { · 41 unmodified lines | ||
| 176 | 250 | {state.projects.map((project) => { |
| 177 | 251 | const threads = state.threads |
| 178 | 252 | .filter((t) => t.projectId === project.id) |
| 179 | − .sort((a, b) => { | |
| 180 | − const byDate = | |
| 181 | − new Date(b.createdAt).getTime() - | |
| 182 | − new Date(a.createdAt).getTime(); | |
| 253 | + .toSorted((a, b) => { | |
| 254 | + const byDate = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); | |
| 183 | 255 | if (byDate !== 0) return byDate; |
| 184 | 256 | return b.id.localeCompare(a.id); |
| 185 | 257 | }); |
| @@ -205,15 +277,15 @@ export default function Sidebar() { · 19 unmodified lines | ||
| 205 | 277 | <div className="ml-2 border-l border-border/80 pl-2"> |
| 206 | 278 | {threads.map((thread) => { |
| 207 | 279 | const isActive = state.activeThreadId === thread.id; |
| 208 | − const threadStatus = threadStatusLabel( | |
| 209 | − thread.session?.status, | |
| 210 | − ); | |
| 280 | + const threadStatus = threadStatusLabel(thread.session?.status); | |
| 211 | 281 | return ( |
| 212 | 282 | <button |
| 213 | 283 | key={thread.id} |
| 214 | 284 | type="button" |
| 215 | 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 | 290 | onClick={() => |
| 219 | 291 | dispatch({ |
| @@ -226,14 +298,10 @@ export default function Sidebar() { · 6 unmodified lines | ||
| 226 | 298 | {threadStatus && ( |
| 227 | 299 | <span className="inline-flex items-center gap-1 text-[10px] text-sky-600 dark:text-sky-300/80"> |
| 228 | 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"> | |
| 230 | − {threadStatus} | |
| 231 | − </span> | |
| 301 | + <span className="hidden xl:inline">{threadStatus}</span> | |
| 232 | 302 | </span> |
| 233 | 303 | )} |
| 234 | − <span className="min-w-0 flex-1 truncate text-xs"> | |
| 235 | − {thread.title} | |
| 236 | − </span> | |
| 304 | + <span className="min-w-0 flex-1 truncate text-xs">{thread.title}</span> | |
| 237 | 305 | </div> |
| 238 | 306 | <span className="shrink-0 text-[10px] text-muted-foreground/40"> |
| 239 | 307 | {formatRelativeTime(thread.createdAt)} |
| @@ -281,12 +349,12 @@ export default function Sidebar() { · 41 unmodified lines | ||
| 281 | 349 | if (e.key === "Escape") setAddingProject(false); |
| 282 | 350 | }} |
| 283 | 351 | /> |
| 284 | − {api && ( | |
| 352 | + {isElectron && api && ( | |
| 285 | 353 | <button |
| 286 | 354 | type="button" |
| 287 | 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 | 356 | onClick={() => void handlePickFolder()} |
| 289 | − disabled={isPickingFolder} | |
| 357 | + disabled={isPickingFolder || isAddingProject} | |
| 290 | 358 | > |
| 291 | 359 | {isPickingFolder ? "Picking folder..." : "Browse for folder"} |
| 292 | 360 | </button> |
| @@ -296,8 +364,9 @@ export default function Sidebar() { · 3 unmodified lines | ||
| 296 | 364 | type="button" |
| 297 | 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 | 366 | onClick={handleAddProject} |
| 367 | + disabled={isAddingProject} | |
| 299 | 368 | > |
| 300 | − Add | |
| 369 | + {isAddingProject ? "Adding..." : "Add"} | |
| 301 | 370 | </button> |
| 302 | 371 | <button |
| 303 | 372 | type="button" |
| @@ -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); | |
| @@ -1,5 +1,6 @@ | ||
| 1 | −import type { ProviderEvent, ProviderKind, ProviderSession } from "@acme/contracts"; | |
| 1 | +import type { NativeApi, ProviderEvent, ProviderKind, ProviderSession } from "@acme/contracts"; | |
| 2 | 2 | import type { ChatMessage, SessionPhase } from "./types"; |
| 3 | +import { createWsNativeApi } from "./wsNativeApi"; | |
| 3 | 4 | |
| 4 | 5 | export const PROVIDER_OPTIONS: Array<{ |
| 5 | 6 | value: ProviderKind; |
| @@ -10,9 +11,21 @@ export const PROVIDER_OPTIONS: Array<{ · 4 unmodified lines | ||
| 10 | 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 | 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 | 31 | export function asObject(value: unknown): Record<string, unknown> | undefined { |
| @@ -4,9 +4,7 @@ import { describe, expect, it } from "vitest"; · 3 unmodified lines | ||
| 4 | 4 | import { type AppState, reducer } from "./store"; |
| 5 | 5 | import type { Thread } from "./types"; |
| 6 | 6 | |
| 7 | −function makeSession( | |
| 8 | − overrides: Partial<ProviderSession> = {}, | |
| 9 | −): ProviderSession { | |
| 7 | +function makeSession(overrides: Partial<ProviderSession> = {}): ProviderSession { | |
| 10 | 8 | return { |
| 11 | 9 | sessionId: "sess-1", |
| 12 | 10 | provider: "codex", |
| @@ -131,4 +129,60 @@ describe("store reducer thread continuity", () => { · 118 unmodified lines | ||
| 131 | 129 | expect(next.threads[0]?.codexThreadId).toBe("thr_new"); |
| 132 | 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 | }); |
| @@ -11,23 +11,14 @@ import { · 10 unmodified lines | ||
| 11 | 11 | import type { ProviderEvent, ProviderSession } from "@acme/contracts"; |
| 12 | 12 | import { resolveModelSlug } from "./model-logic"; |
| 13 | 13 | import { hydratePersistedState, toPersistedState } from "./persistenceSchema"; |
| 14 | −import { | |
| 15 | − applyEventToMessages, | |
| 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"; | |
| 14 | +import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic"; | |
| 15 | +import { DEFAULT_RUNTIME_MODE, type Project, type RuntimeMode, type Thread } from "./types"; | |
| 26 | 16 | |
| 27 | 17 | // ── Actions ────────────────────────────────────────────────────────── |
| 28 | 18 | |
| 29 | 19 | type Action = |
| 30 | 20 | | { type: "ADD_PROJECT"; project: Project } |
| 21 | + | { type: "SYNC_PROJECTS"; projects: Project[] } | |
| 31 | 22 | | { type: "TOGGLE_PROJECT"; projectId: string } |
| 32 | 23 | | { type: "ADD_THREAD"; thread: Thread } |
| 33 | 24 | | { type: "SET_ACTIVE_THREAD"; threadId: string } |
| @@ -98,10 +89,7 @@ function persistState(state: AppState): void { · 64 unmodified lines | ||
| 98 | 89 | if (typeof window === "undefined") return; |
| 99 | 90 | |
| 100 | 91 | try { |
| 101 | − window.localStorage.setItem( | |
| 102 | − PERSISTED_STATE_KEY, | |
| 103 | − JSON.stringify(toPersistedState(state)), | |
| 104 | − ); | |
| 92 | + window.localStorage.setItem(PERSISTED_STATE_KEY, JSON.stringify(toPersistedState(state))); | |
| 105 | 93 | for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) { |
| 106 | 94 | window.localStorage.removeItem(legacyKey); |
| 107 | 95 | } |
| @@ -143,10 +131,7 @@ function getEventThreadId(event: ProviderEvent): string | undefined { · 35 unmodified lines | ||
| 143 | 131 | ); |
| 144 | 132 | } |
| 145 | 133 | |
| 146 | −function shouldIgnoreForeignThreadEvent( | |
| 147 | − thread: Thread, | |
| 148 | − event: ProviderEvent, | |
| 149 | −): boolean { | |
| 134 | +function shouldIgnoreForeignThreadEvent(thread: Thread, event: ProviderEvent): boolean { | |
| 150 | 135 | const eventThreadId = getEventThreadId(event); |
| 151 | 136 | if (!eventThreadId) { |
| 152 | 137 | return false; |
| @@ -175,10 +160,7 @@ function durationMs(startIso: string, endIso: string): number | undefined { · 22 unmodified lines | ||
| 175 | 160 | return end - start; |
| 176 | 161 | } |
| 177 | 162 | |
| 178 | −function updateTurnFields( | |
| 179 | − thread: Thread, | |
| 180 | − event: ProviderEvent, | |
| 181 | −): Partial<Thread> { | |
| 163 | +function updateTurnFields(thread: Thread, event: ProviderEvent): Partial<Thread> { | |
| 182 | 164 | if (event.method === "turn/started") { |
| 183 | 165 | return { |
| 184 | 166 | latestTurnId: getEventTurnId(event) ?? thread.latestTurnId, |
| @@ -195,9 +177,7 @@ function updateTurnFields( · 10 unmodified lines | ||
| 195 | 177 | ? thread.latestTurnStartedAt |
| 196 | 178 | : undefined; |
| 197 | 179 | const elapsed = |
| 198 | − startedAt && startedAt.length > 0 | |
| 199 | − ? durationMs(startedAt, event.createdAt) | |
| 200 | − : undefined; | |
| 180 | + startedAt && startedAt.length > 0 ? durationMs(startedAt, event.createdAt) : undefined; | |
| 201 | 181 | |
| 202 | 182 | return { |
| 203 | 183 | latestTurnId: completedTurnId ?? thread.latestTurnId, |
| @@ -214,6 +194,9 @@ function updateTurnFields( · 10 unmodified lines | ||
| 214 | 194 | export function reducer(state: AppState, action: Action): AppState { |
| 215 | 195 | switch (action.type) { |
| 216 | 196 | case "ADD_PROJECT": |
| 197 | + if (state.projects.some((project) => project.cwd === action.project.cwd)) { | |
| 198 | + return state; | |
| 199 | + } | |
| 217 | 200 | return { |
| 218 | 201 | ...state, |
| 219 | 202 | projects: [ |
| @@ -225,6 +208,48 @@ export function reducer(state: AppState, action: Action): AppState { · 5 unmodified lines | ||
| 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 | 253 | case "TOGGLE_PROJECT": |
| 229 | 254 | return { |
| 230 | 255 | ...state, |
| @@ -265,24 +290,16 @@ export function reducer(state: AppState, action: Action): AppState { · 34 unmodified lines | ||
| 265 | 290 | ...(() => { |
| 266 | 291 | const eventThreadId = getEventThreadId(event); |
| 267 | 292 | const shouldRebindIdentity = |
| 268 | − event.method === "thread/started" && | |
| 269 | − t.session?.status === "connecting"; | |
| 293 | + event.method === "thread/started" && t.session?.status === "connecting"; | |
| 270 | 294 | return { |
| 271 | 295 | codexThreadId: shouldRebindIdentity |
| 272 | − ? eventThreadId ?? t.codexThreadId | |
| 273 | − : t.codexThreadId ?? eventThreadId ?? null, | |
| 274 | − error: | |
| 275 | − (event.kind === "error" && event.message | |
| 276 | − ? event.message | |
| 277 | − : t.error), | |
| 296 | + ? (eventThreadId ?? t.codexThreadId) | |
| 297 | + : (t.codexThreadId ?? eventThreadId ?? null), | |
| 298 | + error: event.kind === "error" && event.message ? event.message : t.error, | |
| 278 | 299 | }; |
| 279 | 300 | })(), |
| 280 | 301 | session: t.session ? evolveSession(t.session, event) : t.session, |
| 281 | − messages: applyEventToMessages( | |
| 282 | − t.messages, | |
| 283 | − event, | |
| 284 | − activeAssistantItemRef, | |
| 285 | − ), | |
| 302 | + messages: applyEventToMessages(t.messages, event, activeAssistantItemRef), | |
| 286 | 303 | events: [event, ...t.events], |
| 287 | 304 | ...updateTurnFields(t, event), |
| 288 | 305 | })), |
| @@ -2,8 +2,14 @@ · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | import type { NativeApi } from "@acme/contracts"; |
| 4 | 4 | |
| 5 | +interface DesktopBridge { | |
| 6 | + getWsUrl: () => string | null; | |
| 7 | + pickFolder: () => Promise<string | null>; | |
| 8 | +} | |
| 9 | + | |
| 5 | 10 | declare global { |
| 6 | 11 | interface Window { |
| 7 | 12 | nativeApi?: NativeApi; |
| 13 | + desktopBridge?: DesktopBridge; | |
| 8 | 14 | } |
| 9 | 15 | } |
| @@ -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 | +} | |
| @@ -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 | +} | |
| @@ -11,8 +11,14 @@ export default defineConfig({ · 10 unmodified lines | ||
| 11 | 11 | }), |
| 12 | 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 | 20 | server: { |
| 15 | − port: Number(process.env.ELECTRON_RENDERER_PORT ?? 5173), | |
| 21 | + port: Number(process.env.PORT ?? 5173), | |
| 16 | 22 | strictPort: true, |
| 17 | 23 | }, |
| 18 | 24 | build: { |
| @@ -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 | +} | |
| @@ -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"); | |
File renamed without changes.
File renamed without changes.
| @@ -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 | +} | |
| @@ -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 | +}); | |
| @@ -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 | +} | |
| @@ -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 | +} | |
File renamed without changes.
File renamed without changes.
| @@ -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 | +}); | |
| @@ -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 | +} | |
| @@ -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 | +} | |
| @@ -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 | +}); | |
| @@ -16,17 +16,15 @@ · 15 unmodified lines | ||
| 16 | 16 | "name": "@acme/desktop", |
| 17 | 17 | "version": "0.0.0", |
| 18 | 18 | "dependencies": { |
| 19 | − "@acme/contracts": "workspace:*", | |
| 20 | 19 | "electron": "33.4.11", |
| 21 | − "node-pty": "^1.0.0", | |
| 22 | 20 | }, |
| 23 | 21 | "devDependencies": { |
| 24 | − "@electron/rebuild": "^3.7.0", | |
| 25 | 22 | "@types/node": "^22.10.2", |
| 26 | 23 | "concurrently": "^9.1.2", |
| 27 | 24 | "electronmon": "^2.0.2", |
| 28 | 25 | "tsup": "^8.3.5", |
| 29 | 26 | "typescript": "^5.7.3", |
| 27 | + "vitest": "^3.0.0", | |
| 30 | 28 | "wait-on": "^8.0.2", |
| 31 | 29 | }, |
| 32 | 30 | }, |
| @@ -55,6 +53,26 @@ · 22 unmodified lines | ||
| 55 | 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 | 76 | "packages/contracts": { |
| 59 | 77 | "name": "@acme/contracts", |
| 60 | 78 | "version": "0.0.0", |
| @@ -114,10 +132,6 @@ · 53 unmodified lines | ||
| 114 | 132 | |
| 115 | 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 | 135 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], |
| 122 | 136 | |
| 123 | 137 | "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], |
| @@ -170,8 +184,6 @@ · 46 unmodified lines | ||
| 170 | 184 | |
| 171 | 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 | 187 | "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="], |
| 176 | 188 | |
| 177 | 189 | "@hapi/formula": ["@hapi/formula@3.0.2", "", {}, "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw=="], |
| @@ -194,12 +206,6 @@ · 16 unmodified lines | ||
| 194 | 206 | |
| 195 | 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 | 209 | "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww=="], |
| 204 | 210 | |
| 205 | 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 @@ · 114 unmodified lines | ||
| 320 | 326 | |
| 321 | 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 | 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 | 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 @@ · 34 unmodified lines | ||
| 362 | 366 | |
| 363 | 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 | 371 | "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], |
| 366 | 372 | |
| 367 | 373 | "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], |
| @@ -382,16 +388,8 @@ · 14 unmodified lines | ||
| 382 | 388 | |
| 383 | 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 | 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 | 393 | "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], |
| 396 | 394 | |
| 397 | 395 | "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], |
| @@ -402,38 +400,28 @@ · 4 unmodified lines | ||
| 402 | 400 | |
| 403 | 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 | 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 | 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 | 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 | 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 | 413 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], |
| 424 | 414 | |
| 425 | 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 | 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 | 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 | 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 | 425 | "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], |
| 438 | 426 | |
| 439 | 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 @@ · 20 unmodified lines | ||
| 460 | 448 | |
| 461 | 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 | 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 | 453 | "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], |
| 476 | 454 | |
| 477 | 455 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], |
| @@ -484,8 +462,6 @@ · 6 unmodified lines | ||
| 484 | 462 | |
| 485 | 463 | "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], |
| 486 | 464 | |
| 487 | − "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], | |
| 488 | − | |
| 489 | 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 | 467 | "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], |
| @@ -494,8 +470,6 @@ · 2 unmodified lines | ||
| 494 | 470 | |
| 495 | 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 | 473 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], |
| 500 | 474 | |
| 501 | 475 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], |
| @@ -506,12 +480,16 @@ · 4 unmodified lines | ||
| 506 | 480 | |
| 507 | 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 | 487 | "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], |
| 512 | 488 | |
| 513 | 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 | 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 | 495 | "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], |
| @@ -534,16 +512,12 @@ · 16 unmodified lines | ||
| 534 | 512 | |
| 535 | 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 | 515 | "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], |
| 540 | 516 | |
| 541 | 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 | 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 | 521 | "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], |
| 548 | 522 | |
| 549 | 523 | "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], |
| @@ -568,8 +542,6 @@ · 18 unmodified lines | ||
| 568 | 542 | |
| 569 | 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 | 545 | "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], |
| 574 | 546 | |
| 575 | 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 @@ · 10 unmodified lines | ||
| 586 | 558 | |
| 587 | 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=="], | |
| 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=="], | |
| 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=="], | |
| 594 | 562 | |
| 595 | 563 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], |
| 596 | 564 | |
| @@ -606,7 +574,7 @@ · 9 unmodified lines | ||
| 606 | 574 | |
| 607 | 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 | 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 @@ · 29 unmodified lines | ||
| 642 | 610 | |
| 643 | 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 | 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 | 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 | 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 | 619 | "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], |
| 674 | 620 | |
| 675 | 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 | 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 | 627 | "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], |
| 680 | 628 | |
| 681 | 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=="], | |
| 684 | − | |
| 685 | − "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="], | |
| 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=="], | |
| 686 | 632 | |
| 687 | 633 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], |
| 688 | 634 | |
| 689 | 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=="], | |
| 692 | − | |
| 693 | − "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], | |
| 637 | + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], | |
| 694 | 638 | |
| 695 | 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 @@ · 11 unmodified lines | ||
| 708 | 652 | |
| 709 | 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 | 657 | "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], |
| 714 | 658 | |
| @@ -746,8 +690,6 @@ · 31 unmodified lines | ||
| 746 | 690 | |
| 747 | 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 | 693 | "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], |
| 752 | 694 | |
| 753 | 695 | "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], |
| @@ -756,14 +698,12 @@ · 2 unmodified lines | ||
| 756 | 698 | |
| 757 | 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 | 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 | 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 | 707 | "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], |
| 768 | 708 | |
| 769 | 709 | "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], |
| @@ -862,30 +802,10 @@ · 92 unmodified lines | ||
| 862 | 802 | |
| 863 | 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 | 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 | 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 | 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 | 811 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], |
| @@ -894,20 +814,8 @@ · 2 unmodified lines | ||
| 894 | 814 | |
| 895 | 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 | 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 | 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 | 821 | "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], |
| @@ -918,9 +826,7 @@ · 4 unmodified lines | ||
| 918 | 826 | |
| 919 | 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=="], | |
| 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=="], | |
| 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=="], | |
| 924 | 830 | |
| 925 | 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 @@ · 1 unmodified line | ||
| 928 | 834 | |
| 929 | 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 | 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 | 839 | "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], |
| 940 | 840 | |
| 941 | 841 | "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], |
| @@ -956,14 +856,8 @@ · 14 unmodified lines | ||
| 956 | 856 | |
| 957 | 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 | 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 | 861 | "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], |
| 968 | 862 | |
| 969 | 863 | "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], |
| @@ -980,10 +874,6 @@ · 10 unmodified lines | ||
| 980 | 874 | |
| 981 | 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 | 877 | "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], |
| 988 | 878 | |
| 989 | 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 @@ · 14 unmodified lines | ||
| 1004 | 894 | |
| 1005 | 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=="], | |
| 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=="], | |
| 897 | + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], | |
| 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 | 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 | 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 | 907 | "runtime-required": ["runtime-required@1.1.0", "", {}, "sha512-yX97f5E0WfNpcQnfVjap6vzQcvErkYYCx6eTK4siqGEdC8lglwypUFgZVTX7ShvIlgfkC4XGFl9O1KTYcff0pw=="], |
| 1020 | 908 | |
| 1021 | 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 | 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 | 915 | "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], |
| 1032 | 916 | |
| 1033 | 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 | 919 | "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], |
| 1040 | 920 | |
| 1041 | 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 | 923 | "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], |
| 1052 | 924 | |
| 1053 | 925 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], |
| @@ -1056,16 +928,12 @@ · 2 unmodified lines | ||
| 1056 | 928 | |
| 1057 | 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 | 931 | "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], |
| 1062 | 932 | |
| 1063 | 933 | "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], |
| 1064 | 934 | |
| 1065 | 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 | 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 | 939 | "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], |
| @@ -1082,12 +950,12 @@ · 10 unmodified lines | ||
| 1082 | 950 | |
| 1083 | 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 | 955 | "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], |
| 1086 | 956 | |
| 1087 | 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 | 959 | "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], |
| 1092 | 960 | |
| 1093 | 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 @@ · 24 unmodified lines | ||
| 1118 | 986 | |
| 1119 | 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 | 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 | 993 | "turbo-darwin-64": ["turbo-darwin-64@2.8.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kXRLfcygLOeNcP6JquqRLmGB/ATjjfehiojL2dJkL7GFm3SPSXbq7oNj8UbD8XriYQ5hPaSuz59iF1ijPHkTw=="], |
| @@ -1142,10 +1012,6 @@ · 18 unmodified lines | ||
| 1142 | 1012 | |
| 1143 | 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 | 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 | 1017 | "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], |
| @@ -1158,14 +1024,12 @@ · 6 unmodified lines | ||
| 1158 | 1024 | |
| 1159 | 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 | 1029 | "unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="], |
| 1164 | 1030 | |
| 1165 | 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 | 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 | 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 @@ · 8 unmodified lines | ||
| 1180 | 1044 | |
| 1181 | 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 | 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 | 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 | 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 | 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 | 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 @@ · 7 unmodified lines | ||
| 1206 | 1070 | |
| 1207 | 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 | 1073 | "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], |
| 1220 | 1074 | |
| 1221 | 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 @@ · 8 unmodified lines | ||
| 1230 | 1084 | |
| 1231 | 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 | 1087 | "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], |
| 1236 | 1088 | |
| 1237 | 1089 | "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], |
| @@ -1240,46 +1092,20 @@ · 2 unmodified lines | ||
| 1240 | 1092 | |
| 1241 | 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=="], | |
| 1244 | − | |
| 1245 | − "make-fetch-happen/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=="], | |
| 1246 | 1096 | |
| 1247 | 1097 | "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], |
| 1248 | 1098 | |
| 1249 | 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 | 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 | 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 | 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 | 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 | 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 | 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 @@ · 45 unmodified lines | ||
| 1331 | 1157 | "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], |
| 1332 | 1158 | |
| 1333 | 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 | } |
| @@ -6,13 +6,19 @@ · 5 unmodified lines | ||
| 6 | 6 | "packages/*" |
| 7 | 7 | ], |
| 8 | 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 | 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 | 17 | "typecheck": "turbo run typecheck", |
| 12 | 18 | "lint": "oxlint --report-unused-disable-directives", |
| 13 | 19 | "test": "turbo run test", |
| 20 | + "test:desktop-smoke": "bun run --cwd apps/desktop smoke-test", | |
| 14 | 21 | "fmt": "oxfmt", |
| 15 | − "smoke-test": "turbo run smoke-test", | |
| 16 | 22 | "build:contracts": "bun run --cwd packages/contracts build" |
| 17 | 23 | }, |
| 18 | 24 | "devDependencies": { |
| @@ -4,3 +4,5 @@ export * from "./agent"; · 3 unmodified lines | ||
| 4 | 4 | export * from "./terminal"; |
| 5 | 5 | export * from "./provider"; |
| 6 | 6 | export * from "./model"; |
| 7 | +export * from "./ws"; | |
| 8 | +export * from "./project"; | |
| @@ -9,6 +9,12 @@ import type { · 8 unmodified lines | ||
| 9 | 9 | ProviderStopSessionInput, |
| 10 | 10 | ProviderTurnStartResult, |
| 11 | 11 | } from "./provider"; |
| 12 | +import type { | |
| 13 | + ProjectAddInput, | |
| 14 | + ProjectAddResult, | |
| 15 | + ProjectListResult, | |
| 16 | + ProjectRemoveInput, | |
| 17 | +} from "./project"; | |
| 12 | 18 | import type { TerminalCommandInput, TerminalCommandResult } from "./terminal"; |
| 13 | 19 | import type { NewTodoInput, Todo } from "./todo"; |
| 14 | 20 | |
| @@ -19,28 +25,6 @@ export const EDITORS = [ · 4 unmodified lines | ||
| 19 | 25 | |
| 20 | 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 | 28 | export interface NativeApi { |
| 45 | 29 | todos: { |
| 46 | 30 | list: () => Promise<Todo[]>; |
| @@ -70,6 +54,11 @@ export interface NativeApi { · 23 unmodified lines | ||
| 70 | 54 | listSessions: () => Promise<ProviderSession[]>; |
| 71 | 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 | 62 | shell: { |
| 74 | 63 | openInEditor: (cwd: string, editor: EditorId) => Promise<void>; |
| 75 | 64 | }; |
| @@ -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 | +}); | |
| @@ -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>; | |
| @@ -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>; | |
| @@ -1,12 +1,24 @@ | ||
| 1 | 1 | { |
| 2 | 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 | 15 | "tasks": { |
| 5 | 16 | "build": { |
| 6 | 17 | "dependsOn": ["^build"], |
| 7 | 18 | "outputs": ["dist/**", "dist-electron/**"] |
| 8 | 19 | }, |
| 9 | 20 | "dev": { |
| 21 | + "dependsOn": ["@acme/contracts#build"], | |
| 10 | 22 | "cache": false, |
| 11 | 23 | "persistent": true |
| 12 | 24 | }, |
| @@ -17,11 +29,6 @@ · 4 unmodified lines | ||
| 17 | 29 | "test": { |
| 18 | 30 | "dependsOn": ["^build"], |
| 19 | 31 | "outputs": [] |
| 20 | − }, | |
| 21 | − "smoke-test": { | |
| 22 | − "dependsOn": ["build"], | |
| 23 | − "cache": false, | |
| 24 | − "outputs": [] | |
| 25 | 32 | } |
| 26 | 33 | } |
| 27 | 34 | } |