create-bunspace 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -0
- package/dist/bin.js +5755 -0
- package/dist/templates/monorepo/CLAUDE.md +164 -0
- package/dist/templates/monorepo/LICENSE +21 -0
- package/dist/templates/monorepo/MUST-FOLLOW-GUIDELINES.md +269 -0
- package/dist/templates/monorepo/README.md +74 -0
- package/dist/templates/monorepo/SYNC_VERIFICATION.md +1 -0
- package/dist/templates/monorepo/apps/example/package.json +19 -0
- package/dist/templates/monorepo/apps/example/src/index.ts +23 -0
- package/dist/templates/monorepo/apps/example/src/types/index.ts +7 -0
- package/dist/templates/monorepo/apps/example/src/utils/index.ts +7 -0
- package/dist/templates/monorepo/core/packages/main/package.json +41 -0
- package/dist/templates/monorepo/core/packages/main/rolldown.config.ts +24 -0
- package/dist/templates/monorepo/core/packages/main/src/index.ts +80 -0
- package/dist/templates/monorepo/core/packages/main/src/types/constants.ts +15 -0
- package/dist/templates/monorepo/core/packages/main/src/types/index.ts +8 -0
- package/dist/templates/monorepo/core/packages/main/src/types/main.types.ts +25 -0
- package/dist/templates/monorepo/core/packages/main/src/utils/index.ts +5 -0
- package/dist/templates/monorepo/core/packages/utils/package.json +43 -0
- package/dist/templates/monorepo/core/packages/utils/rolldown.config.ts +34 -0
- package/dist/templates/monorepo/core/packages/utils/src/index.ts +2 -0
- package/dist/templates/monorepo/core/packages/utils/src/logger.ts +68 -0
- package/dist/templates/monorepo/core/packages/utils/src/result.ts +146 -0
- package/dist/templates/monorepo/core/packages/utils/src/types/constants.ts +15 -0
- package/dist/templates/monorepo/core/packages/utils/src/types/index.ts +8 -0
- package/dist/templates/monorepo/core/packages/utils/src/types/utils.types.ts +32 -0
- package/dist/templates/monorepo/core/packages/utils/src/utils/index.ts +5 -0
- package/dist/templates/monorepo/oxlint.json +14 -0
- package/dist/templates/monorepo/package.json +39 -0
- package/dist/templates/monorepo/tsconfig.json +35 -0
- package/dist/templates/telegram-bot/.oxlintrc.json +33 -0
- package/dist/templates/telegram-bot/.prettierignore +5 -0
- package/dist/templates/telegram-bot/.prettierrc +26 -0
- package/dist/templates/telegram-bot/CLAUDE.deploy.md +356 -0
- package/dist/templates/telegram-bot/CLAUDE.dev.md +266 -0
- package/dist/templates/telegram-bot/CLAUDE.md +280 -0
- package/dist/templates/telegram-bot/Dockerfile +46 -0
- package/dist/templates/telegram-bot/README.md +245 -0
- package/dist/templates/telegram-bot/apps/.gitkeep +0 -0
- package/dist/templates/telegram-bot/bun.lock +208 -0
- package/dist/templates/telegram-bot/core/.env.example +71 -0
- package/dist/templates/telegram-bot/core/README.md +1067 -0
- package/dist/templates/telegram-bot/core/package.json +15 -0
- package/dist/templates/telegram-bot/core/src/config/env.ts +131 -0
- package/dist/templates/telegram-bot/core/src/config/index.ts +97 -0
- package/dist/templates/telegram-bot/core/src/config/logging.ts +110 -0
- package/dist/templates/telegram-bot/core/src/handlers/control.ts +85 -0
- package/dist/templates/telegram-bot/core/src/handlers/health.ts +83 -0
- package/dist/templates/telegram-bot/core/src/handlers/logs.ts +126 -0
- package/dist/templates/telegram-bot/core/src/index.ts +161 -0
- package/dist/templates/telegram-bot/core/src/middleware/auth.ts +41 -0
- package/dist/templates/telegram-bot/core/src/middleware/error-handler.ts +41 -0
- package/dist/templates/telegram-bot/core/src/middleware/logging.ts +1 -0
- package/dist/templates/telegram-bot/core/src/middleware/topics.ts +55 -0
- package/dist/templates/telegram-bot/core/src/types/bot.ts +92 -0
- package/dist/templates/telegram-bot/core/src/types/constants.ts +50 -0
- package/dist/templates/telegram-bot/core/src/types/result.ts +1 -0
- package/dist/templates/telegram-bot/core/src/utils/bot-manager.test.ts +111 -0
- package/dist/templates/telegram-bot/core/src/utils/bot-manager.ts +201 -0
- package/dist/templates/telegram-bot/core/src/utils/commands.ts +63 -0
- package/dist/templates/telegram-bot/core/src/utils/formatters.ts +82 -0
- package/dist/templates/telegram-bot/core/src/utils/instance-manager.ts +189 -0
- package/dist/templates/telegram-bot/core/src/utils/memory.ts +33 -0
- package/dist/templates/telegram-bot/core/src/utils/result.ts +26 -0
- package/dist/templates/telegram-bot/core/src/utils/telegram.ts +31 -0
- package/dist/templates/telegram-bot/core/src/utils/type-guards.ts +71 -0
- package/dist/templates/telegram-bot/core/tsconfig.json +9 -0
- package/dist/templates/telegram-bot/docker-compose.yml +37 -0
- package/dist/templates/telegram-bot/docs/cli-commands.md +377 -0
- package/dist/templates/telegram-bot/docs/development.md +363 -0
- package/dist/templates/telegram-bot/docs/environment.md +460 -0
- package/dist/templates/telegram-bot/docs/examples/middleware-auth.md +335 -0
- package/dist/templates/telegram-bot/docs/examples/simple-command.md +207 -0
- package/dist/templates/telegram-bot/docs/examples/webhook-setup.md +362 -0
- package/dist/templates/telegram-bot/docs/getting-started.md +223 -0
- package/dist/templates/telegram-bot/docs/troubleshooting.md +489 -0
- package/dist/templates/telegram-bot/package.json +49 -0
- package/dist/templates/telegram-bot/packages/utils/package.json +12 -0
- package/dist/templates/telegram-bot/packages/utils/src/index.ts +2 -0
- package/dist/templates/telegram-bot/packages/utils/src/logger.ts +72 -0
- package/dist/templates/telegram-bot/packages/utils/src/result.ts +80 -0
- package/dist/templates/telegram-bot/tools/README.md +47 -0
- package/dist/templates/telegram-bot/tools/commands/doctor.ts +460 -0
- package/dist/templates/telegram-bot/tools/commands/index.ts +35 -0
- package/dist/templates/telegram-bot/tools/commands/ngrok.ts +207 -0
- package/dist/templates/telegram-bot/tools/commands/setup.ts +368 -0
- package/dist/templates/telegram-bot/tools/commands/status.ts +140 -0
- package/dist/templates/telegram-bot/tools/index.ts +16 -0
- package/dist/templates/telegram-bot/tools/package.json +12 -0
- package/dist/templates/telegram-bot/tools/utils/index.ts +13 -0
- package/dist/templates/telegram-bot/tsconfig.json +22 -0
- package/dist/templates/telegram-bot/vitest.config.ts +29 -0
- package/package.json +35 -0
- package/templates/monorepo/CLAUDE.md +164 -0
- package/templates/monorepo/LICENSE +21 -0
- package/templates/monorepo/MUST-FOLLOW-GUIDELINES.md +269 -0
- package/templates/monorepo/README.md +74 -0
- package/templates/monorepo/apps/example/package.json +19 -0
- package/templates/monorepo/apps/example/src/index.ts +23 -0
- package/templates/monorepo/apps/example/src/types/index.ts +7 -0
- package/templates/monorepo/apps/example/src/utils/index.ts +7 -0
- package/templates/monorepo/core/packages/main/package.json +41 -0
- package/templates/monorepo/core/packages/main/rolldown.config.ts +24 -0
- package/templates/monorepo/core/packages/main/src/index.ts +80 -0
- package/templates/monorepo/core/packages/main/src/types/constants.ts +15 -0
- package/templates/monorepo/core/packages/main/src/types/index.ts +8 -0
- package/templates/monorepo/core/packages/main/src/types/main.types.ts +25 -0
- package/templates/monorepo/core/packages/main/src/utils/index.ts +5 -0
- package/templates/monorepo/core/packages/utils/package.json +43 -0
- package/templates/monorepo/core/packages/utils/rolldown.config.ts +34 -0
- package/templates/monorepo/core/packages/utils/src/index.ts +2 -0
- package/templates/monorepo/core/packages/utils/src/logger.ts +68 -0
- package/templates/monorepo/core/packages/utils/src/result.ts +146 -0
- package/templates/monorepo/core/packages/utils/src/types/constants.ts +15 -0
- package/templates/monorepo/core/packages/utils/src/types/index.ts +8 -0
- package/templates/monorepo/core/packages/utils/src/types/utils.types.ts +32 -0
- package/templates/monorepo/core/packages/utils/src/utils/index.ts +5 -0
- package/templates/monorepo/oxlint.json +14 -0
- package/templates/monorepo/package.json +39 -0
- package/templates/monorepo/tsconfig.json +35 -0
- package/templates/telegram-bot/.oxlintrc.json +33 -0
- package/templates/telegram-bot/.prettierignore +5 -0
- package/templates/telegram-bot/.prettierrc +26 -0
- package/templates/telegram-bot/CLAUDE.deploy.md +356 -0
- package/templates/telegram-bot/CLAUDE.dev.md +266 -0
- package/templates/telegram-bot/CLAUDE.md +280 -0
- package/templates/telegram-bot/Dockerfile +46 -0
- package/templates/telegram-bot/README.md +245 -0
- package/templates/telegram-bot/apps/.gitkeep +0 -0
- package/templates/telegram-bot/bun.lock +208 -0
- package/templates/telegram-bot/core/.env.example +71 -0
- package/templates/telegram-bot/core/README.md +1067 -0
- package/templates/telegram-bot/core/package.json +15 -0
- package/templates/telegram-bot/core/src/config/env.ts +131 -0
- package/templates/telegram-bot/core/src/config/index.ts +97 -0
- package/templates/telegram-bot/core/src/config/logging.ts +110 -0
- package/templates/telegram-bot/core/src/handlers/control.ts +85 -0
- package/templates/telegram-bot/core/src/handlers/health.ts +83 -0
- package/templates/telegram-bot/core/src/handlers/logs.ts +126 -0
- package/templates/telegram-bot/core/src/index.ts +161 -0
- package/templates/telegram-bot/core/src/middleware/auth.ts +41 -0
- package/templates/telegram-bot/core/src/middleware/error-handler.ts +41 -0
- package/templates/telegram-bot/core/src/middleware/logging.ts +1 -0
- package/templates/telegram-bot/core/src/middleware/topics.ts +55 -0
- package/templates/telegram-bot/core/src/types/bot.ts +92 -0
- package/templates/telegram-bot/core/src/types/constants.ts +50 -0
- package/templates/telegram-bot/core/src/types/result.ts +1 -0
- package/templates/telegram-bot/core/src/utils/bot-manager.test.ts +111 -0
- package/templates/telegram-bot/core/src/utils/bot-manager.ts +201 -0
- package/templates/telegram-bot/core/src/utils/commands.ts +63 -0
- package/templates/telegram-bot/core/src/utils/formatters.ts +82 -0
- package/templates/telegram-bot/core/src/utils/instance-manager.ts +189 -0
- package/templates/telegram-bot/core/src/utils/memory.ts +33 -0
- package/templates/telegram-bot/core/src/utils/result.ts +26 -0
- package/templates/telegram-bot/core/src/utils/telegram.ts +31 -0
- package/templates/telegram-bot/core/src/utils/type-guards.ts +71 -0
- package/templates/telegram-bot/core/tsconfig.json +9 -0
- package/templates/telegram-bot/docker-compose.yml +37 -0
- package/templates/telegram-bot/docs/cli-commands.md +377 -0
- package/templates/telegram-bot/docs/development.md +363 -0
- package/templates/telegram-bot/docs/environment.md +460 -0
- package/templates/telegram-bot/docs/examples/middleware-auth.md +335 -0
- package/templates/telegram-bot/docs/examples/simple-command.md +207 -0
- package/templates/telegram-bot/docs/examples/webhook-setup.md +362 -0
- package/templates/telegram-bot/docs/getting-started.md +223 -0
- package/templates/telegram-bot/docs/troubleshooting.md +489 -0
- package/templates/telegram-bot/package.json +49 -0
- package/templates/telegram-bot/packages/utils/package.json +12 -0
- package/templates/telegram-bot/packages/utils/src/index.ts +2 -0
- package/templates/telegram-bot/packages/utils/src/logger.ts +72 -0
- package/templates/telegram-bot/packages/utils/src/result.ts +80 -0
- package/templates/telegram-bot/tools/README.md +47 -0
- package/templates/telegram-bot/tools/commands/doctor.ts +460 -0
- package/templates/telegram-bot/tools/commands/index.ts +35 -0
- package/templates/telegram-bot/tools/commands/ngrok.ts +207 -0
- package/templates/telegram-bot/tools/commands/setup.ts +368 -0
- package/templates/telegram-bot/tools/commands/status.ts +140 -0
- package/templates/telegram-bot/tools/index.ts +16 -0
- package/templates/telegram-bot/tools/package.json +12 -0
- package/templates/telegram-bot/tools/utils/index.ts +13 -0
- package/templates/telegram-bot/tsconfig.json +22 -0
- package/templates/telegram-bot/vitest.config.ts +29 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { config as loadEnv } from 'dotenv'
|
|
3
|
+
import { readFile, writeFile } from 'fs/promises'
|
|
4
|
+
import { resolve } from 'path'
|
|
5
|
+
import { existsSync } from 'fs'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
import type { BotCommand } from './index.js'
|
|
8
|
+
|
|
9
|
+
// Simple logger for CLI (independent of bot's logger)
|
|
10
|
+
const cliLogger = {
|
|
11
|
+
info: (msg: string) => console.log(chalk.blue('ℹ'), msg),
|
|
12
|
+
success: (msg: string) => console.log(chalk.green('✓'), msg),
|
|
13
|
+
error: (msg: string) => console.error(chalk.red('✗'), msg),
|
|
14
|
+
warn: (msg: string) => console.log(chalk.yellow('⚠'), msg),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface NgrokCommand extends BotCommand {
|
|
18
|
+
name: 'ngrok'
|
|
19
|
+
description: string
|
|
20
|
+
register: (program: Command) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const command: NgrokCommand = {
|
|
24
|
+
name: 'ngrok',
|
|
25
|
+
description: 'Start ngrok tunnel for webhook testing',
|
|
26
|
+
|
|
27
|
+
register(program: Command) {
|
|
28
|
+
program
|
|
29
|
+
.command('ngrok')
|
|
30
|
+
.description('Start ngrok tunnel for webhook testing')
|
|
31
|
+
.option('-p, --port <port>', 'Port to forward (default: 3000)', '3000')
|
|
32
|
+
.option('-e, --environment <env>', 'Environment (local|staging|production)', 'local')
|
|
33
|
+
.option('-w, --webhook-url', 'Auto-update webhook URL in .env', false)
|
|
34
|
+
.option('-s, --start-bot', 'Auto-start bot after ngrok', false)
|
|
35
|
+
.option('-f, --force', 'Force start even if conflict detected', false)
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
await handleNgrok(options)
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default command
|
|
43
|
+
|
|
44
|
+
async function handleNgrok(options: {
|
|
45
|
+
port: string
|
|
46
|
+
environment: string
|
|
47
|
+
webhookUrl: boolean
|
|
48
|
+
startBot: boolean
|
|
49
|
+
force: boolean
|
|
50
|
+
}): Promise<void> {
|
|
51
|
+
const env = options.environment || 'local'
|
|
52
|
+
|
|
53
|
+
// Determine environment file path
|
|
54
|
+
const envPath = resolve(`./core/.env.${env}`)
|
|
55
|
+
|
|
56
|
+
// Check if environment file exists
|
|
57
|
+
if (!existsSync(envPath)) {
|
|
58
|
+
cliLogger.error(`Environment file not found: ${chalk.cyan(envPath)}`)
|
|
59
|
+
cliLogger.info(`Create it first or use ${chalk.yellow('--environment local')}`)
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Load environment file
|
|
64
|
+
loadEnv({ path: envPath })
|
|
65
|
+
|
|
66
|
+
cliLogger.info(`Starting ngrok for ${chalk.cyan(env)}`)
|
|
67
|
+
|
|
68
|
+
// Check instance conflicts if not forced
|
|
69
|
+
if (!options.force) {
|
|
70
|
+
const lockFiles = await discoverLockFiles()
|
|
71
|
+
const conflictingInstance = lockFiles.find((lockFile) => {
|
|
72
|
+
const instanceEnv = lockFile.split('/').pop()?.replace('.lock', '').replace('mks-bot-', '') || ''
|
|
73
|
+
return instanceEnv === env
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (conflictingInstance) {
|
|
77
|
+
cliLogger.warn(`Possible instance conflict detected for ${chalk.cyan(env)}`)
|
|
78
|
+
cliLogger.info(`Use ${chalk.yellow('--force')} to start anyway`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check ngrok installation
|
|
83
|
+
const ngrokCheck = Bun.spawn(['ngrok', 'version'], {
|
|
84
|
+
stdout: 'pipe',
|
|
85
|
+
stderr: 'pipe',
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const ngrokVersion = await ngrokCheck.exited
|
|
89
|
+
if (ngrokVersion !== 0) {
|
|
90
|
+
cliLogger.error('ngrok not found. Install from https://ngrok.com/download')
|
|
91
|
+
process.exit(1)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
cliLogger.info(`Starting ngrok tunnel for port ${options.port}`)
|
|
95
|
+
|
|
96
|
+
const ngrokProcess = Bun.spawn(['ngrok', 'http', options.port], {
|
|
97
|
+
stdout: 'inherit',
|
|
98
|
+
stderr: 'inherit',
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Wait a bit for ngrok to start and get the URL
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
103
|
+
|
|
104
|
+
// Try to get the tunnel URL from ngrok API
|
|
105
|
+
const tunnelUrl = await getNgrokTunnelUrl()
|
|
106
|
+
|
|
107
|
+
if (tunnelUrl) {
|
|
108
|
+
cliLogger.success(`Tunnel: ${chalk.underline(tunnelUrl)}`)
|
|
109
|
+
|
|
110
|
+
// Auto-update webhook URL if requested
|
|
111
|
+
if (options.webhookUrl) {
|
|
112
|
+
await updateEnvFile(envPath, {
|
|
113
|
+
TG_WEBHOOK_URL: tunnelUrl,
|
|
114
|
+
TG_MODE: 'webhook',
|
|
115
|
+
TG_NGROK_ENABLED: 'true',
|
|
116
|
+
})
|
|
117
|
+
cliLogger.success(`Updated ${chalk.cyan(envPath)}`)
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
cliLogger.warn('Could not detect tunnel URL. Check ngrok output above.')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options.startBot) {
|
|
124
|
+
cliLogger.info('Starting bot...')
|
|
125
|
+
|
|
126
|
+
const botProcess = Bun.spawn(['bun', 'run', 'start'], {
|
|
127
|
+
cwd: resolve('.'),
|
|
128
|
+
stdout: 'inherit',
|
|
129
|
+
stderr: 'inherit',
|
|
130
|
+
env: {
|
|
131
|
+
...process.env,
|
|
132
|
+
TG_ENV: env,
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
process.on('SIGINT', () => {
|
|
137
|
+
cliLogger.warn('Shutting down...')
|
|
138
|
+
ngrokProcess.kill()
|
|
139
|
+
botProcess.kill()
|
|
140
|
+
process.exit(0)
|
|
141
|
+
})
|
|
142
|
+
} else {
|
|
143
|
+
process.on('SIGINT', () => {
|
|
144
|
+
cliLogger.warn('Stopping ngrok...')
|
|
145
|
+
ngrokProcess.kill()
|
|
146
|
+
process.exit(0)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await ngrokProcess.exited
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function discoverLockFiles(): Promise<string[]> {
|
|
154
|
+
const { glob } = await import('glob')
|
|
155
|
+
try {
|
|
156
|
+
return await glob('core/tmp/*.lock', { absolute: true })
|
|
157
|
+
} catch {
|
|
158
|
+
return []
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface NgrokApiTunnel {
|
|
163
|
+
public_url: string
|
|
164
|
+
proto: string
|
|
165
|
+
name: string
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface NgrokApiResponse {
|
|
169
|
+
tunnels: NgrokApiTunnel[]
|
|
170
|
+
uri: string
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function getNgrokTunnelUrl(): Promise<string | null> {
|
|
174
|
+
try {
|
|
175
|
+
const response = await fetch('http://127.0.0.1:4040/api/tunnels')
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
const data = (await response.json()) as NgrokApiResponse
|
|
180
|
+
// eslint-disable-next-line ts/no-unnecessary-condition -- Runtime check
|
|
181
|
+
if (data.tunnels && data.tunnels.length > 0) {
|
|
182
|
+
return data.tunnels[0]?.public_url ?? null
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function updateEnvFile(
|
|
191
|
+
path: string,
|
|
192
|
+
updates: Record<string, string>
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
const content = await readFile(path, 'utf-8')
|
|
195
|
+
const lines = content.split('\n')
|
|
196
|
+
|
|
197
|
+
const updated = lines.map((line) => {
|
|
198
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
199
|
+
if (line.startsWith(`${key}=`)) {
|
|
200
|
+
return `${key}=${value}`
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return line
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
await writeFile(path, updated.join('\n'))
|
|
207
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { readFile, writeFile, copyFile } from 'fs/promises'
|
|
3
|
+
import { existsSync } from 'fs'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
import { input, select, confirm } from '@inquirer/prompts'
|
|
7
|
+
import type { BotCommand } from './index.js'
|
|
8
|
+
|
|
9
|
+
const cliLogger = {
|
|
10
|
+
info: (msg: string) => console.log(chalk.blue('ℹ'), msg),
|
|
11
|
+
success: (msg: string) => console.log(chalk.green('✓'), msg),
|
|
12
|
+
error: (msg: string) => console.error(chalk.red('✗'), msg),
|
|
13
|
+
warn: (msg: string) => console.log(chalk.yellow('⚠'), msg),
|
|
14
|
+
title: (msg: string) => console.log(chalk.cyan.bold('\n' + msg + '\n')),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SetupCommand extends BotCommand {
|
|
18
|
+
name: 'setup'
|
|
19
|
+
description: string
|
|
20
|
+
register: (program: Command) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SetupOptions {
|
|
24
|
+
token?: string
|
|
25
|
+
mode?: 'polling' | 'webhook'
|
|
26
|
+
environment?: 'local' | 'staging' | 'production'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SetupConfig {
|
|
30
|
+
TG_BOT_TOKEN: string
|
|
31
|
+
TG_MODE: 'polling' | 'webhook'
|
|
32
|
+
TG_WEBHOOK_URL?: string
|
|
33
|
+
TG_WEBHOOK_SECRET?: string
|
|
34
|
+
TG_ENV: string
|
|
35
|
+
TG_INSTANCE_NAME: string
|
|
36
|
+
TG_LOG_CHAT_ID?: string
|
|
37
|
+
TG_LOG_TOPIC_ID?: number
|
|
38
|
+
TG_CONTROL_CHAT_ID?: string
|
|
39
|
+
TG_CONTROL_TOPIC_ID?: number
|
|
40
|
+
TG_AUTHORIZED_USER_IDS?: string
|
|
41
|
+
LOG_LEVEL?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const command: SetupCommand = {
|
|
45
|
+
name: 'setup',
|
|
46
|
+
description: 'Interactive environment setup',
|
|
47
|
+
|
|
48
|
+
register(program: Command) {
|
|
49
|
+
program
|
|
50
|
+
.command('setup')
|
|
51
|
+
.description('Configure environment variables interactively')
|
|
52
|
+
.option('-t, --token <value>', 'Bot token from @BotFather')
|
|
53
|
+
.option('-m, --mode <polling|webhook>', 'Bot operation mode')
|
|
54
|
+
.option('-e, --environment <local|staging|production>', 'Target environment', 'local')
|
|
55
|
+
.action(async (options) => {
|
|
56
|
+
await handleSetup(options)
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default command
|
|
62
|
+
|
|
63
|
+
async function handleSetup(options: SetupOptions): Promise<void> {
|
|
64
|
+
cliLogger.title('mks-telegram-bot Setup')
|
|
65
|
+
|
|
66
|
+
const environment = options.environment ?? 'local'
|
|
67
|
+
const envFile = join(process.cwd(), 'core', `.env.${environment}`)
|
|
68
|
+
const envExample = join(process.cwd(), 'core', '.env.example')
|
|
69
|
+
|
|
70
|
+
// Check if .env file already exists
|
|
71
|
+
if (existsSync(envFile)) {
|
|
72
|
+
cliLogger.warn(`Environment file already exists: ${envFile}`)
|
|
73
|
+
|
|
74
|
+
const shouldContinue = await confirm({
|
|
75
|
+
message: 'Do you want to overwrite it?',
|
|
76
|
+
default: false,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if (!shouldContinue) {
|
|
80
|
+
cliLogger.info('Setup cancelled')
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Copy .env.example to .env.{environment}
|
|
86
|
+
if (!existsSync(envFile) && existsSync(envExample)) {
|
|
87
|
+
await copyFile(envExample, envFile)
|
|
88
|
+
cliLogger.success(`Created ${envFile}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Read existing env file to preserve comments
|
|
92
|
+
let envContent = ''
|
|
93
|
+
if (existsSync(envFile)) {
|
|
94
|
+
envContent = await readFile(envFile, 'utf-8')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Interactive prompts (skip if provided via flags)
|
|
98
|
+
const config: SetupConfig = await gatherConfig(options, envContent)
|
|
99
|
+
|
|
100
|
+
// Update env file
|
|
101
|
+
const updatedContent = updateEnvFile(envContent, config)
|
|
102
|
+
await writeFile(envFile, updatedContent, 'utf-8')
|
|
103
|
+
|
|
104
|
+
cliLogger.success(`Environment configured: ${envFile}`)
|
|
105
|
+
cliLogger.info('\nNext steps:')
|
|
106
|
+
cliLogger.info(' 1. Review the configuration in the env file')
|
|
107
|
+
cliLogger.info(' 2. Run: bun run dev')
|
|
108
|
+
cliLogger.info(' 3. Send /start to your bot in Telegram')
|
|
109
|
+
|
|
110
|
+
// Validate token if provided
|
|
111
|
+
if (config.TG_BOT_TOKEN) {
|
|
112
|
+
await validateToken(config.TG_BOT_TOKEN)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function gatherConfig(options: SetupOptions, _envContent: string): Promise<SetupConfig> {
|
|
117
|
+
const config: Partial<SetupConfig> = {}
|
|
118
|
+
|
|
119
|
+
// Bot token
|
|
120
|
+
if (options.token) {
|
|
121
|
+
config.TG_BOT_TOKEN = options.token
|
|
122
|
+
} else {
|
|
123
|
+
cliLogger.info('\nTo get a bot token, open Telegram and talk to @BotFather:')
|
|
124
|
+
cliLogger.info(' 1. Send /newbot')
|
|
125
|
+
cliLogger.info(' 2. Choose a name for your bot')
|
|
126
|
+
cliLogger.info(' 3. Choose a username (must end in "bot")')
|
|
127
|
+
cliLogger.info(' 4. Copy the token provided\n')
|
|
128
|
+
|
|
129
|
+
config.TG_BOT_TOKEN = await input({
|
|
130
|
+
message: 'Enter your bot token:',
|
|
131
|
+
validate: (value: string) => {
|
|
132
|
+
if (!value || value.trim().length === 0) {
|
|
133
|
+
return 'Bot token is required'
|
|
134
|
+
}
|
|
135
|
+
if (!value.includes(':')) {
|
|
136
|
+
return 'Invalid token format (should be like 123456:ABC-DEF1234...)'
|
|
137
|
+
}
|
|
138
|
+
return true
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Mode
|
|
144
|
+
if (options.mode) {
|
|
145
|
+
config.TG_MODE = options.mode
|
|
146
|
+
} else {
|
|
147
|
+
const modeSelection = await select({
|
|
148
|
+
message: 'Select bot operation mode:',
|
|
149
|
+
choices: [
|
|
150
|
+
{ name: 'Polling (recommended for development)', value: 'polling' },
|
|
151
|
+
{ name: 'Webhook (recommended for production)', value: 'webhook' },
|
|
152
|
+
],
|
|
153
|
+
})
|
|
154
|
+
config.TG_MODE = modeSelection as 'polling' | 'webhook'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Webhook configuration (if webhook mode)
|
|
158
|
+
if (config.TG_MODE === 'webhook') {
|
|
159
|
+
cliLogger.warn('\nWebhook mode requires a public HTTPS endpoint')
|
|
160
|
+
|
|
161
|
+
config.TG_WEBHOOK_URL = await input({
|
|
162
|
+
message: 'Enter webhook URL (https://your-domain.com/webhook):',
|
|
163
|
+
validate: (value: string) => {
|
|
164
|
+
if (!value || value.trim().length === 0) {
|
|
165
|
+
return 'Webhook URL is required for webhook mode'
|
|
166
|
+
}
|
|
167
|
+
if (!value.startsWith('https://')) {
|
|
168
|
+
return 'Webhook URL must use HTTPS'
|
|
169
|
+
}
|
|
170
|
+
return true
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
config.TG_WEBHOOK_SECRET = await input({
|
|
175
|
+
message: 'Enter webhook secret (min 16 chars):',
|
|
176
|
+
validate: (value: string) => {
|
|
177
|
+
if (!value || value.length < 16) {
|
|
178
|
+
return 'Webhook secret must be at least 16 characters'
|
|
179
|
+
}
|
|
180
|
+
return true
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Environment identification
|
|
186
|
+
const envSelection = await select({
|
|
187
|
+
message: 'Select environment:',
|
|
188
|
+
choices: [
|
|
189
|
+
{ name: 'Local (development)', value: 'local' },
|
|
190
|
+
{ name: 'Staging (testing)', value: 'staging' },
|
|
191
|
+
{ name: 'Production', value: 'production' },
|
|
192
|
+
],
|
|
193
|
+
default: options.environment ?? 'local',
|
|
194
|
+
})
|
|
195
|
+
config.TG_ENV = envSelection
|
|
196
|
+
|
|
197
|
+
// Instance name
|
|
198
|
+
config.TG_INSTANCE_NAME = await input({
|
|
199
|
+
message: 'Enter instance name:',
|
|
200
|
+
default: 'mks-bot',
|
|
201
|
+
validate: (value: string) => {
|
|
202
|
+
if (!value || value.trim().length === 0) {
|
|
203
|
+
return 'Instance name is required'
|
|
204
|
+
}
|
|
205
|
+
return true
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Optional: Log streaming
|
|
210
|
+
const enableLogStreaming = await confirm({
|
|
211
|
+
message: 'Enable log streaming to Telegram?',
|
|
212
|
+
default: false,
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
if (enableLogStreaming) {
|
|
216
|
+
cliLogger.info('\nTo get chat IDs:')
|
|
217
|
+
cliLogger.info(' 1. Add your bot to a group or channel')
|
|
218
|
+
cliLogger.info(' 2. Send a message to the bot')
|
|
219
|
+
cliLogger.info(' 3. Use: bun run cli status --json to see updates')
|
|
220
|
+
cliLogger.info(' 4. Or use @GetTelegraphBot in Telegram\n')
|
|
221
|
+
|
|
222
|
+
config.TG_LOG_CHAT_ID = await input({
|
|
223
|
+
message: 'Enter log chat ID (optional, press Enter to skip):',
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
if (config.TG_LOG_CHAT_ID && config.TG_LOG_CHAT_ID.trim().length > 0) {
|
|
227
|
+
const useTopic = await confirm({
|
|
228
|
+
message: 'Use a topic for log messages?',
|
|
229
|
+
default: false,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
if (useTopic) {
|
|
233
|
+
const topicId = await input({
|
|
234
|
+
message: 'Enter topic ID:',
|
|
235
|
+
validate: (value: string) => {
|
|
236
|
+
const num = Number.parseInt(value, 10)
|
|
237
|
+
if (Number.isNaN(num)) {
|
|
238
|
+
return 'Topic ID must be a number'
|
|
239
|
+
}
|
|
240
|
+
return true
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
config.TG_LOG_TOPIC_ID = Number.parseInt(topicId, 10)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Optional: Control commands
|
|
249
|
+
const enableControl = await confirm({
|
|
250
|
+
message: 'Enable control commands (/stop, /restart, etc.)?',
|
|
251
|
+
default: false,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
if (enableControl) {
|
|
255
|
+
config.TG_CONTROL_CHAT_ID = await input({
|
|
256
|
+
message: 'Enter control chat ID:',
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (config.TG_CONTROL_CHAT_ID && config.TG_CONTROL_CHAT_ID.trim().length > 0) {
|
|
260
|
+
const useTopic = await confirm({
|
|
261
|
+
message: 'Use a topic for control messages?',
|
|
262
|
+
default: false,
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
if (useTopic) {
|
|
266
|
+
const topicId = await input({
|
|
267
|
+
message: 'Enter topic ID:',
|
|
268
|
+
validate: (value: string) => {
|
|
269
|
+
const num = Number.parseInt(value, 10)
|
|
270
|
+
if (Number.isNaN(num)) {
|
|
271
|
+
return 'Topic ID must be a number'
|
|
272
|
+
}
|
|
273
|
+
return true
|
|
274
|
+
},
|
|
275
|
+
})
|
|
276
|
+
config.TG_CONTROL_TOPIC_ID = Number.parseInt(topicId, 10)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Authorized users
|
|
281
|
+
const authUsers = await input({
|
|
282
|
+
message: 'Enter authorized user IDs (comma-separated):',
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
if (authUsers && authUsers.trim().length > 0) {
|
|
286
|
+
config.TG_AUTHORIZED_USER_IDS = authUsers
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Log level
|
|
291
|
+
const logLevel = await select({
|
|
292
|
+
message: 'Select log level:',
|
|
293
|
+
choices: [
|
|
294
|
+
{ name: 'Debug (verbose)', value: 'debug' },
|
|
295
|
+
{ name: 'Info (default)', value: 'info' },
|
|
296
|
+
{ name: 'Warnings only', value: 'warn' },
|
|
297
|
+
{ name: 'Errors only', value: 'error' },
|
|
298
|
+
],
|
|
299
|
+
default: 'info',
|
|
300
|
+
})
|
|
301
|
+
config.LOG_LEVEL = logLevel
|
|
302
|
+
|
|
303
|
+
return config as SetupConfig
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function updateEnvFile(content: string, config: SetupConfig): string {
|
|
307
|
+
const lines = content.split('\n')
|
|
308
|
+
const updated: string[] = []
|
|
309
|
+
|
|
310
|
+
for (const line of lines) {
|
|
311
|
+
// Skip empty lines and comments
|
|
312
|
+
if (line.trim().length === 0 || line.trim().startsWith('#')) {
|
|
313
|
+
updated.push(line)
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const [key] = line.split('=')
|
|
318
|
+
|
|
319
|
+
// Update matching keys
|
|
320
|
+
if (key === 'TG_BOT_TOKEN' && config.TG_BOT_TOKEN !== undefined) {
|
|
321
|
+
updated.push(`${key}=${config.TG_BOT_TOKEN}`)
|
|
322
|
+
} else if (key === 'TG_MODE' && config.TG_MODE !== undefined) {
|
|
323
|
+
updated.push(`${key}=${config.TG_MODE}`)
|
|
324
|
+
} else if (key === 'TG_WEBHOOK_URL' && config.TG_WEBHOOK_URL !== undefined) {
|
|
325
|
+
updated.push(`${key}=${config.TG_WEBHOOK_URL}`)
|
|
326
|
+
} else if (key === 'TG_WEBHOOK_SECRET' && config.TG_WEBHOOK_SECRET !== undefined) {
|
|
327
|
+
updated.push(`${key}=${config.TG_WEBHOOK_SECRET}`)
|
|
328
|
+
} else if (key === 'TG_ENV' && config.TG_ENV !== undefined) {
|
|
329
|
+
updated.push(`${key}=${config.TG_ENV}`)
|
|
330
|
+
} else if (key === 'TG_INSTANCE_NAME' && config.TG_INSTANCE_NAME !== undefined) {
|
|
331
|
+
updated.push(`${key}=${config.TG_INSTANCE_NAME}`)
|
|
332
|
+
} else if (key === 'TG_LOG_CHAT_ID' && config.TG_LOG_CHAT_ID !== undefined) {
|
|
333
|
+
updated.push(`${key}=${config.TG_LOG_CHAT_ID}`)
|
|
334
|
+
} else if (key === 'TG_LOG_TOPIC_ID' && config.TG_LOG_TOPIC_ID !== undefined) {
|
|
335
|
+
updated.push(`${key}=${config.TG_LOG_TOPIC_ID}`)
|
|
336
|
+
} else if (key === 'TG_CONTROL_CHAT_ID' && config.TG_CONTROL_CHAT_ID !== undefined) {
|
|
337
|
+
updated.push(`${key}=${config.TG_CONTROL_CHAT_ID}`)
|
|
338
|
+
} else if (key === 'TG_CONTROL_TOPIC_ID' && config.TG_CONTROL_TOPIC_ID !== undefined) {
|
|
339
|
+
updated.push(`${key}=${config.TG_CONTROL_TOPIC_ID}`)
|
|
340
|
+
} else if (key === 'TG_AUTHORIZED_USER_IDS' && config.TG_AUTHORIZED_USER_IDS !== undefined) {
|
|
341
|
+
updated.push(`${key}=${config.TG_AUTHORIZED_USER_IDS}`)
|
|
342
|
+
} else if (key === 'LOG_LEVEL' && config.LOG_LEVEL !== undefined) {
|
|
343
|
+
updated.push(`${key}=${config.LOG_LEVEL}`)
|
|
344
|
+
} else {
|
|
345
|
+
updated.push(line)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return updated.join('\n')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function validateToken(token: string): Promise<void> {
|
|
353
|
+
cliLogger.info('\nValidating bot token with Telegram API...')
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const response = await fetch(`https://api.telegram.org/bot${token}/getMe`)
|
|
357
|
+
const data = (await response.json()) as { ok: boolean; result?: { username: string; first_name: string } }
|
|
358
|
+
|
|
359
|
+
if (data.ok && data.result) {
|
|
360
|
+
cliLogger.success(`Bot connected: @${data.result.username} (${data.result.first_name})`)
|
|
361
|
+
} else {
|
|
362
|
+
cliLogger.warn('Could not validate token. The bot may not work correctly.')
|
|
363
|
+
cliLogger.info('Make sure the token is correct and try again.')
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
cliLogger.warn('Could not reach Telegram API. Check your internet connection.')
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { readFile } from 'fs/promises'
|
|
3
|
+
import { existsSync } from 'fs'
|
|
4
|
+
import { glob } from 'glob'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
import type { BotCommand } from './index.js'
|
|
7
|
+
|
|
8
|
+
const cliLogger = {
|
|
9
|
+
info: (msg: string) => console.log(chalk.blue('ℹ'), msg),
|
|
10
|
+
success: (msg: string) => console.log(chalk.green('✓'), msg),
|
|
11
|
+
error: (msg: string) => console.error(chalk.red('✗'), msg),
|
|
12
|
+
warn: (msg: string) => console.log(chalk.yellow('⚠'), msg),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface StatusCommand extends BotCommand {
|
|
16
|
+
name: 'status'
|
|
17
|
+
description: string
|
|
18
|
+
register: (program: Command) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface InstanceInfo {
|
|
22
|
+
pid: number
|
|
23
|
+
instanceId: string
|
|
24
|
+
environment: string
|
|
25
|
+
instanceName: string
|
|
26
|
+
startTime: string
|
|
27
|
+
nodeVersion: string
|
|
28
|
+
cwd: string
|
|
29
|
+
running: boolean
|
|
30
|
+
uptime: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const command: StatusCommand = {
|
|
34
|
+
name: 'status',
|
|
35
|
+
description: 'Show running bot instances',
|
|
36
|
+
|
|
37
|
+
register(program: Command) {
|
|
38
|
+
program
|
|
39
|
+
.command('status')
|
|
40
|
+
.description('Show all running bot instances')
|
|
41
|
+
.option('-j, --json', 'Output as JSON')
|
|
42
|
+
.action(async (options) => {
|
|
43
|
+
await handleStatus(options)
|
|
44
|
+
})
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default command
|
|
49
|
+
|
|
50
|
+
async function handleStatus(options: { json?: boolean }): Promise<void> {
|
|
51
|
+
const instances = await discoverInstances()
|
|
52
|
+
|
|
53
|
+
if (instances.length === 0) {
|
|
54
|
+
cliLogger.warn('No instances found')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (options.json) {
|
|
59
|
+
console.log(JSON.stringify(instances, null, 2))
|
|
60
|
+
} else {
|
|
61
|
+
displayInstancesTable(instances)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function discoverInstances(): Promise<InstanceInfo[]> {
|
|
66
|
+
const instances: InstanceInfo[] = []
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const lockFiles = await glob('core/tmp/*.lock', { absolute: false })
|
|
70
|
+
|
|
71
|
+
// Sequential file loading is intentional for error handling
|
|
72
|
+
/* oxlint-disable no-await-in-loop */
|
|
73
|
+
for (const lockFile of lockFiles) {
|
|
74
|
+
try {
|
|
75
|
+
if (!existsSync(lockFile)) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const content = await readFile(lockFile, 'utf-8')
|
|
80
|
+
const lockData = JSON.parse(content)
|
|
81
|
+
|
|
82
|
+
instances.push({
|
|
83
|
+
...lockData,
|
|
84
|
+
running: isProcessRunning(lockData.pid),
|
|
85
|
+
uptime: calculateUptime(lockData.startTime),
|
|
86
|
+
})
|
|
87
|
+
} catch {
|
|
88
|
+
// Skip invalid lock files
|
|
89
|
+
// oxlint-disable-next-line no-console -- Intentional logging
|
|
90
|
+
cliLogger.warn(`Failed to read ${lockFile}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/* oxlint-enable no-await-in-loop */
|
|
94
|
+
} catch {
|
|
95
|
+
cliLogger.error('Failed to scan for instances')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return instances
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isProcessRunning(pid: number): boolean {
|
|
102
|
+
try {
|
|
103
|
+
process.kill(pid, 0)
|
|
104
|
+
return true
|
|
105
|
+
} catch {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function calculateUptime(startTime: string): string {
|
|
111
|
+
const start = new Date(startTime)
|
|
112
|
+
const now = new Date()
|
|
113
|
+
const diff = now.getTime() - start.getTime()
|
|
114
|
+
|
|
115
|
+
const seconds = Math.floor(diff / 1000)
|
|
116
|
+
const minutes = Math.floor(seconds / 60)
|
|
117
|
+
const hours = Math.floor(minutes / 60)
|
|
118
|
+
const days = Math.floor(hours / 24)
|
|
119
|
+
|
|
120
|
+
if (days > 0) return `${days}d ${hours % 24}h`
|
|
121
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`
|
|
122
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`
|
|
123
|
+
return `${seconds}s`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function displayInstancesTable(instances: InstanceInfo[]): void {
|
|
127
|
+
const tableData = instances.map((i) => ({
|
|
128
|
+
PID: i.pid.toString(),
|
|
129
|
+
Environment: i.environment,
|
|
130
|
+
Name: i.instanceName,
|
|
131
|
+
Status: i.running
|
|
132
|
+
? chalk.green('✓ Running')
|
|
133
|
+
: chalk.red('✗ Stopped'),
|
|
134
|
+
Uptime: i.uptime,
|
|
135
|
+
}))
|
|
136
|
+
|
|
137
|
+
console.log('')
|
|
138
|
+
console.table(tableData)
|
|
139
|
+
console.log('')
|
|
140
|
+
}
|