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.
Files changed (2) hide show
  1. package/dist/index.js +150 -21
  2. 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 || 'Ov23li5fcQaiCsVtaMKK';
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
- // OAuth login: opens browser
192
- const port = 8400;
193
- const redirectUri = `http://localhost:${port}/callback`;
194
- const scope = 'read:user,user:email';
195
- const state = crypto.randomUUID();
196
- const authUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}&state=${state}`;
197
- console.log('Opening GitHub...');
198
- console.log('If your browser does not open, visit this URL:\n');
199
- console.log(authUrl + '\n');
200
- openBrowser(authUrl);
201
- const code = await waitForCallback(port, state);
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({ code, redirect_uri: redirectUri }),
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 assign <user> "<title>" [--points N] Assign task
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
- console.log(`\n API key: ${data.api_key}\n`);
4004
- console.log(` Share this with the member. They set:`);
4005
- console.log(` export LATINFO_API_KEY=${data.api_key}`);
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 headers = { Authorization: `Bearer ${adminSecret}`, 'Content-Type': 'application/json' };
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.1",
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": {