@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.
- package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
- package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
- package/.claude/worktrees/agent-multi-profile/README.md +405 -0
- package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
- package/.claude/worktrees/agent-multi-profile/package.json +78 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
- package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
- package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
- package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
- package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
- package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
- package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
- package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
- package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
- package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
- package/README.md +65 -0
- package/package.json +1 -1
- package/src/commands/AgentCommand.js +210 -0
- package/src/commands/AgentInitCommand.js +316 -0
- package/src/commands/PollWorkerCommand.js +11 -322
- package/src/config/config.js +44 -6
- package/src/document-type-shared.js +1 -1
- package/src/document-types/pedimento-completo.js +8 -1
- package/src/file-detection.js +9 -0
- package/src/index.js +55 -0
- package/src/scoring/scoring-engine.js +1 -1
- package/src/services/LoggingService.js +35 -0
- package/src/services/PipelineApiService.js +7 -1
- package/src/services/PipelineJobRunner.js +389 -0
- package/src/services/ProfileManager.js +164 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
import appConfig from '../config/config.js';
|
|
4
|
+
import { PathNormalizer } from '../utils/PathNormalizer.js';
|
|
5
|
+
import logger from './LoggingService.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tuning env vars that can be overridden per job via pipeline_config.extraOptions
|
|
9
|
+
* (delivered as job.scanConfig). Values not present in the job revert to whatever
|
|
10
|
+
* the process started with, so overrides never leak between jobs/profiles.
|
|
11
|
+
*/
|
|
12
|
+
const TUNING_ENV_KEYS = [
|
|
13
|
+
'SCAN_EXCLUDE_PATTERNS',
|
|
14
|
+
'SCAN_BATCH_SIZE',
|
|
15
|
+
'PUSH_BATCH_SIZE',
|
|
16
|
+
'PUSH_UPLOAD_BATCH_SIZE',
|
|
17
|
+
'MAX_CONCURRENT_SOURCES',
|
|
18
|
+
'MAX_API_CONNECTIONS',
|
|
19
|
+
'API_CONNECTION_TIMEOUT',
|
|
20
|
+
'BATCH_DELAY',
|
|
21
|
+
'BATCH_SIZE',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pipeline Job Runner
|
|
26
|
+
* Executes pipeline jobs (scan/identify/propagate/push/full) fetched from the API.
|
|
27
|
+
* Extracted from PollWorkerCommand so both `arela worker --poll` (single tenant)
|
|
28
|
+
* and `arela agent` (multi-profile) share the same job execution logic.
|
|
29
|
+
*
|
|
30
|
+
* NOT safe for concurrent jobs: it reconfigures the appConfig singleton and
|
|
31
|
+
* mutates process.env per job. Callers must run one job at a time per process.
|
|
32
|
+
*/
|
|
33
|
+
export class PipelineJobRunner {
|
|
34
|
+
/**
|
|
35
|
+
* @param {import('./PipelineApiService.js').PipelineApiService} pipelineApi - API where progress/completion is reported
|
|
36
|
+
* @param {string} serverId - Server identifier used for heartbeats and scan registration
|
|
37
|
+
*/
|
|
38
|
+
constructor(pipelineApi, serverId) {
|
|
39
|
+
this.pipelineApi = pipelineApi;
|
|
40
|
+
this.serverId = serverId;
|
|
41
|
+
this.currentJob = null;
|
|
42
|
+
// Baseline for tuning overrides: what the process started with (.env or nothing)
|
|
43
|
+
this.tuningEnvSnapshot = Object.fromEntries(
|
|
44
|
+
TUNING_ENV_KEYS.map((key) => [key, process.env[key]]),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Process a job end-to-end: heartbeats, progress, completion/failure reporting.
|
|
50
|
+
* @param {Object} job - Job payload from GET /worker/jobs/next
|
|
51
|
+
*/
|
|
52
|
+
async processJob(job) {
|
|
53
|
+
this.currentJob = job;
|
|
54
|
+
|
|
55
|
+
// Start heartbeat timer to keep worker alive during long-running jobs
|
|
56
|
+
const heartbeatInterval = setInterval(async () => {
|
|
57
|
+
try {
|
|
58
|
+
await this.pipelineApi.sendHeartbeat(this.serverId, 'busy');
|
|
59
|
+
logger.debug(`💓 Heartbeat sent for ${this.serverId}`);
|
|
60
|
+
} catch {
|
|
61
|
+
// Non-critical — progress updates also refresh heartbeat
|
|
62
|
+
}
|
|
63
|
+
}, 20000);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
logger.info(`🔄 Processing ${job.type} job ${job.id}`);
|
|
67
|
+
|
|
68
|
+
// DEBUG: Delay to observe busy status in UI (remove in production)
|
|
69
|
+
const debugDelay = parseInt(process.env.DEBUG_JOB_DELAY) || 0;
|
|
70
|
+
if (debugDelay > 0) {
|
|
71
|
+
console.log(
|
|
72
|
+
`⏳ DEBUG: Waiting ${debugDelay / 1000}s before executing job...`,
|
|
73
|
+
);
|
|
74
|
+
await this.#sleep(debugDelay);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create progress callback that reports to API
|
|
78
|
+
const onProgress = async (percent, message) => {
|
|
79
|
+
await this.pipelineApi.updateProgress(
|
|
80
|
+
job.id,
|
|
81
|
+
percent,
|
|
82
|
+
message,
|
|
83
|
+
null,
|
|
84
|
+
job.type,
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Configure API targets for this job
|
|
89
|
+
this.#configureApiTargets(job);
|
|
90
|
+
|
|
91
|
+
// Execute the appropriate command
|
|
92
|
+
let result;
|
|
93
|
+
switch (job.type) {
|
|
94
|
+
case 'scan':
|
|
95
|
+
result = await this.#processScanJob(job, onProgress);
|
|
96
|
+
break;
|
|
97
|
+
case 'identify':
|
|
98
|
+
result = await this.#processIdentifyJob(job, onProgress);
|
|
99
|
+
break;
|
|
100
|
+
case 'propagate':
|
|
101
|
+
result = await this.#processPropagateJob(job, onProgress);
|
|
102
|
+
break;
|
|
103
|
+
case 'push':
|
|
104
|
+
result = await this.#processPushJob(job, onProgress);
|
|
105
|
+
break;
|
|
106
|
+
case 'full':
|
|
107
|
+
result = await this.#processFullPipeline(job, onProgress);
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
throw new Error(`Unknown job type: ${job.type}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Mark job as completed
|
|
114
|
+
await this.pipelineApi.completeJob(job.id, result);
|
|
115
|
+
logger.success(`✅ Job ${job.id} completed successfully`);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logger.error(`❌ Job ${job.id} failed: ${error.message}`);
|
|
118
|
+
await this.pipelineApi.failJob(job.id, error.message);
|
|
119
|
+
} finally {
|
|
120
|
+
clearInterval(heartbeatInterval);
|
|
121
|
+
this.currentJob = null;
|
|
122
|
+
// Reset API configuration
|
|
123
|
+
this.#resetApiConfig();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Configure API targets for cross-tenant operations
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
#configureApiTargets(job) {
|
|
132
|
+
if (job.sourceApi && job.targetApi) {
|
|
133
|
+
appConfig.setCrossTenantTargets(job.sourceApi, job.targetApi);
|
|
134
|
+
} else {
|
|
135
|
+
appConfig.setApiTarget(job.sourceApi || 'agencia');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reset API configuration after job
|
|
141
|
+
* @private
|
|
142
|
+
*/
|
|
143
|
+
#resetApiConfig() {
|
|
144
|
+
// Clear cross-tenant mode
|
|
145
|
+
appConfig.api.sourceTarget = null;
|
|
146
|
+
appConfig.api.targetTarget = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Override scan configuration from job data
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
#overrideScanConfig(job) {
|
|
154
|
+
// The runner's serverId always wins: a stale serverId inside job.scanConfig
|
|
155
|
+
// must not leak into scan registration (or into other profiles' jobs).
|
|
156
|
+
process.env.ARELA_SERVER_ID = this.serverId;
|
|
157
|
+
|
|
158
|
+
if (job.scanConfig) {
|
|
159
|
+
if (job.scanConfig.companySlug) {
|
|
160
|
+
process.env.ARELA_COMPANY_SLUG = job.scanConfig.companySlug;
|
|
161
|
+
}
|
|
162
|
+
if (job.scanConfig.basePath) {
|
|
163
|
+
process.env.UPLOAD_BASE_PATH = job.scanConfig.basePath;
|
|
164
|
+
process.env.ARELA_BASE_PATH_LABEL = job.scanConfig.basePath;
|
|
165
|
+
}
|
|
166
|
+
if (job.scanConfig.directoryLevel !== undefined) {
|
|
167
|
+
process.env.SCAN_DIRECTORY_LEVEL = String(
|
|
168
|
+
job.scanConfig.directoryLevel,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Per-job tuning from extraOptions; absent keys revert to the process baseline
|
|
174
|
+
for (const key of TUNING_ENV_KEYS) {
|
|
175
|
+
const value = job.scanConfig?.[key];
|
|
176
|
+
if (value !== undefined && value !== null) {
|
|
177
|
+
process.env[key] = String(value);
|
|
178
|
+
} else if (this.tuningEnvSnapshot[key] !== undefined) {
|
|
179
|
+
process.env[key] = this.tuningEnvSnapshot[key];
|
|
180
|
+
} else {
|
|
181
|
+
delete process.env[key];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Override scan directories if provided
|
|
186
|
+
if (job.scanDirectories && job.scanDirectories.length > 0) {
|
|
187
|
+
const allAbsolute = job.scanDirectories.every((d) =>
|
|
188
|
+
PathNormalizer.isAbsolutePath(d),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (allAbsolute) {
|
|
192
|
+
const ancestor = this.#commonAncestor(job.scanDirectories);
|
|
193
|
+
// Check if ancestor is meaningful (not just root or a drive letter)
|
|
194
|
+
const isUseful =
|
|
195
|
+
ancestor.length > 1 && !/^[a-zA-Z]:[/\\]?$/.test(ancestor);
|
|
196
|
+
|
|
197
|
+
if (isUseful) {
|
|
198
|
+
// Common ancestor found — set as base path, make sources relative
|
|
199
|
+
process.env.UPLOAD_BASE_PATH = ancestor;
|
|
200
|
+
process.env.ARELA_BASE_PATH_LABEL = ancestor;
|
|
201
|
+
const relativeSources = job.scanDirectories.map(
|
|
202
|
+
(d) => path.relative(ancestor, d) || '.',
|
|
203
|
+
);
|
|
204
|
+
process.env.UPLOAD_SOURCES = relativeSources.join('|');
|
|
205
|
+
} else {
|
|
206
|
+
// Cross-drive or no common ancestor — wildcard base, absolute sources
|
|
207
|
+
process.env.UPLOAD_BASE_PATH = '*';
|
|
208
|
+
process.env.ARELA_BASE_PATH_LABEL = '*';
|
|
209
|
+
process.env.UPLOAD_SOURCES = job.scanDirectories.join('|');
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
process.env.UPLOAD_SOURCES = job.scanDirectories.join('|');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Override file extensions if provided
|
|
217
|
+
if (job.fileExtensions && job.fileExtensions.length > 0) {
|
|
218
|
+
process.env.UPLOAD_FILE_EXTENSIONS = job.fileExtensions.join(',');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Reload cached config from the updated env vars
|
|
222
|
+
appConfig.reloadScanConfig();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Compute the longest common ancestor directory of a list of absolute paths.
|
|
227
|
+
* Uses '/' as separator (PathNormalizer normalizes Windows \\ to /).
|
|
228
|
+
* @param {string[]} paths
|
|
229
|
+
* @returns {string}
|
|
230
|
+
*/
|
|
231
|
+
#commonAncestor(paths) {
|
|
232
|
+
if (paths.length === 0) return '/';
|
|
233
|
+
if (paths.length === 1) return paths[0];
|
|
234
|
+
|
|
235
|
+
// Normalize separators so O:\exp\... becomes O:/exp/...
|
|
236
|
+
const normalized = paths.map((p) => PathNormalizer.normalizeSeparators(p));
|
|
237
|
+
const split = normalized.map((p) => p.split('/').filter(Boolean));
|
|
238
|
+
const minLen = Math.min(...split.map((s) => s.length));
|
|
239
|
+
const common = [];
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < minLen; i++) {
|
|
242
|
+
const seg = split[0][i];
|
|
243
|
+
if (split.every((s) => s[i] === seg)) {
|
|
244
|
+
common.push(seg);
|
|
245
|
+
} else {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Preserve drive letter format (e.g., 'O:' → 'O:/')
|
|
251
|
+
if (common.length > 0 && /^[a-zA-Z]:$/.test(common[0])) {
|
|
252
|
+
return common[0] + '/' + common.slice(1).join('/');
|
|
253
|
+
}
|
|
254
|
+
return '/' + common.join('/');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Process a scan job
|
|
259
|
+
* @private
|
|
260
|
+
*/
|
|
261
|
+
async #processScanJob(job, onProgress) {
|
|
262
|
+
const { ScanCommand } = await import('../commands/ScanCommand.js');
|
|
263
|
+
const scanCommand = new ScanCommand();
|
|
264
|
+
|
|
265
|
+
this.#overrideScanConfig(job);
|
|
266
|
+
|
|
267
|
+
const options = {
|
|
268
|
+
api: job.sourceApi || 'agencia',
|
|
269
|
+
countFirst: false,
|
|
270
|
+
stream: true,
|
|
271
|
+
onProgress,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return scanCommand.execute(options);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Process an identify job
|
|
279
|
+
* @private
|
|
280
|
+
*/
|
|
281
|
+
async #processIdentifyJob(job, onProgress) {
|
|
282
|
+
const { IdentifyCommand } = await import('../commands/IdentifyCommand.js');
|
|
283
|
+
const identifyCommand = new IdentifyCommand();
|
|
284
|
+
|
|
285
|
+
this.#overrideScanConfig(job);
|
|
286
|
+
|
|
287
|
+
const options = {
|
|
288
|
+
api: job.sourceApi || 'agencia',
|
|
289
|
+
batchSize: 100,
|
|
290
|
+
showStats: false,
|
|
291
|
+
onProgress,
|
|
292
|
+
...(job.fileId && { fileId: job.fileId }),
|
|
293
|
+
...(job.table && { table: job.table }),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return identifyCommand.execute(options);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Process a propagate job
|
|
301
|
+
* @private
|
|
302
|
+
*/
|
|
303
|
+
async #processPropagateJob(job, onProgress) {
|
|
304
|
+
const { PropagateCommand } = await import(
|
|
305
|
+
'../commands/PropagateCommand.js'
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
this.#overrideScanConfig(job);
|
|
309
|
+
|
|
310
|
+
const options = {
|
|
311
|
+
api: job.sourceApi || 'agencia',
|
|
312
|
+
batchSize: 50,
|
|
313
|
+
showStats: false,
|
|
314
|
+
onProgress,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const propagateCommand = new PropagateCommand(options);
|
|
318
|
+
return propagateCommand.execute();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Process a push job
|
|
323
|
+
* @private
|
|
324
|
+
*/
|
|
325
|
+
async #processPushJob(job, onProgress) {
|
|
326
|
+
const { PushCommand } = await import('../commands/PushCommand.js');
|
|
327
|
+
const pushCommand = new PushCommand();
|
|
328
|
+
|
|
329
|
+
this.#overrideScanConfig(job);
|
|
330
|
+
|
|
331
|
+
const options = {
|
|
332
|
+
scanApi: job.sourceApi || 'agencia',
|
|
333
|
+
pushApi: job.targetApi || 'cliente',
|
|
334
|
+
rfcs: [job.rfc],
|
|
335
|
+
batchSize: 100,
|
|
336
|
+
uploadBatchSize: 10,
|
|
337
|
+
folderStructure: job.folderStructure || 'pedimento',
|
|
338
|
+
autoOrganize: true,
|
|
339
|
+
showStats: false,
|
|
340
|
+
onProgress,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
return pushCommand.execute(options);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Process full pipeline (scan → identify → propagate → push)
|
|
348
|
+
* @private
|
|
349
|
+
*/
|
|
350
|
+
async #processFullPipeline(job, onProgress) {
|
|
351
|
+
const results = {};
|
|
352
|
+
|
|
353
|
+
// Step 1: Scan (0-25%)
|
|
354
|
+
logger.info('📁 Step 1/4: Scanning files...');
|
|
355
|
+
const wrappedProgress1 = (pct, msg) =>
|
|
356
|
+
onProgress(pct * 0.25, `[Scan] ${msg}`);
|
|
357
|
+
results.scan = await this.#processScanJob(job, wrappedProgress1);
|
|
358
|
+
|
|
359
|
+
// Step 2: Identify (25-50%)
|
|
360
|
+
logger.info('🔍 Step 2/4: Identifying documents...');
|
|
361
|
+
const wrappedProgress2 = (pct, msg) =>
|
|
362
|
+
onProgress(25 + pct * 0.25, `[Identify] ${msg}`);
|
|
363
|
+
results.identify = await this.#processIdentifyJob(job, wrappedProgress2);
|
|
364
|
+
|
|
365
|
+
// Step 3: Propagate (50-75%)
|
|
366
|
+
logger.info('🔄 Step 3/4: Propagating paths...');
|
|
367
|
+
const wrappedProgress3 = (pct, msg) =>
|
|
368
|
+
onProgress(50 + pct * 0.25, `[Propagate] ${msg}`);
|
|
369
|
+
results.propagate = await this.#processPropagateJob(job, wrappedProgress3);
|
|
370
|
+
|
|
371
|
+
// Step 4: Push (75-100%)
|
|
372
|
+
logger.info('📤 Step 4/4: Pushing to storage...');
|
|
373
|
+
const wrappedProgress4 = (pct, msg) =>
|
|
374
|
+
onProgress(75 + pct * 0.25, `[Push] ${msg}`);
|
|
375
|
+
results.push = await this.#processPushJob(job, wrappedProgress4);
|
|
376
|
+
|
|
377
|
+
return results;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Sleep for specified milliseconds
|
|
382
|
+
* @private
|
|
383
|
+
*/
|
|
384
|
+
#sleep(ms) {
|
|
385
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export default PipelineJobRunner;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PROFILES_PATH = '~/.arela/profiles.json';
|
|
6
|
+
export const DEFAULT_AGENT_LOG_PATH = '~/.arela/arela-agent.log';
|
|
7
|
+
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Expand a leading '~' to the user's home directory
|
|
11
|
+
* @param {string} inputPath
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function expandHome(inputPath) {
|
|
15
|
+
if (inputPath && inputPath.startsWith('~')) {
|
|
16
|
+
return path.join(os.homedir(), inputPath.slice(1));
|
|
17
|
+
}
|
|
18
|
+
return inputPath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Profile Manager
|
|
23
|
+
* Loads and validates the agent's profiles.json — the only local configuration
|
|
24
|
+
* the multi-profile agent needs: one machine serverId plus {name, url, token}
|
|
25
|
+
* per tenant API. Everything else (scan directories, tuning, schedule) lives
|
|
26
|
+
* server-side in pipeline_config.
|
|
27
|
+
*
|
|
28
|
+
* Minimal schema:
|
|
29
|
+
* {
|
|
30
|
+
* "version": 1,
|
|
31
|
+
* "serverId": "PALCO-01",
|
|
32
|
+
* "pollIntervalMs": 5000, // optional
|
|
33
|
+
* "logFile": "~/.arela/arela-agent.log", // optional
|
|
34
|
+
* "profiles": [
|
|
35
|
+
* { "name": "ktj", "url": "https://…", "token": "ak_…" },
|
|
36
|
+
* {
|
|
37
|
+
* "name": "cem", "url": "https://…", "token": "ak_…",
|
|
38
|
+
* "targets": { "agencia": { "baseUrl": "https://…", "token": "ak_…" } },
|
|
39
|
+
* "env": { "SCAN_DIRECTORY_LEVEL": "2" }
|
|
40
|
+
* }
|
|
41
|
+
* ]
|
|
42
|
+
* }
|
|
43
|
+
*/
|
|
44
|
+
export class ProfileManager {
|
|
45
|
+
/**
|
|
46
|
+
* Load, validate and normalize the profiles file.
|
|
47
|
+
* @param {string} [configPath] - Path to profiles.json (supports '~')
|
|
48
|
+
* @returns {{serverId: string, pollIntervalMs: number, logFile: string, profiles: Object[]}}
|
|
49
|
+
*/
|
|
50
|
+
static loadProfiles(configPath = DEFAULT_PROFILES_PATH) {
|
|
51
|
+
const resolvedPath = path.resolve(expandHome(configPath));
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Profiles file not found: ${resolvedPath}. ` +
|
|
56
|
+
`Run 'arela agent init --from <dir...>' to generate one from existing .env folders.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(`Invalid JSON in ${resolvedPath}: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!raw.serverId || typeof raw.serverId !== 'string') {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`${resolvedPath}: 'serverId' is required at the root level (one per machine, e.g. "PALCO-01")`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!Array.isArray(raw.profiles) || raw.profiles.length === 0) {
|
|
74
|
+
throw new Error(`${resolvedPath}: 'profiles' must be a non-empty array`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const seenNames = new Set();
|
|
78
|
+
const profiles = [];
|
|
79
|
+
|
|
80
|
+
raw.profiles.forEach((profile, index) => {
|
|
81
|
+
const label = `Profile ${profile?.name ? `'${profile.name}'` : `#${index + 1}`}`;
|
|
82
|
+
|
|
83
|
+
if (!profile.name || typeof profile.name !== 'string') {
|
|
84
|
+
throw new Error(`Profile #${index + 1}: 'name' is required`);
|
|
85
|
+
}
|
|
86
|
+
const name = profile.name.toLowerCase();
|
|
87
|
+
if (seenNames.has(name)) {
|
|
88
|
+
throw new Error(`${label}: duplicate profile name`);
|
|
89
|
+
}
|
|
90
|
+
seenNames.add(name);
|
|
91
|
+
|
|
92
|
+
if (profile.enabled === false) return;
|
|
93
|
+
|
|
94
|
+
this.#validateEndpoint(label, profile.url, profile.token);
|
|
95
|
+
|
|
96
|
+
const targets = {
|
|
97
|
+
// Same tenant answers under every symbolic name jobs may reference
|
|
98
|
+
agencia: { baseUrl: profile.url, token: profile.token },
|
|
99
|
+
cliente: { baseUrl: profile.url, token: profile.token },
|
|
100
|
+
[name]: { baseUrl: profile.url, token: profile.token },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Explicit targets override symbolic names (real cross-tenant cases)
|
|
104
|
+
for (const [targetName, targetConfig] of Object.entries(
|
|
105
|
+
profile.targets || {},
|
|
106
|
+
)) {
|
|
107
|
+
this.#validateEndpoint(
|
|
108
|
+
`${label}: targets.${targetName}`,
|
|
109
|
+
targetConfig?.baseUrl,
|
|
110
|
+
targetConfig?.token,
|
|
111
|
+
);
|
|
112
|
+
targets[targetName.toLowerCase()] = {
|
|
113
|
+
baseUrl: targetConfig.baseUrl,
|
|
114
|
+
token: targetConfig.token,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (profile.env && typeof profile.env !== 'object') {
|
|
119
|
+
throw new Error(`${label}: 'env' must be an object of key/value pairs`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
profiles.push({
|
|
123
|
+
name,
|
|
124
|
+
url: profile.url,
|
|
125
|
+
token: profile.token,
|
|
126
|
+
serverId: raw.serverId,
|
|
127
|
+
targets,
|
|
128
|
+
env: profile.env || {},
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (profiles.length === 0) {
|
|
133
|
+
throw new Error(`${resolvedPath}: all profiles are disabled`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
serverId: raw.serverId,
|
|
138
|
+
pollIntervalMs:
|
|
139
|
+
parseInt(raw.pollIntervalMs) > 0
|
|
140
|
+
? parseInt(raw.pollIntervalMs)
|
|
141
|
+
: DEFAULT_POLL_INTERVAL_MS,
|
|
142
|
+
logFile: path.resolve(expandHome(raw.logFile || DEFAULT_AGENT_LOG_PATH)),
|
|
143
|
+
profiles,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Validate a {url, token} pair with a per-field error message
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
static #validateEndpoint(label, url, token) {
|
|
152
|
+
if (!url || typeof url !== 'string') {
|
|
153
|
+
throw new Error(`${label}: 'url' is required`);
|
|
154
|
+
}
|
|
155
|
+
if (!/^https?:\/\//.test(url)) {
|
|
156
|
+
throw new Error(`${label}: 'url' must start with http:// or https://`);
|
|
157
|
+
}
|
|
158
|
+
if (!token || typeof token !== 'string') {
|
|
159
|
+
throw new Error(`${label}: 'token' is required`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default ProfileManager;
|