latinfo 0.20.6 → 0.20.7
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/dist/index.js +120 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4073,7 +4073,68 @@ async function importsStatus() {
|
|
|
4073
4073
|
}
|
|
4074
4074
|
}
|
|
4075
4075
|
// --- Sources ---
|
|
4076
|
-
async function sourcesCmd() {
|
|
4076
|
+
async function sourcesCmd(args = []) {
|
|
4077
|
+
const sourceName = args[0];
|
|
4078
|
+
// Detail view: latinfo sources pe-servir-sanctions
|
|
4079
|
+
if (sourceName && sourceName !== '--all') {
|
|
4080
|
+
try {
|
|
4081
|
+
const repo = getRepoPath();
|
|
4082
|
+
const yamlPath = path_1.default.join(repo, 'sources', `${sourceName}.yaml`);
|
|
4083
|
+
if (!fs_1.default.existsSync(yamlPath)) {
|
|
4084
|
+
console.error(`Source not found: ${sourceName}`);
|
|
4085
|
+
console.error(`Available sources:`);
|
|
4086
|
+
const files = fs_1.default.readdirSync(path_1.default.join(repo, 'sources')).filter(f => f.endsWith('.yaml'));
|
|
4087
|
+
for (const f of files)
|
|
4088
|
+
console.error(` ${f.replace('.yaml', '')}`);
|
|
4089
|
+
process.exit(1);
|
|
4090
|
+
}
|
|
4091
|
+
const content = fs_1.default.readFileSync(yamlPath, 'utf-8');
|
|
4092
|
+
const get = (key) => content.match(new RegExp(`^${key}:\\s*(.+)`, 'm'))?.[1]?.trim() || '—';
|
|
4093
|
+
const getQuoted = (key) => content.match(new RegExp(`${key}:\\s*"?([^"\\n]+)"?`))?.[1]?.trim() || '—';
|
|
4094
|
+
// Get import metadata
|
|
4095
|
+
let rows = '—', lastImport = '—';
|
|
4096
|
+
try {
|
|
4097
|
+
const adminSecret = requireAdmin();
|
|
4098
|
+
const res = await fetch(`${API_URL}/admin/imports`, { headers: { Authorization: `Bearer ${adminSecret}` } });
|
|
4099
|
+
if (res.ok) {
|
|
4100
|
+
const imports = await res.json();
|
|
4101
|
+
// Match by source name (handle co-rues vs co-rues-registry)
|
|
4102
|
+
const meta = imports.find(i => sourceName.startsWith(i.source) || i.source.startsWith(sourceName));
|
|
4103
|
+
if (meta) {
|
|
4104
|
+
rows = meta.rows?.toLocaleString() || '—';
|
|
4105
|
+
lastImport = meta.importedAt ? `${meta.importedAt.slice(0, 16)} (${meta.ageHours}h ago)` : '—';
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
catch { }
|
|
4110
|
+
// Parse fields
|
|
4111
|
+
const fields = [];
|
|
4112
|
+
const fieldMatches = content.matchAll(/- name: (\S+)/g);
|
|
4113
|
+
for (const m of fieldMatches)
|
|
4114
|
+
fields.push(m[1]);
|
|
4115
|
+
console.log(`
|
|
4116
|
+
${sourceName}
|
|
4117
|
+
|
|
4118
|
+
Country: ${get('country') === 'pe' ? 'Peru' : get('country') === 'co' ? 'Colombia' : get('country')}
|
|
4119
|
+
Institution: ${get('institution').toUpperCase()}
|
|
4120
|
+
Dataset: ${get('dataset')}
|
|
4121
|
+
URL: ${get('url')}
|
|
4122
|
+
Format: ${get('format')}
|
|
4123
|
+
Schedule: ${get('schedule')}
|
|
4124
|
+
Records: ${rows}
|
|
4125
|
+
Last import: ${lastImport}
|
|
4126
|
+
ID: ${getQuoted('name')} (${getQuoted('length')} digits)
|
|
4127
|
+
Fields: ${fields.join(', ')}
|
|
4128
|
+
Script: ${get('import_script') !== '—' ? get('import_script') : `src/imports/${sourceName}.ts`}
|
|
4129
|
+
Min rows: ${get('min_rows')}
|
|
4130
|
+
`);
|
|
4131
|
+
}
|
|
4132
|
+
catch (e) {
|
|
4133
|
+
console.error(`Cannot read source: ${e.message}`);
|
|
4134
|
+
}
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
// List view: latinfo sources (or --all)
|
|
4077
4138
|
// Get import metadata dynamically from admin endpoint
|
|
4078
4139
|
let data = [];
|
|
4079
4140
|
try {
|
|
@@ -4107,29 +4168,71 @@ async function sourcesCmd() {
|
|
|
4107
4168
|
console.log('No sources found.');
|
|
4108
4169
|
return;
|
|
4109
4170
|
}
|
|
4171
|
+
// Read schedule + change detection from YAMLs
|
|
4172
|
+
const yamlMeta = {};
|
|
4173
|
+
try {
|
|
4174
|
+
const repo = getRepoPath();
|
|
4175
|
+
const sourcesDir = path_1.default.join(repo, 'sources');
|
|
4176
|
+
for (const f of fs_1.default.readdirSync(sourcesDir).filter(f => f.endsWith('.yaml'))) {
|
|
4177
|
+
const content = fs_1.default.readFileSync(path_1.default.join(sourcesDir, f), 'utf-8');
|
|
4178
|
+
const name = f.replace('.yaml', '');
|
|
4179
|
+
const schedule = content.match(/^schedule:\s*(\S+)/m)?.[1] || '?';
|
|
4180
|
+
const url = content.match(/^url:\s*(\S+)/m)?.[1] || '';
|
|
4181
|
+
// Sources with HTTP URLs (not APIs/scraping) can do HEAD request change detection
|
|
4182
|
+
const hasCheck = url.startsWith('http') && !content.includes('format: api');
|
|
4183
|
+
yamlMeta[name] = { schedule, hasCheck };
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
catch { }
|
|
4187
|
+
// Colors
|
|
4188
|
+
const R = '\x1b[31m';
|
|
4189
|
+
const G = '\x1b[32m';
|
|
4190
|
+
const Y = '\x1b[33m';
|
|
4191
|
+
const D = '\x1b[90m';
|
|
4192
|
+
const X = '\x1b[0m';
|
|
4193
|
+
const noColor = !process.stdout.isTTY;
|
|
4110
4194
|
console.log('\n SOURCES\n');
|
|
4111
|
-
console.log(` ${'Source'.padEnd(35)} ${'Records'.padStart(12)} ${'Updated'.padEnd(18)} Command`);
|
|
4112
|
-
console.log(` ${'─'.repeat(35)} ${'─'.repeat(12)} ${'─'.repeat(18)} ${'─'.repeat(30)}`);
|
|
4113
4195
|
for (const s of data) {
|
|
4114
|
-
// Parse source name: pe-sunat-padron → country=pe, institution=sunat, dataset=padron
|
|
4115
4196
|
const parts = s.source.split('-');
|
|
4116
4197
|
const country = s.country || (parts[0] === 'pe' ? 'Peru' : parts[0] === 'co' ? 'Colombia' : parts[0]);
|
|
4117
4198
|
const institution = s.institution || parts.slice(1, -1).join('-');
|
|
4118
4199
|
const dataset = s.dataset || parts[parts.length - 1];
|
|
4119
4200
|
const label = `${country} — ${institution.toUpperCase()} ${dataset}`;
|
|
4120
4201
|
const rows = s.rows ? s.rows.toLocaleString() : '—';
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
let
|
|
4125
|
-
if (
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
else
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4202
|
+
// Age with minutes support + colors
|
|
4203
|
+
const ageH = s.ageHours ?? -1;
|
|
4204
|
+
let ageStr;
|
|
4205
|
+
let ageColor;
|
|
4206
|
+
if (ageH < 0) {
|
|
4207
|
+
ageStr = '—';
|
|
4208
|
+
ageColor = D;
|
|
4209
|
+
}
|
|
4210
|
+
else if (ageH < 1) {
|
|
4211
|
+
const mins = Math.round(ageH * 60);
|
|
4212
|
+
ageStr = `${mins}m ago`;
|
|
4213
|
+
ageColor = G;
|
|
4214
|
+
}
|
|
4215
|
+
else if (ageH < 24) {
|
|
4216
|
+
ageStr = `${ageH}h ago`;
|
|
4217
|
+
ageColor = G;
|
|
4218
|
+
}
|
|
4219
|
+
else if (ageH < 72) {
|
|
4220
|
+
ageStr = `${Math.floor(ageH / 24)}d ago`;
|
|
4221
|
+
ageColor = Y;
|
|
4222
|
+
}
|
|
4223
|
+
else {
|
|
4224
|
+
ageStr = `${Math.floor(ageH / 24)}d ago`;
|
|
4225
|
+
ageColor = R;
|
|
4226
|
+
}
|
|
4227
|
+
const meta = yamlMeta[s.source] || yamlMeta[s.source.replace(/-registry$/, '')] || {};
|
|
4228
|
+
const schedule = (meta.schedule || '?').padEnd(7);
|
|
4229
|
+
const check = meta.hasCheck ? 'yes' : '—';
|
|
4230
|
+
if (noColor) {
|
|
4231
|
+
console.log(` ${label.padEnd(32)} ${rows.padStart(12)} ${ageStr.padEnd(8)} ${schedule} ${check.padEnd(5)}`);
|
|
4232
|
+
}
|
|
4233
|
+
else {
|
|
4234
|
+
console.log(` ${label.padEnd(32)} ${rows.padStart(12)} ${ageColor}${ageStr.padEnd(8)}${X} ${D}${schedule}${X} ${meta.hasCheck ? G : D}${check.padEnd(5)}${X}`);
|
|
4235
|
+
}
|
|
4133
4236
|
}
|
|
4134
4237
|
console.log();
|
|
4135
4238
|
}
|
|
@@ -4503,7 +4606,7 @@ else {
|
|
|
4503
4606
|
users().catch(e => { console.error(e); process.exit(1); });
|
|
4504
4607
|
break;
|
|
4505
4608
|
case 'sources':
|
|
4506
|
-
sourcesCmd().catch(e => { console.error(e); process.exit(1); });
|
|
4609
|
+
sourcesCmd(args).catch(e => { console.error(e); process.exit(1); });
|
|
4507
4610
|
break;
|
|
4508
4611
|
case 'plan':
|
|
4509
4612
|
plan().catch(e => { console.error(e); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "latinfo",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.7",
|
|
4
4
|
"description": "Tax registry & procurement API for Latin America. Query RUC, DNI, NIT, licitaciones from Peru & Colombia. Offline MPHF search, full OCDS data, updated daily.",
|
|
5
5
|
"homepage": "https://latinfo.dev",
|
|
6
6
|
"repository": {
|