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.
- package/dist/index.js +214 -7
- 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
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
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.
|
|
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": {
|