From 41d18ca89846309f4642aa9537caf9609a97482f Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Tue, 16 Sep 2025 21:05:11 -0400 Subject: [PATCH 01/12] xtermjs init --- apps/mobile/package.json | 5 +- apps/web/package.json | 9 +- .../.gitignore | 24 + .../eslint.config.js | 23 + .../index.html | 13 + .../package.json | 33 + .../src/App.tsx | 33 + .../src/main.tsx | 9 + .../src/vite-env.d.ts | 1 + .../tsconfig.app.json | 27 + .../tsconfig.json | 7 + .../tsconfig.node.json | 25 + .../vite.config.ts | 7 + .../react-native-xtermjs-webview/.gitignore | 24 + .../react-native-xtermjs-webview/README.md | 69 ++ .../eslint.config.js | 23 + .../react-native-xtermjs-webview/package.json | 39 + .../src/index.tsx | 6 + .../src/vite-env.d.ts | 1 + .../tsconfig.app.json | 27 + .../tsconfig.json | 7 + .../tsconfig.node.json | 25 + .../vite.config.ts | 21 + pnpm-lock.yaml | 964 ++++++++++++------ 24 files changed, 1112 insertions(+), 310 deletions(-) create mode 100644 packages/react-native-xtermjs-webview-internal/.gitignore create mode 100644 packages/react-native-xtermjs-webview-internal/eslint.config.js create mode 100644 packages/react-native-xtermjs-webview-internal/index.html create mode 100644 packages/react-native-xtermjs-webview-internal/package.json create mode 100644 packages/react-native-xtermjs-webview-internal/src/App.tsx create mode 100644 packages/react-native-xtermjs-webview-internal/src/main.tsx create mode 100644 packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts create mode 100644 packages/react-native-xtermjs-webview-internal/tsconfig.app.json create mode 100644 packages/react-native-xtermjs-webview-internal/tsconfig.json create mode 100644 packages/react-native-xtermjs-webview-internal/tsconfig.node.json create mode 100644 packages/react-native-xtermjs-webview-internal/vite.config.ts create mode 100644 packages/react-native-xtermjs-webview/.gitignore create mode 100644 packages/react-native-xtermjs-webview/README.md create mode 100644 packages/react-native-xtermjs-webview/eslint.config.js create mode 100644 packages/react-native-xtermjs-webview/package.json create mode 100644 packages/react-native-xtermjs-webview/src/index.tsx create mode 100644 packages/react-native-xtermjs-webview/src/vite-env.d.ts create mode 100644 packages/react-native-xtermjs-webview/tsconfig.app.json create mode 100644 packages/react-native-xtermjs-webview/tsconfig.json create mode 100644 packages/react-native-xtermjs-webview/tsconfig.node.json create mode 100644 packages/react-native-xtermjs-webview/vite.config.ts diff --git a/apps/mobile/package.json b/apps/mobile/package.json index fd3303e..9d82d26 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -34,7 +34,7 @@ "@react-navigation/native": "^7.1.8", "@shopify/flash-list": "2.0.2", "@tanstack/react-form": "^1.20.0", - "@tanstack/react-query": "^5.87.4", + "@tanstack/react-query": "^5.89.0", "date-fns": "^4.1.0", "expo": "54.0.8", "expo-clipboard": "~8.0.7", @@ -64,8 +64,9 @@ "react-native-safe-area-context": "~5.6.1", "react-native-screens": "~4.16.0", "react-native-web": "~0.21.1", + "react-native-webview": "13.15.0", "react-native-worklets": "~0.5.1", - "zod": "^4.1.8" + "zod": "^4.1.9" }, "devDependencies": { "@epic-web/config": "^1.21.3", diff --git a/apps/web/package.json b/apps/web/package.json index c7a690f..52bce94 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,19 +17,20 @@ "dependencies": { "@astrojs/vercel": "^8.2.7", "@fressh/assets": "workspace:*", - "@tailwindcss/vite": "^4.1.13", + "@tailwindcss/vite": "4.1.9", "@vercel/analytics": "^1.5.0", "astro": "^5.13.7", - "tailwindcss": "^4.1.13" + "tailwindcss": "4.1.10" }, "devDependencies": { "@epic-web/config": "^1.21.3", - "@typescript-eslint/parser": "^8.43.0", + "@typescript-eslint/parser": "^8.44.0", "eslint": "^9.35.0", "eslint-plugin-astro": "^1.3.1", "eslint-plugin-jsx-a11y": "^6.10.2", "prettier": "^3.6.2", "prettier-plugin-astro": "0.14.1", - "prettier-plugin-tailwindcss": "^0.6.14" + "prettier-plugin-tailwindcss": "^0.6.14", + "vite": "6.3.6" } } diff --git a/packages/react-native-xtermjs-webview-internal/.gitignore b/packages/react-native-xtermjs-webview-internal/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/react-native-xtermjs-webview-internal/eslint.config.js b/packages/react-native-xtermjs-webview-internal/eslint.config.js new file mode 100644 index 0000000..d94e7de --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/packages/react-native-xtermjs-webview-internal/index.html b/packages/react-native-xtermjs-webview-internal/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/packages/react-native-xtermjs-webview-internal/package.json b/packages/react-native-xtermjs-webview-internal/package.json new file mode 100644 index 0000000..3b808b5 --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/package.json @@ -0,0 +1,33 @@ +{ + "name": "react-native-xtermjs-webview-internal", + "private": true, + "version": "0.0.0", + "type": "module", + "files": [ + "dist" + ], + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@xterm/xterm": "^5.5.0", + "react": "19.1.0", + "react-dom": "19.1.0" + }, + "devDependencies": { + "@eslint/js": "^9.35.0", + "@types/react": "~19.1.12", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.2", + "eslint": "^9.35.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.4.0", + "typescript": "~5.9.2", + "typescript-eslint": "^8.44.0", + "vite": "6.3.6" + } +} diff --git a/packages/react-native-xtermjs-webview-internal/src/App.tsx b/packages/react-native-xtermjs-webview-internal/src/App.tsx new file mode 100644 index 0000000..5cc38c7 --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/src/App.tsx @@ -0,0 +1,33 @@ +import { useEffect, useRef, useState } from 'react'; +import { Terminal } from '@xterm/xterm'; +import '@xterm/xterm/css/xterm.css'; + +export function App() { + const [count, setCount] = useState(0); + const terminalRef = useRef(null); + + useEffect(() => { + if (!terminalRef.current) return; + const terminal = new Terminal(); + terminal.open(terminalRef.current); + terminal.write('Hello from Xterm.js!'); + }, []); + + return ( + <> +

Xterm.js

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+
+ + ); +} diff --git a/packages/react-native-xtermjs-webview-internal/src/main.tsx b/packages/react-native-xtermjs-webview-internal/src/main.tsx new file mode 100644 index 0000000..34430f2 --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App.tsx'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts b/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/react-native-xtermjs-webview-internal/tsconfig.app.json b/packages/react-native-xtermjs-webview-internal/tsconfig.app.json new file mode 100644 index 0000000..227a6c6 --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/packages/react-native-xtermjs-webview-internal/tsconfig.json b/packages/react-native-xtermjs-webview-internal/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/packages/react-native-xtermjs-webview-internal/tsconfig.node.json b/packages/react-native-xtermjs-webview-internal/tsconfig.node.json new file mode 100644 index 0000000..f85a399 --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/react-native-xtermjs-webview-internal/vite.config.ts b/packages/react-native-xtermjs-webview-internal/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/packages/react-native-xtermjs-webview/.gitignore b/packages/react-native-xtermjs-webview/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/packages/react-native-xtermjs-webview/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/react-native-xtermjs-webview/README.md b/packages/react-native-xtermjs-webview/README.md new file mode 100644 index 0000000..7959ce4 --- /dev/null +++ b/packages/react-native-xtermjs-webview/README.md @@ -0,0 +1,69 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/packages/react-native-xtermjs-webview/eslint.config.js b/packages/react-native-xtermjs-webview/eslint.config.js new file mode 100644 index 0000000..d94e7de --- /dev/null +++ b/packages/react-native-xtermjs-webview/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/packages/react-native-xtermjs-webview/package.json b/packages/react-native-xtermjs-webview/package.json new file mode 100644 index 0000000..8167fa4 --- /dev/null +++ b/packages/react-native-xtermjs-webview/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-native-xtermjs-webview", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + ".": "./dist/index.js" + }, + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react-native-xtermjs-webview-internal": "workspace:*" + }, + "peerDependencies": { + "react": "19.1.0", + "react-dom": "19.1.0", + "react-native-webview": "13.15.0" + }, + "devDependencies": { + "@eslint/js": "^9.35.0", + "@types/react": "~19.1.12", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.2", + "eslint": "^9.35.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.4.0", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-native-webview": "13.15.0", + "typescript": "~5.9.2", + "typescript-eslint": "^8.44.0", + "vite": "6.3.6" + } +} diff --git a/packages/react-native-xtermjs-webview/src/index.tsx b/packages/react-native-xtermjs-webview/src/index.tsx new file mode 100644 index 0000000..4033fab --- /dev/null +++ b/packages/react-native-xtermjs-webview/src/index.tsx @@ -0,0 +1,6 @@ +import { WebView } from 'react-native-webview'; +import htmlString from 'react-native-xtermjs-webview-internal/dist/assets/index.html?raw'; + +export function XtermJsWebView() { + return ; +} diff --git a/packages/react-native-xtermjs-webview/src/vite-env.d.ts b/packages/react-native-xtermjs-webview/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/react-native-xtermjs-webview/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/react-native-xtermjs-webview/tsconfig.app.json b/packages/react-native-xtermjs-webview/tsconfig.app.json new file mode 100644 index 0000000..227a6c6 --- /dev/null +++ b/packages/react-native-xtermjs-webview/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/packages/react-native-xtermjs-webview/tsconfig.json b/packages/react-native-xtermjs-webview/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/packages/react-native-xtermjs-webview/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/packages/react-native-xtermjs-webview/tsconfig.node.json b/packages/react-native-xtermjs-webview/tsconfig.node.json new file mode 100644 index 0000000..f85a399 --- /dev/null +++ b/packages/react-native-xtermjs-webview/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/react-native-xtermjs-webview/vite.config.ts b/packages/react-native-xtermjs-webview/vite.config.ts new file mode 100644 index 0000000..4f0fd03 --- /dev/null +++ b/packages/react-native-xtermjs-webview/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import packageJson from './package.json' +import { resolve } from 'path' + + + +export default defineConfig({ + plugins: [react()], + build: { + rollupOptions: { + external: Object.keys(packageJson.peerDependencies || {}), + }, + lib: { + entry: resolve(__dirname, 'src/index.tsx'), + name: 'ReactNativeXtermJsWebView', + formats: ['es'], + fileName: 'react-native-xtermjs-webview', + } + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad16a53..a3b0f68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,7 @@ importers: devDependencies: '@epic-web/config': specifier: ^1.21.3 - version: 1.21.3(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2) + version: 1.21.3(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2) cross-env: specifier: ^10.0.0 version: 10.0.0 @@ -68,14 +68,14 @@ importers: specifier: ^1.20.0 version: 1.20.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-query': - specifier: ^5.87.4 - version: 5.87.4(react@19.1.0) + specifier: ^5.89.0 + version: 5.89.0(react@19.1.0) date-fns: specifier: ^4.1.0 version: 4.1.0 expo: specifier: 54.0.8 - version: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + version: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-clipboard: specifier: ~8.0.7 version: 8.0.7(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) @@ -111,7 +111,7 @@ importers: version: 8.0.8(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-router: specifier: 6.0.6 - version: 6.0.6(be6ce5c43438e2db372447bf3c6a78fa) + version: 6.0.6(795f20997b5e74199ec1f471a585ccc7) expo-secure-store: specifier: ~15.0.7 version: 15.0.7(expo@54.0.8) @@ -157,16 +157,19 @@ importers: react-native-web: specifier: ~0.21.1 version: 0.21.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-native-webview: + specifier: 13.15.0 + version: 13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react-native-worklets: specifier: ~0.5.1 version: 0.5.1(@babel/core@7.28.3)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) zod: - specifier: ^4.1.8 - version: 4.1.8 + specifier: ^4.1.9 + version: 4.1.9 devDependencies: '@epic-web/config': specifier: ^1.21.3 - version: 1.21.3(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2) + version: 1.21.3(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2) '@types/react': specifier: ~19.1.12 version: 19.1.12 @@ -178,7 +181,7 @@ importers: version: 9.35.0(jiti@2.5.1) eslint-config-expo: specifier: ~10.0.0 - version: 10.0.0(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + version: 10.0.0(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) jiti: specifier: ^2.5.1 version: 2.5.1 @@ -202,29 +205,29 @@ importers: dependencies: '@astrojs/vercel': specifier: ^8.2.7 - version: 8.2.7(astro@5.13.7(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.1)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1))(react@19.1.0)(rollup@4.50.1) + version: 8.2.7(astro@5.13.7(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.2)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1))(react@19.1.1)(rollup@4.50.2) '@fressh/assets': specifier: workspace:* version: link:../../packages/assets '@tailwindcss/vite': - specifier: ^4.1.13 - version: 4.1.13(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) + specifier: 4.1.9 + version: 4.1.9(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(react@19.1.0) + version: 1.5.0(react@19.1.1) astro: specifier: ^5.13.7 - version: 5.13.7(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.1)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + version: 5.13.7(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.2)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tailwindcss: - specifier: ^4.1.13 - version: 4.1.13 + specifier: 4.1.10 + version: 4.1.10 devDependencies: '@epic-web/config': specifier: ^1.21.3 - version: 1.21.3(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2) + version: 1.21.3(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2) '@typescript-eslint/parser': - specifier: ^8.43.0 - version: 8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + specifier: ^8.44.0 + version: 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: specifier: ^9.35.0 version: 9.35.0(jiti@2.5.1) @@ -243,6 +246,9 @@ importers: prettier-plugin-tailwindcss: specifier: ^0.6.14 version: 0.6.14(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2) + vite: + specifier: 6.3.6 + version: 6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) packages/assets: {} @@ -310,6 +316,101 @@ importers: specifier: ~5.9.2 version: 5.9.2 + packages/react-native-xtermjs-webview: + dependencies: + react-native-xtermjs-webview-internal: + specifier: workspace:* + version: link:../react-native-xtermjs-webview-internal + devDependencies: + '@eslint/js': + specifier: ^9.35.0 + version: 9.35.0 + '@types/react': + specifier: ~19.1.12 + version: 19.1.12 + '@types/react-dom': + specifier: ^19.1.7 + version: 19.1.9(@types/react@19.1.12) + '@vitejs/plugin-react': + specifier: ^5.0.2 + version: 5.0.2(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) + eslint: + specifier: ^9.35.0 + version: 9.35.0(jiti@2.5.1) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.35.0(jiti@2.5.1)) + eslint-plugin-react-refresh: + specifier: ^0.4.20 + version: 0.4.20(eslint@9.35.0(jiti@2.5.1)) + globals: + specifier: ^16.4.0 + version: 16.4.0 + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + react-native-webview: + specifier: 13.15.0 + version: 13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + typescript: + specifier: ~5.9.2 + version: 5.9.2 + typescript-eslint: + specifier: ^8.44.0 + version: 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + vite: + specifier: 6.3.6 + version: 6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + + packages/react-native-xtermjs-webview-internal: + dependencies: + '@xterm/xterm': + specifier: ^5.5.0 + version: 5.5.0 + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@eslint/js': + specifier: ^9.35.0 + version: 9.35.0 + '@types/react': + specifier: ~19.1.12 + version: 19.1.12 + '@types/react-dom': + specifier: ^19.1.7 + version: 19.1.9(@types/react@19.1.12) + '@vitejs/plugin-react': + specifier: ^5.0.2 + version: 5.0.2(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) + eslint: + specifier: ^9.35.0 + version: 9.35.0(jiti@2.5.1) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.35.0(jiti@2.5.1)) + eslint-plugin-react-refresh: + specifier: ^0.4.20 + version: 0.4.20(eslint@9.35.0(jiti@2.5.1)) + globals: + specifier: ^16.4.0 + version: 16.4.0 + typescript: + specifier: ~5.9.2 + version: 5.9.2 + typescript-eslint: + specifier: ^8.44.0 + version: 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + vite: + specifier: 6.3.6 + version: 6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + packages: '@0no-co/graphql.web@1.2.0': @@ -474,6 +575,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} engines: {node: '>=6.9.0'} @@ -1049,10 +1155,18 @@ packages: resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.2': resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -1851,9 +1965,6 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -2494,6 +2605,9 @@ packages: peerDependencies: release-it: ^18.0.0 || ^19.0.0 + '@rolldown/pluginutils@1.0.0-beta.34': + resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==} + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -2503,108 +2617,108 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.50.1': - resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} + '@rollup/rollup-android-arm-eabi@4.50.2': + resolution: {integrity: sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.50.1': - resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} + '@rollup/rollup-android-arm64@4.50.2': + resolution: {integrity: sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.50.1': - resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} + '@rollup/rollup-darwin-arm64@4.50.2': + resolution: {integrity: sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.50.1': - resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} + '@rollup/rollup-darwin-x64@4.50.2': + resolution: {integrity: sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.50.1': - resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} + '@rollup/rollup-freebsd-arm64@4.50.2': + resolution: {integrity: sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.50.1': - resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} + '@rollup/rollup-freebsd-x64@4.50.2': + resolution: {integrity: sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.50.1': - resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} + '@rollup/rollup-linux-arm-gnueabihf@4.50.2': + resolution: {integrity: sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.50.1': - resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} + '@rollup/rollup-linux-arm-musleabihf@4.50.2': + resolution: {integrity: sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.50.1': - resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} + '@rollup/rollup-linux-arm64-gnu@4.50.2': + resolution: {integrity: sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.50.1': - resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} + '@rollup/rollup-linux-arm64-musl@4.50.2': + resolution: {integrity: sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.50.1': - resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} + '@rollup/rollup-linux-loong64-gnu@4.50.2': + resolution: {integrity: sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.50.1': - resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} + '@rollup/rollup-linux-ppc64-gnu@4.50.2': + resolution: {integrity: sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.50.1': - resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} + '@rollup/rollup-linux-riscv64-gnu@4.50.2': + resolution: {integrity: sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.50.1': - resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} + '@rollup/rollup-linux-riscv64-musl@4.50.2': + resolution: {integrity: sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.50.1': - resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} + '@rollup/rollup-linux-s390x-gnu@4.50.2': + resolution: {integrity: sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.50.1': - resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} + '@rollup/rollup-linux-x64-gnu@4.50.2': + resolution: {integrity: sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.50.1': - resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} + '@rollup/rollup-linux-x64-musl@4.50.2': + resolution: {integrity: sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.50.1': - resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} + '@rollup/rollup-openharmony-arm64@4.50.2': + resolution: {integrity: sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.50.1': - resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} + '@rollup/rollup-win32-arm64-msvc@4.50.2': + resolution: {integrity: sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.50.1': - resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} + '@rollup/rollup-win32-ia32-msvc@4.50.2': + resolution: {integrity: sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.50.1': - resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} + '@rollup/rollup-win32-x64-msvc@4.50.2': + resolution: {integrity: sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==} cpu: [x64] os: [win32] @@ -2673,65 +2787,65 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@tailwindcss/node@4.1.13': - resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==} + '@tailwindcss/node@4.1.9': + resolution: {integrity: sha512-ZFsgw6lbtcZKYPWvf6zAuCVSuer7UQ2Z5P8BETHcpA4x/3NwOjAIXmRnYfG77F14f9bPeuR4GaNz3ji1JkQMeQ==} - '@tailwindcss/oxide-android-arm64@4.1.13': - resolution: {integrity: sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==} + '@tailwindcss/oxide-android-arm64@4.1.9': + resolution: {integrity: sha512-X4mBUUJ3DPqODhtdT5Ju55feJwBN+hP855Z7c0t11Jzece9KRtdM41ljMrCcureKMh96mcOh2gxahkp1yE+BOQ==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.13': - resolution: {integrity: sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==} + '@tailwindcss/oxide-darwin-arm64@4.1.9': + resolution: {integrity: sha512-jnWnqz71ZLXUbJLW53m9dSQakLBfaWxAd9TAibimrNdQfZKyie+xGppdDCZExtYwUdflt3kOT9y1JUgYXVEQmw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.13': - resolution: {integrity: sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==} + '@tailwindcss/oxide-darwin-x64@4.1.9': + resolution: {integrity: sha512-+Ui6LlvZ6aCPvSwv3l16nYb6gu1N6RamFz7hSu5aqaiPrDQqD1LPT/e8r2/laSVwFjRyOZxQQ/gvGxP3ihA2rw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.13': - resolution: {integrity: sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==} + '@tailwindcss/oxide-freebsd-x64@4.1.9': + resolution: {integrity: sha512-BWqCh0uoXMprwWfG7+oyPW53VCh6G08pxY0IIN/i5DQTpPnCJ4zm2W8neH9kW1v1f6RXP3b2qQjAzrAcnQ5e9w==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': - resolution: {integrity: sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.9': + resolution: {integrity: sha512-U8itjQb5TVc80aV5Yo+JtKo+qS95CV4XLrKEtSLQFoTD/c9j3jk4WZipYT+9Jxqem29qCMRPxjEZ3s+wTT4XCw==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': - resolution: {integrity: sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.9': + resolution: {integrity: sha512-dKlGraoNvyTrR7ovLw3Id9yTwc+l0NYg8bwOkYqk+zltvGns8bPvVr6PH5jATdc75kCGd6kDRmP4p1LwqCnPJQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.13': - resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.9': + resolution: {integrity: sha512-qCZ4QTrZaBEgNM13pGjvakdmid1Kw3CUCEQzgVAn64Iud7zSxOGwK1usg+hrwrOfFH7vXZZr8OhzC8fJTRq5NA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.13': - resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.9': + resolution: {integrity: sha512-bmzkAWQjRlY9udmg/a1bOtZpV14ZCdrB74PZrd7Oz/wK62Rk+m9+UV3BsgGfOghyO5Qu5ZDciADzDMZbi9n1+g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.13': - resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==} + '@tailwindcss/oxide-linux-x64-musl@4.1.9': + resolution: {integrity: sha512-NpvPQsXj1raDHhd+g2SUvZQoTPWfYAsyYo9h4ZqV7EOmR+aj7LCAE5hnXNnrJ5Egy/NiO3Hs7BNpSbsPEOpORg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.13': - resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==} + '@tailwindcss/oxide-wasm32-wasi@4.1.9': + resolution: {integrity: sha512-G93Yuf3xrpTxDUCSh685d1dvOkqOB0Gy+Bchv9Zy3k+lNw/9SEgsHit50xdvp1/p9yRH2TeDHJeDLUiV4mlTkA==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -2742,32 +2856,32 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': - resolution: {integrity: sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.9': + resolution: {integrity: sha512-Eq9FZzZe/NPkUiSMY+eY7r5l7msuFlm6wC6lnV11m8885z0vs9zx48AKTfw0UbVecTRV5wMxKb3Kmzx2LoUIWg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.13': - resolution: {integrity: sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.9': + resolution: {integrity: sha512-oZ4zkthMXMJN2w/vu3jEfuqWTW7n8giGYDV/SfhBGRNehNMOBqh3YUAEv+8fv2YDJEzL4JpXTNTiSXW3UiUwBw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.13': - resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==} + '@tailwindcss/oxide@4.1.9': + resolution: {integrity: sha512-oqjNxOBt1iNRAywjiH+VFsfovx/hVt4mxe0kOkRMAbbcCwbJg5e2AweFqyGN7gtmE1TJXnvnyX7RWTR1l72ciQ==} engines: {node: '>= 10'} - '@tailwindcss/vite@4.1.13': - resolution: {integrity: sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==} + '@tailwindcss/vite@4.1.9': + resolution: {integrity: sha512-JdcROJysSGRpDq0JT5XPxRjF3rq4QnZD/PsNUVIQrMyYHUIBxRFPTUmGlWjy24igeC3rAgcRIDGLSd9AsljW5A==} peerDependencies: - vite: ^5.2.0 || ^6 || ^7 + vite: ^5.2.0 || ^6 '@tanstack/form-core@1.20.0': resolution: {integrity: sha512-FGlKvcsusOf4756vtN1EoDI4h50r4/11eTcpF3NcnE04N/bSn2gP7cdhG6tYA0lJWzM9H1pNIzZ86uZ4MHB9eA==} - '@tanstack/query-core@5.87.4': - resolution: {integrity: sha512-uNsg6zMxraEPDVO2Bn+F3/ctHi+Zsk+MMpcN8h6P7ozqD088F6mFY5TfGM7zuyIrL7HKpDyu6QHfLWiDxh3cuw==} + '@tanstack/query-core@5.89.0': + resolution: {integrity: sha512-joFV1MuPhSLsKfTzwjmPDrp8ENfZ9N23ymFu07nLfn3JCkSHy0CFgsyhHTJOmWaumC/WiNIKM0EJyduCF/Ih/Q==} '@tanstack/react-form@1.20.0': resolution: {integrity: sha512-1UfWqEYRnHr4cooGbHiTQqoqus8soNUH+RLD6UyhIQEvomOSQMX0JgX+zGSl08tIugrnWcAnh50n5T9IIs/Evw==} @@ -2778,8 +2892,8 @@ packages: '@tanstack/react-start': optional: true - '@tanstack/react-query@5.87.4': - resolution: {integrity: sha512-T5GT/1ZaNsUXf5I3RhcYuT17I4CPlbZgyLxc/ZGv7ciS6esytlbjb3DgUFO6c8JWYMDpdjSWInyGZUErgzqhcA==} + '@tanstack/react-query@5.89.0': + resolution: {integrity: sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A==} peerDependencies: react: ^18 || ^19 @@ -2868,6 +2982,11 @@ packages: resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==} deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed. + '@types/react-dom@19.1.9': + resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} + peerDependencies: + '@types/react': ^19.0.0 + '@types/react@19.1.12': resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==} @@ -2908,6 +3027,14 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/eslint-plugin@8.44.0': + resolution: {integrity: sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.44.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@7.18.0': resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} engines: {node: ^18.18.0 || >=20.0.0} @@ -2925,8 +3052,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.43.0': - resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==} + '@typescript-eslint/parser@8.44.0': + resolution: {integrity: sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2938,8 +3065,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.43.0': - resolution: {integrity: sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==} + '@typescript-eslint/project-service@8.44.0': + resolution: {integrity: sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -2960,6 +3087,10 @@ packages: resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.44.0': + resolution: {integrity: sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.41.0': resolution: {integrity: sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2972,6 +3103,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.44.0': + resolution: {integrity: sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@7.18.0': resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -2989,6 +3126,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.44.0': + resolution: {integrity: sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/types@5.62.0': resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3005,6 +3149,10 @@ packages: resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.44.0': + resolution: {integrity: sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@5.62.0': resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3029,8 +3177,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.43.0': - resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==} + '@typescript-eslint/typescript-estree@8.44.0': + resolution: {integrity: sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -3054,6 +3202,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.44.0': + resolution: {integrity: sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@5.62.0': resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3070,6 +3225,10 @@ packages: resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.44.0': + resolution: {integrity: sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -3223,6 +3382,12 @@ packages: '@vercel/routing-utils@5.1.1': resolution: {integrity: sha512-EyOik06V2fPXAbKY087BM7DMOQOJK+9mubwwox1TkDi21tMeJcMYwsXwepm6ZmyZ5u0j1TpJW172fP4MbzaCcg==} + '@vitejs/plugin-react@5.0.2': + resolution: {integrity: sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/eslint-plugin@1.3.6': resolution: {integrity: sha512-sa/QAljHbUP+sMdPjK8e/6nS2+QB/bh1aDKEkAKMqsKVzBXqz4LRYfT7UVGIP8LMIrskGTxqAbHuiL+FOYWzHg==} peerDependencies: @@ -3242,6 +3407,9 @@ packages: resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + '@xterm/xterm@5.5.0': + resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} + abbrev@3.0.1: resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} engines: {node: ^18.17.0 || >=20.5.0} @@ -4117,6 +4285,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -4583,6 +4760,11 @@ packages: peerDependencies: eslint: ^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8 + eslint-plugin-react-refresh@0.4.20: + resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==} + peerDependencies: + eslint: '>=8.40' + eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} engines: {node: '>=4'} @@ -5187,6 +5369,10 @@ packages: resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} engines: {node: '>=18'} + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -7244,6 +7430,12 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + react-native-webview@13.15.0: + resolution: {integrity: sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==} + peerDependencies: + react: '*' + react-native: '*' + react-native-worklets@0.5.1: resolution: {integrity: sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==} peerDependencies: @@ -7266,6 +7458,10 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -7300,6 +7496,10 @@ packages: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + react@19.1.1: + resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + engines: {node: '>=0.10.0'} + read-package-up@11.0.0: resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} engines: {node: '>=18'} @@ -7503,8 +7703,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.50.1: - resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} + rollup@4.50.2: + resolution: {integrity: sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -7931,8 +8131,11 @@ packages: engines: {node: '>=18.18.0'} hasBin: true - tailwindcss@4.1.13: - resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==} + tailwindcss@4.1.10: + resolution: {integrity: sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==} + + tailwindcss@4.1.9: + resolution: {integrity: sha512-anBZRcvfNMsQdHB9XSGzAtIQWlhs49uK75jfkwrqjRUbjt4d7q9RE1wR1xWyfYZhLFnFX4ahWp88Au2lcEw5IQ==} tapable@2.2.3: resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} @@ -7986,6 +8189,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -8149,6 +8356,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + typescript-eslint@8.44.0: + resolution: {integrity: sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -8732,8 +8946,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.1.8: - resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==} + zod@4.1.9: + resolution: {integrity: sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -8799,14 +9013,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/vercel@8.2.7(astro@5.13.7(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.1)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1))(react@19.1.0)(rollup@4.50.1)': + '@astrojs/vercel@8.2.7(astro@5.13.7(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.2)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1))(react@19.1.1)(rollup@4.50.2)': dependencies: '@astrojs/internal-helpers': 0.7.2 - '@vercel/analytics': 1.5.0(react@19.1.0) + '@vercel/analytics': 1.5.0(react@19.1.1) '@vercel/functions': 2.2.13 - '@vercel/nft': 0.29.4(rollup@4.50.1) + '@vercel/nft': 0.29.4(rollup@4.50.2) '@vercel/routing-utils': 5.1.1 - astro: 5.13.7(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.1)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + astro: 5.13.7(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.2)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) esbuild: 0.25.9 tinyglobby: 0.2.14 transitivePeerDependencies: @@ -8979,7 +9193,7 @@ snapshots: dependencies: '@babel/template': 7.27.2 '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -8999,6 +9213,10 @@ snapshots: dependencies: '@babel/types': 7.28.2 + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.3)': dependencies: '@babel/core': 7.28.3 @@ -9700,11 +9918,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.2': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@bcoe/v8-coverage@0.2.3': {} '@capsizecss/unpack@2.4.0': @@ -9746,11 +9981,11 @@ snapshots: tslib: 2.8.1 optional: true - '@epic-web/config@1.21.3(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2)': + '@epic-web/config@1.21.3(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2)': dependencies: '@total-typescript/ts-reset': 0.6.1 '@vitest/eslint-plugin': 1.3.6(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-jest-dom: 5.5.0(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-playwright: 2.2.2(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.35.0(jiti@2.5.1)) @@ -9952,7 +10187,7 @@ snapshots: connect: 3.7.0 debug: 4.4.1 env-editor: 0.4.2 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) freeport-async: 2.0.0 getenv: 2.0.0 glob: 10.4.5 @@ -9984,7 +10219,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.6(be6ce5c43438e2db372447bf3c6a78fa) + expo-router: 6.0.6(795f20997b5e74199ec1f471a585ccc7) react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -10005,7 +10240,7 @@ snapshots: '@expo/plist': 0.4.6 '@expo/sdk-runtime-versions': 1.0.0 chalk: 4.1.2 - debug: 4.4.1 + debug: 4.4.3 getenv: 2.0.0 glob: 10.4.5 resolve-from: 5.0.0 @@ -10173,7 +10408,7 @@ snapshots: postcss: 8.4.49 resolve-from: 5.0.0 optionalDependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - bufferutil - supports-color @@ -10182,7 +10417,7 @@ snapshots: '@expo/metro-runtime@6.1.1(expo@54.0.8)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)': dependencies: anser: 1.4.10 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) pretty-format: 29.7.0 react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) @@ -10245,7 +10480,7 @@ snapshots: '@expo/json-file': 10.0.7 '@react-native/normalize-colors': 0.81.4 debug: 4.4.1 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) resolve-from: 5.0.0 semver: 7.7.2 xml2js: 0.6.0 @@ -10776,11 +11011,6 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.30 - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.30 - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/source-map@0.3.11': @@ -11011,16 +11241,17 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-collection@1.1.7(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.0) '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.12)(react@19.1.0)': dependencies: @@ -11034,18 +11265,18 @@ snapshots: optionalDependencies: '@types/react': 19.1.12 - '@radix-ui/react-dialog@1.1.15(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.0) '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.12)(react@19.1.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.0) - '@radix-ui/react-portal': 1.1.9(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-presence': 1.1.5(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.0) aria-hidden: 1.2.6 @@ -11054,6 +11285,7 @@ snapshots: react-remove-scroll: 2.7.1(@types/react@19.1.12)(react@19.1.0) optionalDependencies: '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) '@radix-ui/react-direction@1.1.1(@types/react@19.1.12)(react@19.1.0)': dependencies: @@ -11061,17 +11293,18 @@ snapshots: optionalDependencies: '@types/react': 19.1.12 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.0) '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.12)(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) '@radix-ui/react-focus-guards@1.1.3(@types/react@19.1.12)(react@19.1.0)': dependencies: @@ -11079,15 +11312,16 @@ snapshots: optionalDependencies: '@types/react': 19.1.12 - '@radix-ui/react-focus-scope@1.1.7(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) '@radix-ui/react-id@1.1.1(@types/react@19.1.12)(react@19.1.0)': dependencies: @@ -11096,16 +11330,17 @@ snapshots: optionalDependencies: '@types/react': 19.1.12 - '@radix-ui/react-portal@1.1.9(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) - '@radix-ui/react-presence@1.1.5(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.0) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.0) @@ -11113,30 +11348,33 @@ snapshots: react-dom: 19.1.0(react@19.1.0) optionalDependencies: '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) - '@radix-ui/react-primitive@2.1.3(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) - '@radix-ui/react-roving-focus@1.1.11(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.0) '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.0) '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.0) '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) '@radix-ui/react-slot@1.2.0(@types/react@19.1.12)(react@19.1.0)': dependencies: @@ -11152,20 +11390,21 @@ snapshots: optionalDependencies: '@types/react': 19.1.12 - '@radix-ui/react-tabs@1.1.13(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.0) '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.0) '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.0) - '@radix-ui/react-presence': 1.1.5(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.12)(react@19.1.0)': dependencies: @@ -11620,75 +11859,77 @@ snapshots: - conventional-commits-filter - conventional-commits-parser - '@rollup/pluginutils@5.3.0(rollup@4.50.1)': + '@rolldown/pluginutils@1.0.0-beta.34': {} + + '@rollup/pluginutils@5.3.0(rollup@4.50.2)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.50.1 + rollup: 4.50.2 - '@rollup/rollup-android-arm-eabi@4.50.1': + '@rollup/rollup-android-arm-eabi@4.50.2': optional: true - '@rollup/rollup-android-arm64@4.50.1': + '@rollup/rollup-android-arm64@4.50.2': optional: true - '@rollup/rollup-darwin-arm64@4.50.1': + '@rollup/rollup-darwin-arm64@4.50.2': optional: true - '@rollup/rollup-darwin-x64@4.50.1': + '@rollup/rollup-darwin-x64@4.50.2': optional: true - '@rollup/rollup-freebsd-arm64@4.50.1': + '@rollup/rollup-freebsd-arm64@4.50.2': optional: true - '@rollup/rollup-freebsd-x64@4.50.1': + '@rollup/rollup-freebsd-x64@4.50.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + '@rollup/rollup-linux-arm-gnueabihf@4.50.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.50.1': + '@rollup/rollup-linux-arm-musleabihf@4.50.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.50.1': + '@rollup/rollup-linux-arm64-gnu@4.50.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.50.1': + '@rollup/rollup-linux-arm64-musl@4.50.2': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + '@rollup/rollup-linux-loong64-gnu@4.50.2': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.50.1': + '@rollup/rollup-linux-ppc64-gnu@4.50.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.50.1': + '@rollup/rollup-linux-riscv64-gnu@4.50.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.50.1': + '@rollup/rollup-linux-riscv64-musl@4.50.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.50.1': + '@rollup/rollup-linux-s390x-gnu@4.50.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.50.1': + '@rollup/rollup-linux-x64-gnu@4.50.2': optional: true - '@rollup/rollup-linux-x64-musl@4.50.1': + '@rollup/rollup-linux-x64-musl@4.50.2': optional: true - '@rollup/rollup-openharmony-arm64@4.50.1': + '@rollup/rollup-openharmony-arm64@4.50.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.50.1': + '@rollup/rollup-win32-arm64-msvc@4.50.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.50.1': + '@rollup/rollup-win32-ia32-msvc@4.50.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.50.1': + '@rollup/rollup-win32-x64-msvc@4.50.2': optional: true '@rtsao/scc@1.1.0': {} @@ -11765,82 +12006,82 @@ snapshots: dependencies: tslib: 2.8.1 - '@tailwindcss/node@4.1.13': + '@tailwindcss/node@4.1.9': dependencies: - '@jridgewell/remapping': 2.3.5 + '@ampproject/remapping': 2.3.0 enhanced-resolve: 5.18.3 jiti: 2.5.1 lightningcss: 1.30.1 magic-string: 0.30.19 source-map-js: 1.2.1 - tailwindcss: 4.1.13 + tailwindcss: 4.1.9 - '@tailwindcss/oxide-android-arm64@4.1.13': + '@tailwindcss/oxide-android-arm64@4.1.9': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.13': + '@tailwindcss/oxide-darwin-arm64@4.1.9': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.13': + '@tailwindcss/oxide-darwin-x64@4.1.9': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.13': + '@tailwindcss/oxide-freebsd-x64@4.1.9': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.9': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.9': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.13': + '@tailwindcss/oxide-linux-arm64-musl@4.1.9': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.13': + '@tailwindcss/oxide-linux-x64-gnu@4.1.9': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.13': + '@tailwindcss/oxide-linux-x64-musl@4.1.9': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.13': + '@tailwindcss/oxide-wasm32-wasi@4.1.9': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.9': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.13': + '@tailwindcss/oxide-win32-x64-msvc@4.1.9': optional: true - '@tailwindcss/oxide@4.1.13': + '@tailwindcss/oxide@4.1.9': dependencies: detect-libc: 2.0.4 tar: 7.4.3 optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.13 - '@tailwindcss/oxide-darwin-arm64': 4.1.13 - '@tailwindcss/oxide-darwin-x64': 4.1.13 - '@tailwindcss/oxide-freebsd-x64': 4.1.13 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.13 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.13 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.13 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.13 - '@tailwindcss/oxide-linux-x64-musl': 4.1.13 - '@tailwindcss/oxide-wasm32-wasi': 4.1.13 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.13 + '@tailwindcss/oxide-android-arm64': 4.1.9 + '@tailwindcss/oxide-darwin-arm64': 4.1.9 + '@tailwindcss/oxide-darwin-x64': 4.1.9 + '@tailwindcss/oxide-freebsd-x64': 4.1.9 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.9 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.9 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.9 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.9 + '@tailwindcss/oxide-linux-x64-musl': 4.1.9 + '@tailwindcss/oxide-wasm32-wasi': 4.1.9 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.9 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.9 - '@tailwindcss/vite@4.1.13(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': + '@tailwindcss/vite@4.1.9(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': dependencies: - '@tailwindcss/node': 4.1.13 - '@tailwindcss/oxide': 4.1.13 - tailwindcss: 4.1.13 + '@tailwindcss/node': 4.1.9 + '@tailwindcss/oxide': 4.1.9 + tailwindcss: 4.1.9 vite: 6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) '@tanstack/form-core@1.20.0': dependencies: '@tanstack/store': 0.7.4 - '@tanstack/query-core@5.87.4': {} + '@tanstack/query-core@5.89.0': {} '@tanstack/react-form@1.20.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: @@ -11852,9 +12093,9 @@ snapshots: transitivePeerDependencies: - react-dom - '@tanstack/react-query@5.87.4(react@19.1.0)': + '@tanstack/react-query@5.89.0(react@19.1.0)': dependencies: - '@tanstack/query-core': 5.87.4 + '@tanstack/query-core': 5.89.0 react: 19.1.0 '@tanstack/react-store@0.7.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': @@ -11955,6 +12196,10 @@ snapshots: dependencies: parse-path: 7.1.0 + '@types/react-dom@19.1.9(@types/react@19.1.12)': + dependencies: + '@types/react': 19.1.12 + '@types/react@19.1.12': dependencies: csstype: 3.1.3 @@ -12008,6 +12253,23 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/type-utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.44.0 + eslint: 9.35.0(jiti@2.5.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@7.18.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 @@ -12033,12 +12295,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: - '@typescript-eslint/scope-manager': 8.43.0 - '@typescript-eslint/types': 8.43.0 - '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.43.0 + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.44.0 debug: 4.4.1 eslint: 9.35.0(jiti@2.5.1) typescript: 5.9.2 @@ -12047,17 +12309,17 @@ snapshots: '@typescript-eslint/project-service@8.41.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2) '@typescript-eslint/types': 8.43.0 debug: 4.4.1 typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.43.0(typescript@5.9.2)': + '@typescript-eslint/project-service@8.44.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2) - '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.2) + '@typescript-eslint/types': 8.44.0 debug: 4.4.1 typescript: 5.9.2 transitivePeerDependencies: @@ -12083,6 +12345,11 @@ snapshots: '@typescript-eslint/types': 8.43.0 '@typescript-eslint/visitor-keys': 8.43.0 + '@typescript-eslint/scope-manager@8.44.0': + dependencies: + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/visitor-keys': 8.44.0 + '@typescript-eslint/tsconfig-utils@8.41.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 @@ -12091,6 +12358,10 @@ snapshots: dependencies: typescript: 5.9.2 + '@typescript-eslint/tsconfig-utils@8.44.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + '@typescript-eslint/type-utils@7.18.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) @@ -12115,6 +12386,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': + dependencies: + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + debug: 4.4.1 + eslint: 9.35.0(jiti@2.5.1) + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@5.62.0': {} '@typescript-eslint/types@7.18.0': {} @@ -12123,11 +12406,13 @@ snapshots: '@typescript-eslint/types@8.43.0': {} + '@typescript-eslint/types@8.44.0': {} + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.2)': dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.1 + debug: 4.4.3 globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 @@ -12168,12 +12453,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.43.0(typescript@5.9.2)': + '@typescript-eslint/typescript-estree@8.44.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/project-service': 8.43.0(typescript@5.9.2) - '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2) - '@typescript-eslint/types': 8.43.0 - '@typescript-eslint/visitor-keys': 8.43.0 + '@typescript-eslint/project-service': 8.44.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.2) + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/visitor-keys': 8.44.0 debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -12221,6 +12506,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.5.1)) + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) + eslint: 9.35.0(jiti@2.5.1) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@5.62.0': dependencies: '@typescript-eslint/types': 5.62.0 @@ -12241,6 +12537,11 @@ snapshots: '@typescript-eslint/types': 8.43.0 eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.44.0': + dependencies: + '@typescript-eslint/types': 8.44.0 + eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -12314,18 +12615,18 @@ snapshots: '@urql/core': 5.2.0 wonka: 6.3.5 - '@vercel/analytics@1.5.0(react@19.1.0)': + '@vercel/analytics@1.5.0(react@19.1.1)': optionalDependencies: - react: 19.1.0 + react: 19.1.1 '@vercel/functions@2.2.13': dependencies: '@vercel/oidc': 2.0.2 - '@vercel/nft@0.29.4(rollup@4.50.1)': + '@vercel/nft@0.29.4(rollup@4.50.2)': dependencies: '@mapbox/node-pre-gyp': 2.0.0 - '@rollup/pluginutils': 5.3.0(rollup@4.50.1) + '@rollup/pluginutils': 5.3.0(rollup@4.50.2) acorn: 8.15.0 acorn-import-attributes: 1.9.5(acorn@8.15.0) async-sema: 3.1.1 @@ -12353,6 +12654,18 @@ snapshots: optionalDependencies: ajv: 6.12.6 + '@vitejs/plugin-react@5.0.2(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.3 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.3) + '@rolldown/pluginutils': 1.0.0-beta.34 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@vitest/eslint-plugin@1.3.6(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@typescript-eslint/scope-manager': 8.43.0 @@ -12367,6 +12680,8 @@ snapshots: '@xmldom/xmldom@0.8.11': {} + '@xterm/xterm@5.5.0': {} + abbrev@3.0.1: {} abort-controller@3.0.0: @@ -12572,7 +12887,7 @@ snapshots: transitivePeerDependencies: - supports-color - astro@5.13.7(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.1)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1): + astro@5.13.7(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.2)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.12.2 '@astrojs/internal-helpers': 0.7.2 @@ -12580,7 +12895,7 @@ snapshots: '@astrojs/telemetry': 3.3.0 '@capsizecss/unpack': 2.4.0 '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.3.0(rollup@4.50.1) + '@rollup/pluginutils': 5.3.0(rollup@4.50.2) acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 @@ -12753,7 +13068,7 @@ snapshots: babel-plugin-jest-hoist@30.0.1: dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 '@types/babel__core': 7.20.5 babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.3): @@ -12846,7 +13161,7 @@ snapshots: resolve-from: 5.0.0 optionalDependencies: '@babel/runtime': 7.28.3 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - '@babel/core' - supports-color @@ -12865,7 +13180,7 @@ snapshots: babel-walk@3.0.0-canary-5: dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 bail@2.0.2: {} @@ -13260,8 +13575,8 @@ snapshots: constantinople@4.0.1: dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 content-type@1.0.5: {} @@ -13451,6 +13766,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decamelize@1.2.0: {} decode-formdata@0.9.0: {} @@ -13808,12 +14127,12 @@ snapshots: eslint: 9.35.0(jiti@2.5.1) semver: 7.7.2 - eslint-config-expo@10.0.0(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2): + eslint-config-expo@10.0.0(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2): dependencies: '@typescript-eslint/eslint-plugin': 8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) '@typescript-eslint/parser': 8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0)(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-expo: 1.0.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.35.0(jiti@2.5.1)) @@ -13844,7 +14163,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0)(eslint@9.35.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0)(eslint@9.35.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -13856,7 +14175,7 @@ snapshots: unrs-resolver: 1.11.1 optionalDependencies: eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color @@ -13867,7 +14186,7 @@ snapshots: '@typescript-eslint/parser': 8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0)(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)))(eslint-plugin-import@2.32.0)(eslint@9.35.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color @@ -13907,7 +14226,7 @@ snapshots: lodash: 4.17.21 string-natural-compare: 3.0.1 - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1)): dependencies: '@typescript-eslint/types': 8.43.0 comment-parser: 1.4.1 @@ -13920,7 +14239,7 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color @@ -14006,6 +14325,10 @@ snapshots: eslint: 9.35.0(jiti@2.5.1) eslint-plugin-react-native-globals: 0.1.2 + eslint-plugin-react-refresh@0.4.20(eslint@9.35.0(jiti@2.5.1)): + dependencies: + eslint: 9.35.0(jiti@2.5.1) + eslint-plugin-react@7.37.5(eslint@9.35.0(jiti@2.5.1)): dependencies: array-includes: 3.1.9 @@ -14183,7 +14506,7 @@ snapshots: expo-asset@12.0.8(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: '@expo/image-utils': 0.8.7 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-constants: 18.0.9(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)) react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) @@ -14192,7 +14515,7 @@ snapshots: expo-clipboard@8.0.7(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) @@ -14200,7 +14523,7 @@ snapshots: dependencies: '@expo/config': 12.0.9 '@expo/env': 2.0.7 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) transitivePeerDependencies: - supports-color @@ -14208,11 +14531,11 @@ snapshots: expo-crypto@15.0.7(expo@54.0.8): dependencies: base64-js: 1.5.1 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-dev-client@6.0.12(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-dev-launcher: 6.0.11(expo@54.0.8) expo-dev-menu: 7.0.11(expo@54.0.8) expo-dev-menu-interface: 2.0.0(expo@54.0.8) @@ -14223,7 +14546,7 @@ snapshots: expo-dev-launcher@6.0.11(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-dev-menu: 7.0.11(expo@54.0.8) expo-manifests: 1.0.8(expo@54.0.8) transitivePeerDependencies: @@ -14231,42 +14554,42 @@ snapshots: expo-dev-menu-interface@2.0.0(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-dev-menu@7.0.11(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-dev-menu-interface: 2.0.0(expo@54.0.8) expo-document-picker@14.0.7(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-file-system@19.0.14(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) expo-font@14.0.8(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) fontfaceobserver: 2.3.0 react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) expo-glass-effect@0.1.4(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) expo-haptics@15.0.7(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-image@3.0.8(expo@54.0.8)(react-native-web@0.21.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) optionalDependencies: @@ -14276,7 +14599,7 @@ snapshots: expo-keep-awake@15.0.7(expo@54.0.8)(react@19.1.0): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react: 19.1.0 expo-linking@8.0.8(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): @@ -14292,7 +14615,7 @@ snapshots: expo-manifests@1.0.8(expo@54.0.8): dependencies: '@expo/config': 12.0.8 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-json-utils: 0.15.0 transitivePeerDependencies: - supports-color @@ -14312,20 +14635,20 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) - expo-router@6.0.6(be6ce5c43438e2db372447bf3c6a78fa): + expo-router@6.0.6(795f20997b5e74199ec1f471a585ccc7): dependencies: '@expo/metro-runtime': 6.1.1(expo@54.0.8)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 '@expo/server': 0.7.4 '@radix-ui/react-slot': 1.2.0(@types/react@19.1.12)(react@19.1.0) - '@radix-ui/react-tabs': 1.1.13(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-navigation/bottom-tabs': 7.4.7(@react-navigation/native@7.1.17(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) '@react-navigation/native': 7.1.17(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) '@react-navigation/native-stack': 7.3.26(@react-navigation/native@7.1.17(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) client-only: 0.0.1 debug: 4.4.1 escape-string-regexp: 4.0.0 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-constants: 18.0.9(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)) expo-linking: 8.0.8(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) fast-deep-equal: 3.1.3 @@ -14343,7 +14666,7 @@ snapshots: sf-symbols-typescript: 2.1.0 shallowequal: 1.1.0 use-latest-callback: 0.2.4(react@19.1.0) - vaul: 1.1.2(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) @@ -14357,12 +14680,12 @@ snapshots: expo-secure-store@15.0.7(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) expo-splash-screen@31.0.10(expo@54.0.8): dependencies: '@expo/prebuild-config': 54.0.3(expo@54.0.8) - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - supports-color @@ -14374,7 +14697,7 @@ snapshots: expo-symbols@1.0.7(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) sf-symbols-typescript: 2.1.0 @@ -14382,7 +14705,7 @@ snapshots: dependencies: '@react-native/normalize-colors': 0.81.4 debug: 4.4.1 - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) optionalDependencies: react-native-web: 0.21.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -14391,9 +14714,9 @@ snapshots: expo-updates-interface@2.0.0(expo@54.0.8): dependencies: - expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + expo: 54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) - expo@54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): + expo@54.0.8(@babel/core@7.28.3)(@expo/metro-runtime@6.1.1)(expo-router@6.0.6)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: '@babel/runtime': 7.28.3 '@expo/cli': 54.0.6(expo-router@6.0.6)(expo@54.0.8)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)) @@ -14420,6 +14743,7 @@ snapshots: whatwg-url-without-unicode: 8.0.0-3 optionalDependencies: '@expo/metro-runtime': 6.1.1(expo@54.0.8)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) + react-native-webview: 13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - '@babel/core' - '@modelcontextprotocol/sdk' @@ -14658,7 +14982,7 @@ snapshots: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -14750,6 +15074,8 @@ snapshots: globals@16.3.0: {} + globals@16.4.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -15285,7 +15611,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.28.3 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.4 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.7.2 @@ -15301,7 +15627,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.30 - debug: 4.4.1 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -16205,7 +16531,7 @@ snapshots: metro-source-map@0.83.1: dependencies: '@babel/traverse': 7.28.3 - '@babel/traverse--for-generate-function-map': '@babel/traverse@7.28.3' + '@babel/traverse--for-generate-function-map': '@babel/traverse@7.28.4' '@babel/types': 7.28.2 flow-enums-runtime: 0.0.6 invariant: 2.2.4 @@ -16478,7 +16804,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -17389,6 +17715,13 @@ snapshots: transitivePeerDependencies: - encoding + react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): + dependencies: + escape-string-regexp: 4.0.0 + invariant: 2.2.4 + react: 19.1.0 + react-native: 0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0) + react-native-worklets@0.5.1(@babel/core@7.28.3)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0): dependencies: '@babel/core': 7.28.3 @@ -17457,6 +17790,8 @@ snapshots: react-refresh@0.14.2: {} + react-refresh@0.17.0: {} + react-remove-scroll-bar@2.3.8(@types/react@19.1.12)(react@19.1.0): dependencies: react: 19.1.0 @@ -17486,6 +17821,9 @@ snapshots: react@19.1.0: {} + react@19.1.1: + optional: true + read-package-up@11.0.0: dependencies: find-up-simple: 1.0.1 @@ -17772,31 +18110,31 @@ snapshots: dependencies: glob: 7.2.3 - rollup@4.50.1: + rollup@4.50.2: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.50.1 - '@rollup/rollup-android-arm64': 4.50.1 - '@rollup/rollup-darwin-arm64': 4.50.1 - '@rollup/rollup-darwin-x64': 4.50.1 - '@rollup/rollup-freebsd-arm64': 4.50.1 - '@rollup/rollup-freebsd-x64': 4.50.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.50.1 - '@rollup/rollup-linux-arm-musleabihf': 4.50.1 - '@rollup/rollup-linux-arm64-gnu': 4.50.1 - '@rollup/rollup-linux-arm64-musl': 4.50.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.50.1 - '@rollup/rollup-linux-ppc64-gnu': 4.50.1 - '@rollup/rollup-linux-riscv64-gnu': 4.50.1 - '@rollup/rollup-linux-riscv64-musl': 4.50.1 - '@rollup/rollup-linux-s390x-gnu': 4.50.1 - '@rollup/rollup-linux-x64-gnu': 4.50.1 - '@rollup/rollup-linux-x64-musl': 4.50.1 - '@rollup/rollup-openharmony-arm64': 4.50.1 - '@rollup/rollup-win32-arm64-msvc': 4.50.1 - '@rollup/rollup-win32-ia32-msvc': 4.50.1 - '@rollup/rollup-win32-x64-msvc': 4.50.1 + '@rollup/rollup-android-arm-eabi': 4.50.2 + '@rollup/rollup-android-arm64': 4.50.2 + '@rollup/rollup-darwin-arm64': 4.50.2 + '@rollup/rollup-darwin-x64': 4.50.2 + '@rollup/rollup-freebsd-arm64': 4.50.2 + '@rollup/rollup-freebsd-x64': 4.50.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.2 + '@rollup/rollup-linux-arm-musleabihf': 4.50.2 + '@rollup/rollup-linux-arm64-gnu': 4.50.2 + '@rollup/rollup-linux-arm64-musl': 4.50.2 + '@rollup/rollup-linux-loong64-gnu': 4.50.2 + '@rollup/rollup-linux-ppc64-gnu': 4.50.2 + '@rollup/rollup-linux-riscv64-gnu': 4.50.2 + '@rollup/rollup-linux-riscv64-musl': 4.50.2 + '@rollup/rollup-linux-s390x-gnu': 4.50.2 + '@rollup/rollup-linux-x64-gnu': 4.50.2 + '@rollup/rollup-linux-x64-musl': 4.50.2 + '@rollup/rollup-openharmony-arm64': 4.50.2 + '@rollup/rollup-win32-arm64-msvc': 4.50.2 + '@rollup/rollup-win32-ia32-msvc': 4.50.2 + '@rollup/rollup-win32-x64-msvc': 4.50.2 fsevents: 2.3.3 run-applescript@7.1.0: {} @@ -18310,7 +18648,9 @@ snapshots: transitivePeerDependencies: - typescript - tailwindcss@4.1.13: {} + tailwindcss@4.1.10: {} + + tailwindcss@4.1.9: {} tapable@2.2.3: {} @@ -18366,6 +18706,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -18515,6 +18860,17 @@ snapshots: transitivePeerDependencies: - supports-color + typescript-eslint@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2): + dependencies: + '@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + eslint: 9.35.0(jiti@2.5.1) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + typescript@5.9.2: {} ua-parser-js@1.0.41: {} @@ -18733,9 +19089,9 @@ snapshots: vary@1.1.2: {} - vaul@1.1.2(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + vaul@1.1.2(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@radix-ui/react-dialog': 1.1.15(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) transitivePeerDependencies: @@ -18763,8 +19119,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.50.1 - tinyglobby: 0.2.14 + rollup: 4.50.2 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.3.0 fsevents: 2.3.3 @@ -18878,8 +19234,8 @@ snapshots: with@7.0.2: dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 assert-never: 1.4.0 babel-walk: 3.0.0-canary-5 @@ -19021,6 +19377,6 @@ snapshots: zod@3.25.76: {} - zod@4.1.8: {} + zod@4.1.9: {} zwitch@2.0.4: {} From b3eb42c34852359afc79c6c150aa5a9d569b560a Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Wed, 17 Sep 2025 00:33:36 -0400 Subject: [PATCH 02/12] some things working --- apps/mobile/package.json | 1 + apps/mobile/src/app/(tabs)/shell/_layout.tsx | 1 + apps/mobile/src/app/(tabs)/shell/detail.tsx | 120 +++-- .../eslint.config.js | 48 +- .../index.html | 20 +- .../package.json | 14 +- .../prettier.config.mjs | 14 + .../src/App.tsx | 33 -- .../src/main.tsx | 34 +- .../src/vite-env.d.ts | 8 + .../tsconfig.app.json | 46 +- .../tsconfig.json | 10 +- .../tsconfig.node.json | 42 +- .../turbo.json | 7 + .../vite.config.ts | 8 +- .../react-native-xtermjs-webview/README.md | 104 ++-- .../eslint.config.js | 48 +- .../react-native-xtermjs-webview/package.json | 17 +- .../prettier.config.mjs | 14 + .../src/index.tsx | 49 +- .../tsconfig.app.json | 46 +- .../tsconfig.json | 10 +- .../tsconfig.node.json | 42 +- .../react-native-xtermjs-webview/turbo.json | 7 + .../vite.config.ts | 54 +- pnpm-lock.yaml | 486 +++++++++++++++++- 26 files changed, 954 insertions(+), 329 deletions(-) create mode 100644 packages/react-native-xtermjs-webview-internal/prettier.config.mjs delete mode 100644 packages/react-native-xtermjs-webview-internal/src/App.tsx create mode 100644 packages/react-native-xtermjs-webview-internal/turbo.json create mode 100644 packages/react-native-xtermjs-webview/prettier.config.mjs create mode 100644 packages/react-native-xtermjs-webview/turbo.json diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 9d82d26..2d2d7ad 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -48,6 +48,7 @@ "expo-haptics": "~15.0.7", "expo-image": "~3.0.8", "expo-linking": "~8.0.8", + "@fressh/react-native-xtermjs-webview": "workspace:*", "expo-router": "6.0.6", "expo-secure-store": "~15.0.7", "expo-splash-screen": "~31.0.10", diff --git a/apps/mobile/src/app/(tabs)/shell/_layout.tsx b/apps/mobile/src/app/(tabs)/shell/_layout.tsx index 562de01..2cd6b01 100644 --- a/apps/mobile/src/app/(tabs)/shell/_layout.tsx +++ b/apps/mobile/src/app/(tabs)/shell/_layout.tsx @@ -10,6 +10,7 @@ export default function TabsShellStack() { headerBlurEffect: undefined, headerTransparent: false, headerStyle: { backgroundColor: theme.colors.surface }, + headerTintColor: theme.colors.textPrimary, headerTitleStyle: { color: theme.colors.textPrimary, }, diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx index 39d3711..8568532 100644 --- a/apps/mobile/src/app/(tabs)/shell/detail.tsx +++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx @@ -1,5 +1,10 @@ import { Ionicons } from '@expo/vector-icons'; import { RnRussh } from '@fressh/react-native-uniffi-russh'; +import { + XtermJsWebView, + type XtermWebViewHandle, +} from '@fressh/react-native-xtermjs-webview'; + import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; import React, { useEffect, useRef, useState } from 'react'; import { @@ -13,11 +18,15 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { useTheme } from '@/lib/theme'; +const renderer: 'xtermjs' | 'rn-text' = 'xtermjs'; +const decoder = new TextDecoder('utf-8'); + export default function TabsShellDetail() { return ; } function ShellDetail() { + const xtermWebViewRef = useRef(null); const { connectionId, channelId } = useLocalSearchParams<{ connectionId?: string; channelId?: string; @@ -38,10 +47,10 @@ function ShellDetail() { useEffect(() => { if (!connection) return; - const decoder = new TextDecoder('utf-8'); const listenerId = connection.addChannelListener((data: ArrayBuffer) => { try { const bytes = new Uint8Array(data); + xtermWebViewRef.current?.write(bytes); const chunk = decoder.decode(bytes); setShellData((prev) => prev + chunk); } catch (e) { @@ -85,51 +94,76 @@ function ShellDetail() { { backgroundColor: theme.colors.background }, ]} > - - - + {renderer === 'xtermjs' ? ( + { + // document.body.style.backgroundColor = '${theme.colors.background}'; + // document.body.style.color = '${theme.colors.textPrimary}'; + // document.body.style.fontSize = '80px'; + // const termDiv = document.getElementById('terminal'); + // termDiv.style.backgroundColor = '${theme.colors.background}'; + // termDiv.style.color = '${theme.colors.textPrimary}'; + // window.terminal.options.fontSize = 50; + // }, 50); + // `} + onMessage={(event) => { + console.log('onMessage', event.nativeEvent.data); + }} + /> + ) : ( + - {shellData || 'Connected. Output will appear here...'} - - - - { - await shell?.sendData( - Uint8Array.from(new TextEncoder().encode(command + '\n')).buffer, - ); - }} - /> + + + {shellData || 'Connected. Output will appear here...'} + + + + )} + { + await shell?.sendData( + Uint8Array.from(new TextEncoder().encode(command + '\n')) + .buffer, + ); + }} + /> + ); diff --git a/packages/react-native-xtermjs-webview-internal/eslint.config.js b/packages/react-native-xtermjs-webview-internal/eslint.config.js index d94e7de..a1e9c7c 100644 --- a/packages/react-native-xtermjs-webview-internal/eslint.config.js +++ b/packages/react-native-xtermjs-webview-internal/eslint.config.js @@ -1,23 +1,27 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { globalIgnores } from 'eslint/config' +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import { globalIgnores, defineConfig } from 'eslint/config'; -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]); diff --git a/packages/react-native-xtermjs-webview-internal/index.html b/packages/react-native-xtermjs-webview-internal/index.html index e4b78ea..a7be769 100644 --- a/packages/react-native-xtermjs-webview-internal/index.html +++ b/packages/react-native-xtermjs-webview-internal/index.html @@ -1,13 +1,13 @@ - - - - - Vite + React + TS - - -
- - + + + + +
+ + diff --git a/packages/react-native-xtermjs-webview-internal/package.json b/packages/react-native-xtermjs-webview-internal/package.json index 3b808b5..72f7db1 100644 --- a/packages/react-native-xtermjs-webview-internal/package.json +++ b/packages/react-native-xtermjs-webview-internal/package.json @@ -1,5 +1,5 @@ { - "name": "react-native-xtermjs-webview-internal", + "name": "@fressh/react-native-xtermjs-webview-internal", "private": true, "version": "0.0.0", "type": "module", @@ -9,7 +9,11 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "eslint .", + "fmt:check": "cross-env SORT_IMPORTS=true prettier --check .", + "fmt": "cross-env SORT_IMPORTS=true prettier --write .", + "eslint:check": "eslint . --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint --fix --report-unused-disable-directives --max-warnings 0 .", + "typecheck": "tsc --noEmit", "preview": "vite preview" }, "dependencies": { @@ -23,11 +27,15 @@ "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.2", "eslint": "^9.35.0", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.2.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.4.0", + "@epic-web/config": "^1.21.3", "typescript": "~5.9.2", "typescript-eslint": "^8.44.0", - "vite": "6.3.6" + "vite": "6.3.6", + "vite-plugin-singlefile": "^2.3.0" } } diff --git a/packages/react-native-xtermjs-webview-internal/prettier.config.mjs b/packages/react-native-xtermjs-webview-internal/prettier.config.mjs new file mode 100644 index 0000000..7b0e9e2 --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/prettier.config.mjs @@ -0,0 +1,14 @@ +import epicConfig from '@epic-web/config/prettier'; +// Sometimes this plugin can remove imports that are being edited. +// As a workaround we will only use this in the cli. (pnpm run fmt) +const sortImports = process.env.SORT_IMPORTS === 'true-never'; + +/** @type {import("prettier").Options} */ +export default { + ...epicConfig, + semi: true, + plugins: [ + ...(sortImports ? ['prettier-plugin-organize-imports'] : []), + ...(epicConfig.plugins || []), + ], +}; diff --git a/packages/react-native-xtermjs-webview-internal/src/App.tsx b/packages/react-native-xtermjs-webview-internal/src/App.tsx deleted file mode 100644 index 5cc38c7..0000000 --- a/packages/react-native-xtermjs-webview-internal/src/App.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { Terminal } from '@xterm/xterm'; -import '@xterm/xterm/css/xterm.css'; - -export function App() { - const [count, setCount] = useState(0); - const terminalRef = useRef(null); - - useEffect(() => { - if (!terminalRef.current) return; - const terminal = new Terminal(); - terminal.open(terminalRef.current); - terminal.write('Hello from Xterm.js!'); - }, []); - - return ( - <> -

Xterm.js

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

-
- - ); -} diff --git a/packages/react-native-xtermjs-webview-internal/src/main.tsx b/packages/react-native-xtermjs-webview-internal/src/main.tsx index 34430f2..cfc87a8 100644 --- a/packages/react-native-xtermjs-webview-internal/src/main.tsx +++ b/packages/react-native-xtermjs-webview-internal/src/main.tsx @@ -1,9 +1,27 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App.tsx'; +import { Terminal } from '@xterm/xterm'; +import '@xterm/xterm/css/xterm.css'; -createRoot(document.getElementById('root')!).render( - - - , -); +const decoder = new TextDecoder('utf-8'); + +const terminal = new Terminal(); +terminal.open(document.getElementById('terminal')!); +terminal.write('Hello from Xterm.js!'); +window.terminal = terminal; +const postMessage = (arg: string) => { + window.ReactNativeWebView?.postMessage?.(arg); +}; +setTimeout(() => { + postMessage('DEBUG: set timeout'); +}, 1000); +function terminalWriteBase64(base64Data: string) { + try { + postMessage(`DEBUG: terminalWriteBase64 ${base64Data}`); + const data = new Uint8Array(Buffer.from(base64Data, 'base64')); + postMessage(`DEBUG: terminalWriteBase64 decoded ${decoder.decode(data)}`); + + terminal.write(data); + } catch (e) { + postMessage(`DEBUG: terminalWriteBase64 error ${e}`); + } +} +window.terminalWriteBase64 = terminalWriteBase64; diff --git a/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts b/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts index 11f02fe..4e37827 100644 --- a/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts +++ b/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts @@ -1 +1,9 @@ /// + +interface Window { + terminal?: Terminal; + terminalWriteBase64?: (data: string) => void; + ReactNativeWebView?: { + postMessage?: (data: string) => void; + }; +} diff --git a/packages/react-native-xtermjs-webview-internal/tsconfig.app.json b/packages/react-native-xtermjs-webview-internal/tsconfig.app.json index 227a6c6..213a1d9 100644 --- a/packages/react-native-xtermjs-webview-internal/tsconfig.app.json +++ b/packages/react-native-xtermjs-webview-internal/tsconfig.app.json @@ -1,27 +1,27 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src"] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] } diff --git a/packages/react-native-xtermjs-webview-internal/tsconfig.json b/packages/react-native-xtermjs-webview-internal/tsconfig.json index 1ffef60..fb12418 100644 --- a/packages/react-native-xtermjs-webview-internal/tsconfig.json +++ b/packages/react-native-xtermjs-webview-internal/tsconfig.json @@ -1,7 +1,7 @@ { - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] } diff --git a/packages/react-native-xtermjs-webview-internal/tsconfig.node.json b/packages/react-native-xtermjs-webview-internal/tsconfig.node.json index f85a399..1a5ed45 100644 --- a/packages/react-native-xtermjs-webview-internal/tsconfig.node.json +++ b/packages/react-native-xtermjs-webview-internal/tsconfig.node.json @@ -1,25 +1,25 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2023", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] } diff --git a/packages/react-native-xtermjs-webview-internal/turbo.json b/packages/react-native-xtermjs-webview-internal/turbo.json new file mode 100644 index 0000000..335d504 --- /dev/null +++ b/packages/react-native-xtermjs-webview-internal/turbo.json @@ -0,0 +1,7 @@ +{ + "extends": ["//"], + "tasks": { + "lint": {}, + "lint:check": {} + } +} diff --git a/packages/react-native-xtermjs-webview-internal/vite.config.ts b/packages/react-native-xtermjs-webview-internal/vite.config.ts index 8b0f57b..208116f 100644 --- a/packages/react-native-xtermjs-webview-internal/vite.config.ts +++ b/packages/react-native-xtermjs-webview-internal/vite.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite'; +import { viteSingleFile } from 'vite-plugin-singlefile'; // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [viteSingleFile()], +}); diff --git a/packages/react-native-xtermjs-webview/README.md b/packages/react-native-xtermjs-webview/README.md index 7959ce4..a03c0c5 100644 --- a/packages/react-native-xtermjs-webview/README.md +++ b/packages/react-native-xtermjs-webview/README.md @@ -1,69 +1,77 @@ # React + TypeScript + Vite -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +This template provides a minimal setup to get React working in Vite with HMR and +some ESLint rules. Currently, two official plugins are available: -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) + uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) + uses [SWC](https://swc.rs/) for Fast Refresh ## Expanding the ESLint configuration -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +If you are developing a production application, we recommend updating the +configuration to enable type-aware lint rules: ```js export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... - // Remove tseslint.configs.recommended and replace with this - ...tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - ...tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - ...tseslint.configs.stylisticTypeChecked, + // Remove tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]); ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: +You can also install +[eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) +and +[eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) +for React-specific lint rules: ```js // eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +import reactX from 'eslint-plugin-react-x'; +import reactDom from 'eslint-plugin-react-dom'; export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]); ``` diff --git a/packages/react-native-xtermjs-webview/eslint.config.js b/packages/react-native-xtermjs-webview/eslint.config.js index d94e7de..a1e9c7c 100644 --- a/packages/react-native-xtermjs-webview/eslint.config.js +++ b/packages/react-native-xtermjs-webview/eslint.config.js @@ -1,23 +1,27 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { globalIgnores } from 'eslint/config' +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import { globalIgnores, defineConfig } from 'eslint/config'; -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]); diff --git a/packages/react-native-xtermjs-webview/package.json b/packages/react-native-xtermjs-webview/package.json index 8167fa4..5c594bc 100644 --- a/packages/react-native-xtermjs-webview/package.json +++ b/packages/react-native-xtermjs-webview/package.json @@ -1,5 +1,5 @@ { - "name": "react-native-xtermjs-webview", + "name": "@fressh/react-native-xtermjs-webview", "private": true, "version": "0.0.0", "type": "module", @@ -9,11 +9,16 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "eslint .", + "fmt:check": "cross-env SORT_IMPORTS=true prettier --check .", + "fmt": "cross-env SORT_IMPORTS=true prettier --write .", + "eslint:check": "eslint . --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint --fix --report-unused-disable-directives --max-warnings 0 .", + "typecheck": "tsc --noEmit", "preview": "vite preview" }, "dependencies": { - "react-native-xtermjs-webview-internal": "workspace:*" + "@fressh/react-native-xtermjs-webview-internal": "workspace:*", + "js-base64": "^3.7.8" }, "peerDependencies": { "react": "19.1.0", @@ -21,6 +26,7 @@ "react-native-webview": "13.15.0" }, "devDependencies": { + "@epic-web/config": "^1.21.3", "@eslint/js": "^9.35.0", "@types/react": "~19.1.12", "@types/react-dom": "^19.1.7", @@ -29,11 +35,14 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.4.0", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.2.0", "react": "19.1.0", "react-dom": "19.1.0", "react-native-webview": "13.15.0", "typescript": "~5.9.2", "typescript-eslint": "^8.44.0", - "vite": "6.3.6" + "vite": "6.3.6", + "vite-plugin-dts": "^4.5.4" } } diff --git a/packages/react-native-xtermjs-webview/prettier.config.mjs b/packages/react-native-xtermjs-webview/prettier.config.mjs new file mode 100644 index 0000000..7b0e9e2 --- /dev/null +++ b/packages/react-native-xtermjs-webview/prettier.config.mjs @@ -0,0 +1,14 @@ +import epicConfig from '@epic-web/config/prettier'; +// Sometimes this plugin can remove imports that are being edited. +// As a workaround we will only use this in the cli. (pnpm run fmt) +const sortImports = process.env.SORT_IMPORTS === 'true-never'; + +/** @type {import("prettier").Options} */ +export default { + ...epicConfig, + semi: true, + plugins: [ + ...(sortImports ? ['prettier-plugin-organize-imports'] : []), + ...(epicConfig.plugins || []), + ], +}; diff --git a/packages/react-native-xtermjs-webview/src/index.tsx b/packages/react-native-xtermjs-webview/src/index.tsx index 4033fab..f2e9153 100644 --- a/packages/react-native-xtermjs-webview/src/index.tsx +++ b/packages/react-native-xtermjs-webview/src/index.tsx @@ -1,6 +1,49 @@ +import { useImperativeHandle, useRef, type ComponentProps } from 'react'; import { WebView } from 'react-native-webview'; -import htmlString from 'react-native-xtermjs-webview-internal/dist/assets/index.html?raw'; +import htmlString from '@fressh/react-native-xtermjs-webview-internal/dist/index.html?raw'; +import { Base64 } from 'js-base64'; -export function XtermJsWebView() { - return ; +type StrictOmit = Omit; + +export type XtermWebViewHandle = { + write: (data: Uint8Array) => void; +}; +const decoder = new TextDecoder('utf-8'); + +export function XtermJsWebView({ + ref, + ...props +}: StrictOmit, 'source' | 'originWhitelist'> & { + ref: React.RefObject; +}) { + const webViewRef = useRef(null); + + useImperativeHandle(ref, () => { + return { + write: (data) => { + const base64Data = Base64.fromUint8Array(data); + console.log('writing rn side', { + base64Data, + dataLength: data.length, + }); + + console.log( + 'try to decode', + decoder.decode(Base64.toUint8Array(base64Data)), + ); + webViewRef.current?.injectJavaScript(` + window?.terminalWriteBase64('${base64Data}'); + `); + }, + }; + }); + + return ( + + ); } diff --git a/packages/react-native-xtermjs-webview/tsconfig.app.json b/packages/react-native-xtermjs-webview/tsconfig.app.json index 227a6c6..213a1d9 100644 --- a/packages/react-native-xtermjs-webview/tsconfig.app.json +++ b/packages/react-native-xtermjs-webview/tsconfig.app.json @@ -1,27 +1,27 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src"] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] } diff --git a/packages/react-native-xtermjs-webview/tsconfig.json b/packages/react-native-xtermjs-webview/tsconfig.json index 1ffef60..fb12418 100644 --- a/packages/react-native-xtermjs-webview/tsconfig.json +++ b/packages/react-native-xtermjs-webview/tsconfig.json @@ -1,7 +1,7 @@ { - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] } diff --git a/packages/react-native-xtermjs-webview/tsconfig.node.json b/packages/react-native-xtermjs-webview/tsconfig.node.json index f85a399..1a5ed45 100644 --- a/packages/react-native-xtermjs-webview/tsconfig.node.json +++ b/packages/react-native-xtermjs-webview/tsconfig.node.json @@ -1,25 +1,25 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2023", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] } diff --git a/packages/react-native-xtermjs-webview/turbo.json b/packages/react-native-xtermjs-webview/turbo.json new file mode 100644 index 0000000..335d504 --- /dev/null +++ b/packages/react-native-xtermjs-webview/turbo.json @@ -0,0 +1,7 @@ +{ + "extends": ["//"], + "tasks": { + "lint": {}, + "lint:check": {} + } +} diff --git a/packages/react-native-xtermjs-webview/vite.config.ts b/packages/react-native-xtermjs-webview/vite.config.ts index 4f0fd03..f0cb13b 100644 --- a/packages/react-native-xtermjs-webview/vite.config.ts +++ b/packages/react-native-xtermjs-webview/vite.config.ts @@ -1,21 +1,37 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import packageJson from './package.json' -import { resolve } from 'path' - - +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; +import dts from 'vite-plugin-dts'; export default defineConfig({ - plugins: [react()], - build: { - rollupOptions: { - external: Object.keys(packageJson.peerDependencies || {}), - }, - lib: { - entry: resolve(__dirname, 'src/index.tsx'), - name: 'ReactNativeXtermJsWebView', - formats: ['es'], - fileName: 'react-native-xtermjs-webview', - } - }, -}) + plugins: [ + react(), + dts({ + tsconfigPath: './tsconfig.app.json', + // This makes dist/ look nice but breaks Cmd + Click + rollupTypes: false, + // We need this or the types defined in package.json will be missing + // If rollupTypes is true, this is forced true + insertTypesEntry: true, + compilerOptions: { + // This allows Cmd + Click from different packages in the monorepo + declarationMap: true, + }, + }), + ], + build: { + sourcemap: true, + rollupOptions: { + external: ['react', 'react/jsx-runtime', 'react-native-webview'], + // external: () => { + // fs.writeFileSync('dep.log', `${dep}\n`, { flag: 'a' }); + // return false; + // } + }, + lib: { + entry: resolve(__dirname, 'src/index.tsx'), + formats: ['es'], + fileName: () => 'index.js', + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3b0f68..fa308fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,9 @@ importers: '@fressh/react-native-uniffi-russh': specifier: workspace:* version: link:../../packages/react-native-uniffi-russh + '@fressh/react-native-xtermjs-webview': + specifier: workspace:* + version: link:../../packages/react-native-xtermjs-webview '@react-native-segmented-control/segmented-control': specifier: 2.5.7 version: 2.5.7(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) @@ -318,10 +321,16 @@ importers: packages/react-native-xtermjs-webview: dependencies: - react-native-xtermjs-webview-internal: + '@fressh/react-native-xtermjs-webview-internal': specifier: workspace:* version: link:../react-native-xtermjs-webview-internal + js-base64: + specifier: ^3.7.8 + version: 3.7.8 devDependencies: + '@epic-web/config': + specifier: ^1.21.3 + version: 1.21.3(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2) '@eslint/js': specifier: ^9.35.0 version: 9.35.0 @@ -346,6 +355,12 @@ importers: globals: specifier: ^16.4.0 version: 16.4.0 + prettier: + specifier: ^3.6.2 + version: 3.6.2 + prettier-plugin-organize-imports: + specifier: ^4.2.0 + version: 4.2.0(prettier@3.6.2)(typescript@5.9.2) react: specifier: 19.1.0 version: 19.1.0 @@ -364,6 +379,9 @@ importers: vite: specifier: 6.3.6 version: 6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + vite-plugin-dts: + specifier: ^4.5.4 + version: 4.5.4(@types/node@24.3.0)(rollup@4.50.2)(typescript@5.9.2)(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) packages/react-native-xtermjs-webview-internal: dependencies: @@ -377,6 +395,9 @@ importers: specifier: 19.1.0 version: 19.1.0(react@19.1.0) devDependencies: + '@epic-web/config': + specifier: ^1.21.3 + version: 1.21.3(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2) '@eslint/js': specifier: ^9.35.0 version: 9.35.0 @@ -401,6 +422,12 @@ importers: globals: specifier: ^16.4.0 version: 16.4.0 + prettier: + specifier: ^3.6.2 + version: 3.6.2 + prettier-plugin-organize-imports: + specifier: ^4.2.0 + version: 4.2.0(prettier@3.6.2)(typescript@5.9.2) typescript: specifier: ~5.9.2 version: 5.9.2 @@ -410,6 +437,9 @@ importers: vite: specifier: 6.3.6 version: 6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + vite-plugin-singlefile: + specifier: ^2.3.0 + version: 2.3.0(rollup@4.50.2)(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) packages: @@ -1836,6 +1866,14 @@ packages: '@types/node': optional: true + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1995,6 +2033,19 @@ packages: engines: {node: '>=18'} hasBin: true + '@microsoft/api-extractor-model@7.30.7': + resolution: {integrity: sha512-TBbmSI2/BHpfR9YhQA7nH0nqVmGgJ0xH0Ex4D99/qBDAUpnhA2oikGmdXanbw9AWWY/ExBYIpkmY8dBHdla3YQ==} + + '@microsoft/api-extractor@7.52.13': + resolution: {integrity: sha512-K6/bBt8zZfn9yc06gNvA+/NlBGJC/iJlObpdufXHEJtqcD4Dln4ITCLZpwP3DNZ5NyBFeTkKdv596go3V72qlA==} + hasBin: true + + '@microsoft/tsdoc-config@0.17.1': + resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==} + + '@microsoft/tsdoc@0.15.1': + resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -2725,6 +2776,28 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@rushstack/node-core-library@5.14.0': + resolution: {integrity: sha512-eRong84/rwQUlATGFW3TMTYVyqL1vfW9Lf10PH+mVGfIb9HzU3h5AASNIw+axnBLjnD0n3rT5uQBwu9fvzATrg==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rig-package@0.5.3': + resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==} + + '@rushstack/terminal@0.16.0': + resolution: {integrity: sha512-WEvNuKkoR1PXorr9SxO0dqFdSp1BA+xzDrIm/Bwlc5YHg2FFg6oS+uCTYjerOhFuqCW+A3vKBm6EmKWSHfgx/A==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@5.0.3': + resolution: {integrity: sha512-bgPhQEqLVv/2hwKLYv/XvsTWNZ9B/+X1zJ7WgQE9rO5oiLzrOZvkIW4pk13yOQBhHyjcND5qMOa6p83t+Z66iQ==} + '@shikijs/core@3.12.2': resolution: {integrity: sha512-L1Safnhra3tX/oJK5kYHaWmLEBJi1irASwewzY3taX5ibyXyMkkSDZlq01qigjryOBwrXSdFgTiZ3ryzSNeu7Q==} @@ -2915,6 +2988,9 @@ packages: '@tybys/wasm-util@0.10.0': resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -3400,9 +3476,38 @@ packages: vitest: optional: true + '@volar/language-core@2.4.23': + resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} + + '@volar/source-map@2.4.23': + resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} + + '@volar/typescript@2.4.23': + resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} + '@vscode/sudo-prompt@9.3.1': resolution: {integrity: sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==} + '@vue/compiler-core@3.5.21': + resolution: {integrity: sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==} + + '@vue/compiler-dom@3.5.21': + resolution: {integrity: sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/shared@3.5.21': + resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==} + '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} @@ -3453,9 +3558,34 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + + ajv@8.13.0: + resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + + alien-signals@0.4.14: + resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + anser@1.4.10: resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} @@ -4071,6 +4201,9 @@ packages: compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -4086,6 +4219,9 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} @@ -4260,6 +4396,9 @@ packages: dayjs@1.11.18: resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -4505,6 +4644,10 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -5464,6 +5607,10 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + hermes-estree@0.28.1: resolution: {integrity: sha512-w3nxl/RGM7LBae0v8LH2o36+8VqwOZGv9rX1wyoWT6YaKZLqpJZ0YQ5P0LVr3tuRpf7vCx0iIG4i/VmBJejxTQ==} @@ -5561,6 +5708,10 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} @@ -6065,9 +6216,15 @@ packages: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-stringify@1.0.2: resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} @@ -6114,6 +6271,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -6161,6 +6321,9 @@ packages: '@types/node': '>=18' typescript: '>=5.0.4' + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + lan-network@0.1.7: resolution: {integrity: sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==} hasBin: true @@ -6257,6 +6420,10 @@ packages: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -6321,6 +6488,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} @@ -6610,6 +6781,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -6642,6 +6817,9 @@ packages: engines: {node: '>=10'} hasBin: true + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -6652,6 +6830,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -7006,6 +7187,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -7089,6 +7273,9 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} @@ -7317,6 +7504,9 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -7760,6 +7950,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -7993,6 +8188,10 @@ packages: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -8363,6 +8562,11 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -8641,6 +8845,22 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-plugin-dts@4.5.4: + resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite-plugin-singlefile@2.3.0: + resolution: {integrity: sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A==} + engines: {node: '>18.0.0'} + peerDependencies: + rollup: ^4.44.1 + vite: ^5.4.11 || ^6.0.0 || ^7.0.0 + vite@6.3.6: resolution: {integrity: sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -8696,6 +8916,9 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -8881,6 +9104,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -9121,7 +9347,7 @@ snapshots: '@babel/core': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.1 + debug: 4.4.3 lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -10749,6 +10975,12 @@ snapshots: optionalDependencies: '@types/node': 24.3.0 + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -11067,6 +11299,41 @@ snapshots: - encoding - supports-color + '@microsoft/api-extractor-model@7.30.7(@types/node@24.3.0)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@24.3.0) + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.52.13(@types/node@24.3.0)': + dependencies: + '@microsoft/api-extractor-model': 7.30.7(@types/node@24.3.0) + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@24.3.0) + '@rushstack/rig-package': 0.5.3 + '@rushstack/terminal': 0.16.0(@types/node@24.3.0) + '@rushstack/ts-command-line': 5.0.3(@types/node@24.3.0) + lodash: 4.17.21 + minimatch: 10.0.3 + resolve: 1.22.10 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/tsdoc-config@0.17.1': + dependencies: + '@microsoft/tsdoc': 0.15.1 + ajv: 8.12.0 + jju: 1.4.0 + resolve: 1.22.10 + + '@microsoft/tsdoc@0.15.1': {} + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.5.0 @@ -11934,6 +12201,40 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@rushstack/node-core-library@5.14.0(@types/node@24.3.0)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 11.3.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.10 + semver: 7.5.4 + optionalDependencies: + '@types/node': 24.3.0 + + '@rushstack/rig-package@0.5.3': + dependencies: + resolve: 1.22.10 + strip-json-comments: 3.1.1 + + '@rushstack/terminal@0.16.0(@types/node@24.3.0)': + dependencies: + '@rushstack/node-core-library': 5.14.0(@types/node@24.3.0) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 24.3.0 + + '@rushstack/ts-command-line@5.0.3(@types/node@24.3.0)': + dependencies: + '@rushstack/terminal': 0.16.0(@types/node@24.3.0) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + '@shikijs/core@3.12.2': dependencies: '@shikijs/types': 3.12.2 @@ -12116,6 +12417,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/argparse@1.0.38': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.3 @@ -12311,7 +12614,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2) '@typescript-eslint/types': 8.43.0 - debug: 4.4.1 + debug: 4.4.3 typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -12320,7 +12623,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.2) '@typescript-eslint/types': 8.44.0 - debug: 4.4.1 + debug: 4.4.3 typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -12366,7 +12669,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) '@typescript-eslint/utils': 7.18.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) - debug: 4.4.1 + debug: 4.4.3 eslint: 9.35.0(jiti@2.5.1) ts-api-utils: 1.4.3(typescript@5.9.2) optionalDependencies: @@ -12379,7 +12682,7 @@ snapshots: '@typescript-eslint/types': 8.41.0 '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2) '@typescript-eslint/utils': 8.41.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) - debug: 4.4.1 + debug: 4.4.3 eslint: 9.35.0(jiti@2.5.1) ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 @@ -12391,7 +12694,7 @@ snapshots: '@typescript-eslint/types': 8.44.0 '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) - debug: 4.4.1 + debug: 4.4.3 eslint: 9.35.0(jiti@2.5.1) ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 @@ -12426,7 +12729,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1 + debug: 4.4.3 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -12676,8 +12979,53 @@ snapshots: transitivePeerDependencies: - supports-color + '@volar/language-core@2.4.23': + dependencies: + '@volar/source-map': 2.4.23 + + '@volar/source-map@2.4.23': {} + + '@volar/typescript@2.4.23': + dependencies: + '@volar/language-core': 2.4.23 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + '@vscode/sudo-prompt@9.3.1': {} + '@vue/compiler-core@3.5.21': + dependencies: + '@babel/parser': 7.28.4 + '@vue/shared': 3.5.21 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.21': + dependencies: + '@vue/compiler-core': 3.5.21 + '@vue/shared': 3.5.21 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.0(typescript@5.9.2)': + dependencies: + '@volar/language-core': 2.4.23 + '@vue/compiler-dom': 3.5.21 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.21 + alien-signals: 0.4.14 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.2 + + '@vue/shared@3.5.21': {} + '@xmldom/xmldom@0.8.11': {} '@xterm/xterm@5.5.0': {} @@ -12714,6 +13062,14 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ajv-draft-04@1.0.0(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + + ajv-formats@3.0.1(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -12721,6 +13077,22 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.12.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ajv@8.13.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + alien-signals@0.4.14: {} + anser@1.4.10: {} ansi-align@3.0.1: @@ -13535,6 +13907,8 @@ snapshots: array-ify: 1.0.0 dot-prop: 5.3.0 + compare-versions@6.1.1: {} + compressible@2.0.18: dependencies: mime-db: 1.54.0 @@ -13560,6 +13934,8 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + confbox@0.1.8: {} + confbox@0.2.2: {} connect@3.7.0: @@ -13754,6 +14130,8 @@ snapshots: dayjs@1.11.18: {} + de-indent@1.0.2: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -13949,6 +14327,8 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@4.5.0: {} + entities@6.0.1: {} env-editor@0.4.2: {} @@ -15237,6 +15617,8 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 + he@1.2.0: {} + hermes-estree@0.28.1: {} hermes-estree@0.29.1: {} @@ -15282,14 +15664,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -15329,6 +15711,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-lazy@4.0.0: {} + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -16039,6 +16423,8 @@ snapshots: jiti@2.5.1: {} + jju@1.4.0: {} + joi@17.13.3: dependencies: '@hapi/hoek': 9.3.0 @@ -16047,6 +16433,8 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 + js-base64@3.7.8: {} + js-stringify@1.0.2: {} js-tokens@4.0.0: {} @@ -16092,6 +16480,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -16150,6 +16540,8 @@ snapshots: zod: 3.25.76 zod-validation-error: 3.5.3(zod@3.25.76) + kolorist@1.8.0: {} + lan-network@0.1.7: {} language-subtag-registry@0.3.23: {} @@ -16231,6 +16623,12 @@ snapshots: pify: 3.0.0 strip-bom: 3.0.0 + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -16289,6 +16687,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lru-cache@7.18.3: {} macos-release@3.4.0: {} @@ -16852,6 +17254,10 @@ snapshots: mimic-function@5.0.1: {} + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -16876,12 +17282,21 @@ snapshots: mkdirp@3.0.1: {} + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + mrmime@2.0.1: {} ms@2.0.0: {} ms@2.1.3: {} + muggle-string@0.4.1: {} + mute-stream@2.0.0: {} mz@2.7.0: @@ -17236,7 +17651,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 get-uri: 6.0.5 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -17306,6 +17721,8 @@ snapshots: parseurl@1.3.3: {} + path-browserify@1.0.1: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -17357,6 +17774,12 @@ snapshots: dependencies: find-up: 4.1.0 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + pkg-types@2.3.0: dependencies: confbox: 0.2.2 @@ -17560,6 +17983,8 @@ snapshots: dependencies: side-channel: 1.1.0 + quansync@0.2.11: {} + query-string@7.1.3: dependencies: decode-uri-component: 0.2.2 @@ -18186,6 +18611,10 @@ snapshots: semver@6.3.1: {} + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + semver@7.6.3: {} semver@7.7.2: {} @@ -18389,7 +18818,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -18466,6 +18895,8 @@ snapshots: strict-uri-encode@2.0.0: {} + string-argv@0.3.2: {} + string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -18871,6 +19302,8 @@ snapshots: transitivePeerDependencies: - supports-color + typescript@5.8.2: {} + typescript@5.9.2: {} ua-parser-js@1.0.41: {} @@ -19113,6 +19546,31 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vite-plugin-dts@4.5.4(@types/node@24.3.0)(rollup@4.50.2)(typescript@5.9.2)(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)): + dependencies: + '@microsoft/api-extractor': 7.52.13(@types/node@24.3.0) + '@rollup/pluginutils': 5.3.0(rollup@4.50.2) + '@volar/typescript': 2.4.23 + '@vue/language-core': 2.2.0(typescript@5.9.2) + compare-versions: 6.1.1 + debug: 4.4.3 + kolorist: 1.8.0 + local-pkg: 1.1.2 + magic-string: 0.30.19 + typescript: 5.9.2 + optionalDependencies: + vite: 6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + + vite-plugin-singlefile@2.3.0(rollup@4.50.2)(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)): + dependencies: + micromatch: 4.0.8 + rollup: 4.50.2 + vite: 6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.9 @@ -19138,6 +19596,8 @@ snapshots: void-elements@3.1.0: {} + vscode-uri@3.1.0: {} + walk-up-path@4.0.0: {} walker@1.0.8: @@ -19315,6 +19775,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yallist@5.0.0: {} yaml@2.8.1: {} From 86ff6762a3315af8652e5e76d77398026cbb256b Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:44:16 -0400 Subject: [PATCH 03/12] some things working --- apps/mobile/package.json | 3 +- apps/mobile/src/app/(tabs)/shell/detail.tsx | 226 +++++------------- .../index.html | 4 +- .../package.json | 8 +- .../src/main.tsx | 24 +- .../src/vite-env.d.ts | 1 + .../src/index.tsx | 30 ++- pnpm-lock.yaml | 18 ++ 8 files changed, 125 insertions(+), 189 deletions(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 2d2d7ad..146475a 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -28,6 +28,7 @@ "@expo/vector-icons": "^15.0.2", "@fressh/assets": "workspace:*", "@fressh/react-native-uniffi-russh": "workspace:*", + "@fressh/react-native-xtermjs-webview": "workspace:*", "@react-native-segmented-control/segmented-control": "2.5.7", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.4", @@ -48,13 +49,13 @@ "expo-haptics": "~15.0.7", "expo-image": "~3.0.8", "expo-linking": "~8.0.8", - "@fressh/react-native-xtermjs-webview": "workspace:*", "expo-router": "6.0.6", "expo-secure-store": "~15.0.7", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", "expo-symbols": "~1.0.7", "expo-system-ui": "~6.0.7", + "p-queue": "^8.1.1", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.4", diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx index 8568532..fe0b018 100644 --- a/apps/mobile/src/app/(tabs)/shell/detail.tsx +++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx @@ -5,22 +5,15 @@ import { type XtermWebViewHandle, } from '@fressh/react-native-xtermjs-webview'; +import { useQueryClient } from '@tanstack/react-query'; import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; -import React, { useEffect, useRef, useState } from 'react'; -import { - Platform, - Pressable, - ScrollView, - Text, - TextInput, - View, -} from 'react-native'; +import PQueue from 'p-queue'; +import React, { useEffect, useRef } from 'react'; +import { Pressable, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { disconnectSshConnectionAndInvalidateQuery } from '@/lib/query-fns'; import { useTheme } from '@/lib/theme'; -const renderer: 'xtermjs' | 'rn-text' = 'xtermjs'; -const decoder = new TextDecoder('utf-8'); - export default function TabsShellDetail() { return ; } @@ -43,29 +36,42 @@ function ShellDetail() { ? RnRussh.getSshShell(String(connectionId), channelIdNum) : undefined; - const [shellData, setShellData] = useState(''); + function sendDataToXterm(data: ArrayBuffer) { + try { + const bytes = new Uint8Array(data); + console.log('sendDataToXterm', new TextDecoder().decode(bytes)); + xtermWebViewRef.current?.write(bytes); + } catch (e) { + console.warn('Failed to decode shell data', e); + } + } + + const queueRef = useRef(null); useEffect(() => { - if (!connection) return; + if (!queueRef.current) + queueRef.current = new PQueue({ + concurrency: 1, + intervalCap: 1, // <= one task per interval + interval: 100, // <= 100ms between tasks + autoStart: false, // <= buffer until we start() + }); + const xtermQueue = queueRef.current; + if (!connection || !xtermQueue) return; const listenerId = connection.addChannelListener((data: ArrayBuffer) => { - try { - const bytes = new Uint8Array(data); - xtermWebViewRef.current?.write(bytes); - const chunk = decoder.decode(bytes); - setShellData((prev) => prev + chunk); - } catch (e) { - console.warn('Failed to decode shell data', e); - } + console.log('ssh.onData', new TextDecoder().decode(new Uint8Array(data))); + void xtermQueue.add(() => { + sendDataToXterm(data); + }); }); return () => { connection.removeChannelListener(listenerId); + xtermQueue.pause(); + xtermQueue.clear(); }; - }, [connection]); + }, [connection, queueRef]); - const scrollViewRef = useRef(null); - useEffect(() => { - scrollViewRef.current?.scrollToEnd({ animated: true }); - }, [shellData]); + const queryClient = useQueryClient(); return ( @@ -77,9 +83,15 @@ function ShellDetail() { accessibilityLabel="Disconnect" hitSlop={10} onPress={async () => { + if (!connection) return; try { - await connection?.disconnect(); - } catch {} + await disconnectSshConnectionAndInvalidateQuery({ + connectionId: connection.connectionId, + queryClient: queryClient, + }); + } catch (e) { + console.warn('Failed to disconnect', e); + } router.replace('/shell'); }} > @@ -94,140 +106,30 @@ function ShellDetail() { { backgroundColor: theme.colors.background }, ]} > - - {renderer === 'xtermjs' ? ( - { - // document.body.style.backgroundColor = '${theme.colors.background}'; - // document.body.style.color = '${theme.colors.textPrimary}'; - // document.body.style.fontSize = '80px'; - // const termDiv = document.getElementById('terminal'); - // termDiv.style.backgroundColor = '${theme.colors.background}'; - // termDiv.style.color = '${theme.colors.textPrimary}'; - // window.terminal.options.fontSize = 50; - // }, 50); - // `} - onMessage={(event) => { - console.log('onMessage', event.nativeEvent.data); - }} - /> - ) : ( - - - - {shellData || 'Connected. Output will appear here...'} - - - - )} - { - await shell?.sendData( - Uint8Array.from(new TextEncoder().encode(command + '\n')) - .buffer, - ); - }} - /> - + { + window.fitAddon?.fit(); +}, 1_000); + `} + onMessage={(message) => { + if (message.type === 'initialized') { + console.log('xterm.onMessage initialized'); + queueRef.current?.start(); + return; + } + const data = message.data; + console.log('xterm.onMessage', new TextDecoder().decode(data)); + void shell?.sendData(data.buffer as ArrayBuffer); + }} + /> ); } - -function CommandInput(props: { - executeCommand: (command: string) => Promise; -}) { - const [command, setCommand] = useState(''); - - async function handleExecute() { - if (!command.trim()) return; - await props.executeCommand(command); - setCommand(''); - } - - return ( - - - - - Execute - - - - ); -} diff --git a/packages/react-native-xtermjs-webview-internal/index.html b/packages/react-native-xtermjs-webview-internal/index.html index a7be769..6f1ec3b 100644 --- a/packages/react-native-xtermjs-webview-internal/index.html +++ b/packages/react-native-xtermjs-webview-internal/index.html @@ -1,9 +1,9 @@ - + - +
{ window.ReactNativeWebView?.postMessage?.(arg); }; setTimeout(() => { - postMessage('DEBUG: set timeout'); -}, 1000); + postMessage('initialized'); +}, 10); + +terminal.onData((data) => { + const base64Data = Base64.encode(data); + postMessage(base64Data); +}); function terminalWriteBase64(base64Data: string) { try { - postMessage(`DEBUG: terminalWriteBase64 ${base64Data}`); - const data = new Uint8Array(Buffer.from(base64Data, 'base64')); - postMessage(`DEBUG: terminalWriteBase64 decoded ${decoder.decode(data)}`); - + const data = Base64.toUint8Array(base64Data); terminal.write(data); } catch (e) { postMessage(`DEBUG: terminalWriteBase64 error ${e}`); diff --git a/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts b/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts index 4e37827..065b340 100644 --- a/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts +++ b/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts @@ -2,6 +2,7 @@ interface Window { terminal?: Terminal; + fitAddon?: FitAddon; terminalWriteBase64?: (data: string) => void; ReactNativeWebView?: { postMessage?: (data: string) => void; diff --git a/packages/react-native-xtermjs-webview/src/index.tsx b/packages/react-native-xtermjs-webview/src/index.tsx index f2e9153..da9cd66 100644 --- a/packages/react-native-xtermjs-webview/src/index.tsx +++ b/packages/react-native-xtermjs-webview/src/index.tsx @@ -8,13 +8,19 @@ type StrictOmit = Omit; export type XtermWebViewHandle = { write: (data: Uint8Array) => void; }; -const decoder = new TextDecoder('utf-8'); export function XtermJsWebView({ ref, + onMessage, ...props -}: StrictOmit, 'source' | 'originWhitelist'> & { +}: StrictOmit< + ComponentProps, + 'source' | 'originWhitelist' | 'onMessage' +> & { ref: React.RefObject; + onMessage?: ( + data: { type: 'data'; data: Uint8Array } | { type: 'initialized' }, + ) => void; }) { const webViewRef = useRef(null); @@ -22,17 +28,8 @@ export function XtermJsWebView({ return { write: (data) => { const base64Data = Base64.fromUint8Array(data); - console.log('writing rn side', { - base64Data, - dataLength: data.length, - }); - - console.log( - 'try to decode', - decoder.decode(Base64.toUint8Array(base64Data)), - ); webViewRef.current?.injectJavaScript(` - window?.terminalWriteBase64('${base64Data}'); + window?.terminalWriteBase64?.('${base64Data}'); `); }, }; @@ -43,6 +40,15 @@ export function XtermJsWebView({ ref={webViewRef} originWhitelist={['*']} source={{ html: htmlString }} + onMessage={(event) => { + const message = event.nativeEvent.data; + if (message === 'initialized') { + onMessage?.({ type: 'initialized' }); + return; + } + const data = Base64.toUint8Array(message); + onMessage?.({ type: 'data', data }); + }} {...props} /> ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa308fa..641b396 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,6 +130,9 @@ importers: expo-system-ui: specifier: ~6.0.7 version: 6.0.7(expo@54.0.8)(react-native-web@0.21.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0)) + p-queue: + specifier: ^8.1.1 + version: 8.1.1 react: specifier: 19.1.0 version: 19.1.0 @@ -385,9 +388,15 @@ importers: packages/react-native-xtermjs-webview-internal: dependencies: + '@xterm/addon-fit': + specifier: ^0.10.0 + version: 0.10.0(@xterm/xterm@5.5.0) '@xterm/xterm': specifier: ^5.5.0 version: 5.5.0 + js-base64: + specifier: ^3.7.8 + version: 3.7.8 react: specifier: 19.1.0 version: 19.1.0 @@ -3512,6 +3521,11 @@ packages: resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + '@xterm/addon-fit@0.10.0': + resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + '@xterm/xterm@5.5.0': resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} @@ -13028,6 +13042,10 @@ snapshots: '@xmldom/xmldom@0.8.11': {} + '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + '@xterm/xterm@5.5.0': {} abbrev@3.0.1: {} From 2f5568a6d5efdd18abdefebc63abb51253d408da Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:22:50 -0400 Subject: [PATCH 04/12] rm one pkg --- apps/mobile/src/app/(tabs)/shell/detail.tsx | 13 ++-- .../.gitignore | 24 ------- .../eslint.config.js | 27 ------- .../package.json | 43 ------------ .../prettier.config.mjs | 14 ---- .../tsconfig.json | 7 -- .../tsconfig.node.json | 25 ------- .../turbo.json | 7 -- .../react-native-xtermjs-webview/.gitignore | 1 + .../index.html | 2 +- .../react-native-xtermjs-webview/package.json | 8 ++- .../src-internal}/main.tsx | 0 .../src-internal}/vite-env.d.ts | 0 .../src/index.tsx | 6 +- .../tsconfig.app-internal.json} | 4 +- .../tsconfig.json | 1 + .../tsconfig.node.json | 2 +- .../react-native-xtermjs-webview/turbo.json | 12 ++++ .../vite.config.internal.ts} | 3 + pnpm-lock.yaml | 70 ++----------------- 20 files changed, 44 insertions(+), 225 deletions(-) delete mode 100644 packages/react-native-xtermjs-webview-internal/.gitignore delete mode 100644 packages/react-native-xtermjs-webview-internal/eslint.config.js delete mode 100644 packages/react-native-xtermjs-webview-internal/package.json delete mode 100644 packages/react-native-xtermjs-webview-internal/prettier.config.mjs delete mode 100644 packages/react-native-xtermjs-webview-internal/tsconfig.json delete mode 100644 packages/react-native-xtermjs-webview-internal/tsconfig.node.json delete mode 100644 packages/react-native-xtermjs-webview-internal/turbo.json rename packages/{react-native-xtermjs-webview-internal => react-native-xtermjs-webview}/index.html (82%) rename packages/{react-native-xtermjs-webview-internal/src => react-native-xtermjs-webview/src-internal}/main.tsx (100%) rename packages/{react-native-xtermjs-webview-internal/src => react-native-xtermjs-webview/src-internal}/vite-env.d.ts (100%) rename packages/{react-native-xtermjs-webview-internal/tsconfig.app.json => react-native-xtermjs-webview/tsconfig.app-internal.json} (84%) rename packages/{react-native-xtermjs-webview-internal/vite.config.ts => react-native-xtermjs-webview/vite.config.internal.ts} (82%) diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx index fe0b018..3d5adc7 100644 --- a/apps/mobile/src/app/(tabs)/shell/detail.tsx +++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx @@ -38,9 +38,9 @@ function ShellDetail() { function sendDataToXterm(data: ArrayBuffer) { try { - const bytes = new Uint8Array(data); - console.log('sendDataToXterm', new TextDecoder().decode(bytes)); - xtermWebViewRef.current?.write(bytes); + const bytes = new Uint8Array(data.slice()); + console.log('sendDataToXterm', new TextDecoder().decode(bytes.slice())); + xtermWebViewRef.current?.write(bytes.slice()); } catch (e) { console.warn('Failed to decode shell data', e); } @@ -59,7 +59,10 @@ function ShellDetail() { const xtermQueue = queueRef.current; if (!connection || !xtermQueue) return; const listenerId = connection.addChannelListener((data: ArrayBuffer) => { - console.log('ssh.onData', new TextDecoder().decode(new Uint8Array(data))); + console.log( + 'ssh.onData', + new TextDecoder().decode(new Uint8Array(data.slice())), + ); void xtermQueue.add(() => { sendDataToXterm(data); }); @@ -126,7 +129,7 @@ setTimeout(() => { } const data = message.data; console.log('xterm.onMessage', new TextDecoder().decode(data)); - void shell?.sendData(data.buffer as ArrayBuffer); + void shell?.sendData(data.slice().buffer as ArrayBuffer); }} /> diff --git a/packages/react-native-xtermjs-webview-internal/.gitignore b/packages/react-native-xtermjs-webview-internal/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/packages/react-native-xtermjs-webview-internal/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/packages/react-native-xtermjs-webview-internal/eslint.config.js b/packages/react-native-xtermjs-webview-internal/eslint.config.js deleted file mode 100644 index a1e9c7c..0000000 --- a/packages/react-native-xtermjs-webview-internal/eslint.config.js +++ /dev/null @@ -1,27 +0,0 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import reactHooks from 'eslint-plugin-react-hooks'; -import reactRefresh from 'eslint-plugin-react-refresh'; -import tseslint from 'typescript-eslint'; -import { globalIgnores, defineConfig } from 'eslint/config'; - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - }, - }, -]); diff --git a/packages/react-native-xtermjs-webview-internal/package.json b/packages/react-native-xtermjs-webview-internal/package.json deleted file mode 100644 index d607c1f..0000000 --- a/packages/react-native-xtermjs-webview-internal/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@fressh/react-native-xtermjs-webview-internal", - "private": true, - "version": "0.0.0", - "type": "module", - "files": [ - "dist" - ], - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "fmt:check": "cross-env SORT_IMPORTS=true prettier --check .", - "fmt": "cross-env SORT_IMPORTS=true prettier --write .", - "eslint:check": "eslint . --report-unused-disable-directives --max-warnings 0", - "lint:fix": "eslint --fix --report-unused-disable-directives --max-warnings 0 .", - "typecheck": "tsc --noEmit", - "preview": "vite preview" - }, - "dependencies": { - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", - "js-base64": "^3.7.8", - "react": "19.1.0", - "react-dom": "19.1.0" - }, - "devDependencies": { - "@epic-web/config": "^1.21.3", - "@eslint/js": "^9.35.0", - "@types/react": "~19.1.12", - "@types/react-dom": "^19.1.7", - "@vitejs/plugin-react": "^5.0.2", - "eslint": "^9.35.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.4.0", - "prettier": "^3.6.2", - "prettier-plugin-organize-imports": "^4.2.0", - "typescript": "~5.9.2", - "typescript-eslint": "^8.44.0", - "vite": "6.3.6", - "vite-plugin-singlefile": "^2.3.0" - } -} diff --git a/packages/react-native-xtermjs-webview-internal/prettier.config.mjs b/packages/react-native-xtermjs-webview-internal/prettier.config.mjs deleted file mode 100644 index 7b0e9e2..0000000 --- a/packages/react-native-xtermjs-webview-internal/prettier.config.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import epicConfig from '@epic-web/config/prettier'; -// Sometimes this plugin can remove imports that are being edited. -// As a workaround we will only use this in the cli. (pnpm run fmt) -const sortImports = process.env.SORT_IMPORTS === 'true-never'; - -/** @type {import("prettier").Options} */ -export default { - ...epicConfig, - semi: true, - plugins: [ - ...(sortImports ? ['prettier-plugin-organize-imports'] : []), - ...(epicConfig.plugins || []), - ], -}; diff --git a/packages/react-native-xtermjs-webview-internal/tsconfig.json b/packages/react-native-xtermjs-webview-internal/tsconfig.json deleted file mode 100644 index fb12418..0000000 --- a/packages/react-native-xtermjs-webview-internal/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/packages/react-native-xtermjs-webview-internal/tsconfig.node.json b/packages/react-native-xtermjs-webview-internal/tsconfig.node.json deleted file mode 100644 index 1a5ed45..0000000 --- a/packages/react-native-xtermjs-webview-internal/tsconfig.node.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2023", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/packages/react-native-xtermjs-webview-internal/turbo.json b/packages/react-native-xtermjs-webview-internal/turbo.json deleted file mode 100644 index 335d504..0000000 --- a/packages/react-native-xtermjs-webview-internal/turbo.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": ["//"], - "tasks": { - "lint": {}, - "lint:check": {} - } -} diff --git a/packages/react-native-xtermjs-webview/.gitignore b/packages/react-native-xtermjs-webview/.gitignore index a547bf3..054f5aa 100644 --- a/packages/react-native-xtermjs-webview/.gitignore +++ b/packages/react-native-xtermjs-webview/.gitignore @@ -9,6 +9,7 @@ lerna-debug.log* node_modules dist +dist-internal dist-ssr *.local diff --git a/packages/react-native-xtermjs-webview-internal/index.html b/packages/react-native-xtermjs-webview/index.html similarity index 82% rename from packages/react-native-xtermjs-webview-internal/index.html rename to packages/react-native-xtermjs-webview/index.html index 6f1ec3b..7725761 100644 --- a/packages/react-native-xtermjs-webview-internal/index.html +++ b/packages/react-native-xtermjs-webview/index.html @@ -8,6 +8,6 @@ id="terminal" style="margin: 0; padding: 0; width: 100%; height: 100%" >
- + diff --git a/packages/react-native-xtermjs-webview/package.json b/packages/react-native-xtermjs-webview/package.json index 5c594bc..9eff657 100644 --- a/packages/react-native-xtermjs-webview/package.json +++ b/packages/react-native-xtermjs-webview/package.json @@ -8,7 +8,8 @@ }, "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build:main": "tsc -b && vite build", + "build:internal": "tsc -b && vite build --config vite.config.internal.ts", "fmt:check": "cross-env SORT_IMPORTS=true prettier --check .", "fmt": "cross-env SORT_IMPORTS=true prettier --write .", "eslint:check": "eslint . --report-unused-disable-directives --max-warnings 0", @@ -17,7 +18,6 @@ "preview": "vite preview" }, "dependencies": { - "@fressh/react-native-xtermjs-webview-internal": "workspace:*", "js-base64": "^3.7.8" }, "peerDependencies": { @@ -31,8 +31,12 @@ "@types/react": "~19.1.12", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.2", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", + "js-base64": "^3.7.8", "eslint": "^9.35.0", "eslint-plugin-react-hooks": "^5.2.0", + "vite-plugin-singlefile": "^2.3.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.4.0", "prettier": "^3.6.2", diff --git a/packages/react-native-xtermjs-webview-internal/src/main.tsx b/packages/react-native-xtermjs-webview/src-internal/main.tsx similarity index 100% rename from packages/react-native-xtermjs-webview-internal/src/main.tsx rename to packages/react-native-xtermjs-webview/src-internal/main.tsx diff --git a/packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts b/packages/react-native-xtermjs-webview/src-internal/vite-env.d.ts similarity index 100% rename from packages/react-native-xtermjs-webview-internal/src/vite-env.d.ts rename to packages/react-native-xtermjs-webview/src-internal/vite-env.d.ts diff --git a/packages/react-native-xtermjs-webview/src/index.tsx b/packages/react-native-xtermjs-webview/src/index.tsx index da9cd66..f04c098 100644 --- a/packages/react-native-xtermjs-webview/src/index.tsx +++ b/packages/react-native-xtermjs-webview/src/index.tsx @@ -1,6 +1,6 @@ import { useImperativeHandle, useRef, type ComponentProps } from 'react'; import { WebView } from 'react-native-webview'; -import htmlString from '@fressh/react-native-xtermjs-webview-internal/dist/index.html?raw'; +import htmlString from '../dist-internal/index.html?raw'; import { Base64 } from 'js-base64'; type StrictOmit = Omit; @@ -27,7 +27,7 @@ export function XtermJsWebView({ useImperativeHandle(ref, () => { return { write: (data) => { - const base64Data = Base64.fromUint8Array(data); + const base64Data = Base64.fromUint8Array(data.slice()); webViewRef.current?.injectJavaScript(` window?.terminalWriteBase64?.('${base64Data}'); `); @@ -46,7 +46,7 @@ export function XtermJsWebView({ onMessage?.({ type: 'initialized' }); return; } - const data = Base64.toUint8Array(message); + const data = Base64.toUint8Array(message.slice()); onMessage?.({ type: 'data', data }); }} {...props} diff --git a/packages/react-native-xtermjs-webview-internal/tsconfig.app.json b/packages/react-native-xtermjs-webview/tsconfig.app-internal.json similarity index 84% rename from packages/react-native-xtermjs-webview-internal/tsconfig.app.json rename to packages/react-native-xtermjs-webview/tsconfig.app-internal.json index 213a1d9..0609598 100644 --- a/packages/react-native-xtermjs-webview-internal/tsconfig.app.json +++ b/packages/react-native-xtermjs-webview/tsconfig.app-internal.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app-internal.tsbuildinfo", "target": "ES2022", "useDefineForClassFields": true, "lib": ["ES2022", "DOM", "DOM.Iterable"], @@ -23,5 +23,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src"] + "include": ["src-internal"] } diff --git a/packages/react-native-xtermjs-webview/tsconfig.json b/packages/react-native-xtermjs-webview/tsconfig.json index fb12418..da25836 100644 --- a/packages/react-native-xtermjs-webview/tsconfig.json +++ b/packages/react-native-xtermjs-webview/tsconfig.json @@ -2,6 +2,7 @@ "files": [], "references": [ { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.app-internal.json" }, { "path": "./tsconfig.node.json" } ] } diff --git a/packages/react-native-xtermjs-webview/tsconfig.node.json b/packages/react-native-xtermjs-webview/tsconfig.node.json index 1a5ed45..da9370a 100644 --- a/packages/react-native-xtermjs-webview/tsconfig.node.json +++ b/packages/react-native-xtermjs-webview/tsconfig.node.json @@ -21,5 +21,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "vite.config.internal.ts"] } diff --git a/packages/react-native-xtermjs-webview/turbo.json b/packages/react-native-xtermjs-webview/turbo.json index 335d504..7715278 100644 --- a/packages/react-native-xtermjs-webview/turbo.json +++ b/packages/react-native-xtermjs-webview/turbo.json @@ -1,6 +1,18 @@ { "extends": ["//"], "tasks": { + "build": { + "dependsOn": ["build:internal", "build:main"] + }, + "build:main": { + "inputs": ["src/**"], + "dependsOn": ["build:internal"], + "outputs": ["dist/**"] + }, + "build:internal": { + "inputs": ["src-internal/**"], + "outputs": ["dist-internal/**"] + }, "lint": {}, "lint:check": {} } diff --git a/packages/react-native-xtermjs-webview-internal/vite.config.ts b/packages/react-native-xtermjs-webview/vite.config.internal.ts similarity index 82% rename from packages/react-native-xtermjs-webview-internal/vite.config.ts rename to packages/react-native-xtermjs-webview/vite.config.internal.ts index 208116f..958bf7c 100644 --- a/packages/react-native-xtermjs-webview-internal/vite.config.ts +++ b/packages/react-native-xtermjs-webview/vite.config.internal.ts @@ -4,4 +4,7 @@ import { viteSingleFile } from 'vite-plugin-singlefile'; // https://vite.dev/config/ export default defineConfig({ plugins: [viteSingleFile()], + build: { + outDir: 'dist-internal', + }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 641b396..102483b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -324,9 +324,6 @@ importers: packages/react-native-xtermjs-webview: dependencies: - '@fressh/react-native-xtermjs-webview-internal': - specifier: workspace:* - version: link:../react-native-xtermjs-webview-internal js-base64: specifier: ^3.7.8 version: 3.7.8 @@ -346,6 +343,12 @@ importers: '@vitejs/plugin-react': specifier: ^5.0.2 version: 5.0.2(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) + '@xterm/addon-fit': + specifier: ^0.10.0 + version: 0.10.0(@xterm/xterm@5.5.0) + '@xterm/xterm': + specifier: ^5.5.0 + version: 5.5.0 eslint: specifier: ^9.35.0 version: 9.35.0(jiti@2.5.1) @@ -385,67 +388,6 @@ importers: vite-plugin-dts: specifier: ^4.5.4 version: 4.5.4(@types/node@24.3.0)(rollup@4.50.2)(typescript@5.9.2)(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) - - packages/react-native-xtermjs-webview-internal: - dependencies: - '@xterm/addon-fit': - specifier: ^0.10.0 - version: 0.10.0(@xterm/xterm@5.5.0) - '@xterm/xterm': - specifier: ^5.5.0 - version: 5.5.0 - js-base64: - specifier: ^3.7.8 - version: 3.7.8 - react: - specifier: 19.1.0 - version: 19.1.0 - react-dom: - specifier: 19.1.0 - version: 19.1.0(react@19.1.0) - devDependencies: - '@epic-web/config': - specifier: ^1.21.3 - version: 1.21.3(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.5.1))(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2)(typescript@5.9.2) - '@eslint/js': - specifier: ^9.35.0 - version: 9.35.0 - '@types/react': - specifier: ~19.1.12 - version: 19.1.12 - '@types/react-dom': - specifier: ^19.1.7 - version: 19.1.9(@types/react@19.1.12) - '@vitejs/plugin-react': - specifier: ^5.0.2 - version: 5.0.2(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) - eslint: - specifier: ^9.35.0 - version: 9.35.0(jiti@2.5.1) - eslint-plugin-react-hooks: - specifier: ^5.2.0 - version: 5.2.0(eslint@9.35.0(jiti@2.5.1)) - eslint-plugin-react-refresh: - specifier: ^0.4.20 - version: 0.4.20(eslint@9.35.0(jiti@2.5.1)) - globals: - specifier: ^16.4.0 - version: 16.4.0 - prettier: - specifier: ^3.6.2 - version: 3.6.2 - prettier-plugin-organize-imports: - specifier: ^4.2.0 - version: 4.2.0(prettier@3.6.2)(typescript@5.9.2) - typescript: - specifier: ~5.9.2 - version: 5.9.2 - typescript-eslint: - specifier: ^8.44.0 - version: 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) - vite: - specifier: 6.3.6 - version: 6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) vite-plugin-singlefile: specifier: ^2.3.0 version: 2.3.0(rollup@4.50.2)(vite@6.3.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) From b24d44155d0c54cdfd1d44ddaeb122effc77812e Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Wed, 17 Sep 2025 19:35:49 -0400 Subject: [PATCH 05/12] one ai prompt --- apps/mobile/src/app/(tabs)/shell/detail.tsx | 87 +++++----- .../src-internal/main.tsx | 154 +++++++++++++++--- .../src/index.tsx | 153 +++++++++++++++-- 3 files changed, 301 insertions(+), 93 deletions(-) diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx index 3d5adc7..f03df40 100644 --- a/apps/mobile/src/app/(tabs)/shell/detail.tsx +++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx @@ -7,7 +7,6 @@ import { import { useQueryClient } from '@tanstack/react-query'; import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; -import PQueue from 'p-queue'; import React, { useEffect, useRef } from 'react'; import { Pressable, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -19,7 +18,7 @@ export default function TabsShellDetail() { } function ShellDetail() { - const xtermWebViewRef = useRef(null); + const xtermRef = useRef(null); const { connectionId, channelId } = useLocalSearchParams<{ connectionId?: string; channelId?: string; @@ -36,43 +35,24 @@ function ShellDetail() { ? RnRussh.getSshShell(String(connectionId), channelIdNum) : undefined; - function sendDataToXterm(data: ArrayBuffer) { - try { - const bytes = new Uint8Array(data.slice()); - console.log('sendDataToXterm', new TextDecoder().decode(bytes.slice())); - xtermWebViewRef.current?.write(bytes.slice()); - } catch (e) { - console.warn('Failed to decode shell data', e); - } - } - - const queueRef = useRef(null); - + /** + * SSH -> xterm (remote output) + * Send bytes only; batching is handled inside XtermJsWebView. + */ useEffect(() => { - if (!queueRef.current) - queueRef.current = new PQueue({ - concurrency: 1, - intervalCap: 1, // <= one task per interval - interval: 100, // <= 100ms between tasks - autoStart: false, // <= buffer until we start() - }); - const xtermQueue = queueRef.current; - if (!connection || !xtermQueue) return; + if (!connection) return; + const listenerId = connection.addChannelListener((data: ArrayBuffer) => { - console.log( - 'ssh.onData', - new TextDecoder().decode(new Uint8Array(data.slice())), - ); - void xtermQueue.add(() => { - sendDataToXterm(data); - }); + // Forward bytes to terminal (no string conversion) + xtermRef.current?.write(new Uint8Array(data)); }); + return () => { connection.removeChannelListener(listenerId); - xtermQueue.pause(); - xtermQueue.clear(); + // Flush any buffered writes on unmount + xtermRef.current?.flush?.(); }; - }, [connection, queueRef]); + }, [connection]); const queryClient = useQueryClient(); @@ -90,7 +70,7 @@ function ShellDetail() { try { await disconnectSshConnectionAndInvalidateQuery({ connectionId: connection.connectionId, - queryClient: queryClient, + queryClient, }); } catch (e) { console.warn('Failed to disconnect', e); @@ -110,26 +90,33 @@ function ShellDetail() { ]} > { - window.fitAddon?.fit(); -}, 1_000); - `} + ref={xtermRef} + style={{ flex: 1 }} + // Optional: set initial theme/font + onLoadEnd={() => { + // Set theme bg/fg and font settings once WebView loads; the page will + // still send 'initialized' after xterm is ready. + xtermRef.current?.setTheme?.( + theme.colors.background, + theme.colors.text, + ); + xtermRef.current?.setFont?.('Menlo, ui-monospace, monospace', 14); + }} onMessage={(message) => { if (message.type === 'initialized') { - console.log('xterm.onMessage initialized'); - queueRef.current?.start(); + // Terminal is ready; you could send a greeting or focus it + xtermRef.current?.focus?.(); return; } - const data = message.data; - console.log('xterm.onMessage', new TextDecoder().decode(data)); - void shell?.sendData(data.slice().buffer as ArrayBuffer); + if (message.type === 'data') { + // xterm user input -> SSH + // NOTE: msg.data is a fresh Uint8Array starting at offset 0 + void shell?.sendData(message.data.buffer as ArrayBuffer); + return; + } + if (message.type === 'debug') { + console.log('xterm.debug', message.message); + } }} /> diff --git a/packages/react-native-xtermjs-webview/src-internal/main.tsx b/packages/react-native-xtermjs-webview/src-internal/main.tsx index 2222441..c51b35d 100644 --- a/packages/react-native-xtermjs-webview/src-internal/main.tsx +++ b/packages/react-native-xtermjs-webview/src-internal/main.tsx @@ -1,33 +1,137 @@ import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import { Base64 } from 'js-base64'; - import '@xterm/xterm/css/xterm.css'; -const terminal = new Terminal(); -const fitAddon = new FitAddon(); -terminal.loadAddon(fitAddon); -terminal.open(document.getElementById('terminal')!); -fitAddon.fit(); -window.terminal = terminal; -window.fitAddon = fitAddon; -const postMessage = (arg: string) => { - window.ReactNativeWebView?.postMessage?.(arg); -}; -setTimeout(() => { - postMessage('initialized'); -}, 10); - -terminal.onData((data) => { - const base64Data = Base64.encode(data); - postMessage(base64Data); +/** + * Xterm setup + */ +const term = new Terminal({ + allowProposedApi: true, + convertEol: true, + scrollback: 10000, }); -function terminalWriteBase64(base64Data: string) { +const fitAddon = new FitAddon(); +term.loadAddon(fitAddon); + +const root = document.getElementById('terminal')!; +term.open(root); +fitAddon.fit(); + +// Expose for debugging (optional) +window.terminal = term; +window.fitAddon = fitAddon; + +/** + * Post typed messages to React Native + */ +const post = (msg: unknown) => + window.ReactNativeWebView?.postMessage?.(JSON.stringify(msg)); + +/** + * Encode/decode helpers + */ +const enc = new TextEncoder(); + +/** + * Initial handshake + */ +setTimeout(() => post({ type: 'initialized' }), 0); + +/** + * User input from xterm -> RN (SSH) + * Send UTF-8 bytes only (Base64-encoded) + */ +term.onData((data /* string */) => { + const bytes = enc.encode(data); + const b64 = Base64.fromUint8Array(bytes); + post({ type: 'input', b64 }); +}); + +/** + * Message handler for RN -> WebView control/data + * We support: write, resize, setFont, setTheme, clear, focus + */ +window.addEventListener('message', (e: MessageEvent) => { try { - const data = Base64.toUint8Array(base64Data); - terminal.write(data); - } catch (e) { - postMessage(`DEBUG: terminalWriteBase64 error ${e}`); + const msg = JSON.parse(e.data); + if (!msg || typeof msg.type !== 'string') return; + + switch (msg.type) { + case 'write': { + // Either a single b64 or an array of chunks + if (typeof msg.b64 === 'string') { + const bytes = Base64.toUint8Array(msg.b64); + term.write(bytes); + } else if (Array.isArray(msg.chunks)) { + for (const b64 of msg.chunks) { + const bytes = Base64.toUint8Array(b64); + term.write(bytes); + } + } + break; + } + + case 'resize': { + if (typeof msg.cols === 'number' && typeof msg.rows === 'number') { + try { + term.resize(msg.cols, msg.rows); + } finally { + fitAddon.fit(); + } + } else { + // If cols/rows not provided, try fit + fitAddon.fit(); + } + break; + } + + case 'setFont': { + const { family, size } = msg; + if (family) document.body.style.fontFamily = family; + if (typeof size === 'number') + document.body.style.fontSize = `${size}px`; + fitAddon.fit(); + break; + } + + case 'setTheme': { + const { background, foreground } = msg; + if (background) document.body.style.backgroundColor = background; + // xterm theme API (optional) + term.options = { + ...term.options, + theme: { + ...(term.options.theme ?? {}), + background, + foreground, + }, + }; + break; + } + + case 'clear': { + term.clear(); + break; + } + + case 'focus': { + term.focus(); + break; + } + } + } catch (err) { + post({ type: 'debug', message: `message handler error: ${String(err)}` }); } -} -window.terminalWriteBase64 = terminalWriteBase64; +}); + +/** + * Handle container resize + */ +new ResizeObserver(() => { + try { + fitAddon.fit(); + } catch (err) { + post({ type: 'debug', message: `resize observer error: ${String(err)}` }); + } +}); diff --git a/packages/react-native-xtermjs-webview/src/index.tsx b/packages/react-native-xtermjs-webview/src/index.tsx index f04c098..c2e6676 100644 --- a/packages/react-native-xtermjs-webview/src/index.tsx +++ b/packages/react-native-xtermjs-webview/src/index.tsx @@ -1,12 +1,48 @@ -import { useImperativeHandle, useRef, type ComponentProps } from 'react'; +import React, { useEffect, useImperativeHandle, useRef } from 'react'; import { WebView } from 'react-native-webview'; import htmlString from '../dist-internal/index.html?raw'; import { Base64 } from 'js-base64'; type StrictOmit = Omit; +type InboundMessage = + | { type: 'initialized' } + | { type: 'input'; b64: string } // user typed data from xterm -> RN + | { type: 'debug'; message: string }; + +type OutboundMessage = + | { type: 'write'; b64: string } + | { type: 'write'; chunks: string[] } + | { type: 'resize'; cols?: number; rows?: number } + | { type: 'setFont'; family?: string; size?: number } + | { type: 'setTheme'; background?: string; foreground?: string } + | { type: 'clear' } + | { type: 'focus' }; + export type XtermWebViewHandle = { + /** + * Push raw bytes (Uint8Array) into the terminal. + * Writes are batched (rAF or >=8KB) for performance. + */ write: (data: Uint8Array) => void; + + /** Force-flush any buffered output immediately. */ + flush: () => void; + + /** Resize the terminal to given cols/rows (optional, fit addon also runs). */ + resize: (cols?: number, rows?: number) => void; + + /** Set font props inside the WebView page. */ + setFont: (family?: string, size?: number) => void; + + /** Set basic theme colors (background/foreground). */ + setTheme: (background?: string, foreground?: string) => void; + + /** Clear terminal contents. */ + clear: () => void; + + /** Focus the terminal input. */ + focus: () => void; }; export function XtermJsWebView({ @@ -14,26 +50,95 @@ export function XtermJsWebView({ onMessage, ...props }: StrictOmit< - ComponentProps, + React.ComponentProps, 'source' | 'originWhitelist' | 'onMessage' > & { ref: React.RefObject; onMessage?: ( - data: { type: 'data'; data: Uint8Array } | { type: 'initialized' }, + msg: + | { type: 'initialized' } + | { type: 'data'; data: Uint8Array } // input from xterm (user typed) + | { type: 'debug'; message: string }, ) => void; }) { const webViewRef = useRef(null); - useImperativeHandle(ref, () => { - return { - write: (data) => { - const base64Data = Base64.fromUint8Array(data.slice()); - webViewRef.current?.injectJavaScript(` - window?.terminalWriteBase64?.('${base64Data}'); - `); - }, + // ---- RN -> WebView message sender via injectJavaScript + window MessageEvent + const send = (obj: OutboundMessage) => { + const payload = JSON.stringify(obj); + const js = `window.dispatchEvent(new MessageEvent('message',{data:${JSON.stringify( + payload, + )}})); true;`; + webViewRef.current?.injectJavaScript(js); + }; + + // ---- rAF + 8KB coalescer for writes + const writeBufferRef = useRef(null); + const rafIdRef = useRef(null); + const THRESHOLD = 8 * 1024; // 8KB + + const flush = () => { + if (!writeBufferRef.current) return; + const b64 = Base64.fromUint8Array(writeBufferRef.current); + writeBufferRef.current = null; + if (rafIdRef.current != null) { + cancelAnimationFrame(rafIdRef.current); + rafIdRef.current = null; + } + send({ type: 'write', b64 }); + }; + + const scheduleFlush = () => { + if (rafIdRef.current != null) return; + rafIdRef.current = requestAnimationFrame(() => { + rafIdRef.current = null; + flush(); + }); + }; + + const write = (data: Uint8Array) => { + if (!data || data.length === 0) return; + const chunk = data; // already a fresh Uint8Array per caller + if (!writeBufferRef.current) { + writeBufferRef.current = chunk; + } else { + // concat + const a = writeBufferRef.current; + const merged = new Uint8Array(a.length + chunk.length); + merged.set(a, 0); + merged.set(chunk, a.length); + writeBufferRef.current = merged; + } + if ((writeBufferRef.current?.length ?? 0) >= THRESHOLD) { + flush(); + } else { + scheduleFlush(); + } + }; + + useImperativeHandle(ref, () => ({ + write, + flush, + resize: (cols?: number, rows?: number) => + send({ type: 'resize', cols, rows }), + setFont: (family?: string, size?: number) => + send({ type: 'setFont', family, size }), + setTheme: (background?: string, foreground?: string) => + send({ type: 'setTheme', background, foreground }), + clear: () => send({ type: 'clear' }), + focus: () => send({ type: 'focus' }), + })); + + // Cleanup pending rAF on unmount + useEffect(() => { + return () => { + if (rafIdRef.current != null) { + cancelAnimationFrame(rafIdRef.current); + rafIdRef.current = null; + } + writeBufferRef.current = null; }; - }); + }, []); return ( { - const message = event.nativeEvent.data; - if (message === 'initialized') { - onMessage?.({ type: 'initialized' }); - return; + try { + const msg: InboundMessage = JSON.parse(event.nativeEvent.data); + if (msg.type === 'initialized') { + onMessage?.({ type: 'initialized' }); + return; + } + if (msg.type === 'input') { + // Convert base64 -> bytes for the caller (SSH writer) + const bytes = Base64.toUint8Array(msg.b64); + onMessage?.({ type: 'data', data: bytes }); + return; + } + if (msg.type === 'debug') { + onMessage?.({ type: 'debug', message: msg.message }); + return; + } + } catch { + // ignore unknown payloads } - const data = Base64.toUint8Array(message.slice()); - onMessage?.({ type: 'data', data }); }} {...props} /> From 07f7ce9a6f327c477bb124cf5c709f9af8baf0f9 Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Wed, 17 Sep 2025 19:35:57 -0400 Subject: [PATCH 06/12] lint passing ai prompt --- apps/mobile/src/app/(tabs)/shell/detail.tsx | 24 ++++++++++++++++--- .../src-internal/main.tsx | 1 + 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx index f03df40..e8ddf00 100644 --- a/apps/mobile/src/app/(tabs)/shell/detail.tsx +++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx @@ -42,15 +42,17 @@ function ShellDetail() { useEffect(() => { if (!connection) return; + const xterm = xtermRef.current; + const listenerId = connection.addChannelListener((data: ArrayBuffer) => { // Forward bytes to terminal (no string conversion) - xtermRef.current?.write(new Uint8Array(data)); + xterm?.write(new Uint8Array(data)); }); return () => { connection.removeChannelListener(listenerId); // Flush any buffered writes on unmount - xtermRef.current?.flush?.(); + xterm?.flush?.(); }; }, [connection]); @@ -92,13 +94,29 @@ function ShellDetail() { { + xtermRef.current?.clear?.(); + }} + onContentProcessDidTerminate={() => { + xtermRef.current?.clear?.(); + }} // Optional: set initial theme/font onLoadEnd={() => { // Set theme bg/fg and font settings once WebView loads; the page will // still send 'initialized' after xterm is ready. xtermRef.current?.setTheme?.( theme.colors.background, - theme.colors.text, + theme.colors.textPrimary, ); xtermRef.current?.setFont?.('Menlo, ui-monospace, monospace', 14); }} diff --git a/packages/react-native-xtermjs-webview/src-internal/main.tsx b/packages/react-native-xtermjs-webview/src-internal/main.tsx index c51b35d..2c1a51b 100644 --- a/packages/react-native-xtermjs-webview/src-internal/main.tsx +++ b/packages/react-native-xtermjs-webview/src-internal/main.tsx @@ -10,6 +10,7 @@ const term = new Terminal({ allowProposedApi: true, convertEol: true, scrollback: 10000, + cursorBlink: true, }); const fitAddon = new FitAddon(); term.loadAddon(fitAddon); From c445eef1d10514738ece78cc0235831b17fe5e4c Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Wed, 17 Sep 2025 20:24:25 -0400 Subject: [PATCH 07/12] some commit --- apps/mobile/src/app/(tabs)/shell/detail.tsx | 5 +++-- packages/react-native-xtermjs-webview/src/index.tsx | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx index e8ddf00..377a47f 100644 --- a/apps/mobile/src/app/(tabs)/shell/detail.tsx +++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx @@ -46,7 +46,8 @@ function ShellDetail() { const listenerId = connection.addChannelListener((data: ArrayBuffer) => { // Forward bytes to terminal (no string conversion) - xterm?.write(new Uint8Array(data)); + const uInt8 = new Uint8Array(data); + xterm?.write(uInt8); }); return () => { @@ -118,7 +119,7 @@ function ShellDetail() { theme.colors.background, theme.colors.textPrimary, ); - xtermRef.current?.setFont?.('Menlo, ui-monospace, monospace', 14); + xtermRef.current?.setFont?.('Menlo, ui-monospace, monospace', 50); }} onMessage={(message) => { if (message.type === 'initialized') { diff --git a/packages/react-native-xtermjs-webview/src/index.tsx b/packages/react-native-xtermjs-webview/src/index.tsx index c2e6676..3fd7fe6 100644 --- a/packages/react-native-xtermjs-webview/src/index.tsx +++ b/packages/react-native-xtermjs-webview/src/index.tsx @@ -66,6 +66,7 @@ export function XtermJsWebView({ // ---- RN -> WebView message sender via injectJavaScript + window MessageEvent const send = (obj: OutboundMessage) => { const payload = JSON.stringify(obj); + console.log('sending msg', payload); const js = `window.dispatchEvent(new MessageEvent('message',{data:${JSON.stringify( payload, )}})); true;`; @@ -148,6 +149,7 @@ export function XtermJsWebView({ onMessage={(event) => { try { const msg: InboundMessage = JSON.parse(event.nativeEvent.data); + console.log('received msg', msg); if (msg.type === 'initialized') { onMessage?.({ type: 'initialized' }); return; From b5410f03945762fed7968b7b610d45e97ef9f468 Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Wed, 17 Sep 2025 20:38:09 -0400 Subject: [PATCH 08/12] sorta working but bad init --- apps/mobile/src/app/(tabs)/shell/detail.tsx | 81 ++++++--- .../src-internal/main.tsx | 107 ++++++++---- .../src/index.tsx | 164 ++++++++++-------- 3 files changed, 233 insertions(+), 119 deletions(-) diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx index 377a47f..cd3619b 100644 --- a/apps/mobile/src/app/(tabs)/shell/detail.tsx +++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx @@ -19,6 +19,9 @@ export default function TabsShellDetail() { function ShellDetail() { const xtermRef = useRef(null); + const terminalReadyRef = useRef(false); // gate for initial SSH output buffering + const pendingOutputRef = useRef([]); // bytes we got before xterm init + const { connectionId, channelId } = useLocalSearchParams<{ connectionId?: string; channelId?: string; @@ -37,22 +40,28 @@ function ShellDetail() { /** * SSH -> xterm (remote output) - * Send bytes only; batching is handled inside XtermJsWebView. + * If xterm isn't ready yet, buffer and flush on 'initialized'. */ useEffect(() => { if (!connection) return; - const xterm = xtermRef.current; - const listenerId = connection.addChannelListener((data: ArrayBuffer) => { - // Forward bytes to terminal (no string conversion) - const uInt8 = new Uint8Array(data); - xterm?.write(uInt8); + const listenerId = connection.addChannelListener((ab: ArrayBuffer) => { + const bytes = new Uint8Array(ab); + if (!terminalReadyRef.current) { + // Buffer until WebView->xterm has signaled 'initialized' + pendingOutputRef.current.push(bytes); + // Debug + console.log('SSH->buffer', { len: bytes.length }); + return; + } + // Forward bytes immediately + console.log('SSH->xterm', { len: bytes.length }); + xterm?.write(bytes); }); return () => { connection.removeChannelListener(listenerId); - // Flush any buffered writes on unmount xterm?.flush?.(); }; }, [connection]); @@ -95,6 +104,7 @@ function ShellDetail() { { + console.log('WebView render process gone, clearing terminal'); xtermRef.current?.clear?.(); }} onContentProcessDidTerminate={() => { + console.log( + 'WKWebView content process terminated, clearing terminal', + ); xtermRef.current?.clear?.(); }} - // Optional: set initial theme/font + // xterm-flavored props for styling/behavior + fontFamily="Menlo, ui-monospace, monospace" + fontSize={15} + cursorBlink + scrollback={10000} + themeBackground={theme.colors.background} + themeForeground={theme.colors.textPrimary} + // page load => we can push initial options/theme right away; + // xterm itself will still send 'initialized' once it's truly ready. onLoadEnd={() => { - // Set theme bg/fg and font settings once WebView loads; the page will - // still send 'initialized' after xterm is ready. - xtermRef.current?.setTheme?.( - theme.colors.background, - theme.colors.textPrimary, - ); - xtermRef.current?.setFont?.('Menlo, ui-monospace, monospace', 50); + console.log('WebView onLoadEnd'); }} - onMessage={(message) => { - if (message.type === 'initialized') { - // Terminal is ready; you could send a greeting or focus it + onMessage={(m) => { + console.log('received msg', m); + if (m.type === 'initialized') { + terminalReadyRef.current = true; + + // Flush any buffered SSH output (welcome banners, etc.) + if (pendingOutputRef.current.length) { + const total = pendingOutputRef.current.reduce( + (n, a) => n + a.length, + 0, + ); + console.log('Flushing buffered output', { + chunks: pendingOutputRef.current.length, + bytes: total, + }); + for (const chunk of pendingOutputRef.current) { + xtermRef.current?.write(chunk); + } + pendingOutputRef.current = []; + xtermRef.current?.flush?.(); + } + + // Focus after ready to pop the soft keyboard (iOS needs this prop) xtermRef.current?.focus?.(); return; } - if (message.type === 'data') { + if (m.type === 'data') { // xterm user input -> SSH // NOTE: msg.data is a fresh Uint8Array starting at offset 0 - void shell?.sendData(message.data.buffer as ArrayBuffer); + console.log('xterm->SSH', { len: m.data.length }); + void shell?.sendData(m.data.buffer as ArrayBuffer); return; } - if (message.type === 'debug') { - console.log('xterm.debug', message.message); + if (m.type === 'debug') { + console.log('xterm.debug', m.message); } }} /> diff --git a/packages/react-native-xtermjs-webview/src-internal/main.tsx b/packages/react-native-xtermjs-webview/src-internal/main.tsx index 2c1a51b..90dca6c 100644 --- a/packages/react-native-xtermjs-webview/src-internal/main.tsx +++ b/packages/react-native-xtermjs-webview/src-internal/main.tsx @@ -19,7 +19,7 @@ const root = document.getElementById('terminal')!; term.open(root); fitAddon.fit(); -// Expose for debugging (optional) +// Expose for debugging (typed via vite-env.d.ts) window.terminal = term; window.fitAddon = fitAddon; @@ -30,7 +30,7 @@ const post = (msg: unknown) => window.ReactNativeWebView?.postMessage?.(JSON.stringify(msg)); /** - * Encode/decode helpers + * Encode helper */ const enc = new TextEncoder(); @@ -50,74 +50,123 @@ term.onData((data /* string */) => { }); /** - * Message handler for RN -> WebView control/data - * We support: write, resize, setFont, setTheme, clear, focus + * RN -> WebView control/data + * Supported: write, resize, setFont, setTheme, setOptions, clear, focus + * NOTE: Never spread term.options (it contains cols/rows). Only set keys you intend. */ window.addEventListener('message', (e: MessageEvent) => { try { - const msg = JSON.parse(e.data); + const msg = JSON.parse(e.data) as + | { type: 'write'; b64?: string; chunks?: string[] } + | { type: 'resize'; cols?: number; rows?: number } + | { type: 'setFont'; family?: string; size?: number } + | { type: 'setTheme'; background?: string; foreground?: string } + | { + type: 'setOptions'; + opts: Partial<{ + cursorBlink: boolean; + scrollback: number; + fontFamily: string; + fontSize: number; + }>; + } + | { type: 'clear' } + | { type: 'focus' }; + if (!msg || typeof msg.type !== 'string') return; switch (msg.type) { case 'write': { - // Either a single b64 or an array of chunks if (typeof msg.b64 === 'string') { const bytes = Base64.toUint8Array(msg.b64); term.write(bytes); + post({ type: 'debug', message: `write(bytes=${bytes.length})` }); } else if (Array.isArray(msg.chunks)) { for (const b64 of msg.chunks) { const bytes = Base64.toUint8Array(b64); term.write(bytes); } + post({ + type: 'debug', + message: `write(chunks=${msg.chunks.length})`, + }); } break; } case 'resize': { + // Prefer fitAddon.fit(); only call resize if explicit cols/rows provided. if (typeof msg.cols === 'number' && typeof msg.rows === 'number') { - try { - term.resize(msg.cols, msg.rows); - } finally { - fitAddon.fit(); - } - } else { - // If cols/rows not provided, try fit - fitAddon.fit(); + term.resize(msg.cols, msg.rows); + post({ type: 'debug', message: `resize(${msg.cols}x${msg.rows})` }); } + fitAddon.fit(); break; } case 'setFont': { const { family, size } = msg; - if (family) document.body.style.fontFamily = family; - if (typeof size === 'number') - document.body.style.fontSize = `${size}px`; - fitAddon.fit(); + const patch: Partial = {}; + if (family) patch.fontFamily = family; + if (typeof size === 'number') patch.fontSize = size; + if (Object.keys(patch).length) { + term.options = patch; // no spread -> avoids cols/rows setters + post({ + type: 'debug', + message: `setFont(${family ?? ''}, ${size ?? ''})`, + }); + fitAddon.fit(); + } break; } case 'setTheme': { const { background, foreground } = msg; - if (background) document.body.style.backgroundColor = background; - // xterm theme API (optional) - term.options = { - ...term.options, - theme: { - ...(term.options.theme ?? {}), - background, - foreground, - }, - }; + const theme: Partial = {}; + if (background) { + theme.background = background; + document.body.style.backgroundColor = background; + } + if (foreground) theme.foreground = foreground; + if (Object.keys(theme).length) { + term.options = { theme }; // set only theme + post({ + type: 'debug', + message: `setTheme(bg=${background ?? ''}, fg=${foreground ?? ''})`, + }); + } + break; + } + + case 'setOptions': { + const opts = msg.opts ?? {}; + // Filter out cols/rows defensively + const { cursorBlink, scrollback, fontFamily, fontSize } = opts; + const patch: Partial = {}; + if (typeof cursorBlink === 'boolean') patch.cursorBlink = cursorBlink; + if (typeof scrollback === 'number') patch.scrollback = scrollback; + if (fontFamily) patch.fontFamily = fontFamily; + if (typeof fontSize === 'number') patch.fontSize = fontSize; + if (Object.keys(patch).length) { + term.options = patch; + post({ + type: 'debug', + message: `setOptions(${Object.keys(patch).join(',')})`, + }); + if (patch.fontFamily || patch.fontSize) fitAddon.fit(); + } break; } case 'clear': { term.clear(); + post({ type: 'debug', message: 'clear()' }); break; } case 'focus': { term.focus(); + post({ type: 'debug', message: 'focus()' }); break; } } @@ -127,7 +176,7 @@ window.addEventListener('message', (e: MessageEvent) => { }); /** - * Handle container resize + * Keep terminal size in sync with container */ new ResizeObserver(() => { try { diff --git a/packages/react-native-xtermjs-webview/src/index.tsx b/packages/react-native-xtermjs-webview/src/index.tsx index 3fd7fe6..1bb99fa 100644 --- a/packages/react-native-xtermjs-webview/src/index.tsx +++ b/packages/react-native-xtermjs-webview/src/index.tsx @@ -16,105 +16,115 @@ type OutboundMessage = | { type: 'resize'; cols?: number; rows?: number } | { type: 'setFont'; family?: string; size?: number } | { type: 'setTheme'; background?: string; foreground?: string } + | { + type: 'setOptions'; + opts: Partial<{ + cursorBlink: boolean; + scrollback: number; + fontFamily: string; + fontSize: number; + }>; + } | { type: 'clear' } | { type: 'focus' }; +export type XtermInbound = + | { type: 'initialized' } + | { type: 'data'; data: Uint8Array } + | { type: 'debug'; message: string }; + export type XtermWebViewHandle = { - /** - * Push raw bytes (Uint8Array) into the terminal. - * Writes are batched (rAF or >=8KB) for performance. - */ - write: (data: Uint8Array) => void; - - /** Force-flush any buffered output immediately. */ - flush: () => void; - - /** Resize the terminal to given cols/rows (optional, fit addon also runs). */ + write: (data: Uint8Array) => void; // bytes in (batched) + flush: () => void; // force-flush outgoing writes resize: (cols?: number, rows?: number) => void; - - /** Set font props inside the WebView page. */ setFont: (family?: string, size?: number) => void; - - /** Set basic theme colors (background/foreground). */ setTheme: (background?: string, foreground?: string) => void; - - /** Clear terminal contents. */ + setOptions: ( + opts: OutboundMessage extends { type: 'setOptions'; opts: infer O } + ? O + : never, + ) => void; clear: () => void; - - /** Focus the terminal input. */ focus: () => void; }; +export interface XtermJsWebViewProps + extends StrictOmit< + React.ComponentProps, + 'source' | 'originWhitelist' | 'onMessage' + > { + ref: React.RefObject; + onMessage?: (msg: XtermInbound) => void; + + // xterm-ish props (applied via setOptions before/after init) + fontFamily?: string; + fontSize?: number; + cursorBlink?: boolean; + scrollback?: number; + themeBackground?: string; + themeForeground?: string; +} + export function XtermJsWebView({ ref, onMessage, + fontFamily, + fontSize, + cursorBlink, + scrollback, + themeBackground, + themeForeground, ...props -}: StrictOmit< - React.ComponentProps, - 'source' | 'originWhitelist' | 'onMessage' -> & { - ref: React.RefObject; - onMessage?: ( - msg: - | { type: 'initialized' } - | { type: 'data'; data: Uint8Array } // input from xterm (user typed) - | { type: 'debug'; message: string }, - ) => void; -}) { - const webViewRef = useRef(null); +}: XtermJsWebViewProps) { + const webRef = useRef(null); - // ---- RN -> WebView message sender via injectJavaScript + window MessageEvent + // ---- RN -> WebView message sender const send = (obj: OutboundMessage) => { const payload = JSON.stringify(obj); console.log('sending msg', payload); const js = `window.dispatchEvent(new MessageEvent('message',{data:${JSON.stringify( payload, )}})); true;`; - webViewRef.current?.injectJavaScript(js); + webRef.current?.injectJavaScript(js); }; // ---- rAF + 8KB coalescer for writes - const writeBufferRef = useRef(null); - const rafIdRef = useRef(null); - const THRESHOLD = 8 * 1024; // 8KB + const bufRef = useRef(null); + const rafRef = useRef(null); + const THRESHOLD = 8 * 1024; const flush = () => { - if (!writeBufferRef.current) return; - const b64 = Base64.fromUint8Array(writeBufferRef.current); - writeBufferRef.current = null; - if (rafIdRef.current != null) { - cancelAnimationFrame(rafIdRef.current); - rafIdRef.current = null; + if (!bufRef.current) return; + const b64 = Base64.fromUint8Array(bufRef.current); + bufRef.current = null; + if (rafRef.current != null) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; } send({ type: 'write', b64 }); }; - const scheduleFlush = () => { - if (rafIdRef.current != null) return; - rafIdRef.current = requestAnimationFrame(() => { - rafIdRef.current = null; + const schedule = () => { + if (rafRef.current != null) return; + rafRef.current = requestAnimationFrame(() => { + rafRef.current = null; flush(); }); }; const write = (data: Uint8Array) => { if (!data || data.length === 0) return; - const chunk = data; // already a fresh Uint8Array per caller - if (!writeBufferRef.current) { - writeBufferRef.current = chunk; + if (!bufRef.current) { + bufRef.current = data; } else { - // concat - const a = writeBufferRef.current; - const merged = new Uint8Array(a.length + chunk.length); + const a = bufRef.current; + const merged = new Uint8Array(a.length + data.length); merged.set(a, 0); - merged.set(chunk, a.length); - writeBufferRef.current = merged; - } - if ((writeBufferRef.current?.length ?? 0) >= THRESHOLD) { - flush(); - } else { - scheduleFlush(); + merged.set(data, a.length); + bufRef.current = merged; } + if ((bufRef.current?.length ?? 0) >= THRESHOLD) flush(); + else schedule(); }; useImperativeHandle(ref, () => ({ @@ -126,6 +136,7 @@ export function XtermJsWebView({ send({ type: 'setFont', family, size }), setTheme: (background?: string, foreground?: string) => send({ type: 'setTheme', background, foreground }), + setOptions: (opts) => send({ type: 'setOptions', opts }), clear: () => send({ type: 'clear' }), focus: () => send({ type: 'focus' }), })); @@ -133,29 +144,46 @@ export function XtermJsWebView({ // Cleanup pending rAF on unmount useEffect(() => { return () => { - if (rafIdRef.current != null) { - cancelAnimationFrame(rafIdRef.current); - rafIdRef.current = null; - } - writeBufferRef.current = null; + if (rafRef.current != null) cancelAnimationFrame(rafRef.current); + rafRef.current = null; + bufRef.current = null; }; }, []); + // Push initial options/theme whenever props change + useEffect(() => { + const opts: Record = {}; + if (typeof cursorBlink === 'boolean') opts.cursorBlink = cursorBlink; + if (typeof scrollback === 'number') opts.scrollback = scrollback; + if (fontFamily) opts.fontFamily = fontFamily; + if (typeof fontSize === 'number') opts.fontSize = fontSize; + if (Object.keys(opts).length) send({ type: 'setOptions', opts }); + }, [cursorBlink, scrollback, fontFamily, fontSize]); + + useEffect(() => { + if (themeBackground || themeForeground) { + send({ + type: 'setTheme', + background: themeBackground, + foreground: themeForeground, + }); + } + }, [themeBackground, themeForeground]); + return ( { + onMessage={(e) => { try { - const msg: InboundMessage = JSON.parse(event.nativeEvent.data); + const msg: InboundMessage = JSON.parse(e.nativeEvent.data); console.log('received msg', msg); if (msg.type === 'initialized') { onMessage?.({ type: 'initialized' }); return; } if (msg.type === 'input') { - // Convert base64 -> bytes for the caller (SSH writer) const bytes = Base64.toUint8Array(msg.b64); onMessage?.({ type: 'data', data: bytes }); return; From beb3b5fc6c07a9bbc8cba726728f2422be089511 Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Wed, 17 Sep 2025 21:08:58 -0400 Subject: [PATCH 09/12] more stuff --- apps/mobile/src/app/(tabs)/shell/detail.tsx | 45 +-- .../src-internal/main.tsx | 310 +++++++++--------- .../src-internal/vite-env.d.ts | 9 - .../src/index.tsx | 2 +- 4 files changed, 176 insertions(+), 190 deletions(-) diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx index cd3619b..85f3c4d 100644 --- a/apps/mobile/src/app/(tabs)/shell/detail.tsx +++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx @@ -19,8 +19,8 @@ export default function TabsShellDetail() { function ShellDetail() { const xtermRef = useRef(null); - const terminalReadyRef = useRef(false); // gate for initial SSH output buffering - const pendingOutputRef = useRef([]); // bytes we got before xterm init + const terminalReadyRef = useRef(false); + const pendingOutputRef = useRef([]); const { connectionId, channelId } = useLocalSearchParams<{ connectionId?: string; @@ -38,24 +38,19 @@ function ShellDetail() { ? RnRussh.getSshShell(String(connectionId), channelIdNum) : undefined; - /** - * SSH -> xterm (remote output) - * If xterm isn't ready yet, buffer and flush on 'initialized'. - */ + // SSH -> xterm (remote output). Buffer until xterm is initialized. useEffect(() => { if (!connection) return; + const xterm = xtermRef.current; const listenerId = connection.addChannelListener((ab: ArrayBuffer) => { const bytes = new Uint8Array(ab); if (!terminalReadyRef.current) { - // Buffer until WebView->xterm has signaled 'initialized' pendingOutputRef.current.push(bytes); - // Debug console.log('SSH->buffer', { len: bytes.length }); return; } - // Forward bytes immediately console.log('SSH->xterm', { len: bytes.length }); xterm?.write(bytes); }); @@ -104,7 +99,7 @@ function ShellDetail() { { - console.log('WebView render process gone, clearing terminal'); - xtermRef.current?.clear?.(); - }} - onContentProcessDidTerminate={() => { - console.log( - 'WKWebView content process terminated, clearing terminal', - ); - xtermRef.current?.clear?.(); - }} - // xterm-flavored props for styling/behavior + // xterm-ish props (applied via setOptions inside the page) fontFamily="Menlo, ui-monospace, monospace" - fontSize={15} + fontSize={18} // bump if it still feels small cursorBlink scrollback={10000} themeBackground={theme.colors.background} themeForeground={theme.colors.textPrimary} - // page load => we can push initial options/theme right away; - // xterm itself will still send 'initialized' once it's truly ready. + onRenderProcessGone={() => { + console.log('WebView render process gone -> clear()'); + xtermRef.current?.clear?.(); + }} + onContentProcessDidTerminate={() => { + console.log('WKWebView content process terminated -> clear()'); + xtermRef.current?.clear?.(); + }} onLoadEnd={() => { console.log('WebView onLoadEnd'); }} @@ -142,7 +133,7 @@ function ShellDetail() { if (m.type === 'initialized') { terminalReadyRef.current = true; - // Flush any buffered SSH output (welcome banners, etc.) + // Flush buffered banner/welcome lines if (pendingOutputRef.current.length) { const total = pendingOutputRef.current.reduce( (n, a) => n + a.length, @@ -159,13 +150,11 @@ function ShellDetail() { xtermRef.current?.flush?.(); } - // Focus after ready to pop the soft keyboard (iOS needs this prop) + // Focus to pop the keyboard (iOS needs the prop we set) xtermRef.current?.focus?.(); return; } if (m.type === 'data') { - // xterm user input -> SSH - // NOTE: msg.data is a fresh Uint8Array starting at offset 0 console.log('xterm->SSH', { len: m.data.length }); void shell?.sendData(m.data.buffer as ArrayBuffer); return; diff --git a/packages/react-native-xtermjs-webview/src-internal/main.tsx b/packages/react-native-xtermjs-webview/src-internal/main.tsx index 90dca6c..7d5272e 100644 --- a/packages/react-native-xtermjs-webview/src-internal/main.tsx +++ b/packages/react-native-xtermjs-webview/src-internal/main.tsx @@ -3,25 +3,16 @@ import { FitAddon } from '@xterm/addon-fit'; import { Base64 } from 'js-base64'; import '@xterm/xterm/css/xterm.css'; -/** - * Xterm setup - */ -const term = new Terminal({ - allowProposedApi: true, - convertEol: true, - scrollback: 10000, - cursorBlink: true, -}); -const fitAddon = new FitAddon(); -term.loadAddon(fitAddon); - -const root = document.getElementById('terminal')!; -term.open(root); -fitAddon.fit(); - -// Expose for debugging (typed via vite-env.d.ts) -window.terminal = term; -window.fitAddon = fitAddon; +declare global { + interface Window { + terminal?: Terminal; + fitAddon?: FitAddon; + terminalWriteBase64?: (data: string) => void; + ReactNativeWebView?: { postMessage?: (data: string) => void }; + __FRESSH_XTERM_BRIDGE__?: boolean; + __FRESSH_XTERM_MSG_HANDLER__?: (e: MessageEvent) => void; + } +} /** * Post typed messages to React Native @@ -30,158 +21,173 @@ const post = (msg: unknown) => window.ReactNativeWebView?.postMessage?.(JSON.stringify(msg)); /** - * Encode helper + * Idempotent boot guard: ensure we only install once. + * If the script happens to run twice (dev reloads, double-mounts), we bail out early. */ -const enc = new TextEncoder(); +if (window.__FRESSH_XTERM_BRIDGE__) { + post({ + type: 'debug', + message: 'bridge already installed; ignoring duplicate boot', + }); +} else { + window.__FRESSH_XTERM_BRIDGE__ = true; -/** - * Initial handshake - */ -setTimeout(() => post({ type: 'initialized' }), 0); + // ---- Xterm setup + const term = new Terminal({ + allowProposedApi: true, + convertEol: true, + scrollback: 10000, + cursorBlink: true, + }); + const fitAddon = new FitAddon(); + term.loadAddon(fitAddon); -/** - * User input from xterm -> RN (SSH) - * Send UTF-8 bytes only (Base64-encoded) - */ -term.onData((data /* string */) => { - const bytes = enc.encode(data); - const b64 = Base64.fromUint8Array(bytes); - post({ type: 'input', b64 }); -}); + const root = document.getElementById('terminal')!; + term.open(root); + fitAddon.fit(); -/** - * RN -> WebView control/data - * Supported: write, resize, setFont, setTheme, setOptions, clear, focus - * NOTE: Never spread term.options (it contains cols/rows). Only set keys you intend. - */ -window.addEventListener('message', (e: MessageEvent) => { - try { - const msg = JSON.parse(e.data) as - | { type: 'write'; b64?: string; chunks?: string[] } - | { type: 'resize'; cols?: number; rows?: number } - | { type: 'setFont'; family?: string; size?: number } - | { type: 'setTheme'; background?: string; foreground?: string } - | { - type: 'setOptions'; - opts: Partial<{ - cursorBlink: boolean; - scrollback: number; - fontFamily: string; - fontSize: number; - }>; - } - | { type: 'clear' } - | { type: 'focus' }; + // Expose for debugging (typed) + window.terminal = term; + window.fitAddon = fitAddon; - if (!msg || typeof msg.type !== 'string') return; + // Encode helper + const enc = new TextEncoder(); - switch (msg.type) { - case 'write': { - if (typeof msg.b64 === 'string') { - const bytes = Base64.toUint8Array(msg.b64); - term.write(bytes); - post({ type: 'debug', message: `write(bytes=${bytes.length})` }); - } else if (Array.isArray(msg.chunks)) { - for (const b64 of msg.chunks) { - const bytes = Base64.toUint8Array(b64); + // Initial handshake (send once) + setTimeout(() => post({ type: 'initialized' }), 8_000); + + // User input from xterm -> RN (SSH) as UTF-8 bytes (Base64) + term.onData((data /* string */) => { + const bytes = enc.encode(data); + const b64 = Base64.fromUint8Array(bytes); + post({ type: 'input', b64 }); + }); + + // Remove old handler if any (just in case) + if (window.__FRESSH_XTERM_MSG_HANDLER__) { + window.removeEventListener('message', window.__FRESSH_XTERM_MSG_HANDLER__!); + } + + // RN -> WebView handler (write, resize, setFont, setTheme, setOptions, clear, focus) + const handler = (e: MessageEvent) => { + try { + const msg = JSON.parse(e.data) as + | { type: 'write'; b64?: string; chunks?: string[] } + | { type: 'resize'; cols?: number; rows?: number } + | { type: 'setFont'; family?: string; size?: number } + | { type: 'setTheme'; background?: string; foreground?: string } + | { + type: 'setOptions'; + opts: Partial<{ + cursorBlink: boolean; + scrollback: number; + fontFamily: string; + fontSize: number; + }>; + } + | { type: 'clear' } + | { type: 'focus' }; + + if (!msg || typeof msg.type !== 'string') return; + + switch (msg.type) { + case 'write': { + if (typeof msg.b64 === 'string') { + const bytes = Base64.toUint8Array(msg.b64); term.write(bytes); + post({ type: 'debug', message: `write(bytes=${bytes.length})` }); + } else if (Array.isArray(msg.chunks)) { + for (const b64 of msg.chunks) { + const bytes = Base64.toUint8Array(b64); + term.write(bytes); + } + post({ + type: 'debug', + message: `write(chunks=${msg.chunks.length})`, + }); } - post({ - type: 'debug', - message: `write(chunks=${msg.chunks.length})`, - }); + break; } - break; - } - case 'resize': { - // Prefer fitAddon.fit(); only call resize if explicit cols/rows provided. - if (typeof msg.cols === 'number' && typeof msg.rows === 'number') { - term.resize(msg.cols, msg.rows); - post({ type: 'debug', message: `resize(${msg.cols}x${msg.rows})` }); - } - fitAddon.fit(); - break; - } - - case 'setFont': { - const { family, size } = msg; - const patch: Partial = {}; - if (family) patch.fontFamily = family; - if (typeof size === 'number') patch.fontSize = size; - if (Object.keys(patch).length) { - term.options = patch; // no spread -> avoids cols/rows setters - post({ - type: 'debug', - message: `setFont(${family ?? ''}, ${size ?? ''})`, - }); + case 'resize': { + if (typeof msg.cols === 'number' && typeof msg.rows === 'number') { + term.resize(msg.cols, msg.rows); + post({ type: 'debug', message: `resize(${msg.cols}x${msg.rows})` }); + } fitAddon.fit(); + break; } - break; - } - case 'setTheme': { - const { background, foreground } = msg; - const theme: Partial = {}; - if (background) { - theme.background = background; - document.body.style.backgroundColor = background; + case 'setFont': { + const { family, size } = msg; + const patch: Partial = {}; + if (family) patch.fontFamily = family; + if (typeof size === 'number') patch.fontSize = size; + if (Object.keys(patch).length) { + term.options = patch; // never spread existing options (avoids cols/rows setters) + post({ + type: 'debug', + message: `setFont(${family ?? ''}, ${size ?? ''})`, + }); + fitAddon.fit(); + } + break; } - if (foreground) theme.foreground = foreground; - if (Object.keys(theme).length) { - term.options = { theme }; // set only theme - post({ - type: 'debug', - message: `setTheme(bg=${background ?? ''}, fg=${foreground ?? ''})`, - }); + + case 'setTheme': { + const { background, foreground } = msg; + const theme: Partial = {}; + if (background) { + theme.background = background; + document.body.style.backgroundColor = background; + } + if (foreground) theme.foreground = foreground; + if (Object.keys(theme).length) { + term.options = { theme }; // set only theme + post({ + type: 'debug', + message: `setTheme(bg=${background ?? ''}, fg=${foreground ?? ''})`, + }); + } + break; } - break; - } - case 'setOptions': { - const opts = msg.opts ?? {}; - // Filter out cols/rows defensively - const { cursorBlink, scrollback, fontFamily, fontSize } = opts; - const patch: Partial = {}; - if (typeof cursorBlink === 'boolean') patch.cursorBlink = cursorBlink; - if (typeof scrollback === 'number') patch.scrollback = scrollback; - if (fontFamily) patch.fontFamily = fontFamily; - if (typeof fontSize === 'number') patch.fontSize = fontSize; - if (Object.keys(patch).length) { - term.options = patch; - post({ - type: 'debug', - message: `setOptions(${Object.keys(patch).join(',')})`, - }); - if (patch.fontFamily || patch.fontSize) fitAddon.fit(); + case 'setOptions': { + const opts = msg.opts ?? {}; + const { cursorBlink, scrollback, fontFamily, fontSize } = opts; + const patch: Partial = {}; + if (typeof cursorBlink === 'boolean') patch.cursorBlink = cursorBlink; + if (typeof scrollback === 'number') patch.scrollback = scrollback; + if (fontFamily) patch.fontFamily = fontFamily; + if (typeof fontSize === 'number') patch.fontSize = fontSize; + if (Object.keys(patch).length) { + term.options = patch; + post({ + type: 'debug', + message: `setOptions(${Object.keys(patch).join(',')})`, + }); + if (patch.fontFamily || patch.fontSize) fitAddon.fit(); + } + break; } - break; - } - case 'clear': { - term.clear(); - post({ type: 'debug', message: 'clear()' }); - break; - } + case 'clear': { + term.clear(); + post({ type: 'debug', message: 'clear()' }); + break; + } - case 'focus': { - term.focus(); - post({ type: 'debug', message: 'focus()' }); - break; + case 'focus': { + term.focus(); + post({ type: 'debug', message: 'focus()' }); + break; + } } + } catch (err) { + post({ type: 'debug', message: `message handler error: ${String(err)}` }); } - } catch (err) { - post({ type: 'debug', message: `message handler error: ${String(err)}` }); - } -}); + }; -/** - * Keep terminal size in sync with container - */ -new ResizeObserver(() => { - try { - fitAddon.fit(); - } catch (err) { - post({ type: 'debug', message: `resize observer error: ${String(err)}` }); - } -}); + window.__FRESSH_XTERM_MSG_HANDLER__ = handler; + window.addEventListener('message', handler); +} diff --git a/packages/react-native-xtermjs-webview/src-internal/vite-env.d.ts b/packages/react-native-xtermjs-webview/src-internal/vite-env.d.ts index 065b340..11f02fe 100644 --- a/packages/react-native-xtermjs-webview/src-internal/vite-env.d.ts +++ b/packages/react-native-xtermjs-webview/src-internal/vite-env.d.ts @@ -1,10 +1 @@ /// - -interface Window { - terminal?: Terminal; - fitAddon?: FitAddon; - terminalWriteBase64?: (data: string) => void; - ReactNativeWebView?: { - postMessage?: (data: string) => void; - }; -} diff --git a/packages/react-native-xtermjs-webview/src/index.tsx b/packages/react-native-xtermjs-webview/src/index.tsx index 1bb99fa..6780d43 100644 --- a/packages/react-native-xtermjs-webview/src/index.tsx +++ b/packages/react-native-xtermjs-webview/src/index.tsx @@ -56,7 +56,7 @@ export interface XtermJsWebViewProps ref: React.RefObject; onMessage?: (msg: XtermInbound) => void; - // xterm-ish props (applied via setOptions before/after init) + // xterm-ish props fontFamily?: string; fontSize?: number; cursorBlink?: boolean; From 0fa28b2134a14740413a4e6c8e9502a5b1ef8ef0 Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:06:54 -0400 Subject: [PATCH 10/12] New rust lib passing lint --- .../rust/uniffi-russh/src/lib.rs | 478 +++++++++++++----- packages/react-native-uniffi-russh/src/api.ts | 381 ++++++++------ 2 files changed, 580 insertions(+), 279 deletions(-) diff --git a/packages/react-native-uniffi-russh/rust/uniffi-russh/src/lib.rs b/packages/react-native-uniffi-russh/rust/uniffi-russh/src/lib.rs index 0653ae9..a72febf 100644 --- a/packages/react-native-uniffi-russh/rust/uniffi-russh/src/lib.rs +++ b/packages/react-native-uniffi-russh/rust/uniffi-russh/src/lib.rs @@ -7,35 +7,22 @@ use std::collections::HashMap; use std::fmt; -use std::sync::{Arc, Mutex, Weak}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::sync::{atomic::{AtomicU64, AtomicUsize, Ordering}, Arc, Mutex, Weak}; +use std::time::{SystemTime, UNIX_EPOCH, Duration}; use rand::rngs::OsRng; use thiserror::Error; -use tokio::sync::Mutex as AsyncMutex; +use tokio::sync::{broadcast, Mutex as AsyncMutex}; use russh::{self, client, ChannelMsg, Disconnect}; use russh::client::{Config as ClientConfig, Handle as ClientHandle}; use russh_keys::{Algorithm as KeyAlgorithm, EcdsaCurve, PrivateKey}; use russh_keys::ssh_key::{self, LineEnding}; -use once_cell::sync::Lazy; +use bytes::Bytes; uniffi::setup_scaffolding!(); -// Simpler aliases to satisfy clippy type-complexity. -type ListenerEntry = (u64, Arc); -type ListenerList = Vec; - -// Type aliases to keep static types simple and satisfy clippy. -type ConnectionId = String; -type ChannelId = u32; -type ShellKey = (ConnectionId, ChannelId); -type ConnMap = HashMap>; -type ShellMap = HashMap>; - -// ---------- Global registries (strong references; lifecycle managed explicitly) ---------- -static CONNECTIONS: Lazy> = Lazy::new(|| Mutex::new(HashMap::new())); -static SHELLS: Lazy> = Lazy::new(|| Mutex::new(HashMap::new())); +// No global registries; handles are the only access points. /// ---------- Types ---------- @@ -130,10 +117,30 @@ pub trait StatusListener: Send + Sync { fn on_change(&self, status: SSHConnectionStatus); } -/// Channel data callback (stdout/stderr unified) +// Stream kind for terminal output +#[derive(Debug, Clone, Copy, PartialEq, uniffi::Enum)] +pub enum StreamKind { Stdout, Stderr } + +#[derive(Debug, Clone, PartialEq, uniffi::Record)] +pub struct TerminalChunk { + pub seq: u64, + pub t_ms: f64, + pub stream: StreamKind, + pub bytes: Vec, +} + +#[derive(Debug, Clone, PartialEq, uniffi::Record)] +pub struct DroppedRange { pub from_seq: u64, pub to_seq: u64 } + +#[derive(Debug, Clone, PartialEq, uniffi::Enum)] +pub enum ShellEvent { + Chunk(TerminalChunk), + Dropped { from_seq: u64, to_seq: u64 }, +} + #[uniffi::export(with_foreign)] -pub trait ChannelListener: Send + Sync { - fn on_data(&self, data: Vec); +pub trait ShellListener: Send + Sync { + fn on_event(&self, ev: ShellEvent); } /// Key types for generation @@ -152,6 +159,38 @@ pub struct StartShellOptions { pub on_status_change: Option>, } +#[derive(Debug, Clone, PartialEq, uniffi::Enum)] +pub enum Cursor { + Head, + TailBytes { bytes: u64 }, + Seq { seq: u64 }, + TimeMs { t_ms: f64 }, + Live, +} + +#[derive(Debug, Clone, PartialEq, uniffi::Record)] +pub struct ListenerOptions { + pub cursor: Cursor, + pub coalesce_ms: Option, +} + +#[derive(Debug, Clone, PartialEq, uniffi::Record)] +pub struct BufferReadResult { + pub chunks: Vec, + pub next_seq: u64, + pub dropped: Option, +} + +#[derive(Debug, Clone, PartialEq, uniffi::Record)] +pub struct BufferStats { + pub ring_bytes: u64, + pub used_bytes: u64, + pub chunks: u64, + pub head_seq: u64, + pub tail_seq: u64, + pub dropped_bytes_total: u64, +} + /// Snapshot of current connection info for property-like access in TS. #[derive(Debug, Clone, PartialEq, uniffi::Record)] pub struct SshConnectionInfo { @@ -186,10 +225,6 @@ pub struct SSHConnection { // Weak self for child sessions to refer back without cycles. self_weak: AsyncMutex>, - - // Data listeners for whatever shell is active. We track by id for removal. - listeners: Arc>, - next_listener_id: Arc>, // simple counter guarded by same kind of mutex } #[derive(uniffi::Object)] @@ -204,20 +239,43 @@ pub struct ShellSession { shell_status_listener: Option>, created_at_ms: f64, pty: PtyType, + + // Ring buffer + ring: Arc>>>, + ring_bytes_capacity: Arc, + used_bytes: Arc>, + dropped_bytes_total: Arc, + head_seq: Arc, + tail_seq: Arc, + + // Live broadcast + sender: broadcast::Sender>, + + // Listener tasks management + listener_tasks: Arc>>>, + next_listener_id: AtomicU64, + default_coalesce_ms: AtomicU64, } impl fmt::Debug for SSHConnection { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let listeners_len = self.listeners.lock().map(|v| v.len()).unwrap_or(0); f.debug_struct("SSHConnection") .field("connection_details", &self.connection_details) .field("created_at_ms", &self.created_at_ms) .field("tcp_established_at_ms", &self.tcp_established_at_ms) - .field("listeners_len", &listeners_len) .finish() } } +// Internal chunk type kept in ring/broadcast +#[derive(Debug)] +struct Chunk { + seq: u64, + t_ms: f64, + stream: StreamKind, + bytes: Bytes, +} + /// Minimal client::Handler. struct NoopHandler; impl client::Handler for NoopHandler { @@ -247,21 +305,6 @@ impl SSHConnection { } } - /// Add a channel listener and get an id you can later remove with. - pub fn add_channel_listener(&self, listener: Arc) -> u64 { - let mut guard = self.listeners.lock().unwrap(); - let mut id_guard = self.next_listener_id.lock().unwrap(); - let id = *id_guard + 1; - *id_guard = id; - guard.push((id, listener)); - id - } - pub fn remove_channel_listener(&self, id: u64) { - if let Ok(mut v) = self.listeners.lock() { - v.retain(|(lid, _)| *lid != id); - } - } - /// Start a shell with the given PTY. Emits only Shell* statuses via options.on_status_change. pub async fn start_shell(&self, opts: StartShellOptions) -> Result, SshError> { // Prevent double-start (safe default). @@ -286,24 +329,60 @@ impl SSHConnection { // Split for read/write; spawn reader. let (mut reader, writer) = ch.split(); - let listeners = self.listeners.clone(); + + // Setup ring + broadcast for this session + let (tx, _rx) = broadcast::channel::>(1024); + let ring = Arc::new(Mutex::new(std::collections::VecDeque::>::new())); + let used_bytes = Arc::new(Mutex::new(0usize)); + let next_seq = Arc::new(AtomicU64::new(1)); + let head_seq = Arc::new(AtomicU64::new(1)); + let tail_seq = Arc::new(AtomicU64::new(0)); + let dropped_bytes_total = Arc::new(AtomicU64::new(0)); + let ring_bytes_capacity = Arc::new(AtomicUsize::new(2 * 1024 * 1024)); // default 2MiB + let default_coalesce_ms = AtomicU64::new(16); // default 16ms + + let ring_clone = ring.clone(); + let used_bytes_clone = used_bytes.clone(); + let tx_clone = tx.clone(); + let ring_bytes_capacity_c = ring_bytes_capacity.clone(); + let dropped_bytes_total_c = dropped_bytes_total.clone(); + let head_seq_c = head_seq.clone(); + let tail_seq_c = tail_seq.clone(); + let next_seq_c = next_seq.clone(); let shell_listener_for_task = shell_status_listener.clone(); let reader_task = tokio::spawn(async move { + let max_chunk = 16 * 1024; // 16KB loop { match reader.wait().await { Some(ChannelMsg::Data { data }) => { - if let Ok(cl) = listeners.lock() { - let snapshot = cl.clone(); - let buf = data.to_vec(); - for (_, l) in snapshot { l.on_data(buf.clone()); } - } + append_and_broadcast( + &data, + StreamKind::Stdout, + &ring_clone, + &used_bytes_clone, + &ring_bytes_capacity_c, + &dropped_bytes_total_c, + &head_seq_c, + &tail_seq_c, + &next_seq_c, + &tx_clone, + max_chunk, + ); } Some(ChannelMsg::ExtendedData { data, .. }) => { - if let Ok(cl) = listeners.lock() { - let snapshot = cl.clone(); - let buf = data.to_vec(); - for (_, l) in snapshot { l.on_data(buf.clone()); } - } + append_and_broadcast( + &data, + StreamKind::Stderr, + &ring_clone, + &used_bytes_clone, + &ring_bytes_capacity_c, + &dropped_bytes_total_c, + &head_seq_c, + &tail_seq_c, + &next_seq_c, + &tx_clone, + max_chunk, + ); } Some(ChannelMsg::Close) | None => { if let Some(sl) = shell_listener_for_task.as_ref() { @@ -311,7 +390,7 @@ impl SSHConnection { } break; } - _ => { /* ignore others */ } + _ => {} } } }); @@ -324,16 +403,20 @@ impl SSHConnection { shell_status_listener, created_at_ms: now_ms(), pty, + ring, + ring_bytes_capacity, + used_bytes, + dropped_bytes_total, + head_seq, + tail_seq, + sender: tx, + listener_tasks: Arc::new(Mutex::new(HashMap::new())), + next_listener_id: AtomicU64::new(1), + default_coalesce_ms, }); *self.shell.lock().await = Some(session.clone()); - // Register shell in global registry - if let Some(parent) = self.self_weak.lock().await.upgrade() { - let key = (parent.connection_id.clone(), channel_id); - if let Ok(mut map) = SHELLS.lock() { map.insert(key, session.clone()); } - } - // Report ShellConnected. if let Some(sl) = session.shell_status_listener.as_ref() { sl.on_change(SSHConnectionStatus::ShellConnected); @@ -342,12 +425,7 @@ impl SSHConnection { Ok(session) } - /// Send bytes to the active shell (stdin). - pub async fn send_data(&self, data: Vec) -> Result<(), SshError> { - let guard = self.shell.lock().await; - let session = guard.as_ref().ok_or(SshError::Disconnected)?; - session.send_data(data).await - } + // Note: send_data now lives on ShellSession // No exported close_shell: shell closure is handled via ShellSession::close() @@ -360,8 +438,6 @@ impl SSHConnection { let h = self.handle.lock().await; h.disconnect(Disconnect::ByApplication, "bye", "").await?; - // Remove from registry after disconnect - if let Ok(mut map) = CONNECTIONS.lock() { map.remove(&self.connection_id); } Ok(()) } } @@ -386,6 +462,160 @@ impl ShellSession { /// Close the associated shell channel and stop its reader task. pub async fn close(&self) -> Result<(), SshError> { self.close_internal().await } + + /// Configure ring buffer policy. + pub async fn set_buffer_policy(&self, ring_bytes: Option, coalesce_ms: Option) { + if let Some(rb) = ring_bytes { self.ring_bytes_capacity.store(rb as usize, Ordering::Relaxed); self.evict_if_needed(); } + if let Some(cm) = coalesce_ms { self.default_coalesce_ms.store(cm as u64, Ordering::Relaxed); } + } + + /// Buffer statistics snapshot. + pub fn buffer_stats(&self) -> BufferStats { + let used = *self.used_bytes.lock().unwrap() as u64; + let chunks = self.ring.lock().map(|q| q.len() as u64).unwrap_or(0); + BufferStats { + ring_bytes: self.ring_bytes_capacity.load(Ordering::Relaxed) as u64, + used_bytes: used, + chunks, + head_seq: self.head_seq.load(Ordering::Relaxed), + tail_seq: self.tail_seq.load(Ordering::Relaxed), + dropped_bytes_total: self.dropped_bytes_total.load(Ordering::Relaxed), + } + } + + /// Current next sequence number. + pub fn current_seq(&self) -> u64 { self.tail_seq.load(Ordering::Relaxed).saturating_add(1) } + + /// Read the ring buffer from a cursor. + pub fn read_buffer(&self, cursor: Cursor, max_bytes: Option) -> BufferReadResult { + let max_total = max_bytes.unwrap_or(512 * 1024) as usize; // default 512KB + let mut out_chunks: Vec = Vec::new(); + let mut dropped: Option = None; + let head_seq_now = self.head_seq.load(Ordering::Relaxed); + let tail_seq_now = self.tail_seq.load(Ordering::Relaxed); + + // Lock ring to determine start and collect arcs, then drop lock. + let (_start_idx_unused, _start_seq, arcs): (usize, u64, Vec>) = { + let ring = self.ring.lock().unwrap(); + let (start_seq, idx) = match cursor { + Cursor::Head => (head_seq_now, 0usize), + Cursor::Seq { seq: mut s } => { + if s < head_seq_now { dropped = Some(DroppedRange { from_seq: s, to_seq: head_seq_now - 1 }); s = head_seq_now; } + let idx = s.saturating_sub(head_seq_now) as usize; + (s, idx.min(ring.len())) + } + Cursor::TimeMs { t_ms: t } => { + // linear scan to find first chunk with t_ms >= t + let mut idx = 0usize; let mut s = head_seq_now; + for (i, ch) in ring.iter().enumerate() { if ch.t_ms >= t { idx = i; s = ch.seq; break; } } + (s, idx) + } + Cursor::TailBytes { bytes: n } => { + // Walk from tail backwards until approx n bytes, then forward. + let mut bytes = 0usize; let mut idx = ring.len(); + for i in (0..ring.len()).rev() { + let b = ring[i].bytes.len(); + if bytes >= n as usize { idx = i + 1; break; } + bytes += b; idx = i; + } + let s = if idx < ring.len() { ring[idx].seq } else { tail_seq_now.saturating_add(1) }; + (s, idx) + } + Cursor::Live => (tail_seq_now.saturating_add(1), ring.len()), + }; + let arcs: Vec> = ring.iter().skip(idx).cloned().collect(); + (idx, start_seq, arcs) + }; + + // Build output respecting max_bytes + let mut total = 0usize; + for ch in arcs { + let len = ch.bytes.len(); + if total + len > max_total { break; } + out_chunks.push(TerminalChunk { seq: ch.seq, t_ms: ch.t_ms, stream: ch.stream, bytes: ch.bytes.clone().to_vec() }); + total += len; + } + let next_seq = if let Some(last) = out_chunks.last() { last.seq + 1 } else { tail_seq_now.saturating_add(1) }; + BufferReadResult { chunks: out_chunks, next_seq, dropped } + } + + /// Add a listener with optional replay and live follow. + pub fn add_listener(&self, listener: Arc, opts: ListenerOptions) -> u64 { + // Synchronous replay phase + let replay = self.read_buffer(opts.cursor.clone(), None); + if let Some(dr) = replay.dropped.as_ref() { listener.on_event(ShellEvent::Dropped { from_seq: dr.from_seq, to_seq: dr.to_seq }); } + for ch in replay.chunks { listener.on_event(ShellEvent::Chunk(ch)); } + + // Live phase + let mut rx = self.sender.subscribe(); + let id = self.next_listener_id.fetch_add(1, Ordering::Relaxed); + let default_coalesce_ms = self.default_coalesce_ms.load(Ordering::Relaxed) as u32; + let coalesce_ms = opts.coalesce_ms.unwrap_or(default_coalesce_ms); + + let handle = tokio::spawn(async move { + let mut last_seq_seen: u64 = replay.next_seq.saturating_sub(1); + let mut acc: Vec = Vec::new(); + let mut acc_stream: Option; + let mut acc_last_seq: u64; + let mut acc_last_t: f64; + let window = Duration::from_millis(coalesce_ms as u64); + let mut pending_drop_from: Option = None; + + loop { + // First receive an item + let first = match rx.recv().await { + Ok(c) => c, + Err(broadcast::error::RecvError::Lagged(_n)) => { pending_drop_from = Some(last_seq_seen.saturating_add(1)); continue; } + Err(broadcast::error::RecvError::Closed) => break, + }; + if let Some(from) = pending_drop_from.take() { + if from <= first.seq.saturating_sub(1) { + listener.on_event(ShellEvent::Dropped { from_seq: from, to_seq: first.seq - 1 }); + } + } + // Start accumulating + acc.clear(); acc_stream = Some(first.stream); acc_last_seq = first.seq; acc_last_t = first.t_ms; acc.extend_from_slice(&first.bytes); + last_seq_seen = first.seq; + + // Drain within window while same stream + let mut deadline = tokio::time::Instant::now() + window; + loop { + let timeout = tokio::time::sleep_until(deadline); + tokio::pin!(timeout); + tokio::select! { + _ = &mut timeout => break, + msg = rx.recv() => { + match msg { + Ok(c) => { + if Some(c.stream) == acc_stream { acc.extend_from_slice(&c.bytes); acc_last_seq = c.seq; acc_last_t = c.t_ms; last_seq_seen = c.seq; } + else { // flush and start new + let chunk = TerminalChunk { seq: acc_last_seq, t_ms: acc_last_t, stream: acc_stream.unwrap_or(StreamKind::Stdout), bytes: std::mem::take(&mut acc) }; + listener.on_event(ShellEvent::Chunk(chunk)); + acc_stream = Some(c.stream); acc_last_seq = c.seq; acc_last_t = c.t_ms; acc.extend_from_slice(&c.bytes); last_seq_seen = c.seq; + deadline = tokio::time::Instant::now() + window; + } + } + Err(broadcast::error::RecvError::Lagged(_n)) => { pending_drop_from = Some(last_seq_seen.saturating_add(1)); break; } + Err(broadcast::error::RecvError::Closed) => { break; } + } + } + } + } + if let Some(s) = acc_stream.take() { + let chunk = TerminalChunk { seq: acc_last_seq, t_ms: acc_last_t, stream: s, bytes: std::mem::take(&mut acc) }; + listener.on_event(ShellEvent::Chunk(chunk)); + } + } + }); + if let Ok(mut map) = self.listener_tasks.lock() { map.insert(id, handle); } + id + } + + pub fn remove_listener(&self, id: u64) { + if let Ok(mut map) = self.listener_tasks.lock() { + if let Some(h) = map.remove(&id) { h.abort(); } + } + } } // Internal lifecycle helpers (not exported via UniFFI) @@ -403,13 +633,22 @@ impl ShellSession { if let Some(current) = guard.as_ref() { if current.channel_id == self.channel_id { *guard = None; } } - // Remove from registry - if let Ok(mut map) = SHELLS.lock() { - map.remove(&(parent.connection_id.clone(), self.channel_id)); - } } Ok(()) } + + fn evict_if_needed(&self) { + let cap = self.ring_bytes_capacity.load(Ordering::Relaxed); + let mut ring = self.ring.lock().unwrap(); + let mut used = self.used_bytes.lock().unwrap(); + while *used > cap { + if let Some(front) = ring.pop_front() { + *used -= front.bytes.len(); + self.dropped_bytes_total.fetch_add(front.bytes.len() as u64, Ordering::Relaxed); + self.head_seq.store(front.seq.saturating_add(1), Ordering::Relaxed); + } else { break; } + } + } } /// ---------- Top-level API ---------- @@ -459,57 +698,12 @@ pub async fn connect(options: ConnectOptions) -> Result, SshE handle: AsyncMutex::new(handle), shell: AsyncMutex::new(None), self_weak: AsyncMutex::new(Weak::new()), - listeners: Arc::new(Mutex::new(Vec::new())), - next_listener_id: Arc::new(Mutex::new(0)), }); // Initialize weak self reference. *conn.self_weak.lock().await = Arc::downgrade(&conn); - // Register connection in global registry (strong ref; explicit lifecycle) - if let Ok(mut map) = CONNECTIONS.lock() { map.insert(conn.connection_id.clone(), conn.clone()); } Ok(conn) } -/// ---------- Registry/listing API ---------- - -#[uniffi::export] -pub fn list_ssh_connections() -> Vec { - // Collect clones outside the lock to avoid holding a MutexGuard across await - let conns: Vec> = CONNECTIONS - .lock() - .map(|map| map.values().cloned().collect()) - .unwrap_or_default(); - let mut out = Vec::with_capacity(conns.len()); - for conn in conns { out.push(conn.info()); } - out -} - -#[uniffi::export] -pub fn list_ssh_shells() -> Vec { - // Collect shells outside the lock to avoid holding a MutexGuard across await - let shells: Vec> = SHELLS - .lock() - .map(|map| map.values().cloned().collect()) - .unwrap_or_default(); - let mut out = Vec::with_capacity(shells.len()); - for shell in shells { out.push(shell.info()); } - out -} - -#[uniffi::export] -pub fn get_ssh_connection(id: String) -> Result, SshError> { - if let Ok(map) = CONNECTIONS.lock() { if let Some(conn) = map.get(&id) { return Ok(conn.clone()); } } - Err(SshError::Disconnected) -} - -// list_ssh_shells_for_connection removed; derive in JS from list_ssh_connections + get_ssh_shell - -#[uniffi::export] -pub fn get_ssh_shell(connection_id: String, channel_id: u32) -> Result, SshError> { - let key = (connection_id, channel_id); - if let Ok(map) = SHELLS.lock() { if let Some(shell) = map.get(&key) { return Ok(shell.clone()); } } - Err(SshError::Disconnected) -} - #[uniffi::export(async_runtime = "tokio")] pub async fn generate_key_pair(key_type: KeyType) -> Result { let mut rng = OsRng; @@ -532,3 +726,53 @@ fn now_ms() -> f64 { .unwrap_or_default(); d.as_millis() as f64 } + +#[allow(clippy::too_many_arguments)] +fn append_and_broadcast( + data: &[u8], + stream: StreamKind, + ring: &Arc>>>, + used_bytes: &Arc>, + ring_bytes_capacity: &Arc, + dropped_bytes_total: &Arc, + head_seq: &Arc, + tail_seq: &Arc, + next_seq: &Arc, + sender: &broadcast::Sender>, + max_chunk: usize, +) { + let mut offset = 0usize; + while offset < data.len() { + let end = (offset + max_chunk).min(data.len()); + let slice = &data[offset..end]; + let seq = next_seq.fetch_add(1, Ordering::Relaxed); + let t_ms = now_ms(); + let chunk = Arc::new(Chunk { seq, t_ms, stream, bytes: Bytes::copy_from_slice(slice) }); + // push to ring + { + let mut q = ring.lock().unwrap(); + q.push_back(chunk.clone()); + } + { + let mut used = used_bytes.lock().unwrap(); + *used += slice.len(); + tail_seq.store(seq, Ordering::Relaxed); + // evict if needed + let cap = ring_bytes_capacity.load(Ordering::Relaxed); + if *used > cap { + let mut q = ring.lock().unwrap(); + while *used > cap { + if let Some(front) = q.pop_front() { + *used -= front.bytes.len(); + dropped_bytes_total.fetch_add(front.bytes.len() as u64, Ordering::Relaxed); + head_seq.store(front.seq.saturating_add(1), Ordering::Relaxed); + } else { break; } + } + } + } + // broadcast + let _ = sender.send(chunk); + + offset = end; + } +} diff --git a/packages/react-native-uniffi-russh/src/api.ts b/packages/react-native-uniffi-russh/src/api.ts index ab6923c..34befea 100644 --- a/packages/react-native-uniffi-russh/src/api.ts +++ b/packages/react-native-uniffi-russh/src/api.ts @@ -1,7 +1,14 @@ /** * We cannot make the generated code match this API exactly because uniffi * - Doesn't support ts literals for rust enums - * - Doesn't support passing a js object with methods and properties to rust + * - Doesn't support passing a js object with methods and properties to or from rust. + * + * The second issue is much harder to get around than the first. + * In practice it means that if you want to pass an object with callbacks and props to rust, it need to be in seperate args. + * If you want to pass an object with callbacks and props from rust to js (like ssh handles), you need to instead only pass an object with callbacks + * just make one of the callbacks a sync info() callback. + * + * Then in this api wrapper we can smooth over those rough edges. * See: - https://jhugman.github.io/uniffi-bindgen-react-native/idioms/callback-interfaces.html */ import * as GeneratedRussh from './index'; @@ -9,6 +16,10 @@ import * as GeneratedRussh from './index'; // #region Ideal API +// ───────────────────────────────────────────────────────────────────────────── +// Core types +// ───────────────────────────────────────────────────────────────────────────── + export type ConnectionDetails = { host: string; port: number; @@ -18,6 +29,17 @@ export type ConnectionDetails = { | { type: 'key'; privateKey: string }; }; +export type SshConnectionStatus = + | 'tcpConnecting' + | 'tcpConnected' + | 'tcpDisconnected' + | 'shellConnecting' + | 'shellConnected' + | 'shellDisconnected'; + +export type PtyType = + | 'Vanilla' | 'Vt100' | 'Vt102' | 'Vt220' | 'Ansi' | 'Xterm' | 'Xterm256'; + export type ConnectOptions = ConnectionDetails & { onStatusChange?: (status: SshConnectionStatus) => void; abortSignal?: AbortSignal; @@ -27,56 +49,97 @@ export type StartShellOptions = { pty: PtyType; onStatusChange?: (status: SshConnectionStatus) => void; abortSignal?: AbortSignal; -} +}; + +export type StreamKind = 'stdout' | 'stderr'; + +export type TerminalChunk = { + /** Monotonic sequence number from the shell start (Rust u64; JS uses number). */ + seq: number; + /** Milliseconds since UNIX epoch (double). */ + tMs: number; + stream: StreamKind; + bytes: Uint8Array; +}; + +export type DropNotice = { kind: 'dropped'; fromSeq: number; toSeq: number }; +export type ListenerEvent = TerminalChunk | DropNotice; + +export type Cursor = + | { mode: 'head' } // earliest available in ring + | { mode: 'tailBytes'; bytes: number } // last N bytes (best-effort) + | { mode: 'seq'; seq: number } // from a given sequence + | { mode: 'time'; tMs: number } // from timestamp + | { mode: 'live' }; // no replay, live only + +export type ListenerOptions = { + cursor: Cursor; + /** Optional per-listener coalescing window in ms (e.g., 10–25). */ + coalesceMs?: number; +}; + +export type BufferStats = { + ringBytes: number; // configured capacity + usedBytes: number; // current usage + chunks: number; // chunks kept + headSeq: number; // oldest seq retained + tailSeq: number; // newest seq retained + droppedBytesTotal: number; // cumulative eviction +}; + +export type BufferReadResult = { + chunks: TerminalChunk[]; + nextSeq: number; + dropped?: { fromSeq: number; toSeq: number }; +}; + +// ───────────────────────────────────────────────────────────────────────────── +// Handles +// ───────────────────────────────────────────────────────────────────────────── + export type SshConnection = { - connectionId: string; + readonly connectionId: string; readonly createdAtMs: number; readonly tcpEstablishedAtMs: number; readonly connectionDetails: ConnectionDetails; - startShell: (params: StartShellOptions) => Promise; - addChannelListener: (listener: (data: ArrayBuffer) => void) => bigint; - removeChannelListener: (id: bigint) => void; - disconnect: (params?: { signal: AbortSignal }) => Promise; + + startShell: (opts: StartShellOptions) => Promise; + disconnect: (opts?: { signal?: AbortSignal }) => Promise; }; -export type SshShellSession = { +export type SshShell = { readonly channelId: number; readonly createdAtMs: number; - readonly pty: GeneratedRussh.PtyType; + readonly pty: PtyType; readonly connectionId: string; - sendData: ( - data: ArrayBuffer, - options?: { signal: AbortSignal } - ) => Promise; - close: (params?: { signal: AbortSignal }) => Promise; + + // I/O + sendData: (data: ArrayBuffer, opts?: { signal?: AbortSignal }) => Promise; + close: (opts?: { signal?: AbortSignal }) => Promise; + + // Buffer policy & stats + setBufferPolicy: (policy: { ringBytes?: number; coalesceMs?: number }) => Promise; + bufferStats: () => Promise; + currentSeq: () => Promise; + + // Replay + live + readBuffer: (cursor: Cursor, maxBytes?: number) => Promise; + addListener: ( + cb: (ev: ListenerEvent) => void, + opts: ListenerOptions + ) => bigint; + removeListener: (id: bigint) => void; }; - type RusshApi = { - connect: (options: ConnectOptions) => Promise; - - getSshConnection: (id: string) => SshConnection | undefined; - getSshShell: (connectionId: string, channelId: number) => SshShellSession | undefined; - listSshConnections: () => SshConnection[]; - listSshShells: () => SshShellSession[]; - listSshConnectionsWithShells: () => (SshConnection & { shells: SshShellSession[] })[]; - - generateKeyPair: (type: PrivateKeyType) => Promise; - uniffiInitAsync: () => Promise; -} + connect: (opts: ConnectOptions) => Promise; + generateKeyPair: (type: 'rsa' | 'ecdsa' | 'ed25519') => Promise; +}; // #endregion -// #region Weird stuff we have to do to get uniffi to have that ideal API - -const privateKeyTypeLiteralToEnum = { - rsa: GeneratedRussh.KeyType.Rsa, - ecdsa: GeneratedRussh.KeyType.Ecdsa, - ed25519: GeneratedRussh.KeyType.Ed25519, -} as const satisfies Record; -export type PrivateKeyType = keyof typeof privateKeyTypeLiteralToEnum; - +// #region Wrapper to match the ideal API const ptyTypeLiteralToEnum = { Vanilla: GeneratedRussh.PtyType.Vanilla, @@ -87,8 +150,16 @@ const ptyTypeLiteralToEnum = { Xterm: GeneratedRussh.PtyType.Xterm, Xterm256: GeneratedRussh.PtyType.Xterm256, } as const satisfies Record; -export type PtyType = keyof typeof ptyTypeLiteralToEnum; +const ptyEnumToLiteral: Record = { + [GeneratedRussh.PtyType.Vanilla]: 'Vanilla', + [GeneratedRussh.PtyType.Vt100]: 'Vt100', + [GeneratedRussh.PtyType.Vt102]: 'Vt102', + [GeneratedRussh.PtyType.Vt220]: 'Vt220', + [GeneratedRussh.PtyType.Ansi]: 'Ansi', + [GeneratedRussh.PtyType.Xterm]: 'Xterm', + [GeneratedRussh.PtyType.Xterm256]: 'Xterm256', +}; const sshConnStatusEnumToLiteral = { [GeneratedRussh.SshConnectionStatus.TcpConnecting]: 'tcpConnecting', @@ -97,160 +168,151 @@ const sshConnStatusEnumToLiteral = { [GeneratedRussh.SshConnectionStatus.ShellConnecting]: 'shellConnecting', [GeneratedRussh.SshConnectionStatus.ShellConnected]: 'shellConnected', [GeneratedRussh.SshConnectionStatus.ShellDisconnected]: 'shellDisconnected', -} as const satisfies Record; -export type SshConnectionStatus = (typeof sshConnStatusEnumToLiteral)[keyof typeof sshConnStatusEnumToLiteral]; +} as const satisfies Record; +const streamEnumToLiteral = { + [GeneratedRussh.StreamKind.Stdout]: 'stdout', + [GeneratedRussh.StreamKind.Stderr]: 'stderr', +} as const satisfies Record; function generatedConnDetailsToIdeal(details: GeneratedRussh.ConnectionDetails): ConnectionDetails { + const security: ConnectionDetails['security'] = details.security instanceof GeneratedRussh.Security.Password + ? { type: 'password', password: details.security.inner.password } + : { type: 'key', privateKey: details.security.inner.keyId }; + return { host: details.host, port: details.port, username: details.username, security }; +} + +function cursorToGenerated(cursor: Cursor): GeneratedRussh.Cursor { + switch (cursor.mode) { + case 'head': + return new GeneratedRussh.Cursor.Head(); + case 'tailBytes': + return new GeneratedRussh.Cursor.TailBytes({ bytes: BigInt(cursor.bytes) }); + case 'seq': + return new GeneratedRussh.Cursor.Seq({ seq: BigInt(cursor.seq) }); + case 'time': + return new GeneratedRussh.Cursor.TimeMs({ tMs: cursor.tMs }); + case 'live': + return new GeneratedRussh.Cursor.Live(); + } +} + +function toTerminalChunk(ch: GeneratedRussh.TerminalChunk): TerminalChunk { return { - host: details.host, - port: details.port, - username: details.username, - security: details.security instanceof GeneratedRussh.Security.Password ? { type: 'password', password: details.security.inner.password } : { type: 'key', privateKey: details.security.inner.keyId }, + seq: Number(ch.seq), + tMs: ch.tMs, + stream: streamEnumToLiteral[ch.stream], + bytes: new Uint8Array(ch.bytes as any), }; } -function wrapConnection(conn: GeneratedRussh.SshConnectionInterface): SshConnection { - // Wrap startShell in-place to preserve the UniFFI object's internal pointer. - const originalStartShell = conn.startShell.bind(conn); - const betterStartShell = async (params: StartShellOptions) => { - const shell = await originalStartShell( - { - pty: ptyTypeLiteralToEnum[params.pty], - onStatusChange: params.onStatusChange - ? { onChange: (statusEnum) => params.onStatusChange?.(sshConnStatusEnumToLiteral[statusEnum]!) } - : undefined, - }, - params.abortSignal ? { signal: params.abortSignal } : undefined, - ); - return wrapShellSession(shell); - }; - - // Accept a function for onData and adapt to the generated listener object. - const originalAddChannelListener = conn.addChannelListener.bind(conn); - const betterAddChannelListener = (listener: (data: ArrayBuffer) => void) => - originalAddChannelListener({ onData: listener }); - - const connInfo = conn.info(); - return { - connectionId: connInfo.connectionId, - connectionDetails: generatedConnDetailsToIdeal(connInfo.connectionDetails), - createdAtMs: connInfo.createdAtMs, - tcpEstablishedAtMs: connInfo.tcpEstablishedAtMs, - startShell: betterStartShell, - addChannelListener: betterAddChannelListener, - removeChannelListener: conn.removeChannelListener.bind(conn), - disconnect: conn.disconnect.bind(conn), - }; -} - -function wrapShellSession(shell: GeneratedRussh.ShellSessionInterface): SshShellSession { +function wrapShellSession(shell: GeneratedRussh.ShellSessionInterface): SshShell { const info = shell.info(); + const setBufferPolicy: SshShell['setBufferPolicy'] = async (policy) => { + await shell.setBufferPolicy(policy.ringBytes != null ? BigInt(policy.ringBytes) : undefined, policy.coalesceMs); + }; + + const bufferStats: SshShell['bufferStats'] = async () => { + const s = shell.bufferStats(); + return { + ringBytes: Number(s.ringBytes), + usedBytes: Number(s.usedBytes), + chunks: Number(s.chunks), + headSeq: Number(s.headSeq), + tailSeq: Number(s.tailSeq), + droppedBytesTotal: Number(s.droppedBytesTotal), + }; + }; + + const readBuffer: SshShell['readBuffer'] = async (cursor, maxBytes) => { + const res = shell.readBuffer(cursorToGenerated(cursor), maxBytes != null ? BigInt(maxBytes) : undefined); + return { + chunks: res.chunks.map(toTerminalChunk), + nextSeq: Number(res.nextSeq), + dropped: res.dropped ? { fromSeq: Number(res.dropped.fromSeq), toSeq: Number(res.dropped.toSeq) } : undefined, + } satisfies BufferReadResult; + }; + + const addListener: SshShell['addListener'] = (cb, opts) => { + const listener = { + onEvent: (ev: GeneratedRussh.ShellEvent) => { + if (ev instanceof GeneratedRussh.ShellEvent.Chunk) { + cb(toTerminalChunk(ev.inner[0]!)); + } else if (ev instanceof GeneratedRussh.ShellEvent.Dropped) { + cb({ kind: 'dropped', fromSeq: Number(ev.inner.fromSeq), toSeq: Number(ev.inner.toSeq) }); + } + } + } satisfies GeneratedRussh.ShellListener; + + const id = shell.addListener(listener, { cursor: cursorToGenerated(opts.cursor), coalesceMs: opts.coalesceMs }); + return BigInt(id); + }; + return { channelId: info.channelId, createdAtMs: info.createdAtMs, - pty: info.pty, + pty: ptyEnumToLiteral[info.pty], connectionId: info.connectionId, - sendData: shell.sendData.bind(shell), - close: shell.close.bind(shell) + sendData: (data, o) => shell.sendData(data, o?.signal ? { signal: o.signal } : undefined), + close: (o) => shell.close(o?.signal ? { signal: o.signal } : undefined), + setBufferPolicy, + bufferStats, + currentSeq: async () => Number(shell.currentSeq()), + readBuffer, + addListener, + removeListener: (id) => shell.removeListener(id), + }; +} + +function wrapConnection(conn: GeneratedRussh.SshConnectionInterface): SshConnection { + const inf = conn.info(); + return { + connectionId: inf.connectionId, + connectionDetails: generatedConnDetailsToIdeal(inf.connectionDetails), + createdAtMs: inf.createdAtMs, + tcpEstablishedAtMs: inf.tcpEstablishedAtMs, + startShell: async (params) => { + const shell = await conn.startShell( + { + pty: ptyTypeLiteralToEnum[params.pty], + onStatusChange: params.onStatusChange + ? { onChange: (statusEnum) => params.onStatusChange!(sshConnStatusEnumToLiteral[statusEnum]) } + : undefined, + }, + params.abortSignal ? { signal: params.abortSignal } : undefined, + ); + return wrapShellSession(shell); + }, + disconnect: (opts) => conn.disconnect(opts?.signal ? { signal: opts.signal } : undefined), }; } async function connect(options: ConnectOptions): Promise { const security = options.security.type === 'password' - ? new GeneratedRussh.Security.Password({ - password: options.security.password, - }) + ? new GeneratedRussh.Security.Password({ password: options.security.password }) : new GeneratedRussh.Security.Key({ keyId: options.security.privateKey }); - const sshConnectionInterface = await GeneratedRussh.connect( + const sshConnection = await GeneratedRussh.connect( { host: options.host, port: options.port, username: options.username, security, onStatusChange: options.onStatusChange ? { - onChange: (statusEnum) => { - const tsLiteral = sshConnStatusEnumToLiteral[statusEnum]; - if (!tsLiteral) throw new Error(`Invalid status enum: ${statusEnum}`); - options.onStatusChange?.(tsLiteral); - }, + onChange: (statusEnum) => options.onStatusChange!(sshConnStatusEnumToLiteral[statusEnum]) } : undefined, }, - options.abortSignal - ? { - signal: options.abortSignal, - } - : undefined + options.abortSignal ? { signal: options.abortSignal } : undefined, ); - return wrapConnection(sshConnectionInterface); + return wrapConnection(sshConnection); } -// Optional registry lookups: return undefined if not found/disconnected -function getSshConnection(id: string): SshConnection | undefined { - try { - const conn = GeneratedRussh.getSshConnection(id); - return wrapConnection(conn); - } catch { - return undefined; - } +async function generateKeyPair(type: 'rsa' | 'ecdsa' | 'ed25519') { + const map = { rsa: GeneratedRussh.KeyType.Rsa, ecdsa: GeneratedRussh.KeyType.Ecdsa, ed25519: GeneratedRussh.KeyType.Ed25519 } as const; + return GeneratedRussh.generateKeyPair(map[type]); } -function getSshShell(connectionId: string, channelId: number): SshShellSession | undefined { - try { - const shell = GeneratedRussh.getSshShell(connectionId, channelId); - return wrapShellSession(shell); - } catch { - return undefined; - } -} - -function listSshConnections(): SshConnection[] { - const infos = GeneratedRussh.listSshConnections(); - const out: SshConnection[] = []; - for (const info of infos) { - try { - const conn = GeneratedRussh.getSshConnection(info.connectionId); - out.push(wrapConnection(conn)); - } catch { - // ignore entries that no longer exist between snapshot and lookup - } - } - return out; -} - -function listSshShells(): SshShellSession[] { - const infos = GeneratedRussh.listSshShells(); - const out: SshShellSession[] = []; - for (const info of infos) { - try { - const shell = GeneratedRussh.getSshShell(info.connectionId, info.channelId); - out.push(wrapShellSession(shell)); - } catch { - // ignore entries that no longer exist between snapshot and lookup - } - } - return out; -} - -/** - * TODO: This feels a bit hacky. It is probably more effecient to do this join in rust and send - * the joined result to the app. - */ -function listSshConnectionsWithShells(): (SshConnection & { shells: SshShellSession[] })[] { - const connections = listSshConnections(); - const shells = listSshShells(); - return connections.map(connection => ({ - ...connection, - shells: shells.filter(shell => shell.connectionId === connection.connectionId), - })); -} - - -async function generateKeyPair(type: PrivateKeyType) { - return GeneratedRussh.generateKeyPair(privateKeyTypeLiteralToEnum[type]); -} // #endregion @@ -258,9 +320,4 @@ export const RnRussh = { uniffiInitAsync: GeneratedRussh.uniffiInitAsync, connect, generateKeyPair, - getSshConnection, - listSshConnections, - listSshShells, - listSshConnectionsWithShells, - getSshShell, } satisfies RusshApi; From 7c448e2ec373d8381f0263036f5dc9456f6ca0f5 Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:12:39 -0400 Subject: [PATCH 11/12] new broken --- apps/mobile/src/app/(tabs)/shell/detail.tsx | 113 +++++++++++------- apps/mobile/src/app/(tabs)/shell/index.tsx | 26 ++-- apps/mobile/src/lib/query-fns.ts | 28 +++-- apps/mobile/src/lib/ssh-registry.ts | 108 +++++++++++++++++ .../src/index.tsx | 11 ++ 5 files changed, 215 insertions(+), 71 deletions(-) create mode 100644 apps/mobile/src/lib/ssh-registry.ts diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx index 85f3c4d..8915c54 100644 --- a/apps/mobile/src/app/(tabs)/shell/detail.tsx +++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx @@ -1,26 +1,56 @@ import { Ionicons } from '@expo/vector-icons'; -import { RnRussh } from '@fressh/react-native-uniffi-russh'; +import { + type ListenerEvent, + type TerminalChunk, +} from '@fressh/react-native-uniffi-russh'; import { XtermJsWebView, type XtermWebViewHandle, } from '@fressh/react-native-xtermjs-webview'; import { useQueryClient } from '@tanstack/react-query'; -import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; -import React, { useEffect, useRef } from 'react'; -import { Pressable, View } from 'react-native'; +import { + Stack, + useLocalSearchParams, + useRouter, + useFocusEffect, +} from 'expo-router'; +import React, { startTransition, useEffect, useRef, useState } from 'react'; +import { Pressable, View, Text } from 'react-native'; + import { SafeAreaView } from 'react-native-safe-area-context'; import { disconnectSshConnectionAndInvalidateQuery } from '@/lib/query-fns'; +import { getSession } from '@/lib/ssh-registry'; import { useTheme } from '@/lib/theme'; export default function TabsShellDetail() { + const [ready, setReady] = useState(false); + + useFocusEffect( + React.useCallback(() => { + startTransition(() => setReady(true)); // React 19: non-urgent + + return () => setReady(false); + }, []), + ); + + if (!ready) return ; return ; } +function RouteSkeleton() { + return ( + + Loading + + ); +} + function ShellDetail() { const xtermRef = useRef(null); const terminalReadyRef = useRef(false); - const pendingOutputRef = useRef([]); + // Legacy buffer no longer used; relying on Rust ring for replay + const listenerIdRef = useRef(null); const { connectionId, channelId } = useLocalSearchParams<{ connectionId?: string; @@ -30,36 +60,23 @@ function ShellDetail() { const theme = useTheme(); const channelIdNum = Number(channelId); - const connection = connectionId - ? RnRussh.getSshConnection(String(connectionId)) - : undefined; - const shell = + const sess = connectionId && channelId - ? RnRussh.getSshShell(String(connectionId), channelIdNum) + ? getSession(String(connectionId), channelIdNum) : undefined; + const connection = sess?.connection; + const shell = sess?.shell; - // SSH -> xterm (remote output). Buffer until xterm is initialized. + // SSH -> xterm: on initialized, replay ring head then attach live listener useEffect(() => { - if (!connection) return; - const xterm = xtermRef.current; - - const listenerId = connection.addChannelListener((ab: ArrayBuffer) => { - const bytes = new Uint8Array(ab); - if (!terminalReadyRef.current) { - pendingOutputRef.current.push(bytes); - console.log('SSH->buffer', { len: bytes.length }); - return; - } - console.log('SSH->xterm', { len: bytes.length }); - xterm?.write(bytes); - }); - return () => { - connection.removeChannelListener(listenerId); + if (shell && listenerIdRef.current != null) + shell.removeListener(listenerIdRef.current); + listenerIdRef.current = null; xterm?.flush?.(); }; - }, [connection]); + }, [shell]); const queryClient = useQueryClient(); @@ -133,21 +150,33 @@ function ShellDetail() { if (m.type === 'initialized') { terminalReadyRef.current = true; - // Flush buffered banner/welcome lines - if (pendingOutputRef.current.length) { - const total = pendingOutputRef.current.reduce( - (n, a) => n + a.length, - 0, - ); - console.log('Flushing buffered output', { - chunks: pendingOutputRef.current.length, - bytes: total, - }); - for (const chunk of pendingOutputRef.current) { - xtermRef.current?.write(chunk); - } - pendingOutputRef.current = []; - xtermRef.current?.flush?.(); + // Replay from head, then attach live listener + if (shell) { + void (async () => { + const res = await shell.readBuffer({ mode: 'head' }); + console.log('readBuffer(head)', { + chunks: res.chunks.length, + nextSeq: res.nextSeq, + dropped: res.dropped, + }); + if (res.chunks.length) { + const chunks = res.chunks.map((c) => c.bytes); + xtermRef.current?.writeMany?.(chunks); + xtermRef.current?.flush?.(); + } + const id = shell.addListener( + (ev: ListenerEvent) => { + if ('kind' in ev && ev.kind === 'dropped') { + console.log('listener.dropped', ev); + return; + } + const chunk = ev as TerminalChunk; + xtermRef.current?.write(chunk.bytes); + }, + { cursor: { mode: 'live' } }, + ); + listenerIdRef.current = id; + })(); } // Focus to pop the keyboard (iOS needs the prop we set) diff --git a/apps/mobile/src/app/(tabs)/shell/index.tsx b/apps/mobile/src/app/(tabs)/shell/index.tsx index f364ff0..20ca6d3 100644 --- a/apps/mobile/src/app/(tabs)/shell/index.tsx +++ b/apps/mobile/src/app/(tabs)/shell/index.tsx @@ -1,8 +1,5 @@ import { Ionicons } from '@expo/vector-icons'; -import { - type RnRussh, - type SshConnection, -} from '@fressh/react-native-uniffi-russh'; +import { type SshConnection } from '@fressh/react-native-uniffi-russh'; import { FlashList } from '@shopify/flash-list'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { formatDistanceToNow } from 'date-fns'; @@ -24,6 +21,7 @@ import { listSshShellsQueryOptions, type ShellWithConnection, } from '@/lib/query-fns'; +import { type listConnectionsWithShells as registryList } from '@/lib/ssh-registry'; import { useTheme } from '@/lib/theme'; export default function TabsShellList() { @@ -80,11 +78,9 @@ type ActionTarget = connection: SshConnection; }; -function LoadedState({ - connections, -}: { - connections: ReturnType; -}) { +type ConnectionsList = ReturnType; + +function LoadedState({ connections }: { connections: ConnectionsList }) { const [actionTarget, setActionTarget] = React.useState( null, ); @@ -137,9 +133,7 @@ function FlatView({ connectionsWithShells, setActionTarget, }: { - connectionsWithShells: ReturnType< - typeof RnRussh.listSshConnectionsWithShells - >; + connectionsWithShells: ConnectionsList; setActionTarget: (target: ActionTarget) => void; }) { const flatShells = React.useMemo(() => { @@ -149,7 +143,7 @@ function FlatView({ }, []); }, [connectionsWithShells]); return ( - data={flatShells} keyExtractor={(item) => `${item.connectionId}:${item.channelId}`} renderItem={({ item }) => ( @@ -176,15 +170,13 @@ function GroupedView({ connectionsWithShells, setActionTarget, }: { - connectionsWithShells: ReturnType< - typeof RnRussh.listSshConnectionsWithShells - >; + connectionsWithShells: ConnectionsList; setActionTarget: (target: ActionTarget) => void; }) { const theme = useTheme(); const [expanded, setExpanded] = React.useState>({}); return ( - data={connectionsWithShells} // estimatedItemSize={80} keyExtractor={(item) => item.connectionId} diff --git a/apps/mobile/src/lib/query-fns.ts b/apps/mobile/src/lib/query-fns.ts index 639600f..76c164d 100644 --- a/apps/mobile/src/lib/query-fns.ts +++ b/apps/mobile/src/lib/query-fns.ts @@ -1,8 +1,4 @@ -import { - RnRussh, - type SshConnection, - type SshShellSession, -} from '@fressh/react-native-uniffi-russh'; +import { RnRussh } from '@fressh/react-native-uniffi-russh'; import { queryOptions, useMutation, @@ -11,6 +7,11 @@ import { } from '@tanstack/react-query'; import { useRouter } from 'expo-router'; import { secretsManager, type InputConnectionDetails } from './secrets-manager'; +import { + listConnectionsWithShells as registryList, + registerSession, + type ShellWithConnection, +} from './ssh-registry'; import { AbortSignalTimeout } from './utils'; export const useSshConnMutation = () => { @@ -57,6 +58,9 @@ export const useSshConnMutation = () => { `${sshConnection.connectionDetails.username}@${sshConnection.connectionDetails.host}:${sshConnection.connectionDetails.port}|${Math.floor(sshConnection.createdAtMs)}`; console.log('Connected to SSH server', connectionId, channelId); + // Track in registry for app use + registerSession(sshConnection, shellInterface); + await queryClient.invalidateQueries({ queryKey: listSshShellsQueryOptions.queryKey, }); @@ -77,19 +81,17 @@ export const useSshConnMutation = () => { export const listSshShellsQueryOptions = queryOptions({ queryKey: ['ssh-shells'], - queryFn: () => RnRussh.listSshConnectionsWithShells(), + queryFn: () => registryList(), }); -export type ShellWithConnection = SshShellSession & { - connection: SshConnection; -}; +export type { ShellWithConnection }; export const closeSshShellAndInvalidateQuery = async (params: { channelId: number; connectionId: string; queryClient: QueryClient; }) => { - const currentActiveShells = RnRussh.listSshConnectionsWithShells(); + const currentActiveShells = registryList(); const connection = currentActiveShells.find( (c) => c.connectionId === params.connectionId, ); @@ -97,7 +99,6 @@ export const closeSshShellAndInvalidateQuery = async (params: { const shell = connection.shells.find((s) => s.channelId === params.channelId); if (!shell) throw new Error('Shell not found'); await shell.close(); - if (connection.shells.length <= 1) await connection.disconnect(); await params.queryClient.invalidateQueries({ queryKey: listSshShellsQueryOptions.queryKey, }); @@ -107,7 +108,10 @@ export const disconnectSshConnectionAndInvalidateQuery = async (params: { connectionId: string; queryClient: QueryClient; }) => { - const connection = RnRussh.getSshConnection(params.connectionId); + const currentActiveShells = registryList(); + const connection = currentActiveShells.find( + (c) => c.connectionId === params.connectionId, + ); if (!connection) throw new Error('Connection not found'); await connection.disconnect(); await params.queryClient.invalidateQueries({ diff --git a/apps/mobile/src/lib/ssh-registry.ts b/apps/mobile/src/lib/ssh-registry.ts new file mode 100644 index 0000000..ec3f6b2 --- /dev/null +++ b/apps/mobile/src/lib/ssh-registry.ts @@ -0,0 +1,108 @@ +import { + RnRussh, + type SshConnection, + type SshShell, +} from '@fressh/react-native-uniffi-russh'; + +// Simple in-memory registry owned by JS to track active handles. +// Keyed by `${connectionId}:${channelId}`. + +export type SessionKey = string; + +export type StoredSession = { + connection: SshConnection; + shell: SshShell; +}; + +const sessions = new Map(); + +export function makeSessionKey( + connectionId: string, + channelId: number, +): SessionKey { + return `${connectionId}:${channelId}`; +} + +export function registerSession( + connection: SshConnection, + shell: SshShell, +): SessionKey { + const key = makeSessionKey(connection.connectionId, shell.channelId); + sessions.set(key, { connection, shell }); + return key; +} + +export function getSession( + connectionId: string, + channelId: number, +): StoredSession | undefined { + return sessions.get(makeSessionKey(connectionId, channelId)); +} + +export function removeSession(connectionId: string, channelId: number): void { + sessions.delete(makeSessionKey(connectionId, channelId)); +} + +export function listSessions(): StoredSession[] { + return Array.from(sessions.values()); +} + +// Legacy list view expected shape +export type ShellWithConnection = StoredSession['shell'] & { + connection: SshConnection; +}; + +export function listConnectionsWithShells(): (SshConnection & { + shells: StoredSession['shell'][]; +})[] { + // Group shells by connection + const byConn = new Map(); + for (const { connection, shell } of sessions.values()) { + const g = byConn.get(connection.connectionId) ?? { + conn: connection, + shells: [], + }; + g.shells.push(shell); + byConn.set(connection.connectionId, g); + } + return Array.from(byConn.values()).map(({ conn, shells }) => ({ + ...conn, + shells, + })); +} + +// Convenience helpers for flows +export async function connectAndStart( + details: Parameters[0], +) { + const conn = await RnRussh.connect(details); + const shell = await conn.startShell({ pty: 'Xterm' }); + registerSession(conn, shell); + return { conn, shell }; +} + +export async function closeShell(connectionId: string, channelId: number) { + const sess = getSession(connectionId, channelId); + if (!sess) return; + await sess.shell.close(); + removeSession(connectionId, channelId); +} + +export async function disconnectConnection(connectionId: string) { + const remaining = Array.from(sessions.entries()).filter( + ([, v]) => v.connection.connectionId === connectionId, + ); + for (const [key, sess] of remaining) { + try { + await sess.shell.close(); + } catch {} + sessions.delete(key); + } + // Find one connection handle for this id to disconnect + const conn = remaining[0]?.[1].connection; + if (conn) { + try { + await conn.disconnect(); + } catch {} + } +} diff --git a/packages/react-native-xtermjs-webview/src/index.tsx b/packages/react-native-xtermjs-webview/src/index.tsx index 6780d43..c0fff7d 100644 --- a/packages/react-native-xtermjs-webview/src/index.tsx +++ b/packages/react-native-xtermjs-webview/src/index.tsx @@ -35,6 +35,8 @@ export type XtermInbound = export type XtermWebViewHandle = { write: (data: Uint8Array) => void; // bytes in (batched) + // Efficiently write many chunks in one postMessage (for initial replay) + writeMany: (chunks: Uint8Array[]) => void; flush: () => void; // force-flush outgoing writes resize: (cols?: number, rows?: number) => void; setFont: (family?: string, size?: number) => void; @@ -127,8 +129,17 @@ export function XtermJsWebView({ else schedule(); }; + const writeMany = (chunks: Uint8Array[]) => { + if (!chunks || chunks.length === 0) return; + // Ensure any pending small buffered write is flushed before bulk write + flush(); + const b64s = chunks.map((c) => Base64.fromUint8Array(c)); + send({ type: 'write', chunks: b64s }); + }; + useImperativeHandle(ref, () => ({ write, + writeMany, flush, resize: (cols?: number, rows?: number) => send({ type: 'resize', cols, rows }), From 808f476bac05d204d46f57e314d56633f6bd3f11 Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Thu, 18 Sep 2025 00:17:35 -0400 Subject: [PATCH 12/12] Working xtermjs! --- apps/mobile/src/app/(tabs)/shell/detail.tsx | 9 ++- .../rust/uniffi-russh/src/lib.rs | 55 +++++++++++++------ packages/react-native-uniffi-russh/src/api.ts | 11 +++- .../src-internal/main.tsx | 2 +- 4 files changed, 55 insertions(+), 22 deletions(-) diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx index 8915c54..a28a0e8 100644 --- a/apps/mobile/src/app/(tabs)/shell/detail.tsx +++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx @@ -148,6 +148,7 @@ function ShellDetail() { onMessage={(m) => { console.log('received msg', m); if (m.type === 'initialized') { + if (terminalReadyRef.current) return; terminalReadyRef.current = true; // Replay from head, then attach live listener @@ -173,8 +174,9 @@ function ShellDetail() { const chunk = ev as TerminalChunk; xtermRef.current?.write(chunk.bytes); }, - { cursor: { mode: 'live' } }, + { cursor: { mode: 'seq', seq: res.nextSeq } }, ); + console.log('shell listener attached', id.toString()); listenerIdRef.current = id; })(); } @@ -185,7 +187,10 @@ function ShellDetail() { } if (m.type === 'data') { console.log('xterm->SSH', { len: m.data.length }); - void shell?.sendData(m.data.buffer as ArrayBuffer); + // Ensure we send the exact slice; send CR only for Enter. + const { buffer, byteOffset, byteLength } = m.data; + const ab = buffer.slice(byteOffset, byteOffset + byteLength); + void shell?.sendData(ab as ArrayBuffer); return; } if (m.type === 'debug') { diff --git a/packages/react-native-uniffi-russh/rust/uniffi-russh/src/lib.rs b/packages/react-native-uniffi-russh/rust/uniffi-russh/src/lib.rs index a72febf..710f06c 100644 --- a/packages/react-native-uniffi-russh/rust/uniffi-russh/src/lib.rs +++ b/packages/react-native-uniffi-russh/rust/uniffi-russh/src/lib.rs @@ -255,6 +255,7 @@ pub struct ShellSession { listener_tasks: Arc>>>, next_listener_id: AtomicU64, default_coalesce_ms: AtomicU64, + rt_handle: tokio::runtime::Handle, } impl fmt::Debug for SSHConnection { @@ -324,7 +325,20 @@ impl SSHConnection { let channel_id: u32 = ch.id().into(); // Request PTY & shell. - ch.request_pty(true, pty.as_ssh_name(), 80, 24, 0, 0, &[]).await?; + // Request a PTY with basic sane defaults: enable ECHO and set speeds. + // RFC4254 terminal mode opcodes: 53=ECHO, 128=TTY_OP_ISPEED, 129=TTY_OP_OSPEED + let modes: &[(russh::Pty, u32)] = &[ + (russh::Pty::ECHO, 1), + (russh::Pty::ECHOK, 1), + (russh::Pty::ECHOE, 1), + (russh::Pty::ICANON, 1), + (russh::Pty::ISIG, 1), + (russh::Pty::ICRNL, 1), + (russh::Pty::ONLCR, 1), + (russh::Pty::TTY_OP_ISPEED, 38400), + (russh::Pty::TTY_OP_OSPEED, 38400), + ]; + ch.request_pty(true, pty.as_ssh_name(), 80, 24, 0, 0, modes).await?; ch.request_shell(true).await?; // Split for read/write; spawn reader. @@ -413,6 +427,7 @@ impl SSHConnection { listener_tasks: Arc::new(Mutex::new(HashMap::new())), next_listener_id: AtomicU64::new(1), default_coalesce_ms, + rt_handle: tokio::runtime::Handle::current(), }); *self.shell.lock().await = Some(session.clone()); @@ -471,8 +486,8 @@ impl ShellSession { /// Buffer statistics snapshot. pub fn buffer_stats(&self) -> BufferStats { - let used = *self.used_bytes.lock().unwrap() as u64; - let chunks = self.ring.lock().map(|q| q.len() as u64).unwrap_or(0); + let used = *self.used_bytes.lock().unwrap_or_else(|p| p.into_inner()) as u64; + let chunks = match self.ring.lock() { Ok(q) => q.len() as u64, Err(p) => p.into_inner().len() as u64 }; BufferStats { ring_bytes: self.ring_bytes_capacity.load(Ordering::Relaxed) as u64, used_bytes: used, @@ -496,7 +511,7 @@ impl ShellSession { // Lock ring to determine start and collect arcs, then drop lock. let (_start_idx_unused, _start_seq, arcs): (usize, u64, Vec>) = { - let ring = self.ring.lock().unwrap(); + let ring = match self.ring.lock() { Ok(g) => g, Err(p) => p.into_inner() }; let (start_seq, idx) = match cursor { Cursor::Head => (head_seq_now, 0usize), Cursor::Seq { seq: mut s } => { @@ -540,19 +555,25 @@ impl ShellSession { } /// Add a listener with optional replay and live follow. - pub fn add_listener(&self, listener: Arc, opts: ListenerOptions) -> u64 { - // Synchronous replay phase + pub fn add_listener(&self, listener: Arc, opts: ListenerOptions) -> Result { + // Snapshot for replay; emit from task to avoid re-entrant callbacks during FFI. let replay = self.read_buffer(opts.cursor.clone(), None); - if let Some(dr) = replay.dropped.as_ref() { listener.on_event(ShellEvent::Dropped { from_seq: dr.from_seq, to_seq: dr.to_seq }); } - for ch in replay.chunks { listener.on_event(ShellEvent::Chunk(ch)); } - - // Live phase let mut rx = self.sender.subscribe(); let id = self.next_listener_id.fetch_add(1, Ordering::Relaxed); + eprintln!("ShellSession.add_listener -> id={id}"); let default_coalesce_ms = self.default_coalesce_ms.load(Ordering::Relaxed) as u32; let coalesce_ms = opts.coalesce_ms.unwrap_or(default_coalesce_ms); - let handle = tokio::spawn(async move { + let rt = self.rt_handle.clone(); + let handle = rt.spawn(async move { + // Emit replay first + if let Some(dr) = replay.dropped.as_ref() { + listener.on_event(ShellEvent::Dropped { from_seq: dr.from_seq, to_seq: dr.to_seq }); + } + for ch in replay.chunks.into_iter() { + listener.on_event(ShellEvent::Chunk(ch)); + } + let mut last_seq_seen: u64 = replay.next_seq.saturating_sub(1); let mut acc: Vec = Vec::new(); let mut acc_stream: Option; @@ -608,7 +629,7 @@ impl ShellSession { } }); if let Ok(mut map) = self.listener_tasks.lock() { map.insert(id, handle); } - id + Ok(id) } pub fn remove_listener(&self, id: u64) { @@ -639,8 +660,8 @@ impl ShellSession { fn evict_if_needed(&self) { let cap = self.ring_bytes_capacity.load(Ordering::Relaxed); - let mut ring = self.ring.lock().unwrap(); - let mut used = self.used_bytes.lock().unwrap(); + let mut ring = match self.ring.lock() { Ok(g) => g, Err(p) => p.into_inner() }; + let mut used = self.used_bytes.lock().unwrap_or_else(|p| p.into_inner()); while *used > cap { if let Some(front) = ring.pop_front() { *used -= front.bytes.len(); @@ -750,17 +771,17 @@ fn append_and_broadcast( let chunk = Arc::new(Chunk { seq, t_ms, stream, bytes: Bytes::copy_from_slice(slice) }); // push to ring { - let mut q = ring.lock().unwrap(); + let mut q = match ring.lock() { Ok(g) => g, Err(p) => p.into_inner() }; q.push_back(chunk.clone()); } { - let mut used = used_bytes.lock().unwrap(); + let mut used = used_bytes.lock().unwrap_or_else(|p| p.into_inner()); *used += slice.len(); tail_seq.store(seq, Ordering::Relaxed); // evict if needed let cap = ring_bytes_capacity.load(Ordering::Relaxed); if *used > cap { - let mut q = ring.lock().unwrap(); + let mut q = match ring.lock() { Ok(g) => g, Err(p) => p.into_inner() }; while *used > cap { if let Some(front) = q.pop_front() { *used -= front.bytes.len(); diff --git a/packages/react-native-uniffi-russh/src/api.ts b/packages/react-native-uniffi-russh/src/api.ts index 34befea..97f5385 100644 --- a/packages/react-native-uniffi-russh/src/api.ts +++ b/packages/react-native-uniffi-russh/src/api.ts @@ -245,8 +245,15 @@ function wrapShellSession(shell: GeneratedRussh.ShellSessionInterface): SshShell } } satisfies GeneratedRussh.ShellListener; - const id = shell.addListener(listener, { cursor: cursorToGenerated(opts.cursor), coalesceMs: opts.coalesceMs }); - return BigInt(id); + try { + const id = shell.addListener(listener, { cursor: cursorToGenerated(opts.cursor), coalesceMs: opts.coalesceMs }); + if (id === 0n) { + throw new Error('Failed to attach shell listener (id=0)'); + } + return BigInt(id); + } catch (e) { + throw new Error(`addListener failed: ${String((e as any)?.message ?? e)}`); + } }; return { diff --git a/packages/react-native-xtermjs-webview/src-internal/main.tsx b/packages/react-native-xtermjs-webview/src-internal/main.tsx index 7d5272e..7b0b51e 100644 --- a/packages/react-native-xtermjs-webview/src-internal/main.tsx +++ b/packages/react-native-xtermjs-webview/src-internal/main.tsx @@ -54,7 +54,7 @@ if (window.__FRESSH_XTERM_BRIDGE__) { const enc = new TextEncoder(); // Initial handshake (send once) - setTimeout(() => post({ type: 'initialized' }), 8_000); + setTimeout(() => post({ type: 'initialized' }), 500); // User input from xterm -> RN (SSH) as UTF-8 bytes (Base64) term.onData((data /* string */) => {