@arela/uploader 1.1.3 β†’ 1.2.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 (99) hide show
  1. package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
  2. package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
  3. package/.claude/worktrees/agent-multi-profile/README.md +405 -0
  4. package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
  5. package/.claude/worktrees/agent-multi-profile/package.json +78 -0
  6. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
  7. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
  8. package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
  9. package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
  10. package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
  11. package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
  12. package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
  13. package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
  14. package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
  15. package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
  16. package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
  17. package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
  18. package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
  19. package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
  20. package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
  21. package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
  22. package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
  23. package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
  24. package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
  25. package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
  26. package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
  27. package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
  28. package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
  29. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
  30. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
  31. package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
  32. package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
  33. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
  34. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
  35. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
  36. package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
  37. package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
  38. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
  39. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
  40. package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
  41. package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
  42. package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
  43. package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
  44. package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
  45. package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
  46. package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
  47. package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
  48. package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
  49. package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
  50. package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
  51. package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
  52. package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
  53. package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
  54. package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
  55. package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
  56. package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
  57. package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
  58. package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
  59. package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
  60. package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
  61. package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
  62. package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
  63. package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
  64. package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
  65. package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
  66. package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
  67. package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
  68. package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
  69. package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
  70. package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
  71. package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
  72. package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
  73. package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
  74. package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
  75. package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  76. package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
  77. package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
  78. package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
  79. package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
  80. package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
  81. package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
  82. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
  83. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
  84. package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
  85. package/README.md +65 -0
  86. package/package.json +1 -1
  87. package/src/commands/AgentCommand.js +210 -0
  88. package/src/commands/AgentInitCommand.js +316 -0
  89. package/src/commands/PollWorkerCommand.js +11 -322
  90. package/src/config/config.js +44 -6
  91. package/src/document-type-shared.js +1 -1
  92. package/src/document-types/pedimento-completo.js +8 -1
  93. package/src/file-detection.js +9 -0
  94. package/src/index.js +55 -0
  95. package/src/scoring/scoring-engine.js +1 -1
  96. package/src/services/LoggingService.js +35 -0
  97. package/src/services/PipelineApiService.js +7 -1
  98. package/src/services/PipelineJobRunner.js +389 -0
  99. package/src/services/ProfileManager.js +164 -0
@@ -0,0 +1,890 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+
4
+ import agentCommand from './commands/AgentCommand.js';
5
+ import agentInitCommand from './commands/AgentInitCommand.js';
6
+ import datastageCommand from './commands/DatastageCommand.js';
7
+ import gdriveSyncCommand from './commands/GDriveSyncCommand.js';
8
+ import identifyCommand from './commands/IdentifyCommand.js';
9
+ import pollWorkerCommand from './commands/PollWorkerCommand.js';
10
+ import PropagateCommand from './commands/PropagateCommand.js';
11
+ import PushCommand from './commands/PushCommand.js';
12
+ import scanCommand from './commands/ScanCommand.js';
13
+ import UploadCommand from './commands/UploadCommand.js';
14
+ import watchCommand from './commands/WatchCommand.js';
15
+ import workerCommand from './commands/WorkerCommand.js';
16
+ import appConfig from './config/config.js';
17
+ import ErrorHandler from './errors/ErrorHandler.js';
18
+ import { getCurrentPdfFile } from './file-detection.js';
19
+ import logger from './services/LoggingService.js';
20
+
21
+ // Exception names thrown by pdf.js (used via pdf-parse) when a PDF is
22
+ // malformed, encrypted, or truncated. Matched by error.name because the
23
+ // classes live inside pdfjs-dist and are not exported by pdf-parse.
24
+ const PDFJS_PARSE_ERRORS = new Set([
25
+ 'XRefEntryException',
26
+ 'XRefParseException',
27
+ 'InvalidPDFException',
28
+ 'MissingPDFException',
29
+ 'PasswordException',
30
+ 'FormatError',
31
+ ]);
32
+
33
+ /**
34
+ * Arela Uploader CLI
35
+ * Professional file uploader with document detection and organization
36
+ */
37
+ class ArelaUploaderCLI {
38
+ constructor() {
39
+ this.program = new Command();
40
+ this.errorHandler = new ErrorHandler(logger);
41
+ this.identifyCommand = identifyCommand;
42
+ this.scanCommand = scanCommand;
43
+ this.uploadCommand = new UploadCommand();
44
+ this.watchCommand = watchCommand;
45
+ this.datastageCommand = datastageCommand;
46
+
47
+ this.#setupProgram();
48
+ this.#setupCommands();
49
+ this.#setupErrorHandling();
50
+ }
51
+
52
+ /**
53
+ * Setup the main program configuration
54
+ * @private
55
+ */
56
+ #setupProgram() {
57
+ this.program
58
+ .name('arela')
59
+ .description(
60
+ 'CLI to upload files/directories to Arela with automatic processing',
61
+ )
62
+ .version(appConfig.packageVersion)
63
+ .option('-v, --verbose', 'Enable verbose logging')
64
+ .option('--clear-log', 'Clear the log file before starting');
65
+ }
66
+
67
+ /**
68
+ * Setup CLI commands
69
+ * @private
70
+ */
71
+ #setupCommands() {
72
+ // Main upload command
73
+ this.program
74
+ .command('upload')
75
+ .description('Upload files to Arela with automatic processing')
76
+ .option(
77
+ '--api <target>',
78
+ 'API target: default|agencia|cliente',
79
+ 'default',
80
+ )
81
+ .option(
82
+ '--source-api <target>',
83
+ 'Source API for reading data (cross-tenant mode): agencia|cliente',
84
+ )
85
+ .option(
86
+ '--target-api <target>',
87
+ 'Target API for uploading files (cross-tenant mode): agencia|cliente',
88
+ )
89
+ .option(
90
+ '-b, --batch-size <size>',
91
+ 'Number of files to process in each batch',
92
+ '10',
93
+ )
94
+ .option('-p, --prefix <prefix>', 'Prefix for uploaded files')
95
+ .option(
96
+ '--folder-structure <structure>',
97
+ 'Custom folder structure for organization',
98
+ )
99
+ .option('--client-path <path>', 'Override client path for metadata')
100
+ .option(
101
+ '--auto-detect-structure',
102
+ 'Automatically detect folder structure from file paths',
103
+ )
104
+ .option('--auto-detect', 'Enable automatic document type detection')
105
+ .option('--auto-organize', 'Enable automatic file organization')
106
+ .option('--force-supabase', 'Force direct Supabase upload mode')
107
+ .option('--skip-processed', 'Skip files that have already been processed')
108
+ .option('--show-stats', 'Show performance statistics')
109
+ .option(
110
+ '--upload-by-rfc',
111
+ 'Upload files based on specific RFC values from UPLOAD_RFCS',
112
+ )
113
+ .option(
114
+ '--run-all-phases',
115
+ 'Run all processing phases (stats, detection, organization)',
116
+ )
117
+ .action(async (options) => {
118
+ try {
119
+ // Handle cross-tenant mode (source and target APIs)
120
+ if (options.sourceApi && options.targetApi) {
121
+ appConfig.setCrossTenantTargets(
122
+ options.sourceApi,
123
+ options.targetApi,
124
+ );
125
+ } else if (options.api && options.api !== 'default') {
126
+ // Set single API target if specified
127
+ appConfig.setApiTarget(options.api);
128
+ }
129
+
130
+ // Handle --upload-by-rfc as a specific operation
131
+ if (options.uploadByRfc) {
132
+ const databaseService = await import(
133
+ './services/DatabaseService.js'
134
+ );
135
+ console.log('🎯 Running RFC-based upload...');
136
+ const result = await databaseService.default.uploadFilesByRfc({
137
+ batchSize: parseInt(options.batchSize) || 10,
138
+ showProgress: true,
139
+ folderStructure: options.folderStructure,
140
+ apiTarget: options.api,
141
+ sourceApi: options.sourceApi,
142
+ targetApi: options.targetApi,
143
+ });
144
+ console.log(
145
+ `βœ… RFC upload completed: ${result.processedCount} processed, ${result.uploadedCount} uploaded, ${result.errorCount} errors`,
146
+ );
147
+ return;
148
+ }
149
+
150
+ await this.uploadCommand.execute(options);
151
+ } catch (error) {
152
+ this.errorHandler.handleFatalError(error, { command: 'upload' });
153
+ }
154
+ });
155
+
156
+ // Stats-only command
157
+ this.program
158
+ .command('stats')
159
+ .description('Collect file statistics without uploading')
160
+ .option(
161
+ '--api <target>',
162
+ 'API target: agencia|cliente|default',
163
+ 'default',
164
+ )
165
+ .option(
166
+ '-b, --batch-size <size>',
167
+ 'Number of files to process in each batch',
168
+ '10',
169
+ )
170
+ .option('--client-path <path>', 'Override client path for metadata')
171
+ .option(
172
+ '--stats-only',
173
+ 'Collect file statistics without uploading (backward compatibility)',
174
+ )
175
+ .option(
176
+ '--run-all-phases',
177
+ 'Run all processing phases (stats, detection, organization)',
178
+ )
179
+ .option('--show-stats', 'Show performance statistics')
180
+ .action(async (options) => {
181
+ try {
182
+ // Set API target if specified
183
+ if (options.api && options.api !== 'default') {
184
+ appConfig.setApiTarget(options.api);
185
+ }
186
+ const statsOptions = { ...options, statsOnly: true };
187
+ await this.uploadCommand.execute(statsOptions);
188
+ } catch (error) {
189
+ this.errorHandler.handleFatalError(error, { command: 'stats' });
190
+ }
191
+ });
192
+
193
+ // Scan command (optimized stats collection with streaming)
194
+ this.program
195
+ .command('scan')
196
+ .description(
197
+ 'Scan filesystem and collect file statistics (optimized with streaming)',
198
+ )
199
+ .option(
200
+ '--api <target>',
201
+ 'API target: agencia|cliente|default',
202
+ 'default',
203
+ )
204
+ .option(
205
+ '--count-first',
206
+ 'Count files first for percentage-based progress (slower start)',
207
+ )
208
+ .option(
209
+ '--no-stream',
210
+ 'Use synchronous file discovery instead of streaming (original approach)',
211
+ )
212
+ .action(async (options) => {
213
+ try {
214
+ // Set API target if specified
215
+ if (options.api && options.api !== 'default') {
216
+ appConfig.setApiTarget(options.api);
217
+ }
218
+ await this.scanCommand.execute(options);
219
+ } catch (error) {
220
+ this.errorHandler.handleFatalError(error, { command: 'scan' });
221
+ }
222
+ });
223
+
224
+ // Datastage command β€” upload monthly datastage *.zip files from a directory
225
+ this.program
226
+ .command('datastage')
227
+ .description(
228
+ 'πŸ“¦ Upload monthly datastage *.zip files from a directory to Arela',
229
+ )
230
+ .requiredOption(
231
+ '--dir <path>',
232
+ 'Directory containing *.zip files (non-recursive)',
233
+ )
234
+ .option(
235
+ '--api <target>',
236
+ 'API target: default|agencia|cliente',
237
+ 'default',
238
+ )
239
+ .option('--retry-failed', 'Re-attempt files in failed status')
240
+ .option('--show-stats', 'Print final stats summary')
241
+ .action(async (options) => {
242
+ try {
243
+ if (options.api && options.api !== 'default') {
244
+ appConfig.setApiTarget(options.api);
245
+ }
246
+ await this.datastageCommand.execute(options);
247
+ } catch (error) {
248
+ this.errorHandler.handleFatalError(error, { command: 'datastage' });
249
+ }
250
+ });
251
+
252
+ // Detection command
253
+ this.program
254
+ .command('detect')
255
+ .description('Run document detection on existing file records')
256
+ .option(
257
+ '--api <target>',
258
+ 'API target: agencia|cliente|default',
259
+ 'default',
260
+ )
261
+ .option(
262
+ '-b, --batch-size <size>',
263
+ 'Number of files to process in each batch',
264
+ '10',
265
+ )
266
+ .option(
267
+ '--detect-pdfs',
268
+ 'Run PDF detection on existing database records (backward compatibility)',
269
+ )
270
+ .option(
271
+ '--propagate-arela-path',
272
+ 'Propagate arela_path from pedimento records to related files',
273
+ )
274
+ .action(async (options) => {
275
+ try {
276
+ // Set API target if specified
277
+ if (options.api && options.api !== 'default') {
278
+ appConfig.setApiTarget(options.api);
279
+ }
280
+
281
+ const databaseService = await import('./services/DatabaseService.js');
282
+
283
+ // Handle --propagate-arela-path as a specific operation
284
+ if (options.propagateArelaPath) {
285
+ console.log('πŸ”„ Running arela_path propagation...');
286
+ const result = await databaseService.default.propagateArelaPath({
287
+ showProgress: true,
288
+ });
289
+ console.log(
290
+ `βœ… Propagation completed: ${result.processedCount} processed, ${result.updatedCount} updated, ${result.errorCount} errors`,
291
+ );
292
+ return;
293
+ }
294
+
295
+ // Default behavior: run PDF detection
296
+ console.log(
297
+ 'πŸ” Running PDF detection on existing database records...',
298
+ );
299
+ const result =
300
+ await databaseService.default.detectPedimentosInDatabase({
301
+ batchSize: parseInt(options.batchSize) || 10,
302
+ });
303
+ console.log(
304
+ `βœ… Detection completed: ${result.detectedCount} detected, ${result.processedCount} processed, ${result.errorCount} errors`,
305
+ );
306
+ } catch (error) {
307
+ this.errorHandler.handleFatalError(error, { command: 'detect' });
308
+ }
309
+ });
310
+
311
+ // Configuration command
312
+ this.program
313
+ .command('config')
314
+ .description('Show current configuration')
315
+ .action(() => {
316
+ try {
317
+ this.#showConfiguration();
318
+ } catch (error) {
319
+ this.errorHandler.handleFatalError(error, { command: 'config' });
320
+ }
321
+ });
322
+
323
+ // Query command for inspection
324
+ this.program
325
+ .command('query')
326
+ .description('Query database for file status and information')
327
+ .option(
328
+ '--ready-files',
329
+ 'Show files that are ready for upload (detected but not uploaded)',
330
+ )
331
+ .action(async (options) => {
332
+ try {
333
+ const databaseService = await import('./services/DatabaseService.js');
334
+
335
+ if (options.readyFiles) {
336
+ console.log('πŸ” Querying files ready for upload...');
337
+
338
+ const readyFiles =
339
+ await databaseService.default.getFilesReadyForUpload();
340
+
341
+ if (readyFiles.length === 0) {
342
+ console.log('ℹ️ No files are currently ready for upload');
343
+ console.log(
344
+ ' Tip: Run "arela detect" and "arela detect --propagate-arela-path" first to prepare files for upload',
345
+ );
346
+ } else {
347
+ console.log(
348
+ `\nπŸ“‹ ${readyFiles.length} files are ready for upload!`,
349
+ );
350
+ console.log(
351
+ ' Use "arela upload --upload-by-rfc" to upload them to Arela API',
352
+ );
353
+ }
354
+ return;
355
+ }
356
+
357
+ // Default behavior: show help for query command
358
+ console.log('Available query options:');
359
+ console.log(' --ready-files Show files ready for upload');
360
+ } catch (error) {
361
+ this.errorHandler.handleFatalError(error, { command: 'query' });
362
+ }
363
+ });
364
+
365
+ // ============================================================================
366
+ // NEW SIMPLIFIED COMMANDS (Optimized versions with better naming)
367
+ // ============================================================================
368
+
369
+ // Identify command - simplified version of "detect --detect-pdfs"
370
+ this.program
371
+ .command('identify')
372
+ .description('πŸ” Identify document types using matchers (optimized)')
373
+ .option(
374
+ '--api <target>',
375
+ 'API target: agencia|cliente|default',
376
+ 'default',
377
+ )
378
+ .option(
379
+ '-b, --batch-size <size>',
380
+ 'Number of files to process in each batch',
381
+ '100',
382
+ )
383
+ .option(
384
+ '--table <tableName>',
385
+ 'Process only this scan table (instead of all instance tables)',
386
+ )
387
+ .option(
388
+ '--reset-attempts',
389
+ 'Reset detection_attempts to 0 before processing so previously-failed files are retried',
390
+ )
391
+ .option(
392
+ '--path-prefix <mapping>',
393
+ 'Remap file path prefix for cross-platform access. Format: FROM:TO e.g. "O:/=/Volumes/nas/"',
394
+ )
395
+ .option('--show-stats', 'Show performance statistics')
396
+ .action(async (options) => {
397
+ try {
398
+ await this.identifyCommand.execute(options);
399
+ } catch (error) {
400
+ this.errorHandler.handleFatalError(error, { command: 'identify' });
401
+ }
402
+ });
403
+
404
+ // Propagate command - simplified version of "detect --propagate-arela-path"
405
+ this.program
406
+ .command('propagate')
407
+ .description(
408
+ 'πŸ”„ Propagate arela_path from pedimentos to related files (optimized)',
409
+ )
410
+ .option(
411
+ '--api <target>',
412
+ 'API target: agencia|cliente|default',
413
+ 'default',
414
+ )
415
+ .option(
416
+ '-b, --batch-size <size>',
417
+ 'Number of pedimentos to process per batch',
418
+ '50',
419
+ )
420
+ .option('--show-stats', 'Show performance statistics')
421
+ .action(async (options) => {
422
+ try {
423
+ const propagateCommand = new PropagateCommand(options);
424
+ await propagateCommand.execute();
425
+ } catch (error) {
426
+ this.errorHandler.handleFatalError(error, { command: 'propagate' });
427
+ }
428
+ });
429
+
430
+ // Push command - simplified version of "upload --upload-by-rfc"
431
+ this.program
432
+ .command('push')
433
+ .description('πŸ“€ Upload files by RFC to Arela API (optimized)')
434
+ .option(
435
+ '--api <target>',
436
+ 'API target for scan operations: default|agencia|cliente',
437
+ 'default',
438
+ )
439
+ .option(
440
+ '--scan-api <target>',
441
+ 'API for reading scan table: default|agencia|cliente',
442
+ 'default',
443
+ )
444
+ .option(
445
+ '--push-api <target>',
446
+ 'API for uploading files: default|agencia|cliente',
447
+ )
448
+ .option(
449
+ '--source-api <target>',
450
+ 'Source API for reading data (cross-tenant mode): agencia|cliente',
451
+ )
452
+ .option(
453
+ '--target-api <target>',
454
+ 'Target API for uploading files (cross-tenant mode): agencia|cliente',
455
+ )
456
+ .option(
457
+ '-b, --batch-size <size>',
458
+ 'Number of files to fetch per batch',
459
+ '100',
460
+ )
461
+ .option(
462
+ '--upload-batch-size <size>',
463
+ 'Number of files to upload concurrently',
464
+ '10',
465
+ )
466
+ .option(
467
+ '--rfcs <rfcs>',
468
+ 'Comma-separated RFCs to filter (overrides PUSH_RFCS env var)',
469
+ )
470
+ .option(
471
+ '--years <years>',
472
+ 'Comma-separated years to filter (overrides PUSH_YEARS env var)',
473
+ )
474
+ .option(
475
+ '--folder-structure <path>',
476
+ 'Storage path prefix (overrides PUSH_FOLDER_STRUCTURE env var)',
477
+ )
478
+ .option('--no-auto-organize', 'Disable automatic file organization')
479
+ .option('--show-stats', 'Show performance statistics')
480
+ .action(async (options) => {
481
+ try {
482
+ // Handle cross-tenant mode (source and target APIs)
483
+ // Map source-api/target-api to scan-api/push-api for consistency
484
+ if (options.sourceApi && options.targetApi) {
485
+ appConfig.setCrossTenantTargets(
486
+ options.sourceApi,
487
+ options.targetApi,
488
+ );
489
+ // Also set scan-api and push-api for PushCommand compatibility
490
+ options.scanApi = options.sourceApi;
491
+ options.pushApi = options.targetApi;
492
+ }
493
+
494
+ // Parse comma-separated values
495
+ if (options.rfcs) {
496
+ options.rfcs = options.rfcs
497
+ .split(',')
498
+ .map((r) => r.trim().toUpperCase())
499
+ .filter(Boolean);
500
+ }
501
+ if (options.years) {
502
+ options.years = options.years
503
+ .split(',')
504
+ .map((y) => parseInt(y.trim(), 10))
505
+ .filter((y) => !isNaN(y));
506
+ }
507
+
508
+ const pushCommand = new PushCommand();
509
+ await pushCommand.execute(options);
510
+ } catch (error) {
511
+ this.errorHandler.handleFatalError(error, { command: 'push' });
512
+ }
513
+ });
514
+
515
+ // ============================================================================
516
+ // END OF NEW SIMPLIFIED COMMANDS
517
+ // ============================================================================
518
+
519
+ // GDrive sync command - mirror a Google Drive folder to local before scan
520
+ this.program
521
+ .command('gdrive-sync')
522
+ .description(
523
+ '☁️ Mirror a Google Drive folder to local filesystem (pre-scan source)',
524
+ )
525
+ .option(
526
+ '--root-folder <id>',
527
+ 'Drive folder ID to sync (overrides GDRIVE_ROOT_FOLDER_ID)',
528
+ )
529
+ .option(
530
+ '--dest <path>',
531
+ 'Local mirror destination (overrides GDRIVE_LOCAL_MIRROR_PATH)',
532
+ )
533
+ .option('--full', 'Ignore state file and re-verify all files')
534
+ .option('--dry-run', 'List/plan only, no downloads or writes')
535
+ .action(async (options) => {
536
+ try {
537
+ await gdriveSyncCommand.execute(options);
538
+ } catch (error) {
539
+ this.errorHandler.handleFatalError(error, {
540
+ command: 'gdrive-sync',
541
+ });
542
+ }
543
+ });
544
+
545
+ // Watch command
546
+ this.program
547
+ .command('watch')
548
+ .description(
549
+ 'Monitor directories for file changes and upload automatically',
550
+ )
551
+ .option(
552
+ '--api <target>',
553
+ 'API target: default|agencia|cliente',
554
+ 'default',
555
+ )
556
+ .option(
557
+ '--source-api <target>',
558
+ 'Source API for reading data (cross-tenant mode): agencia|cliente',
559
+ )
560
+ .option(
561
+ '--target-api <target>',
562
+ 'Target API for uploading files (cross-tenant mode): agencia|cliente',
563
+ )
564
+ .option(
565
+ '-d, --directories <paths>',
566
+ 'Comma-separated directories to watch',
567
+ )
568
+ .option(
569
+ '-s, --strategy <strategy>',
570
+ 'Upload strategy: individual|batch|full-structure',
571
+ 'batch',
572
+ )
573
+ .option('--debounce <ms>', 'Debounce delay in milliseconds', '1000')
574
+ .option(
575
+ '-b, --batch-size <size>',
576
+ 'Number of files to process in each batch',
577
+ '10',
578
+ )
579
+ .option(
580
+ '--poll <ms>',
581
+ 'Use polling instead of native file system events (interval in ms)',
582
+ )
583
+ .option('--ignore <patterns>', 'Comma-separated patterns to ignore')
584
+ .option('--auto-detect', 'Enable automatic document type detection')
585
+ .option('--auto-organize', 'Enable automatic file organization')
586
+ .option(
587
+ '--auto-processing',
588
+ 'Enable automatic 4-step pipeline (stats, detect, propagate, upload)',
589
+ )
590
+ .option('--dry-run', 'Simulate changes without uploading')
591
+ .option('--verbose', 'Enable verbose logging')
592
+ .action(async (options) => {
593
+ try {
594
+ // Handle cross-tenant mode (source and target APIs)
595
+ if (options.sourceApi && options.targetApi) {
596
+ appConfig.setCrossTenantTargets(
597
+ options.sourceApi,
598
+ options.targetApi,
599
+ );
600
+ } else if (options.api && options.api !== 'default') {
601
+ // Set single API target if specified
602
+ appConfig.setApiTarget(options.api);
603
+ }
604
+
605
+ await this.watchCommand.execute(options);
606
+ } catch (error) {
607
+ this.errorHandler.handleFatalError(error, { command: 'watch' });
608
+ }
609
+ });
610
+
611
+ // ============================================================================
612
+ // WORKER MODE - BullMQ job processor
613
+ // ============================================================================
614
+
615
+ // Worker command - process jobs from BullMQ queues
616
+ this.program
617
+ .command('worker')
618
+ .description('πŸ”§ Run as BullMQ worker to process pipeline jobs from UI')
619
+ .option(
620
+ '--queues <queues>',
621
+ 'Comma-separated queues to listen to (default: all pipeline queues)',
622
+ )
623
+ .option(
624
+ '-c, --concurrency <number>',
625
+ 'Number of concurrent jobs per queue',
626
+ '1',
627
+ )
628
+ .option(
629
+ '--poll',
630
+ 'Use HTTP polling mode instead of BullMQ (for environments without Redis)',
631
+ )
632
+ .option(
633
+ '--api <target>',
634
+ 'API target for polling (e.g., agencia, cliente, ktj)',
635
+ 'agencia',
636
+ )
637
+ .action(async (options) => {
638
+ try {
639
+ if (options.poll) {
640
+ // HTTP polling mode - for Windows Server or environments without Redis
641
+ await pollWorkerCommand.execute(options);
642
+ } else {
643
+ // BullMQ mode - default for environments with Redis
644
+ await workerCommand.execute(options);
645
+ }
646
+ } catch (error) {
647
+ this.errorHandler.handleFatalError(error, { command: 'worker' });
648
+ }
649
+ });
650
+
651
+ // ============================================================================
652
+ // AGENT MODE - one process polling N tenant APIs (replaces N worker terminals)
653
+ // ============================================================================
654
+
655
+ const agent = this.program
656
+ .command('agent')
657
+ .description(
658
+ 'πŸ€– Multi-profile polling agent: one process for all tenant APIs (profiles.json)',
659
+ );
660
+
661
+ agent
662
+ .command('run', { isDefault: true })
663
+ .description('Run the agent (default: `arela agent` runs this)')
664
+ .option(
665
+ '--config <path>',
666
+ 'Path to profiles.json (default: ~/.arela/profiles.json)',
667
+ )
668
+ .option('--interval <ms>', 'Base poll interval per idle round')
669
+ .option(
670
+ '--profile <names>',
671
+ 'Comma-separated subset of profiles to run (debugging)',
672
+ )
673
+ .action(async (options) => {
674
+ try {
675
+ await agentCommand.execute(options);
676
+ } catch (error) {
677
+ this.errorHandler.handleFatalError(error, { command: 'agent' });
678
+ }
679
+ });
680
+
681
+ agent
682
+ .command('init')
683
+ .description(
684
+ 'Generate profiles.json + pipeline_config seeds from legacy per-folder .env files',
685
+ )
686
+ .requiredOption(
687
+ '--from <dirs...>',
688
+ 'Folders containing the legacy .env files (one per RFCΓ—source)',
689
+ )
690
+ .option(
691
+ '--server-id <id>',
692
+ 'Machine server id (default: ARELA_SERVER_ID from first .env, else hostname)',
693
+ )
694
+ .option('--out <dir>', 'Output directory', '~/.arela')
695
+ .option('--force', 'Overwrite an existing profiles.json')
696
+ .action(async (options) => {
697
+ try {
698
+ await agentInitCommand.execute(options.from, options);
699
+ } catch (error) {
700
+ this.errorHandler.handleFatalError(error, { command: 'agent init' });
701
+ }
702
+ });
703
+
704
+ // Version command (already handled by program.version())
705
+
706
+ // Help command
707
+ this.program
708
+ .command('help')
709
+ .description('Show help information')
710
+ .action(() => {
711
+ this.program.help();
712
+ });
713
+ }
714
+
715
+ /**
716
+ * Setup global error handling
717
+ * @private
718
+ */
719
+ #setupErrorHandling() {
720
+ // Handle uncaught exceptions
721
+ process.on('uncaughtException', (error) => {
722
+ this.errorHandler.handleFatalError(error, {
723
+ context: 'uncaughtException',
724
+ });
725
+ });
726
+
727
+ // Handle unhandled promise rejections
728
+ process.on('unhandledRejection', (reason, promise) => {
729
+ const error =
730
+ reason instanceof Error ? reason : new Error(String(reason));
731
+
732
+ // pdf.js rejects internal worker promises when a PDF is corrupt; those
733
+ // rejections cannot be caught around the parser call, and one bad file
734
+ // must not abort the rest of the batch.
735
+ if (PDFJS_PARSE_ERRORS.has(error.name)) {
736
+ const file = getCurrentPdfFile();
737
+ console.warn(
738
+ `⚠️ Corrupt or unreadable PDF${file ? ` (${file})` : ''}: ${error.message} β€” skipping`,
739
+ );
740
+ this.errorHandler.handleError(error, {
741
+ context: 'unhandledRejection',
742
+ file,
743
+ });
744
+ return;
745
+ }
746
+
747
+ this.errorHandler.handleFatalError(error, {
748
+ context: 'unhandledRejection',
749
+ promise: promise.toString(),
750
+ });
751
+ });
752
+
753
+ // Handle SIGINT (Ctrl+C)
754
+ process.on('SIGINT', () => {
755
+ console.log('\nπŸ‘‹ Received SIGINT. Gracefully shutting down...');
756
+ logger.info('Application interrupted by user (SIGINT)');
757
+ logger.flush();
758
+ process.exit(0);
759
+ });
760
+
761
+ // Handle SIGTERM
762
+ process.on('SIGTERM', () => {
763
+ console.log('\nπŸ‘‹ Received SIGTERM. Gracefully shutting down...');
764
+ logger.info('Application terminated by system (SIGTERM)');
765
+ logger.flush();
766
+ process.exit(0);
767
+ });
768
+ }
769
+
770
+ /**
771
+ * Show current configuration
772
+ * @private
773
+ */
774
+ #showConfiguration() {
775
+ console.log('πŸ”§ Current Configuration:');
776
+ console.log(` Version: ${appConfig.packageVersion}`);
777
+ console.log('\nπŸ“‘ API Configuration (Multi-Tenant):');
778
+ console.log(` Active Target: ${appConfig.api.activeTarget || 'default'}`);
779
+ console.log('\n 🌐 Default API:');
780
+ console.log(` URL: ${appConfig.api.baseUrl || 'Not configured'}`);
781
+ console.log(
782
+ ` Token: ${appConfig.api.token ? 'βœ… Set' : '❌ Not set'}`,
783
+ );
784
+ console.log('\n 🏒 Agencia API:');
785
+ console.log(
786
+ ` URL: ${appConfig.api.targets.agencia?.baseUrl || 'Not configured'}`,
787
+ );
788
+ console.log(
789
+ ` Token: ${appConfig.api.targets.agencia?.token ? 'βœ… Set' : '❌ Not set'}`,
790
+ );
791
+ console.log('\n πŸ‘€ Cliente API:');
792
+ console.log(
793
+ ` URL: ${appConfig.api.targets.cliente?.baseUrl || 'Not configured'}`,
794
+ );
795
+ console.log(
796
+ ` Token: ${appConfig.api.targets.cliente?.token ? 'βœ… Set' : '❌ Not set'}`,
797
+ );
798
+ console.log('\nπŸ—„οΈ Supabase Configuration:');
799
+ console.log(` URL: ${appConfig.supabase.url || 'Not configured'}`);
800
+ console.log(` Key: ${appConfig.supabase.key ? 'βœ… Set' : '❌ Not set'}`);
801
+ console.log(` Bucket: ${appConfig.supabase.bucket || 'Not configured'}`);
802
+ console.log('\nπŸ“ Upload Configuration:');
803
+ console.log(
804
+ ` Base Path: ${appConfig.upload.basePath || 'Not configured'}`,
805
+ );
806
+ console.log(
807
+ ` Sources: ${appConfig.upload.sources?.join(', ') || 'Not configured'}`,
808
+ );
809
+ console.log(
810
+ ` RFCs: ${appConfig.upload.rfcs?.join(', ') || 'Not configured'}`,
811
+ );
812
+ console.log('\n⚑ Performance Configuration:');
813
+ console.log(` Batch Delay: ${appConfig.performance.batchDelay}ms`);
814
+ console.log(
815
+ ` Progress Update Interval: ${appConfig.performance.progressUpdateInterval}`,
816
+ );
817
+ console.log(` Log Buffer Size: ${appConfig.performance.logBufferSize}`);
818
+ console.log('\nπŸ“ Logging Configuration:');
819
+ console.log(
820
+ ` Verbose: ${appConfig.logging.verbose ? 'βœ… Enabled' : '❌ Disabled'}`,
821
+ );
822
+ console.log(` Log File: ${appConfig.logging.logFilePath}`);
823
+ console.log('\n🎯 Service Availability:');
824
+ console.log(
825
+ ` API Mode (default): ${appConfig.isApiModeAvailable() ? 'βœ… Available' : '❌ Not available'}`,
826
+ );
827
+ console.log(
828
+ ` API Mode (agencia): ${appConfig.isApiModeAvailable('agencia') ? 'βœ… Available' : '❌ Not available'}`,
829
+ );
830
+ console.log(
831
+ ` API Mode (cliente): ${appConfig.isApiModeAvailable('cliente') ? 'βœ… Available' : '❌ Not available'}`,
832
+ );
833
+ console.log(
834
+ ` Supabase Mode: ${appConfig.isSupabaseModeAvailable() ? 'βœ… Available' : '❌ Not available'}`,
835
+ );
836
+ console.log('\nπŸ“‹ Uso Multi-Tenant:');
837
+ console.log(
838
+ ' arela stats --api agencia # Registrar archivos en BD agencia',
839
+ );
840
+ console.log(
841
+ ' arela detect --api agencia # Detectar pedimentos en BD agencia',
842
+ );
843
+ console.log(
844
+ ' arela upload --api cliente # Subir archivos al cliente configurado',
845
+ );
846
+ console.log('\nπŸ‘οΈ Modo Watch Multi-Tenant:');
847
+ console.log(
848
+ ' arela watch --api cliente # Watch con API cliente',
849
+ );
850
+ console.log(
851
+ ' arela watch --source-api agencia --target-api cliente # Cross-tenant watch',
852
+ );
853
+ console.log(
854
+ '\nπŸ’‘ Tip: Configura ARELA_API_CLIENTE_URL y ARELA_API_CLIENTE_TOKEN',
855
+ );
856
+ console.log(' en .env para apuntar al cliente especΓ­fico que necesites.');
857
+ }
858
+
859
+ /**
860
+ * Parse command line arguments and execute
861
+ */
862
+ async run() {
863
+ try {
864
+ // Set verbose mode if requested globally
865
+ const args = process.argv;
866
+ if (args.includes('-v') || args.includes('--verbose')) {
867
+ logger.setVerbose(true);
868
+ }
869
+
870
+ // Clear log if requested globally
871
+ if (args.includes('--clear-log')) {
872
+ logger.clearLogFile();
873
+ logger.info('Log file cleared');
874
+ }
875
+
876
+ // Log application start
877
+ logger.info(`Arela Uploader v${appConfig.packageVersion} started`);
878
+ logger.info(`Command: ${args.slice(2).join(' ')}`);
879
+
880
+ // Parse and execute commands
881
+ await this.program.parseAsync();
882
+ } catch (error) {
883
+ this.errorHandler.handleFatalError(error, { context: 'cli-execution' });
884
+ }
885
+ }
886
+ }
887
+
888
+ // Create and run the CLI application
889
+ const cli = new ArelaUploaderCLI();
890
+ await cli.run();