diff --git a/AGENTS.md b/AGENTS.md index a30b518f..15f2deb8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,6 +74,22 @@ npm test # Run all tests (client + server) npm run check # Typecheck + test without building (safe while prod runs) npm run verify # Build + test (catches type errors that vitest misses) npm run test:coverage # Generate coverage report + +# Run a single test file +npx vitest run test/unit/client/components/TabBar.test.tsx +# Server tests use a separate config +npx vitest run --config vitest.server.config.ts test/server/ws-protocol.test.ts +# Run tests matching a name pattern +npx vitest run -t "handles resize" +``` + +There are **two vitest configs**: `vitest.config.ts` (client, jsdom environment) and `vitest.server.config.ts` (server, node environment). Client tests live in `test/unit/client/`, `test/integration/client/`, and `test/e2e/`. Server tests live in `test/server/`, `test/unit/server/`, and `test/integration/server/`. The default `npm test` runs the client config; `npm run test:all` runs both. + +### Linting +```bash +npm run lint # ESLint on src/ (includes jsx-a11y) +npm run lint:fix # Auto-fix lint issues +npm run typecheck # Typecheck client + server ``` ## Architecture @@ -85,16 +101,23 @@ npm run test:coverage # Generate coverage report ### Directory Structure - `src/` - React frontend application - - `components/` - UI components (TabBar, Sidebar, TerminalView, HistoryView, etc.) - - `store/` - Redux slices (tabs, connection, sessions, settings, claude) - - `lib/` - Utilities (api.ts, claude-types.ts) + - `components/` - UI components (TabBar, Sidebar, TerminalView, panes/, claude-chat/, context-menu/, etc.) + - `store/` - Redux slices, middleware, selectors, and persistence + - `hooks/` - Custom React hooks (activity monitor, notifications, mobile, theme) + - `lib/` - Client utilities (ws-client.ts, api.ts, pane-utils.ts, terminal helpers) - `server/` - Node.js/Express backend - - `index.ts` - HTTP/REST routes and server entry + - `*-router.ts` - Express route modules (terminals, sessions, settings, ai, debug, files, etc.) - `ws-handler.ts` - WebSocket protocol handler - `terminal-registry.ts` - PTY lifecycle management - - `claude-session.ts` - Claude session discovery & indexing - - `claude-indexer.ts` - File watcher for ~/.claude directory -- `test/` - Test suites organized by unit/integration and client/server + - `coding-cli/` - Coding CLI provider abstraction (Claude, Codex) with session indexer and manager + - `session-scanner/` - File watcher service for session discovery and caching + - `sessions-sync/` - Delta sync for session updates to clients + - `tabs-registry/` - Persistent tab/pane state store + - `updater/` - Auto-update version checking +- `shared/` - Code shared between client and server + - `ws-protocol.ts` - Single source of truth for WebSocket message schemas (Zod) + - `path-utils.ts` - Cross-platform path utilities +- `test/` - Tests organized by scope: `unit/client/`, `unit/server/`, `integration/server/`, `integration/client/`, `server/`, `e2e/` ### Key Architectural Patterns @@ -146,3 +169,10 @@ All components **must** be accessible for browser-use automation and WCAG compli - `@/` → `src/` - `@test/` → `test/` +- `@shared/` → `shared/` + +## TypeScript Configs + +Two separate configs reflect the client/server split: +- `tsconfig.json` — Client (Bundler resolution, ESNext modules, jsx). Includes `src/` and `shared/`. +- `tsconfig.server.json` — Server (NodeNext resolution, ESM). Server imports must use `.js` extensions. diff --git a/CLAUDE.md b/CLAUDE.md index eef4bd20..078c29c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,5 @@ -@AGENTS.md \ No newline at end of file +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +@AGENTS.md diff --git a/package-lock.json b/package-lock.json index 7564b916..f67db5e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "freshell", - "version": "0.5.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "freshell", - "version": "0.5.0", + "version": "0.6.0", "dependencies": { "@ai-sdk/google": "^3.0.29", "@anthropic-ai/claude-agent-sdk": "^0.2.40", diff --git a/server/terminal-registry.ts b/server/terminal-registry.ts index 323aad1f..62eb0b7b 100644 --- a/server/terminal-registry.ts +++ b/server/terminal-registry.ts @@ -630,6 +630,8 @@ export function buildSpawnSpec( // Strip inherited env vars that interfere with child terminal behaviour: // - CLAUDECODE: causes child Claude processes to refuse to start ("nested session" error) // - CI/NO_COLOR/FORCE_COLOR/COLOR: disables interactive color in user PTYs + // - PORT/VITE_PORT/AUTH_TOKEN/ALLOWED_ORIGINS: server-specific vars that cause + // port conflicts and leak credentials into child processes // - NODE_ENV/npm_lifecycle_script: server's production env leaks into child shells, // breaking tools like React test-utils that check NODE_ENV const { @@ -638,6 +640,10 @@ export function buildSpawnSpec( NO_COLOR: _noColor, FORCE_COLOR: _forceColor, COLOR: _color, + PORT: _port, + VITE_PORT: _vitePort, + AUTH_TOKEN: _authToken, + ALLOWED_ORIGINS: _allowedOrigins, NODE_ENV: _nodeEnv, npm_lifecycle_script: _npmLifecycleScript, ...parentEnv diff --git a/test/unit/server/terminal-env.test.ts b/test/unit/server/terminal-env.test.ts index 68ea0cfd..a34e8c8e 100644 --- a/test/unit/server/terminal-env.test.ts +++ b/test/unit/server/terminal-env.test.ts @@ -59,4 +59,26 @@ describe('TerminalRegistry env injection', () => { expect(env.FRESHELL_TAB_ID).toBe('tab_x') expect(env.FRESHELL_PANE_ID).toBe('pane_y') }) + + it('strips server-specific env vars from child PTY processes', () => { + process.env.PORT = '3001' + process.env.VITE_PORT = '5173' + process.env.AUTH_TOKEN = 'secret-token' + process.env.ALLOWED_ORIGINS = 'http://localhost:3001' + // Sanity: keep a non-stripped var to confirm env is still passed + process.env.HOME = '/home/test' + + const registry = new TerminalRegistry() + registry.create({ mode: 'shell', envContext: { tabId: 'tab_1', paneId: 'pane_1' } }) + + const call = vi.mocked(pty.spawn).mock.calls[0] + const env = call[2].env as Record + + expect(env.PORT).toBeUndefined() + expect(env.VITE_PORT).toBeUndefined() + expect(env.AUTH_TOKEN).toBeUndefined() + expect(env.ALLOWED_ORIGINS).toBeUndefined() + // Non-stripped vars still pass through + expect(env.HOME).toBe('/home/test') + }) })