@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,316 @@
|
|
|
1
|
+
import { parse as parseEnv } from 'dotenv';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_AGENT_LOG_PATH,
|
|
8
|
+
expandHome,
|
|
9
|
+
} from '../services/ProfileManager.js';
|
|
10
|
+
|
|
11
|
+
const TUNING_ENV_KEYS = [
|
|
12
|
+
'SCAN_EXCLUDE_PATTERNS',
|
|
13
|
+
'SCAN_BATCH_SIZE',
|
|
14
|
+
'PUSH_BATCH_SIZE',
|
|
15
|
+
'PUSH_UPLOAD_BATCH_SIZE',
|
|
16
|
+
'MAX_CONCURRENT_SOURCES',
|
|
17
|
+
'MAX_API_CONNECTIONS',
|
|
18
|
+
'API_CONNECTION_TIMEOUT',
|
|
19
|
+
'BATCH_DELAY',
|
|
20
|
+
'BATCH_SIZE',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Agent Init Command Handler
|
|
25
|
+
* Migration helper: reads the legacy per-folder .env files (one folder per
|
|
26
|
+
* RFC×source) and generates, WITHOUT calling any API:
|
|
27
|
+
* - profiles.json → minimal agent config (folders on the same tenant API
|
|
28
|
+
* collapse into one profile)
|
|
29
|
+
* - seed-configs.json → one pipeline_config payload per folder (the data that
|
|
30
|
+
* moves server-side: scan directories, tuning, schedule)
|
|
31
|
+
* - seed-configs.sh → ready-to-run curls to POST those payloads
|
|
32
|
+
*
|
|
33
|
+
* Tokens never leave the machine: everything is generated locally for review.
|
|
34
|
+
*
|
|
35
|
+
* Usage:
|
|
36
|
+
* arela agent init --from ../ktj-cli-maya ../ktj-cli-matro --server-id PALCO-01
|
|
37
|
+
*/
|
|
38
|
+
export class AgentInitCommand {
|
|
39
|
+
/**
|
|
40
|
+
* Execute the init command
|
|
41
|
+
* @param {string[]} dirs - Folders containing legacy .env files
|
|
42
|
+
* @param {Object} options
|
|
43
|
+
* @param {string} options.serverId - Machine server id (default: ARELA_SERVER_ID from first .env, else hostname)
|
|
44
|
+
* @param {string} options.out - Output directory (default ~/.arela)
|
|
45
|
+
* @param {boolean} options.force - Overwrite an existing profiles.json
|
|
46
|
+
*/
|
|
47
|
+
async execute(dirs, options = {}) {
|
|
48
|
+
if (!dirs || dirs.length === 0) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
'At least one folder is required: arela agent init --from <dir1> <dir2> …',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const outDir = path.resolve(expandHome(options.out || '~/.arela'));
|
|
55
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
56
|
+
|
|
57
|
+
const profilesPath = path.join(outDir, 'profiles.json');
|
|
58
|
+
if (fs.existsSync(profilesPath) && !options.force) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`${profilesPath} already exists. Use --force to overwrite it.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const parsedFolders = [];
|
|
65
|
+
for (const dir of dirs) {
|
|
66
|
+
const resolvedDir = path.resolve(expandHome(dir));
|
|
67
|
+
const envPath = path.join(resolvedDir, '.env');
|
|
68
|
+
if (!fs.existsSync(envPath)) {
|
|
69
|
+
console.warn(`⚠️ Skipping ${resolvedDir}: no .env file found`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const env = parseEnv(fs.readFileSync(envPath));
|
|
73
|
+
const clienteUrl = env.ARELA_API_CLIENTE_URL || env.ARELA_API_URL || null;
|
|
74
|
+
const clienteToken =
|
|
75
|
+
env.ARELA_API_CLIENTE_TOKEN || env.ARELA_API_TOKEN || null;
|
|
76
|
+
if (!clienteUrl || !clienteToken) {
|
|
77
|
+
console.warn(
|
|
78
|
+
`⚠️ Skipping ${resolvedDir}: no ARELA_API_CLIENTE_URL/_TOKEN (nor ARELA_API_URL/_TOKEN)`,
|
|
79
|
+
);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
parsedFolders.push({ dir: resolvedDir, env, clienteUrl, clienteToken });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (parsedFolders.length === 0) {
|
|
86
|
+
throw new Error('No usable .env files found in the given folders');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const serverId =
|
|
90
|
+
options.serverId ||
|
|
91
|
+
parsedFolders.find((f) => f.env.ARELA_SERVER_ID)?.env.ARELA_SERVER_ID ||
|
|
92
|
+
os.hostname();
|
|
93
|
+
|
|
94
|
+
// Group folders by tenant API URL → one profile per tenant
|
|
95
|
+
const profilesByUrl = new Map();
|
|
96
|
+
for (const folder of parsedFolders) {
|
|
97
|
+
const key = folder.clienteUrl.replace(/\/+$/, '').toLowerCase();
|
|
98
|
+
if (!profilesByUrl.has(key)) {
|
|
99
|
+
const profile = {
|
|
100
|
+
name: this.#deriveProfileName(folder, profilesByUrl),
|
|
101
|
+
url: folder.clienteUrl.replace(/\/+$/, ''),
|
|
102
|
+
token: folder.clienteToken,
|
|
103
|
+
};
|
|
104
|
+
const agenciaUrl = folder.env.ARELA_API_AGENCIA_URL;
|
|
105
|
+
if (
|
|
106
|
+
agenciaUrl &&
|
|
107
|
+
agenciaUrl.replace(/\/+$/, '').toLowerCase() !== key
|
|
108
|
+
) {
|
|
109
|
+
profile.targets = {
|
|
110
|
+
agencia: {
|
|
111
|
+
baseUrl: agenciaUrl.replace(/\/+$/, ''),
|
|
112
|
+
token: folder.env.ARELA_API_AGENCIA_TOKEN || folder.clienteToken,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
profilesByUrl.set(key, profile);
|
|
117
|
+
}
|
|
118
|
+
folder.profile = profilesByUrl.get(key);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const profilesJson = {
|
|
122
|
+
version: 1,
|
|
123
|
+
serverId,
|
|
124
|
+
pollIntervalMs: 5000,
|
|
125
|
+
logFile: DEFAULT_AGENT_LOG_PATH,
|
|
126
|
+
profiles: [...profilesByUrl.values()],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// One pipeline_config payload per folder×RFC
|
|
130
|
+
const seeds = [];
|
|
131
|
+
for (const folder of parsedFolders) {
|
|
132
|
+
const rfcs = this.#splitPipe(
|
|
133
|
+
folder.env.UPLOAD_RFCS || folder.env.PUSH_RFCS,
|
|
134
|
+
);
|
|
135
|
+
if (rfcs.length === 0) {
|
|
136
|
+
console.warn(
|
|
137
|
+
`⚠️ ${folder.dir}: no UPLOAD_RFCS/PUSH_RFCS — using placeholder RFC_PENDIENTE (edit before seeding)`,
|
|
138
|
+
);
|
|
139
|
+
rfcs.push('RFC_PENDIENTE');
|
|
140
|
+
}
|
|
141
|
+
for (const rfc of rfcs) {
|
|
142
|
+
seeds.push({
|
|
143
|
+
profile: folder.profile.name,
|
|
144
|
+
apiUrl: folder.profile.url,
|
|
145
|
+
sourceFolder: folder.dir,
|
|
146
|
+
config: this.#buildPipelineConfig(folder, rfc, serverId),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const seedsJsonPath = path.join(outDir, 'seed-configs.json');
|
|
152
|
+
const seedsShPath = path.join(outDir, 'seed-configs.sh');
|
|
153
|
+
|
|
154
|
+
fs.writeFileSync(profilesPath, JSON.stringify(profilesJson, null, 2));
|
|
155
|
+
try {
|
|
156
|
+
fs.chmodSync(profilesPath, 0o600);
|
|
157
|
+
} catch {
|
|
158
|
+
// chmod is best-effort on Windows
|
|
159
|
+
}
|
|
160
|
+
fs.writeFileSync(seedsJsonPath, JSON.stringify(seeds, null, 2));
|
|
161
|
+
fs.writeFileSync(seedsShPath, this.#buildSeedScript(seeds, profilesJson));
|
|
162
|
+
try {
|
|
163
|
+
fs.chmodSync(seedsShPath, 0o755);
|
|
164
|
+
} catch {
|
|
165
|
+
// chmod is best-effort on Windows
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(`\n✅ Generated from ${parsedFolders.length} folder(s):`);
|
|
169
|
+
console.log(
|
|
170
|
+
` 📄 ${profilesPath} (${profilesJson.profiles.length} profile(s))`,
|
|
171
|
+
);
|
|
172
|
+
console.log(
|
|
173
|
+
` 📄 ${seedsJsonPath} (${seeds.length} pipeline_config payload(s))`,
|
|
174
|
+
);
|
|
175
|
+
console.log(` 📄 ${seedsShPath}`);
|
|
176
|
+
console.log('\nNext steps:');
|
|
177
|
+
console.log(' 1. Review both files (RFCs, scan directories, slugs)');
|
|
178
|
+
console.log(` 2. Seed the server configs: bash ${seedsShPath}`);
|
|
179
|
+
console.log(' 3. Start the agent: arela agent');
|
|
180
|
+
console.log(
|
|
181
|
+
' 4. Once stable, enable scheduling per config (scheduleEnabled) and the PIPELINE_SCHEDULER cronjob per tenant',
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Profile name from the API hostname's first label (ktj.api… → 'ktj'),
|
|
187
|
+
* falling back to the folder basename; deduped with numeric suffixes.
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
#deriveProfileName(folder, profilesByUrl) {
|
|
191
|
+
let base;
|
|
192
|
+
try {
|
|
193
|
+
base = new URL(folder.clienteUrl).hostname.split('.')[0];
|
|
194
|
+
} catch {
|
|
195
|
+
base = path.basename(folder.dir);
|
|
196
|
+
}
|
|
197
|
+
base = base.toLowerCase();
|
|
198
|
+
const taken = new Set(
|
|
199
|
+
[...profilesByUrl.values()].map((profile) => profile.name),
|
|
200
|
+
);
|
|
201
|
+
let name = base;
|
|
202
|
+
let suffix = 2;
|
|
203
|
+
while (taken.has(name)) {
|
|
204
|
+
name = `${base}-${suffix++}`;
|
|
205
|
+
}
|
|
206
|
+
return name;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build the POST /api/uploader/pipeline/config payload for one folder×RFC
|
|
211
|
+
* @private
|
|
212
|
+
*/
|
|
213
|
+
#buildPipelineConfig(folder, rfc, serverId) {
|
|
214
|
+
const { env, dir } = folder;
|
|
215
|
+
|
|
216
|
+
// Folder naming convention 'ktj-cli-maya' → source slug 'maya'
|
|
217
|
+
const basenameParts = path.basename(dir).split('-');
|
|
218
|
+
const agenciaSlug =
|
|
219
|
+
basenameParts.length > 1
|
|
220
|
+
? basenameParts[basenameParts.length - 1]
|
|
221
|
+
: env.ARELA_COMPANY_SLUG || path.basename(dir);
|
|
222
|
+
|
|
223
|
+
const basePath = env.UPLOAD_BASE_PATH;
|
|
224
|
+
const sources = this.#splitPipe(env.UPLOAD_SOURCES);
|
|
225
|
+
let scanDirectories = [];
|
|
226
|
+
if (basePath && basePath !== '*' && sources.length > 0) {
|
|
227
|
+
const trimmedBase = basePath.replace(/[\\/]+$/, '');
|
|
228
|
+
const separator = trimmedBase.includes('\\') ? '\\' : '/';
|
|
229
|
+
scanDirectories = sources.map(
|
|
230
|
+
(source) => `${trimmedBase}${separator}${source}`,
|
|
231
|
+
);
|
|
232
|
+
} else if (basePath && basePath !== '*') {
|
|
233
|
+
scanDirectories = [basePath];
|
|
234
|
+
} else if (sources.length > 0) {
|
|
235
|
+
scanDirectories = sources;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const extraOptions = {};
|
|
239
|
+
if (env.ARELA_COMPANY_SLUG) {
|
|
240
|
+
extraOptions.companySlug = env.ARELA_COMPANY_SLUG;
|
|
241
|
+
}
|
|
242
|
+
if (env.SCAN_DIRECTORY_LEVEL !== undefined) {
|
|
243
|
+
extraOptions.directoryLevel = parseInt(env.SCAN_DIRECTORY_LEVEL) || 0;
|
|
244
|
+
}
|
|
245
|
+
for (const key of TUNING_ENV_KEYS) {
|
|
246
|
+
if (env[key] !== undefined) {
|
|
247
|
+
extraOptions[key] = env[key];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const crossTenant = !!folder.profile.targets?.agencia;
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
rfc,
|
|
255
|
+
agenciaSlug: agenciaSlug.toLowerCase(),
|
|
256
|
+
displayName: `${rfc} — ${agenciaSlug}`,
|
|
257
|
+
scanDirectories,
|
|
258
|
+
...(env.UPLOAD_FILE_EXTENSIONS && {
|
|
259
|
+
fileExtensions: env.UPLOAD_FILE_EXTENSIONS.split(',')
|
|
260
|
+
.map((extension) => extension.trim())
|
|
261
|
+
.filter(Boolean),
|
|
262
|
+
}),
|
|
263
|
+
...(env.PUSH_BUCKET && { pushBucket: env.PUSH_BUCKET }),
|
|
264
|
+
folderStructure: env.PUSH_FOLDER_STRUCTURE || 'pedimento',
|
|
265
|
+
serverId,
|
|
266
|
+
sourceApi: crossTenant ? 'agencia' : 'cliente',
|
|
267
|
+
targetApi: 'cliente',
|
|
268
|
+
active: true,
|
|
269
|
+
extraOptions,
|
|
270
|
+
// Enable after the agent runs stable (see README)
|
|
271
|
+
scheduleEnabled: false,
|
|
272
|
+
scheduleIntervalHours: 24,
|
|
273
|
+
scheduleStartTime: '02:00',
|
|
274
|
+
scheduleJobType: 'full',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Build the curl script that seeds every pipeline_config
|
|
280
|
+
* @private
|
|
281
|
+
*/
|
|
282
|
+
#buildSeedScript(seeds, profilesJson) {
|
|
283
|
+
const tokenByProfile = Object.fromEntries(
|
|
284
|
+
profilesJson.profiles.map((profile) => [profile.name, profile.token]),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
let script = '#!/usr/bin/env bash\n';
|
|
288
|
+
script += '# Generated by `arela agent init` — review before running.\n';
|
|
289
|
+
script += '# Seeds one pipeline_config row per legacy folder×RFC.\n';
|
|
290
|
+
script += 'set -euo pipefail\n\n';
|
|
291
|
+
|
|
292
|
+
for (const seed of seeds) {
|
|
293
|
+
script += `# ${seed.config.rfc} / ${seed.config.agenciaSlug} (from ${seed.sourceFolder})\n`;
|
|
294
|
+
script += `curl -sS -X POST '${seed.apiUrl}/api/uploader/pipeline/config' \\\n`;
|
|
295
|
+
script += ` -H 'x-api-key: ${tokenByProfile[seed.profile]}' \\\n`;
|
|
296
|
+
script += ` -H 'Content-Type: application/json' \\\n`;
|
|
297
|
+
script += ` --data '${JSON.stringify(seed.config).replace(/'/g, `'\\''`)}'\n`;
|
|
298
|
+
script += 'echo\n\n';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return script;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Split a pipe-separated env value into trimmed parts
|
|
306
|
+
* @private
|
|
307
|
+
*/
|
|
308
|
+
#splitPipe(value) {
|
|
309
|
+
return (value || '')
|
|
310
|
+
.split('|')
|
|
311
|
+
.map((part) => part.trim())
|
|
312
|
+
.filter(Boolean);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export default new AgentInitCommand();
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
1
|
import logger from '../services/LoggingService.js';
|
|
4
2
|
import { PipelineApiService } from '../services/PipelineApiService.js';
|
|
3
|
+
import { PipelineJobRunner } from '../services/PipelineJobRunner.js';
|
|
5
4
|
|
|
6
5
|
import appConfig from '../config/config.js';
|
|
7
6
|
import ErrorHandler from '../errors/ErrorHandler.js';
|
|
8
|
-
import { PathNormalizer } from '../utils/PathNormalizer.js';
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
9
|
* Poll Worker Command Handler
|
|
12
10
|
* Runs arela-uploader as an HTTP-polling worker, fetching jobs from the API.
|
|
13
11
|
* Alternative to BullMQ/Redis-based worker for environments without Redis access.
|
|
12
|
+
* Job execution lives in PipelineJobRunner (shared with `arela agent`).
|
|
14
13
|
*
|
|
15
14
|
* Usage:
|
|
16
15
|
* arela worker --poll
|
|
@@ -27,6 +26,7 @@ export class PollWorkerCommand {
|
|
|
27
26
|
this.isShuttingDown = false;
|
|
28
27
|
this.currentJob = null;
|
|
29
28
|
this.pipelineApi = null;
|
|
29
|
+
this.jobRunner = null;
|
|
30
30
|
this.pollTimer = null;
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -54,8 +54,9 @@ export class PollWorkerCommand {
|
|
|
54
54
|
|
|
55
55
|
this.serverId = serverId;
|
|
56
56
|
|
|
57
|
-
// Initialize Pipeline API service
|
|
57
|
+
// Initialize Pipeline API service and the shared job runner
|
|
58
58
|
this.pipelineApi = new PipelineApiService(apiTarget);
|
|
59
|
+
this.jobRunner = new PipelineJobRunner(this.pipelineApi, serverId);
|
|
59
60
|
|
|
60
61
|
console.log('\n🔧 Starting Arela Poll Worker');
|
|
61
62
|
console.log(`📡 API Target: ${apiTarget}`);
|
|
@@ -94,7 +95,12 @@ export class PollWorkerCommand {
|
|
|
94
95
|
if (job && job.id) {
|
|
95
96
|
console.log(`📥 Got job: ${job.type} for RFC ${job.rfc}`);
|
|
96
97
|
logger.info(`📥 Got job: ${job.id} - ${job.type} for RFC ${job.rfc}`);
|
|
97
|
-
|
|
98
|
+
this.currentJob = job;
|
|
99
|
+
try {
|
|
100
|
+
await this.jobRunner.processJob(job);
|
|
101
|
+
} finally {
|
|
102
|
+
this.currentJob = null;
|
|
103
|
+
}
|
|
98
104
|
} else {
|
|
99
105
|
// Only log to file, not console (too noisy)
|
|
100
106
|
logger.debug('📭 No jobs available, waiting...');
|
|
@@ -111,323 +117,6 @@ export class PollWorkerCommand {
|
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
/**
|
|
115
|
-
* Process a job
|
|
116
|
-
* @private
|
|
117
|
-
*/
|
|
118
|
-
async #processJob(job) {
|
|
119
|
-
this.currentJob = job;
|
|
120
|
-
|
|
121
|
-
// Start heartbeat timer to keep worker alive during long-running jobs
|
|
122
|
-
const heartbeatInterval = setInterval(async () => {
|
|
123
|
-
try {
|
|
124
|
-
await this.pipelineApi.sendHeartbeat(this.serverId, 'busy');
|
|
125
|
-
logger.debug(`💓 Heartbeat sent for ${this.serverId}`);
|
|
126
|
-
} catch {
|
|
127
|
-
// Non-critical — progress updates also refresh heartbeat
|
|
128
|
-
}
|
|
129
|
-
}, 20000);
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
logger.info(`🔄 Processing ${job.type} job ${job.id}`);
|
|
133
|
-
|
|
134
|
-
// DEBUG: Delay to observe busy status in UI (remove in production)
|
|
135
|
-
const debugDelay = parseInt(process.env.DEBUG_JOB_DELAY) || 0;
|
|
136
|
-
if (debugDelay > 0) {
|
|
137
|
-
console.log(
|
|
138
|
-
`⏳ DEBUG: Waiting ${debugDelay / 1000}s before executing job...`,
|
|
139
|
-
);
|
|
140
|
-
await this.#sleep(debugDelay);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Create progress callback that reports to API
|
|
144
|
-
const onProgress = async (percent, message) => {
|
|
145
|
-
await this.pipelineApi.updateProgress(
|
|
146
|
-
job.id,
|
|
147
|
-
percent,
|
|
148
|
-
message,
|
|
149
|
-
null,
|
|
150
|
-
job.type,
|
|
151
|
-
);
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
// Configure API targets for this job
|
|
155
|
-
this.#configureApiTargets(job);
|
|
156
|
-
|
|
157
|
-
// Execute the appropriate command
|
|
158
|
-
let result;
|
|
159
|
-
switch (job.type) {
|
|
160
|
-
case 'scan':
|
|
161
|
-
result = await this.#processScanJob(job, onProgress);
|
|
162
|
-
break;
|
|
163
|
-
case 'identify':
|
|
164
|
-
result = await this.#processIdentifyJob(job, onProgress);
|
|
165
|
-
break;
|
|
166
|
-
case 'propagate':
|
|
167
|
-
result = await this.#processPropagateJob(job, onProgress);
|
|
168
|
-
break;
|
|
169
|
-
case 'push':
|
|
170
|
-
result = await this.#processPushJob(job, onProgress);
|
|
171
|
-
break;
|
|
172
|
-
case 'full':
|
|
173
|
-
result = await this.#processFullPipeline(job, onProgress);
|
|
174
|
-
break;
|
|
175
|
-
default:
|
|
176
|
-
throw new Error(`Unknown job type: ${job.type}`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Mark job as completed
|
|
180
|
-
await this.pipelineApi.completeJob(job.id, result);
|
|
181
|
-
logger.success(`✅ Job ${job.id} completed successfully`);
|
|
182
|
-
} catch (error) {
|
|
183
|
-
logger.error(`❌ Job ${job.id} failed: ${error.message}`);
|
|
184
|
-
await this.pipelineApi.failJob(job.id, error.message);
|
|
185
|
-
} finally {
|
|
186
|
-
clearInterval(heartbeatInterval);
|
|
187
|
-
this.currentJob = null;
|
|
188
|
-
// Reset API configuration
|
|
189
|
-
this.#resetApiConfig();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Configure API targets for cross-tenant operations
|
|
195
|
-
* @private
|
|
196
|
-
*/
|
|
197
|
-
#configureApiTargets(job) {
|
|
198
|
-
if (job.sourceApi && job.targetApi) {
|
|
199
|
-
appConfig.setCrossTenantTargets(job.sourceApi, job.targetApi);
|
|
200
|
-
} else {
|
|
201
|
-
appConfig.setApiTarget(job.sourceApi || 'agencia');
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Reset API configuration after job
|
|
207
|
-
* @private
|
|
208
|
-
*/
|
|
209
|
-
#resetApiConfig() {
|
|
210
|
-
// Clear cross-tenant mode
|
|
211
|
-
appConfig.api.sourceTarget = null;
|
|
212
|
-
appConfig.api.targetTarget = null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Override scan configuration from job data
|
|
217
|
-
* @private
|
|
218
|
-
*/
|
|
219
|
-
#overrideScanConfig(job) {
|
|
220
|
-
if (job.scanConfig) {
|
|
221
|
-
if (job.scanConfig.companySlug) {
|
|
222
|
-
process.env.ARELA_COMPANY_SLUG = job.scanConfig.companySlug;
|
|
223
|
-
}
|
|
224
|
-
if (job.scanConfig.serverId) {
|
|
225
|
-
process.env.ARELA_SERVER_ID = job.scanConfig.serverId;
|
|
226
|
-
}
|
|
227
|
-
if (job.scanConfig.basePath) {
|
|
228
|
-
process.env.UPLOAD_BASE_PATH = job.scanConfig.basePath;
|
|
229
|
-
process.env.ARELA_BASE_PATH_LABEL = job.scanConfig.basePath;
|
|
230
|
-
}
|
|
231
|
-
if (job.scanConfig.directoryLevel !== undefined) {
|
|
232
|
-
process.env.SCAN_DIRECTORY_LEVEL = String(
|
|
233
|
-
job.scanConfig.directoryLevel,
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Override scan directories if provided
|
|
239
|
-
if (job.scanDirectories && job.scanDirectories.length > 0) {
|
|
240
|
-
const allAbsolute = job.scanDirectories.every((d) =>
|
|
241
|
-
PathNormalizer.isAbsolutePath(d),
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
if (allAbsolute) {
|
|
245
|
-
const ancestor = this.#commonAncestor(job.scanDirectories);
|
|
246
|
-
// Check if ancestor is meaningful (not just root or a drive letter)
|
|
247
|
-
const isUseful =
|
|
248
|
-
ancestor.length > 1 && !/^[a-zA-Z]:[/\\]?$/.test(ancestor);
|
|
249
|
-
|
|
250
|
-
if (isUseful) {
|
|
251
|
-
// Common ancestor found — set as base path, make sources relative
|
|
252
|
-
process.env.UPLOAD_BASE_PATH = ancestor;
|
|
253
|
-
process.env.ARELA_BASE_PATH_LABEL = ancestor;
|
|
254
|
-
const relativeSources = job.scanDirectories.map(
|
|
255
|
-
(d) => path.relative(ancestor, d) || '.',
|
|
256
|
-
);
|
|
257
|
-
process.env.UPLOAD_SOURCES = relativeSources.join('|');
|
|
258
|
-
} else {
|
|
259
|
-
// Cross-drive or no common ancestor — wildcard base, absolute sources
|
|
260
|
-
process.env.UPLOAD_BASE_PATH = '*';
|
|
261
|
-
process.env.ARELA_BASE_PATH_LABEL = '*';
|
|
262
|
-
process.env.UPLOAD_SOURCES = job.scanDirectories.join('|');
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
process.env.UPLOAD_SOURCES = job.scanDirectories.join('|');
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Override file extensions if provided
|
|
270
|
-
if (job.fileExtensions && job.fileExtensions.length > 0) {
|
|
271
|
-
process.env.UPLOAD_FILE_EXTENSIONS = job.fileExtensions.join(',');
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Reload cached config from the updated env vars
|
|
275
|
-
appConfig.reloadScanConfig();
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Compute the longest common ancestor directory of a list of absolute paths.
|
|
280
|
-
* Uses '/' as separator (PathNormalizer normalizes Windows \\ to /).
|
|
281
|
-
* @param {string[]} paths
|
|
282
|
-
* @returns {string}
|
|
283
|
-
*/
|
|
284
|
-
#commonAncestor(paths) {
|
|
285
|
-
if (paths.length === 0) return '/';
|
|
286
|
-
if (paths.length === 1) return paths[0];
|
|
287
|
-
|
|
288
|
-
// Normalize separators so O:\exp\... becomes O:/exp/...
|
|
289
|
-
const normalized = paths.map((p) => PathNormalizer.normalizeSeparators(p));
|
|
290
|
-
const split = normalized.map((p) => p.split('/').filter(Boolean));
|
|
291
|
-
const minLen = Math.min(...split.map((s) => s.length));
|
|
292
|
-
const common = [];
|
|
293
|
-
|
|
294
|
-
for (let i = 0; i < minLen; i++) {
|
|
295
|
-
const seg = split[0][i];
|
|
296
|
-
if (split.every((s) => s[i] === seg)) {
|
|
297
|
-
common.push(seg);
|
|
298
|
-
} else {
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Preserve drive letter format (e.g., 'O:' → 'O:/')
|
|
304
|
-
if (common.length > 0 && /^[a-zA-Z]:$/.test(common[0])) {
|
|
305
|
-
return common[0] + '/' + common.slice(1).join('/');
|
|
306
|
-
}
|
|
307
|
-
return '/' + common.join('/');
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Process a scan job
|
|
312
|
-
* @private
|
|
313
|
-
*/
|
|
314
|
-
async #processScanJob(job, onProgress) {
|
|
315
|
-
const { ScanCommand } = await import('./ScanCommand.js');
|
|
316
|
-
const scanCommand = new ScanCommand();
|
|
317
|
-
|
|
318
|
-
this.#overrideScanConfig(job);
|
|
319
|
-
|
|
320
|
-
const options = {
|
|
321
|
-
api: job.sourceApi || 'agencia',
|
|
322
|
-
countFirst: false,
|
|
323
|
-
stream: true,
|
|
324
|
-
onProgress,
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
return scanCommand.execute(options);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Process an identify job
|
|
332
|
-
* @private
|
|
333
|
-
*/
|
|
334
|
-
async #processIdentifyJob(job, onProgress) {
|
|
335
|
-
const { IdentifyCommand } = await import('./IdentifyCommand.js');
|
|
336
|
-
const identifyCommand = new IdentifyCommand();
|
|
337
|
-
|
|
338
|
-
this.#overrideScanConfig(job);
|
|
339
|
-
|
|
340
|
-
const options = {
|
|
341
|
-
api: job.sourceApi || 'agencia',
|
|
342
|
-
batchSize: 100,
|
|
343
|
-
showStats: false,
|
|
344
|
-
onProgress,
|
|
345
|
-
...(job.fileId && { fileId: job.fileId }),
|
|
346
|
-
...(job.table && { table: job.table }),
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
return identifyCommand.execute(options);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Process a propagate job
|
|
354
|
-
* @private
|
|
355
|
-
*/
|
|
356
|
-
async #processPropagateJob(job, onProgress) {
|
|
357
|
-
const { PropagateCommand } = await import('./PropagateCommand.js');
|
|
358
|
-
|
|
359
|
-
this.#overrideScanConfig(job);
|
|
360
|
-
|
|
361
|
-
const options = {
|
|
362
|
-
api: job.sourceApi || 'agencia',
|
|
363
|
-
batchSize: 50,
|
|
364
|
-
showStats: false,
|
|
365
|
-
onProgress,
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
const propagateCommand = new PropagateCommand(options);
|
|
369
|
-
return propagateCommand.execute();
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Process a push job
|
|
374
|
-
* @private
|
|
375
|
-
*/
|
|
376
|
-
async #processPushJob(job, onProgress) {
|
|
377
|
-
const { PushCommand } = await import('./PushCommand.js');
|
|
378
|
-
const pushCommand = new PushCommand();
|
|
379
|
-
|
|
380
|
-
this.#overrideScanConfig(job);
|
|
381
|
-
|
|
382
|
-
const options = {
|
|
383
|
-
scanApi: job.sourceApi || 'agencia',
|
|
384
|
-
pushApi: job.targetApi || 'cliente',
|
|
385
|
-
rfcs: [job.rfc],
|
|
386
|
-
batchSize: 100,
|
|
387
|
-
uploadBatchSize: 10,
|
|
388
|
-
folderStructure: job.folderStructure || 'pedimento',
|
|
389
|
-
autoOrganize: true,
|
|
390
|
-
showStats: false,
|
|
391
|
-
onProgress,
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
return pushCommand.execute(options);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Process full pipeline (scan → identify → propagate → push)
|
|
399
|
-
* @private
|
|
400
|
-
*/
|
|
401
|
-
async #processFullPipeline(job, onProgress) {
|
|
402
|
-
const results = {};
|
|
403
|
-
|
|
404
|
-
// Step 1: Scan (0-25%)
|
|
405
|
-
logger.info('📁 Step 1/4: Scanning files...');
|
|
406
|
-
const wrappedProgress1 = (pct, msg) =>
|
|
407
|
-
onProgress(pct * 0.25, `[Scan] ${msg}`);
|
|
408
|
-
results.scan = await this.#processScanJob(job, wrappedProgress1);
|
|
409
|
-
|
|
410
|
-
// Step 2: Identify (25-50%)
|
|
411
|
-
logger.info('🔍 Step 2/4: Identifying documents...');
|
|
412
|
-
const wrappedProgress2 = (pct, msg) =>
|
|
413
|
-
onProgress(25 + pct * 0.25, `[Identify] ${msg}`);
|
|
414
|
-
results.identify = await this.#processIdentifyJob(job, wrappedProgress2);
|
|
415
|
-
|
|
416
|
-
// Step 3: Propagate (50-75%)
|
|
417
|
-
logger.info('🔄 Step 3/4: Propagating paths...');
|
|
418
|
-
const wrappedProgress3 = (pct, msg) =>
|
|
419
|
-
onProgress(50 + pct * 0.25, `[Propagate] ${msg}`);
|
|
420
|
-
results.propagate = await this.#processPropagateJob(job, wrappedProgress3);
|
|
421
|
-
|
|
422
|
-
// Step 4: Push (75-100%)
|
|
423
|
-
logger.info('📤 Step 4/4: Pushing to storage...');
|
|
424
|
-
const wrappedProgress4 = (pct, msg) =>
|
|
425
|
-
onProgress(75 + pct * 0.25, `[Push] ${msg}`);
|
|
426
|
-
results.push = await this.#processPushJob(job, wrappedProgress4);
|
|
427
|
-
|
|
428
|
-
return results;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
120
|
/**
|
|
432
121
|
* Sleep for specified milliseconds
|
|
433
122
|
* @private
|