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.
Files changed (2) hide show
  1. package/dist/index.js +120 -17
  2. 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
- const age = s.ageHours != null ? (s.ageHours < 24 ? `${s.ageHours}h ago` : `${Math.floor(s.ageHours / 24)}d ago`) : '—';
4122
- // Build route: pe-sunat-padron pe/sunat/padron, co-rues → co/rues/registry
4123
- const segs = s.source.split('-');
4124
- let route;
4125
- if (segs.length >= 3)
4126
- route = `${segs[0]}/${segs.slice(1, -1).join('-')}/${segs[segs.length - 1]}`;
4127
- else if (segs.length === 2)
4128
- route = `${segs[0]}/${segs[1]}/registry`;
4129
- else
4130
- route = s.source;
4131
- const idName = s.idName || 'id';
4132
- console.log(` ${label.padEnd(35)} ${rows.padStart(12)} ${age.padEnd(18)} latinfo ${route} <${idName}>`);
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.6",
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": {