latinfo 0.20.1 → 0.20.3
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 +150 -21
- 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 ---
|
|
@@ -157,7 +157,35 @@ async function apiRequest(config, path) {
|
|
|
157
157
|
return res;
|
|
158
158
|
}
|
|
159
159
|
// --- Commands ---
|
|
160
|
-
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
|
+
}
|
|
161
189
|
// PAT login: no browser needed
|
|
162
190
|
if (token) {
|
|
163
191
|
const authRes = await fetch(`${API_URL}/auth/github`, {
|
|
@@ -188,21 +216,52 @@ async function login(token) {
|
|
|
188
216
|
console.log(`Logged in as ${authData.github_username}${config.is_team ? ` (team: ${config.team_role})` : ''}`);
|
|
189
217
|
return;
|
|
190
218
|
}
|
|
191
|
-
//
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
console.log(
|
|
199
|
-
console.log(
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
}
|
|
202
261
|
const authRes = await fetch(`${API_URL}/auth/github`, {
|
|
203
262
|
method: 'POST',
|
|
204
263
|
headers: { 'Content-Type': 'application/json' },
|
|
205
|
-
body: JSON.stringify({
|
|
264
|
+
body: JSON.stringify({ access_token: ghAccessToken }),
|
|
206
265
|
});
|
|
207
266
|
if (!authRes.ok) {
|
|
208
267
|
console.error('Error getting API key:', await authRes.text());
|
|
@@ -1546,6 +1605,8 @@ Free and unlimited. No credit card needed.`);
|
|
|
1546
1605
|
console.log(`
|
|
1547
1606
|
TEAM
|
|
1548
1607
|
tasks My tasks
|
|
1608
|
+
tasks board Bounty board (available tasks)
|
|
1609
|
+
tasks claim <id> Claim a bounty
|
|
1549
1610
|
tasks complete <id> Mark done
|
|
1550
1611
|
tasks rank Team ranking
|
|
1551
1612
|
pipe local <source> Import data locally
|
|
@@ -1557,7 +1618,8 @@ ADMIN
|
|
|
1557
1618
|
team add <username> [--admin] Add team member
|
|
1558
1619
|
team remove <username> Remove member
|
|
1559
1620
|
team list List all members
|
|
1560
|
-
tasks
|
|
1621
|
+
tasks post "<title>" [--points N] Post bounty (anyone can claim)
|
|
1622
|
+
tasks assign <user> "<title>" [--points N] Assign task to specific member
|
|
1561
1623
|
tasks approve <id> Approve + award points
|
|
1562
1624
|
tasks reject <id> "<reason>" Reject back
|
|
1563
1625
|
tasks delete <id> Delete task
|
|
@@ -4000,9 +4062,16 @@ async function teamCmd(args) {
|
|
|
4000
4062
|
}
|
|
4001
4063
|
console.log(`Added ${username} to team (${data.role}).`);
|
|
4002
4064
|
if (data.api_key) {
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4065
|
+
if (args.includes('--login')) {
|
|
4066
|
+
// Save key locally — login as this user
|
|
4067
|
+
saveConfig({ api_key: data.api_key, github_username: username, is_team: true, team_role: data.role });
|
|
4068
|
+
console.log(`Logged in as ${username} (${data.role}).`);
|
|
4069
|
+
}
|
|
4070
|
+
else {
|
|
4071
|
+
console.log(`\n API key: ${data.api_key}\n`);
|
|
4072
|
+
console.log(` Share this with the member. They set:`);
|
|
4073
|
+
console.log(` export LATINFO_API_KEY=${data.api_key}`);
|
|
4074
|
+
}
|
|
4006
4075
|
}
|
|
4007
4076
|
try {
|
|
4008
4077
|
const { execSync: exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
@@ -4064,9 +4133,16 @@ async function teamCmd(args) {
|
|
|
4064
4133
|
async function tasksCmd(args) {
|
|
4065
4134
|
const sub = args[0];
|
|
4066
4135
|
// Admin commands use ADMIN_SECRET
|
|
4067
|
-
if (['assign', 'approve', 'reject', 'delete'].includes(sub)) {
|
|
4136
|
+
if (['assign', 'approve', 'reject', 'delete', 'post'].includes(sub)) {
|
|
4068
4137
|
const adminSecret = requireAdmin();
|
|
4069
|
-
const
|
|
4138
|
+
const config = loadConfig();
|
|
4139
|
+
// approve/reject need API key auth + admin secret header; others use admin secret directly
|
|
4140
|
+
const needsApiKey = ['approve', 'reject'].includes(sub) && config?.api_key;
|
|
4141
|
+
const headers = {
|
|
4142
|
+
Authorization: needsApiKey ? `Bearer ${config.api_key}` : `Bearer ${adminSecret}`,
|
|
4143
|
+
'Content-Type': 'application/json',
|
|
4144
|
+
...(needsApiKey ? { 'X-Admin-Secret': adminSecret } : {}),
|
|
4145
|
+
};
|
|
4070
4146
|
switch (sub) {
|
|
4071
4147
|
case 'assign': {
|
|
4072
4148
|
const username = args[1];
|
|
@@ -4089,6 +4165,26 @@ async function tasksCmd(args) {
|
|
|
4089
4165
|
console.log(`Task #${data.id} assigned to ${username}: "${title}" (${data.points} pts)`);
|
|
4090
4166
|
break;
|
|
4091
4167
|
}
|
|
4168
|
+
case 'post': {
|
|
4169
|
+
const title = args[1];
|
|
4170
|
+
const pointsIdx = args.indexOf('--points');
|
|
4171
|
+
const points = pointsIdx !== -1 ? parseInt(args[pointsIdx + 1]) : 10;
|
|
4172
|
+
if (!title) {
|
|
4173
|
+
console.error('Usage: latinfo tasks post "<title>" [--points N]');
|
|
4174
|
+
process.exit(1);
|
|
4175
|
+
}
|
|
4176
|
+
const res = await fetch(`${API_URL}/team/tasks`, {
|
|
4177
|
+
method: 'POST', headers,
|
|
4178
|
+
body: JSON.stringify({ title, points }),
|
|
4179
|
+
});
|
|
4180
|
+
const data = await res.json();
|
|
4181
|
+
if (!res.ok) {
|
|
4182
|
+
console.error(data.message || data.error);
|
|
4183
|
+
process.exit(1);
|
|
4184
|
+
}
|
|
4185
|
+
console.log(`Bounty #${data.id} posted: "${title}" (${data.points} pts)`);
|
|
4186
|
+
break;
|
|
4187
|
+
}
|
|
4092
4188
|
case 'approve': {
|
|
4093
4189
|
const taskId = args[1];
|
|
4094
4190
|
if (!taskId) {
|
|
@@ -4152,6 +4248,39 @@ async function tasksCmd(args) {
|
|
|
4152
4248
|
}
|
|
4153
4249
|
const headers = { Authorization: `Bearer ${config.api_key}`, 'Content-Type': 'application/json' };
|
|
4154
4250
|
switch (sub) {
|
|
4251
|
+
case 'board': {
|
|
4252
|
+
const res = await fetch(`${API_URL}/team/board`, { headers });
|
|
4253
|
+
const bounties = await res.json();
|
|
4254
|
+
if (!res.ok) {
|
|
4255
|
+
console.error('Failed to get board');
|
|
4256
|
+
process.exit(1);
|
|
4257
|
+
}
|
|
4258
|
+
if (bounties.length === 0) {
|
|
4259
|
+
console.log('No bounties available.');
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
console.log('\n BOUNTY BOARD\n');
|
|
4263
|
+
for (const b of bounties) {
|
|
4264
|
+
console.log(` [#${b.id}] ${String(b.points).padStart(3)} pts ${b.title}`);
|
|
4265
|
+
}
|
|
4266
|
+
console.log(`\n Claim one: latinfo tasks claim <id>\n`);
|
|
4267
|
+
break;
|
|
4268
|
+
}
|
|
4269
|
+
case 'claim': {
|
|
4270
|
+
const taskId = args[1];
|
|
4271
|
+
if (!taskId) {
|
|
4272
|
+
console.error('Usage: latinfo tasks claim <task-id>');
|
|
4273
|
+
process.exit(1);
|
|
4274
|
+
}
|
|
4275
|
+
const res = await fetch(`${API_URL}/team/tasks/${taskId}/claim`, { method: 'POST', headers });
|
|
4276
|
+
const data = await res.json();
|
|
4277
|
+
if (!res.ok) {
|
|
4278
|
+
console.error(data.message || data.error);
|
|
4279
|
+
process.exit(1);
|
|
4280
|
+
}
|
|
4281
|
+
console.log(`Bounty #${taskId} claimed. Complete it: latinfo tasks complete ${taskId}`);
|
|
4282
|
+
break;
|
|
4283
|
+
}
|
|
4155
4284
|
case 'complete': {
|
|
4156
4285
|
const taskId = args[1];
|
|
4157
4286
|
if (!taskId) {
|
|
@@ -4260,7 +4389,7 @@ else {
|
|
|
4260
4389
|
// Admin commands (flat)
|
|
4261
4390
|
switch (command) {
|
|
4262
4391
|
case 'login':
|
|
4263
|
-
login(tokenFlag).catch(e => { console.error(e); process.exit(1); });
|
|
4392
|
+
login(tokenFlag, args[0]).catch(e => { console.error(e); process.exit(1); });
|
|
4264
4393
|
break;
|
|
4265
4394
|
case 'logout':
|
|
4266
4395
|
logout();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "latinfo",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.3",
|
|
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": {
|