langtrain 0.2.1 → 0.2.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/cli.js +21 -14
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +21 -14
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -2
- package/src/cli/auth.ts +102 -14
- package/src/cli/index.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "langtrain",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Unified JavaScript SDK for Langtrain Ecosystem",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"form-data": "^4.0.5",
|
|
46
46
|
"gradient-string": "^3.0.0",
|
|
47
47
|
"kleur": "^4.1.5",
|
|
48
|
-
"langtrain": "^0.1.15",
|
|
49
48
|
"langtune": "^0.1.1",
|
|
50
49
|
"langvision": "^0.1.1"
|
|
51
50
|
}
|
package/src/cli/auth.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
import { password, isCancel, cancel, intro, green, yellow, red, bgMagenta, black, spinner, gray, cyan, dim, bold } from './ui';
|
|
1
|
+
import { password, isCancel, cancel, intro, green, yellow, red, bgMagenta, black, spinner, gray, cyan, dim, bold, note } from './ui';
|
|
2
2
|
import { getConfig, saveConfig } from './config';
|
|
3
3
|
import { SubscriptionClient, SubscriptionInfo } from '../index';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const API_BASE = 'https://api.langtrain.ai/api/v1';
|
|
10
|
+
|
|
11
|
+
async function openBrowser(url: string) {
|
|
12
|
+
try {
|
|
13
|
+
const command = process.platform === 'win32'
|
|
14
|
+
? `start ${url}`
|
|
15
|
+
: process.platform === 'darwin'
|
|
16
|
+
? `open "${url}"`
|
|
17
|
+
: `xdg-open "${url}"`;
|
|
18
|
+
await execAsync(command);
|
|
19
|
+
} catch {
|
|
20
|
+
// Silently fail if browser can't open
|
|
21
|
+
}
|
|
22
|
+
}
|
|
4
23
|
|
|
5
24
|
/**
|
|
6
25
|
* Quick check if API key is stored (no network call).
|
|
@@ -28,9 +47,86 @@ export async function ensureAuth(): Promise<string> {
|
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
/**
|
|
31
|
-
* Interactive login —
|
|
50
|
+
* Interactive login — browser-based OAuth flow with API key fallback.
|
|
32
51
|
*/
|
|
33
52
|
export async function handleLogin() {
|
|
53
|
+
const s = spinner();
|
|
54
|
+
s.start('Connecting to Langtrain...');
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// 1. Request device code
|
|
58
|
+
const { data: codeData } = await axios.post(`${API_BASE}/auth/device/code`);
|
|
59
|
+
const { device_code, user_code, verification_url, expires_in, interval } = codeData;
|
|
60
|
+
|
|
61
|
+
s.stop(green('Connected.'));
|
|
62
|
+
|
|
63
|
+
console.log('\n ' + bgMagenta(black(' AUTHENTICATION ')) + '\n');
|
|
64
|
+
console.log(` To log in, please open your browser to:\n ${cyan(verification_url + '?code=' + user_code)}\n`);
|
|
65
|
+
|
|
66
|
+
note(
|
|
67
|
+
`Confirmation Code:\n${bold(user_code)}`,
|
|
68
|
+
'Verify in Browser'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
console.log(gray(' Opening browser automatically...'));
|
|
72
|
+
await openBrowser(`${verification_url}?code=${user_code}`);
|
|
73
|
+
|
|
74
|
+
const pollSpinner = spinner();
|
|
75
|
+
pollSpinner.start('Waiting for approval in browser...');
|
|
76
|
+
|
|
77
|
+
// 2. Poll for token
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
const timeout = expires_in * 1000;
|
|
80
|
+
|
|
81
|
+
while (Date.now() - startTime < timeout) {
|
|
82
|
+
try {
|
|
83
|
+
const { data: tokenData } = await axios.post(`${API_BASE}/auth/device/token?device_code=${device_code}`);
|
|
84
|
+
|
|
85
|
+
if (tokenData.status === 'approved') {
|
|
86
|
+
const apiKey = tokenData.api_key;
|
|
87
|
+
pollSpinner.stop(green('Approved!'));
|
|
88
|
+
|
|
89
|
+
const verifySpinner = spinner();
|
|
90
|
+
verifySpinner.start('Verifying credentials...');
|
|
91
|
+
|
|
92
|
+
const client = new SubscriptionClient({ apiKey });
|
|
93
|
+
const info = await client.getStatus();
|
|
94
|
+
|
|
95
|
+
const planBadge = info.plan === 'pro'
|
|
96
|
+
? bgMagenta(black(' PRO '))
|
|
97
|
+
: info.plan === 'enterprise'
|
|
98
|
+
? bgMagenta(black(' ENTERPRISE '))
|
|
99
|
+
: ' FREE ';
|
|
100
|
+
|
|
101
|
+
verifySpinner.stop(green(`Authenticated ${planBadge}`));
|
|
102
|
+
|
|
103
|
+
const config = getConfig();
|
|
104
|
+
saveConfig({ ...config, apiKey });
|
|
105
|
+
console.log(green(' ✔ Credentials saved to ~/.langtrain/config.json\n'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (tokenData.status === 'expired') {
|
|
110
|
+
pollSpinner.stop(red('Device code expired.'));
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Wait for the requested interval
|
|
115
|
+
await new Promise(r => setTimeout(r, interval * 1000));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// Ignore network errors during polling
|
|
118
|
+
await new Promise(r => setTimeout(r, interval * 1000));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pollSpinner.stop(yellow('Login timed out.'));
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
s.stop(red('Could not reach Langtrain server.'));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. Fallback to manual entry if browser flow fails
|
|
128
|
+
console.log(gray('\n Browser login failed or timed out. Falling back to manual entry.'));
|
|
129
|
+
|
|
34
130
|
while (true) {
|
|
35
131
|
console.log(dim(' ─────────────────────────────────────'));
|
|
36
132
|
console.log(gray(' Get your API Key at: ') + cyan('https://app.langtrain.xyz/home/api'));
|
|
@@ -49,8 +145,8 @@ export async function handleLogin() {
|
|
|
49
145
|
process.exit(0);
|
|
50
146
|
}
|
|
51
147
|
|
|
52
|
-
const
|
|
53
|
-
|
|
148
|
+
const verifySpinner = spinner();
|
|
149
|
+
verifySpinner.start('Verifying API Key...');
|
|
54
150
|
|
|
55
151
|
try {
|
|
56
152
|
const client = new SubscriptionClient({ apiKey: apiKey as string });
|
|
@@ -62,22 +158,14 @@ export async function handleLogin() {
|
|
|
62
158
|
? bgMagenta(black(' ENTERPRISE '))
|
|
63
159
|
: ' FREE ';
|
|
64
160
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Show initial token info if available
|
|
68
|
-
if (info.usage) {
|
|
69
|
-
const used = info.usage.tokensUsedThisMonth || 0;
|
|
70
|
-
const limit = info.usage.tokenLimit || 10000;
|
|
71
|
-
const pct = Math.round((used / limit) * 100);
|
|
72
|
-
console.log(dim(` Tokens: ${used.toLocaleString()} / ${limit.toLocaleString()} (${pct}% used)`));
|
|
73
|
-
}
|
|
161
|
+
verifySpinner.stop(green(`Authenticated ${planBadge}`));
|
|
74
162
|
|
|
75
163
|
const config = getConfig();
|
|
76
164
|
saveConfig({ ...config, apiKey: apiKey as string });
|
|
77
165
|
console.log(green(' ✔ Credentials saved to ~/.langtrain/config.json\n'));
|
|
78
166
|
return;
|
|
79
167
|
} catch (e: any) {
|
|
80
|
-
|
|
168
|
+
verifySpinner.stop(red('Invalid API Key. Please try again.'));
|
|
81
169
|
}
|
|
82
170
|
}
|
|
83
171
|
}
|