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/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 data = fs.readFileSync(CONFIG_PATH, "utf-8");
11
- return JSON.parse(data);
12
- } catch (e) {
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
- export function saveConfig(config) {
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 updated = { ...existing, ...config };
22
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2), "utf-8");
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
- return process.env.RG_API_URL || loadConfig().apiUrl || "https://rafaygen.online/api/cli";
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
- export function getModel() {
46
- return loadConfig().model || "default";
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 inquirer = (await import("inquirer")).default;
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(chalk.yellow("You are not logged in. Let's get you authenticated!\n"));
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)", value: "browser" },
66
- { name: "Paste Token Manually", value: "token" },
67
- { name: "Device Code (Headless)", value: "device" },
68
- { name: "Exit CLI", value: "exit" }
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
- const { token } = await inquirer.prompt([
79
- {
80
- type: "password",
81
- name: "token",
82
- message: "Paste your RafayGen Personal Access Token:",
83
- validate: (input) => input.trim() !== "" ? true : "Token cannot be empty"
84
- }
85
- ]);
86
- setToken(token.trim());
87
- printSuccess("Successfully logged in to RafayGen!");
88
- return;
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 PORT = 8080;
93
- const CLIENT_ID = "580872142938-2k11f1ced5749euggkqquj5quch0tf43.apps.googleusercontent.com";
94
- // We use response_type=token. The token comes in the URL hash, so we serve an HTML page to parse it.
95
- const loginUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=http://localhost:${PORT}/callback&response_type=token&scope=email%20profile`;
96
-
97
- console.log(chalk.cyan(`\nSpinning up local server...`));
98
- console.log(chalk.gray(`Waiting for Google Authentication on port ${PORT}...`));
99
-
100
- const http = await import("http");
101
-
102
- await new Promise((resolve) => {
103
- const server = http.createServer((req, res) => {
104
- if (req.url?.startsWith("/callback")) {
105
- // Serve an HTML page that extracts the hash and POSTs it back to us
106
- res.writeHead(200, { "Content-Type": "text/html" });
107
- res.end(`
108
- <html>
109
- <head><title>Authenticating...</title></head>
110
- <body>
111
- <h2>Authenticating... Please wait.</h2>
112
- <script>
113
- const hash = window.location.hash.substring(1);
114
- const params = new URLSearchParams(hash);
115
- const token = params.get('access_token');
116
- if (token) {
117
- fetch('/token', { method: 'POST', body: token }).then(() => {
118
- document.body.innerHTML = '<h2>Authentication Successful! You may close this tab.</h2>';
119
- });
120
- } else {
121
- document.body.innerHTML = '<h2>Error: No token found.</h2>';
122
- }
123
- </script>
124
- </body>
125
- </html>
126
- `);
127
- } else if (req.url === "/token" && req.method === "POST") {
128
- let body = '';
129
- req.on('data', chunk => body += chunk.toString());
130
- req.on('end', () => {
131
- setToken(body.trim());
132
- printSuccess("Successfully logged in via Google OAuth!");
133
- res.writeHead(200);
134
- res.end();
135
- server.close();
136
- resolve();
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
- res.writeHead(404);
140
- res.end();
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
- server.listen(PORT, async () => {
145
- console.log(chalk.cyan(`Opening browser to Google Login...`));
146
- try {
147
- await open(loginUrl);
148
- } catch (e) {
149
- console.log(chalk.yellow(`Could not open browser automatically. Please open this link:\n${loginUrl}`));
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
- return;
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 code = Math.floor(100000 + Math.random() * 900000);
158
- console.log(chalk.magenta.bold(`\nYour Device Code is: ${code}`));
159
- console.log(chalk.white(`1. Go to https://rafaygen.com/device on your phone or computer.`));
160
- console.log(chalk.white(`2. Enter the code above.`));
161
-
162
- console.log(chalk.gray("(Device flow polling is simulated in this CLI version)"));
163
- const { token } = await inquirer.prompt([
164
- {
165
- type: "password",
166
- name: "token",
167
- message: "Or just paste the token manually for now:",
168
- validate: (input) => input.trim() !== "" ? true : "Token cannot be empty"
169
- }
170
- ]);
171
- setToken(token.trim());
172
- printSuccess("Successfully logged in to RafayGen!");
173
- return;
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
  }