+3
-27
README.md
+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
+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
+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
+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
+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
+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
+1
-1
packages/appview/src/routes.ts
+2
-2
packages/client/src/components/StatusList.tsx
+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
-9
packages/client/src/vite-env.d.ts
+2
-4
packages/client/vite.config.ts
+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
+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
-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
···