rafaygen-cli 1.3.1 → 1.3.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/bin/rgcli.js +302 -29
- package/package.json +2 -2
- package/src/agent.js +1297 -191
- package/src/auth.js +446 -105
- package/src/executor.js +737 -0
- package/src/state.js +254 -10
- package/src/ui.js +419 -51
package/src/auth.js
CHANGED
|
@@ -1,59 +1,206 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import os from "os";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import open from "open";
|
|
7
|
+
import http from "http";
|
|
8
|
+
|
|
9
|
+
// ─── Paths ──────────────────────────────────────────────────────────────────────
|
|
4
10
|
|
|
5
11
|
const CONFIG_PATH = path.join(os.homedir(), ".rgcli.json");
|
|
12
|
+
const RGCLI_DIR = path.join(os.homedir(), ".rgcli");
|
|
13
|
+
const SESSIONS_DIR = path.join(RGCLI_DIR, "sessions");
|
|
14
|
+
const SKILLS_DIR = path.join(RGCLI_DIR, "skills");
|
|
15
|
+
const MCP_CONFIG_PATH = path.join(RGCLI_DIR, "mcp.json");
|
|
16
|
+
|
|
17
|
+
const DEFAULT_API_URL = "http://localhost:3000/api/cli";
|
|
18
|
+
|
|
19
|
+
// ─── 1. loadConfig / saveConfig ─────────────────────────────────────────────────
|
|
6
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Reads the entire ~/.rgcli.json config file.
|
|
23
|
+
* Returns an empty object if the file doesn't exist or is malformed.
|
|
24
|
+
*/
|
|
7
25
|
export function loadConfig() {
|
|
8
26
|
if (fs.existsSync(CONFIG_PATH)) {
|
|
9
27
|
try {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
28
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
return {};
|
|
34
|
+
} catch {
|
|
13
35
|
return {};
|
|
14
36
|
}
|
|
15
37
|
}
|
|
16
38
|
return {};
|
|
17
39
|
}
|
|
18
40
|
|
|
19
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Merges the given partial config into the existing ~/.rgcli.json and writes it.
|
|
43
|
+
* Creates the file if it doesn't exist.
|
|
44
|
+
*/
|
|
45
|
+
export function saveConfig(partial) {
|
|
20
46
|
const existing = loadConfig();
|
|
21
|
-
const
|
|
22
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(
|
|
47
|
+
const merged = { ...existing, ...partial };
|
|
48
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2), "utf-8");
|
|
23
49
|
}
|
|
24
50
|
|
|
51
|
+
// ─── 2. getToken / setToken ─────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the stored authentication token, or undefined if not set.
|
|
55
|
+
* Checks the RG_TOKEN env var first, then the config file.
|
|
56
|
+
*/
|
|
25
57
|
export function getToken() {
|
|
58
|
+
if (process.env.RG_TOKEN) {
|
|
59
|
+
return process.env.RG_TOKEN;
|
|
60
|
+
}
|
|
26
61
|
return loadConfig().token;
|
|
27
62
|
}
|
|
28
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Persists the authentication token into ~/.rgcli.json.
|
|
66
|
+
*/
|
|
29
67
|
export function setToken(token) {
|
|
30
68
|
saveConfig({ token });
|
|
31
69
|
}
|
|
32
70
|
|
|
71
|
+
// ─── 3. getApiUrl / setApiUrl ───────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns the API URL.
|
|
75
|
+
* Priority: RG_API_URL env var → config file → default.
|
|
76
|
+
*/
|
|
33
77
|
export function getApiUrl() {
|
|
34
|
-
|
|
78
|
+
if (process.env.RG_API_URL) {
|
|
79
|
+
return process.env.RG_API_URL;
|
|
80
|
+
}
|
|
81
|
+
return loadConfig().apiUrl || DEFAULT_API_URL;
|
|
35
82
|
}
|
|
36
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Persists the API URL into ~/.rgcli.json.
|
|
86
|
+
*/
|
|
37
87
|
export function setApiUrl(apiUrl) {
|
|
38
88
|
saveConfig({ apiUrl });
|
|
39
89
|
}
|
|
40
90
|
|
|
91
|
+
// ─── 4. getModel / setModel ─────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns the currently selected model name.
|
|
95
|
+
* Priority: RG_MODEL env var → config file → "default".
|
|
96
|
+
*/
|
|
97
|
+
export function getModel() {
|
|
98
|
+
if (process.env.RG_MODEL) {
|
|
99
|
+
return process.env.RG_MODEL;
|
|
100
|
+
}
|
|
101
|
+
return loadConfig().model || "default";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Persists the selected model name into ~/.rgcli.json.
|
|
106
|
+
*/
|
|
41
107
|
export function setModel(model) {
|
|
42
108
|
saveConfig({ model });
|
|
43
109
|
}
|
|
44
110
|
|
|
45
|
-
|
|
46
|
-
|
|
111
|
+
// ─── 5. getProfile / setProfile ─────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns the stored user profile object, or null if not set.
|
|
115
|
+
* Profile shape: { name, email, avatar }
|
|
116
|
+
*/
|
|
117
|
+
export function getProfile() {
|
|
118
|
+
const config = loadConfig();
|
|
119
|
+
return config.profile || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Persists the user profile object into ~/.rgcli.json.
|
|
124
|
+
* @param {object} profile — { name?: string, email?: string, avatar?: string }
|
|
125
|
+
*/
|
|
126
|
+
export function setProfile(profile) {
|
|
127
|
+
saveConfig({ profile });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── 6. clearConfig — Logout ────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Completely wipes the authentication token and profile from the config.
|
|
134
|
+
* Keeps other settings (apiUrl, model, etc.) intact.
|
|
135
|
+
*/
|
|
136
|
+
export function clearConfig() {
|
|
137
|
+
const config = loadConfig();
|
|
138
|
+
delete config.token;
|
|
139
|
+
delete config.profile;
|
|
140
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── 8. getSessionDir ───────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Returns the path to ~/.rgcli/sessions/.
|
|
147
|
+
* Creates the directory tree if it doesn't already exist.
|
|
148
|
+
*/
|
|
149
|
+
export function getSessionDir() {
|
|
150
|
+
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
151
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
return SESSIONS_DIR;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── 9. getMcpConfigPath ────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns the path to ~/.rgcli/mcp.json.
|
|
160
|
+
* Ensures the parent directory exists, and creates an empty JSON object file
|
|
161
|
+
* if the file doesn't exist yet.
|
|
162
|
+
*/
|
|
163
|
+
export function getMcpConfigPath() {
|
|
164
|
+
if (!fs.existsSync(RGCLI_DIR)) {
|
|
165
|
+
fs.mkdirSync(RGCLI_DIR, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
if (!fs.existsSync(MCP_CONFIG_PATH)) {
|
|
168
|
+
fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify({}, null, 2), "utf-8");
|
|
169
|
+
}
|
|
170
|
+
return MCP_CONFIG_PATH;
|
|
47
171
|
}
|
|
48
172
|
|
|
173
|
+
// ─── 10. getSkillsDir ──────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns the path to ~/.rgcli/skills/.
|
|
177
|
+
* Creates the directory tree if it doesn't already exist.
|
|
178
|
+
*/
|
|
179
|
+
export function getSkillsDir() {
|
|
180
|
+
if (!fs.existsSync(SKILLS_DIR)) {
|
|
181
|
+
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
return SKILLS_DIR;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── 7. startAuthLoop — Interactive Authentication ──────────────────────────────
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Launches an interactive authentication loop with three methods:
|
|
190
|
+
* a) Browser Login — Google OAuth via a localhost callback server on port 8080
|
|
191
|
+
* b) Paste Token Manually — password input prompt
|
|
192
|
+
* c) Device Code — generate a 6-digit code, show verification URL, then paste token
|
|
193
|
+
*
|
|
194
|
+
* All methods store the token via setToken() and print a success message.
|
|
195
|
+
* The loop re-prompts on failure until the user authenticates or exits.
|
|
196
|
+
*/
|
|
49
197
|
export async function startAuthLoop() {
|
|
50
|
-
const
|
|
51
|
-
const chalk = (await import("chalk")).default;
|
|
52
|
-
const open = (await import("open")).default;
|
|
53
|
-
const { printAsciiLogo, printSuccess } = await import("./ui.js");
|
|
198
|
+
const { printAsciiLogo, printSuccess, printError } = await import("./ui.js");
|
|
54
199
|
|
|
55
200
|
printAsciiLogo();
|
|
56
|
-
console.log(
|
|
201
|
+
console.log(
|
|
202
|
+
chalk.yellow("You are not logged in. Let's get you authenticated!\n")
|
|
203
|
+
);
|
|
57
204
|
|
|
58
205
|
while (true) {
|
|
59
206
|
const { method } = await inquirer.prompt([
|
|
@@ -62,115 +209,309 @@ export async function startAuthLoop() {
|
|
|
62
209
|
name: "method",
|
|
63
210
|
message: "How would you like to login to RafayGen?",
|
|
64
211
|
choices: [
|
|
65
|
-
{ name: "Browser Login (Recommended)
|
|
66
|
-
{ name: "Paste Token Manually
|
|
67
|
-
{ name: "Device Code (Headless)
|
|
68
|
-
{ name: "Exit CLI
|
|
69
|
-
]
|
|
70
|
-
}
|
|
212
|
+
{ name: `${chalk.green("●")} Browser Login (Recommended)`, value: "browser" },
|
|
213
|
+
{ name: `${chalk.yellow("●")} Paste Token Manually`, value: "token" },
|
|
214
|
+
{ name: `${chalk.magenta("●")} Device Code (Headless)`, value: "device" },
|
|
215
|
+
{ name: `${chalk.red("●")} Exit CLI`, value: "exit" },
|
|
216
|
+
],
|
|
217
|
+
},
|
|
71
218
|
]);
|
|
72
219
|
|
|
220
|
+
// ── Exit ────────────────────────────────────────────────────────────────
|
|
73
221
|
if (method === "exit") {
|
|
222
|
+
console.log(chalk.gray("Goodbye.\n"));
|
|
74
223
|
process.exit(0);
|
|
75
224
|
}
|
|
76
225
|
|
|
226
|
+
// ── Method B: Paste Token Manually ──────────────────────────────────────
|
|
77
227
|
if (method === "token") {
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
228
|
+
try {
|
|
229
|
+
const { token } = await inquirer.prompt([
|
|
230
|
+
{
|
|
231
|
+
type: "password",
|
|
232
|
+
name: "token",
|
|
233
|
+
mask: "*",
|
|
234
|
+
message: "Paste your RafayGen Personal Access Token:",
|
|
235
|
+
validate: (input) =>
|
|
236
|
+
input.trim().length > 0 ? true : "Token cannot be empty.",
|
|
237
|
+
},
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const trimmed = token.trim();
|
|
241
|
+
setToken(trimmed);
|
|
242
|
+
printSuccess("Successfully logged in to RafayGen!");
|
|
243
|
+
return;
|
|
244
|
+
} catch (err) {
|
|
245
|
+
printError(`Token input failed: ${err.message}`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
89
248
|
}
|
|
90
249
|
|
|
250
|
+
// ── Method A: Browser Login (Google OAuth) ──────────────────────────────
|
|
91
251
|
if (method === "browser") {
|
|
92
|
-
const
|
|
93
|
-
const CLIENT_ID =
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
252
|
+
const CALLBACK_PORT = 8080;
|
|
253
|
+
const CLIENT_ID =
|
|
254
|
+
"580872142938-2k11f1ced5749euggkqquj5quch0tf43.apps.googleusercontent.com";
|
|
255
|
+
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
256
|
+
const SCOPES = "email profile";
|
|
257
|
+
const loginUrl =
|
|
258
|
+
`https://accounts.google.com/o/oauth2/v2/auth` +
|
|
259
|
+
`?client_id=${encodeURIComponent(CLIENT_ID)}` +
|
|
260
|
+
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
|
|
261
|
+
`&response_type=token` +
|
|
262
|
+
`&scope=${encodeURIComponent(SCOPES)}`;
|
|
263
|
+
|
|
264
|
+
console.log(chalk.cyan("\nSpinning up local authentication server..."));
|
|
265
|
+
console.log(
|
|
266
|
+
chalk.gray(
|
|
267
|
+
`Listening for Google OAuth callback on port ${CALLBACK_PORT}...`
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const callbackHtml = `<!DOCTYPE html>
|
|
272
|
+
<html lang="en">
|
|
273
|
+
<head>
|
|
274
|
+
<meta charset="UTF-8">
|
|
275
|
+
<title>RafayGen — Authenticating</title>
|
|
276
|
+
<style>
|
|
277
|
+
body {
|
|
278
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
279
|
+
display: flex; justify-content: center; align-items: center;
|
|
280
|
+
min-height: 100vh; margin: 0;
|
|
281
|
+
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
|
|
282
|
+
color: #fff;
|
|
283
|
+
}
|
|
284
|
+
.card {
|
|
285
|
+
background: rgba(255,255,255,0.07); backdrop-filter: blur(12px);
|
|
286
|
+
border-radius: 16px; padding: 48px 40px; text-align: center;
|
|
287
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4); max-width: 420px;
|
|
288
|
+
}
|
|
289
|
+
h2 { margin: 0 0 12px; font-size: 1.5em; }
|
|
290
|
+
p { margin: 0; opacity: 0.8; font-size: 0.95em; }
|
|
291
|
+
.success h2 { color: #4ade80; }
|
|
292
|
+
.error h2 { color: #f87171; }
|
|
293
|
+
.spinner {
|
|
294
|
+
width: 40px; height: 40px; margin: 0 auto 20px;
|
|
295
|
+
border: 4px solid rgba(255,255,255,0.2);
|
|
296
|
+
border-top-color: #60a5fa; border-radius: 50%;
|
|
297
|
+
animation: spin 0.8s linear infinite;
|
|
298
|
+
}
|
|
299
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
300
|
+
</style>
|
|
301
|
+
</head>
|
|
302
|
+
<body>
|
|
303
|
+
<div class="card" id="card">
|
|
304
|
+
<div class="spinner" id="spinner"></div>
|
|
305
|
+
<h2 id="title">Authenticating…</h2>
|
|
306
|
+
<p id="msg">Please wait while we verify your identity.</p>
|
|
307
|
+
</div>
|
|
308
|
+
<script>
|
|
309
|
+
(function() {
|
|
310
|
+
var hash = window.location.hash.substring(1);
|
|
311
|
+
var params = new URLSearchParams(hash);
|
|
312
|
+
var accessToken = params.get("access_token");
|
|
313
|
+
var card = document.getElementById("card");
|
|
314
|
+
var title = document.getElementById("title");
|
|
315
|
+
var msg = document.getElementById("msg");
|
|
316
|
+
var spin = document.getElementById("spinner");
|
|
317
|
+
|
|
318
|
+
if (accessToken) {
|
|
319
|
+
fetch("/token", {
|
|
320
|
+
method: "POST",
|
|
321
|
+
headers: { "Content-Type": "text/plain" },
|
|
322
|
+
body: accessToken
|
|
323
|
+
})
|
|
324
|
+
.then(function(res) {
|
|
325
|
+
spin.style.display = "none";
|
|
326
|
+
if (res.ok) {
|
|
327
|
+
card.className = "card success";
|
|
328
|
+
title.textContent = "Authentication Successful!";
|
|
329
|
+
msg.textContent = "You may close this tab and return to the CLI.";
|
|
138
330
|
} else {
|
|
139
|
-
|
|
140
|
-
|
|
331
|
+
card.className = "card error";
|
|
332
|
+
title.textContent = "Authentication Failed";
|
|
333
|
+
msg.textContent = "The server rejected the token. Please try again.";
|
|
141
334
|
}
|
|
335
|
+
})
|
|
336
|
+
.catch(function() {
|
|
337
|
+
spin.style.display = "none";
|
|
338
|
+
card.className = "card error";
|
|
339
|
+
title.textContent = "Network Error";
|
|
340
|
+
msg.textContent = "Could not reach the local server.";
|
|
142
341
|
});
|
|
342
|
+
} else {
|
|
343
|
+
spin.style.display = "none";
|
|
344
|
+
card.className = "card error";
|
|
345
|
+
title.textContent = "No Token Received";
|
|
346
|
+
msg.textContent = "Google did not return an access token. Please try again.";
|
|
347
|
+
}
|
|
348
|
+
})();
|
|
349
|
+
</script>
|
|
350
|
+
</body>
|
|
351
|
+
</html>`;
|
|
143
352
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
353
|
+
try {
|
|
354
|
+
await new Promise((resolve, reject) => {
|
|
355
|
+
let settled = false;
|
|
356
|
+
|
|
357
|
+
const server = http.createServer((req, res) => {
|
|
358
|
+
// ── Callback page ───────────────────────────────────────────
|
|
359
|
+
if (req.url && req.url.startsWith("/callback")) {
|
|
360
|
+
res.writeHead(200, {
|
|
361
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
362
|
+
"Cache-Control": "no-store",
|
|
363
|
+
});
|
|
364
|
+
res.end(callbackHtml);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── Token receiver ──────────────────────────────────────────
|
|
369
|
+
if (req.url === "/token" && req.method === "POST") {
|
|
370
|
+
let body = "";
|
|
371
|
+
req.on("data", (chunk) => {
|
|
372
|
+
body += chunk.toString();
|
|
373
|
+
});
|
|
374
|
+
req.on("end", () => {
|
|
375
|
+
const receivedToken = body.trim();
|
|
376
|
+
if (receivedToken.length === 0) {
|
|
377
|
+
res.writeHead(400);
|
|
378
|
+
res.end("Empty token");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
setToken(receivedToken);
|
|
382
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
383
|
+
res.end("OK");
|
|
384
|
+
printSuccess("Successfully logged in via Google OAuth!");
|
|
385
|
+
settled = true;
|
|
386
|
+
server.close(() => resolve());
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── Anything else ───────────────────────────────────────────
|
|
392
|
+
res.writeHead(404);
|
|
393
|
+
res.end("Not Found");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
server.on("error", (err) => {
|
|
397
|
+
if (!settled) {
|
|
398
|
+
settled = true;
|
|
399
|
+
if (err.code === "EADDRINUSE") {
|
|
400
|
+
printError(
|
|
401
|
+
`Port ${CALLBACK_PORT} is already in use. Close whatever is using it and try again.`
|
|
402
|
+
);
|
|
403
|
+
} else {
|
|
404
|
+
printError(`Could not start auth server: ${err.message}`);
|
|
405
|
+
}
|
|
406
|
+
reject(err);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// 5-minute timeout so it doesn't hang forever
|
|
411
|
+
const timeout = setTimeout(() => {
|
|
412
|
+
if (!settled) {
|
|
413
|
+
settled = true;
|
|
414
|
+
printError(
|
|
415
|
+
"Browser login timed out after 5 minutes. Please try again."
|
|
416
|
+
);
|
|
417
|
+
server.close(() => reject(new Error("Timeout")));
|
|
418
|
+
}
|
|
419
|
+
}, 5 * 60 * 1000);
|
|
420
|
+
|
|
421
|
+
server.listen(CALLBACK_PORT, async () => {
|
|
422
|
+
console.log(chalk.cyan("Opening browser to Google Login..."));
|
|
423
|
+
try {
|
|
424
|
+
await open(loginUrl);
|
|
425
|
+
} catch {
|
|
426
|
+
console.log(
|
|
427
|
+
chalk.yellow(
|
|
428
|
+
`Could not open browser automatically.\nPlease open this URL manually:\n`
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
console.log(chalk.underline.blueBright(loginUrl) + "\n");
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Clean up timeout when server closes normally
|
|
436
|
+
server.on("close", () => clearTimeout(timeout));
|
|
151
437
|
});
|
|
152
|
-
|
|
153
|
-
|
|
438
|
+
|
|
439
|
+
return; // auth successful
|
|
440
|
+
} catch {
|
|
441
|
+
// Server error or timeout — loop back to method selection
|
|
442
|
+
console.log(
|
|
443
|
+
chalk.gray("Returning to login method selection...\n")
|
|
444
|
+
);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
154
447
|
}
|
|
155
448
|
|
|
449
|
+
// ── Method C: Device Code ─────────────────────────────────────────────
|
|
156
450
|
if (method === "device") {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
console.log(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
451
|
+
const deviceCode = String(
|
|
452
|
+
Math.floor(100000 + Math.random() * 900000)
|
|
453
|
+
);
|
|
454
|
+
const verificationUrl = "https://rafaygen.com/device";
|
|
455
|
+
|
|
456
|
+
console.log("");
|
|
457
|
+
console.log(
|
|
458
|
+
chalk.bgMagenta.white.bold(" DEVICE CODE ") +
|
|
459
|
+
" " +
|
|
460
|
+
chalk.magenta.bold(deviceCode)
|
|
461
|
+
);
|
|
462
|
+
console.log("");
|
|
463
|
+
console.log(
|
|
464
|
+
chalk.white(" 1. Open ") +
|
|
465
|
+
chalk.underline.blueBright(verificationUrl) +
|
|
466
|
+
chalk.white(" on any device.")
|
|
467
|
+
);
|
|
468
|
+
console.log(
|
|
469
|
+
chalk.white(" 2. Enter the code ") +
|
|
470
|
+
chalk.magenta.bold(deviceCode) +
|
|
471
|
+
chalk.white(" when prompted.")
|
|
472
|
+
);
|
|
473
|
+
console.log(
|
|
474
|
+
chalk.white(
|
|
475
|
+
" 3. Approve the login, then paste the resulting token below."
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
console.log("");
|
|
479
|
+
|
|
480
|
+
// Attempt to open the verification URL in the browser automatically
|
|
481
|
+
try {
|
|
482
|
+
await open(verificationUrl);
|
|
483
|
+
console.log(
|
|
484
|
+
chalk.gray(" (Opened verification page in your default browser)\n")
|
|
485
|
+
);
|
|
486
|
+
} catch {
|
|
487
|
+
console.log(
|
|
488
|
+
chalk.gray(
|
|
489
|
+
" (Could not open browser — please navigate there manually)\n"
|
|
490
|
+
)
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const { token } = await inquirer.prompt([
|
|
496
|
+
{
|
|
497
|
+
type: "password",
|
|
498
|
+
name: "token",
|
|
499
|
+
mask: "*",
|
|
500
|
+
message:
|
|
501
|
+
"After approving the device code, paste the token you received:",
|
|
502
|
+
validate: (input) =>
|
|
503
|
+
input.trim().length > 0 ? true : "Token cannot be empty.",
|
|
504
|
+
},
|
|
505
|
+
]);
|
|
506
|
+
|
|
507
|
+
const trimmed = token.trim();
|
|
508
|
+
setToken(trimmed);
|
|
509
|
+
printSuccess("Successfully logged in via Device Code!");
|
|
510
|
+
return;
|
|
511
|
+
} catch (err) {
|
|
512
|
+
printError(`Device code login failed: ${err.message}`);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
174
515
|
}
|
|
175
516
|
}
|
|
176
517
|
}
|