create-bunspace 0.2.5 → 0.3.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/dist/templates/telegram-bot/CLAUDE.deploy.md +2 -3
- package/dist/templates/telegram-bot/CLAUDE.dev.md +304 -5
- package/dist/templates/telegram-bot/CLAUDE.md +166 -89
- package/dist/templates/telegram-bot/README.md +252 -129
- package/dist/templates/telegram-bot/bun.lock +146 -3
- package/dist/templates/telegram-bot/core/.env.example +6 -0
- package/dist/templates/telegram-bot/core/src/config/env.ts +130 -1
- package/dist/templates/telegram-bot/core/src/config/logging.ts +3 -1
- package/dist/templates/telegram-bot/core/src/handlers/config-export.ts +122 -0
- package/dist/templates/telegram-bot/core/src/handlers/control.ts +37 -11
- package/dist/templates/telegram-bot/core/src/handlers/health.ts +21 -26
- package/dist/templates/telegram-bot/core/src/handlers/info.ts +191 -0
- package/dist/templates/telegram-bot/core/src/handlers/listener.ts +168 -0
- package/dist/templates/telegram-bot/core/src/handlers/logs.ts +14 -7
- package/dist/templates/telegram-bot/core/src/index.ts +29 -0
- package/dist/templates/telegram-bot/core/src/utils/formatters.ts +55 -19
- package/dist/templates/telegram-bot/core/src/utils/instance-manager.ts +6 -2
- package/dist/templates/telegram-bot/core/src/utils/message-builder.ts +180 -0
- package/dist/templates/telegram-bot/docs/automatizacion_integral_de_bots_de_telegram_con_type_script.md +326 -0
- package/dist/templates/telegram-bot/docs/cli-commands.md +514 -5
- package/dist/templates/telegram-bot/docs/environment.md +191 -3
- package/dist/templates/telegram-bot/docs/getting-started.md +202 -15
- package/dist/templates/telegram-bot/package.json +5 -2
- package/dist/templates/telegram-bot/packages/utils/src/logger.ts +1 -0
- package/dist/templates/telegram-bot/tools/commands/doctor.ts +62 -0
- package/dist/templates/telegram-bot/tools/commands/setup.ts +984 -170
- package/package.json +1 -1
- package/templates/telegram-bot/CLAUDE.deploy.md +2 -3
- package/templates/telegram-bot/CLAUDE.dev.md +304 -5
- package/templates/telegram-bot/CLAUDE.md +166 -89
- package/templates/telegram-bot/README.md +252 -129
- package/templates/telegram-bot/bun.lock +146 -3
- package/templates/telegram-bot/core/.env.example +6 -0
- package/templates/telegram-bot/core/src/config/env.ts +130 -1
- package/templates/telegram-bot/core/src/config/logging.ts +3 -1
- package/templates/telegram-bot/core/src/handlers/config-export.ts +122 -0
- package/templates/telegram-bot/core/src/handlers/control.ts +37 -11
- package/templates/telegram-bot/core/src/handlers/health.ts +21 -26
- package/templates/telegram-bot/core/src/handlers/info.ts +191 -0
- package/templates/telegram-bot/core/src/handlers/listener.ts +168 -0
- package/templates/telegram-bot/core/src/handlers/logs.ts +14 -7
- package/templates/telegram-bot/core/src/index.ts +29 -0
- package/templates/telegram-bot/core/src/utils/formatters.ts +55 -19
- package/templates/telegram-bot/core/src/utils/instance-manager.ts +6 -2
- package/templates/telegram-bot/core/src/utils/message-builder.ts +180 -0
- package/templates/telegram-bot/docs/automatizacion_integral_de_bots_de_telegram_con_type_script.md +326 -0
- package/templates/telegram-bot/docs/cli-commands.md +514 -5
- package/templates/telegram-bot/docs/environment.md +191 -3
- package/templates/telegram-bot/docs/getting-started.md +202 -15
- package/templates/telegram-bot/package.json +5 -2
- package/templates/telegram-bot/packages/utils/src/logger.ts +1 -0
- package/templates/telegram-bot/tools/commands/doctor.ts +62 -0
- package/templates/telegram-bot/tools/commands/setup.ts +984 -170
|
@@ -22,17 +22,21 @@ interface SetupCommand extends BotCommand {
|
|
|
22
22
|
|
|
23
23
|
interface SetupOptions {
|
|
24
24
|
token?: string
|
|
25
|
-
|
|
25
|
+
botMode?: 'polling' | 'webhook'
|
|
26
26
|
environment?: 'local' | 'staging' | 'production'
|
|
27
|
+
setupMode?: SetupMode
|
|
28
|
+
update?: boolean
|
|
29
|
+
auto?: boolean
|
|
30
|
+
createTopics?: boolean
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
interface SetupConfig {
|
|
30
|
-
TG_BOT_TOKEN
|
|
31
|
-
TG_MODE
|
|
34
|
+
TG_BOT_TOKEN?: string
|
|
35
|
+
TG_MODE?: 'polling' | 'webhook'
|
|
32
36
|
TG_WEBHOOK_URL?: string
|
|
33
37
|
TG_WEBHOOK_SECRET?: string
|
|
34
|
-
TG_ENV
|
|
35
|
-
TG_INSTANCE_NAME
|
|
38
|
+
TG_ENV?: string
|
|
39
|
+
TG_INSTANCE_NAME?: string
|
|
36
40
|
TG_LOG_CHAT_ID?: string
|
|
37
41
|
TG_LOG_TOPIC_ID?: number
|
|
38
42
|
TG_CONTROL_CHAT_ID?: string
|
|
@@ -41,6 +45,16 @@ interface SetupConfig {
|
|
|
41
45
|
LOG_LEVEL?: string
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
type SetupMode = 'new-bot' | 'add-ids' | 'create-topics' | 'bootstrap' | 'manual'
|
|
49
|
+
|
|
50
|
+
type SetupContext = {
|
|
51
|
+
envExists: boolean
|
|
52
|
+
hasToken: boolean
|
|
53
|
+
tokenValid: boolean
|
|
54
|
+
hasChatId: boolean
|
|
55
|
+
hasTopics: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
const command: SetupCommand = {
|
|
45
59
|
name: 'setup',
|
|
46
60
|
description: 'Interactive environment setup',
|
|
@@ -48,10 +62,11 @@ const command: SetupCommand = {
|
|
|
48
62
|
register(program: Command) {
|
|
49
63
|
program
|
|
50
64
|
.command('setup')
|
|
51
|
-
.description('Configure environment variables
|
|
65
|
+
.description('Configure environment variables with intelligent flow')
|
|
52
66
|
.option('-t, --token <value>', 'Bot token from @BotFather')
|
|
53
|
-
.option('-m, --mode <polling|webhook>', 'Bot operation mode')
|
|
67
|
+
.option('-m, --bot-mode <polling|webhook>', 'Bot operation mode')
|
|
54
68
|
.option('-e, --environment <local|staging|production>', 'Target environment', 'local')
|
|
69
|
+
.option('--setup-mode <new-bot|add-ids|create-topics|bootstrap|manual>', 'Setup mode (skip prompt)')
|
|
55
70
|
.action(async (options) => {
|
|
56
71
|
await handleSetup(options)
|
|
57
72
|
})
|
|
@@ -60,129 +75,532 @@ const command: SetupCommand = {
|
|
|
60
75
|
|
|
61
76
|
export default command
|
|
62
77
|
|
|
63
|
-
async function handleSetup(options: SetupOptions): Promise<void> {
|
|
64
|
-
cliLogger.title('mks-telegram-bot Setup')
|
|
78
|
+
async function handleSetup(options: SetupOptions & { mode?: SetupMode }): Promise<void> {
|
|
79
|
+
cliLogger.title('🚀 mks-telegram-bot Setup')
|
|
65
80
|
|
|
66
81
|
const environment = options.environment ?? 'local'
|
|
67
82
|
const envFile = join(process.cwd(), 'core', `.env.${environment}`)
|
|
68
83
|
const envExample = join(process.cwd(), 'core', '.env.example')
|
|
69
84
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
// Pre-check: detect context
|
|
86
|
+
const context = await detectContext(envFile, envExample)
|
|
87
|
+
showContextSummary(context)
|
|
73
88
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
// Determine setup mode
|
|
90
|
+
let setupMode: SetupMode
|
|
91
|
+
if (options.setupMode) {
|
|
92
|
+
setupMode = options.setupMode
|
|
93
|
+
} else {
|
|
94
|
+
setupMode = await selectSetupMode(context)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
cliLogger.info(`\nMode: ${chalk.cyan(setupMode.toUpperCase())}`)
|
|
98
|
+
console.log('')
|
|
99
|
+
|
|
100
|
+
// Pre-checks before execution
|
|
101
|
+
const preChecks = await runPreChecks(envFile, environment, setupMode)
|
|
102
|
+
displayPreChecks(preChecks)
|
|
103
|
+
|
|
104
|
+
if (!preChecks.canProceed) {
|
|
105
|
+
cliLogger.error('Cannot proceed due to errors. Please fix the issues above and try again.')
|
|
106
|
+
return
|
|
107
|
+
}
|
|
78
108
|
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
// Create tracker for changes
|
|
110
|
+
const tracker = new SetupTracker()
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Execute based on mode
|
|
114
|
+
switch (setupMode) {
|
|
115
|
+
case 'new-bot':
|
|
116
|
+
await setupNewBot(options, envFile, envExample, environment, tracker)
|
|
117
|
+
break
|
|
118
|
+
case 'add-ids':
|
|
119
|
+
await setupAddIds(options, envFile, environment, tracker)
|
|
120
|
+
break
|
|
121
|
+
case 'create-topics':
|
|
122
|
+
await setupCreateTopics(options, envFile, environment, tracker)
|
|
123
|
+
break
|
|
124
|
+
case 'bootstrap':
|
|
125
|
+
await setupBootstrap(options, envFile, environment, tracker)
|
|
126
|
+
break
|
|
127
|
+
case 'manual':
|
|
128
|
+
await setupManual(options, envFile, envExample, environment, tracker)
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
tracker.show()
|
|
133
|
+
showFinalSummary(setupMode, environment)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof Error && error.message === 'CANCELLED') {
|
|
136
|
+
cliLogger.info('\nOperation cancelled')
|
|
81
137
|
return
|
|
82
138
|
}
|
|
139
|
+
throw error
|
|
83
140
|
}
|
|
141
|
+
}
|
|
84
142
|
|
|
85
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Detect current context (what exists, what's configured)
|
|
145
|
+
*/
|
|
146
|
+
async function detectContext(envFile: string, envExample: string): Promise<SetupContext> {
|
|
147
|
+
const envExists = existsSync(envFile)
|
|
148
|
+
let hasToken = false
|
|
149
|
+
let tokenValid = false
|
|
150
|
+
let hasChatId = false
|
|
151
|
+
let hasTopics = false
|
|
152
|
+
|
|
153
|
+
if (envExists) {
|
|
154
|
+
const envContent = await readFile(envFile, 'utf-8')
|
|
155
|
+
const tokenMatch = envContent.match(/^TG_BOT_TOKEN=(.+)$/m)
|
|
156
|
+
const chatIdMatch = envContent.match(/^TG_CONTROL_CHAT_ID=(.+)$/m)
|
|
157
|
+
const topicMatch = envContent.match(/^TG_CONTROL_TOPIC_ID=(.+)$/m)
|
|
158
|
+
|
|
159
|
+
hasToken = !!tokenMatch?.[1]?.trim()
|
|
160
|
+
hasChatId = !!chatIdMatch?.[1]?.trim()
|
|
161
|
+
hasTopics = !!topicMatch?.[1]?.trim()
|
|
162
|
+
|
|
163
|
+
if (hasToken) {
|
|
164
|
+
tokenValid = await validateTokenSilently(tokenMatch![1]!)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { envExists, hasToken, tokenValid, hasChatId, hasTopics }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Show context summary to user
|
|
173
|
+
*/
|
|
174
|
+
function showContextSummary(context: SetupContext): void {
|
|
175
|
+
console.log('')
|
|
176
|
+
console.log(chalk.bold('Current State:'))
|
|
177
|
+
if (!context.envExists) {
|
|
178
|
+
console.log(` ${chalk.yellow('●')} Environment file: ${chalk.dim('Not created')}`)
|
|
179
|
+
} else {
|
|
180
|
+
console.log(` ${chalk.green('●')} Environment file: ${chalk.dim('Exists')}`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!context.hasToken) {
|
|
184
|
+
console.log(` ${chalk.yellow('●')} Bot token: ${chalk.dim('Not configured')}`)
|
|
185
|
+
} else if (!context.tokenValid) {
|
|
186
|
+
console.log(` ${chalk.red('●')} Bot token: ${chalk.dim('Invalid')}`)
|
|
187
|
+
} else {
|
|
188
|
+
console.log(` ${chalk.green('●')} Bot token: ${chalk.dim('Valid')}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!context.hasChatId) {
|
|
192
|
+
console.log(` ${chalk.yellow('●')} Control Chat ID: ${chalk.dim('Not configured')}`)
|
|
193
|
+
} else {
|
|
194
|
+
console.log(` ${chalk.green('●')} Control Chat ID: ${chalk.dim('Configured')}`)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!context.hasTopics) {
|
|
198
|
+
console.log(` ${chalk.yellow('●')} Forum Topics: ${chalk.dim('Not created')}`)
|
|
199
|
+
} else {
|
|
200
|
+
console.log(` ${chalk.green('●')} Forum Topics: ${chalk.dim('Created')}`)
|
|
201
|
+
}
|
|
202
|
+
console.log('')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Select setup mode based on context
|
|
207
|
+
*/
|
|
208
|
+
async function selectSetupMode(context: SetupContext): Promise<SetupMode> {
|
|
209
|
+
const choices = [
|
|
210
|
+
{
|
|
211
|
+
name: '🚀 Setup new bot (recommended for first time)',
|
|
212
|
+
description: 'Setup token + detect IDs + create topics',
|
|
213
|
+
value: 'new-bot',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: '🔧 Add IDs to existing bot',
|
|
217
|
+
description: 'Auto-detect Chat/User/Topic IDs',
|
|
218
|
+
value: 'add-ids',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: '🧵 Create forum topics',
|
|
222
|
+
description: 'Create topics in existing group',
|
|
223
|
+
value: 'create-topics',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: '🤖 Full bootstrap (@BotFather)',
|
|
227
|
+
description: 'Create bot + group + topics from scratch',
|
|
228
|
+
value: 'bootstrap',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: '⚙️ Manual configuration',
|
|
232
|
+
description: 'Configure each variable manually',
|
|
233
|
+
value: 'manual',
|
|
234
|
+
},
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
return (await select({
|
|
238
|
+
message: 'What do you want to do?',
|
|
239
|
+
choices,
|
|
240
|
+
})) as SetupMode
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Mode 1: Setup new bot (recommended)
|
|
245
|
+
* - Setup basic config
|
|
246
|
+
* - Auto-configure to detect IDs
|
|
247
|
+
* - Create topics
|
|
248
|
+
*/
|
|
249
|
+
async function setupNewBot(
|
|
250
|
+
options: SetupOptions,
|
|
251
|
+
envFile: string,
|
|
252
|
+
envExample: string,
|
|
253
|
+
environment: 'local' | 'staging' | 'production',
|
|
254
|
+
tracker: SetupTracker
|
|
255
|
+
): Promise<void> {
|
|
256
|
+
cliLogger.title('📋 Step 1: Basic Configuration')
|
|
257
|
+
|
|
258
|
+
// Copy .env.example if needed
|
|
86
259
|
if (!existsSync(envFile) && existsSync(envExample)) {
|
|
87
260
|
await copyFile(envExample, envFile)
|
|
88
261
|
cliLogger.success(`Created ${envFile}`)
|
|
262
|
+
tracker.created('Environment file')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Gather basic config
|
|
266
|
+
const envContent = existsSync(envFile) ? await readFile(envFile, 'utf-8') : ''
|
|
267
|
+
const config = await gatherBasicConfig(options, envContent)
|
|
268
|
+
|
|
269
|
+
// Update env file
|
|
270
|
+
const updatedContent = updateEnvFile(envContent, config, false)
|
|
271
|
+
await writeFile(envFile, updatedContent, 'utf-8')
|
|
272
|
+
cliLogger.success(`Updated ${envFile}`)
|
|
273
|
+
tracker.updated('Bot configuration')
|
|
274
|
+
|
|
275
|
+
// Validate token
|
|
276
|
+
if (config.TG_BOT_TOKEN) {
|
|
277
|
+
await validateToken(config.TG_BOT_TOKEN)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Step 2: Auto-configure
|
|
281
|
+
cliLogger.title('🔧 Step 2: Auto-Detect IDs')
|
|
282
|
+
const runAuto = await confirm({
|
|
283
|
+
message: 'Auto-detect Chat IDs, User IDs, and Topic IDs?',
|
|
284
|
+
default: true,
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
if (runAuto && config.TG_BOT_TOKEN) {
|
|
288
|
+
const { handleAutoConfigure } = await import('./auto-configure.js')
|
|
289
|
+
await handleAutoConfigure({
|
|
290
|
+
timeout: 60,
|
|
291
|
+
environment,
|
|
292
|
+
})
|
|
293
|
+
tracker.updated('Chat and User IDs')
|
|
294
|
+
} else {
|
|
295
|
+
cliLogger.info('Skipping auto-configure')
|
|
296
|
+
tracker.skipped('Auto-configure')
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Step 3: Create topics
|
|
300
|
+
cliLogger.title('🧵 Step 3: Create Forum Topics')
|
|
301
|
+
const runTopics = await confirm({
|
|
302
|
+
message: 'Create forum topics (General, Control, Logs, Config, Bugs)?',
|
|
303
|
+
default: true,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
if (runTopics && config.TG_BOT_TOKEN) {
|
|
307
|
+
const { handleCreateTopics } = await import('./create-topics.js')
|
|
308
|
+
await handleCreateTopics({ environment })
|
|
309
|
+
tracker.created('Forum topics')
|
|
310
|
+
} else {
|
|
311
|
+
cliLogger.info('Skipping topic creation')
|
|
312
|
+
tracker.skipped('Topic creation')
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Mode 2: Add IDs to existing bot
|
|
318
|
+
* - Only runs auto-configure
|
|
319
|
+
*/
|
|
320
|
+
async function setupAddIds(
|
|
321
|
+
options: SetupOptions,
|
|
322
|
+
envFile: string,
|
|
323
|
+
environment: 'local' | 'staging' | 'production',
|
|
324
|
+
tracker: SetupTracker
|
|
325
|
+
): Promise<void> {
|
|
326
|
+
// Check if bot token exists
|
|
327
|
+
if (!existsSync(envFile)) {
|
|
328
|
+
cliLogger.error(`Environment file not found: ${envFile}`)
|
|
329
|
+
cliLogger.info('Please run setup first to configure the bot token.')
|
|
330
|
+
throw new Error('CANCELLED')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const envContent = await readFile(envFile, 'utf-8')
|
|
334
|
+
const tokenMatch = envContent.match(/^TG_BOT_TOKEN=(.+)$/m)
|
|
335
|
+
const botToken = tokenMatch?.[1]?.trim()
|
|
336
|
+
|
|
337
|
+
if (!botToken) {
|
|
338
|
+
cliLogger.error('TG_BOT_TOKEN not found in environment file')
|
|
339
|
+
throw new Error('CANCELLED')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
cliLogger.info(`Bot token found: ${botToken.slice(0, 10)}...`)
|
|
343
|
+
|
|
344
|
+
// Validate token
|
|
345
|
+
const valid = await validateTokenSilently(botToken)
|
|
346
|
+
if (!valid) {
|
|
347
|
+
cliLogger.error('Bot token is invalid. Please run setup to configure a valid token.')
|
|
348
|
+
throw new Error('CANCELLED')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
cliLogger.success('Bot token is valid')
|
|
352
|
+
|
|
353
|
+
// Run auto-configure
|
|
354
|
+
cliLogger.info('\n💡 Send messages to your bot from different contexts:')
|
|
355
|
+
cliLogger.info(' - Direct message (DM)')
|
|
356
|
+
cliLogger.info(' - Mention in a group')
|
|
357
|
+
cliLogger.info(' - Reply to a message in a topic')
|
|
358
|
+
cliLogger.info(' Press Ctrl+C when done\n')
|
|
359
|
+
|
|
360
|
+
const { handleAutoConfigure } = await import('./auto-configure.js')
|
|
361
|
+
await handleAutoConfigure({
|
|
362
|
+
timeout: 60,
|
|
363
|
+
environment,
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
tracker.updated('Chat and User IDs')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Mode 3: Create topics
|
|
371
|
+
* - Only runs create-topics
|
|
372
|
+
*/
|
|
373
|
+
async function setupCreateTopics(
|
|
374
|
+
options: SetupOptions,
|
|
375
|
+
envFile: string,
|
|
376
|
+
environment: 'local' | 'staging' | 'production',
|
|
377
|
+
tracker: SetupTracker
|
|
378
|
+
): Promise<void> {
|
|
379
|
+
// Check if bot token exists
|
|
380
|
+
if (!existsSync(envFile)) {
|
|
381
|
+
cliLogger.error(`Environment file not found: ${envFile}`)
|
|
382
|
+
throw new Error('CANCELLED')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const envContent = await readFile(envFile, 'utf-8')
|
|
386
|
+
const tokenMatch = envContent.match(/^TG_BOT_TOKEN=(.+)$/m)
|
|
387
|
+
const chatIdMatch = envContent.match(/^TG_CONTROL_CHAT_ID=(.+)$/m)
|
|
388
|
+
|
|
389
|
+
const botToken = tokenMatch?.[1]?.trim()
|
|
390
|
+
const chatId = chatIdMatch?.[1]?.trim()
|
|
391
|
+
|
|
392
|
+
if (!botToken) {
|
|
393
|
+
cliLogger.error('TG_BOT_TOKEN not found in environment file')
|
|
394
|
+
throw new Error('CANCELLED')
|
|
89
395
|
}
|
|
90
396
|
|
|
91
|
-
|
|
92
|
-
|
|
397
|
+
if (!chatId) {
|
|
398
|
+
cliLogger.warn('TG_CONTROL_CHAT_ID not found')
|
|
399
|
+
|
|
400
|
+
const manualChatId = await input({
|
|
401
|
+
message: 'Enter the Chat ID of the group:',
|
|
402
|
+
validate: validateChatId,
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
// Update env file with chat ID
|
|
406
|
+
const updated = updateEnvVar(envContent, 'TG_CONTROL_CHAT_ID', manualChatId)
|
|
407
|
+
await writeFile(envFile, updated, 'utf-8')
|
|
408
|
+
cliLogger.success(`Updated ${envFile}`)
|
|
409
|
+
tracker.updated('Control chat ID')
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Run create-topics
|
|
413
|
+
cliLogger.info('\n💡 Creating 5 topics for organization:')
|
|
414
|
+
cliLogger.info(' - General: General bot messages')
|
|
415
|
+
cliLogger.info(' - Control: Control command responses')
|
|
416
|
+
cliLogger.info(' - Logs: Error and log messages')
|
|
417
|
+
cliLogger.info(' - Config: Configuration updates')
|
|
418
|
+
cliLogger.info(' - Bugs: Bug reports and issues\n')
|
|
419
|
+
|
|
420
|
+
const { handleCreateTopics } = await import('./create-topics.js')
|
|
421
|
+
await handleCreateTopics({ environment })
|
|
422
|
+
|
|
423
|
+
tracker.created('Forum topics')
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Mode 4: Full bootstrap
|
|
428
|
+
* - Runs the bootstrap command
|
|
429
|
+
*/
|
|
430
|
+
async function setupBootstrap(
|
|
431
|
+
options: SetupOptions,
|
|
432
|
+
envFile: string,
|
|
433
|
+
environment: 'local' | 'staging' | 'production',
|
|
434
|
+
tracker: SetupTracker
|
|
435
|
+
): Promise<void> {
|
|
436
|
+
cliLogger.title('🤖 Full Bootstrap with @BotFather')
|
|
437
|
+
cliLogger.info('This will create a bot, group, and topics automatically.')
|
|
438
|
+
cliLogger.info('You will need your Telegram API credentials from https://my.telegram.org')
|
|
439
|
+
console.log('')
|
|
440
|
+
|
|
441
|
+
cliLogger.info('\n💡 Make sure you have:')
|
|
442
|
+
cliLogger.info(' 1. Telegram API credentials (api_id and api_hash)')
|
|
443
|
+
cliLogger.info(' 2. Phone number connected to your Telegram account')
|
|
444
|
+
cliLogger.info(' 3. A session file or willingness to login\n')
|
|
445
|
+
|
|
446
|
+
const proceed = await confirm({
|
|
447
|
+
message: 'Continue with full bootstrap?',
|
|
448
|
+
default: true,
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
if (!proceed) {
|
|
452
|
+
throw new Error('CANCELLED')
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Import and run bootstrap
|
|
456
|
+
cliLogger.info('\nLaunching bootstrap command...\n')
|
|
457
|
+
cliLogger.warn('Full bootstrap is handled by the "bun run bootstrap" command')
|
|
458
|
+
cliLogger.info('Please run: bun run bootstrap')
|
|
459
|
+
cliLogger.info('Or use: bun run cli bootstrap\n')
|
|
460
|
+
|
|
461
|
+
tracker.created('Bootstrap setup (manual)')
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Mode 5: Manual configuration
|
|
466
|
+
* - Original detailed setup
|
|
467
|
+
*/
|
|
468
|
+
async function setupManual(
|
|
469
|
+
options: SetupOptions,
|
|
470
|
+
envFile: string,
|
|
471
|
+
envExample: string,
|
|
472
|
+
environment: 'local' | 'staging' | 'production',
|
|
473
|
+
tracker: SetupTracker
|
|
474
|
+
): Promise<void> {
|
|
475
|
+
cliLogger.title('⚙️ Manual Configuration')
|
|
476
|
+
|
|
477
|
+
// Check if file exists
|
|
93
478
|
if (existsSync(envFile)) {
|
|
94
|
-
|
|
479
|
+
cliLogger.warn(`\nEnvironment file already exists: ${envFile}`)
|
|
480
|
+
|
|
481
|
+
const action = await select({
|
|
482
|
+
message: 'What would you like to do?',
|
|
483
|
+
choices: [
|
|
484
|
+
{ name: 'Update specific values', value: 'update' },
|
|
485
|
+
{ name: 'Overwrite everything', value: 'overwrite' },
|
|
486
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
487
|
+
],
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
if (action === 'cancel') {
|
|
491
|
+
throw new Error('CANCELLED')
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (action === 'update') {
|
|
495
|
+
options.update = true
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Copy .env.example if needed
|
|
500
|
+
if (!existsSync(envFile) && existsSync(envExample)) {
|
|
501
|
+
await copyFile(envExample, envFile)
|
|
502
|
+
cliLogger.success(`Created ${envFile}`)
|
|
503
|
+
tracker.created('Environment file')
|
|
95
504
|
}
|
|
96
505
|
|
|
97
|
-
|
|
98
|
-
|
|
506
|
+
const envContent = existsSync(envFile) ? await readFile(envFile, 'utf-8') : ''
|
|
507
|
+
|
|
508
|
+
// Gather all config
|
|
509
|
+
const config = await gatherManualConfig(options, envContent, environment)
|
|
99
510
|
|
|
100
511
|
// Update env file
|
|
101
|
-
const updatedContent = updateEnvFile(envContent, config)
|
|
512
|
+
const updatedContent = updateEnvFile(envContent, config, options.update)
|
|
102
513
|
await writeFile(envFile, updatedContent, 'utf-8')
|
|
103
514
|
|
|
104
|
-
cliLogger.success(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
515
|
+
cliLogger.success(`\nEnvironment configured: ${envFile}`)
|
|
516
|
+
|
|
517
|
+
if (options.update) {
|
|
518
|
+
tracker.updated('Configuration')
|
|
519
|
+
} else {
|
|
520
|
+
tracker.created('Configuration')
|
|
521
|
+
}
|
|
109
522
|
|
|
110
|
-
// Validate token
|
|
523
|
+
// Validate token
|
|
111
524
|
if (config.TG_BOT_TOKEN) {
|
|
112
525
|
await validateToken(config.TG_BOT_TOKEN)
|
|
113
526
|
}
|
|
114
527
|
}
|
|
115
528
|
|
|
116
|
-
|
|
529
|
+
/**
|
|
530
|
+
* Gather basic config (token, mode, env)
|
|
531
|
+
*/
|
|
532
|
+
async function gatherBasicConfig(options: SetupOptions, envContent: string): Promise<SetupConfig> {
|
|
117
533
|
const config: Partial<SetupConfig> = {}
|
|
118
534
|
|
|
119
535
|
// Bot token
|
|
120
536
|
if (options.token) {
|
|
121
537
|
config.TG_BOT_TOKEN = options.token
|
|
122
538
|
} else {
|
|
123
|
-
cliLogger.info('\
|
|
124
|
-
cliLogger.info('
|
|
125
|
-
cliLogger.info('
|
|
126
|
-
cliLogger.info('
|
|
127
|
-
cliLogger.info('
|
|
539
|
+
cliLogger.info('\n💡 To get a bot token:')
|
|
540
|
+
cliLogger.info(' 1. Open Telegram and talk to @BotFather')
|
|
541
|
+
cliLogger.info(' 2. Send /newbot')
|
|
542
|
+
cliLogger.info(' 3. Choose a name and username')
|
|
543
|
+
cliLogger.info(' 4. Copy the token (format: 123456:ABC-DEF1234...)\n')
|
|
128
544
|
|
|
129
545
|
config.TG_BOT_TOKEN = await input({
|
|
130
546
|
message: 'Enter your bot token:',
|
|
131
|
-
validate:
|
|
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
|
-
},
|
|
547
|
+
validate: validateBotToken,
|
|
140
548
|
})
|
|
141
549
|
}
|
|
142
550
|
|
|
143
551
|
// Mode
|
|
144
|
-
if (options.
|
|
145
|
-
config.TG_MODE = options.
|
|
552
|
+
if (options.botMode) {
|
|
553
|
+
config.TG_MODE = options.botMode
|
|
146
554
|
} else {
|
|
147
|
-
|
|
555
|
+
config.TG_MODE = await select({
|
|
148
556
|
message: 'Select bot operation mode:',
|
|
149
557
|
choices: [
|
|
150
|
-
{
|
|
151
|
-
|
|
558
|
+
{
|
|
559
|
+
name: 'Polling (recommended for development)',
|
|
560
|
+
value: 'polling',
|
|
561
|
+
description: 'Bot polls Telegram for updates (simpler setup)',
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: 'Webhook (recommended for production)',
|
|
565
|
+
value: 'webhook',
|
|
566
|
+
description: 'Telegram sends updates to your server (faster, needs HTTPS)',
|
|
567
|
+
},
|
|
152
568
|
],
|
|
153
|
-
})
|
|
154
|
-
config.TG_MODE = modeSelection as 'polling' | 'webhook'
|
|
569
|
+
}) as 'polling' | 'webhook'
|
|
155
570
|
}
|
|
156
571
|
|
|
157
|
-
// Webhook
|
|
572
|
+
// Webhook config if needed
|
|
158
573
|
if (config.TG_MODE === 'webhook') {
|
|
159
|
-
cliLogger.warn('\
|
|
574
|
+
cliLogger.warn('\n⚠️ Webhook mode requires a public HTTPS endpoint')
|
|
575
|
+
cliLogger.info(' Use ngrok for local testing: bun run ngrok\n')
|
|
160
576
|
|
|
161
577
|
config.TG_WEBHOOK_URL = await input({
|
|
162
|
-
message: 'Enter webhook URL
|
|
578
|
+
message: 'Enter webhook URL:',
|
|
579
|
+
default: 'https://your-domain.com/webhook',
|
|
163
580
|
validate: (value: string) => {
|
|
164
581
|
if (!value || value.trim().length === 0) {
|
|
165
|
-
return 'Webhook URL is required
|
|
582
|
+
return 'Webhook URL is required'
|
|
166
583
|
}
|
|
167
584
|
if (!value.startsWith('https://')) {
|
|
168
|
-
return '
|
|
585
|
+
return 'Must use HTTPS'
|
|
169
586
|
}
|
|
170
587
|
return true
|
|
171
588
|
},
|
|
172
589
|
})
|
|
173
590
|
|
|
174
591
|
config.TG_WEBHOOK_SECRET = await input({
|
|
175
|
-
message: 'Enter webhook secret
|
|
592
|
+
message: 'Enter webhook secret:',
|
|
593
|
+
default: 'change-this-secret-in-production',
|
|
176
594
|
validate: (value: string) => {
|
|
177
595
|
if (!value || value.length < 16) {
|
|
178
|
-
return '
|
|
596
|
+
return 'Must be at least 16 characters'
|
|
179
597
|
}
|
|
180
598
|
return true
|
|
181
599
|
},
|
|
182
600
|
})
|
|
183
601
|
}
|
|
184
602
|
|
|
185
|
-
// Environment
|
|
603
|
+
// Environment
|
|
186
604
|
const envSelection = await select({
|
|
187
605
|
message: 'Select environment:',
|
|
188
606
|
choices: [
|
|
@@ -206,109 +624,182 @@ async function gatherConfig(options: SetupOptions, _envContent: string): Promise
|
|
|
206
624
|
},
|
|
207
625
|
})
|
|
208
626
|
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
message: '
|
|
212
|
-
|
|
627
|
+
// Log level
|
|
628
|
+
config.LOG_LEVEL = await select({
|
|
629
|
+
message: 'Select log level:',
|
|
630
|
+
choices: [
|
|
631
|
+
{ name: 'Debug (verbose)', value: 'debug' },
|
|
632
|
+
{ name: 'Info (default)', value: 'info' },
|
|
633
|
+
{ name: 'Warnings only', value: 'warn' },
|
|
634
|
+
{ name: 'Errors only', value: 'error' },
|
|
635
|
+
],
|
|
636
|
+
default: 'info',
|
|
213
637
|
})
|
|
214
638
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
639
|
+
return config as SetupConfig
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Gather full manual config
|
|
644
|
+
*/
|
|
645
|
+
async function gatherManualConfig(options: SetupOptions, envContent: string, environment: string): Promise<SetupConfig> {
|
|
646
|
+
const config = await gatherBasicConfig(options, envContent)
|
|
647
|
+
|
|
648
|
+
// Update mode: ask what to update
|
|
649
|
+
let updateWhat: Set<string> | null = null
|
|
650
|
+
if (options.update) {
|
|
651
|
+
cliLogger.info('\n📝 Update mode: select what to update')
|
|
221
652
|
|
|
222
|
-
|
|
223
|
-
message: '
|
|
653
|
+
const choices = await select({
|
|
654
|
+
message: 'What do you want to configure?',
|
|
655
|
+
choices: [
|
|
656
|
+
{ name: 'Control commands only', value: 'control' },
|
|
657
|
+
{ name: 'Everything', value: 'all' },
|
|
658
|
+
{ name: 'Skip', value: 'skip' },
|
|
659
|
+
],
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
if (choices === 'skip') {
|
|
663
|
+
throw new Error('CANCELLED')
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
updateWhat = choices === 'control'
|
|
667
|
+
? new Set(['TG_CONTROL_CHAT_ID', 'TG_AUTHORIZED_USER_IDS'])
|
|
668
|
+
: new Set(['TG_BOT_TOKEN', 'TG_MODE', 'TG_WEBHOOK_URL', 'TG_CONTROL_CHAT_ID',
|
|
669
|
+
'TG_AUTHORIZED_USER_IDS', 'TG_LOG_CHAT_ID', 'TG_LOG_TOPIC_ID'])
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const shouldPrompt = (field: string): boolean => {
|
|
673
|
+
if (!options.update || !updateWhat) return true
|
|
674
|
+
return updateWhat.has(field)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const getExisting = (key: string): string | undefined => {
|
|
678
|
+
const match = envContent.match(new RegExp(`^${key}=(.+)$`, 'm'))
|
|
679
|
+
return match?.[1]?.trim()
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Log streaming
|
|
683
|
+
if (shouldPrompt('TG_LOG_CHAT_ID')) {
|
|
684
|
+
const enableLogStreaming = await confirm({
|
|
685
|
+
message: 'Enable log streaming to Telegram?',
|
|
686
|
+
default: !!getExisting('TG_LOG_CHAT_ID'),
|
|
224
687
|
})
|
|
225
688
|
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
689
|
+
if (enableLogStreaming) {
|
|
690
|
+
cliLogger.info('\n💡 To get chat IDs:')
|
|
691
|
+
cliLogger.info(' 1. Add bot to a group')
|
|
692
|
+
cliLogger.info(' 2. Send a message')
|
|
693
|
+
cliLogger.info(' 3. Use auto-configure to detect the ID\n')
|
|
694
|
+
|
|
695
|
+
config.TG_LOG_CHAT_ID = await input({
|
|
696
|
+
message: 'Enter log chat ID:',
|
|
697
|
+
default: getExisting('TG_LOG_CHAT_ID'),
|
|
698
|
+
validate: validateChatId,
|
|
230
699
|
})
|
|
231
700
|
|
|
232
|
-
if (
|
|
233
|
-
const
|
|
234
|
-
message: '
|
|
235
|
-
|
|
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
|
-
},
|
|
701
|
+
if (config.TG_LOG_CHAT_ID && config.TG_LOG_CHAT_ID.trim().length > 0) {
|
|
702
|
+
const useTopic = await confirm({
|
|
703
|
+
message: 'Use a topic for logs?',
|
|
704
|
+
default: !!getExisting('TG_LOG_TOPIC_ID'),
|
|
242
705
|
})
|
|
243
|
-
|
|
706
|
+
|
|
707
|
+
if (useTopic) {
|
|
708
|
+
const topicId = await input({
|
|
709
|
+
message: 'Enter topic ID:',
|
|
710
|
+
default: getExisting('TG_LOG_TOPIC_ID'),
|
|
711
|
+
validate: validateTopicId,
|
|
712
|
+
})
|
|
713
|
+
config.TG_LOG_TOPIC_ID = parseInt(topicId, 10)
|
|
714
|
+
}
|
|
244
715
|
}
|
|
245
716
|
}
|
|
246
717
|
}
|
|
247
718
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (enableControl) {
|
|
255
|
-
config.TG_CONTROL_CHAT_ID = await input({
|
|
256
|
-
message: 'Enter control chat ID:',
|
|
719
|
+
// Control commands
|
|
720
|
+
if (shouldPrompt('TG_CONTROL_CHAT_ID')) {
|
|
721
|
+
const existingControl = getExisting('TG_CONTROL_CHAT_ID')
|
|
722
|
+
const enableControl = await confirm({
|
|
723
|
+
message: 'Enable control commands (/stop, /restart, etc.)?',
|
|
724
|
+
default: !!existingControl,
|
|
257
725
|
})
|
|
258
726
|
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
message: '
|
|
262
|
-
default:
|
|
727
|
+
if (enableControl) {
|
|
728
|
+
config.TG_CONTROL_CHAT_ID = await input({
|
|
729
|
+
message: 'Enter control chat ID:',
|
|
730
|
+
default: existingControl,
|
|
731
|
+
validate: validateChatId,
|
|
263
732
|
})
|
|
264
733
|
|
|
265
|
-
if (
|
|
266
|
-
const
|
|
267
|
-
message: '
|
|
268
|
-
|
|
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
|
-
},
|
|
734
|
+
if (config.TG_CONTROL_CHAT_ID && config.TG_CONTROL_CHAT_ID.trim().length > 0) {
|
|
735
|
+
const useTopic = await confirm({
|
|
736
|
+
message: 'Use a topic for control messages?',
|
|
737
|
+
default: !!getExisting('TG_CONTROL_TOPIC_ID'),
|
|
275
738
|
})
|
|
276
|
-
|
|
739
|
+
|
|
740
|
+
if (useTopic) {
|
|
741
|
+
const topicId = await input({
|
|
742
|
+
message: 'Enter topic ID:',
|
|
743
|
+
default: getExisting('TG_CONTROL_TOPIC_ID'),
|
|
744
|
+
validate: validateTopicId,
|
|
745
|
+
})
|
|
746
|
+
config.TG_CONTROL_TOPIC_ID = parseInt(topicId, 10)
|
|
747
|
+
}
|
|
277
748
|
}
|
|
749
|
+
|
|
750
|
+
// Authorized users
|
|
751
|
+
config.TG_AUTHORIZED_USER_IDS = await input({
|
|
752
|
+
message: 'Enter authorized user IDs (comma-separated):',
|
|
753
|
+
default: getExisting('TG_AUTHORIZED_USER_IDS'),
|
|
754
|
+
validate: validateUserIds,
|
|
755
|
+
})
|
|
278
756
|
}
|
|
757
|
+
}
|
|
279
758
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
message: 'Enter authorized user IDs (comma-separated):',
|
|
283
|
-
})
|
|
759
|
+
return config as SetupConfig
|
|
760
|
+
}
|
|
284
761
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
762
|
+
/**
|
|
763
|
+
* Validate token silently (no output)
|
|
764
|
+
*/
|
|
765
|
+
async function validateTokenSilently(token: string): Promise<boolean> {
|
|
766
|
+
try {
|
|
767
|
+
const response = await fetch(`https://api.telegram.org/bot${token}/getMe`)
|
|
768
|
+
const data = await response.json() as { ok: boolean }
|
|
769
|
+
return data.ok
|
|
770
|
+
} catch {
|
|
771
|
+
return false
|
|
288
772
|
}
|
|
773
|
+
}
|
|
289
774
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
775
|
+
/**
|
|
776
|
+
* Validate token and show result
|
|
777
|
+
*/
|
|
778
|
+
async function validateToken(token: string): Promise<void> {
|
|
779
|
+
cliLogger.info('\nValidating bot token...')
|
|
302
780
|
|
|
303
|
-
|
|
781
|
+
try {
|
|
782
|
+
const response = await fetch(`https://api.telegram.org/bot${token}/getMe`)
|
|
783
|
+
const data = (await response.json()) as { ok: boolean; result?: { username: string; first_name: string } }
|
|
784
|
+
|
|
785
|
+
if (data.ok && data.result) {
|
|
786
|
+
cliLogger.success(`Bot connected: @${data.result.username} (${data.result.first_name})`)
|
|
787
|
+
} else {
|
|
788
|
+
cliLogger.warn('Could not validate token')
|
|
789
|
+
}
|
|
790
|
+
} catch {
|
|
791
|
+
cliLogger.warn('Could not reach Telegram API')
|
|
792
|
+
}
|
|
304
793
|
}
|
|
305
794
|
|
|
306
|
-
|
|
795
|
+
/**
|
|
796
|
+
* Update env file with config
|
|
797
|
+
*/
|
|
798
|
+
function updateEnvFile(content: string, config: SetupConfig, updateMode = false): string {
|
|
307
799
|
const lines = content.split('\n')
|
|
308
800
|
const updated: string[] = []
|
|
309
801
|
|
|
310
802
|
for (const line of lines) {
|
|
311
|
-
// Skip empty lines and comments
|
|
312
803
|
if (line.trim().length === 0 || line.trim().startsWith('#')) {
|
|
313
804
|
updated.push(line)
|
|
314
805
|
continue
|
|
@@ -316,31 +807,16 @@ function updateEnvFile(content: string, config: SetupConfig): string {
|
|
|
316
807
|
|
|
317
808
|
const [key] = line.split('=')
|
|
318
809
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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}`)
|
|
810
|
+
if (!key) {
|
|
811
|
+
updated.push(line)
|
|
812
|
+
continue
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const value = getConfigValue(config, key)
|
|
816
|
+
if (value !== undefined) {
|
|
817
|
+
updated.push(`${key}=${value}`)
|
|
818
|
+
} else if (!updateMode) {
|
|
819
|
+
updated.push(line)
|
|
344
820
|
} else {
|
|
345
821
|
updated.push(line)
|
|
346
822
|
}
|
|
@@ -349,20 +825,358 @@ function updateEnvFile(content: string, config: SetupConfig): string {
|
|
|
349
825
|
return updated.join('\n')
|
|
350
826
|
}
|
|
351
827
|
|
|
352
|
-
|
|
353
|
-
|
|
828
|
+
function getConfigValue(config: SetupConfig, key: string): string | undefined {
|
|
829
|
+
const value = config[key as keyof SetupConfig]
|
|
354
830
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
831
|
+
if (value === undefined || value === null) {
|
|
832
|
+
return undefined
|
|
833
|
+
}
|
|
358
834
|
|
|
359
|
-
|
|
360
|
-
|
|
835
|
+
return String(value)
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Update a single env variable
|
|
840
|
+
*/
|
|
841
|
+
function updateEnvVar(content: string, key: string, value: string): string {
|
|
842
|
+
const exists = new RegExp(`^${key}=(.+)$`, 'm').test(content)
|
|
843
|
+
|
|
844
|
+
if (exists) {
|
|
845
|
+
return content.replace(new RegExp(`^${key}=(.+)$`, 'm'), `${key}=${value}`)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const commentedMatch = content.match(new RegExp(`^#\\s*${key}=(.+)$`, 'm'))
|
|
849
|
+
if (commentedMatch) {
|
|
850
|
+
return content.replace(new RegExp(`^#\\s*${key}=(.+)$`, 'm'), `${key}=${value}`)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return content.trimEnd() + `\n${key}=${value}\n`
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ============================================================================
|
|
857
|
+
// FASE 4: Validators
|
|
858
|
+
// ============================================================================
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Validate bot token format
|
|
862
|
+
*/
|
|
863
|
+
function validateBotToken(token: string): true | string {
|
|
864
|
+
if (!token || token.trim().length === 0) {
|
|
865
|
+
return 'Token is required'
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (!token.includes(':')) {
|
|
869
|
+
return 'Invalid format. Token must be: ID:HASH (e.g., 123456:ABC-DEF1234...)'
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const parts = token.split(':')
|
|
873
|
+
if (parts.length !== 2) {
|
|
874
|
+
return 'Invalid format. Token must be: ID:HASH (e.g., 123456:ABC-DEF1234...)'
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const [id, hash] = parts
|
|
878
|
+
|
|
879
|
+
if (!id) {
|
|
880
|
+
return 'Token ID missing. Make sure you copied the complete token'
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (!hash) {
|
|
884
|
+
return 'Token hash missing. Make sure you copied the complete token'
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const idNum = parseInt(id, 10)
|
|
888
|
+
if (isNaN(idNum) || idNum < 100000) {
|
|
889
|
+
return 'Invalid token ID. Must be a number >= 100000'
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (hash.length < 35) {
|
|
893
|
+
return 'Token hash too short. Make sure you copied the complete token'
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return true
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Validate chat ID (must be negative for supergroups)
|
|
901
|
+
*/
|
|
902
|
+
function validateChatId(chatId: string): true | string {
|
|
903
|
+
if (!chatId || chatId.trim().length === 0) {
|
|
904
|
+
return 'Chat ID is required'
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const id = parseInt(chatId, 10)
|
|
908
|
+
if (isNaN(id)) {
|
|
909
|
+
return 'Chat ID must be a number'
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (id > 0) {
|
|
913
|
+
return 'Chat IDs for groups are negative (e.g., -1001234567890)'
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (!chatId.startsWith('-100')) {
|
|
917
|
+
return 'Supergroup chat IDs start with -100 (e.g., -1001234567890)'
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return true
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Validate topic ID (must be positive)
|
|
925
|
+
*/
|
|
926
|
+
function validateTopicId(topicId: string): true | string {
|
|
927
|
+
if (!topicId || topicId.trim().length === 0) {
|
|
928
|
+
return 'Topic ID is required'
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const id = parseInt(topicId, 10)
|
|
932
|
+
if (isNaN(id)) {
|
|
933
|
+
return 'Topic ID must be a number'
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (id <= 0) {
|
|
937
|
+
return 'Topic IDs must be positive'
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return true
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Validate user IDs (comma-separated)
|
|
945
|
+
*/
|
|
946
|
+
function validateUserIds(userIds: string): true | string {
|
|
947
|
+
if (!userIds || userIds.trim().length === 0) {
|
|
948
|
+
return 'At least one user ID is required'
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const ids = userIds.split(',').map(id => id.trim())
|
|
952
|
+
for (const id of ids) {
|
|
953
|
+
const num = parseInt(id, 10)
|
|
954
|
+
if (isNaN(num)) {
|
|
955
|
+
return `Invalid user ID: "${id}". Must be a number`
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return true
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// ============================================================================
|
|
963
|
+
// FASE 4: Pre-checks
|
|
964
|
+
// ============================================================================
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Pre-check results
|
|
968
|
+
*/
|
|
969
|
+
interface PreCheckResult {
|
|
970
|
+
canProceed: boolean
|
|
971
|
+
errors: string[]
|
|
972
|
+
warnings: string[]
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Run pre-checks before operations
|
|
977
|
+
*/
|
|
978
|
+
async function runPreChecks(
|
|
979
|
+
envFile: string,
|
|
980
|
+
environment: 'local' | 'staging' | 'production',
|
|
981
|
+
mode: SetupMode
|
|
982
|
+
): Promise<PreCheckResult> {
|
|
983
|
+
const result: PreCheckResult = {
|
|
984
|
+
canProceed: true,
|
|
985
|
+
errors: [],
|
|
986
|
+
warnings: [],
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Read env file
|
|
990
|
+
const envContent = existsSync(envFile) ? await readFile(envFile, 'utf-8') : ''
|
|
991
|
+
|
|
992
|
+
// Extract values
|
|
993
|
+
const tokenMatch = envContent.match(/^TG_BOT_TOKEN=(.+)$/m)
|
|
994
|
+
const chatIdMatch = envContent.match(/^TG_CONTROL_CHAT_ID=(.+)$/m)
|
|
995
|
+
const topicMatch = envContent.match(/^TG_CONTROL_TOPIC_ID=(.+)$/m)
|
|
996
|
+
|
|
997
|
+
const token = tokenMatch?.[1]?.trim()
|
|
998
|
+
const chatId = chatIdMatch?.[1]?.trim()
|
|
999
|
+
const topicId = topicMatch?.[1]?.trim()
|
|
1000
|
+
|
|
1001
|
+
// Check 1: Token validation for modes that need it
|
|
1002
|
+
if (mode === 'new-bot' || mode === 'add-ids' || mode === 'create-topics' || mode === 'manual') {
|
|
1003
|
+
if (!token) {
|
|
1004
|
+
result.errors.push('Bot token not found. Please configure TG_BOT_TOKEN first.')
|
|
1005
|
+
result.canProceed = false
|
|
361
1006
|
} else {
|
|
362
|
-
|
|
363
|
-
|
|
1007
|
+
const tokenValidation = validateBotToken(token)
|
|
1008
|
+
if (tokenValidation !== true) {
|
|
1009
|
+
result.errors.push(`Invalid bot token: ${tokenValidation}`)
|
|
1010
|
+
result.canProceed = false
|
|
1011
|
+
} else {
|
|
1012
|
+
// Validate with API
|
|
1013
|
+
const isValid = await validateTokenSilently(token)
|
|
1014
|
+
if (!isValid) {
|
|
1015
|
+
result.errors.push('Bot token is invalid or expired. Please check with @BotFather')
|
|
1016
|
+
result.canProceed = false
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
364
1019
|
}
|
|
365
|
-
} catch {
|
|
366
|
-
cliLogger.warn('Could not reach Telegram API. Check your internet connection.')
|
|
367
1020
|
}
|
|
1021
|
+
|
|
1022
|
+
// Check 2: Chat ID validation for modes that need it
|
|
1023
|
+
if (mode === 'add-ids' || mode === 'create-topics' || mode === 'manual') {
|
|
1024
|
+
if (!chatId) {
|
|
1025
|
+
result.warnings.push('Control chat ID not configured. You need to add the bot to a group first.')
|
|
1026
|
+
} else {
|
|
1027
|
+
const chatIdValidation = validateChatId(chatId)
|
|
1028
|
+
if (chatIdValidation !== true) {
|
|
1029
|
+
result.warnings.push(`Control chat ID issue: ${chatIdValidation}`)
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Check 3: Topic ID validation for create-topics mode
|
|
1035
|
+
if (mode === 'create-topics') {
|
|
1036
|
+
if (topicId) {
|
|
1037
|
+
result.warnings.push('Topics already created. Re-running will create new topics.')
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Check 4: Bootstrap mode has special requirements
|
|
1042
|
+
if (mode === 'bootstrap') {
|
|
1043
|
+
if (!envContent) {
|
|
1044
|
+
result.warnings.push('Environment file will be created by bootstrap command')
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return result
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Display pre-check results
|
|
1053
|
+
*/
|
|
1054
|
+
function displayPreChecks(result: PreCheckResult): void {
|
|
1055
|
+
console.log('')
|
|
1056
|
+
|
|
1057
|
+
if (result.errors.length > 0) {
|
|
1058
|
+
cliLogger.error('Pre-check Errors:')
|
|
1059
|
+
result.errors.forEach(error => console.log(` ${chalk.red('✗')} ${error}`))
|
|
1060
|
+
console.log('')
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (result.warnings.length > 0) {
|
|
1064
|
+
cliLogger.warn('Pre-check Warnings:')
|
|
1065
|
+
result.warnings.forEach(warning => console.log(` ${chalk.yellow('⚠')} ${warning}`))
|
|
1066
|
+
console.log('')
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (result.canProceed && result.errors.length === 0) {
|
|
1070
|
+
cliLogger.success('All pre-checks passed')
|
|
1071
|
+
console.log('')
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// ============================================================================
|
|
1076
|
+
// FASE 5: DX Improvements
|
|
1077
|
+
// ============================================================================
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Changes summary for tracking what was done
|
|
1081
|
+
*/
|
|
1082
|
+
interface ChangesSummary {
|
|
1083
|
+
created: string[]
|
|
1084
|
+
updated: string[]
|
|
1085
|
+
skipped: string[]
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Track changes during setup
|
|
1090
|
+
*/
|
|
1091
|
+
class SetupTracker {
|
|
1092
|
+
private changes: ChangesSummary = {
|
|
1093
|
+
created: [],
|
|
1094
|
+
updated: [],
|
|
1095
|
+
skipped: [],
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
created(item: string): void {
|
|
1099
|
+
this.changes.created.push(item)
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
updated(item: string): void {
|
|
1103
|
+
this.changes.updated.push(item)
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
skipped(item: string): void {
|
|
1107
|
+
this.changes.skipped.push(item)
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
show(): void {
|
|
1111
|
+
console.log('')
|
|
1112
|
+
cliLogger.title('📊 Changes Summary')
|
|
1113
|
+
|
|
1114
|
+
if (this.changes.created.length > 0) {
|
|
1115
|
+
console.log(chalk.green('Created:'))
|
|
1116
|
+
this.changes.created.forEach(item => console.log(` ${chalk.green('+')} ${item}`))
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (this.changes.updated.length > 0) {
|
|
1120
|
+
console.log(chalk.yellow('Updated:'))
|
|
1121
|
+
this.changes.updated.forEach(item => console.log(` ${chalk.yellow('~')} ${item}`))
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (this.changes.skipped.length > 0) {
|
|
1125
|
+
console.log(chalk.dim('Skipped:'))
|
|
1126
|
+
this.changes.skipped.forEach(item => console.log(` ${chalk.dim('-')} ${item}`))
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (this.changes.created.length === 0 && this.changes.updated.length === 0) {
|
|
1130
|
+
console.log(chalk.dim('No changes made'))
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
console.log('')
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Show final summary with next steps
|
|
1139
|
+
*/
|
|
1140
|
+
function showFinalSummary(mode: SetupMode, environment: string): void {
|
|
1141
|
+
console.log('')
|
|
1142
|
+
cliLogger.title('✅ Setup Complete')
|
|
1143
|
+
|
|
1144
|
+
const steps = {
|
|
1145
|
+
'new-bot': [
|
|
1146
|
+
`1. Add the bot to a group: ${chalk.cyan('https://t.me/your_bot')}`,
|
|
1147
|
+
`2. Make the bot admin in the group`,
|
|
1148
|
+
`3. Run: ${chalk.yellow('bun run dev')}`,
|
|
1149
|
+
`4. Send /start to your bot`,
|
|
1150
|
+
],
|
|
1151
|
+
'add-ids': [
|
|
1152
|
+
`1. Review detected IDs in core/.env.${environment}`,
|
|
1153
|
+
`2. Run: ${chalk.yellow('bun run dev')}`,
|
|
1154
|
+
`3. Test the control commands`,
|
|
1155
|
+
],
|
|
1156
|
+
'create-topics': [
|
|
1157
|
+
`1. Verify topics were created in your group`,
|
|
1158
|
+
`2. Topic IDs are saved in core/.env.${environment}`,
|
|
1159
|
+
`3. Run: ${chalk.yellow('bun run dev')}`,
|
|
1160
|
+
],
|
|
1161
|
+
'bootstrap': [
|
|
1162
|
+
`1. Review the configuration in core/.env.${environment}`,
|
|
1163
|
+
`2. Your bot is ready to use`,
|
|
1164
|
+
`3. Run: ${chalk.yellow('bun run dev')}`,
|
|
1165
|
+
],
|
|
1166
|
+
'manual': [
|
|
1167
|
+
`1. Review configuration in core/.env.${environment}`,
|
|
1168
|
+
`2. Run: ${chalk.yellow('bun run dev')}`,
|
|
1169
|
+
`3. Send /start to your bot`,
|
|
1170
|
+
],
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
console.log('')
|
|
1174
|
+
console.log(chalk.bold('Next Steps:'))
|
|
1175
|
+
steps[mode]?.forEach(step => console.log(` ${step}`))
|
|
1176
|
+
console.log('')
|
|
1177
|
+
|
|
1178
|
+
// Suggest running doctor for verification
|
|
1179
|
+
console.log(chalk.bold.cyan('💡 Pro Tip:'))
|
|
1180
|
+
console.log(` Run ${chalk.yellow('bun run doctor')} to verify your configuration before starting`)
|
|
1181
|
+
console.log('')
|
|
368
1182
|
}
|