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.
Files changed (53) hide show
  1. package/dist/templates/telegram-bot/CLAUDE.deploy.md +2 -3
  2. package/dist/templates/telegram-bot/CLAUDE.dev.md +304 -5
  3. package/dist/templates/telegram-bot/CLAUDE.md +166 -89
  4. package/dist/templates/telegram-bot/README.md +252 -129
  5. package/dist/templates/telegram-bot/bun.lock +146 -3
  6. package/dist/templates/telegram-bot/core/.env.example +6 -0
  7. package/dist/templates/telegram-bot/core/src/config/env.ts +130 -1
  8. package/dist/templates/telegram-bot/core/src/config/logging.ts +3 -1
  9. package/dist/templates/telegram-bot/core/src/handlers/config-export.ts +122 -0
  10. package/dist/templates/telegram-bot/core/src/handlers/control.ts +37 -11
  11. package/dist/templates/telegram-bot/core/src/handlers/health.ts +21 -26
  12. package/dist/templates/telegram-bot/core/src/handlers/info.ts +191 -0
  13. package/dist/templates/telegram-bot/core/src/handlers/listener.ts +168 -0
  14. package/dist/templates/telegram-bot/core/src/handlers/logs.ts +14 -7
  15. package/dist/templates/telegram-bot/core/src/index.ts +29 -0
  16. package/dist/templates/telegram-bot/core/src/utils/formatters.ts +55 -19
  17. package/dist/templates/telegram-bot/core/src/utils/instance-manager.ts +6 -2
  18. package/dist/templates/telegram-bot/core/src/utils/message-builder.ts +180 -0
  19. package/dist/templates/telegram-bot/docs/automatizacion_integral_de_bots_de_telegram_con_type_script.md +326 -0
  20. package/dist/templates/telegram-bot/docs/cli-commands.md +514 -5
  21. package/dist/templates/telegram-bot/docs/environment.md +191 -3
  22. package/dist/templates/telegram-bot/docs/getting-started.md +202 -15
  23. package/dist/templates/telegram-bot/package.json +5 -2
  24. package/dist/templates/telegram-bot/packages/utils/src/logger.ts +1 -0
  25. package/dist/templates/telegram-bot/tools/commands/doctor.ts +62 -0
  26. package/dist/templates/telegram-bot/tools/commands/setup.ts +984 -170
  27. package/package.json +1 -1
  28. package/templates/telegram-bot/CLAUDE.deploy.md +2 -3
  29. package/templates/telegram-bot/CLAUDE.dev.md +304 -5
  30. package/templates/telegram-bot/CLAUDE.md +166 -89
  31. package/templates/telegram-bot/README.md +252 -129
  32. package/templates/telegram-bot/bun.lock +146 -3
  33. package/templates/telegram-bot/core/.env.example +6 -0
  34. package/templates/telegram-bot/core/src/config/env.ts +130 -1
  35. package/templates/telegram-bot/core/src/config/logging.ts +3 -1
  36. package/templates/telegram-bot/core/src/handlers/config-export.ts +122 -0
  37. package/templates/telegram-bot/core/src/handlers/control.ts +37 -11
  38. package/templates/telegram-bot/core/src/handlers/health.ts +21 -26
  39. package/templates/telegram-bot/core/src/handlers/info.ts +191 -0
  40. package/templates/telegram-bot/core/src/handlers/listener.ts +168 -0
  41. package/templates/telegram-bot/core/src/handlers/logs.ts +14 -7
  42. package/templates/telegram-bot/core/src/index.ts +29 -0
  43. package/templates/telegram-bot/core/src/utils/formatters.ts +55 -19
  44. package/templates/telegram-bot/core/src/utils/instance-manager.ts +6 -2
  45. package/templates/telegram-bot/core/src/utils/message-builder.ts +180 -0
  46. package/templates/telegram-bot/docs/automatizacion_integral_de_bots_de_telegram_con_type_script.md +326 -0
  47. package/templates/telegram-bot/docs/cli-commands.md +514 -5
  48. package/templates/telegram-bot/docs/environment.md +191 -3
  49. package/templates/telegram-bot/docs/getting-started.md +202 -15
  50. package/templates/telegram-bot/package.json +5 -2
  51. package/templates/telegram-bot/packages/utils/src/logger.ts +1 -0
  52. package/templates/telegram-bot/tools/commands/doctor.ts +62 -0
  53. 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
- mode?: 'polling' | 'webhook'
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: string
31
- TG_MODE: 'polling' | 'webhook'
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: string
35
- TG_INSTANCE_NAME: string
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 interactively')
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
- // Check if .env file already exists
71
- if (existsSync(envFile)) {
72
- cliLogger.warn(`Environment file already exists: ${envFile}`)
85
+ // Pre-check: detect context
86
+ const context = await detectContext(envFile, envExample)
87
+ showContextSummary(context)
73
88
 
74
- const shouldContinue = await confirm({
75
- message: 'Do you want to overwrite it?',
76
- default: false,
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
- if (!shouldContinue) {
80
- cliLogger.info('Setup cancelled')
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
- // Copy .env.example to .env.{environment}
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
- // Read existing env file to preserve comments
92
- let envContent = ''
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
- envContent = await readFile(envFile, 'utf-8')
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
- // Interactive prompts (skip if provided via flags)
98
- const config: SetupConfig = await gatherConfig(options, envContent)
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(`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')
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 if provided
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
- async function gatherConfig(options: SetupOptions, _envContent: string): Promise<SetupConfig> {
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('\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')
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: (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
- },
547
+ validate: validateBotToken,
140
548
  })
141
549
  }
142
550
 
143
551
  // Mode
144
- if (options.mode) {
145
- config.TG_MODE = options.mode
552
+ if (options.botMode) {
553
+ config.TG_MODE = options.botMode
146
554
  } else {
147
- const modeSelection = await select({
555
+ config.TG_MODE = await select({
148
556
  message: 'Select bot operation mode:',
149
557
  choices: [
150
- { name: 'Polling (recommended for development)', value: 'polling' },
151
- { name: 'Webhook (recommended for production)', value: 'webhook' },
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 configuration (if webhook mode)
572
+ // Webhook config if needed
158
573
  if (config.TG_MODE === 'webhook') {
159
- cliLogger.warn('\nWebhook mode requires a public HTTPS endpoint')
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 (https://your-domain.com/webhook):',
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 for webhook mode'
582
+ return 'Webhook URL is required'
166
583
  }
167
584
  if (!value.startsWith('https://')) {
168
- return 'Webhook URL must use HTTPS'
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 (min 16 chars):',
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 'Webhook secret must be at least 16 characters'
596
+ return 'Must be at least 16 characters'
179
597
  }
180
598
  return true
181
599
  },
182
600
  })
183
601
  }
184
602
 
185
- // Environment identification
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
- // Optional: Log streaming
210
- const enableLogStreaming = await confirm({
211
- message: 'Enable log streaming to Telegram?',
212
- default: false,
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
- 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')
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
- config.TG_LOG_CHAT_ID = await input({
223
- message: 'Enter log chat ID (optional, press Enter to skip):',
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 (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,
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 (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
- },
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
- config.TG_LOG_TOPIC_ID = Number.parseInt(topicId, 10)
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
- // 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:',
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 (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,
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 (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
- },
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
- config.TG_CONTROL_TOPIC_ID = Number.parseInt(topicId, 10)
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
- // Authorized users
281
- const authUsers = await input({
282
- message: 'Enter authorized user IDs (comma-separated):',
283
- })
759
+ return config as SetupConfig
760
+ }
284
761
 
285
- if (authUsers && authUsers.trim().length > 0) {
286
- config.TG_AUTHORIZED_USER_IDS = authUsers
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
- // 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
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
- return config as SetupConfig
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
- function updateEnvFile(content: string, config: SetupConfig): string {
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
- // 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}`)
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
- async function validateToken(token: string): Promise<void> {
353
- cliLogger.info('\nValidating bot token with Telegram API...')
828
+ function getConfigValue(config: SetupConfig, key: string): string | undefined {
829
+ const value = config[key as keyof SetupConfig]
354
830
 
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 } }
831
+ if (value === undefined || value === null) {
832
+ return undefined
833
+ }
358
834
 
359
- if (data.ok && data.result) {
360
- cliLogger.success(`Bot connected: @${data.result.username} (${data.result.first_name})`)
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
- cliLogger.warn('Could not validate token. The bot may not work correctly.')
363
- cliLogger.info('Make sure the token is correct and try again.')
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
  }