the statusphere demo reworked into a vite/react app in a monorepo

remove ngrok stuff

Changed files
+71 -398
packages
scripts
+3 -27
README.md
··· 20 # Install dependencies 21 pnpm install 22 23 - # Option 1: Local development (login won't work due to OAuth requirements) 24 pnpm dev 25 - 26 - # Option 2: Development with OAuth login support (recommended) 27 - pnpm dev:oauth 28 ``` 29 - 30 - ### OAuth Development 31 - 32 - Due to OAuth requirements, HTTPS is needed for development. We've made this easy: 33 - 34 - - `pnpm dev:oauth` - Sets up everything automatically: 35 - 1. Starts ngrok to create an HTTPS tunnel 36 - 2. Configures environment variables with the ngrok URL 37 - 3. Starts both the API server and client app 38 - 4. Handles proper shutdown of all processes 39 - 40 - This all-in-one command makes OAuth development seamless. 41 42 ### Additional Commands 43 ··· 89 90 ## Environment Variables 91 92 - Create a `.env` file in the root directory with: 93 94 ``` 95 - # Required for AT Protocol authentication 96 - ATP_SERVICE_DID=did:plc:your-service-did 97 - ATP_CLIENT_ID=your-client-id 98 - ATP_CLIENT_SECRET=your-client-secret 99 - ATP_REDIRECT_URI=https://your-domain.com/oauth-callback 100 - 101 - # Optional 102 - PORT=3001 103 - SESSION_SECRET=your-session-secret 104 ``` 105 106 ## Requirements 107 108 - Node.js 18+ 109 - pnpm 9+ 110 - - ngrok (for OAuth development) 111 112 ## License 113
··· 20 # Install dependencies 21 pnpm install 22 23 pnpm dev 24 ``` 25 26 ### Additional Commands 27 ··· 73 74 ## Environment Variables 75 76 + Copy the `.env.template` file in the appview to `.env`: 77 78 ``` 79 + cd packages/appview 80 + cp .env.template .env 81 ``` 82 83 ## Requirements 84 85 - Node.js 18+ 86 - pnpm 9+ 87 88 ## License 89
+3 -3
package.json
··· 6 "license": "MIT", 7 "private": true, 8 "scripts": { 9 - "dev": "concurrently \"pnpm --filter @statusphere/appview dev\" \"pnpm --filter @statusphere/client dev\"", 10 "dev:appview": "pnpm --filter @statusphere/appview dev", 11 "dev:client": "pnpm --filter @statusphere/client dev", 12 - "dev:oauth": "node scripts/setup-ngrok.js", 13 - "lexgen": "pnpm --filter @statusphere/lexicon build", 14 "build": "pnpm build:lexicon && pnpm build:client && pnpm build:appview", 15 "build:lexicon": "pnpm --filter @statusphere/lexicon build", 16 "build:appview": "pnpm --filter @statusphere/appview build",
··· 6 "license": "MIT", 7 "private": true, 8 "scripts": { 9 + "dev": "pnpm lexgen && concurrently \"pnpm dev:appview\" \"pnpm dev:client\"", 10 + "dev:lexicon": "pnpm --filter @statusphere/lexicon dev", 11 "dev:appview": "pnpm --filter @statusphere/appview dev", 12 "dev:client": "pnpm --filter @statusphere/client dev", 13 + "lexgen": "pnpm --filter @statusphere/lexicon lexgen", 14 "build": "pnpm build:lexicon && pnpm build:client && pnpm build:appview", 15 "build:lexicon": "pnpm --filter @statusphere/lexicon build", 16 "build:appview": "pnpm --filter @statusphere/appview build",
+11
packages/appview/.env.template
···
··· 1 + # Environment Configuration 2 + NODE_ENV="development" # Options: 'development', 'production' 3 + PORT="3001" # The port your server will listen on 4 + VITE_PORT="3000" # The port the vite dev server is on (dev only) 5 + HOST="127.0.0.1" # Hostname for the server 6 + PUBLIC_URL="" # Set when deployed publicly, e.g. "https://mysite.com". Informs OAuth client id. 7 + DB_PATH=":memory:" # The SQLite database path. Leave as ":memory:" to use a temporary in-memory database. 8 + 9 + # Secrets 10 + # Must set this in production. May be generated with `openssl rand -base64 33` 11 + # COOKIE_SECRET=""
+9 -16
packages/appview/src/auth/client.ts
··· 5 import { SessionStore, StateStore } from './storage' 6 7 export const createClient = async (db: Database) => { 8 - // Get the ngrok URL from environment variables 9 - const ngrokUrl = env.NGROK_URL 10 - 11 - if (!ngrokUrl && env.NODE_ENV === 'development') { 12 - console.warn( 13 - 'WARNING: NGROK_URL is not set. OAuth login might not work properly.', 14 - ) 15 - console.warn( 16 - 'You should run ngrok and set the NGROK_URL environment variable.', 17 - ) 18 - console.warn('Example: NGROK_URL=https://abcd-123-45-678-90.ngrok.io') 19 - } else if (env.NODE_ENV === 'production' && !env.PUBLIC_URL) { 20 throw new Error('PUBLIC_URL is not set') 21 } 22 23 - const baseUrl = ngrokUrl || env.PUBLIC_URL || `http://127.0.0.1:${env.PORT}` 24 25 return new NodeOAuthClient({ 26 clientMetadata: { 27 client_name: 'Statusphere React App', 28 - client_id: `${baseUrl}/api/client-metadata.json`, 29 - client_uri: baseUrl, 30 - redirect_uris: [`${baseUrl}/api/oauth/callback`], 31 scope: 'atproto transition:generic', 32 grant_types: ['authorization_code', 'refresh_token'], 33 response_types: ['code'],
··· 5 import { SessionStore, StateStore } from './storage' 6 7 export const createClient = async (db: Database) => { 8 + if (env.isProduction && !env.PUBLIC_URL) { 9 throw new Error('PUBLIC_URL is not set') 10 } 11 12 + const publicUrl = env.PUBLIC_URL 13 + const url = publicUrl || `http://127.0.0.1:${env.VITE_PORT}` 14 + const enc = encodeURIComponent 15 16 return new NodeOAuthClient({ 17 clientMetadata: { 18 client_name: 'Statusphere React App', 19 + client_id: publicUrl 20 + ? `${url}/api/client-metadata.json` 21 + : `http://localhost?redirect_uri=${enc(`${url}/api/oauth/callback`)}&scope=${enc('atproto transition:generic')}`, 22 + client_uri: url, 23 + redirect_uris: [`${url}/api/oauth/callback`], 24 scope: 'atproto transition:generic', 25 grant_types: ['authorization_code', 'refresh_token'], 26 response_types: ['code'],
+31 -45
packages/appview/src/index.ts
··· 79 'http://127.0.0.1:3000', // Alternative React address 80 ] 81 82 - // If we have an ngrok URL defined, add it to allowed origins 83 - if (env.NGROK_URL) { 84 - try { 85 - const ngrokOrigin = new URL(env.NGROK_URL) 86 - const ngrokClientOrigin = `${ngrokOrigin.protocol}//${ngrokOrigin.hostname}:3000` 87 - allowedOrigins.push(ngrokClientOrigin) 88 - } catch (err) { 89 - console.error('Failed to parse NGROK_URL for CORS:', err) 90 - } 91 - } 92 - 93 // Check if the request origin is in our allowed list or is an ngrok domain 94 - if ( 95 - allowedOrigins.indexOf(origin) !== -1 || 96 - origin.includes('ngrok-free.app') 97 - ) { 98 callback(null, true) 99 } else { 100 console.warn(`⚠️ CORS blocked origin: ${origin}`) ··· 122 app.use(express.json()) 123 app.use(express.urlencoded({ extended: true })) 124 125 - // Two versions of the API routes: 126 - // 1. Mounted at /api for the client 127 app.use('/api', router) 128 129 - // Serve static files from the frontend build 130 - const frontendPath = path.resolve( 131 - __dirname, 132 - '../../../packages/client/dist', 133 - ) 134 135 - // Check if the frontend build exists 136 - if (fs.existsSync(frontendPath)) { 137 - logger.info(`Serving frontend static files from: ${frontendPath}`) 138 139 - // Serve static files 140 - app.use(express.static(frontendPath)) 141 142 - // Heathcheck 143 - app.get('/health', (req, res) => { 144 - res.status(200).json({ status: 'ok' }) 145 - }) 146 147 - // For any other requests, send the index.html file 148 - app.get('*', (req, res) => { 149 - // Only handle non-API paths 150 - if (!req.path.startsWith('/api/')) { 151 - res.sendFile(path.join(frontendPath, 'index.html')) 152 - } else { 153 - res.status(404).json({ error: 'API endpoint not found' }) 154 - } 155 - }) 156 - } else { 157 - logger.warn(`Frontend build not found at: ${frontendPath}`) 158 - app.use('*', (_req, res) => { 159 - res.sendStatus(404) 160 - }) 161 } 162 163 // Use the port from env (should be 3001 for the API server)
··· 79 'http://127.0.0.1:3000', // Alternative React address 80 ] 81 82 // Check if the request origin is in our allowed list or is an ngrok domain 83 + if (allowedOrigins.indexOf(origin) !== -1) { 84 callback(null, true) 85 } else { 86 console.warn(`⚠️ CORS blocked origin: ${origin}`) ··· 108 app.use(express.json()) 109 app.use(express.urlencoded({ extended: true })) 110 111 app.use('/api', router) 112 113 + // Serve static files from the frontend build - prod only 114 + if (env.isProduction) { 115 + const frontendPath = path.resolve( 116 + __dirname, 117 + '../../../packages/client/dist', 118 + ) 119 120 + // Check if the frontend build exists 121 + if (fs.existsSync(frontendPath)) { 122 + logger.info(`Serving frontend static files from: ${frontendPath}`) 123 124 + // Serve static files 125 + app.use(express.static(frontendPath)) 126 127 + // Heathcheck 128 + app.get('/health', (req, res) => { 129 + res.status(200).json({ status: 'ok' }) 130 + }) 131 132 + // For any other requests, send the index.html file 133 + app.get('*', (req, res) => { 134 + // Only handle non-API paths 135 + if (!req.path.startsWith('/api/')) { 136 + res.sendFile(path.join(frontendPath, 'index.html')) 137 + } else { 138 + res.status(404).json({ error: 'API endpoint not found' }) 139 + } 140 + }) 141 + } else { 142 + logger.warn(`Frontend build not found at: ${frontendPath}`) 143 + app.use('*', (_req, res) => { 144 + res.sendStatus(404) 145 + }) 146 + } 147 } 148 149 // Use the port from env (should be 3001 for the API server)
+6 -7
packages/appview/src/lib/env.ts
··· 1 import dotenv from 'dotenv' 2 - import { cleanEnv, host, port, str, testOnly, url } from 'envalid' 3 4 dotenv.config() 5 ··· 8 devDefault: testOnly('test'), 9 choices: ['development', 'production', 'test'], 10 }), 11 - HOST: host({ devDefault: testOnly('localhost') }), 12 - PORT: port({ devDefault: testOnly(3001) }), 13 DB_PATH: str({ devDefault: ':memory:' }), 14 - COOKIE_SECRET: str({ devDefault: '00000000000000000000000000000000' }), 15 - ATPROTO_SERVER: str({ default: 'https://bsky.social' }), 16 SERVICE_DID: str({ default: undefined }), 17 - PUBLIC_URL: str({ default: 'http://localhost:3001' }), 18 - NGROK_URL: str({ default: '' }), 19 })
··· 1 import dotenv from 'dotenv' 2 + import { cleanEnv, host, port, str, testOnly } from 'envalid' 3 4 dotenv.config() 5 ··· 8 devDefault: testOnly('test'), 9 choices: ['development', 'production', 'test'], 10 }), 11 + HOST: host({ devDefault: '127.0.0.1' }), 12 + PORT: port({ devDefault: 3001 }), 13 + VITE_PORT: port({ devDefault: 3000 }), 14 DB_PATH: str({ devDefault: ':memory:' }), 15 + COOKIE_SECRET: str({ devDefault: '0'.repeat(32) }), 16 SERVICE_DID: str({ default: undefined }), 17 + PUBLIC_URL: str({ devDefault: '' }), 18 })
+1 -1
packages/appview/src/routes.ts
··· 238 .selectFrom('status') 239 .selectAll() 240 .orderBy('indexedAt', 'desc') 241 - .limit(10) 242 .execute() 243 244 res.json({
··· 238 .selectFrom('status') 239 .selectAll() 240 .orderBy('indexedAt', 'desc') 241 + .limit(30) 242 .execute() 243 244 res.json({
+2 -2
packages/client/src/components/StatusList.tsx
··· 4 5 const StatusList = () => { 6 // Use React Query to fetch and cache statuses 7 - const { data, isLoading, isError, error } = useQuery({ 8 queryKey: ['statuses'], 9 queryFn: async () => { 10 const data = await api.getStatuses() ··· 17 // Destructure data 18 const statuses = data?.statuses || [] 19 20 - if (isLoading && !data) { 21 return ( 22 <div className="py-4 text-center text-gray-500 dark:text-gray-400"> 23 Loading statuses...
··· 4 5 const StatusList = () => { 6 // Use React Query to fetch and cache statuses 7 + const { data, isPending, isError, error } = useQuery({ 8 queryKey: ['statuses'], 9 queryFn: async () => { 10 const data = await api.getStatuses() ··· 17 // Destructure data 18 const statuses = data?.statuses || [] 19 20 + if (isPending && !data) { 21 return ( 22 <div className="py-4 text-center text-gray-500 dark:text-gray-400"> 23 Loading statuses...
-9
packages/client/src/vite-env.d.ts
··· 1 - /// <reference types="vite/client" /> 2 - 3 - interface ImportMetaEnv { 4 - readonly VITE_API_URL: string 5 - } 6 - 7 - interface ImportMeta { 8 - readonly env: ImportMetaEnv 9 - }
···
+2 -4
packages/client/vite.config.ts
··· 1 - import path from 'path' 2 import tailwindcss from '@tailwindcss/vite' 3 import react from '@vitejs/plugin-react' 4 import { defineConfig } from 'vite' ··· 14 tailwindcss(), 15 ], 16 server: { 17 port: 3000, 18 - // allow ngrok 19 - allowedHosts: true, 20 proxy: { 21 '/api': { 22 target: 'http://localhost:3001', 23 changeOrigin: true, 24 - rewrite: (path) => path.replace(/^\/api/, ''), 25 }, 26 }, 27 },
··· 1 + import path from 'node:path' 2 import tailwindcss from '@tailwindcss/vite' 3 import react from '@vitejs/plugin-react' 4 import { defineConfig } from 'vite' ··· 14 tailwindcss(), 15 ], 16 server: { 17 + host: '127.0.0.1', 18 port: 3000, 19 proxy: { 20 '/api': { 21 target: 'http://localhost:3001', 22 changeOrigin: true, 23 }, 24 }, 25 },
+3 -3
packages/lexicon/package.json
··· 8 "types": "dist/index.d.ts", 9 "private": true, 10 "scripts": { 11 - "build": "pnpm run lexgen && tsup", 12 "dev": "tsup --watch", 13 "clean": "rimraf dist", 14 "typecheck": "tsc --noEmit", 15 - "lexgen": "lex gen-api ./src ../../lexicons/xyz/statusphere/* ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* --yes", 16 - "postinstall": "pnpm run build" 17 }, 18 "dependencies": { 19 "@atproto/api": "^0.14.7",
··· 8 "types": "dist/index.d.ts", 9 "private": true, 10 "scripts": { 11 + "build": "pnpm lexgen && tsup", 12 "dev": "tsup --watch", 13 "clean": "rimraf dist", 14 "typecheck": "tsc --noEmit", 15 + "lexgen": "lex gen-api ./src ../../lexicons/xyz/statusphere/* ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* --yes && pnpm format", 16 + "format": "prettier --write src" 17 }, 18 "dependencies": { 19 "@atproto/api": "^0.14.7",
-281
scripts/setup-ngrok.js
··· 1 - #!/usr/bin/env node 2 - 3 - /** 4 - * This script automatically sets up ngrok for development. 5 - * It: 6 - * 1. Starts ngrok to tunnel to localhost:3001 7 - * 2. Gets the public HTTPS URL via ngrok's API 8 - * 3. Updates the appview .env file with the ngrok URL 9 - * 4. Starts both the API server and client app 10 - */ 11 - 12 - const { execSync, spawn } = require('child_process') 13 - const fs = require('fs') 14 - const path = require('path') 15 - const http = require('http') 16 - const { URL } = require('url') 17 - 18 - const appviewEnvPath = path.join(__dirname, '..', 'packages', 'appview', '.env') 19 - const clientEnvPath = path.join(__dirname, '..', 'packages', 'client', '.env') 20 - 21 - // Check if ngrok is installed 22 - try { 23 - execSync('ngrok --version', { stdio: 'ignore' }) 24 - } catch (error) { 25 - console.error('❌ ngrok is not installed or not in your PATH.') 26 - console.error('Please install ngrok from https://ngrok.com/download') 27 - process.exit(1) 28 - } 29 - 30 - // Kill any existing ngrok processes 31 - try { 32 - if (process.platform === 'win32') { 33 - execSync('taskkill /f /im ngrok.exe', { stdio: 'ignore' }) 34 - } else { 35 - execSync('pkill -f ngrok', { stdio: 'ignore' }) 36 - } 37 - // Wait for processes to terminate 38 - try { 39 - execSync('sleep 1') 40 - } catch (e) {} 41 - } catch (error) { 42 - // If no process was found, it will throw an error, which we can ignore 43 - } 44 - 45 - console.log('🚀 Starting ngrok...') 46 - 47 - // Start ngrok process - now we're exposing the client (3000) instead of the API (3001) 48 - // This way the whole app will be served through ngrok 49 - const ngrokProcess = spawn('ngrok', ['http', '3000'], { 50 - stdio: ['ignore', 'pipe', 'pipe'], // Allow stdout, stderr 51 - }) 52 - 53 - let devProcesses = null 54 - 55 - // Helper function to update .env files 56 - function updateEnvFile(filePath, ngrokUrl) { 57 - if (!fs.existsSync(filePath)) { 58 - fs.writeFileSync(filePath, '') 59 - } 60 - 61 - const content = fs.readFileSync(filePath, 'utf8') 62 - 63 - if (filePath.includes('appview')) { 64 - // Update NGROK_URL in the appview package 65 - const varName = 'NGROK_URL' 66 - const publicUrlName = 'PUBLIC_URL' 67 - const regex = new RegExp(`^${varName}=.*$`, 'm') 68 - const publicUrlRegex = new RegExp(`^${publicUrlName}=.*$`, 'm') 69 - 70 - // Update content 71 - let updatedContent = content 72 - 73 - // Update or add NGROK_URL 74 - if (regex.test(updatedContent)) { 75 - updatedContent = updatedContent.replace(regex, `${varName}=${ngrokUrl}`) 76 - } else { 77 - updatedContent = `${updatedContent}\n${varName}=${ngrokUrl}\n` 78 - } 79 - 80 - // Update or add PUBLIC_URL - set it to the ngrok URL too 81 - if (publicUrlRegex.test(updatedContent)) { 82 - updatedContent = updatedContent.replace( 83 - publicUrlRegex, 84 - `${publicUrlName}=${ngrokUrl}`, 85 - ) 86 - } else { 87 - updatedContent = `${updatedContent}\n${publicUrlName}=${ngrokUrl}\n` 88 - } 89 - 90 - fs.writeFileSync(filePath, updatedContent) 91 - console.log( 92 - `✅ Updated ${path.basename(filePath)} with ${varName}=${ngrokUrl} and ${publicUrlName}=${ngrokUrl}`, 93 - ) 94 - } else if (filePath.includes('client')) { 95 - // For client, set VITE_API_URL to "/api" - this ensures it uses the proxy setup 96 - const varName = 'VITE_API_URL' 97 - const regex = new RegExp(`^${varName}=.*$`, 'm') 98 - 99 - let updatedContent 100 - if (regex.test(content)) { 101 - // Update existing variable 102 - updatedContent = content.replace(regex, `${varName}=/api`) 103 - } else { 104 - // Add new variable 105 - updatedContent = `${content}\n${varName}=/api\n` 106 - } 107 - 108 - fs.writeFileSync(filePath, updatedContent) 109 - console.log( 110 - `✅ Updated ${path.basename(filePath)} with ${varName}=/api (proxy to API server)`, 111 - ) 112 - } 113 - } 114 - 115 - // Function to start the development servers 116 - function startDevServers() { 117 - console.log('🚀 Starting development servers...') 118 - 119 - // Free port 3001 if it's in use 120 - try { 121 - if (process.platform !== 'win32') { 122 - // Kill any process using port 3001 123 - execSync('kill $(lsof -t -i:3001 2>/dev/null) 2>/dev/null || true') 124 - // Wait for port to be released 125 - execSync('sleep 1') 126 - } 127 - } catch (error) { 128 - // Ignore errors 129 - } 130 - 131 - // Start both servers 132 - devProcesses = spawn('pnpm', ['--filter', '@statusphere/appview', 'dev'], { 133 - stdio: 'inherit', 134 - detached: false, 135 - }) 136 - 137 - const clientProcess = spawn( 138 - 'pnpm', 139 - ['--filter', '@statusphere/client', 'dev'], 140 - { 141 - stdio: 'inherit', 142 - detached: false, 143 - }, 144 - ) 145 - 146 - devProcesses.on('close', (code) => { 147 - console.log(`API server exited with code ${code}`) 148 - killAllProcesses() 149 - }) 150 - 151 - clientProcess.on('close', (code) => { 152 - console.log(`Client app exited with code ${code}`) 153 - killAllProcesses() 154 - }) 155 - } 156 - 157 - // Function to get the ngrok URL from its API 158 - function getNgrokUrl() { 159 - return new Promise((resolve, reject) => { 160 - // Wait a bit for ngrok to start its API server 161 - setTimeout(() => { 162 - http 163 - .get('http://localhost:4040/api/tunnels', (res) => { 164 - let data = '' 165 - 166 - res.on('data', (chunk) => { 167 - data += chunk 168 - }) 169 - 170 - res.on('end', () => { 171 - try { 172 - const tunnels = JSON.parse(data).tunnels 173 - if (tunnels && tunnels.length > 0) { 174 - // Find HTTPS tunnel 175 - const httpsTunnel = tunnels.find((t) => t.proto === 'https') 176 - if (httpsTunnel) { 177 - resolve(httpsTunnel.public_url) 178 - } else { 179 - reject(new Error('No HTTPS tunnel found')) 180 - } 181 - } else { 182 - reject(new Error('No tunnels found')) 183 - } 184 - } catch (error) { 185 - reject(error) 186 - } 187 - }) 188 - }) 189 - .on('error', (err) => { 190 - reject(err) 191 - }) 192 - }, 2000) // Give ngrok a couple seconds to start 193 - }) 194 - } 195 - 196 - // Poll the ngrok API until we get a URL 197 - function pollNgrokApi() { 198 - getNgrokUrl() 199 - .then((ngrokUrl) => { 200 - console.log(`🌍 ngrok URL: ${ngrokUrl}`) 201 - 202 - // Update .env files with the ngrok URL 203 - updateEnvFile(appviewEnvPath, ngrokUrl) 204 - // We'll still call this but it will be skipped per our updated logic 205 - updateEnvFile(clientEnvPath, ngrokUrl) 206 - 207 - // Start development servers 208 - startDevServers() 209 - }) 210 - .catch(() => { 211 - // Try again in 1 second 212 - setTimeout(pollNgrokApi, 1000) 213 - }) 214 - } 215 - 216 - // Start polling after a short delay 217 - setTimeout(pollNgrokApi, 1000) 218 - 219 - // Handle errors 220 - ngrokProcess.stderr.on('data', (data) => { 221 - console.error('------- NGROK ERROR -------') 222 - console.error(data.toString()) 223 - console.error('---------------------------') 224 - }) 225 - 226 - // Handle ngrok process exit 227 - ngrokProcess.on('close', (code) => { 228 - console.log(`ngrok process exited with code ${code}`) 229 - // Call our kill function to ensure everything is properly cleaned up 230 - killAllProcesses() 231 - }) 232 - 233 - // Function to properly terminate all child processes 234 - function killAllProcesses() { 235 - console.log('\nShutting down development environment...') 236 - 237 - // Get ngrok process PID for force kill if needed 238 - const ngrokPid = ngrokProcess.pid 239 - 240 - // Kill main processes with a normal signal first 241 - if (devProcesses) { 242 - try { 243 - devProcesses.kill() 244 - } catch (e) {} 245 - } 246 - 247 - try { 248 - ngrokProcess.kill() 249 - } catch (e) {} 250 - 251 - // Force kill ngrok if normal kill fails 252 - try { 253 - if (process.platform === 'win32') { 254 - execSync(`taskkill /F /PID ${ngrokPid} 2>nul`, { stdio: 'ignore' }) 255 - } else { 256 - execSync(`kill -9 ${ngrokPid} 2>/dev/null || true`, { stdio: 'ignore' }) 257 - // Also kill any remaining ngrok processes 258 - execSync('pkill -9 -f ngrok 2>/dev/null || true', { stdio: 'ignore' }) 259 - } 260 - } catch (e) { 261 - // Ignore errors if processes are already gone 262 - } 263 - 264 - // Kill any process on port 3001 to ensure clean exit 265 - try { 266 - if (process.platform !== 'win32') { 267 - execSync('kill $(lsof -t -i:3001 2>/dev/null) 2>/dev/null || true', { 268 - stdio: 'ignore', 269 - }) 270 - } 271 - } catch (e) { 272 - // Ignore errors 273 - } 274 - 275 - process.exit(0) 276 - } 277 - 278 - // Handle various termination signals 279 - process.on('SIGINT', killAllProcesses) // Ctrl+C 280 - process.on('SIGTERM', killAllProcesses) // Kill command 281 - process.on('SIGHUP', killAllProcesses) // Terminal closed
···