@arela/uploader 1.1.4 → 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 (95) 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/index.js +55 -0
  92. package/src/services/LoggingService.js +35 -0
  93. package/src/services/PipelineApiService.js +7 -1
  94. package/src/services/PipelineJobRunner.js +389 -0
  95. package/src/services/ProfileManager.js +164 -0
@@ -0,0 +1,743 @@
1
+ import cliProgress from 'cli-progress';
2
+ import FormData from 'form-data';
3
+ import fs from 'fs';
4
+ import fetch from 'node-fetch';
5
+ import path from 'path';
6
+
7
+ import logger from '../services/LoggingService.js';
8
+ import ScanApiService from '../services/ScanApiService.js';
9
+
10
+ import appConfig from '../config/config.js';
11
+
12
+ /**
13
+ * Push Command Handler
14
+ * Uploads files with arela_path to storage API
15
+ */
16
+ export class PushCommand {
17
+ constructor() {
18
+ // ScanApiService will be initialized in execute() with proper API target
19
+ this.scanApiService = null;
20
+ this.onProgress = null; // Progress callback for worker mode
21
+ }
22
+
23
+ /**
24
+ * Execute the push command
25
+ * @param {Object} options - Command options
26
+ * @param {Function} options.onProgress - Optional callback for progress updates (percent, message)
27
+ */
28
+ async execute(options) {
29
+ this.onProgress = options.onProgress || null;
30
+ try {
31
+ console.log('\n🚀 Starting arela push command\n');
32
+
33
+ // Validate scan configuration (same config as scan/identify/propagate)
34
+ const errors = this.#validateConfig();
35
+ if (errors.length > 0) {
36
+ console.error('⚠️ Configuration errors:');
37
+ errors.forEach((err) => console.error(` - ${err}`));
38
+ process.exit(1);
39
+ }
40
+
41
+ // Get configuration
42
+ const scanConfig = appConfig.getScanConfig();
43
+ const pushConfig = appConfig.getPushConfig();
44
+
45
+ // Override folderStructure from command option if provided
46
+ if (options.folderStructure) {
47
+ // Clean the folder structure: remove leading/trailing slashes
48
+ pushConfig.folderStructure = options.folderStructure
49
+ .trim()
50
+ .replace(/^\/+/, '')
51
+ .replace(/\/+$/, '');
52
+ }
53
+
54
+ // Determine API targets for scan (read) and push (upload) operations
55
+ // Priority: explicit scan-api/push-api > source-api/target-api > api > default
56
+ const scanApiTarget =
57
+ options.scanApi || options.sourceApi || options.api || 'default';
58
+ const pushApiTarget =
59
+ options.pushApi || options.targetApi || options.api || scanApiTarget;
60
+
61
+ // Initialize ScanApiService with the scan API target
62
+ this.scanApiService = new ScanApiService(scanApiTarget);
63
+
64
+ // Get upload API configuration for the push target
65
+ const uploadApiConfig = appConfig.getApiConfig(pushApiTarget);
66
+
67
+ // Display API configuration
68
+ const isCrossTenant = scanApiTarget !== pushApiTarget;
69
+ if (isCrossTenant) {
70
+ console.log('🔗 Cross-tenant mode enabled:');
71
+ console.log(
72
+ ` 📖 Scan API (read): ${scanApiTarget} → ${appConfig.getApiConfig(scanApiTarget).baseUrl}`,
73
+ );
74
+ console.log(
75
+ ` 📝 Push API (upload): ${pushApiTarget} → ${uploadApiConfig.baseUrl}`,
76
+ );
77
+ } else {
78
+ console.log(
79
+ `🎯 API Target: ${scanApiTarget} → ${uploadApiConfig.baseUrl}`,
80
+ );
81
+ }
82
+ console.log(`📦 Fetch Batch Size: ${options.batchSize}`);
83
+ console.log(`📤 Upload Batch Size: ${options.uploadBatchSize}`);
84
+ if (pushConfig.folderStructure) {
85
+ console.log(
86
+ `📁 Folder Structure Prefix: ${pushConfig.folderStructure}`,
87
+ );
88
+ }
89
+
90
+ // Apply filters
91
+ const filters = {
92
+ rfcs: options.rfcs || pushConfig.rfcs || [],
93
+ years: options.years || pushConfig.years || [],
94
+ };
95
+
96
+ if (filters.rfcs.length > 0) {
97
+ console.log(`🔍 RFC Filter: ${filters.rfcs.join(', ')}`);
98
+ }
99
+ if (filters.years.length > 0) {
100
+ console.log(`🔍 Year Filter: ${filters.years.join(', ')}`);
101
+ }
102
+
103
+ // Fetch all tables for this instance
104
+ console.log('\\n📊 Fetching instance tables...');
105
+ const tables = await this.scanApiService.getInstanceTables(
106
+ scanConfig.companySlug,
107
+ scanConfig.serverId,
108
+ scanConfig.basePathFull,
109
+ );
110
+
111
+ if (tables.length === 0) {
112
+ console.error(
113
+ '\\n❌ No tables found for this instance. Run \"arela scan\" first.\\n',
114
+ );
115
+ process.exit(1);
116
+ }
117
+
118
+ console.log(
119
+ `📋 Found ${tables.length} table${tables.length === 1 ? '' : 's'} to process:`,
120
+ );
121
+ for (const table of tables) {
122
+ console.log(` - ${table.tableName}`);
123
+ }
124
+
125
+ // Process each table
126
+ let totalResults = {
127
+ processed: 0,
128
+ uploaded: 0,
129
+ errors: 0,
130
+ startTime: Date.now(),
131
+ };
132
+
133
+ // Report initial progress
134
+ this.#reportProgress(0, `Starting push on ${tables.length} tables`);
135
+
136
+ for (let i = 0; i < tables.length; i++) {
137
+ const table = tables[i];
138
+ console.log(`\\n🚀 Processing table: ${table.tableName}\\n`);
139
+ this.#reportProgress(
140
+ Math.round((i / tables.length) * 100),
141
+ `Processing table ${i + 1}/${tables.length}: ${table.tableName}`,
142
+ );
143
+
144
+ // Get initial statistics for this table
145
+ const initialStats = await this.scanApiService.getPushStats(
146
+ table.tableName,
147
+ );
148
+ this.#displayStats(' Table Status', initialStats);
149
+
150
+ if (initialStats.pending === 0) {
151
+ console.log(' ✅ No files pending upload. Skipping.\\n');
152
+ continue;
153
+ }
154
+
155
+ console.log(
156
+ `\\n 🚀 Uploading ${initialStats.pending} pending files...\\n`,
157
+ );
158
+
159
+ // Process files for this table
160
+ const results = await this.#processFiles(
161
+ table.tableName,
162
+ filters,
163
+ parseInt(options.batchSize),
164
+ parseInt(options.uploadBatchSize),
165
+ uploadApiConfig,
166
+ options,
167
+ );
168
+
169
+ totalResults.processed += results.processed;
170
+ totalResults.uploaded += results.uploaded;
171
+ totalResults.errors += results.errors;
172
+
173
+ // Display results for this table
174
+ console.log('\\n 📊 Table Results:');
175
+ console.log(` Files Processed: ${results.processed}`);
176
+ console.log(` Uploaded: ${results.uploaded}`);
177
+ console.log(` Errors: ${results.errors}\\n`);
178
+ }
179
+
180
+ // Display combined results
181
+ const duration = ((Date.now() - totalResults.startTime) / 1000).toFixed(
182
+ 1,
183
+ );
184
+ const speed =
185
+ totalResults.processed > 0
186
+ ? (totalResults.processed / parseFloat(duration)).toFixed(0)
187
+ : 0;
188
+
189
+ // Report completion
190
+ this.#reportProgress(
191
+ 100,
192
+ `Push completed: ${totalResults.uploaded} files uploaded`,
193
+ );
194
+
195
+ console.log('\\n✅ Push Complete!\\n');
196
+ console.log('📊 Total Results:');
197
+ console.log(` Tables Processed: ${tables.length}`);
198
+ console.log(` Files Processed: ${totalResults.processed}`);
199
+ console.log(` Uploaded: ${totalResults.uploaded}`);
200
+ console.log(` Errors: ${totalResults.errors}`);
201
+ console.log(` Duration: ${duration}s`);
202
+ console.log(` Speed: ${speed} files/sec\\n`);
203
+ } catch (error) {
204
+ console.error('\n❌ Push failed:', error.message);
205
+ logger.error('Push command error:', error);
206
+ process.exit(1);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Validate configuration
212
+ * @private
213
+ * @returns {string[]} Array of error messages
214
+ */
215
+ #validateConfig() {
216
+ const errors = [];
217
+
218
+ try {
219
+ appConfig.validateScanConfig();
220
+ } catch (err) {
221
+ return [err.message];
222
+ }
223
+
224
+ const scanConfig = appConfig.getScanConfig();
225
+ // When basePathFull is '*' (cross-directory wildcard), tableName is intentionally null.
226
+ // The push command fetches tables dynamically via getInstanceTables, so a static
227
+ // tableName is not required — only companySlug, serverId, and basePathFull matter.
228
+ if (!scanConfig.tableName && scanConfig.basePathFull !== '*') {
229
+ errors.push('Could not generate table name from configuration');
230
+ }
231
+
232
+ return errors;
233
+ }
234
+
235
+ /**
236
+ * Display statistics
237
+ * @private
238
+ */
239
+ #displayStats(title, stats) {
240
+ console.log(`\n📈 ${title}:`);
241
+ console.log(` Total with arela_path: ${stats.totalWithArelaPath}`);
242
+ console.log(` Uploaded: ${stats.uploaded}`);
243
+ console.log(` Pending: ${stats.pending}`);
244
+ if (stats.errors > 0) {
245
+ console.log(` Errors: ${stats.errors}`);
246
+ }
247
+ if (stats.maxAttemptsReached > 0) {
248
+ console.log(` Max Attempts Reached: ${stats.maxAttemptsReached}`);
249
+ }
250
+
251
+ // Show top RFCs if available
252
+ if (stats.byRfc && stats.byRfc.length > 0) {
253
+ console.log('\n 📊 Top RFCs:');
254
+ stats.byRfc.slice(0, 5).forEach((rfc) => {
255
+ const percent = ((rfc.uploaded / rfc.total) * 100).toFixed(1);
256
+ console.log(
257
+ ` ${rfc.rfc}: ${rfc.uploaded}/${rfc.total} (${percent}%)`,
258
+ );
259
+ });
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Process files in batches
265
+ * @private
266
+ */
267
+ async #processFiles(
268
+ tableName,
269
+ filters,
270
+ batchSize,
271
+ uploadBatchSize,
272
+ uploadApiConfig,
273
+ options = {},
274
+ ) {
275
+ const results = {
276
+ processed: 0,
277
+ uploaded: 0,
278
+ errors: 0,
279
+ startTime: Date.now(),
280
+ };
281
+
282
+ // Get total count first for accurate progress (filtered by RFC/year)
283
+ const initialStats = await this.scanApiService.getPushStats(
284
+ tableName,
285
+ filters,
286
+ );
287
+ const totalToProcess = initialStats.pending;
288
+
289
+ let hasMore = true;
290
+
291
+ // Create progress bar with known total
292
+ const progressBar = new cliProgress.SingleBar({
293
+ format:
294
+ '📤 |{bar}| {percentage}% | {value}/{total} | {speed}/s | ✓{uploaded} ✗{errors}',
295
+ barCompleteChar: '█',
296
+ barIncompleteChar: '░',
297
+ hideCursor: true,
298
+ clearOnComplete: false,
299
+ stopOnComplete: true,
300
+ });
301
+
302
+ // Always use offset=0 because uploaded files are removed from pending query results
303
+ // After each batch upload, those files are no longer "pending", so the next query
304
+ // at offset=0 will naturally return the next batch of unprocessed files
305
+
306
+ // Track seen file IDs to detect infinite loops (scan table update failures)
307
+ const seenFileIds = new Set();
308
+ let consecutiveRepeats = 0;
309
+ const MAX_CONSECUTIVE_REPEATS = 3;
310
+
311
+ // Start progress bar with known total
312
+ progressBar.start(totalToProcess, 0, {
313
+ speed: 0,
314
+ uploaded: 0,
315
+ errors: 0,
316
+ });
317
+
318
+ while (hasMore) {
319
+ // Fetch batch of files (always offset=0 since uploaded files are removed from pending)
320
+ const files = await this.scanApiService.fetchFilesForPush(tableName, {
321
+ ...filters,
322
+ offset: 0,
323
+ limit: batchSize,
324
+ });
325
+
326
+ if (files.length === 0) {
327
+ hasMore = false;
328
+ break;
329
+ }
330
+
331
+ // Infinite loop protection: if the same files keep coming back,
332
+ // the scan table update is failing and they stay "pending" forever.
333
+ const allSeen = files.every((f) => seenFileIds.has(f.id));
334
+ if (allSeen) {
335
+ consecutiveRepeats++;
336
+ if (consecutiveRepeats >= MAX_CONSECUTIVE_REPEATS) {
337
+ const msg = `Aborting: same ${files.length} files returned ${MAX_CONSECUTIVE_REPEATS} times — scan table updates are likely failing.`;
338
+ logger.error(msg);
339
+ console.error(`\n⚠ ${msg}`);
340
+ hasMore = false;
341
+ break;
342
+ }
343
+ } else {
344
+ consecutiveRepeats = 0;
345
+ }
346
+ files.forEach((f) => seenFileIds.add(f.id));
347
+
348
+ // Upload files in smaller batches using new CLI upload endpoint
349
+ for (let i = 0; i < files.length; i += uploadBatchSize) {
350
+ const uploadBatch = files.slice(i, i + uploadBatchSize);
351
+ const batchResults = await this.#uploadBatchViaCli(
352
+ tableName,
353
+ uploadBatch,
354
+ uploadApiConfig,
355
+ options,
356
+ );
357
+
358
+ // Update counters from API response
359
+ batchResults.forEach((result) => {
360
+ results.processed++;
361
+ if (result.uploaded) {
362
+ results.uploaded++;
363
+ } else {
364
+ results.errors++;
365
+ }
366
+ });
367
+
368
+ // Update scan table via scanApi (supports cross-tenant mode)
369
+ // This is called on scanApi, not pushApi, because scan tables live on scanApi
370
+ try {
371
+ await this.scanApiService.batchUpdateUpload(tableName, batchResults);
372
+ } catch (updateError) {
373
+ const msg = `Failed to update scan table for batch: ${updateError.message}`;
374
+ logger.error(msg);
375
+ console.error(`\n⚠ ${msg}`);
376
+ // Don't fail the entire process, just log the error
377
+ }
378
+
379
+ // Update progress bar
380
+ const elapsed = (Date.now() - results.startTime) / 1000;
381
+ const speed =
382
+ elapsed > 0 ? (results.processed / elapsed).toFixed(0) : 0;
383
+ progressBar.update(results.processed, {
384
+ speed,
385
+ uploaded: results.uploaded,
386
+ errors: results.errors,
387
+ });
388
+ }
389
+
390
+ // Check if there are more files
391
+ // If we got fewer files than requested, we've processed all pending files
392
+ if (files.length < batchSize) {
393
+ hasMore = false;
394
+ }
395
+ }
396
+
397
+ progressBar.stop();
398
+
399
+ return results;
400
+ }
401
+
402
+ /**
403
+ * Upload a batch of files using the new CLI upload endpoint
404
+ * Note: Scan table is updated separately via scanApi after this batch completes
405
+ * @private
406
+ */
407
+ async #uploadBatchViaCli(tableName, files, uploadApiConfig, options = {}) {
408
+ const pushConfig = appConfig.getPushConfig();
409
+ const results = [];
410
+
411
+ // Process files one by one (simpler for now, can optimize to true batch later)
412
+ for (const file of files) {
413
+ const result = await this.#uploadFileViaCli(
414
+ tableName,
415
+ file,
416
+ uploadApiConfig,
417
+ pushConfig,
418
+ options,
419
+ );
420
+ results.push(result);
421
+ }
422
+
423
+ return results;
424
+ }
425
+
426
+ /**
427
+ * Upload a single file using the CLI upload endpoint
428
+ * @private
429
+ */
430
+ async #uploadFileViaCli(
431
+ tableName,
432
+ file,
433
+ uploadApiConfig,
434
+ pushConfig,
435
+ options = {},
436
+ ) {
437
+ const result = {
438
+ id: file.id,
439
+ uploaded: false,
440
+ uploadError: null,
441
+ uploadPath: null,
442
+ uploadedToStorageId: null,
443
+ };
444
+
445
+ try {
446
+ // Check if file exists
447
+ if (!fs.existsSync(file.absolute_path)) {
448
+ result.uploadError =
449
+ 'FILE_NOT_FOUND: File does not exist on filesystem';
450
+ return result;
451
+ }
452
+
453
+ // Get file stats
454
+ const stats = fs.statSync(file.absolute_path);
455
+ if (!stats.isFile()) {
456
+ result.uploadError = 'NOT_A_FILE: Path is not a regular file';
457
+ return result;
458
+ }
459
+
460
+ // Construct upload path using arela_path
461
+ // arela_path format: RFC/Year/Patente/Aduana/Pedimento/
462
+ const uploadPath = `${file.arela_path}${file.file_name}`;
463
+ result.uploadPath = uploadPath;
464
+
465
+ // Create form data for CLI upload endpoint
466
+ const form = new FormData();
467
+
468
+ // Build folder structure: optionally prefix with PUSH_FOLDER_STRUCTURE env var
469
+ // arela_path already contains the logical path (RFC/Year/Patente/Aduana/Pedimento)
470
+ // folderStructure from config is the bucket prefix (e.g., "documents" or "uploads/2024")
471
+ let arelaPath = file.arela_path.endsWith('/')
472
+ ? file.arela_path.slice(0, -1)
473
+ : file.arela_path;
474
+
475
+ // Prepend folder structure prefix if configured
476
+ const folderStructure = pushConfig.folderStructure
477
+ ? `${pushConfig.folderStructure}/${arelaPath}`
478
+ : arelaPath;
479
+
480
+ // Create a read stream with the original filename (no encoding needed)
481
+ const fileStream = fs.createReadStream(file.absolute_path);
482
+ form.append('files', fileStream, {
483
+ filename: file.file_name,
484
+ contentType: this.#getMimeType(file.file_extension),
485
+ });
486
+
487
+ // Add required fields for CLI upload
488
+ form.append('tableName', tableName);
489
+ form.append('fileId', file.id);
490
+ form.append('folderStructure', folderStructure);
491
+ form.append('rfc', file.rfc);
492
+ form.append('bucket', pushConfig.bucket);
493
+ form.append('autoDetect', 'true');
494
+ form.append(
495
+ 'autoOrganize',
496
+ options.autoOrganize !== false ? 'true' : 'false',
497
+ );
498
+ form.append('batchSize', '1');
499
+ form.append('clientVersion', appConfig.packageVersion);
500
+
501
+ // Upload file using new CLI upload endpoint
502
+ const response = await fetch(
503
+ `${uploadApiConfig.baseUrl}/api/storage/cli-upload`,
504
+ {
505
+ method: 'POST',
506
+ headers: {
507
+ 'x-api-key': uploadApiConfig.token,
508
+ ...form.getHeaders(),
509
+ },
510
+ body: form,
511
+ },
512
+ );
513
+
514
+ if (!response.ok) {
515
+ const errorText = await response.text();
516
+ result.uploadError = `HTTP ${response.status}: ${errorText}`;
517
+ logger.error(`✗ Failed: ${file.file_name} - ${result.uploadError}`);
518
+ return result;
519
+ }
520
+
521
+ const apiResult = await response.json();
522
+
523
+ // Check response from CLI upload endpoint
524
+ if (apiResult.uploaded && apiResult.uploaded.length > 0) {
525
+ const uploadedFile = apiResult.uploaded[0];
526
+ result.uploaded = true;
527
+ // Only assign storageId if it is a valid UUID; ignore placeholder values
528
+ const UUID_RE =
529
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
530
+ result.uploadedToStorageId =
531
+ uploadedFile.storageId && UUID_RE.test(uploadedFile.storageId)
532
+ ? uploadedFile.storageId
533
+ : null;
534
+ logger.info(`✓ Uploaded: ${file.file_name} → ${uploadPath}`);
535
+ } else if (apiResult.errors && apiResult.errors.length > 0) {
536
+ const error = apiResult.errors[0];
537
+ result.uploadError = `UPLOAD_FAILED: ${error.error || 'Upload failed'}`;
538
+ logger.error(`✗ Failed: ${file.file_name} - ${result.uploadError}`);
539
+ } else {
540
+ result.uploadError = 'Unknown upload error - no files uploaded';
541
+ logger.error(`✗ Failed: ${file.file_name} - ${result.uploadError}`);
542
+ }
543
+ } catch (error) {
544
+ result.uploadError = `UPLOAD_ERROR: ${error.message}`;
545
+ logger.error(`✗ Error uploading ${file.file_name}:`, error.message);
546
+ }
547
+
548
+ return result;
549
+ }
550
+
551
+ /**
552
+ * Get MIME type from file extension
553
+ * @private
554
+ */
555
+ #getMimeType(extension) {
556
+ const mimeTypes = {
557
+ pdf: 'application/pdf',
558
+ doc: 'application/msword',
559
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
560
+ xls: 'application/vnd.ms-excel',
561
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
562
+ txt: 'text/plain',
563
+ jpg: 'image/jpeg',
564
+ jpeg: 'image/jpeg',
565
+ png: 'image/png',
566
+ gif: 'image/gif',
567
+ xml: 'application/xml',
568
+ };
569
+ return mimeTypes[extension?.toLowerCase()] || 'application/octet-stream';
570
+ }
571
+
572
+ /**
573
+ * Upload a batch of files (legacy - kept for compatibility)
574
+ * @private
575
+ * @deprecated Use #uploadBatchViaCli instead
576
+ */
577
+ async #uploadBatch(files, uploadApiConfig) {
578
+ const uploadPromises = files.map((file) =>
579
+ this.#uploadFile(file, uploadApiConfig),
580
+ );
581
+ return Promise.all(uploadPromises);
582
+ }
583
+
584
+ /**
585
+ * Upload a single file
586
+ * @private
587
+ */
588
+ async #uploadFile(file, uploadApiConfig) {
589
+ const result = {
590
+ id: file.id,
591
+ uploaded: false,
592
+ uploadError: null,
593
+ uploadPath: null,
594
+ uploadedToStorageId: null,
595
+ };
596
+
597
+ try {
598
+ // Check if file exists
599
+ if (!fs.existsSync(file.absolute_path)) {
600
+ result.uploadError =
601
+ 'FILE_NOT_FOUND: File does not exist on filesystem';
602
+ return result;
603
+ }
604
+
605
+ // Get file stats
606
+ const stats = fs.statSync(file.absolute_path);
607
+ if (!stats.isFile()) {
608
+ result.uploadError = 'NOT_A_FILE: Path is not a regular file';
609
+ return result;
610
+ }
611
+
612
+ // Construct upload path using arela_path
613
+ // arela_path format: RFC/Year/Patente/Aduana/Pedimento/
614
+ const uploadPath = `${file.arela_path}${file.file_name}`;
615
+ result.uploadPath = uploadPath;
616
+
617
+ // Upload file using storage API
618
+ const response = await this.#uploadToStorageApi(
619
+ file.absolute_path,
620
+ uploadPath,
621
+ uploadApiConfig,
622
+ {
623
+ rfc: file.rfc,
624
+ year: file.detected_pedimento_year,
625
+ originalPath: file.relative_path,
626
+ },
627
+ );
628
+
629
+ if (response.success) {
630
+ result.uploaded = true;
631
+ result.uploadedToStorageId = response.fileId;
632
+ logger.info(`✓ Uploaded: ${file.file_name} → ${uploadPath}`);
633
+ } else {
634
+ result.uploadError = `UPLOAD_FAILED: ${response.error || 'Unknown error'}`;
635
+ logger.error(`✗ Failed: ${file.file_name} - ${result.uploadError}`);
636
+ }
637
+ } catch (error) {
638
+ result.uploadError = `UPLOAD_ERROR: ${error.message}`;
639
+ logger.error(`✗ Error uploading ${file.file_name}:`, error.message);
640
+ }
641
+
642
+ return result;
643
+ }
644
+
645
+ /**
646
+ * Upload file to storage API
647
+ * @private
648
+ */
649
+ async #uploadToStorageApi(filePath, uploadPath, apiConfig, metadata = {}) {
650
+ try {
651
+ // Create form data
652
+ const form = new FormData();
653
+ form.append('files', fs.createReadStream(filePath));
654
+
655
+ // uploadPath format: RFC/Year/Patente/Aduana/Pedimento/filename
656
+ // Extract folder structure (arela_path without filename)
657
+ const folderStructure = path.dirname(uploadPath);
658
+
659
+ // Use batch-upload-and-process endpoint (same as legacy upload)
660
+ form.append('bucket', appConfig.getPushConfig().bucket);
661
+ form.append('folderStructure', folderStructure);
662
+
663
+ // Add RFC for multi-database routing
664
+ if (metadata.rfc) {
665
+ form.append('rfc', metadata.rfc);
666
+ }
667
+
668
+ // Enable auto-detection
669
+ form.append('autoDetect', 'true');
670
+ form.append('autoOrganize', 'false');
671
+ form.append('batchSize', '1');
672
+ form.append('clientVersion', appConfig.packageVersion);
673
+
674
+ // Upload file
675
+ const response = await fetch(
676
+ `${apiConfig.baseUrl}/api/storage/batch-upload-and-process`,
677
+ {
678
+ method: 'POST',
679
+ headers: {
680
+ 'x-api-key': apiConfig.token,
681
+ ...form.getHeaders(),
682
+ },
683
+ body: form,
684
+ },
685
+ );
686
+
687
+ if (!response.ok) {
688
+ const errorText = await response.text();
689
+ return {
690
+ success: false,
691
+ error: `HTTP ${response.status}: ${errorText}`,
692
+ };
693
+ }
694
+
695
+ const result = await response.json();
696
+
697
+ // batch-upload-and-process returns { uploaded: [], detected: [], errors: [] }
698
+ // Check if upload was successful
699
+ if (result.uploaded && result.uploaded.length > 0) {
700
+ const uploadedFile = result.uploaded[0];
701
+ return {
702
+ success: true,
703
+ fileId: uploadedFile.fileId,
704
+ path: uploadedFile.path,
705
+ };
706
+ } else if (result.errors && result.errors.length > 0) {
707
+ const error = result.errors[0];
708
+ return {
709
+ success: false,
710
+ error: error.error || 'Upload failed',
711
+ };
712
+ } else {
713
+ return {
714
+ success: false,
715
+ error: 'Unknown upload error - no files uploaded',
716
+ };
717
+ }
718
+ } catch (error) {
719
+ return {
720
+ success: false,
721
+ error: error.message,
722
+ };
723
+ }
724
+ }
725
+
726
+ /**
727
+ * Report progress to callback if available (for worker mode)
728
+ * @private
729
+ * @param {number} percent - Progress percentage (0-100)
730
+ * @param {string} message - Progress message
731
+ */
732
+ #reportProgress(percent, message) {
733
+ if (this.onProgress && typeof this.onProgress === 'function') {
734
+ try {
735
+ this.onProgress(percent, message);
736
+ } catch (error) {
737
+ logger.debug(`Progress callback error: ${error.message}`);
738
+ }
739
+ }
740
+ }
741
+ }
742
+
743
+ export default PushCommand;