latinfo 0.9.0 → 0.10.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/dist/index.js +521 -124
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ const local_search_1 = require("./local-search");
|
|
|
47
47
|
const client_search_1 = require("./client-search");
|
|
48
48
|
const odis_search_1 = require("./odis-search");
|
|
49
49
|
const mphf_search_1 = require("./mphf-search");
|
|
50
|
-
const VERSION = '0.
|
|
50
|
+
const VERSION = '0.10.0';
|
|
51
51
|
const API_URL = process.env.LATINFO_API_URL || 'https://api.latinfo.dev';
|
|
52
52
|
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || 'Ov23li5fcQaiCsVtaMKK';
|
|
53
53
|
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.latinfo');
|
|
@@ -339,7 +339,9 @@ async function search(query) {
|
|
|
339
339
|
const odisMode = allArgs.includes('--odis');
|
|
340
340
|
const mphfMode = allArgs.includes('--mphf');
|
|
341
341
|
// MPHF: fully autonomous client-side search (zero server for search)
|
|
342
|
-
|
|
342
|
+
// Skip auto-detection in demo mode (no config) so demo data path is used
|
|
343
|
+
const config = loadConfig();
|
|
344
|
+
if (mphfMode || (config && !clientMode && !odisMode && (0, mphf_search_1.hasMphfData)(country))) {
|
|
343
345
|
const stripped = query.split(' ').filter((w, i, a) => !['--mphf', '--client', '--odis', '--json'].includes(w) &&
|
|
344
346
|
!(i > 0 && ['--country'].includes(a[i - 1])) &&
|
|
345
347
|
!['--country'].includes(w)).join(' ');
|
|
@@ -416,7 +418,8 @@ async function search(query) {
|
|
|
416
418
|
return;
|
|
417
419
|
}
|
|
418
420
|
// Local search: if data exists in ~/.latinfo/data/, search offline
|
|
419
|
-
|
|
421
|
+
// Skip in demo mode (no config) so demo data path is used
|
|
422
|
+
if (config && (0, local_search_1.hasLocalData)(country)) {
|
|
420
423
|
const results = (0, local_search_1.localSearch)(country, query);
|
|
421
424
|
if (jsonFlag) {
|
|
422
425
|
console.log(JSON.stringify(results));
|
|
@@ -436,7 +439,6 @@ async function search(query) {
|
|
|
436
439
|
return;
|
|
437
440
|
}
|
|
438
441
|
// Fallback: API search
|
|
439
|
-
const config = loadConfig();
|
|
440
442
|
if (!config) {
|
|
441
443
|
const results = (0, demo_data_1.searchDemo)(query);
|
|
442
444
|
if (jsonFlag) {
|
|
@@ -1221,108 +1223,65 @@ function help() {
|
|
|
1221
1223
|
console.log(`latinfo v${VERSION} — Tax registry API for Latin America
|
|
1222
1224
|
|
|
1223
1225
|
USAGE
|
|
1224
|
-
latinfo <
|
|
1226
|
+
latinfo <country> <institution> <dataset> <id|--search query|--dni id> [--json]
|
|
1227
|
+
latinfo <admin-command> [args]
|
|
1225
1228
|
|
|
1226
1229
|
QUICK START
|
|
1227
1230
|
npm install -g latinfo
|
|
1228
|
-
latinfo login
|
|
1229
|
-
latinfo
|
|
1230
|
-
latinfo search "banco de credito"
|
|
1231
|
-
latinfo
|
|
1232
|
-
|
|
1233
|
-
Works instantly with ${Object.keys(demo_data_1.DEMO_DATA).length} embedded records (no login needed).
|
|
1234
|
-
Run 'latinfo login' for 18M+ records and DNI lookup.
|
|
1235
|
-
|
|
1236
|
-
COMMANDS
|
|
1237
|
-
login
|
|
1238
|
-
GitHub OAuth. Opens browser, stores API key in ~/.latinfo/config.json.
|
|
1239
|
-
|
|
1240
|
-
login --token <github_pat>
|
|
1241
|
-
Login with a GitHub Personal Access Token. No browser needed.
|
|
1242
|
-
Create a PAT at github.com/settings/tokens (scope: read:user).
|
|
1243
|
-
|
|
1244
|
-
logout
|
|
1245
|
-
Remove stored credentials.
|
|
1246
|
-
|
|
1247
|
-
whoami
|
|
1248
|
-
Show authenticated GitHub username.
|
|
1249
|
-
--json: { username, api_key }
|
|
1250
|
-
|
|
1251
|
-
ruc <ruc>
|
|
1252
|
-
Lookup by RUC (11 digits).
|
|
1253
|
-
--json fields: ruc, razon_social, estado, condicion, ubigeo,
|
|
1254
|
-
tipo_via, nombre_via, numero, interior, lote, codigo_zona,
|
|
1255
|
-
tipo_zona, departamento, manzana, kilometro
|
|
1256
|
-
|
|
1257
|
-
dni <dni>
|
|
1258
|
-
Lookup by DNI (8 digits). Converts to RUC automatically.
|
|
1259
|
-
--json fields: same as ruc
|
|
1260
|
-
|
|
1261
|
-
search <query>
|
|
1262
|
-
Search by company name (razón social). Returns ranked results
|
|
1263
|
-
with prefix autocomplete and abbreviation handling (S.A.C., E.I.R.L.).
|
|
1264
|
-
--json fields: array of ruc objects
|
|
1265
|
-
|
|
1266
|
-
costs <users> [avg_req/user/month] [pro_%]
|
|
1267
|
-
Simulate Cloudflare cost vs revenue.
|
|
1268
|
-
Defaults: 1000 req/user, 1% Pro.
|
|
1269
|
-
--json fields: users, pro_users, requests, cf_tier, cf_cost,
|
|
1270
|
-
revenue, margin, safe
|
|
1231
|
+
latinfo login
|
|
1232
|
+
latinfo pe sunat padron 20100047218
|
|
1233
|
+
latinfo pe sunat padron --search "banco de credito"
|
|
1234
|
+
latinfo pe sunat padron --dni 09346247
|
|
1271
1235
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1236
|
+
DATA SOURCES
|
|
1237
|
+
Peru — SUNAT
|
|
1238
|
+
latinfo pe sunat padron <ruc> Padrón RUC (18M records)
|
|
1239
|
+
latinfo pe sunat padron --dni <dni> Lookup by DNI
|
|
1240
|
+
latinfo pe sunat padron --search <query> Search by name
|
|
1241
|
+
latinfo pe sunat coactiva <ruc> Tax debt (cobranza coactiva)
|
|
1242
|
+
latinfo pe sunat coactiva --search <query>
|
|
1275
1243
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1244
|
+
Peru — OSCE
|
|
1245
|
+
latinfo pe osce sanctioned <ruc> Sanctioned providers
|
|
1246
|
+
latinfo pe osce sanctioned --search <query>
|
|
1247
|
+
latinfo pe osce fines <ruc> Provider fines
|
|
1248
|
+
latinfo pe osce fines --search <query>
|
|
1280
1249
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
Flags: --category, --min-amount, --max-amount, --buyer, --
|
|
1284
|
-
Run 'latinfo licitaciones help' for details.
|
|
1250
|
+
Peru — OECE
|
|
1251
|
+
latinfo pe oece tenders <query> [flags] Government procurement
|
|
1252
|
+
Flags: --category, --min-amount, --max-amount, --buyer, --status, --limit
|
|
1285
1253
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
Run 'latinfo easypipe help' for details.
|
|
1254
|
+
Colombia — RUES
|
|
1255
|
+
latinfo co rues registry <nit> Business registry (3.3M records)
|
|
1256
|
+
latinfo co rues registry --search <query>
|
|
1290
1257
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1258
|
+
ADMIN
|
|
1259
|
+
login [--token <github_pat>] GitHub OAuth or PAT login
|
|
1260
|
+
logout Remove credentials
|
|
1261
|
+
whoami Show authenticated user
|
|
1262
|
+
imports Show import status
|
|
1263
|
+
imports run <source> Trigger import
|
|
1264
|
+
imports report [days] Import diagnostics
|
|
1265
|
+
costs <users> [avg_req] [pro_%] Cost simulation
|
|
1266
|
+
costs --live Production cost report
|
|
1267
|
+
bench [flags] Stress test API
|
|
1268
|
+
easypipe <command> Generic import pipeline
|
|
1269
|
+
completion [bash|zsh] Shell completions
|
|
1270
|
+
help This help text
|
|
1300
1271
|
|
|
1301
1272
|
FLAGS
|
|
1302
|
-
--json
|
|
1303
|
-
--
|
|
1304
|
-
--
|
|
1305
|
-
|
|
1306
|
-
COUNTRIES
|
|
1307
|
-
pe Peru (SUNAT padrón) — 18M+ records, updated daily. Active.
|
|
1308
|
-
br, mx, co, ar, cl — in development.
|
|
1273
|
+
--json Raw JSON output
|
|
1274
|
+
--search Search by name instead of ID lookup
|
|
1275
|
+
--dni Lookup by DNI (Peru only)
|
|
1276
|
+
--version Print version
|
|
1309
1277
|
|
|
1310
1278
|
PRICING
|
|
1311
|
-
Free 100,000 requests/day
|
|
1312
|
-
Pro 10M requests/month
|
|
1313
|
-
|
|
1314
|
-
LINKS
|
|
1315
|
-
latinfo.dev/docs API reference
|
|
1316
|
-
latinfo.dev/changelog Changelog
|
|
1317
|
-
carrera.instatus.com Status page
|
|
1279
|
+
Free 100,000 requests/day
|
|
1280
|
+
Pro 10M requests/month — $1/month
|
|
1318
1281
|
|
|
1319
1282
|
CONFIG
|
|
1320
|
-
~/.latinfo/config.json API key
|
|
1321
|
-
LATINFO_API_URL Override API
|
|
1322
|
-
|
|
1323
|
-
EXIT CODES
|
|
1324
|
-
0 Success
|
|
1325
|
-
1 Error`);
|
|
1283
|
+
~/.latinfo/config.json API key
|
|
1284
|
+
LATINFO_API_URL Override API URL`);
|
|
1326
1285
|
}
|
|
1327
1286
|
function printLogo() {
|
|
1328
1287
|
if (!process.stdout.isTTY)
|
|
@@ -1379,15 +1338,31 @@ function completion() {
|
|
|
1379
1338
|
if (shell === 'zsh') {
|
|
1380
1339
|
console.log(`#compdef latinfo
|
|
1381
1340
|
_latinfo() {
|
|
1382
|
-
local -a
|
|
1383
|
-
local -a
|
|
1384
|
-
local -a
|
|
1341
|
+
local -a countries=(pe co)
|
|
1342
|
+
local -a admin=(login logout whoami imports plan costs bench easypipe completion help)
|
|
1343
|
+
local -a pe_inst=(sunat osce oece)
|
|
1344
|
+
local -a co_inst=(rues)
|
|
1345
|
+
local -a sunat_ds=(padron coactiva)
|
|
1346
|
+
local -a osce_ds=(sanctioned fines)
|
|
1347
|
+
local -a oece_ds=(tenders)
|
|
1348
|
+
local -a rues_ds=(registry)
|
|
1349
|
+
local -a flags=(--json --search --dni --version)
|
|
1385
1350
|
if (( CURRENT == 2 )); then
|
|
1386
|
-
_describe 'command'
|
|
1387
|
-
elif
|
|
1388
|
-
|
|
1351
|
+
_describe 'command' countries -- admin
|
|
1352
|
+
elif (( CURRENT == 3 )); then
|
|
1353
|
+
case "\${words[2]}" in
|
|
1354
|
+
pe) _describe 'institution' pe_inst;;
|
|
1355
|
+
co) _describe 'institution' co_inst;;
|
|
1356
|
+
esac
|
|
1357
|
+
elif (( CURRENT == 4 )); then
|
|
1358
|
+
case "\${words[3]}" in
|
|
1359
|
+
sunat) _describe 'dataset' sunat_ds;;
|
|
1360
|
+
osce) _describe 'dataset' osce_ds;;
|
|
1361
|
+
oece) _describe 'dataset' oece_ds;;
|
|
1362
|
+
rues) _describe 'dataset' rues_ds;;
|
|
1363
|
+
esac
|
|
1389
1364
|
else
|
|
1390
|
-
_describe 'flag'
|
|
1365
|
+
_describe 'flag' flags
|
|
1391
1366
|
fi
|
|
1392
1367
|
}
|
|
1393
1368
|
compdef _latinfo latinfo`);
|
|
@@ -1395,26 +1370,444 @@ compdef _latinfo latinfo`);
|
|
|
1395
1370
|
else {
|
|
1396
1371
|
console.log(`_latinfo_completions() {
|
|
1397
1372
|
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1398
|
-
local
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1373
|
+
local lvl=\${COMP_CWORD}
|
|
1374
|
+
local w1="\${COMP_WORDS[1]}" w2="\${COMP_WORDS[2]}" w3="\${COMP_WORDS[3]}"
|
|
1375
|
+
if [[ \$lvl -eq 1 ]]; then
|
|
1376
|
+
COMPREPLY=( $(compgen -W "pe co login logout whoami imports plan costs bench easypipe completion help" -- "$cur") )
|
|
1377
|
+
elif [[ \$lvl -eq 2 ]]; then
|
|
1378
|
+
case "$w1" in
|
|
1379
|
+
pe) COMPREPLY=( $(compgen -W "sunat osce oece" -- "$cur") );;
|
|
1380
|
+
co) COMPREPLY=( $(compgen -W "rues" -- "$cur") );;
|
|
1381
|
+
esac
|
|
1382
|
+
elif [[ \$lvl -eq 3 ]]; then
|
|
1383
|
+
case "$w2" in
|
|
1384
|
+
sunat) COMPREPLY=( $(compgen -W "padron coactiva" -- "$cur") );;
|
|
1385
|
+
osce) COMPREPLY=( $(compgen -W "sanctioned fines" -- "$cur") );;
|
|
1386
|
+
oece) COMPREPLY=( $(compgen -W "tenders" -- "$cur") );;
|
|
1387
|
+
rues) COMPREPLY=( $(compgen -W "registry" -- "$cur") );;
|
|
1388
|
+
esac
|
|
1389
|
+
else
|
|
1390
|
+
COMPREPLY=( $(compgen -W "--json --search --dni" -- "$cur") )
|
|
1407
1391
|
fi
|
|
1408
1392
|
}
|
|
1409
1393
|
complete -F _latinfo_completions latinfo`);
|
|
1410
1394
|
}
|
|
1411
1395
|
}
|
|
1396
|
+
// --- Generic source query ---
|
|
1397
|
+
async function sourceQuery(routePath, datasetArgs) {
|
|
1398
|
+
const searchFlag = datasetArgs.includes('--search');
|
|
1399
|
+
const dniFlag = datasetArgs.includes('--dni');
|
|
1400
|
+
if (searchFlag) {
|
|
1401
|
+
const query = datasetArgs.filter(a => a !== '--search').join(' ');
|
|
1402
|
+
if (!query) {
|
|
1403
|
+
if (jsonFlag)
|
|
1404
|
+
jsonError('invalid_input', 'Search query is required.');
|
|
1405
|
+
console.error('Search query is required.');
|
|
1406
|
+
process.exit(1);
|
|
1407
|
+
}
|
|
1408
|
+
const config = requireAuth();
|
|
1409
|
+
const res = await apiRequest(config, `${routePath}/search?q=${encodeURIComponent(query)}`);
|
|
1410
|
+
const data = await res.json();
|
|
1411
|
+
if (jsonFlag) {
|
|
1412
|
+
console.log(JSON.stringify(data));
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
const results = Array.isArray(data) ? data : [];
|
|
1416
|
+
if (results.length === 0) {
|
|
1417
|
+
console.log('No results found.');
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
for (const r of results) {
|
|
1421
|
+
const fields = Object.values(r);
|
|
1422
|
+
console.log(` ${fields.slice(0, 3).join(' ')}`);
|
|
1423
|
+
}
|
|
1424
|
+
console.log(`\n${results.length} result(s)`);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (dniFlag) {
|
|
1428
|
+
const dniVal = datasetArgs.find(a => a !== '--dni' && !a.startsWith('--'));
|
|
1429
|
+
if (!dniVal || !/^\d{8}$/.test(dniVal)) {
|
|
1430
|
+
if (jsonFlag)
|
|
1431
|
+
jsonError('invalid_input', 'Invalid DNI. Must be 8 digits.');
|
|
1432
|
+
console.error('Invalid DNI. Must be 8 digits.');
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
const config = requireAuth();
|
|
1436
|
+
const res = await apiRequest(config, `${routePath}/dni/${dniVal}`);
|
|
1437
|
+
const data = await res.json();
|
|
1438
|
+
if (jsonFlag) {
|
|
1439
|
+
console.log(JSON.stringify(data));
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
for (const [k, v] of Object.entries(data)) {
|
|
1443
|
+
if (v && v !== '-')
|
|
1444
|
+
console.log(` ${k}: ${v}`);
|
|
1445
|
+
}
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
// Direct ID lookup
|
|
1449
|
+
const id = datasetArgs.find(a => !a.startsWith('--'));
|
|
1450
|
+
if (!id) {
|
|
1451
|
+
if (jsonFlag)
|
|
1452
|
+
jsonError('invalid_input', 'ID or --search <query> required.');
|
|
1453
|
+
console.error('ID or --search <query> required.');
|
|
1454
|
+
process.exit(1);
|
|
1455
|
+
}
|
|
1456
|
+
const config = loadConfig();
|
|
1457
|
+
// Demo mode for pe/sunat/padron
|
|
1458
|
+
if (!config && routePath === '/pe/sunat/padron' && /^\d{11}$/.test(id)) {
|
|
1459
|
+
const demo = demo_data_1.DEMO_DATA[id];
|
|
1460
|
+
if (demo) {
|
|
1461
|
+
if (jsonFlag) {
|
|
1462
|
+
console.log(JSON.stringify({ ...demo, _demo: true }));
|
|
1463
|
+
}
|
|
1464
|
+
else {
|
|
1465
|
+
for (const [k, v] of Object.entries(demo)) {
|
|
1466
|
+
if (v && v !== '-')
|
|
1467
|
+
console.log(` ${k}: ${v}`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
process.stderr.write(`Demo data (${Object.keys(demo_data_1.DEMO_DATA).length} records). Run 'latinfo login' for full access.\n`);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
if (jsonFlag)
|
|
1474
|
+
jsonError('not_found', "Not in demo data. Run 'latinfo login' for full access.");
|
|
1475
|
+
console.error("Not in demo data. Run 'latinfo login' for full access.");
|
|
1476
|
+
process.exit(1);
|
|
1477
|
+
}
|
|
1478
|
+
if (!config) {
|
|
1479
|
+
if (jsonFlag)
|
|
1480
|
+
jsonError('auth_required', "Lookup requires login. Run 'latinfo login'");
|
|
1481
|
+
console.error("Lookup requires login. Run 'latinfo login'");
|
|
1482
|
+
process.exit(1);
|
|
1483
|
+
}
|
|
1484
|
+
// Detect primary ID name from route path
|
|
1485
|
+
const idNames = {
|
|
1486
|
+
'/pe/sunat/padron': 'ruc', '/pe/sunat/coactiva': 'ruc',
|
|
1487
|
+
'/pe/osce/sanctioned': 'ruc', '/pe/osce/fines': 'ruc',
|
|
1488
|
+
'/co/rues/registry': 'nit',
|
|
1489
|
+
};
|
|
1490
|
+
const idName = idNames[routePath] || 'id';
|
|
1491
|
+
const res = await apiRequest(config, `${routePath}/${idName}/${id}`);
|
|
1492
|
+
const data = await res.json();
|
|
1493
|
+
if (jsonFlag) {
|
|
1494
|
+
console.log(JSON.stringify(data));
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
for (const [k, v] of Object.entries(data)) {
|
|
1498
|
+
if (v && v !== '-')
|
|
1499
|
+
console.log(` ${k}: ${v}`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
// --- Admin: source management ---
|
|
1503
|
+
function getRepoPath() {
|
|
1504
|
+
const envPath = process.env.LATINFO_REPO_PATH;
|
|
1505
|
+
if (envPath)
|
|
1506
|
+
return envPath;
|
|
1507
|
+
// Try to detect from cwd
|
|
1508
|
+
const candidates = [
|
|
1509
|
+
process.cwd(),
|
|
1510
|
+
path_1.default.join(os_1.default.homedir(), 'Documents/Github/carrerahaus/latinfo-api'),
|
|
1511
|
+
path_1.default.join(os_1.default.homedir(), 'latinfo-api'),
|
|
1512
|
+
];
|
|
1513
|
+
for (const p of candidates) {
|
|
1514
|
+
if (fs_1.default.existsSync(path_1.default.join(p, 'sources')) && fs_1.default.existsSync(path_1.default.join(p, 'src/imports')))
|
|
1515
|
+
return p;
|
|
1516
|
+
}
|
|
1517
|
+
console.error('Cannot find latinfo-api repo. Set LATINFO_REPO_PATH or run from repo dir.');
|
|
1518
|
+
process.exit(1);
|
|
1519
|
+
}
|
|
1520
|
+
function requireAdmin() {
|
|
1521
|
+
// 1. Env var
|
|
1522
|
+
if (process.env.LATINFO_ADMIN_SECRET)
|
|
1523
|
+
return process.env.LATINFO_ADMIN_SECRET;
|
|
1524
|
+
// 2. ~/.latinfo/admin.secret
|
|
1525
|
+
const secretFile = path_1.default.join(CONFIG_DIR, 'admin.secret');
|
|
1526
|
+
if (fs_1.default.existsSync(secretFile))
|
|
1527
|
+
return fs_1.default.readFileSync(secretFile, 'utf-8').trim();
|
|
1528
|
+
// 3. .dev.vars in repo
|
|
1529
|
+
try {
|
|
1530
|
+
const repo = getRepoPath();
|
|
1531
|
+
const devVars = path_1.default.join(repo, '.dev.vars');
|
|
1532
|
+
if (fs_1.default.existsSync(devVars)) {
|
|
1533
|
+
const match = fs_1.default.readFileSync(devVars, 'utf-8').match(/ADMIN_SECRET=(.+)/);
|
|
1534
|
+
if (match)
|
|
1535
|
+
return match[1].trim();
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
catch { }
|
|
1539
|
+
console.error('Admin access not found. Create ~/.latinfo/admin.secret or set LATINFO_ADMIN_SECRET.');
|
|
1540
|
+
process.exit(1);
|
|
1541
|
+
}
|
|
1542
|
+
async function adminCreate(args) {
|
|
1543
|
+
const [country, institution, dataset, ...flags] = args;
|
|
1544
|
+
if (!country || !institution || !dataset) {
|
|
1545
|
+
console.error('Usage: latinfo admin create <country> <institution> <dataset> [--url URL] [--id-name ruc] [--id-length 11] [--encoding utf-8] [--delimiter ","]');
|
|
1546
|
+
process.exit(1);
|
|
1547
|
+
}
|
|
1548
|
+
const name = `${country}-${institution}-${dataset}`;
|
|
1549
|
+
const repo = getRepoPath();
|
|
1550
|
+
const yamlPath = path_1.default.join(repo, 'sources', `${name}.yaml`);
|
|
1551
|
+
if (fs_1.default.existsSync(yamlPath)) {
|
|
1552
|
+
console.error(`Source ${name} already exists: ${yamlPath}`);
|
|
1553
|
+
process.exit(1);
|
|
1554
|
+
}
|
|
1555
|
+
const getFlag = (flag) => {
|
|
1556
|
+
const idx = flags.indexOf(flag);
|
|
1557
|
+
return idx !== -1 ? flags[idx + 1] : undefined;
|
|
1558
|
+
};
|
|
1559
|
+
const url = getFlag('--url') || 'https://example.com/data.csv';
|
|
1560
|
+
const idName = getFlag('--id-name') || 'id';
|
|
1561
|
+
const idLength = getFlag('--id-length') || '11';
|
|
1562
|
+
const encoding = getFlag('--encoding') || 'utf-8';
|
|
1563
|
+
const delimiter = getFlag('--delimiter') || ',';
|
|
1564
|
+
const format = getFlag('--format') || 'csv';
|
|
1565
|
+
const yaml = `name: ${name}
|
|
1566
|
+
country: ${country}
|
|
1567
|
+
institution: ${institution}
|
|
1568
|
+
dataset: ${dataset}
|
|
1569
|
+
source: ${institution}-${dataset}
|
|
1570
|
+
|
|
1571
|
+
url: ${url}
|
|
1572
|
+
format: ${format}
|
|
1573
|
+
encoding: ${encoding}
|
|
1574
|
+
delimiter: "${delimiter}"
|
|
1575
|
+
skip_header: true
|
|
1576
|
+
|
|
1577
|
+
primary_id:
|
|
1578
|
+
name: ${idName}
|
|
1579
|
+
column: 0
|
|
1580
|
+
length: ${idLength}
|
|
1581
|
+
regex: "^\\\\d{${idLength}}$"
|
|
1582
|
+
prefix_length: 5
|
|
1583
|
+
|
|
1584
|
+
alternate_ids: []
|
|
1585
|
+
|
|
1586
|
+
fields:
|
|
1587
|
+
- name: name
|
|
1588
|
+
column: 1
|
|
1589
|
+
search: true
|
|
1590
|
+
- name: status
|
|
1591
|
+
column: 2
|
|
1592
|
+
|
|
1593
|
+
import: custom
|
|
1594
|
+
import_script: src/imports/${name}.ts
|
|
1595
|
+
|
|
1596
|
+
schedule: manual
|
|
1597
|
+
change_detection: none
|
|
1598
|
+
min_rows: 100
|
|
1599
|
+
|
|
1600
|
+
smoke_test:
|
|
1601
|
+
id: ""
|
|
1602
|
+
expect_field: name
|
|
1603
|
+
`;
|
|
1604
|
+
fs_1.default.writeFileSync(yamlPath, yaml);
|
|
1605
|
+
console.log(`Created: ${yamlPath}`);
|
|
1606
|
+
console.log(`\nNext steps:`);
|
|
1607
|
+
console.log(` 1. Edit ${yamlPath} to match your data source`);
|
|
1608
|
+
console.log(` 2. Write import script: latinfo admin upload-script ${name} ./my-import.ts`);
|
|
1609
|
+
console.log(` 3. Test: latinfo admin test ${name}`);
|
|
1610
|
+
console.log(` 4. Publish: latinfo admin publish ${name}`);
|
|
1611
|
+
}
|
|
1612
|
+
async function adminUploadScript(args) {
|
|
1613
|
+
const [sourceName, scriptPath] = args;
|
|
1614
|
+
if (!sourceName || !scriptPath) {
|
|
1615
|
+
console.error('Usage: latinfo admin upload-script <source-name> <script-path>');
|
|
1616
|
+
process.exit(1);
|
|
1617
|
+
}
|
|
1618
|
+
const repo = getRepoPath();
|
|
1619
|
+
const dest = path_1.default.join(repo, 'src', 'imports', `${sourceName}.ts`);
|
|
1620
|
+
const src = path_1.default.resolve(scriptPath);
|
|
1621
|
+
if (!fs_1.default.existsSync(src)) {
|
|
1622
|
+
console.error(`Script not found: ${src}`);
|
|
1623
|
+
process.exit(1);
|
|
1624
|
+
}
|
|
1625
|
+
fs_1.default.copyFileSync(src, dest);
|
|
1626
|
+
console.log(`Copied: ${src} → ${dest}`);
|
|
1627
|
+
}
|
|
1628
|
+
async function adminTest(args) {
|
|
1629
|
+
const [sourceName] = args;
|
|
1630
|
+
if (!sourceName) {
|
|
1631
|
+
console.error('Usage: latinfo admin test <source-name>');
|
|
1632
|
+
process.exit(1);
|
|
1633
|
+
}
|
|
1634
|
+
const repo = getRepoPath();
|
|
1635
|
+
const yamlPath = path_1.default.join(repo, 'sources', `${sourceName}.yaml`);
|
|
1636
|
+
if (!fs_1.default.existsSync(yamlPath)) {
|
|
1637
|
+
console.error(`Source not found: ${yamlPath}`);
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
// Check if import script exists
|
|
1641
|
+
const scriptPath = path_1.default.join(repo, 'src', 'imports', `${sourceName}.ts`);
|
|
1642
|
+
const easypipePath = path_1.default.join(repo, 'src', 'imports', 'easypipe.ts');
|
|
1643
|
+
const useEasypipe = !fs_1.default.existsSync(scriptPath);
|
|
1644
|
+
const cmd = useEasypipe
|
|
1645
|
+
? `npx tsx ${easypipePath} ${yamlPath} --limit 100 --local`
|
|
1646
|
+
: `npx tsx ${scriptPath} --limit 100`;
|
|
1647
|
+
console.log(`Testing ${sourceName}...`);
|
|
1648
|
+
console.log(`Running: ${cmd}\n`);
|
|
1649
|
+
try {
|
|
1650
|
+
const { execSync: run } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
1651
|
+
run(cmd, { stdio: 'inherit', cwd: repo });
|
|
1652
|
+
console.log(`\n[test] ${sourceName}: PASSED`);
|
|
1653
|
+
}
|
|
1654
|
+
catch {
|
|
1655
|
+
console.error(`\n[test] ${sourceName}: FAILED`);
|
|
1656
|
+
process.exit(1);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
async function adminPublish(args) {
|
|
1660
|
+
const [sourceName] = args;
|
|
1661
|
+
if (!sourceName) {
|
|
1662
|
+
console.error('Usage: latinfo admin publish <source-name>');
|
|
1663
|
+
process.exit(1);
|
|
1664
|
+
}
|
|
1665
|
+
const repo = getRepoPath();
|
|
1666
|
+
const yamlPath = path_1.default.join(repo, 'sources', `${sourceName}.yaml`);
|
|
1667
|
+
if (!fs_1.default.existsSync(yamlPath)) {
|
|
1668
|
+
console.error(`Source not found: ${yamlPath}`);
|
|
1669
|
+
process.exit(1);
|
|
1670
|
+
}
|
|
1671
|
+
const { execSync: run } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
1672
|
+
// 1. Add source config to sources.ts
|
|
1673
|
+
console.log(`[publish] Adding ${sourceName} to source registry...`);
|
|
1674
|
+
// TODO: auto-generate sources.ts from YAMLs
|
|
1675
|
+
// 2. Git add + commit + push
|
|
1676
|
+
console.log(`[publish] Committing...`);
|
|
1677
|
+
const files = [`sources/${sourceName}.yaml`];
|
|
1678
|
+
const scriptPath = path_1.default.join(repo, 'src', 'imports', `${sourceName}.ts`);
|
|
1679
|
+
if (fs_1.default.existsSync(scriptPath))
|
|
1680
|
+
files.push(`src/imports/${sourceName}.ts`);
|
|
1681
|
+
try {
|
|
1682
|
+
run(`git add ${files.join(' ')}`, { cwd: repo, stdio: 'pipe' });
|
|
1683
|
+
run(`git commit -m "Add data source: ${sourceName}"`, { cwd: repo, stdio: 'pipe' });
|
|
1684
|
+
run(`git push`, { cwd: repo, stdio: 'pipe' });
|
|
1685
|
+
console.log(`[publish] Pushed to remote.`);
|
|
1686
|
+
}
|
|
1687
|
+
catch (e) {
|
|
1688
|
+
console.error(`[publish] Git error: ${e.message}`);
|
|
1689
|
+
process.exit(1);
|
|
1690
|
+
}
|
|
1691
|
+
// 3. Trigger import
|
|
1692
|
+
console.log(`[publish] Triggering import...`);
|
|
1693
|
+
try {
|
|
1694
|
+
run(`gh workflow run import.yml -f source=${sourceName}`, { cwd: repo, stdio: 'inherit' });
|
|
1695
|
+
console.log(`[publish] Import triggered. Check: gh run list --workflow=import.yml`);
|
|
1696
|
+
}
|
|
1697
|
+
catch {
|
|
1698
|
+
console.log(`[publish] Could not trigger workflow. Run manually: latinfo imports run ${sourceName}`);
|
|
1699
|
+
}
|
|
1700
|
+
console.log(`\n[publish] ${sourceName}: PUBLISHED`);
|
|
1701
|
+
}
|
|
1702
|
+
async function admin(args) {
|
|
1703
|
+
requireAdmin();
|
|
1704
|
+
const [subcommand, ...subArgs] = args;
|
|
1705
|
+
switch (subcommand) {
|
|
1706
|
+
case 'create':
|
|
1707
|
+
await adminCreate(subArgs);
|
|
1708
|
+
break;
|
|
1709
|
+
case 'upload-script':
|
|
1710
|
+
await adminUploadScript(subArgs);
|
|
1711
|
+
break;
|
|
1712
|
+
case 'test':
|
|
1713
|
+
await adminTest(subArgs);
|
|
1714
|
+
break;
|
|
1715
|
+
case 'publish':
|
|
1716
|
+
await adminPublish(subArgs);
|
|
1717
|
+
break;
|
|
1718
|
+
default:
|
|
1719
|
+
console.log(`latinfo admin — data pipeline for public government records
|
|
1720
|
+
|
|
1721
|
+
DO NOT use SQLite, Supabase, Postgres, or any other database.
|
|
1722
|
+
latinfo handles storage, indexing, full-text search, and API serving automatically.
|
|
1723
|
+
Your only job is to download and parse the source data into a TSV file.
|
|
1724
|
+
|
|
1725
|
+
WORKFLOW
|
|
1726
|
+
Step 1: Create a source definition
|
|
1727
|
+
latinfo admin create <country> <institution> <dataset> [flags]
|
|
1728
|
+
|
|
1729
|
+
Example:
|
|
1730
|
+
latinfo admin create pe redam debtors \\
|
|
1731
|
+
--url "https://redam.pj.gob.pe/data.csv" \\
|
|
1732
|
+
--id-name dni --id-length 8 --encoding utf-8
|
|
1733
|
+
|
|
1734
|
+
This generates a YAML config in sources/{name}.yaml
|
|
1735
|
+
Edit the YAML to match your data: column indexes, field names, delimiter.
|
|
1736
|
+
|
|
1737
|
+
Step 2: Write and upload your import script
|
|
1738
|
+
latinfo admin upload-script <source-name> <script.ts>
|
|
1739
|
+
|
|
1740
|
+
Your script must:
|
|
1741
|
+
1. Download the source data (HTTP, crawler, API, etc.)
|
|
1742
|
+
2. Parse it into a TSV file: ID\\tfield1\\tfield2\\t...
|
|
1743
|
+
3. Sort by ID: LC_ALL=C sort -t'\\t' -k1,1
|
|
1744
|
+
4. Call buildBinaryFiles() and buildSearchIndex() from ./build-binary and ./build-search-index
|
|
1745
|
+
5. Call uploadToR2() for each output file
|
|
1746
|
+
6. Call saveImportMeta() at the end
|
|
1747
|
+
|
|
1748
|
+
See SOURCES.md in the repo for the full template and common errors.
|
|
1749
|
+
See src/imports/pe-osce-sanctioned.ts for a clean working example.
|
|
1750
|
+
|
|
1751
|
+
Step 3: Test locally
|
|
1752
|
+
latinfo admin test <source-name>
|
|
1753
|
+
|
|
1754
|
+
Runs your import with --limit 100 and validates the output.
|
|
1755
|
+
Must pass before publishing.
|
|
1756
|
+
|
|
1757
|
+
Step 4: Publish to production
|
|
1758
|
+
latinfo admin publish <source-name>
|
|
1759
|
+
|
|
1760
|
+
Commits your YAML + script, pushes to GitHub, triggers the import workflow.
|
|
1761
|
+
After import completes, the data is live at:
|
|
1762
|
+
API: https://api.latinfo.dev/{country}/{institution}/{dataset}/...
|
|
1763
|
+
CLI: latinfo {country} {institution} {dataset} <id|--search query>
|
|
1764
|
+
|
|
1765
|
+
FLAGS FOR CREATE
|
|
1766
|
+
--url <url> Source data download URL
|
|
1767
|
+
--id-name <name> Primary ID field name (default: id)
|
|
1768
|
+
--id-length <n> Primary ID length in digits (default: 11)
|
|
1769
|
+
--encoding <enc> Source file encoding: utf-8 | iso-8859-1 (default: utf-8)
|
|
1770
|
+
--delimiter <d> Field delimiter (default: ,)
|
|
1771
|
+
--format <fmt> Source format: csv | tsv | txt | xlsm (default: csv)
|
|
1772
|
+
|
|
1773
|
+
NAMING CONVENTION
|
|
1774
|
+
Source name: {country}-{institution}-{dataset}
|
|
1775
|
+
Country: ISO 3166-1 alpha-2 lowercase (pe, co, br, mx, ec, ar, cl)
|
|
1776
|
+
Institution: government agency abbreviation, lowercase
|
|
1777
|
+
Dataset: what the data contains, english, lowercase
|
|
1778
|
+
|
|
1779
|
+
Examples: pe-sunat-padron, pe-osce-sanctioned, co-rues-registry
|
|
1780
|
+
|
|
1781
|
+
ENVIRONMENT
|
|
1782
|
+
LATINFO_ADMIN_SECRET Auto-detected from ~/.latinfo/admin.secret or .dev.vars
|
|
1783
|
+
LATINFO_REPO_PATH Auto-detected from cwd or ~/Documents/Github/carrerahaus/latinfo-api`);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1412
1786
|
// --- Main ---
|
|
1413
1787
|
const [command, ...args] = rawArgs;
|
|
1788
|
+
const COUNTRIES = ['pe', 'co', 'br', 'mx', 'ar', 'cl', 'ec'];
|
|
1414
1789
|
if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
|
|
1415
1790
|
version();
|
|
1416
1791
|
}
|
|
1792
|
+
else if (COUNTRIES.includes(command)) {
|
|
1793
|
+
// New structure: latinfo <country> <institution> <dataset> [args]
|
|
1794
|
+
const [institution, dataset, ...datasetArgs] = args;
|
|
1795
|
+
if (!institution || !dataset) {
|
|
1796
|
+
console.error(`Usage: latinfo ${command} <institution> <dataset> <id|--search query|--dni id>`);
|
|
1797
|
+
console.error(`Example: latinfo ${command} sunat padron 20100047218`);
|
|
1798
|
+
process.exit(1);
|
|
1799
|
+
}
|
|
1800
|
+
// Special case: pe oece tenders → licitaciones (custom routes)
|
|
1801
|
+
if (command === 'pe' && institution === 'oece' && dataset === 'tenders') {
|
|
1802
|
+
licitaciones(datasetArgs).catch(e => { console.error(e); process.exit(1); });
|
|
1803
|
+
}
|
|
1804
|
+
else {
|
|
1805
|
+
const routePath = `/${command}/${institution}/${dataset}`;
|
|
1806
|
+
sourceQuery(routePath, datasetArgs).catch(e => { console.error(e); process.exit(1); });
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1417
1809
|
else {
|
|
1810
|
+
// Admin commands (flat)
|
|
1418
1811
|
switch (command) {
|
|
1419
1812
|
case 'login':
|
|
1420
1813
|
login(tokenFlag).catch(e => { console.error(e); process.exit(1); });
|
|
@@ -1442,25 +1835,12 @@ else {
|
|
|
1442
1835
|
case 'costs':
|
|
1443
1836
|
(liveFlag ? costsLive() : Promise.resolve(costsSimulate(args[0], args[1], args[2]))).catch(e => { console.error(e); process.exit(1); });
|
|
1444
1837
|
break;
|
|
1445
|
-
case 'ruc':
|
|
1446
|
-
ruc(args[0]).catch(e => { console.error(e); process.exit(1); });
|
|
1447
|
-
break;
|
|
1448
|
-
case 'dni':
|
|
1449
|
-
dni(args[0]).catch(e => { console.error(e); process.exit(1); });
|
|
1450
|
-
break;
|
|
1451
|
-
case 'search':
|
|
1452
|
-
search(args.join(' ')).catch(e => { console.error(e); process.exit(1); });
|
|
1453
|
-
break;
|
|
1454
|
-
case 'debtors':
|
|
1455
|
-
debtors(args).catch(e => { console.error(e); process.exit(1); });
|
|
1456
|
-
break;
|
|
1457
|
-
case 'licitaciones':
|
|
1458
|
-
case 'lic':
|
|
1459
|
-
licitaciones(args).catch(e => { console.error(e); process.exit(1); });
|
|
1460
|
-
break;
|
|
1461
1838
|
case 'bench':
|
|
1462
1839
|
bench(args).catch(e => { console.error(e); process.exit(1); });
|
|
1463
1840
|
break;
|
|
1841
|
+
case 'admin':
|
|
1842
|
+
admin(args).catch(e => { console.error(e); process.exit(1); });
|
|
1843
|
+
break;
|
|
1464
1844
|
case 'easypipe':
|
|
1465
1845
|
case 'ep':
|
|
1466
1846
|
easypipe(args).catch(e => { console.error(e); process.exit(1); });
|
|
@@ -1471,6 +1851,23 @@ else {
|
|
|
1471
1851
|
case 'help':
|
|
1472
1852
|
help();
|
|
1473
1853
|
break;
|
|
1854
|
+
// Backward compat: old flat commands redirect
|
|
1855
|
+
case 'ruc':
|
|
1856
|
+
sourceQuery('/pe/sunat/padron', args).catch(e => { console.error(e); process.exit(1); });
|
|
1857
|
+
break;
|
|
1858
|
+
case 'dni':
|
|
1859
|
+
sourceQuery('/pe/sunat/padron', ['--dni', ...args]).catch(e => { console.error(e); process.exit(1); });
|
|
1860
|
+
break;
|
|
1861
|
+
case 'search':
|
|
1862
|
+
sourceQuery('/pe/sunat/padron', ['--search', ...args]).catch(e => { console.error(e); process.exit(1); });
|
|
1863
|
+
break;
|
|
1864
|
+
case 'debtors':
|
|
1865
|
+
sourceQuery('/pe/sunat/coactiva', args).catch(e => { console.error(e); process.exit(1); });
|
|
1866
|
+
break;
|
|
1867
|
+
case 'licitaciones':
|
|
1868
|
+
case 'lic':
|
|
1869
|
+
licitaciones(args).catch(e => { console.error(e); process.exit(1); });
|
|
1870
|
+
break;
|
|
1474
1871
|
default:
|
|
1475
1872
|
printLogo();
|
|
1476
1873
|
help();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "latinfo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
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": {
|