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.
Files changed (182) hide show
  1. package/README.md +181 -0
  2. package/dist/bin.js +5755 -0
  3. package/dist/templates/monorepo/CLAUDE.md +164 -0
  4. package/dist/templates/monorepo/LICENSE +21 -0
  5. package/dist/templates/monorepo/MUST-FOLLOW-GUIDELINES.md +269 -0
  6. package/dist/templates/monorepo/README.md +74 -0
  7. package/dist/templates/monorepo/SYNC_VERIFICATION.md +1 -0
  8. package/dist/templates/monorepo/apps/example/package.json +19 -0
  9. package/dist/templates/monorepo/apps/example/src/index.ts +23 -0
  10. package/dist/templates/monorepo/apps/example/src/types/index.ts +7 -0
  11. package/dist/templates/monorepo/apps/example/src/utils/index.ts +7 -0
  12. package/dist/templates/monorepo/core/packages/main/package.json +41 -0
  13. package/dist/templates/monorepo/core/packages/main/rolldown.config.ts +24 -0
  14. package/dist/templates/monorepo/core/packages/main/src/index.ts +80 -0
  15. package/dist/templates/monorepo/core/packages/main/src/types/constants.ts +15 -0
  16. package/dist/templates/monorepo/core/packages/main/src/types/index.ts +8 -0
  17. package/dist/templates/monorepo/core/packages/main/src/types/main.types.ts +25 -0
  18. package/dist/templates/monorepo/core/packages/main/src/utils/index.ts +5 -0
  19. package/dist/templates/monorepo/core/packages/utils/package.json +43 -0
  20. package/dist/templates/monorepo/core/packages/utils/rolldown.config.ts +34 -0
  21. package/dist/templates/monorepo/core/packages/utils/src/index.ts +2 -0
  22. package/dist/templates/monorepo/core/packages/utils/src/logger.ts +68 -0
  23. package/dist/templates/monorepo/core/packages/utils/src/result.ts +146 -0
  24. package/dist/templates/monorepo/core/packages/utils/src/types/constants.ts +15 -0
  25. package/dist/templates/monorepo/core/packages/utils/src/types/index.ts +8 -0
  26. package/dist/templates/monorepo/core/packages/utils/src/types/utils.types.ts +32 -0
  27. package/dist/templates/monorepo/core/packages/utils/src/utils/index.ts +5 -0
  28. package/dist/templates/monorepo/oxlint.json +14 -0
  29. package/dist/templates/monorepo/package.json +39 -0
  30. package/dist/templates/monorepo/tsconfig.json +35 -0
  31. package/dist/templates/telegram-bot/.oxlintrc.json +33 -0
  32. package/dist/templates/telegram-bot/.prettierignore +5 -0
  33. package/dist/templates/telegram-bot/.prettierrc +26 -0
  34. package/dist/templates/telegram-bot/CLAUDE.deploy.md +356 -0
  35. package/dist/templates/telegram-bot/CLAUDE.dev.md +266 -0
  36. package/dist/templates/telegram-bot/CLAUDE.md +280 -0
  37. package/dist/templates/telegram-bot/Dockerfile +46 -0
  38. package/dist/templates/telegram-bot/README.md +245 -0
  39. package/dist/templates/telegram-bot/apps/.gitkeep +0 -0
  40. package/dist/templates/telegram-bot/bun.lock +208 -0
  41. package/dist/templates/telegram-bot/core/.env.example +71 -0
  42. package/dist/templates/telegram-bot/core/README.md +1067 -0
  43. package/dist/templates/telegram-bot/core/package.json +15 -0
  44. package/dist/templates/telegram-bot/core/src/config/env.ts +131 -0
  45. package/dist/templates/telegram-bot/core/src/config/index.ts +97 -0
  46. package/dist/templates/telegram-bot/core/src/config/logging.ts +110 -0
  47. package/dist/templates/telegram-bot/core/src/handlers/control.ts +85 -0
  48. package/dist/templates/telegram-bot/core/src/handlers/health.ts +83 -0
  49. package/dist/templates/telegram-bot/core/src/handlers/logs.ts +126 -0
  50. package/dist/templates/telegram-bot/core/src/index.ts +161 -0
  51. package/dist/templates/telegram-bot/core/src/middleware/auth.ts +41 -0
  52. package/dist/templates/telegram-bot/core/src/middleware/error-handler.ts +41 -0
  53. package/dist/templates/telegram-bot/core/src/middleware/logging.ts +1 -0
  54. package/dist/templates/telegram-bot/core/src/middleware/topics.ts +55 -0
  55. package/dist/templates/telegram-bot/core/src/types/bot.ts +92 -0
  56. package/dist/templates/telegram-bot/core/src/types/constants.ts +50 -0
  57. package/dist/templates/telegram-bot/core/src/types/result.ts +1 -0
  58. package/dist/templates/telegram-bot/core/src/utils/bot-manager.test.ts +111 -0
  59. package/dist/templates/telegram-bot/core/src/utils/bot-manager.ts +201 -0
  60. package/dist/templates/telegram-bot/core/src/utils/commands.ts +63 -0
  61. package/dist/templates/telegram-bot/core/src/utils/formatters.ts +82 -0
  62. package/dist/templates/telegram-bot/core/src/utils/instance-manager.ts +189 -0
  63. package/dist/templates/telegram-bot/core/src/utils/memory.ts +33 -0
  64. package/dist/templates/telegram-bot/core/src/utils/result.ts +26 -0
  65. package/dist/templates/telegram-bot/core/src/utils/telegram.ts +31 -0
  66. package/dist/templates/telegram-bot/core/src/utils/type-guards.ts +71 -0
  67. package/dist/templates/telegram-bot/core/tsconfig.json +9 -0
  68. package/dist/templates/telegram-bot/docker-compose.yml +37 -0
  69. package/dist/templates/telegram-bot/docs/cli-commands.md +377 -0
  70. package/dist/templates/telegram-bot/docs/development.md +363 -0
  71. package/dist/templates/telegram-bot/docs/environment.md +460 -0
  72. package/dist/templates/telegram-bot/docs/examples/middleware-auth.md +335 -0
  73. package/dist/templates/telegram-bot/docs/examples/simple-command.md +207 -0
  74. package/dist/templates/telegram-bot/docs/examples/webhook-setup.md +362 -0
  75. package/dist/templates/telegram-bot/docs/getting-started.md +223 -0
  76. package/dist/templates/telegram-bot/docs/troubleshooting.md +489 -0
  77. package/dist/templates/telegram-bot/package.json +49 -0
  78. package/dist/templates/telegram-bot/packages/utils/package.json +12 -0
  79. package/dist/templates/telegram-bot/packages/utils/src/index.ts +2 -0
  80. package/dist/templates/telegram-bot/packages/utils/src/logger.ts +72 -0
  81. package/dist/templates/telegram-bot/packages/utils/src/result.ts +80 -0
  82. package/dist/templates/telegram-bot/tools/README.md +47 -0
  83. package/dist/templates/telegram-bot/tools/commands/doctor.ts +460 -0
  84. package/dist/templates/telegram-bot/tools/commands/index.ts +35 -0
  85. package/dist/templates/telegram-bot/tools/commands/ngrok.ts +207 -0
  86. package/dist/templates/telegram-bot/tools/commands/setup.ts +368 -0
  87. package/dist/templates/telegram-bot/tools/commands/status.ts +140 -0
  88. package/dist/templates/telegram-bot/tools/index.ts +16 -0
  89. package/dist/templates/telegram-bot/tools/package.json +12 -0
  90. package/dist/templates/telegram-bot/tools/utils/index.ts +13 -0
  91. package/dist/templates/telegram-bot/tsconfig.json +22 -0
  92. package/dist/templates/telegram-bot/vitest.config.ts +29 -0
  93. package/package.json +35 -0
  94. package/templates/monorepo/CLAUDE.md +164 -0
  95. package/templates/monorepo/LICENSE +21 -0
  96. package/templates/monorepo/MUST-FOLLOW-GUIDELINES.md +269 -0
  97. package/templates/monorepo/README.md +74 -0
  98. package/templates/monorepo/apps/example/package.json +19 -0
  99. package/templates/monorepo/apps/example/src/index.ts +23 -0
  100. package/templates/monorepo/apps/example/src/types/index.ts +7 -0
  101. package/templates/monorepo/apps/example/src/utils/index.ts +7 -0
  102. package/templates/monorepo/core/packages/main/package.json +41 -0
  103. package/templates/monorepo/core/packages/main/rolldown.config.ts +24 -0
  104. package/templates/monorepo/core/packages/main/src/index.ts +80 -0
  105. package/templates/monorepo/core/packages/main/src/types/constants.ts +15 -0
  106. package/templates/monorepo/core/packages/main/src/types/index.ts +8 -0
  107. package/templates/monorepo/core/packages/main/src/types/main.types.ts +25 -0
  108. package/templates/monorepo/core/packages/main/src/utils/index.ts +5 -0
  109. package/templates/monorepo/core/packages/utils/package.json +43 -0
  110. package/templates/monorepo/core/packages/utils/rolldown.config.ts +34 -0
  111. package/templates/monorepo/core/packages/utils/src/index.ts +2 -0
  112. package/templates/monorepo/core/packages/utils/src/logger.ts +68 -0
  113. package/templates/monorepo/core/packages/utils/src/result.ts +146 -0
  114. package/templates/monorepo/core/packages/utils/src/types/constants.ts +15 -0
  115. package/templates/monorepo/core/packages/utils/src/types/index.ts +8 -0
  116. package/templates/monorepo/core/packages/utils/src/types/utils.types.ts +32 -0
  117. package/templates/monorepo/core/packages/utils/src/utils/index.ts +5 -0
  118. package/templates/monorepo/oxlint.json +14 -0
  119. package/templates/monorepo/package.json +39 -0
  120. package/templates/monorepo/tsconfig.json +35 -0
  121. package/templates/telegram-bot/.oxlintrc.json +33 -0
  122. package/templates/telegram-bot/.prettierignore +5 -0
  123. package/templates/telegram-bot/.prettierrc +26 -0
  124. package/templates/telegram-bot/CLAUDE.deploy.md +356 -0
  125. package/templates/telegram-bot/CLAUDE.dev.md +266 -0
  126. package/templates/telegram-bot/CLAUDE.md +280 -0
  127. package/templates/telegram-bot/Dockerfile +46 -0
  128. package/templates/telegram-bot/README.md +245 -0
  129. package/templates/telegram-bot/apps/.gitkeep +0 -0
  130. package/templates/telegram-bot/bun.lock +208 -0
  131. package/templates/telegram-bot/core/.env.example +71 -0
  132. package/templates/telegram-bot/core/README.md +1067 -0
  133. package/templates/telegram-bot/core/package.json +15 -0
  134. package/templates/telegram-bot/core/src/config/env.ts +131 -0
  135. package/templates/telegram-bot/core/src/config/index.ts +97 -0
  136. package/templates/telegram-bot/core/src/config/logging.ts +110 -0
  137. package/templates/telegram-bot/core/src/handlers/control.ts +85 -0
  138. package/templates/telegram-bot/core/src/handlers/health.ts +83 -0
  139. package/templates/telegram-bot/core/src/handlers/logs.ts +126 -0
  140. package/templates/telegram-bot/core/src/index.ts +161 -0
  141. package/templates/telegram-bot/core/src/middleware/auth.ts +41 -0
  142. package/templates/telegram-bot/core/src/middleware/error-handler.ts +41 -0
  143. package/templates/telegram-bot/core/src/middleware/logging.ts +1 -0
  144. package/templates/telegram-bot/core/src/middleware/topics.ts +55 -0
  145. package/templates/telegram-bot/core/src/types/bot.ts +92 -0
  146. package/templates/telegram-bot/core/src/types/constants.ts +50 -0
  147. package/templates/telegram-bot/core/src/types/result.ts +1 -0
  148. package/templates/telegram-bot/core/src/utils/bot-manager.test.ts +111 -0
  149. package/templates/telegram-bot/core/src/utils/bot-manager.ts +201 -0
  150. package/templates/telegram-bot/core/src/utils/commands.ts +63 -0
  151. package/templates/telegram-bot/core/src/utils/formatters.ts +82 -0
  152. package/templates/telegram-bot/core/src/utils/instance-manager.ts +189 -0
  153. package/templates/telegram-bot/core/src/utils/memory.ts +33 -0
  154. package/templates/telegram-bot/core/src/utils/result.ts +26 -0
  155. package/templates/telegram-bot/core/src/utils/telegram.ts +31 -0
  156. package/templates/telegram-bot/core/src/utils/type-guards.ts +71 -0
  157. package/templates/telegram-bot/core/tsconfig.json +9 -0
  158. package/templates/telegram-bot/docker-compose.yml +37 -0
  159. package/templates/telegram-bot/docs/cli-commands.md +377 -0
  160. package/templates/telegram-bot/docs/development.md +363 -0
  161. package/templates/telegram-bot/docs/environment.md +460 -0
  162. package/templates/telegram-bot/docs/examples/middleware-auth.md +335 -0
  163. package/templates/telegram-bot/docs/examples/simple-command.md +207 -0
  164. package/templates/telegram-bot/docs/examples/webhook-setup.md +362 -0
  165. package/templates/telegram-bot/docs/getting-started.md +223 -0
  166. package/templates/telegram-bot/docs/troubleshooting.md +489 -0
  167. package/templates/telegram-bot/package.json +49 -0
  168. package/templates/telegram-bot/packages/utils/package.json +12 -0
  169. package/templates/telegram-bot/packages/utils/src/index.ts +2 -0
  170. package/templates/telegram-bot/packages/utils/src/logger.ts +72 -0
  171. package/templates/telegram-bot/packages/utils/src/result.ts +80 -0
  172. package/templates/telegram-bot/tools/README.md +47 -0
  173. package/templates/telegram-bot/tools/commands/doctor.ts +460 -0
  174. package/templates/telegram-bot/tools/commands/index.ts +35 -0
  175. package/templates/telegram-bot/tools/commands/ngrok.ts +207 -0
  176. package/templates/telegram-bot/tools/commands/setup.ts +368 -0
  177. package/templates/telegram-bot/tools/commands/status.ts +140 -0
  178. package/templates/telegram-bot/tools/index.ts +16 -0
  179. package/templates/telegram-bot/tools/package.json +12 -0
  180. package/templates/telegram-bot/tools/utils/index.ts +13 -0
  181. package/templates/telegram-bot/tsconfig.json +22 -0
  182. 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
+ }