latinfo 0.20.5 → 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 +214 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1587,6 +1587,7 @@ DATA SOURCES
1587
1587
  latinfo co rues registry --search <query>
1588
1588
 
1589
1589
  COMMANDS
1590
+ sources List all data sources with stats
1590
1591
  login [--token <github_pat>] GitHub OAuth or PAT login
1591
1592
  logout Remove credentials
1592
1593
  whoami Show authenticated user
@@ -4038,6 +4039,203 @@ function requireTeamAdmin() {
4038
4039
  process.exit(1);
4039
4040
  }
4040
4041
  }
4042
+ // --- Imports Status ---
4043
+ async function importsStatus() {
4044
+ const { execSync: exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
4045
+ const ghRepo = 'carrerahaus/latinfo-api';
4046
+ // Get recent workflow runs
4047
+ const runsJson = exec(`gh run list --repo ${ghRepo} --workflow=import.yml --limit=5 --json databaseId,status,conclusion,createdAt`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
4048
+ const runs = JSON.parse(runsJson);
4049
+ if (runs.length === 0) {
4050
+ console.log('No recent import runs.');
4051
+ return;
4052
+ }
4053
+ console.log('\n IMPORT STATUS\n');
4054
+ for (const run of runs) {
4055
+ const jobsJson = exec(`gh run view ${run.databaseId} --repo ${ghRepo} --json jobs --jq '.jobs[] | "\\(.name)|\\(.status)|\\(.conclusion // "")"'`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
4056
+ const age = Math.floor((Date.now() - new Date(run.createdAt).getTime()) / 60000);
4057
+ const ageStr = age < 60 ? `${age}m ago` : `${Math.floor(age / 60)}h ago`;
4058
+ const runIcon = run.status === 'completed' ? (run.conclusion === 'success' ? '✅' : '❌') : '🔄';
4059
+ console.log(` ${runIcon} Run #${run.databaseId} (${ageStr})`);
4060
+ for (const line of jobsJson.split('\n')) {
4061
+ if (!line || line.includes('setup'))
4062
+ continue;
4063
+ const [name, status, conclusion] = line.split('|');
4064
+ const icon = status === 'completed' ? (conclusion === 'success' ? ' ✅' : ' ❌') :
4065
+ status === 'in_progress' ? ' 🔄' :
4066
+ status === 'queued' ? ' ⏳' : ' ⬜';
4067
+ const label = status === 'in_progress' ? 'running' :
4068
+ status === 'queued' ? 'queued' :
4069
+ conclusion || status;
4070
+ console.log(` ${icon} ${name.padEnd(25)} ${label}`);
4071
+ }
4072
+ console.log();
4073
+ }
4074
+ }
4075
+ // --- Sources ---
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)
4138
+ // Get import metadata dynamically from admin endpoint
4139
+ let data = [];
4140
+ try {
4141
+ const adminSecret = requireAdmin();
4142
+ const res = await fetch(`${API_URL}/admin/imports`, { headers: { Authorization: `Bearer ${adminSecret}` } });
4143
+ if (res.ok)
4144
+ data = await res.json();
4145
+ }
4146
+ catch {
4147
+ // Not admin — try reading source YAMLs from repo
4148
+ }
4149
+ if (data.length === 0) {
4150
+ // Fallback: read sources/*.yaml from repo
4151
+ try {
4152
+ const repo = getRepoPath();
4153
+ const sourcesDir = path_1.default.join(repo, 'sources');
4154
+ const files = fs_1.default.readdirSync(sourcesDir).filter(f => f.endsWith('.yaml'));
4155
+ for (const f of files) {
4156
+ const content = fs_1.default.readFileSync(path_1.default.join(sourcesDir, f), 'utf-8');
4157
+ const name = f.replace('.yaml', '');
4158
+ const country = content.match(/country:\s*(\S+)/)?.[1] || '?';
4159
+ const institution = content.match(/institution:\s*(\S+)/)?.[1] || '?';
4160
+ const dataset = content.match(/dataset:\s*(\S+)/)?.[1] || '?';
4161
+ const idName = content.match(/name:\s*(\S+)/)?.[1] || 'id';
4162
+ data.push({ source: name, country, institution, dataset, idName, rows: null, ageHours: null });
4163
+ }
4164
+ }
4165
+ catch { }
4166
+ }
4167
+ if (data.length === 0) {
4168
+ console.log('No sources found.');
4169
+ return;
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;
4194
+ console.log('\n SOURCES\n');
4195
+ for (const s of data) {
4196
+ const parts = s.source.split('-');
4197
+ const country = s.country || (parts[0] === 'pe' ? 'Peru' : parts[0] === 'co' ? 'Colombia' : parts[0]);
4198
+ const institution = s.institution || parts.slice(1, -1).join('-');
4199
+ const dataset = s.dataset || parts[parts.length - 1];
4200
+ const label = `${country} — ${institution.toUpperCase()} ${dataset}`;
4201
+ const rows = s.rows ? s.rows.toLocaleString() : '—';
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
+ }
4236
+ }
4237
+ console.log();
4238
+ }
4041
4239
  // --- Team & Tasks ---
4042
4240
  async function teamCmd(args) {
4043
4241
  const sub = args[0];
@@ -4407,6 +4605,9 @@ else {
4407
4605
  case 'users':
4408
4606
  users().catch(e => { console.error(e); process.exit(1); });
4409
4607
  break;
4608
+ case 'sources':
4609
+ sourcesCmd(args).catch(e => { console.error(e); process.exit(1); });
4610
+ break;
4410
4611
  case 'plan':
4411
4612
  plan().catch(e => { console.error(e); process.exit(1); });
4412
4613
  break;
@@ -4415,13 +4616,19 @@ else {
4415
4616
  break;
4416
4617
  // Team-only commands
4417
4618
  case 'imports':
4418
- requireTeam();
4419
- if (args[0] === 'run')
4420
- importsRun(args[1] || 'all').catch(e => { console.error(e); process.exit(1); });
4421
- else if (args[0] === 'report')
4422
- importsReport(parseInt(args[1]) || 7).catch(e => { console.error(e); process.exit(1); });
4423
- else
4424
- imports().catch(e => { console.error(e); process.exit(1); });
4619
+ if (args[0] === 'status') {
4620
+ requireTeam();
4621
+ importsStatus().catch(e => { console.error(e); process.exit(1); });
4622
+ }
4623
+ else {
4624
+ requireTeamAdmin();
4625
+ if (args[0] === 'run')
4626
+ importsRun(args[1] || 'all').catch(e => { console.error(e); process.exit(1); });
4627
+ else if (args[0] === 'report')
4628
+ importsReport(parseInt(args[1]) || 7).catch(e => { console.error(e); process.exit(1); });
4629
+ else
4630
+ imports().catch(e => { console.error(e); process.exit(1); });
4631
+ }
4425
4632
  break;
4426
4633
  case 'costs':
4427
4634
  requireTeamAdmin();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latinfo",
3
- "version": "0.20.5",
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": {