whatsapp-pi 1.0.30 → 1.0.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whatsapp-pi",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "type": "module",
5
5
  "description": "WhatsApp integration extension for Pi",
6
6
  "main": "whatsapp-pi.ts",
@@ -78,62 +78,3 @@ export interface RecentsStore {
78
78
  messagesBySender: Record<string, RecentConversationMessage[]>;
79
79
  updatedAt: number;
80
80
  }
81
-
82
- // QR Code Display Types
83
- export interface QRCodeSession {
84
- id: string;
85
- qrData: string;
86
- expiresAt: Date;
87
- isActive: boolean;
88
- createdAt: Date;
89
- refreshCount: number;
90
- }
91
-
92
- export interface QRDisplayState {
93
- isDisplaying: boolean;
94
- currentSession?: QRCodeSession;
95
- lastInstruction: string;
96
- warningMessage?: string;
97
- }
98
-
99
- export enum PairingStatus {
100
- IDLE = 'idle',
101
- GENERATING_QR = 'generating',
102
- DISPLAYING_QR = 'displaying',
103
- SCANNING = 'scanning',
104
- CONNECTED = 'connected',
105
- FAILED = 'failed'
106
- }
107
-
108
- export type ExtendedSessionStatus = SessionStatus | 'pairing' | 'qr-expired';
109
-
110
- export interface QRCodeDisplayOptions {
111
- terminalWidth?: number;
112
- refreshInterval: number;
113
- showInstructions: boolean;
114
- showExpirationWarning: boolean;
115
- }
116
-
117
- export class QRError extends Error {
118
- constructor(
119
- public code: string,
120
- message: string,
121
- public technical?: string,
122
- public recoverable: boolean = true
123
- ) {
124
- super(message);
125
- this.name = 'QRError';
126
- this.timestamp = new Date();
127
- }
128
-
129
- public timestamp: Date;
130
- }
131
-
132
- export enum QRErrorCode {
133
- TERMINAL_UNSUPPORTED = 'TERMINAL_UNSUPPORTED',
134
- QR_GENERATION_FAILED = 'QR_GENERATION_FAILED',
135
- DISPLAY_ERROR = 'DISPLAY_ERROR',
136
- CONNECTION_TIMEOUT = 'CONNECTION_TIMEOUT',
137
- NETWORK_ERROR = 'NETWORK_ERROR',
138
- INVALID_CREDENTIALS = 'INVALID_CREDENTIALS'
139
- }
@@ -285,72 +285,4 @@ export class SessionManager {
285
285
  getAuthStateDir(): string {
286
286
  return this.authStateDir;
287
287
  }
288
-
289
- // QR Code Detection Methods
290
-
291
- /**
292
- * Checks if QR code flow is needed (no valid credentials exist)
293
- */
294
- public async needsQRCode(): Promise<boolean> {
295
- try {
296
- // Check if we have valid authentication state
297
- const hasValidCredentials = await this.hasValidCredentials();
298
- return !hasValidCredentials;
299
- } catch (error) {
300
- // If we can't determine, assume QR is needed
301
- return true;
302
- }
303
- }
304
-
305
- /**
306
- * Checks if valid credentials exist
307
- */
308
- public async hasValidCredentials(): Promise<boolean> {
309
- try {
310
- // Check if credentials file exists and is readable
311
- const credsPath = join(this.authStateDir, 'creds.json');
312
- await readFile(credsPath, 'utf-8');
313
-
314
- // Also check if we have the hasAuthState flag set
315
- return this.hasAuthState;
316
- } catch {
317
- return false;
318
- }
319
- }
320
-
321
- /**
322
- * Marks QR code pairing as completed
323
- */
324
- public async markQRCompleted(): Promise<void> {
325
- await this.markAuthStateAvailable();
326
- this.status = 'connected';
327
- await this.saveConfig();
328
- }
329
-
330
- /**
331
- * Invalidates current QR session (used for logout/re-pairing)
332
- */
333
- public async invalidateQRSession(): Promise<void> {
334
- // Clear auth state to trigger QR flow on next connection
335
- this.hasAuthState = false;
336
- this.status = 'logged-out';
337
- await this.saveConfig();
338
- }
339
-
340
- /**
341
- * Waits for credentials to become available (with timeout)
342
- */
343
- public async waitForCredentials(timeout: number = 120000): Promise<boolean> {
344
- const startTime = Date.now();
345
- const checkInterval = 1000; // Check every second
346
-
347
- while (Date.now() - startTime < timeout) {
348
- if (await this.hasValidCredentials()) {
349
- return true;
350
- }
351
- await new Promise(resolve => setTimeout(resolve, checkInterval));
352
- }
353
-
354
- return false; // Timeout reached
355
- }
356
288
  }
@@ -7,7 +7,6 @@ import {
7
7
  } from '@whiskeysockets/baileys';
8
8
  import P from 'pino';
9
9
  import { Boom } from '@hapi/boom';
10
- import qr from 'qrcode-terminal';
11
10
  import { SessionManager } from './session.manager.js';
12
11
  import { IncomingMessage, WhatsAppSession, SessionStatus } from '../models/whatsapp.types.js';
13
12
  import { MessageSender } from './message.sender.js';
@@ -120,7 +119,6 @@ export class WhatsAppService {
120
119
 
121
120
  if (qr) {
122
121
  this.sessionManager.setStatus('pairing');
123
- this.displayQRCode(qr);
124
122
  this.onQRCode?.(qr);
125
123
  this.onStatusUpdate?.('| WhatsApp: Pairing...');
126
124
  }
@@ -179,17 +177,17 @@ export class WhatsAppService {
179
177
  await this.sessionManager.markAuthStateAvailable();
180
178
  this.sessionManager.setStatus('connected');
181
179
  this.onStatusUpdate?.('| WhatsApp: Connected');
182
-
183
- // Restore console methods now that connection is established
184
- if (!this.verboseMode) {
185
- console.log = originalConsoleLog;
186
- console.warn = originalConsoleWarn;
187
- console.error = originalConsoleError;
188
- }
189
180
  }
190
181
  });
191
182
 
192
183
  this.socket.ev.on('messages.upsert', (m: any) => this.handleIncomingMessages(m));
184
+
185
+ // Restore console methods after socket creation
186
+ if (!this.verboseMode) {
187
+ console.log = originalConsoleLog;
188
+ console.warn = originalConsoleWarn;
189
+ console.error = originalConsoleError;
190
+ }
193
191
  }
194
192
 
195
193
  public async handleIncomingMessages(m: any) {
@@ -258,28 +256,6 @@ export class WhatsAppService {
258
256
  this.onStatusUpdate = callback;
259
257
  }
260
258
 
261
- private displayQRCode(qrData: string): void {
262
- // Clear screen and show QR code with instructions
263
- console.clear();
264
-
265
- console.log('');
266
- console.log('WhatsApp: Pairing...');
267
- console.log('Scan this QR code with your WhatsApp mobile app:');
268
- console.log('');
269
-
270
- // Display QR code
271
- qr.generate(qrData, { small: true });
272
-
273
- console.log('');
274
- console.log('1. Open WhatsApp on your phone');
275
- console.log('2. Go to Settings > Linked Devices');
276
- console.log('3. Tap "Link a device"');
277
- console.log('4. Point your camera at this QR code');
278
- console.log('');
279
- console.log('QR code will refresh automatically if it expires.');
280
- console.log('');
281
- }
282
-
283
259
  public getLastRemoteJid(): string | null {
284
260
  return this.lastRemoteJid;
285
261
  }
@@ -1,160 +0,0 @@
1
- import { QRCodeDisplayOptions } from '../models/whatsapp.types.js';
2
-
3
- export interface QRDisplayConfig {
4
- terminalWidth?: number;
5
- refreshInterval: number;
6
- maxRefreshAttempts: number;
7
- showInstructions: boolean;
8
- showExpirationWarning: boolean;
9
- expirationWarningTime: number;
10
- retryOnError: boolean;
11
- maxRetries: number;
12
- retryDelay: number;
13
- }
14
-
15
- export class QRConfigService {
16
- private static instance: QRConfigService;
17
- private config: QRDisplayConfig;
18
-
19
- private constructor() {
20
- this.config = this.getDefaultConfig();
21
- this.loadFromEnvironment();
22
- }
23
-
24
- /**
25
- * Gets singleton instance
26
- */
27
- static getInstance(): QRConfigService {
28
- if (!QRConfigService.instance) {
29
- QRConfigService.instance = new QRConfigService();
30
- }
31
- return QRConfigService.instance;
32
- }
33
-
34
- /**
35
- * Gets current configuration
36
- */
37
- getConfig(): QRDisplayConfig {
38
- return { ...this.config };
39
- }
40
-
41
- /**
42
- * Updates configuration
43
- */
44
- updateConfig(config: Partial<QRDisplayConfig>): void {
45
- this.config = { ...this.config, ...config };
46
- }
47
-
48
- /**
49
- * Resets configuration to defaults
50
- */
51
- resetConfig(): void {
52
- this.config = this.getDefaultConfig();
53
- }
54
-
55
- /**
56
- * Gets QR display options for renderer
57
- */
58
- getDisplayOptions(): QRCodeDisplayOptions {
59
- return {
60
- terminalWidth: this.config.terminalWidth,
61
- refreshInterval: this.config.refreshInterval,
62
- showInstructions: this.config.showInstructions,
63
- showExpirationWarning: this.config.showExpirationWarning
64
- };
65
- }
66
-
67
- /**
68
- * Gets retry configuration
69
- */
70
- getRetryConfig() {
71
- return {
72
- retryOnError: this.config.retryOnError,
73
- maxRetries: this.config.maxRetries,
74
- retryDelay: this.config.retryDelay
75
- };
76
- }
77
-
78
- /**
79
- * Gets refresh configuration
80
- */
81
- getRefreshConfig() {
82
- return {
83
- refreshInterval: this.config.refreshInterval,
84
- maxRefreshAttempts: this.config.maxRefreshAttempts,
85
- expirationWarningTime: this.config.expirationWarningTime,
86
- showExpirationWarning: this.config.showExpirationWarning
87
- };
88
- }
89
-
90
- /**
91
- * Loads configuration from environment variables
92
- */
93
- private loadFromEnvironment(): void {
94
- // QR refresh interval in milliseconds
95
- if (process.env.QR_REFRESH_INTERVAL) {
96
- const interval = parseInt(process.env.QR_REFRESH_INTERVAL, 10);
97
- if (!isNaN(interval) && interval > 0) {
98
- this.config.refreshInterval = interval;
99
- }
100
- }
101
-
102
- // Show expiration warning
103
- if (process.env.QR_SHOW_WARNING !== undefined) {
104
- this.config.showExpirationWarning = process.env.QR_SHOW_WARNING === 'true';
105
- }
106
-
107
- // Warning time in seconds
108
- if (process.env.QR_WARNING_TIME) {
109
- const warningTime = parseInt(process.env.QR_WARNING_TIME, 10);
110
- if (!isNaN(warningTime) && warningTime > 0) {
111
- this.config.expirationWarningTime = warningTime;
112
- }
113
- }
114
-
115
- // Max refresh attempts
116
- if (process.env.QR_MAX_REFRESH_ATTEMPTS) {
117
- const maxAttempts = parseInt(process.env.QR_MAX_REFRESH_ATTEMPTS, 10);
118
- if (!isNaN(maxAttempts) && maxAttempts > 0) {
119
- this.config.maxRefreshAttempts = maxAttempts;
120
- }
121
- }
122
-
123
- // Retry on error
124
- if (process.env.QR_RETRY_ON_ERROR !== undefined) {
125
- this.config.retryOnError = process.env.QR_RETRY_ON_ERROR === 'true';
126
- }
127
-
128
- // Max retries
129
- if (process.env.QR_MAX_RETRIES) {
130
- const maxRetries = parseInt(process.env.QR_MAX_RETRIES, 10);
131
- if (!isNaN(maxRetries) && maxRetries >= 0) {
132
- this.config.maxRetries = maxRetries;
133
- }
134
- }
135
-
136
- // Retry delay
137
- if (process.env.QR_RETRY_DELAY) {
138
- const retryDelay = parseInt(process.env.QR_RETRY_DELAY, 10);
139
- if (!isNaN(retryDelay) && retryDelay > 0) {
140
- this.config.retryDelay = retryDelay;
141
- }
142
- }
143
- }
144
-
145
- /**
146
- * Gets default configuration
147
- */
148
- private getDefaultConfig(): QRDisplayConfig {
149
- return {
150
- refreshInterval: 60000, // 60 seconds
151
- maxRefreshAttempts: 10, // Unlimited for practical purposes
152
- showInstructions: true,
153
- showExpirationWarning: true,
154
- expirationWarningTime: 15, // 15 seconds before expiration
155
- retryOnError: true,
156
- maxRetries: 3,
157
- retryDelay: 2000 // 2 seconds
158
- };
159
- }
160
- }
@@ -1,151 +0,0 @@
1
- import { QRError, QRErrorCode } from '../models/whatsapp.types.js';
2
-
3
- export class QRErrorHandler {
4
- private errorHistory: QRError[] = [];
5
- private maxHistorySize = 10;
6
-
7
- /**
8
- * Handles a QR-related error
9
- */
10
- async handleError(error: QRError): Promise<void> {
11
- // Add to error history
12
- this.addToHistory(error);
13
-
14
- // Log the error
15
- this.logError(error);
16
-
17
- // Show user-friendly message
18
- this.displayError(error);
19
- }
20
-
21
- /**
22
- * Determines if an error is recoverable
23
- */
24
- canRecover(error: QRError): boolean {
25
- return error.recoverable;
26
- }
27
-
28
- /**
29
- * Gets recovery action for an error
30
- */
31
- getRecoveryAction(error: QRError): string | null {
32
- switch (error.code) {
33
- case QRErrorCode.TERMINAL_UNSUPPORTED:
34
- return 'Try using a modern terminal (Windows Terminal, iTerm2, or VS Code terminal) that supports UTF-8 characters.';
35
-
36
- case QRErrorCode.QR_GENERATION_FAILED:
37
- return 'Check your internet connection and try running the command again.';
38
-
39
- case QRErrorCode.DISPLAY_ERROR:
40
- return 'Try making your terminal window larger and run the command again.';
41
-
42
- case QRErrorCode.CONNECTION_TIMEOUT:
43
- return 'The QR code expired. A new one will be generated automatically.';
44
-
45
- case QRErrorCode.NETWORK_ERROR:
46
- return 'Check your internet connection and try again.';
47
-
48
- case QRErrorCode.INVALID_CREDENTIALS:
49
- return 'Run /whatsapp-logout to clear credentials and try again.';
50
-
51
- default:
52
- return 'Try restarting the application.';
53
- }
54
- }
55
-
56
- /**
57
- * Creates a standardized QR error
58
- */
59
- createError(code: QRErrorCode, message: string, technical?: string): QRError {
60
- return new QRError(
61
- code,
62
- message,
63
- technical,
64
- this.isRecoverableByDefault(code)
65
- );
66
- }
67
-
68
- /**
69
- * Gets recent error history
70
- */
71
- getErrorHistory(): QRError[] {
72
- return [...this.errorHistory];
73
- }
74
-
75
- /**
76
- * Clears error history
77
- */
78
- clearHistory(): void {
79
- this.errorHistory = [];
80
- }
81
-
82
- /**
83
- * Checks if specific error type has occurred recently
84
- */
85
- hasRecentError(code: QRErrorCode, withinMs: number = 60000): boolean {
86
- const now = new Date();
87
- return this.errorHistory.some(error =>
88
- error.code === code &&
89
- (now.getTime() - error.timestamp.getTime()) < withinMs
90
- );
91
- }
92
-
93
- /**
94
- * Private method to add error to history
95
- */
96
- private addToHistory(error: QRError): void {
97
- this.errorHistory.push(error);
98
-
99
- // Keep only recent errors
100
- if (this.errorHistory.length > this.maxHistorySize) {
101
- this.errorHistory = this.errorHistory.slice(-this.maxHistorySize);
102
- }
103
- }
104
-
105
- /**
106
- * Private method to log error
107
- */
108
- private logError(error: QRError): void {
109
- const logMessage = `[QR Error] ${error.code}: ${error.message}`;
110
-
111
- if (error.technical) {
112
- console.error(`${logMessage} (${error.technical})`);
113
- } else {
114
- console.error(logMessage);
115
- }
116
- }
117
-
118
- /**
119
- * Private method to display error to user
120
- */
121
- private displayError(error: QRError): void {
122
- console.error(`❌ ${error.message}`);
123
-
124
- const recoveryAction = this.getRecoveryAction(error);
125
- if (recoveryAction) {
126
- console.log(`💡 ${recoveryAction}`);
127
- }
128
- }
129
-
130
- /**
131
- * Private method to determine if error is recoverable by default
132
- */
133
- private isRecoverableByDefault(code: QRErrorCode): boolean {
134
- switch (code) {
135
- case QRErrorCode.TERMINAL_UNSUPPORTED:
136
- return false; // Requires terminal change
137
-
138
- case QRErrorCode.INVALID_CREDENTIALS:
139
- return true; // Can be fixed with logout
140
-
141
- case QRErrorCode.QR_GENERATION_FAILED:
142
- case QRErrorCode.DISPLAY_ERROR:
143
- case QRErrorCode.CONNECTION_TIMEOUT:
144
- case QRErrorCode.NETWORK_ERROR:
145
- return true; // Generally recoverable
146
-
147
- default:
148
- return true; // Assume recoverable by default
149
- }
150
- }
151
- }
@@ -1,179 +0,0 @@
1
- import { EventEmitter } from 'events';
2
- import { QRCodeSession, QRError, PairingStatus } from '../models/whatsapp.types.js';
3
-
4
- export interface QREventMap {
5
- 'qr:generated': { qrData: string; expiresAt: Date };
6
- 'qr:expired': { sessionId: string };
7
- 'qr:refreshed': { qrData: string; refreshCount: number };
8
- 'pairing:started': { sessionId: string };
9
- 'pairing:scanning': { sessionId: string };
10
- 'pairing:completed': { sessionId: string };
11
- 'pairing:failed': { sessionId: string; error: QRError };
12
- 'status:changed': { status: PairingStatus };
13
- }
14
-
15
- export class QREventsService extends EventEmitter {
16
- private static instance: QREventsService;
17
-
18
- private constructor() {
19
- super();
20
- this.setMaxListeners(50); // Allow more listeners for QR events
21
- }
22
-
23
- /**
24
- * Gets singleton instance
25
- */
26
- static getInstance(): QREventsService {
27
- if (!QREventsService.instance) {
28
- QREventsService.instance = new QREventsService();
29
- }
30
- return QREventsService.instance;
31
- }
32
-
33
- /**
34
- * Emit QR code generated event
35
- */
36
- emitQRGenerated(qrData: string, expiresAt: Date): void {
37
- this.emit('qr:generated', { qrData, expiresAt });
38
- }
39
-
40
- /**
41
- * Emit QR code expired event
42
- */
43
- emitQRExpired(sessionId: string): void {
44
- this.emit('qr:expired', { sessionId });
45
- }
46
-
47
- /**
48
- * Emit QR code refreshed event
49
- */
50
- emitQRRefreshed(qrData: string, refreshCount: number): void {
51
- this.emit('qr:refreshed', { qrData, refreshCount });
52
- }
53
-
54
- /**
55
- * Emit pairing started event
56
- */
57
- emitPairingStarted(sessionId: string): void {
58
- this.emit('pairing:started', { sessionId });
59
- }
60
-
61
- /**
62
- * Emit pairing scanning event
63
- */
64
- emitPairingScanning(sessionId: string): void {
65
- this.emit('pairing:scanning', { sessionId });
66
- }
67
-
68
- /**
69
- * Emit pairing completed event
70
- */
71
- emitPairingCompleted(sessionId: string): void {
72
- this.emit('pairing:completed', { sessionId });
73
- }
74
-
75
- /**
76
- * Emit pairing failed event
77
- */
78
- emitPairingFailed(sessionId: string, error: QRError): void {
79
- this.emit('pairing:failed', { sessionId, error });
80
- }
81
-
82
- /**
83
- * Emit status changed event
84
- */
85
- emitStatusChanged(status: PairingStatus): void {
86
- this.emit('status:changed', { status });
87
- }
88
-
89
- /**
90
- * Type-safe event listener registration
91
- */
92
- onQRGenerated(callback: (data: QREventMap['qr:generated']) => void): void {
93
- this.on('qr:generated', callback);
94
- }
95
-
96
- onQRExpired(callback: (data: QREventMap['qr:expired']) => void): void {
97
- this.on('qr:expired', callback);
98
- }
99
-
100
- onQRRefreshed(callback: (data: QREventMap['qr:refreshed']) => void): void {
101
- this.on('qr:refreshed', callback);
102
- }
103
-
104
- onPairingStarted(callback: (data: QREventMap['pairing:started']) => void): void {
105
- this.on('pairing:started', callback);
106
- }
107
-
108
- onPairingScanning(callback: (data: QREventMap['pairing:scanning']) => void): void {
109
- this.on('pairing:scanning', callback);
110
- }
111
-
112
- onPairingCompleted(callback: (data: QREventMap['pairing:completed']) => void): void {
113
- this.on('pairing:completed', callback);
114
- }
115
-
116
- onPairingFailed(callback: (data: QREventMap['pairing:failed']) => void): void {
117
- this.on('pairing:failed', callback);
118
- }
119
-
120
- onStatusChanged(callback: (data: QREventMap['status:changed']) => void): void {
121
- this.on('status:changed', callback);
122
- }
123
-
124
- /**
125
- * Type-safe event listener removal
126
- */
127
- offQRGenerated(callback: (data: QREventMap['qr:generated']) => void): void {
128
- this.off('qr:generated', callback);
129
- }
130
-
131
- offQRExpired(callback: (data: QREventMap['qr:expired']) => void): void {
132
- this.off('qr:expired', callback);
133
- }
134
-
135
- offQRRefreshed(callback: (data: QREventMap['qr:refreshed']) => void): void {
136
- this.off('qr:refreshed', callback);
137
- }
138
-
139
- offPairingStarted(callback: (data: QREventMap['pairing:started']) => void): void {
140
- this.off('pairing:started', callback);
141
- }
142
-
143
- offPairingScanning(callback: (data: QREventMap['pairing:scanning']) => void): void {
144
- this.off('pairing:scanning', callback);
145
- }
146
-
147
- offPairingCompleted(callback: (data: QREventMap['pairing:completed']) => void): void {
148
- this.off('pairing:completed', callback);
149
- }
150
-
151
- offPairingFailed(callback: (data: QREventMap['pairing:failed']) => void): void {
152
- this.off('pairing:failed', callback);
153
- }
154
-
155
- offStatusChanged(callback: (data: QREventMap['status:changed']) => void): void {
156
- this.off('status:changed', callback);
157
- }
158
-
159
- /**
160
- * Clear all event listeners
161
- */
162
- clearAllListeners(): void {
163
- this.removeAllListeners();
164
- }
165
-
166
- /**
167
- * Get event listener count for debugging
168
- */
169
- getListenerCount(eventName: keyof QREventMap): number {
170
- return this.listenerCount(eventName);
171
- }
172
-
173
- /**
174
- * Get all event names with listeners
175
- */
176
- getActiveEvents(): (keyof QREventMap)[] {
177
- return Object.keys(this.eventNames()) as (keyof QREventMap)[];
178
- }
179
- }
@@ -1,154 +0,0 @@
1
- import qr from 'qrcode-terminal';
2
- import { QRCodeDisplayOptions, QRError, QRErrorCode } from '../models/whatsapp.types.js';
3
-
4
- export class QRRendererService {
5
- private terminalWidth: number;
6
- private supportsUTF8: boolean;
7
-
8
- constructor() {
9
- this.terminalWidth = this.detectTerminalWidth();
10
- this.supportsUTF8 = this.detectUTF8Support();
11
- }
12
-
13
- /**
14
- * Renders a QR code in the terminal
15
- */
16
- async renderQR(qrData: string, options?: QRCodeDisplayOptions): Promise<void> {
17
- try {
18
- if (!this.supportsUTF8) {
19
- throw new QRError(
20
- QRErrorCode.TERMINAL_UNSUPPORTED,
21
- 'Terminal does not support UTF-8 characters required for QR code display',
22
- 'UTF-8 support detection failed',
23
- false
24
- );
25
- }
26
-
27
- const displayOptions = {
28
- small: true,
29
- ...options
30
- };
31
-
32
- // Clear any existing QR display
33
- await this.clearQR();
34
-
35
- // Render the QR code
36
- qr.generate(qrData, displayOptions);
37
-
38
- } catch (error) {
39
- if (error instanceof QRError) {
40
- throw error;
41
- }
42
- throw new QRError(
43
- QRErrorCode.DISPLAY_ERROR,
44
- 'Failed to render QR code in terminal',
45
- error instanceof Error ? error.message : 'Unknown error',
46
- true
47
- );
48
- }
49
- }
50
-
51
- /**
52
- * Clears the terminal display
53
- */
54
- async clearQR(): Promise<void> {
55
- try {
56
- // Clear screen and move cursor to top
57
- process.stdout.write('\x1b[2J\x1b[H');
58
- } catch (error) {
59
- // Non-critical error, continue
60
- }
61
- }
62
-
63
- /**
64
- * Shows pairing instructions to the user
65
- */
66
- showInstructions(): void {
67
- const instructions = [
68
- '',
69
- 'WhatsApp: Pairing...',
70
- 'Scan this QR code with your WhatsApp mobile app:',
71
- '',
72
- '1. Open WhatsApp on your phone',
73
- '2. Go to Settings > Linked Devices',
74
- '3. Tap "Link a device"',
75
- '4. Point your camera at this QR code',
76
- ''
77
- ];
78
-
79
- instructions.forEach(line => {
80
- console.log(line);
81
- });
82
- }
83
-
84
- /**
85
- * Shows a warning message
86
- */
87
- showWarning(message: string): void {
88
- console.log(`⚠️ ${message}`);
89
- }
90
-
91
- /**
92
- * Shows an error message
93
- */
94
- showError(error: QRError): void {
95
- console.error(`❌ ${error.message}`);
96
- if (error.technical && process.env.NODE_ENV === 'development') {
97
- console.error(`Technical details: ${error.technical}`);
98
- }
99
- if (error.recoverable) {
100
- console.log('💡 You can try again.');
101
- }
102
- }
103
-
104
- /**
105
- * Detects terminal width
106
- */
107
- getTerminalWidth(): number {
108
- return this.terminalWidth;
109
- }
110
-
111
- /**
112
- * Checks if terminal supports UTF-8
113
- */
114
- supportsUTF8Display(): boolean {
115
- return this.supportsUTF8;
116
- }
117
-
118
- /**
119
- * Internal method to detect terminal width
120
- */
121
- private detectTerminalWidth(): number {
122
- try {
123
- if (process.stdout.columns) {
124
- return process.stdout.columns;
125
- }
126
- // Fallback to common terminal width
127
- return 80;
128
- } catch {
129
- return 80;
130
- }
131
- }
132
-
133
- /**
134
- * Internal method to detect UTF-8 support
135
- */
136
- private detectUTF8Support(): boolean {
137
- try {
138
- // Check for common UTF-8 environment variables
139
- const lang = process.env.LANG || '';
140
- const lcAll = process.env.LC_ALL || '';
141
- const termProgram = process.env.TERM_PROGRAM || '';
142
-
143
- return lang.includes('UTF-8') ||
144
- lang.includes('utf8') ||
145
- lcAll.includes('UTF-8') ||
146
- lcAll.includes('utf8') ||
147
- process.platform === 'darwin' || // macOS usually supports UTF-8
148
- termProgram.includes('vscode') || // VS Code terminal
149
- termProgram.includes('hyper'); // Hyper terminal
150
- } catch {
151
- return false;
152
- }
153
- }
154
- }