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,201 @@
1
+ import type { Telegraf } from 'telegraf'
2
+ import type { BotStatus, BotStats } from '../types/bot.js'
3
+ import { ok, type Result, err, type BotError } from './result.js'
4
+ import { botError } from '../types/result.js'
5
+ import { botLogger, badge, kv, colors, colorText } from '../middleware/logging.js'
6
+ import { getConfig } from '../config/index.js'
7
+
8
+ class BotManager {
9
+ private bot: Telegraf | null = null
10
+ private startTime: number | null = null
11
+ private stats: BotStats = {
12
+ messagesProcessed: 0,
13
+ commandsExecuted: 0,
14
+ errorsEncountered: 0,
15
+ uptimeStart: 0,
16
+ lastActivity: 0,
17
+ }
18
+
19
+ constructor() {
20
+ this.stats.uptimeStart = Date.now()
21
+ }
22
+
23
+ setBot(bot: Telegraf): void {
24
+ this.bot = bot
25
+ this.startTime = Date.now()
26
+
27
+ botLogger.info(
28
+ `${badge('BOT', 'pill')} ${kv({
29
+ status: colorText('set', colors.success),
30
+ })}`
31
+ )
32
+ }
33
+
34
+ getStatus(): Result<BotStatus, BotError> {
35
+ const config = getConfig()
36
+ const uptime = this.startTime ? Date.now() - this.startTime : 0
37
+
38
+ return ok({
39
+ status: this.bot ? 'running' : 'stopped',
40
+ mode: config.mode,
41
+ startTime: this.startTime,
42
+ uptime,
43
+ memoryUsage: process.memoryUsage(),
44
+ })
45
+ }
46
+
47
+ getStats(): Result<BotStats, BotError> {
48
+ return ok({ ...this.stats })
49
+ }
50
+
51
+ incrementMessages(): void {
52
+ this.stats.messagesProcessed++
53
+ this.stats.lastActivity = Date.now()
54
+
55
+ botLogger.debug(
56
+ `${badge('STATS', 'rounded')} ${kv({
57
+ messages: this.stats.messagesProcessed,
58
+ })}`
59
+ )
60
+ }
61
+
62
+ incrementCommands(): void {
63
+ this.stats.commandsExecuted++
64
+ this.stats.lastActivity = Date.now()
65
+
66
+ botLogger.debug(
67
+ `${badge('STATS', 'rounded')} ${kv({
68
+ commands: this.stats.commandsExecuted,
69
+ })}`
70
+ )
71
+ }
72
+
73
+ incrementErrors(): void {
74
+ this.stats.errorsEncountered++
75
+ this.stats.lastActivity = Date.now()
76
+
77
+ botLogger.debug(
78
+ `${badge('STATS', 'rounded')} ${kv({
79
+ errors: this.stats.errorsEncountered,
80
+ })}`
81
+ )
82
+ }
83
+
84
+ resetStats(): Result<void, BotError> {
85
+ const oldStats = { ...this.stats }
86
+
87
+ this.stats = {
88
+ messagesProcessed: 0,
89
+ commandsExecuted: 0,
90
+ errorsEncountered: 0,
91
+ uptimeStart: Date.now(),
92
+ lastActivity: 0,
93
+ }
94
+ this.startTime = Date.now()
95
+
96
+ botLogger.info(
97
+ `${badge('STATS', 'rounded')} ${kv({
98
+ action: colorText('reset', colors.warning),
99
+ previous: kv({
100
+ messages: oldStats.messagesProcessed,
101
+ commands: oldStats.commandsExecuted,
102
+ errors: oldStats.errorsEncountered,
103
+ }),
104
+ })}`
105
+ )
106
+
107
+ return ok(undefined)
108
+ }
109
+
110
+ authorize(userId: number): Result<void, BotError> {
111
+ const config = getConfig()
112
+
113
+ if (!config.authorizedUserIds.has(userId)) {
114
+ return err(botError('UNAUTHORIZED', `User ${userId} is not authorized`))
115
+ }
116
+
117
+ return ok(undefined)
118
+ }
119
+
120
+ async start(bot: Telegraf, mode?: 'polling' | 'webhook'): Promise<Result<void, BotError>> {
121
+ const config = getConfig()
122
+ const targetMode = mode || config.mode
123
+
124
+ if (targetMode === 'webhook' && !config.webhookUrl) {
125
+ return err(botError('WEBHOOK_NOT_CONFIGURED', 'Webhook URL not configured'))
126
+ }
127
+
128
+ this.bot = bot
129
+ this.startTime = Date.now()
130
+
131
+ if (targetMode === 'webhook') {
132
+ botLogger.info(
133
+ `${badge('BOT', 'pill')} ${kv({
134
+ action: colorText('start', colors.success),
135
+ mode: colorText('webhook', colors.info),
136
+ url: config.webhookUrl ?? 'none',
137
+ })}`
138
+ )
139
+ } else {
140
+ botLogger.info(
141
+ `${badge('BOT', 'pill')} ${kv({
142
+ action: colorText('start', colors.success),
143
+ mode: colorText('polling', colors.info),
144
+ })}`
145
+ )
146
+ }
147
+
148
+ return ok(undefined)
149
+ }
150
+
151
+ async stop(reason?: string): Promise<Result<void, BotError>> {
152
+ if (!this.bot) {
153
+ return err(botError('BOT_NOT_RUNNING', 'Bot is not running'))
154
+ }
155
+
156
+ botLogger.info(
157
+ `${badge('BOT', 'pill')} ${kv({
158
+ action: colorText('stop', colors.warning),
159
+ reason: colorText(reason || 'manual', colors.dim),
160
+ uptime: this.startTime ? formatDuration(Date.now() - this.startTime) : 'unknown',
161
+ })}`
162
+ )
163
+
164
+ this.bot.stop(reason || 'SIGINT')
165
+ this.bot = null
166
+ this.startTime = null
167
+
168
+ return ok(undefined)
169
+ }
170
+
171
+ async restart(): Promise<Result<void, BotError>> {
172
+ if (!this.bot) {
173
+ return err(botError('BOT_NOT_RUNNING', 'Cannot restart: bot is not running'))
174
+ }
175
+
176
+ botLogger.info(
177
+ `${badge('BOT', 'pill')} ${kv({
178
+ action: colorText('restart', colors.info),
179
+ })}`
180
+ )
181
+
182
+ await this.stop('restart')
183
+ this.startTime = Date.now()
184
+
185
+ return ok(undefined)
186
+ }
187
+ }
188
+
189
+ function formatDuration(ms: number): string {
190
+ const seconds = Math.floor(ms / 1000)
191
+ const minutes = Math.floor(seconds / 60)
192
+ const hours = Math.floor(minutes / 60)
193
+ const days = Math.floor(hours / 24)
194
+
195
+ if (days > 0) return `${days}d ${hours % 24}h`
196
+ if (hours > 0) return `${hours}h ${minutes % 60}m`
197
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`
198
+ return `${seconds}s`
199
+ }
200
+
201
+ export const botManager = new BotManager()
@@ -0,0 +1,63 @@
1
+ import type { Context, Middleware } from 'telegraf'
2
+ import type { Result, BotError } from './result.js'
3
+ import { commandLogger, badge, kv, colors, colorText } from '../middleware/logging.js'
4
+ import { botManager } from './bot-manager.js'
5
+
6
+ export interface CommandConfig {
7
+ name: string
8
+ description?: string
9
+ auth?: boolean
10
+ stats?: boolean
11
+ handler: CommandHandler
12
+ }
13
+
14
+ export type CommandHandler = (ctx: Context, args: string[]) => Promise<Result<void, BotError>>
15
+
16
+ export function createCommandHandler(config: CommandConfig): Middleware<Context> {
17
+ return async (ctx, next) => {
18
+ const userId = ctx.from?.id ?? 'unknown'
19
+ const username = ctx.from?.username ?? 'no-username'
20
+
21
+ commandLogger.info(
22
+ `${badge('CMD', 'rounded')} ${kv({
23
+ cmd: colorText(`/${config.name}`, colors.command),
24
+ user: colorText(String(userId), colors.user),
25
+ username: colorText(`@${username}`, colors.dim),
26
+ })}`
27
+ )
28
+
29
+ const args = extractArgs(ctx)
30
+
31
+ const result = await config.handler(ctx, args)
32
+
33
+ if (!result.ok) {
34
+ commandLogger.error(
35
+ `${badge('FAIL', 'rounded')} ${kv({
36
+ cmd: `/${config.name}`,
37
+ user: userId,
38
+ error: result.error.message,
39
+ })}`
40
+ )
41
+ await ctx.reply(`❌ ${result.error.message}`)
42
+ return
43
+ }
44
+
45
+ commandLogger.success(
46
+ `${badge('OK', 'rounded')} ${kv({
47
+ cmd: `/${config.name}`,
48
+ user: userId,
49
+ })}`
50
+ )
51
+
52
+ if (config.stats) {
53
+ botManager.incrementCommands()
54
+ }
55
+
56
+ return next()
57
+ }
58
+ }
59
+
60
+ function extractArgs(ctx: Context): string[] {
61
+ const text = ctx.message && 'text' in ctx.message ? ctx.message.text : ''
62
+ return text.split(' ').filter(Boolean)
63
+ }
@@ -0,0 +1,82 @@
1
+ import { TimeConstants } from '../types/constants.js'
2
+ import type { BotStatus } from '../types/bot.js'
3
+
4
+ export function formatUptime(uptimeMs: number): string {
5
+ const seconds = Math.floor(uptimeMs / TimeConstants.SECOND)
6
+ const minutes = Math.floor(seconds / 60)
7
+ const hours = Math.floor(minutes / 60)
8
+ const days = Math.floor(hours / 24)
9
+
10
+ if (days > 0) {
11
+ return `${days}d ${hours % 24}h ${minutes % 60}m ${seconds % 60}s`
12
+ } else if (hours > 0) {
13
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`
14
+ } else if (minutes > 0) {
15
+ return `${minutes}m ${seconds % 60}s`
16
+ } else {
17
+ return `${seconds}s`
18
+ }
19
+ }
20
+
21
+ export function formatMemory(memoryUsage: {
22
+ rss: number
23
+ heapTotal: number
24
+ heapUsed: number
25
+ external: number
26
+ arrayBuffers: number
27
+ }): string {
28
+ const mb = (bytes: number) => (bytes / 1024 / 1024).toFixed(2)
29
+
30
+ return `RSS: ${mb(memoryUsage.rss)}MB\nHeap: ${mb(memoryUsage.heapUsed)}/${mb(memoryUsage.heapTotal)}MB`
31
+ }
32
+
33
+ export function formatHealthMessage(status: BotStatus): string {
34
+ const uptime = formatUptime(status.uptime)
35
+ const memory = formatMemory(status.memoryUsage)
36
+
37
+ return `🏥 *Bot Health Status*
38
+ *Status:* ${status.status.toUpperCase()}
39
+ *Mode:* ${status.mode.toUpperCase()}
40
+ *Uptime:* ${uptime}
41
+ *Memory Usage:*
42
+ ${memory}`
43
+ }
44
+
45
+ export function formatStats(stats: {
46
+ messagesProcessed: number
47
+ commandsExecuted: number
48
+ errorsEncountered: number
49
+ }): string {
50
+ return `📊 *Bot Statistics*
51
+
52
+ *Performance:*
53
+ Messages Processed: ${stats.messagesProcessed}
54
+ Commands Executed: ${stats.commandsExecuted}
55
+ Errors Encountered: ${stats.errorsEncountered}
56
+ `
57
+ }
58
+
59
+ export function formatLogEntry(
60
+ timestamp: string,
61
+ level: string,
62
+ component: string,
63
+ message: string
64
+ ): string {
65
+ const levelEmoji = {
66
+ info: 'ℹ️',
67
+ success: '✅',
68
+ warn: '⚠️',
69
+ error: '❌',
70
+ debug: '🔍',
71
+ }
72
+
73
+ const emoji = levelEmoji[level as keyof typeof levelEmoji] || 'ℹ️'
74
+
75
+ return `${emoji} [${component}] ${message}
76
+ _${timestamp}_`
77
+ }
78
+
79
+ export function formatError(error: Error | string): string {
80
+ const message = error instanceof Error ? error.message : String(error)
81
+ return `❌ *Error:* ${message}`
82
+ }
@@ -0,0 +1,189 @@
1
+ import { writeFile, readFile, unlink } from 'fs/promises'
2
+ import { existsSync as existsSyncSync } from 'fs'
3
+ import { resolve } from 'path'
4
+ import type { BotConfig } from '../types/bot.js'
5
+ import { botLogger, badge, kv, colorText, colors } from '../middleware/logging.js'
6
+ import { ok, type Result, err } from './result.js'
7
+ import { botError, type BotError } from '../types/result.js'
8
+
9
+ export interface LockData {
10
+ pid: number
11
+ instanceId: string
12
+ environment: string
13
+ instanceName: string
14
+ startTime: string
15
+ nodeVersion: string
16
+ cwd: string
17
+ }
18
+
19
+ export interface InstanceInfo extends LockData {
20
+ running: boolean
21
+ uptime: string
22
+ }
23
+
24
+ export class InstanceManager {
25
+ private pidFile: string
26
+ private lockFile: string
27
+ private instanceId: string
28
+ private lockBackend: 'pid' | 'redis'
29
+
30
+ constructor(private config: BotConfig) {
31
+ this.instanceId = config.instanceId || this.generateInstanceId()
32
+ this.lockBackend = config.lockBackend || 'pid'
33
+
34
+ const tmpDir = resolve('./core/tmp')
35
+ this.pidFile = resolve(tmpDir, `${config.instanceName}.pid`)
36
+ this.lockFile = resolve(tmpDir, `${config.instanceName}.lock`)
37
+ }
38
+
39
+ async acquireLock(): Promise<Result<void, BotError>> {
40
+ // Skip if instance check disabled
41
+ if (!this.config.instanceCheck) {
42
+ return ok(undefined)
43
+ }
44
+
45
+ botLogger.debug(
46
+ `${badge('INSTANCE', 'rounded')} ${kv({
47
+ action: 'check',
48
+ lockFile: this.lockFile,
49
+ })}`
50
+ )
51
+
52
+ // Check if lock file exists
53
+ if (existsSyncSync(this.lockFile)) {
54
+ const existingInstance = await this.readLockFile()
55
+
56
+ // Check if process is still running
57
+ if (this.isProcessRunning(existingInstance.pid)) {
58
+ // CONFLICT DETECTED
59
+ return err(
60
+ botError(
61
+ 'INSTANCE_CONFLICT',
62
+ `Another instance is already running:\n` +
63
+ ` Name: ${existingInstance.instanceName}\n` +
64
+ ` PID: ${existingInstance.pid}\n` +
65
+ ` Environment: ${existingInstance.environment}\n` +
66
+ ` Started: ${existingInstance.startTime}\n` +
67
+ ` Uptime: ${this.calculateUptime(existingInstance.startTime)}`
68
+ )
69
+ )
70
+ }
71
+
72
+ // Stale lock file, remove it
73
+ botLogger.warn(
74
+ `${badge('INSTANCE', 'rounded')} ${kv({
75
+ action: colorText('stale lock', colors.warning),
76
+ pid: existingInstance.pid,
77
+ })}`
78
+ )
79
+ await this.cleanup()
80
+ }
81
+
82
+ // Write lock files
83
+ await this.writeLockFile()
84
+ botLogger.success(
85
+ `${badge('INSTANCE', 'rounded')} ${kv({
86
+ action: colorText('locked', colors.success),
87
+ id: this.instanceId,
88
+ })}`
89
+ )
90
+
91
+ return ok(undefined)
92
+ }
93
+
94
+ async releaseLock(): Promise<void> {
95
+ try {
96
+ await this.cleanup()
97
+ botLogger.debug(
98
+ `${badge('INSTANCE', 'rounded')} ${kv({
99
+ action: colorText('released', colors.dim),
100
+ })}`
101
+ )
102
+ } catch (error) {
103
+ botLogger.error('Failed to release lock:', error)
104
+ }
105
+ }
106
+
107
+ async getLockData(): Promise<LockData | null> {
108
+ try {
109
+ return await this.readLockFile()
110
+ } catch {
111
+ return null
112
+ }
113
+ }
114
+
115
+ private async writeLockFile(): Promise<void> {
116
+ const lockData: LockData = {
117
+ pid: process.pid,
118
+ instanceId: this.instanceId,
119
+ environment: this.config.environment,
120
+ instanceName: this.config.instanceName,
121
+ startTime: new Date().toISOString(),
122
+ nodeVersion: process.version,
123
+ cwd: process.cwd(),
124
+ }
125
+
126
+ await writeFile(this.pidFile, process.pid.toString())
127
+ await writeFile(this.lockFile, JSON.stringify(lockData, null, 2))
128
+ }
129
+
130
+ private async readLockFile(): Promise<LockData> {
131
+ const content = await readFile(this.lockFile, 'utf-8')
132
+ return JSON.parse(content)
133
+ }
134
+
135
+ private isProcessRunning(pid: number): boolean {
136
+ try {
137
+ // Signal 0 checks if process exists without killing it
138
+ process.kill(pid, 0)
139
+ return true
140
+ } catch {
141
+ return false
142
+ }
143
+ }
144
+
145
+ private calculateUptime(startTime: string): string {
146
+ const start = new Date(startTime)
147
+ const now = new Date()
148
+ const diff = now.getTime() - start.getTime()
149
+
150
+ const seconds = Math.floor(diff / 1000)
151
+ const minutes = Math.floor(seconds / 60)
152
+ const hours = Math.floor(minutes / 60)
153
+ const days = Math.floor(hours / 24)
154
+
155
+ if (days > 0) return `${days}d ${hours % 24}h`
156
+ if (hours > 0) return `${hours}h ${minutes % 60}m`
157
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`
158
+ return `${seconds}s`
159
+ }
160
+
161
+ private async cleanup(): Promise<void> {
162
+ if (existsSyncSync(this.pidFile)) {
163
+ await unlink(this.pidFile)
164
+ }
165
+ if (existsSyncSync(this.lockFile)) {
166
+ await unlink(this.lockFile)
167
+ }
168
+ }
169
+
170
+ private generateInstanceId(): string {
171
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
172
+ }
173
+ }
174
+
175
+ let instanceManagerInstance: InstanceManager | null = null
176
+
177
+ export function getInstanceManager(config?: BotConfig): InstanceManager {
178
+ if (!instanceManagerInstance) {
179
+ if (!config) {
180
+ throw new Error('InstanceManager requires config on first initialization')
181
+ }
182
+ instanceManagerInstance = new InstanceManager(config)
183
+ }
184
+ return instanceManagerInstance
185
+ }
186
+
187
+ export function resetInstanceManager(): void {
188
+ instanceManagerInstance = null
189
+ }
@@ -0,0 +1,33 @@
1
+ import { TimeConstants } from '../types/constants.js'
2
+
3
+ export function formatUptime(ms: number): string {
4
+ const days = Math.floor(ms / TimeConstants.DAY)
5
+ const hours = Math.floor((ms % TimeConstants.DAY) / TimeConstants.HOUR)
6
+ const minutes = Math.floor((ms % TimeConstants.HOUR) / TimeConstants.MINUTE)
7
+ const seconds = Math.floor((ms % TimeConstants.MINUTE) / TimeConstants.SECOND)
8
+
9
+ if (days > 0) return `${days}d ${hours}h ${minutes}m`
10
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`
11
+ if (minutes > 0) return `${minutes}m ${seconds}s`
12
+ return `${seconds}s`
13
+ }
14
+
15
+ export function formatDuration(ms: number): string {
16
+ return formatUptime(ms)
17
+ }
18
+
19
+ export function formatTimestamp(date: Date): string {
20
+ return date.toISOString()
21
+ }
22
+
23
+ export function getUptime(): number {
24
+ if (typeof process.uptime === 'function') {
25
+ return process.uptime() * 1_000
26
+ }
27
+ return Date.now()
28
+ }
29
+
30
+ export function getProcessUptime(): string {
31
+ const uptime = getUptime()
32
+ return formatUptime(uptime)
33
+ }
@@ -0,0 +1,26 @@
1
+ export {
2
+ ok,
3
+ err,
4
+ isOk,
5
+ isErr,
6
+ unwrap,
7
+ unwrapOr,
8
+ unwrapOrElse,
9
+ map,
10
+ mapErr,
11
+ flatMap,
12
+ tap,
13
+ tapErr,
14
+ match,
15
+ collect,
16
+ all,
17
+ fail,
18
+ botError,
19
+ resultError,
20
+ UNKNOWN_ERROR,
21
+ type Result,
22
+ type Ok,
23
+ type Err,
24
+ type BotError,
25
+ type BotErrorCode,
26
+ } from '../types/result.js'
@@ -0,0 +1,31 @@
1
+ export function formatBytes(bytes: number): string {
2
+ const units = ['B', 'KB', 'MB', 'GB', 'TB']
3
+ const threshold = 1024
4
+
5
+ for (const unit of units) {
6
+ if (bytes < threshold) {
7
+ return `${bytes} ${unit}`
8
+ }
9
+ bytes /= threshold
10
+ }
11
+
12
+ return `${bytes.toFixed(2)} PB`
13
+ }
14
+
15
+ export function formatMemory(memory: {
16
+ rss: number
17
+ heapTotal: number
18
+ heapUsed: number
19
+ external: number
20
+ arrayBuffers: number
21
+ }): string {
22
+ return ` RSS: ${formatBytes(memory.rss)}
23
+ Heap Total: ${formatBytes(memory.heapTotal)}
24
+ Heap Used: ${formatBytes(memory.heapUsed)}
25
+ External: ${formatBytes(memory.external)}
26
+ Array Buffers: ${formatBytes(memory.arrayBuffers)}`
27
+ }
28
+
29
+ export function getMemoryStats() {
30
+ return process.memoryUsage()
31
+ }
@@ -0,0 +1,71 @@
1
+ import type { Context } from 'telegraf'
2
+ import { ok, err, type Result, botError } from '../types/result.js'
3
+
4
+ export async function sendMessage(
5
+ ctx: Context,
6
+ chatId: string,
7
+ message: string,
8
+ options?: Record<string, unknown>
9
+ ): Promise<Result<void>> {
10
+ try {
11
+ await ctx.telegram.sendMessage(
12
+ chatId,
13
+ message,
14
+ options as Parameters<typeof ctx.telegram.sendMessage>[2]
15
+ )
16
+ return ok(undefined)
17
+ } catch (error) {
18
+ const errorObj = error instanceof Error ? error : new Error(String(error))
19
+ return err(botError('MESSAGE_FAILED', 'Failed to send message', errorObj))
20
+ }
21
+ }
22
+
23
+ export async function reply(
24
+ ctx: Context,
25
+ message: string,
26
+ options?: Record<string, unknown>
27
+ ): Promise<Result<void>> {
28
+ try {
29
+ await ctx.reply(message, options as Parameters<typeof ctx.reply>[1])
30
+ return ok(undefined)
31
+ } catch (error) {
32
+ const errorObj = error instanceof Error ? error : new Error(String(error))
33
+ return err(botError('MESSAGE_FAILED', 'Failed to reply', errorObj))
34
+ }
35
+ }
36
+
37
+ export async function editMessage(
38
+ ctx: Context,
39
+ chatId: string,
40
+ messageId: number,
41
+ message: string,
42
+ options?: Record<string, unknown>
43
+ ): Promise<Result<void>> {
44
+ try {
45
+ await ctx.telegram.editMessageText(
46
+ chatId,
47
+ messageId,
48
+ undefined,
49
+ message,
50
+ options as Parameters<typeof ctx.telegram.editMessageText>[4]
51
+ )
52
+ return ok(undefined)
53
+ } catch (error) {
54
+ const errorObj = error instanceof Error ? error : new Error(String(error))
55
+ return err(botError('MESSAGE_FAILED', 'Failed to edit message', errorObj))
56
+ }
57
+ }
58
+
59
+ export async function answerCallbackQuery(
60
+ ctx: Context,
61
+ text: string,
62
+ options?: Record<string, unknown>
63
+ ): Promise<Result<void>> {
64
+ try {
65
+ await ctx.answerCbQuery(text, options as Parameters<typeof ctx.answerCbQuery>[1])
66
+ return ok(undefined)
67
+ } catch (error) {
68
+ const errorObj = error instanceof Error ? error : new Error(String(error))
69
+ return err(botError('MESSAGE_FAILED', 'Failed to answer callback query', errorObj))
70
+ }
71
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "types": ["bun-types", "node"]
6
+ },
7
+ "include": ["src/**/*.ts"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }