diff --git a/web/CLAUDE.md b/web/CLAUDE.md index 6223e4b6..0f0e66ae 100644 --- a/web/CLAUDE.md +++ b/web/CLAUDE.md @@ -1,5 +1,29 @@ # Claude Development Notes +After receiving the first user mesage, read spec.md before you proceed. The spec.md contains a map of this code base that should help you navigate it. + +**IMPORTANT**: NEVER USE GREP. ALWAYS USE RIPGREP! + +## Updating spec.md +As code changes, the spec.md might get outdated. If you detect outdated information, ask the user if they want to regenerate the spec.md file. + +### How to regenerate spec.md: +1. Create a todo list to track the analysis tasks +2. Use multiple parallel Task tool calls to analyze: + - Server architecture (src/server/, authentication, session management) + - Client architecture (src/client/, components, services) + - fwd.ts application functionality + - API endpoints and protocols + - Binary buffer format and WebSocket implementation + - HQ mode and distributed architecture +3. Focus on capturing: + - File locations with key line numbers for important functions + - Component responsibilities and data flow + - Protocol specifications and message formats + - Configuration options and CLI arguments +4. Write a concise spec.md that serves as a navigation map, keeping descriptions brief to minimize token usage +5. Include a "Key Files Quick Reference" section for fast lookup + ## Build Process - **Never run build commands** - the user has `npm run dev` running which handles automatic rebuilds - Changes to TypeScript files are automatically compiled and watched @@ -7,15 +31,12 @@ ## Development Workflow - Make changes to source files in `src/` -- The dev server automatically rebuilds and reloads -- Focus on editing source files, not built artifacts +- Format, lint and typecheck after you made changes. + - `npm run format` + - `npm run lint` + - `npm run lint:fix` + - `npm run typecheck` +- Always fix all linting and type checking errors. ## Server Execution -- NEVER RUN THE SERVER YOURSELF, I ALWAYS RUN IT ON THE SIDE VIA NPM RUN DEV! - -## Code Quality -- ESLint and Prettier are configured for the project -- Run `npm run lint` to check for linting issues -- Run `npm run lint:fix` to automatically fix most issues -- Run `npm run format` to format all code with Prettier -- Run `npm run format:check` to check formatting without changing files \ No newline at end of file +- NEVER RUN THE SERVER YOURSELF, I ALWAYS RUN IT ON THE SIDE VIA NPM RUN DEV! \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index b69460f3..bc0ed42f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0", "@xterm/headless": "^5.5.0", + "chalk": "^5.4.1", "express": "^4.19.2", "lit": "^3.3.0", "signal-exit": "^4.1.0", @@ -1507,6 +1508,23 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@jest/core": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.2.tgz", @@ -1568,6 +1586,39 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/@jest/core/node_modules/pretty-format": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", @@ -1751,6 +1802,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@jest/schemas": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", @@ -1780,6 +1848,23 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/snapshot-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@jest/source-map": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", @@ -1854,6 +1939,23 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@jest/types": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz", @@ -1873,6 +1975,23 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -2529,6 +2648,23 @@ "node": ">=18" } }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -3887,6 +4023,23 @@ "@babel/core": "^7.11.0" } }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/babel-plugin-istanbul": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", @@ -4438,17 +4591,12 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -4884,6 +5032,36 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/concurrently/node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -5743,6 +5921,23 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -7193,6 +7388,23 @@ "concat-map": "0.0.1" } }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jake/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -7293,6 +7505,39 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-circus/node_modules/pretty-format": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", @@ -7348,6 +7593,23 @@ } } }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jest-cli/node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -7520,6 +7782,39 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-config/node_modules/pretty-format": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", @@ -7571,6 +7866,39 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-diff/node_modules/pretty-format": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", @@ -7636,6 +7964,39 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-each/node_modules/pretty-format": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", @@ -7780,6 +8141,39 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", @@ -7836,6 +8230,39 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-message-util/node_modules/pretty-format": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", @@ -7935,6 +8362,23 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jest-runner": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.2.tgz", @@ -7969,6 +8413,23 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jest-runtime": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.2.tgz", @@ -8003,6 +8464,23 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jest-snapshot": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.2.tgz", @@ -8049,6 +8527,39 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-snapshot/node_modules/pretty-format": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", @@ -8089,6 +8600,23 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jest-util/node_modules/picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", @@ -8146,6 +8674,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-validate/node_modules/pretty-format": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", @@ -8188,6 +8749,23 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jest-worker": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz", @@ -8394,19 +8972,6 @@ "url": "https://opencollective.com/lint-staged" } }, - "node_modules/lint-staged/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/listr2": { "version": "8.3.3", "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", diff --git a/web/package.json b/web/package.json index 10429349..71c23f10 100644 --- a/web/package.json +++ b/web/package.json @@ -14,7 +14,7 @@ "ensure:dirs": "node scripts/ensure-dirs.js", "bundle": "npm run clean && npm run bundle:assets && npm run bundle:css && npm run bundle:client && npm run bundle:test", "bundle:assets": "node scripts/copy-assets.js", - "bundle:css": "npm run ensure:dirs && npx tailwindcss -i ./src/input.css -o ./public/bundle/output.css --minify", + "bundle:css": "npm run ensure:dirs && npx tailwindcss -i ./src/client/styles.css -o ./public/bundle/styles.css --minify", "bundle:client": "npm run ensure:dirs && esbuild src/client/app-entry.ts --bundle --outfile=public/bundle/client-bundle.js --format=esm --sourcemap", "bundle:test": "npm run ensure:dirs && esbuild src/client/test-terminals-entry.ts --bundle --outfile=public/bundle/terminal.js --format=esm --sourcemap", "start": "node dist/server.js", @@ -27,16 +27,15 @@ "test:ui": "vitest --ui", "test:run": "vitest run", "test:coverage": "vitest run --coverage", - "test:unit": "vitest run src/test/unit", - "test:integration": "vitest run --config vitest.integration.config.ts", - "test:critical": "vitest run src/test/critical.test.ts src/test/smoke.test.ts", - "test:all": "npm run test:unit && npm run test:integration && npm run test:critical" + "test:e2e": "vitest run --config vitest.config.e2e.ts", + "test:all": "npm run test:run" }, "dependencies": { + "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0", "@xterm/headless": "^5.5.0", + "chalk": "^5.4.1", "express": "^4.19.2", "lit": "^3.3.0", - "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0", "signal-exit": "^4.1.0", "ws": "^8.18.2" }, diff --git a/web/server-spec.md b/web/server-spec.md deleted file mode 100644 index b742d244..00000000 --- a/web/server-spec.md +++ /dev/null @@ -1,566 +0,0 @@ -# VibeTunnel Server Specification - -This document provides a comprehensive specification of the VibeTunnel server architecture, including PTY management, terminal state management, distributed HQ mode, and all protocols. This specification is designed to enable implementation in any programming language. - -## Table of Contents - -1. [Overview](#overview) -2. [Server Modes](#server-modes) -3. [Authentication](#authentication) -4. [Session Management](#session-management) -5. [Terminal Management](#terminal-management) -6. [Binary Buffer Protocol](#binary-buffer-protocol) -7. [Stream Format](#stream-format) -8. [API Endpoints](#api-endpoints) -9. [WebSocket Protocols](#websocket-protocols) -10. [HQ Mode Architecture](#hq-mode-architecture) -11. [File System Structure](#file-system-structure) - -## Overview - -VibeTunnel is a terminal session management server that provides: -- Remote terminal session creation and management -- Real-time terminal output streaming -- Session persistence and replay -- Distributed architecture with HQ mode for managing multiple servers -- Binary-optimized terminal buffer synchronization - -## Server Modes - -The server can operate in three modes: - -### 1. Normal Mode (Default) -- Standalone server managing local terminal sessions -- Optional Basic Authentication -- No connection to other servers - -### 2. HQ Mode (`--hq` flag) -- Acts as a headquarters server managing multiple remote servers -- Aggregates sessions from all registered remotes -- Proxies API requests to appropriate remote servers -- Maintains health checks on all remotes - -### 3. Remote Mode (`--hq-url` flag) -- Registers with an HQ server -- Accepts both Basic Auth and Bearer token authentication -- Operates independently if HQ is unavailable -- Provides all normal mode functionality - -## Authentication - -### Configuration - -#### Environment Variables -- `VIBETUNNEL_USERNAME` - Username for Basic Authentication -- `VIBETUNNEL_PASSWORD` - Password for Basic Authentication -- Both must be provided together or neither - -#### Command Line Arguments -- `--username` - Local server username (overrides env var) -- `--password` - Local server password (overrides env var) -- `--hq-url` - URL of HQ server to register with (enables remote mode) -- `--hq-username` - Username for authenticating with HQ -- `--hq-password` - Password for authenticating with HQ -- `--name` - Unique name for this remote server (required with --hq-url) - -### Authentication Flow - -#### Basic Authentication -- Standard HTTP Basic Auth: `Authorization: Basic base64(username:password)` -- Used by clients to authenticate with any server -- Used by remote servers to authenticate with HQ during registration - -#### Bearer Token Authentication -- Format: `Authorization: Bearer ` -- Remote servers generate a unique token (UUID v4) during registration -- HQ uses this token for all API calls to the remote -- Remote servers accept both Basic Auth and Bearer token - -### Authentication Middleware -1. Skip auth if not configured (no username/password and not in remote mode) -2. Skip auth for WebSocket upgrade requests (handled separately) -3. Check Bearer token first if server is in remote mode -4. Fall back to Basic Auth check -5. Return 401 Unauthorized with `WWW-Authenticate: Basic realm="VibeTunnel"` if failed - -## Session Management - -### Session States -- `starting` - Session is being created -- `running` - Session is active -- `exited` - Session has terminated - -### Session Data Structure -```typescript -interface Session { - id: string; // UUID v4 - name: string; // User-friendly name - command: string; // Command line as string - workingDir: string; // Working directory path - status: string; // Session state - exitCode?: number; // Exit code if exited - startedAt: string; // ISO 8601 timestamp - lastModified: string; // ISO 8601 timestamp - pid?: number; // Process ID - waiting?: boolean; // If waiting for input - remoteName?: string; // Name of remote server (HQ mode) -} -``` - -### PTY Service - -The PTY service manages the actual terminal processes using `node-pty`: - -```typescript -interface PtyConfig { - implementation: 'node-pty'; - controlPath: string; // Base directory for session data -} -``` - -Key responsibilities: -- Create PTY sessions with specified dimensions -- Manage session lifecycle (create, kill, cleanup) -- Handle input/output to/from PTY -- Resize terminal dimensions -- Track session metadata - -## Terminal Management - -The Terminal Manager maintains server-side terminal state for efficient buffer synchronization: - -### Terminal State -```typescript -interface TerminalState { - cols: number; // Terminal width - rows: number; // Terminal height - buffer: string[][]; // 2D array of [char, style] pairs - cursor: { - x: number; - y: number; - visible: boolean; - }; - scrollback: string[][]; // Historical lines - title: string; // Terminal title - applicationKeypad: boolean; - applicationCursor: boolean; - bracketedPasteMode: boolean; - origin: boolean; - reverseWraparound: boolean; - wraparound: boolean; - insertMode: boolean; -} -``` - -### Buffer Management -1. Parse ANSI escape sequences from PTY output -2. Update terminal state based on control sequences -3. Maintain accurate buffer representation -4. Support terminal operations: - - Cursor movement - - Text insertion/deletion - - Screen clearing - - Scrolling - - Style changes - -## Binary Buffer Protocol - -The binary buffer protocol provides efficient terminal state synchronization: - -### Snapshot Encoding -``` -[4 bytes: magic "SNAP"] -[4 bytes: version (1)] -[4 bytes: cols] -[4 bytes: rows] -[4 bytes: cursor X] -[4 bytes: cursor Y] -[1 byte: cursor visible] -[4 bytes: scrollback length] -[scrollback data...] -[4 bytes: buffer length] -[buffer data...] -[4 bytes: title length] -[title UTF-8 bytes] -[1 byte: flags] - - bit 0: applicationKeypad - - bit 1: applicationCursor - - bit 2: bracketedPasteMode - - bit 3: origin - - bit 4: reverseWraparound - - bit 5: wraparound - - bit 6: insertMode -``` - -### Line Encoding -Each line is encoded as: -``` -[4 bytes: line length] -[4 bytes: number of cells] -[cell data...] -``` - -### Cell Encoding -Each cell is encoded as: -``` -[4 bytes: character UTF-8 length] -[character UTF-8 bytes] -[4 bytes: style] -``` - -Style is a 32-bit integer: -- Bits 0-7: Foreground color (256 colors) -- Bits 8-15: Background color (256 colors) -- Bit 16: Bold -- Bit 17: Italic -- Bit 18: Underline -- Bit 19: Blink -- Bit 20: Inverse -- Bit 21: Hidden -- Bit 22: Strikethrough - -## Stream Format - -Session output is stored in asciicast v2 format: - -### Header -```json -{ - "version": 2, - "width": 80, - "height": 24, - "timestamp": 1234567890, - "env": {"TERM": "xterm-256color"} -} -``` - -### Events -Each subsequent line is an event: -```json -[timestamp, type, data] -``` - -Types: -- `"o"` - Output data (UTF-8 string) -- `"i"` - Input data (UTF-8 string) -- `"r"` - Resize event (e.g., "80x24") - -## API Endpoints - -### Health Check -``` -GET /api/health -Response: {"status": "ok", "timestamp": "2024-01-01T00:00:00.000Z"} -``` - -### Session Management - -#### List Sessions -``` -GET /api/sessions -Response: Session[] -``` - -In HQ mode, aggregates sessions from all registered remotes. - -#### Create Session -``` -POST /api/sessions -Body: { - "command": ["bash", "-l"], - "workingDir": "/home/user", - "name": "My Session", - "remoteId": "remote-uuid" // Optional, HQ mode only -} -Response: {"sessionId": "uuid"} -``` - -#### Get Session Info -``` -GET /api/sessions/:sessionId -Response: Session -``` - -#### Kill Session -``` -DELETE /api/sessions/:sessionId -Response: {"success": true, "message": "Session killed"} -``` - -#### Cleanup Session -``` -DELETE /api/sessions/:sessionId/cleanup -Response: {"success": true, "message": "Session cleaned up"} -``` - -#### Cleanup All Exited -``` -POST /api/cleanup-exited -Response: { - "success": true, - "message": "N exited sessions cleaned up across all servers", - "localCleaned": 5, - "remoteResults": [ - {"remoteName": "server1", "cleaned": 3}, - {"remoteName": "server2", "cleaned": 2, "error": "timeout"} - ] -} -``` - -### Terminal I/O - -#### Stream Session Output (SSE) -``` -GET /api/sessions/:sessionId/stream -Response: Server-Sent Events stream -``` - -Event format: -``` -event: output -data: {"data": "terminal output...", "timestamp": 1234567890} - -event: exit -data: {"exitCode": 0} -``` - -#### Get Session Snapshot -``` -GET /api/sessions/:sessionId/snapshot -Response: Optimized asciicast v2 format (text/plain) -``` - -Returns events after the last clear screen command. - -#### Get Buffer Stats -``` -GET /api/sessions/:sessionId/buffer/stats -Response: { - "lines": 100, - "cells": 8000, - "scrollbackLines": 500, - "lastModified": "2024-01-01T00:00:00.000Z" -} -``` - -#### Get Buffer -``` -GET /api/sessions/:sessionId/buffer?format=binary -Response: Binary encoded buffer (application/octet-stream) - -GET /api/sessions/:sessionId/buffer?format=json -Response: JSON representation of terminal state -``` - -#### Send Input -``` -POST /api/sessions/:sessionId/input -Body: {"text": "ls -la\n"} -Response: {"success": true} -``` - -Special keys: -- `"arrow_up"`, `"arrow_down"`, `"arrow_left"`, `"arrow_right"` -- `"escape"`, `"enter"`, `"ctrl_enter"`, `"shift_enter"` - -#### Resize Terminal -``` -POST /api/sessions/:sessionId/resize -Body: {"cols": 120, "rows": 40} -Response: {"success": true, "cols": 120, "rows": 40} -``` - -### File System - -#### Browse Directory -``` -GET /api/fs/browse?path=/home/user -Response: { - "absolutePath": "/home/user", - "files": [ - { - "name": "document.txt", - "created": "2024-01-01T00:00:00.000Z", - "lastModified": "2024-01-01T00:00:00.000Z", - "size": 1024, - "isDir": false - } - ] -} -``` - -#### Create Directory -``` -POST /api/mkdir -Body: {"path": "/home/user", "name": "newfolder"} -Response: { - "success": true, - "path": "/home/user/newfolder", - "message": "Directory 'newfolder' created successfully" -} -``` - -### HQ Mode Endpoints - -#### Register Remote -``` -POST /api/remotes/register -Headers: Authorization: Basic -Body: { - "id": "remote-uuid", - "name": "unique-remote-name", - "url": "http://remote-server:4020", - "token": "bearer-token-uuid" -} -Response: { - "success": true, - "remote": {"id": "remote-uuid", "name": "unique-remote-name"} -} -``` - -#### Unregister Remote -``` -DELETE /api/remotes/:remoteId -Headers: Authorization: Basic -Response: {"success": true} -``` - -#### List Remotes -``` -GET /api/remotes -Response: [ - { - "id": "remote-uuid", - "name": "unique-remote-name", - "url": "http://remote-server:4020", - "sessionCount": 5, - "lastHeartbeat": "2024-01-01T00:00:00.000Z" - } -] -``` - -## WebSocket Protocols - -### Buffer Synchronization WebSocket - -Endpoint: `/buffers` - -#### Client → Server Messages - -Subscribe to session: -```json -{"type": "subscribe", "sessionId": "session-uuid"} -``` - -Unsubscribe from session: -```json -{"type": "unsubscribe", "sessionId": "session-uuid"} -``` - -Heartbeat response: -```json -{"type": "pong"} -``` - -#### Server → Client Messages - -Heartbeat: -```json -{"type": "ping"} -``` - -Error: -```json -{"type": "error", "message": "Error description"} -``` - -Binary buffer update: -``` -[1 byte: 0xBF magic byte] -[4 bytes: session ID length (little-endian)] -[N bytes: session ID UTF-8] -[M bytes: encoded buffer snapshot] -``` - -## HQ Mode Architecture - -### Remote Registration -1. Remote server starts with `--hq-url`, `--hq-username`, `--hq-password`, `--name` -2. Remote generates unique ID (UUID v4) and token (UUID v4) -3. Remote sends POST to `/api/remotes/register` with Basic Auth -4. HQ validates unique name and stores remote info -5. Remote registration is complete - -### Health Checking -1. HQ checks each remote every 15 seconds -2. First tries GET `/api/health` with Bearer token -3. Falls back to GET `/api/sessions` if health endpoint not found -4. Updates session tracking from sessions response -5. Removes remote if health check fails - -### Request Proxying -1. Session proxy middleware intercepts requests with session IDs -2. Looks up which remote owns the session -3. Forwards request to remote with Bearer token auth -4. Returns remote's response to client - -### Session Aggregation -1. GET `/api/sessions` in HQ mode fetches from all remotes -2. Adds `remoteName` field to each session -3. Tracks session ownership for future proxying -4. Returns combined list sorted by last modified - -## File System Structure - -``` -~/.vibetunnel/control/ -├── {session-id}/ -│ ├── info.json # Session metadata -│ ├── stream-out # Asciicast v2 format output -│ └── stream-in # Input log (optional) -``` - -### info.json Structure -```json -{ - "version": 1, - "session_id": "uuid", - "name": "Session Name", - "cmdline": ["bash", "-l"], - "cwd": "/home/user", - "env": {}, - "term": "xterm-256color", - "width": 80, - "height": 24, - "started_at": "2024-01-01T00:00:00.000Z", - "pid": 12345, - "status": "running", - "exit_code": null -} -``` - -## Implementation Notes - -### Error Handling -- All endpoints should return appropriate HTTP status codes -- Error responses should include `{"error": "Description"}` -- WebSocket errors should send error message before closing - -### Security Considerations -- HTTPS required for HQ URL -- Tokens should be cryptographically random (UUID v4) -- File system access restricted to home directory and temp -- Input validation on all user-provided paths - -### Performance Considerations -- Stream files are append-only for efficiency -- Binary buffer protocol minimizes data transfer -- Health checks have 5-second timeout -- Proxy requests have 30-second timeout -- Buffer updates are debounced to avoid flooding - -### Compatibility -- UTF-8 encoding throughout -- Little-endian byte order for binary protocol -- ISO 8601 timestamps in UTC -- Line endings normalized to LF (\n) \ No newline at end of file diff --git a/web/snapshot-format.md b/web/snapshot-format.md deleted file mode 100644 index d2140728..00000000 --- a/web/snapshot-format.md +++ /dev/null @@ -1,175 +0,0 @@ -# VibeTunnel Terminal Buffer Snapshot Format - -This document describes the binary format used for efficient terminal buffer snapshots. - -## Overview - -The snapshot format is a compact binary representation of terminal buffer state, designed to minimize data transfer while preserving all terminal attributes. It consists of a header followed by a stream of encoded cells. - -## Format Structure - -``` -┌──────────────┬─────────────────────────────────┐ -│ Header │ Cell Stream │ -│ (32 bytes) │ (variable, 4+ bytes/cell) │ -└──────────────┴─────────────────────────────────┘ -``` - -## Header Format (32 bytes) - Version 2 - -``` -Offset Size Field Description ------- ---- ---------- ----------- -0x00 2 Magic 0x5654 ("VT" in ASCII) -0x02 1 Version Format version (0x02 for 32-bit support) -0x03 1 Flags Reserved for future use -0x04 4 Cols Terminal width (32-bit unsigned, little-endian) -0x08 4 Rows Number of rows in this snapshot (32-bit unsigned, little-endian) -0x0C 4 ViewportY Starting line number in buffer (32-bit signed, little-endian) -0x10 4 CursorX Cursor column position (32-bit signed, little-endian) -0x14 4 CursorY Cursor row position relative to viewport (32-bit signed, little-endian) -0x18 4 Reserved Reserved for future use -``` - -Note: CursorY is relative to the viewport and can be negative if the cursor is above the visible area. - -## Cell Format - -Each cell uses a variable-length encoding: - -### Basic Cell (4 bytes) - ASCII with palette colors - -``` -Offset Size Field Description ------- ---- ---------- ----------- -0x00 1 Character UTF-8 character (ASCII range) -0x01 1 Attributes Bit flags (see below) -0x02 1 FG Color Foreground palette index (0-255) -0x03 1 BG Color Background palette index (0-255) -``` - -### Extended Cell (variable) - Unicode or RGB colors - -``` -Offset Size Field Description ------- ---- ---------- ----------- -0x00 1 Header [2 bits: char_len-1][1 bit: rgb_fg][1 bit: rgb_bg][4 bits: reserved] -0x01 1 Attributes Bit flags (see below) -0x02 1-4 Character UTF-8 character (length from header) -0x?? 1/3 FG Color 1 byte palette or 3 bytes RGB -0x?? 1/3 BG Color 1 byte palette or 3 bytes RGB -``` - -### Attribute Flags (1 byte) - -``` -Bit Flag ---- ---- -0 Bold -1 Italic -2 Underline -3 Dim -4 Inverse -5 Invisible -6 Strikethrough -7 Extended (if set, use extended cell format) -``` - -## Special Encodings - -### Run-Length Encoding - -For repeated cells (common with spaces), use RLE: - -``` -0xFF -``` - -This encodes up to 255 repeated cells. - -### Empty Line Marker - -For completely empty lines (all spaces with default attributes): - -``` -0xFE -``` - -This encodes up to 255 empty lines. - -## Color Encoding - -### Palette Colors (0-255) -Standard xterm 256-color palette indices. - -### RGB Colors (24-bit) -When RGB flag is set in extended cell header: -``` -R (1 byte) G (1 byte) B (1 byte) -``` - -## API Endpoint - -### Request -``` -GET /api/sessions/{sessionId}/buffer?viewportY={Y}&lines={N} -``` - -Parameters: -- `sessionId`: Session identifier -- `viewportY`: Starting line in the terminal buffer (0-based) -- `lines`: Number of lines to return - -### Response -``` -Content-Type: application/octet-stream -Content-Length: {size} - -[Binary data as described above] -``` - -## Example - -For a 80x24 terminal showing "Hello" on black background: - -``` -Header (32 bytes): -56 54 02 00 50 00 00 00 18 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 -│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─ Reserved -│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─ CursorY (0) -│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─ CursorX (5) -│ │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─ ViewportY (0) -│ │ │ │ │ │ │ │ └─┴─┴─┴─ Rows (24) -│ │ │ │ └─┴─┴─┴─ Cols (80) -│ │ │ └─ Flags (0) -│ │ └─ Version (2) -└─┴─ Magic "VT" - -Cells: -48 00 07 00 65 00 07 00 6C 00 07 00 6C 00 07 00 6F 00 07 00 -│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ BG: black (0) -│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ FG: white (7) -│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ Attributes: none (0) -│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ Character: 'o' -[Continue for remaining cells...] - -FF 4B 20 00 07 00 (RLE: 75 spaces) -FE 17 (23 empty lines) -``` - -## Implementation Notes - -1. The server maintains xterm.js Terminal instances for each active session -2. Binary encoding happens on-the-fly when buffer endpoint is called -3. Client can request specific viewport regions for efficient updates -4. Format is designed to be easily parseable with minimal overhead -5. Extended format allows for future enhancements without breaking compatibility - -## Performance Characteristics - -- Basic ASCII terminal: ~4 bytes per cell -- With colors/attributes: ~4-7 bytes per cell -- With RLE compression: ~10-20% of original size for typical terminals -- Network transfer: ~3-8KB for full 80x24 screen (before gzip) \ No newline at end of file diff --git a/web/spec.md b/web/spec.md new file mode 100644 index 00000000..8295b260 --- /dev/null +++ b/web/spec.md @@ -0,0 +1,224 @@ +# VibeTunnel Codebase Map + +A comprehensive navigation guide for the VibeTunnel web terminal system. + +## Project Overview + +VibeTunnel is a web-based terminal multiplexer with distributed architecture support. It provides: +- PTY-based terminal sessions via node-pty +- Real-time terminal streaming (SSE/WebSocket) +- Binary-optimized buffer synchronization +- Distributed HQ/remote server architecture +- Web UI with full terminal emulation + +## Directory Structure + +``` +web/ +├── src/ +│ ├── server/ # Node.js Express server +│ ├── client/ # Lit-based web UI +│ ├── fwd.ts # CLI forwarding tool +│ └── server.ts # Server entry point +├── public/ # Static assets +├── scripts/ # Build scripts +└── docs/ # Documentation +``` + +## Server Architecture (`src/server/`) + +### Core Components + +#### Entry Points +- `server.ts` (1-4): Simple loader for modular server +- `index.ts` (1-49): Server initialization, cleanup intervals, graceful shutdown +- `app.ts` (198-404): Express app factory, CLI parsing, mode configuration + +#### Server Modes +1. **Normal Mode**: Standalone terminal server +2. **HQ Mode** (`--hq`): Central server managing remotes +3. **Remote Mode** (`--hq-url`): Registers with HQ server + +### Authentication (`middleware/auth.ts`) +- Basic Auth: Username/password (46-55) +- Bearer Token: HQ↔Remote communication (28-44) +- Health endpoint bypass (14-16) + +### Session Management + +#### PTY Manager (`pty/pty-manager.ts`) +- `createSession()` (72-167): Spawns PTY processes +- `sendInput()` (265-315): Handles keyboard input +- `killSession()` (453-537): SIGTERM→SIGKILL escalation +- `resizeSession()` (394-447): Terminal dimension changes +- Control pipe support for external sessions (320-349) + +#### Session Manager (`pty/session-manager.ts`) +- Session persistence in `~/.vibetunnel/control/` +- `listSessions()` (155-224): Filesystem-based discovery +- `updateZombieSessions()` (231-257): Dead process cleanup + +#### Terminal Manager (`services/terminal-manager.ts`) +- Headless xterm.js for server-side state (40-69) +- `getBufferSnapshot()` (255-323): Captures terminal buffer +- `encodeSnapshot()` (328-555): Binary protocol encoding +- Debounced buffer notifications (627-642) + +### API Routes (`routes/`) + +#### Sessions (`sessions.ts`) +- `GET /api/sessions` (40-120): List with HQ aggregation +- `POST /api/sessions` (123-199): Create local/remote +- `DELETE /api/sessions/:id` (270-323): Kill session +- `GET /api/sessions/:id/stream` (517-627): SSE streaming +- `POST /api/sessions/:id/input` (630-695): Send input +- `POST /api/sessions/:id/resize` (698-767): Resize terminal +- `GET /api/sessions/:id/buffer` (455-514): Binary snapshot + +#### Remotes (`remotes.ts`) - HQ Mode Only +- `GET /api/remotes` (15-27): List registered servers +- `POST /api/remotes/register` (30-52): Remote registration +- `DELETE /api/remotes/:id` (55-69): Unregister remote + +### Binary Buffer Protocol + +#### Format (`terminal-manager.ts:361-555`) +``` +Header (32 bytes): +- Magic: 0x5654 "VT" (2 bytes) +- Version: 0x01 (1 byte) +- Flags: reserved (1 byte) +- Dimensions: cols, rows (8 bytes) +- Cursor: X, Y, viewport (12 bytes) +- Reserved (4 bytes) + +Rows: 0xFE=empty, 0xFD=content +Cells: Variable-length with type byte +``` + +### WebSocket (`services/buffer-aggregator.ts`) +- Client connections (31-68) +- Message handling (69-127) +- Local session buffers (131-195) +- Remote session proxy (200-232) +- Binary message format (136-164) + +### HQ Mode Components + +#### Remote Registry (`services/remote-registry.ts`) +- Health checks every 15s (118-146) +- Session ownership tracking (82-96) +- Bearer token authentication + +#### HQ Client (`services/hq-client.ts`) +- Registration with HQ (29-58) +- Unregister on shutdown (60-72) + +## Client Architecture (`src/client/`) + +### Core Components + +#### App Entry (`app.ts`) +- Lit-based SPA (15-331) +- Session list polling (74-90) +- URL-based routing `?session=` +- Global keyboard handlers + +#### Terminal Component (`terminal.ts`) +- Custom DOM rendering (634-701) +- Virtual scrolling (537-555) +- Touch/momentum support +- URL highlighting integration +- Copy/paste handling + +#### Session View (`session-view.ts`) +- Full-screen terminal (12-1331) +- SSE streaming (275-333) +- Mobile input overlays +- Resize synchronization + +### Services + +#### Buffer Subscription (`services/buffer-subscription-service.ts`) +- WebSocket client (30-87) +- Binary protocol decoder (163-208) +- Auto-reconnection with backoff + +### Utils + +#### Cast Converter (`utils/cast-converter.ts`) +- Asciinema v2 parser (31-82) +- SSE stream handler (294-427) +- Batch loading (221-283) + +#### Terminal Renderer (`utils/terminal-renderer.ts`) +- Binary buffer decoder (279-424) +- HTML generation +- Style mapping + +## Forward Tool (`src/fwd.ts`) + +### Purpose +CLI tool that spawns PTY sessions integrated with VibeTunnel infrastructure. + +### Key Features +- Interactive terminal forwarding (295-312) +- Monitor-only mode (`--monitor-only`) +- Control pipe handling (140-287) +- Session persistence (439-446) + +### Usage +```bash +npx tsx src/fwd.ts [args...] +npx tsx src/fwd.ts --monitor-only +``` + +### Integration Points +- Uses same PtyManager as server (63) +- Creates sessions in control directory +- Supports resize/kill via control pipe + +## Key Files Quick Reference + +### Server Core +- `src/server/app.ts`: App configuration, CLI parsing +- `src/server/middleware/auth.ts`: Authentication logic +- `src/server/routes/sessions.ts`: Session API endpoints +- `src/server/pty/pty-manager.ts`: PTY process management +- `src/server/services/terminal-manager.ts`: Terminal state & binary protocol +- `src/server/services/buffer-aggregator.ts`: WebSocket buffer distribution + +### Client Core +- `src/client/app.ts`: Main SPA component +- `src/client/components/terminal.ts`: Terminal renderer +- `src/client/components/session-view.ts`: Session viewer +- `src/client/services/buffer-subscription-service.ts`: WebSocket client + +### Configuration +- Environment: `PORT`, `VIBETUNNEL_USERNAME`, `VIBETUNNEL_PASSWORD` +- CLI: `--port`, `--username`, `--password`, `--hq`, `--hq-url`, `--name` + +### Protocols +- REST API: Session CRUD, terminal I/O +- SSE: Real-time output streaming +- WebSocket: Binary buffer updates +- Control pipes: External session control + +## Development Notes + +### Build System +- `npm run dev`: Auto-rebuilds TypeScript +- ESBuild: Fast bundling +- Vitest: Testing framework + +### Testing +- Unit tests: `npm test` +- E2E tests: `npm run test:e2e` +- Integration: `npm run test:integration` + +### Key Dependencies +- node-pty: Cross-platform PTY +- @xterm/headless: Terminal emulation +- Lit: Web components +- Express: HTTP server +- TailwindCSS: Styling \ No newline at end of file diff --git a/web/src/client/assets/index.html b/web/src/client/assets/index.html index bf24259d..fd740800 100644 --- a/web/src/client/assets/index.html +++ b/web/src/client/assets/index.html @@ -22,7 +22,7 @@ - +