latinfo 0.20.0 → 0.20.2
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 +571 -52
- package/dist/licitaciones.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -49,7 +49,7 @@ const odis_search_1 = require("./odis-search");
|
|
|
49
49
|
const mphf_search_1 = require("./mphf-search");
|
|
50
50
|
const VERSION = '0.18.1';
|
|
51
51
|
const API_URL = process.env.LATINFO_API_URL || 'https://api.latinfo.dev';
|
|
52
|
-
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || '
|
|
52
|
+
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || 'Ov23liZAqpaGnYQ6Kp5I';
|
|
53
53
|
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.latinfo');
|
|
54
54
|
const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
|
|
55
55
|
// --- JSON mode ---
|
|
@@ -63,6 +63,10 @@ function jsonError(error, message) {
|
|
|
63
63
|
process.exit(1);
|
|
64
64
|
}
|
|
65
65
|
function loadConfig() {
|
|
66
|
+
// Env var takes priority — zero friction for agents
|
|
67
|
+
if (process.env.LATINFO_API_KEY) {
|
|
68
|
+
return { api_key: process.env.LATINFO_API_KEY, github_username: '', is_team: true, team_role: 'member' };
|
|
69
|
+
}
|
|
66
70
|
try {
|
|
67
71
|
return JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
68
72
|
}
|
|
@@ -153,7 +157,35 @@ async function apiRequest(config, path) {
|
|
|
153
157
|
return res;
|
|
154
158
|
}
|
|
155
159
|
// --- Commands ---
|
|
156
|
-
async function login(token) {
|
|
160
|
+
async function login(token, username) {
|
|
161
|
+
// Zero-friction login: admin generates key for a username (no browser)
|
|
162
|
+
if (username) {
|
|
163
|
+
let adminSecret;
|
|
164
|
+
try {
|
|
165
|
+
adminSecret = requireAdmin();
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
console.error('Admin access required for username login. Need ADMIN_SECRET in .dev.vars or env.');
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
const res = await fetch(`${API_URL}/team/members`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { Authorization: `Bearer ${adminSecret}`, 'Content-Type': 'application/json' },
|
|
174
|
+
body: JSON.stringify({ github_username: username }),
|
|
175
|
+
});
|
|
176
|
+
const data = await res.json();
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
console.error(data.message || data.error);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
if (!data.api_key) {
|
|
182
|
+
console.error('Failed to generate API key');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
saveConfig({ api_key: data.api_key, github_username: username, is_team: true, team_role: data.role });
|
|
186
|
+
console.log(`Logged in as ${username} (${data.role}).`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
157
189
|
// PAT login: no browser needed
|
|
158
190
|
if (token) {
|
|
159
191
|
const authRes = await fetch(`${API_URL}/auth/github`, {
|
|
@@ -169,33 +201,86 @@ async function login(token) {
|
|
|
169
201
|
process.exit(1);
|
|
170
202
|
}
|
|
171
203
|
const authData = await authRes.json();
|
|
172
|
-
|
|
173
|
-
|
|
204
|
+
const config = { api_key: authData.api_key, github_username: authData.github_username };
|
|
205
|
+
// Check team membership
|
|
206
|
+
try {
|
|
207
|
+
const teamRes = await fetch(`${API_URL}/team/me`, { headers: { Authorization: `Bearer ${config.api_key}` } });
|
|
208
|
+
if (teamRes.ok) {
|
|
209
|
+
const team = await teamRes.json();
|
|
210
|
+
config.is_team = true;
|
|
211
|
+
config.team_role = team.role;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch { }
|
|
215
|
+
saveConfig(config);
|
|
216
|
+
console.log(`Logged in as ${authData.github_username}${config.is_team ? ` (team: ${config.team_role})` : ''}`);
|
|
174
217
|
return;
|
|
175
218
|
}
|
|
176
|
-
//
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
console.log(
|
|
184
|
-
console.log(
|
|
185
|
-
|
|
186
|
-
|
|
219
|
+
// Device Flow: no browser redirect needed, works in SSH/containers/agents
|
|
220
|
+
const deviceRes = await fetch('https://github.com/login/device/code', {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
223
|
+
body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, scope: 'read:user,user:email' }),
|
|
224
|
+
});
|
|
225
|
+
const device = await deviceRes.json();
|
|
226
|
+
console.log(`\n Go to: ${device.verification_uri}`);
|
|
227
|
+
console.log(` Enter code: ${device.user_code}\n`);
|
|
228
|
+
// Try to open browser automatically
|
|
229
|
+
try {
|
|
230
|
+
openBrowser(device.verification_uri);
|
|
231
|
+
}
|
|
232
|
+
catch { }
|
|
233
|
+
// Poll until authorized
|
|
234
|
+
let ghAccessToken = null;
|
|
235
|
+
const deadline = Date.now() + device.expires_in * 1000;
|
|
236
|
+
while (Date.now() < deadline) {
|
|
237
|
+
await new Promise(r => setTimeout(r, (device.interval || 5) * 1000));
|
|
238
|
+
const pollRes = await fetch('https://github.com/login/oauth/access_token', {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
241
|
+
body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, device_code: device.device_code, grant_type: 'urn:ietf:params:oauth:grant-type:device_code' }),
|
|
242
|
+
});
|
|
243
|
+
const poll = await pollRes.json();
|
|
244
|
+
if (poll.access_token) {
|
|
245
|
+
ghAccessToken = poll.access_token;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
if (poll.error === 'expired_token') {
|
|
249
|
+
console.error('Code expired. Run latinfo login again.');
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
if (poll.error && poll.error !== 'authorization_pending' && poll.error !== 'slow_down') {
|
|
253
|
+
console.error(`GitHub error: ${poll.error}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!ghAccessToken) {
|
|
258
|
+
console.error('Login timed out.');
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
187
261
|
const authRes = await fetch(`${API_URL}/auth/github`, {
|
|
188
262
|
method: 'POST',
|
|
189
263
|
headers: { 'Content-Type': 'application/json' },
|
|
190
|
-
body: JSON.stringify({
|
|
264
|
+
body: JSON.stringify({ access_token: ghAccessToken }),
|
|
191
265
|
});
|
|
192
266
|
if (!authRes.ok) {
|
|
193
267
|
console.error('Error getting API key:', await authRes.text());
|
|
194
268
|
process.exit(1);
|
|
195
269
|
}
|
|
196
270
|
const authData = await authRes.json();
|
|
197
|
-
|
|
198
|
-
|
|
271
|
+
const config = { api_key: authData.api_key, github_username: authData.github_username };
|
|
272
|
+
// Check team membership
|
|
273
|
+
try {
|
|
274
|
+
const teamRes = await fetch(`${API_URL}/team/me`, { headers: { Authorization: `Bearer ${config.api_key}` } });
|
|
275
|
+
if (teamRes.ok) {
|
|
276
|
+
const team = await teamRes.json();
|
|
277
|
+
config.is_team = true;
|
|
278
|
+
config.team_role = team.role;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch { }
|
|
282
|
+
saveConfig(config);
|
|
283
|
+
console.log(`Logged in as ${authData.github_username}${config.is_team ? ` (team: ${config.team_role})` : ''}`);
|
|
199
284
|
}
|
|
200
285
|
async function ruc(rucNumber) {
|
|
201
286
|
if (!rucNumber || !/^\d{11}$/.test(rucNumber)) {
|
|
@@ -477,10 +562,17 @@ async function search(query) {
|
|
|
477
562
|
function whoami() {
|
|
478
563
|
const config = requireAuth();
|
|
479
564
|
if (jsonFlag) {
|
|
480
|
-
console.log(JSON.stringify({ username: config.github_username, api_key: config.api_key }));
|
|
565
|
+
console.log(JSON.stringify({ username: config.github_username, api_key: config.api_key, is_team: config.is_team, team_role: config.team_role }));
|
|
481
566
|
return;
|
|
482
567
|
}
|
|
483
|
-
|
|
568
|
+
if (config.is_team) {
|
|
569
|
+
const badges = { admin: '★', member: '●' };
|
|
570
|
+
const badge = badges[config.team_role || 'member'] || '●';
|
|
571
|
+
console.log(`${badge} ${config.github_username} [TEAM ${(config.team_role || 'member').toUpperCase()}]`);
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
console.log(config.github_username);
|
|
575
|
+
}
|
|
484
576
|
}
|
|
485
577
|
async function plan() {
|
|
486
578
|
const config = requireAuth();
|
|
@@ -553,7 +645,7 @@ async function adminRequest(path) {
|
|
|
553
645
|
return res;
|
|
554
646
|
}
|
|
555
647
|
async function importsRun(source) {
|
|
556
|
-
const valid = ['pe-sunat-padron', 'pe-oece-
|
|
648
|
+
const valid = ['pe-sunat-padron', 'pe-oece-tenders', 'co-rues', 'all'];
|
|
557
649
|
if (!valid.includes(source)) {
|
|
558
650
|
console.error(`Unknown source. Valid: ${valid.join(', ')}`);
|
|
559
651
|
process.exit(1);
|
|
@@ -1450,11 +1542,12 @@ function logout() {
|
|
|
1450
1542
|
console.log('Logged out.');
|
|
1451
1543
|
}
|
|
1452
1544
|
function help() {
|
|
1545
|
+
const config = loadConfig();
|
|
1546
|
+
const isTeam = config?.is_team;
|
|
1453
1547
|
console.log(`latinfo v${VERSION} — Tax registry API for Latin America
|
|
1454
1548
|
|
|
1455
1549
|
USAGE
|
|
1456
1550
|
latinfo <country> <institution> <dataset> <id|--search query|--dni id> [--json]
|
|
1457
|
-
latinfo <admin-command> [args]
|
|
1458
1551
|
|
|
1459
1552
|
QUICK START
|
|
1460
1553
|
npm install -g latinfo
|
|
@@ -1477,25 +1570,26 @@ DATA SOURCES
|
|
|
1477
1570
|
latinfo pe osce fines <ruc> Provider fines
|
|
1478
1571
|
latinfo pe osce fines --search <query>
|
|
1479
1572
|
|
|
1573
|
+
Peru — SERVIR
|
|
1574
|
+
latinfo pe servir sanctions <dni> Public sector sanctions
|
|
1575
|
+
latinfo pe servir sanctions --search <query>
|
|
1576
|
+
|
|
1577
|
+
Peru — REDAM
|
|
1578
|
+
latinfo pe redam registry <dni> Food debt debtors
|
|
1579
|
+
latinfo pe redam registry --search <query>
|
|
1580
|
+
|
|
1480
1581
|
Peru — OECE
|
|
1481
1582
|
latinfo pe oece tenders <query> [flags] Government procurement
|
|
1482
1583
|
Flags: --category, --min-amount, --max-amount, --buyer, --status, --limit
|
|
1483
1584
|
|
|
1484
1585
|
Colombia — RUES
|
|
1485
|
-
latinfo co rues registry <nit> Business registry (
|
|
1586
|
+
latinfo co rues registry <nit> Business registry (9M+ records)
|
|
1486
1587
|
latinfo co rues registry --search <query>
|
|
1487
1588
|
|
|
1488
|
-
|
|
1589
|
+
COMMANDS
|
|
1489
1590
|
login [--token <github_pat>] GitHub OAuth or PAT login
|
|
1490
1591
|
logout Remove credentials
|
|
1491
1592
|
whoami Show authenticated user
|
|
1492
|
-
imports Show import status
|
|
1493
|
-
imports run <source> Trigger import
|
|
1494
|
-
imports report [days] Import diagnostics
|
|
1495
|
-
costs <users> [avg_req] [pro_%] Cost simulation
|
|
1496
|
-
costs --live Production cost report
|
|
1497
|
-
bench [flags] Stress test API
|
|
1498
|
-
easypipe <command> Generic import pipeline
|
|
1499
1593
|
completion [bash|zsh] Shell completions
|
|
1500
1594
|
help This help text
|
|
1501
1595
|
|
|
@@ -1505,13 +1599,34 @@ FLAGS
|
|
|
1505
1599
|
--dni Lookup by DNI (Peru only)
|
|
1506
1600
|
--version Print version
|
|
1507
1601
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1602
|
+
Free and unlimited. No credit card needed.`);
|
|
1603
|
+
if (isTeam) {
|
|
1604
|
+
const isAdmin = config?.team_role === 'admin';
|
|
1605
|
+
console.log(`
|
|
1606
|
+
TEAM
|
|
1607
|
+
tasks My tasks
|
|
1608
|
+
tasks complete <id> Mark done
|
|
1609
|
+
tasks rank Team ranking
|
|
1610
|
+
pipe local <source> Import data locally
|
|
1611
|
+
pipe publish <source> Publish to production
|
|
1612
|
+
docs <topic> Internal documentation`);
|
|
1613
|
+
if (isAdmin) {
|
|
1614
|
+
console.log(`
|
|
1615
|
+
ADMIN
|
|
1616
|
+
team add <username> [--admin] Add team member
|
|
1617
|
+
team remove <username> Remove member
|
|
1618
|
+
team list List all members
|
|
1619
|
+
tasks assign <user> "<title>" [--points N] Assign task
|
|
1620
|
+
tasks approve <id> Approve + award points
|
|
1621
|
+
tasks reject <id> "<reason>" Reject back
|
|
1622
|
+
tasks delete <id> Delete task
|
|
1623
|
+
tasks list --all All tasks
|
|
1624
|
+
imports Show import status
|
|
1625
|
+
imports run <source> Trigger import
|
|
1626
|
+
bench [flags] Stress test API
|
|
1627
|
+
costs --live Production cost report`);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1515
1630
|
}
|
|
1516
1631
|
function printLogo() {
|
|
1517
1632
|
if (!process.stdout.isTTY)
|
|
@@ -2710,14 +2825,47 @@ async function pipePublish(args) {
|
|
|
2710
2825
|
console.error(`[pipe] Git error: ${e.message}`);
|
|
2711
2826
|
process.exit(1);
|
|
2712
2827
|
}
|
|
2713
|
-
// 2. Deploy Worker
|
|
2714
|
-
console.log(`[pipe]
|
|
2828
|
+
// 2. Deploy Worker via GitHub Actions (no local Cloudflare token needed)
|
|
2829
|
+
console.log(`[pipe] Waiting for deploy workflow (GitHub Actions)...`);
|
|
2715
2830
|
try {
|
|
2716
|
-
|
|
2831
|
+
const { execSync: exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
2832
|
+
const maxWait = 300; // 5 min max
|
|
2833
|
+
const interval = 10;
|
|
2834
|
+
let elapsed = 0;
|
|
2835
|
+
let deployed = false;
|
|
2836
|
+
// Give GitHub a moment to register the push event
|
|
2837
|
+
exec('sleep 5');
|
|
2838
|
+
while (elapsed < maxWait) {
|
|
2839
|
+
const result = exec(`gh run list --workflow=deploy.yml --branch=main --limit=1 --json status,conclusion,headSha`, { cwd: repo, encoding: 'utf-8', stdio: 'pipe' }).toString().trim();
|
|
2840
|
+
const ghRuns = JSON.parse(result);
|
|
2841
|
+
if (ghRuns.length > 0) {
|
|
2842
|
+
const latest = ghRuns[0];
|
|
2843
|
+
if (latest.status === 'completed') {
|
|
2844
|
+
if (latest.conclusion === 'success') {
|
|
2845
|
+
console.log(`[pipe] Deploy successful.`);
|
|
2846
|
+
deployed = true;
|
|
2847
|
+
break;
|
|
2848
|
+
}
|
|
2849
|
+
else {
|
|
2850
|
+
console.error(`[pipe] Deploy failed (${latest.conclusion}) — rolling back`);
|
|
2851
|
+
exec(`git checkout HEAD^ -- src/sources.ts .github/workflows/import.yml && git commit -m "Rollback: remove ${sourceName}" && git push`, { cwd: repo, stdio: 'pipe' });
|
|
2852
|
+
process.exit(1);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
if (elapsed % 30 === 0 && elapsed > 0) {
|
|
2856
|
+
console.log(`[pipe] Still deploying... (${elapsed}s)`);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
exec(`sleep ${interval}`);
|
|
2860
|
+
elapsed += interval;
|
|
2861
|
+
}
|
|
2862
|
+
if (!deployed) {
|
|
2863
|
+
console.error(`[pipe] Deploy timed out after ${maxWait}s`);
|
|
2864
|
+
process.exit(1);
|
|
2865
|
+
}
|
|
2717
2866
|
}
|
|
2718
|
-
catch {
|
|
2719
|
-
console.error(`[pipe] Deploy
|
|
2720
|
-
run(`git checkout HEAD^ -- src/sources.ts .github/workflows/import.yml && git commit -m "Rollback: remove ${sourceName}" && git push`, { cwd: repo, stdio: 'pipe' });
|
|
2867
|
+
catch (e) {
|
|
2868
|
+
console.error(`[pipe] Deploy error: ${e.message}`);
|
|
2721
2869
|
process.exit(1);
|
|
2722
2870
|
}
|
|
2723
2871
|
// 3. Trigger import on runner
|
|
@@ -3408,6 +3556,7 @@ const DOCS = {
|
|
|
3408
3556
|
index: `latinfo docs — complete documentation
|
|
3409
3557
|
|
|
3410
3558
|
TOPICS
|
|
3559
|
+
latinfo docs team Team system, tasks, ranking, and how the AI PM works
|
|
3411
3560
|
latinfo docs pipe How to create a data pipeline (full guide)
|
|
3412
3561
|
latinfo docs fields searchFieldIndex, statusFieldIndex explained
|
|
3413
3562
|
latinfo docs v2 V2 search index + MPHF (mandatory)
|
|
@@ -3416,6 +3565,75 @@ TOPICS
|
|
|
3416
3565
|
latinfo docs troubleshooting Common errors and fixes
|
|
3417
3566
|
latinfo docs architecture How latinfo works internally
|
|
3418
3567
|
latinfo docs api API endpoints and response format`,
|
|
3568
|
+
team: `TEAM SYSTEM
|
|
3569
|
+
|
|
3570
|
+
The AI agent is the project manager (PM). It assigns tasks, tracks progress,
|
|
3571
|
+
and coordinates the team through the CLI. No human PM needed.
|
|
3572
|
+
|
|
3573
|
+
GETTING STARTED (new member)
|
|
3574
|
+
|
|
3575
|
+
1. Install: npm i -g latinfo
|
|
3576
|
+
2. Login: latinfo login (authenticates with your GitHub account)
|
|
3577
|
+
3. See tasks: latinfo tasks (shows your assigned tasks)
|
|
3578
|
+
4. Complete: latinfo tasks complete <id>
|
|
3579
|
+
5. Ranking: latinfo tasks rank
|
|
3580
|
+
|
|
3581
|
+
ADMIN COMMANDS (requires admin access)
|
|
3582
|
+
|
|
3583
|
+
latinfo team add <username> Add team member
|
|
3584
|
+
latinfo team add <username> --admin Add as admin
|
|
3585
|
+
latinfo team remove <username> Remove member
|
|
3586
|
+
latinfo team list List all members + points
|
|
3587
|
+
|
|
3588
|
+
latinfo tasks assign <user> "<title>" [--points N] Assign task (default 10 pts)
|
|
3589
|
+
latinfo tasks approve <id> Approve completed task (awards points)
|
|
3590
|
+
latinfo tasks reject <id> "<reason>" Reject back to member
|
|
3591
|
+
latinfo tasks delete <id> Delete task
|
|
3592
|
+
latinfo tasks list --all See all tasks from all members
|
|
3593
|
+
|
|
3594
|
+
MEMBER COMMANDS (requires latinfo login)
|
|
3595
|
+
|
|
3596
|
+
latinfo tasks My tasks
|
|
3597
|
+
latinfo tasks complete <id> Mark task as done (waits for approval)
|
|
3598
|
+
latinfo tasks rank Team ranking
|
|
3599
|
+
|
|
3600
|
+
TASK FLOW
|
|
3601
|
+
|
|
3602
|
+
Admin assigns task → Member sees it (pending)
|
|
3603
|
+
Member completes → Status changes to "completed"
|
|
3604
|
+
Admin approves → Points awarded, status "approved"
|
|
3605
|
+
Admin rejects → Back to "pending" with reason
|
|
3606
|
+
|
|
3607
|
+
RANKING LEVELS
|
|
3608
|
+
|
|
3609
|
+
Bronze 0-49 pts
|
|
3610
|
+
Silver 50-149 pts
|
|
3611
|
+
Gold 150-299 pts
|
|
3612
|
+
Diamond 300+ pts
|
|
3613
|
+
|
|
3614
|
+
Points come from approved tasks. The ranking updates automatically.
|
|
3615
|
+
Admins cannot manually change points — only approve/reject tasks.
|
|
3616
|
+
|
|
3617
|
+
HOW THE AI PM WORKS
|
|
3618
|
+
|
|
3619
|
+
The AI agent (Claude, GPT, etc.) can run all these commands autonomously:
|
|
3620
|
+
|
|
3621
|
+
latinfo tasks list --all → See everything
|
|
3622
|
+
latinfo tasks assign ... → Assign work
|
|
3623
|
+
latinfo tasks approve ... → Approve completed work
|
|
3624
|
+
latinfo tasks rank → Report progress
|
|
3625
|
+
|
|
3626
|
+
The AI reads "latinfo docs team" to learn how to operate,
|
|
3627
|
+
then manages the team without human intervention.
|
|
3628
|
+
|
|
3629
|
+
FIRST TIME SETUP (admin)
|
|
3630
|
+
|
|
3631
|
+
1. You (admin) run: latinfo team add <their-github-username>
|
|
3632
|
+
2. They run: latinfo login
|
|
3633
|
+
3. You assign: latinfo tasks assign <username> "Find Mexico SAT data sources"
|
|
3634
|
+
4. They work and: latinfo tasks complete <id>
|
|
3635
|
+
5. You approve: latinfo tasks approve <id>
|
|
3636
|
+
6. Everyone sees: latinfo tasks rank`,
|
|
3419
3637
|
pipe: `HOW TO CREATE A DATA PIPELINE
|
|
3420
3638
|
|
|
3421
3639
|
latinfo pipe handles storage, indexing, search, and API serving automatically.
|
|
@@ -3799,6 +4017,288 @@ function docs(args) {
|
|
|
3799
4017
|
}
|
|
3800
4018
|
console.log(content);
|
|
3801
4019
|
}
|
|
4020
|
+
function requireTeam() {
|
|
4021
|
+
const config = loadConfig();
|
|
4022
|
+
if (!config?.is_team) {
|
|
4023
|
+
console.error('This command is only available to team members.');
|
|
4024
|
+
process.exit(1);
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
function requireTeamAdmin() {
|
|
4028
|
+
const config = loadConfig();
|
|
4029
|
+
if (!config?.is_team) {
|
|
4030
|
+
console.error('This command is only available to team members.');
|
|
4031
|
+
process.exit(1);
|
|
4032
|
+
}
|
|
4033
|
+
if (config.team_role !== 'admin') {
|
|
4034
|
+
console.error('This command requires admin access.');
|
|
4035
|
+
process.exit(1);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
// --- Team & Tasks ---
|
|
4039
|
+
async function teamCmd(args) {
|
|
4040
|
+
const sub = args[0];
|
|
4041
|
+
const adminSecret = requireAdmin();
|
|
4042
|
+
const headers = { Authorization: `Bearer ${adminSecret}`, 'Content-Type': 'application/json' };
|
|
4043
|
+
switch (sub) {
|
|
4044
|
+
case 'add': {
|
|
4045
|
+
const username = args[1];
|
|
4046
|
+
const isAdmin = args.includes('--admin');
|
|
4047
|
+
if (!username) {
|
|
4048
|
+
console.error('Usage: latinfo team add <github-username> [--admin]');
|
|
4049
|
+
process.exit(1);
|
|
4050
|
+
}
|
|
4051
|
+
const res = await fetch(`${API_URL}/team/members`, {
|
|
4052
|
+
method: 'POST', headers,
|
|
4053
|
+
body: JSON.stringify({ github_username: username, role: isAdmin ? 'admin' : 'member' }),
|
|
4054
|
+
});
|
|
4055
|
+
const data = await res.json();
|
|
4056
|
+
if (!res.ok) {
|
|
4057
|
+
console.error(data.message || data.error);
|
|
4058
|
+
process.exit(1);
|
|
4059
|
+
}
|
|
4060
|
+
console.log(`Added ${username} to team (${data.role}).`);
|
|
4061
|
+
if (data.api_key) {
|
|
4062
|
+
if (args.includes('--login')) {
|
|
4063
|
+
// Save key locally — login as this user
|
|
4064
|
+
saveConfig({ api_key: data.api_key, github_username: username, is_team: true, team_role: data.role });
|
|
4065
|
+
console.log(`Logged in as ${username} (${data.role}).`);
|
|
4066
|
+
}
|
|
4067
|
+
else {
|
|
4068
|
+
console.log(`\n API key: ${data.api_key}\n`);
|
|
4069
|
+
console.log(` Share this with the member. They set:`);
|
|
4070
|
+
console.log(` export LATINFO_API_KEY=${data.api_key}`);
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
try {
|
|
4074
|
+
const { execSync: exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
4075
|
+
exec(`gh api repos/carrerahaus/latinfo-api/collaborators/${username} -X PUT -f permission=push`, { stdio: 'pipe' });
|
|
4076
|
+
console.log(`Added ${username} as GitHub collaborator.`);
|
|
4077
|
+
}
|
|
4078
|
+
catch {
|
|
4079
|
+
console.log('Note: Could not add as GitHub collaborator (gh CLI required).');
|
|
4080
|
+
}
|
|
4081
|
+
break;
|
|
4082
|
+
}
|
|
4083
|
+
case 'remove': {
|
|
4084
|
+
const username = args[1];
|
|
4085
|
+
if (!username) {
|
|
4086
|
+
console.error('Usage: latinfo team remove <github-username>');
|
|
4087
|
+
process.exit(1);
|
|
4088
|
+
}
|
|
4089
|
+
const res = await fetch(`${API_URL}/team/members/${username}`, { method: 'DELETE', headers });
|
|
4090
|
+
const data = await res.json();
|
|
4091
|
+
if (!res.ok) {
|
|
4092
|
+
console.error(data.message || data.error);
|
|
4093
|
+
process.exit(1);
|
|
4094
|
+
}
|
|
4095
|
+
console.log(`Removed ${username} from team.`);
|
|
4096
|
+
try {
|
|
4097
|
+
const { execSync: exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
4098
|
+
exec(`gh api repos/carrerahaus/latinfo-api/collaborators/${username} -X DELETE`, { stdio: 'pipe' });
|
|
4099
|
+
console.log(`Removed ${username} from GitHub.`);
|
|
4100
|
+
}
|
|
4101
|
+
catch { }
|
|
4102
|
+
break;
|
|
4103
|
+
}
|
|
4104
|
+
case 'list': {
|
|
4105
|
+
const res = await fetch(`${API_URL}/team/members`, { headers });
|
|
4106
|
+
const data = await res.json();
|
|
4107
|
+
if (!res.ok) {
|
|
4108
|
+
console.error('Failed to list members');
|
|
4109
|
+
process.exit(1);
|
|
4110
|
+
}
|
|
4111
|
+
if (data.length === 0) {
|
|
4112
|
+
console.log('No team members.');
|
|
4113
|
+
return;
|
|
4114
|
+
}
|
|
4115
|
+
console.log('\n TEAM\n');
|
|
4116
|
+
for (const m of data) {
|
|
4117
|
+
const level = m.points >= 300 ? 'Diamond' : m.points >= 150 ? 'Gold' : m.points >= 50 ? 'Silver' : 'Bronze';
|
|
4118
|
+
console.log(` ${m.github_username.padEnd(22)} ${m.role.padEnd(8)} ${String(m.points).padStart(4)} pts ${level}`);
|
|
4119
|
+
}
|
|
4120
|
+
console.log();
|
|
4121
|
+
break;
|
|
4122
|
+
}
|
|
4123
|
+
default:
|
|
4124
|
+
console.log(`Usage:
|
|
4125
|
+
latinfo team add <username> [--admin] Add team member
|
|
4126
|
+
latinfo team remove <username> Remove member
|
|
4127
|
+
latinfo team list List all members`);
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
async function tasksCmd(args) {
|
|
4131
|
+
const sub = args[0];
|
|
4132
|
+
// Admin commands use ADMIN_SECRET
|
|
4133
|
+
if (['assign', 'approve', 'reject', 'delete'].includes(sub)) {
|
|
4134
|
+
const adminSecret = requireAdmin();
|
|
4135
|
+
const headers = { Authorization: `Bearer ${adminSecret}`, 'Content-Type': 'application/json' };
|
|
4136
|
+
switch (sub) {
|
|
4137
|
+
case 'assign': {
|
|
4138
|
+
const username = args[1];
|
|
4139
|
+
const title = args[2];
|
|
4140
|
+
const pointsIdx = args.indexOf('--points');
|
|
4141
|
+
const points = pointsIdx !== -1 ? parseInt(args[pointsIdx + 1]) : 10;
|
|
4142
|
+
if (!username || !title) {
|
|
4143
|
+
console.error('Usage: latinfo tasks assign <username> "<title>" [--points N]');
|
|
4144
|
+
process.exit(1);
|
|
4145
|
+
}
|
|
4146
|
+
const res = await fetch(`${API_URL}/team/tasks`, {
|
|
4147
|
+
method: 'POST', headers,
|
|
4148
|
+
body: JSON.stringify({ assignee_username: username, title, points }),
|
|
4149
|
+
});
|
|
4150
|
+
const data = await res.json();
|
|
4151
|
+
if (!res.ok) {
|
|
4152
|
+
console.error(data.message || data.error);
|
|
4153
|
+
process.exit(1);
|
|
4154
|
+
}
|
|
4155
|
+
console.log(`Task #${data.id} assigned to ${username}: "${title}" (${data.points} pts)`);
|
|
4156
|
+
break;
|
|
4157
|
+
}
|
|
4158
|
+
case 'approve': {
|
|
4159
|
+
const taskId = args[1];
|
|
4160
|
+
if (!taskId) {
|
|
4161
|
+
console.error('Usage: latinfo tasks approve <task-id>');
|
|
4162
|
+
process.exit(1);
|
|
4163
|
+
}
|
|
4164
|
+
const res = await fetch(`${API_URL}/team/tasks/${taskId}`, {
|
|
4165
|
+
method: 'PATCH', headers,
|
|
4166
|
+
body: JSON.stringify({ action: 'approve' }),
|
|
4167
|
+
});
|
|
4168
|
+
const data = await res.json();
|
|
4169
|
+
if (!res.ok) {
|
|
4170
|
+
console.error(data.message || data.error);
|
|
4171
|
+
process.exit(1);
|
|
4172
|
+
}
|
|
4173
|
+
console.log(`Task #${taskId} approved. ${data.points_awarded} points awarded.`);
|
|
4174
|
+
break;
|
|
4175
|
+
}
|
|
4176
|
+
case 'reject': {
|
|
4177
|
+
const taskId = args[1];
|
|
4178
|
+
const reason = args[2] || '';
|
|
4179
|
+
if (!taskId) {
|
|
4180
|
+
console.error('Usage: latinfo tasks reject <task-id> "<reason>"');
|
|
4181
|
+
process.exit(1);
|
|
4182
|
+
}
|
|
4183
|
+
const res = await fetch(`${API_URL}/team/tasks/${taskId}`, {
|
|
4184
|
+
method: 'PATCH', headers,
|
|
4185
|
+
body: JSON.stringify({ action: 'reject', reason }),
|
|
4186
|
+
});
|
|
4187
|
+
const data = await res.json();
|
|
4188
|
+
if (!res.ok) {
|
|
4189
|
+
console.error(data.message || data.error);
|
|
4190
|
+
process.exit(1);
|
|
4191
|
+
}
|
|
4192
|
+
console.log(`Task #${taskId} rejected.`);
|
|
4193
|
+
break;
|
|
4194
|
+
}
|
|
4195
|
+
case 'delete': {
|
|
4196
|
+
const taskId = args[1];
|
|
4197
|
+
if (!taskId) {
|
|
4198
|
+
console.error('Usage: latinfo tasks delete <task-id>');
|
|
4199
|
+
process.exit(1);
|
|
4200
|
+
}
|
|
4201
|
+
const res = await fetch(`${API_URL}/team/tasks/${taskId}`, { method: 'DELETE', headers });
|
|
4202
|
+
const data = await res.json();
|
|
4203
|
+
if (!res.ok) {
|
|
4204
|
+
console.error(data.message || data.error);
|
|
4205
|
+
process.exit(1);
|
|
4206
|
+
}
|
|
4207
|
+
console.log(`Task #${taskId} deleted.`);
|
|
4208
|
+
break;
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
return;
|
|
4212
|
+
}
|
|
4213
|
+
// Member commands use API key
|
|
4214
|
+
const config = loadConfig();
|
|
4215
|
+
if (!config?.api_key) {
|
|
4216
|
+
console.error('Not logged in. Run: latinfo login');
|
|
4217
|
+
process.exit(1);
|
|
4218
|
+
}
|
|
4219
|
+
const headers = { Authorization: `Bearer ${config.api_key}`, 'Content-Type': 'application/json' };
|
|
4220
|
+
switch (sub) {
|
|
4221
|
+
case 'complete': {
|
|
4222
|
+
const taskId = args[1];
|
|
4223
|
+
if (!taskId) {
|
|
4224
|
+
console.error('Usage: latinfo tasks complete <task-id>');
|
|
4225
|
+
process.exit(1);
|
|
4226
|
+
}
|
|
4227
|
+
const res = await fetch(`${API_URL}/team/tasks/${taskId}`, {
|
|
4228
|
+
method: 'PATCH', headers,
|
|
4229
|
+
body: JSON.stringify({ action: 'complete' }),
|
|
4230
|
+
});
|
|
4231
|
+
const data = await res.json();
|
|
4232
|
+
if (!res.ok) {
|
|
4233
|
+
console.error(data.message || data.error);
|
|
4234
|
+
process.exit(1);
|
|
4235
|
+
}
|
|
4236
|
+
console.log(`Task #${taskId} marked as completed. Waiting for approval.`);
|
|
4237
|
+
break;
|
|
4238
|
+
}
|
|
4239
|
+
case 'rank': {
|
|
4240
|
+
const res = await fetch(`${API_URL}/team/rank`, { headers });
|
|
4241
|
+
const ranking = await res.json();
|
|
4242
|
+
if (!res.ok) {
|
|
4243
|
+
console.error('Failed to get ranking');
|
|
4244
|
+
process.exit(1);
|
|
4245
|
+
}
|
|
4246
|
+
if (ranking.length === 0) {
|
|
4247
|
+
console.log('No team members yet.');
|
|
4248
|
+
return;
|
|
4249
|
+
}
|
|
4250
|
+
const badges = { Bronze: '●', Silver: '◆', Gold: '★', Diamond: '◈' };
|
|
4251
|
+
console.log('\n RANKING\n');
|
|
4252
|
+
for (const r of ranking) {
|
|
4253
|
+
console.log(` #${r.rank} ${r.username.padEnd(22)} ${String(r.points).padStart(4)} pts ${badges[r.level] || ''} ${r.level}`);
|
|
4254
|
+
}
|
|
4255
|
+
console.log();
|
|
4256
|
+
break;
|
|
4257
|
+
}
|
|
4258
|
+
case 'list': {
|
|
4259
|
+
const allFlag = args.includes('--all');
|
|
4260
|
+
const url = allFlag ? `${API_URL}/team/tasks?all=true` : `${API_URL}/team/tasks`;
|
|
4261
|
+
const reqHeaders = allFlag
|
|
4262
|
+
? { ...headers, 'X-Admin-Secret': requireAdmin() }
|
|
4263
|
+
: headers;
|
|
4264
|
+
const res = await fetch(url, { headers: reqHeaders });
|
|
4265
|
+
const tasksList = await res.json();
|
|
4266
|
+
if (!res.ok) {
|
|
4267
|
+
console.error('Failed to list tasks');
|
|
4268
|
+
process.exit(1);
|
|
4269
|
+
}
|
|
4270
|
+
if (tasksList.length === 0) {
|
|
4271
|
+
console.log('No tasks.');
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
const icons = { pending: '○', completed: '◉', approved: '★' };
|
|
4275
|
+
for (const t of tasksList) {
|
|
4276
|
+
const assignee = allFlag ? ` @${t.assignee_username}` : '';
|
|
4277
|
+
const rejection = t.status === 'pending' && t.reject_reason ? ` (rejected: ${t.reject_reason})` : '';
|
|
4278
|
+
console.log(` [#${t.id}] ${icons[t.status] || t.status} ${t.title}${assignee} (${t.points} pts)${rejection}`);
|
|
4279
|
+
}
|
|
4280
|
+
break;
|
|
4281
|
+
}
|
|
4282
|
+
default: {
|
|
4283
|
+
// No subcommand = show my tasks
|
|
4284
|
+
const res = await fetch(`${API_URL}/team/tasks`, { headers });
|
|
4285
|
+
const tasksList = await res.json();
|
|
4286
|
+
if (!res.ok) {
|
|
4287
|
+
console.error('Failed to list tasks');
|
|
4288
|
+
process.exit(1);
|
|
4289
|
+
}
|
|
4290
|
+
if (tasksList.length === 0) {
|
|
4291
|
+
console.log('No tasks assigned to you.');
|
|
4292
|
+
return;
|
|
4293
|
+
}
|
|
4294
|
+
const icons = { pending: '○', completed: '◉', approved: '★' };
|
|
4295
|
+
for (const t of tasksList) {
|
|
4296
|
+
const rejection = t.status === 'pending' && t.reject_reason ? ` (rejected: ${t.reject_reason})` : '';
|
|
4297
|
+
console.log(` [#${t.id}] ${icons[t.status] || t.status} ${t.title} (${t.points} pts)${rejection}`);
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
3802
4302
|
// --- Main ---
|
|
3803
4303
|
const [command, ...args] = rawArgs;
|
|
3804
4304
|
const COUNTRIES = ['pe', 'co', 'br', 'mx', 'ar', 'cl', 'ec'];
|
|
@@ -3826,7 +4326,7 @@ else {
|
|
|
3826
4326
|
// Admin commands (flat)
|
|
3827
4327
|
switch (command) {
|
|
3828
4328
|
case 'login':
|
|
3829
|
-
login(tokenFlag).catch(e => { console.error(e); process.exit(1); });
|
|
4329
|
+
login(tokenFlag, args[0]).catch(e => { console.error(e); process.exit(1); });
|
|
3830
4330
|
break;
|
|
3831
4331
|
case 'logout':
|
|
3832
4332
|
logout();
|
|
@@ -3837,7 +4337,15 @@ else {
|
|
|
3837
4337
|
case 'users':
|
|
3838
4338
|
users().catch(e => { console.error(e); process.exit(1); });
|
|
3839
4339
|
break;
|
|
4340
|
+
case 'plan':
|
|
4341
|
+
plan().catch(e => { console.error(e); process.exit(1); });
|
|
4342
|
+
break;
|
|
4343
|
+
case 'completion':
|
|
4344
|
+
completion();
|
|
4345
|
+
break;
|
|
4346
|
+
// Team-only commands
|
|
3840
4347
|
case 'imports':
|
|
4348
|
+
requireTeam();
|
|
3841
4349
|
if (args[0] === 'run')
|
|
3842
4350
|
importsRun(args[1] || 'all').catch(e => { console.error(e); process.exit(1); });
|
|
3843
4351
|
else if (args[0] === 'report')
|
|
@@ -3845,38 +4353,49 @@ else {
|
|
|
3845
4353
|
else
|
|
3846
4354
|
imports().catch(e => { console.error(e); process.exit(1); });
|
|
3847
4355
|
break;
|
|
3848
|
-
case 'plan':
|
|
3849
|
-
plan().catch(e => { console.error(e); process.exit(1); });
|
|
3850
|
-
break;
|
|
3851
4356
|
case 'costs':
|
|
4357
|
+
requireTeamAdmin();
|
|
3852
4358
|
(liveFlag ? costsLive() : Promise.resolve(costsSimulate(args[0], args[1], args[2]))).catch(e => { console.error(e); process.exit(1); });
|
|
3853
4359
|
break;
|
|
3854
4360
|
case 'bench':
|
|
4361
|
+
requireTeamAdmin();
|
|
3855
4362
|
bench(args).catch(e => { console.error(e); process.exit(1); });
|
|
3856
4363
|
break;
|
|
3857
4364
|
case 'search-server':
|
|
4365
|
+
requireTeamAdmin();
|
|
3858
4366
|
searchServerStatus().catch(e => { console.error(e); process.exit(1); });
|
|
3859
4367
|
break;
|
|
4368
|
+
case 'team':
|
|
4369
|
+
requireTeamAdmin();
|
|
4370
|
+
teamCmd(args).catch(e => { console.error(e); process.exit(1); });
|
|
4371
|
+
break;
|
|
4372
|
+
case 'tasks':
|
|
4373
|
+
requireTeam();
|
|
4374
|
+
tasksCmd(args).catch(e => { console.error(e); process.exit(1); });
|
|
4375
|
+
break;
|
|
3860
4376
|
case 'pipe':
|
|
4377
|
+
requireTeam();
|
|
3861
4378
|
pipe(args).catch(e => { console.error(e); process.exit(1); });
|
|
3862
4379
|
break;
|
|
3863
4380
|
case 'admin':
|
|
4381
|
+
requireTeamAdmin();
|
|
3864
4382
|
pipe(args).catch(e => { console.error(e); process.exit(1); });
|
|
3865
|
-
break;
|
|
4383
|
+
break;
|
|
3866
4384
|
case 'easypipe':
|
|
3867
4385
|
case 'ep':
|
|
4386
|
+
requireTeam();
|
|
3868
4387
|
easypipe(args).catch(e => { console.error(e); process.exit(1); });
|
|
3869
4388
|
break;
|
|
3870
4389
|
case 'report':
|
|
4390
|
+
requireTeamAdmin();
|
|
3871
4391
|
report(args).catch(e => { console.error(e); process.exit(1); });
|
|
3872
4392
|
break;
|
|
3873
4393
|
case 'issues':
|
|
4394
|
+
requireTeamAdmin();
|
|
3874
4395
|
issues().catch(e => { console.error(e); process.exit(1); });
|
|
3875
4396
|
break;
|
|
3876
|
-
case 'completion':
|
|
3877
|
-
completion();
|
|
3878
|
-
break;
|
|
3879
4397
|
case 'docs':
|
|
4398
|
+
requireTeam();
|
|
3880
4399
|
docs(args);
|
|
3881
4400
|
break;
|
|
3882
4401
|
case 'help':
|
package/dist/licitaciones.js
CHANGED
|
@@ -45,7 +45,7 @@ exports.dataInfo = dataInfo;
|
|
|
45
45
|
const fs = __importStar(require("fs"));
|
|
46
46
|
const path = __importStar(require("path"));
|
|
47
47
|
const os_1 = __importDefault(require("os"));
|
|
48
|
-
const BASE_NAME = 'pe-oece-
|
|
48
|
+
const BASE_NAME = 'pe-oece-tenders';
|
|
49
49
|
const FIELD_COUNT = 12;
|
|
50
50
|
const DATA_DIR = path.join(os_1.default.homedir(), '.latinfo', 'data');
|
|
51
51
|
const FIELD_NAMES = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "latinfo",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
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": {
|