@gravity-rail/cli 0.1.0 → 0.3.0

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.
@@ -0,0 +1,773 @@
1
+ // src/cli-utils/config.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { dirname, join, resolve } from "path";
4
+ var ENVIRONMENTS = {
5
+ dev: {
6
+ apiUrl: "http://localhost:5050",
7
+ frontendUrl: "http://localhost:3000"
8
+ },
9
+ prod: {
10
+ apiUrl: "https://api.gravityrail.com",
11
+ frontendUrl: "https://app.gravityrail.com"
12
+ },
13
+ staging: {
14
+ apiUrl: "https://api.gr-staging.com",
15
+ frontendUrl: "https://app.gr-staging.com"
16
+ }
17
+ };
18
+ var CALLBACK_PORT = 31337;
19
+ var CALLBACK_PATH = "/oauth/callback";
20
+ var REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
21
+ function detectWorktreePorts() {
22
+ let dir = resolve(process.cwd());
23
+ let overridePath;
24
+ while (true) {
25
+ const candidate = join(dir, "docker-compose.override.yml");
26
+ if (existsSync(candidate)) {
27
+ overridePath = candidate;
28
+ break;
29
+ }
30
+ const parent = dirname(dir);
31
+ if (parent === dir) break;
32
+ dir = parent;
33
+ }
34
+ if (!overridePath) return {};
35
+ let content;
36
+ try {
37
+ content = readFileSync(overridePath, "utf-8");
38
+ } catch {
39
+ return {};
40
+ }
41
+ if (!content.includes("worktree-setup.sh")) return {};
42
+ let apiPort;
43
+ let frontendPort;
44
+ for (const match of content.matchAll(/-\s*'(\d+):(\d+)'/g)) {
45
+ const host = parseInt(match[1], 10);
46
+ const container = parseInt(match[2], 10);
47
+ if (container === 5050) apiPort = host;
48
+ if (container === 3e3) frontendPort = host;
49
+ }
50
+ return { apiPort, frontendPort };
51
+ }
52
+ function getEnvironmentConfig(env) {
53
+ if (env.startsWith("dev-cloud:")) {
54
+ const namespace = env.slice("dev-cloud:".length);
55
+ if (!namespace) {
56
+ throw new Error("dev-cloud environment requires a namespace: --env dev-cloud:NAMESPACE");
57
+ }
58
+ return {
59
+ apiUrl: `https://${namespace}-api.gravityrail-dev.com`,
60
+ frontendUrl: `https://${namespace}-app.gravityrail-dev.com`
61
+ };
62
+ }
63
+ const config = ENVIRONMENTS[env];
64
+ if (!config) {
65
+ throw new Error(
66
+ `Invalid environment: ${env}. Use 'dev', 'staging', 'prod', or 'dev-cloud:NAMESPACE'.`
67
+ );
68
+ }
69
+ const apiUrl = process.env.GRAVITY_RAIL_API_URL || config.apiUrl;
70
+ const frontendUrl = process.env.GRAVITY_RAIL_FRONTEND_URL || config.frontendUrl;
71
+ if (apiUrl !== config.apiUrl || frontendUrl !== config.frontendUrl) {
72
+ return { apiUrl, frontendUrl };
73
+ }
74
+ if (env === "dev") {
75
+ const ports = detectWorktreePorts();
76
+ if (ports.apiPort || ports.frontendPort) {
77
+ return {
78
+ apiUrl: ports.apiPort ? `http://localhost:${ports.apiPort}` : config.apiUrl,
79
+ frontendUrl: ports.frontendPort ? `http://localhost:${ports.frontendPort}` : config.frontendUrl
80
+ };
81
+ }
82
+ }
83
+ return config;
84
+ }
85
+
86
+ // src/cli-utils/auth.ts
87
+ import { createServer } from "http";
88
+ import { createHash, randomBytes } from "crypto";
89
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, mkdirSync, chmodSync, unlinkSync } from "fs";
90
+ import { join as join2 } from "path";
91
+ import { exec } from "child_process";
92
+ var CredentialsManager = class {
93
+ cacheDir;
94
+ apiUrl;
95
+ constructor(rootDir, apiUrl) {
96
+ this.cacheDir = join2(rootDir, ".gr-cli");
97
+ this.apiUrl = apiUrl;
98
+ }
99
+ getCredentialsPath(env) {
100
+ if (this.apiUrl) {
101
+ try {
102
+ const url = new URL(this.apiUrl);
103
+ if (url.hostname === "localhost" && url.port && url.port !== "5050") {
104
+ return join2(this.cacheDir, `credentials-${env}-${url.port}.json`);
105
+ }
106
+ } catch {
107
+ }
108
+ }
109
+ return join2(this.cacheDir, `credentials-${env}.json`);
110
+ }
111
+ load(env) {
112
+ const credsPath = this.getCredentialsPath(env);
113
+ if (!existsSync2(credsPath)) {
114
+ return null;
115
+ }
116
+ try {
117
+ const raw = JSON.parse(readFileSync2(credsPath, "utf-8"));
118
+ let creds;
119
+ if (raw.clientId !== void 0) {
120
+ creds = raw;
121
+ } else if (raw.default) {
122
+ creds = raw.default;
123
+ this.save(env, creds);
124
+ } else {
125
+ return null;
126
+ }
127
+ if (creds.expiresAt) {
128
+ const expiresAt = new Date(creds.expiresAt);
129
+ if (expiresAt <= /* @__PURE__ */ new Date()) {
130
+ if (creds.refreshToken) {
131
+ return creds;
132
+ }
133
+ return null;
134
+ }
135
+ }
136
+ return creds;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+ save(env, creds) {
142
+ if (!existsSync2(this.cacheDir)) {
143
+ mkdirSync(this.cacheDir, { recursive: true });
144
+ }
145
+ const credsPath = this.getCredentialsPath(env);
146
+ writeFileSync(credsPath, JSON.stringify(creds, null, 2));
147
+ try {
148
+ chmodSync(credsPath, 384);
149
+ } catch {
150
+ }
151
+ }
152
+ clear(env) {
153
+ const credsPath = this.getCredentialsPath(env);
154
+ if (!existsSync2(credsPath)) {
155
+ return;
156
+ }
157
+ try {
158
+ unlinkSync(credsPath);
159
+ } catch {
160
+ }
161
+ }
162
+ getCacheDir() {
163
+ return this.cacheDir;
164
+ }
165
+ };
166
+ function stderr(msg) {
167
+ process.stderr.write(msg + "\n");
168
+ }
169
+ function isOAuthError(body, errorCode) {
170
+ try {
171
+ const parsed = JSON.parse(body);
172
+ if (parsed.error === errorCode) return true;
173
+ } catch {
174
+ }
175
+ return body.includes(errorCode);
176
+ }
177
+ function validateTokenResponse(tokenData) {
178
+ if (!tokenData.access_token || typeof tokenData.access_token !== "string") {
179
+ throw new Error("Token response missing access_token");
180
+ }
181
+ if (tokenData.token_type && String(tokenData.token_type).toLowerCase() !== "bearer") {
182
+ throw new Error(`Unsupported token type: ${tokenData.token_type}`);
183
+ }
184
+ let expiresIn = 86400;
185
+ if (tokenData.expires_in != null) {
186
+ const raw = Number(tokenData.expires_in);
187
+ if (!Number.isFinite(raw) || raw < 60 || raw > 31536e3) {
188
+ stderr(` Warning: unusual expires_in value (${tokenData.expires_in}), using default`);
189
+ } else {
190
+ expiresIn = raw;
191
+ }
192
+ }
193
+ return {
194
+ accessToken: tokenData.access_token,
195
+ refreshToken: typeof tokenData.refresh_token === "string" ? tokenData.refresh_token : void 0,
196
+ expiresIn
197
+ };
198
+ }
199
+ function generatePKCE() {
200
+ const codeVerifier = randomBytes(64).toString("base64url").slice(0, 128);
201
+ const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
202
+ return { codeVerifier, codeChallenge };
203
+ }
204
+ function openBrowser(url) {
205
+ const platform = process.platform;
206
+ let cmd;
207
+ if (platform === "darwin") {
208
+ cmd = `open "${url}"`;
209
+ } else if (platform === "win32") {
210
+ cmd = `start "" "${url}"`;
211
+ } else {
212
+ cmd = `xdg-open "${url}"`;
213
+ }
214
+ exec(cmd, (err) => {
215
+ if (err) {
216
+ stderr(" Could not open browser automatically.");
217
+ }
218
+ });
219
+ }
220
+ function startCallbackServer() {
221
+ return new Promise((resolve2, reject) => {
222
+ let authCode = null;
223
+ let error = null;
224
+ let receivedState = null;
225
+ const server = createServer((req, res) => {
226
+ const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
227
+ if (url.pathname !== CALLBACK_PATH) {
228
+ res.writeHead(404);
229
+ res.end("Not found");
230
+ return;
231
+ }
232
+ if (url.searchParams.has("error")) {
233
+ error = url.searchParams.get("error");
234
+ const errorDesc = url.searchParams.get("error_description") || "Unknown error";
235
+ res.writeHead(200, { "Content-Type": "text/html" });
236
+ res.end(`
237
+ <html>
238
+ <head><title>Authorization Failed</title></head>
239
+ <body style="font-family: system-ui; text-align: center; padding: 50px;">
240
+ <h1 style="color: #dc2626;">Authorization Failed</h1>
241
+ <p>${errorDesc}</p>
242
+ <p>You can close this window.</p>
243
+ </body>
244
+ </html>
245
+ `);
246
+ server.close();
247
+ reject(new Error(`Authorization failed: ${error}`));
248
+ } else if (url.searchParams.has("code")) {
249
+ authCode = url.searchParams.get("code");
250
+ receivedState = url.searchParams.get("state");
251
+ res.writeHead(200, { "Content-Type": "text/html" });
252
+ res.end(`
253
+ <html>
254
+ <head><title>Authorization Successful</title></head>
255
+ <body style="font-family: system-ui; text-align: center; padding: 50px;">
256
+ <h1 style="color: #16a34a;">Authorization Successful!</h1>
257
+ <p>You can close this window and return to the terminal.</p>
258
+ <script>setTimeout(() => window.close(), 2000);</script>
259
+ </body>
260
+ </html>
261
+ `);
262
+ server.close();
263
+ resolve2({ code: authCode, state: receivedState });
264
+ } else {
265
+ res.writeHead(400);
266
+ res.end("Missing code parameter");
267
+ }
268
+ });
269
+ server.listen(CALLBACK_PORT, "localhost", () => {
270
+ });
271
+ const timeout = setTimeout(
272
+ () => {
273
+ if (!authCode && !error) {
274
+ server.close();
275
+ reject(new Error("Authorization timeout - no response received"));
276
+ }
277
+ },
278
+ 5 * 60 * 1e3
279
+ );
280
+ timeout.unref();
281
+ });
282
+ }
283
+ async function dynamicClientRegistration(envConfig, clientName = "Gravity CLI") {
284
+ stderr(" Registering OAuth client...");
285
+ const registrationUrl = `${envConfig.apiUrl}/api/v2/oauth/register`;
286
+ const registrationData = {
287
+ client_name: clientName,
288
+ redirect_uris: [REDIRECT_URI],
289
+ grant_types: ["authorization_code", "refresh_token"],
290
+ response_types: ["code"],
291
+ token_endpoint_auth_method: "client_secret_basic",
292
+ scope: "workspace:admin"
293
+ };
294
+ const response = await fetch(registrationUrl, {
295
+ method: "POST",
296
+ headers: { "Content-Type": "application/json" },
297
+ body: JSON.stringify(registrationData)
298
+ });
299
+ if (!response.ok) {
300
+ throw new Error(`Client registration failed: ${response.status} - ${await response.text()}`);
301
+ }
302
+ return await response.json();
303
+ }
304
+ async function performOAuthFlow(options) {
305
+ const { env, envConfig, credentialsManager, clientName = "Gravity CLI" } = options;
306
+ let hasRetried = false;
307
+ while (true) {
308
+ let creds = credentialsManager.load(env);
309
+ if (!creds || !creds.clientId) {
310
+ const registration = await dynamicClientRegistration(envConfig, clientName);
311
+ creds = {
312
+ clientId: registration.client_id,
313
+ clientSecret: registration.client_secret
314
+ };
315
+ credentialsManager.save(env, creds);
316
+ } else {
317
+ stderr(" Using cached client credentials");
318
+ }
319
+ const { codeVerifier, codeChallenge } = generatePKCE();
320
+ const state = randomBytes(32).toString("base64url");
321
+ const authParams = new URLSearchParams({
322
+ response_type: "code",
323
+ client_id: creds.clientId,
324
+ redirect_uri: REDIRECT_URI,
325
+ scope: "workspace:admin",
326
+ state,
327
+ code_challenge: codeChallenge,
328
+ code_challenge_method: "S256"
329
+ });
330
+ const authUrl = `${envConfig.apiUrl}/api/v2/oauth/authorize?${authParams}`;
331
+ const callbackPromise = startCallbackServer();
332
+ stderr("\n Opening browser for authorization...");
333
+ stderr(` If browser doesn't open, visit:
334
+ ${authUrl}
335
+ `);
336
+ openBrowser(authUrl);
337
+ stderr(" Waiting for authorization (timeout: 5 minutes)...");
338
+ let code;
339
+ let receivedState;
340
+ try {
341
+ ({ code, state: receivedState } = await callbackPromise);
342
+ } catch (err) {
343
+ const msg = err instanceof Error ? err.message : String(err);
344
+ if (!hasRetried && isOAuthError(msg, "invalid_client")) {
345
+ stderr(" Cached client credentials expired, re-registering...");
346
+ credentialsManager.clear(env);
347
+ hasRetried = true;
348
+ continue;
349
+ }
350
+ throw err;
351
+ }
352
+ if (receivedState !== state) {
353
+ throw new Error("State mismatch - possible CSRF attack");
354
+ }
355
+ stderr(" Authorization code received!");
356
+ stderr(" Exchanging code for tokens...");
357
+ const tokenUrl = `${envConfig.apiUrl}/api/v2/oauth/token`;
358
+ const authHeader = Buffer.from(`${creds.clientId}:${creds.clientSecret}`).toString("base64");
359
+ const tokenResponse = await fetch(tokenUrl, {
360
+ method: "POST",
361
+ headers: {
362
+ Authorization: `Basic ${authHeader}`,
363
+ "Content-Type": "application/x-www-form-urlencoded"
364
+ },
365
+ body: new URLSearchParams({
366
+ grant_type: "authorization_code",
367
+ code,
368
+ redirect_uri: REDIRECT_URI,
369
+ code_verifier: codeVerifier
370
+ })
371
+ });
372
+ if (!tokenResponse.ok) {
373
+ const body = await tokenResponse.text();
374
+ if (!hasRetried && isOAuthError(body, "invalid_client")) {
375
+ stderr(" Cached client credentials expired, re-registering...");
376
+ credentialsManager.clear(env);
377
+ hasRetried = true;
378
+ continue;
379
+ }
380
+ throw new Error(`Token exchange failed: ${tokenResponse.status} - ${body}`);
381
+ }
382
+ const tokenData = await tokenResponse.json();
383
+ const { accessToken, refreshToken, expiresIn } = validateTokenResponse(tokenData);
384
+ creds.accessToken = accessToken;
385
+ creds.refreshToken = refreshToken ?? creds.refreshToken;
386
+ creds.expiresAt = new Date(Date.now() + expiresIn * 1e3).toISOString();
387
+ credentialsManager.save(env, creds);
388
+ stderr(" Authentication successful!");
389
+ return { accessToken, credentials: creds };
390
+ }
391
+ }
392
+ async function refreshAccessToken(envConfig, creds, credentialsManager, env) {
393
+ if (!creds.refreshToken || !creds.clientId || !creds.clientSecret) {
394
+ stderr(" Cannot refresh: missing credentials (re-login required)");
395
+ return null;
396
+ }
397
+ const tokenUrl = `${envConfig.apiUrl}/api/v2/oauth/token`;
398
+ const authHeader = Buffer.from(`${creds.clientId}:${creds.clientSecret}`).toString("base64");
399
+ try {
400
+ const response = await fetch(tokenUrl, {
401
+ method: "POST",
402
+ headers: {
403
+ Authorization: `Basic ${authHeader}`,
404
+ "Content-Type": "application/x-www-form-urlencoded"
405
+ },
406
+ body: new URLSearchParams({
407
+ grant_type: "refresh_token",
408
+ refresh_token: creds.refreshToken
409
+ })
410
+ });
411
+ if (!response.ok) {
412
+ const body = await response.text();
413
+ const isExpiredRefresh = isOAuthError(body, "invalid_grant");
414
+ if (isExpiredRefresh) {
415
+ stderr(" Refresh token expired, re-login required");
416
+ } else {
417
+ stderr(` Token refresh failed (${response.status}), clearing credentials`);
418
+ }
419
+ credentialsManager.clear(env);
420
+ return null;
421
+ }
422
+ const tokenData = await response.json();
423
+ const { accessToken, refreshToken, expiresIn } = validateTokenResponse(tokenData);
424
+ const updated = {
425
+ ...creds,
426
+ accessToken,
427
+ refreshToken: refreshToken ?? creds.refreshToken,
428
+ expiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString()
429
+ };
430
+ credentialsManager.save(env, updated);
431
+ return updated;
432
+ } catch (err) {
433
+ stderr(` Token refresh error: ${err instanceof Error ? err.message : "unknown"}`);
434
+ credentialsManager.clear(env);
435
+ return null;
436
+ }
437
+ }
438
+
439
+ // src/cli-utils/ui.ts
440
+ import * as readline from "readline";
441
+ var NEW_ITEM_MARKER = Symbol("NEW_ITEM");
442
+ async function selectFromList(items, options = {}) {
443
+ const {
444
+ title = "Select an item",
445
+ labelFn = (item) => String(item.name || item.title || item),
446
+ detailFn = null,
447
+ env = null,
448
+ emptyMessage = "No items found.",
449
+ allowNew = false,
450
+ newLabel = "Create new..."
451
+ } = options;
452
+ if ((!items || items.length === 0) && !allowNew) {
453
+ console.log(`
454
+ ${emptyMessage}`);
455
+ return { item: null, isNew: false };
456
+ }
457
+ const displayItems = allowNew ? [NEW_ITEM_MARKER, ...items] : items;
458
+ return new Promise((resolve2) => {
459
+ let selectedIndex = allowNew ? 1 : 0;
460
+ let filterText = "";
461
+ const getFilteredItems = () => {
462
+ if (!filterText) return displayItems;
463
+ const lower = filterText.toLowerCase();
464
+ return displayItems.filter((item) => {
465
+ if (item === NEW_ITEM_MARKER) return true;
466
+ const label = labelFn(item);
467
+ return label.toLowerCase().includes(lower);
468
+ });
469
+ };
470
+ const render = () => {
471
+ const filtered = getFilteredItems();
472
+ process.stdout.write("\x1B[2J\x1B[H");
473
+ console.log("=".repeat(60));
474
+ console.log(`${title} (\u2191/\u2193 to move, Enter to select, Esc to cancel)`);
475
+ console.log("=".repeat(60));
476
+ if (env) {
477
+ console.log(`Environment: ${env}`);
478
+ }
479
+ console.log(`Filter: ${filterText}${filterText ? "" : "(type to filter)"}`);
480
+ console.log("");
481
+ if (filtered.length === 0) {
482
+ console.log(` ${emptyMessage}`);
483
+ console.log("");
484
+ return;
485
+ }
486
+ const maxVisible = 15;
487
+ let startIdx = Math.max(0, selectedIndex - Math.floor(maxVisible / 2));
488
+ let endIdx = Math.min(filtered.length, startIdx + maxVisible);
489
+ if (endIdx - startIdx < maxVisible && startIdx > 0) {
490
+ startIdx = Math.max(0, endIdx - maxVisible);
491
+ }
492
+ if (startIdx > 0) {
493
+ console.log(` ... ${startIdx} more above ...`);
494
+ }
495
+ for (let i = startIdx; i < endIdx; i++) {
496
+ const item = filtered[i];
497
+ const prefix = i === selectedIndex ? "> " : " ";
498
+ let line;
499
+ if (item === NEW_ITEM_MARKER) {
500
+ line = `\x1B[32m${newLabel}\x1B[0m`;
501
+ } else {
502
+ const typedItem = item;
503
+ line = labelFn(typedItem);
504
+ if (detailFn) {
505
+ const detail = detailFn(typedItem);
506
+ if (detail) {
507
+ line = `${line} \x1B[90m${detail}\x1B[0m`;
508
+ }
509
+ }
510
+ }
511
+ if (i === selectedIndex) {
512
+ console.log(`\x1B[7m${prefix}${line}\x1B[0m`);
513
+ } else {
514
+ console.log(`${prefix}${line}`);
515
+ }
516
+ }
517
+ if (endIdx < filtered.length) {
518
+ console.log(` ... ${filtered.length - endIdx} more below ...`);
519
+ }
520
+ console.log("");
521
+ console.log(` ${filtered.length} item(s)`);
522
+ };
523
+ readline.emitKeypressEvents(process.stdin);
524
+ if (process.stdin.isTTY) {
525
+ process.stdin.setRawMode(true);
526
+ }
527
+ render();
528
+ const handleKeypress = (str, key) => {
529
+ const filtered = getFilteredItems();
530
+ if (key.name === "up") {
531
+ selectedIndex = Math.max(0, selectedIndex - 1);
532
+ render();
533
+ } else if (key.name === "down") {
534
+ selectedIndex = Math.min(filtered.length - 1, selectedIndex + 1);
535
+ render();
536
+ } else if (key.name === "return") {
537
+ cleanup();
538
+ if (filtered.length > 0 && selectedIndex < filtered.length) {
539
+ const selected = filtered[selectedIndex];
540
+ if (selected === NEW_ITEM_MARKER) {
541
+ resolve2({ item: null, isNew: true });
542
+ } else {
543
+ resolve2({ item: selected, isNew: false });
544
+ }
545
+ } else {
546
+ resolve2({ item: null, isNew: false });
547
+ }
548
+ } else if (key.name === "escape" || key.ctrl && key.name === "c") {
549
+ cleanup();
550
+ resolve2({ item: null, isNew: false });
551
+ } else if (key.name === "backspace") {
552
+ filterText = filterText.slice(0, -1);
553
+ selectedIndex = allowNew ? 0 : 0;
554
+ render();
555
+ } else if (str && str.length === 1 && !key.ctrl && !key.meta) {
556
+ filterText += str;
557
+ selectedIndex = allowNew ? 0 : 0;
558
+ render();
559
+ }
560
+ };
561
+ const cleanup = () => {
562
+ process.stdin.removeListener("keypress", handleKeypress);
563
+ if (process.stdin.isTTY) {
564
+ process.stdin.setRawMode(false);
565
+ }
566
+ process.stdin.pause();
567
+ process.stdout.write("\x1B[2J\x1B[H");
568
+ };
569
+ process.stdin.on("keypress", handleKeypress);
570
+ process.stdin.resume();
571
+ });
572
+ }
573
+ function printTable(items, columns, options = {}) {
574
+ if (!items || items.length === 0) {
575
+ console.log("\nNo items found.");
576
+ return;
577
+ }
578
+ if (options.title) {
579
+ console.log(`
580
+ === ${options.title} ===`);
581
+ }
582
+ if (options.env) {
583
+ console.log(`Environment: ${options.env}`);
584
+ }
585
+ console.log("");
586
+ const widths = columns.map((col) => {
587
+ const maxContent = Math.max(...items.map((item) => String(col.getValue(item) ?? "").length));
588
+ const width = Math.max(col.header.length, maxContent);
589
+ return col.maxWidth ? Math.min(width, col.maxWidth) : width;
590
+ });
591
+ const headerLine = columns.map((col, i) => col.header.padEnd(widths[i])).join(" ");
592
+ console.log(headerLine);
593
+ console.log(widths.map((w) => "-".repeat(w)).join(" "));
594
+ for (const item of items) {
595
+ const row = columns.map((col, i) => {
596
+ const value = String(col.getValue(item) ?? "");
597
+ const width = widths[i];
598
+ return value.slice(0, width).padEnd(width);
599
+ }).join(" ");
600
+ console.log(row);
601
+ }
602
+ console.log("");
603
+ console.log(`Total: ${items.length} item(s)`);
604
+ }
605
+ function formatTimestamp(isoString) {
606
+ if (!isoString) return "";
607
+ const date = new Date(isoString);
608
+ const now = /* @__PURE__ */ new Date();
609
+ const diff = now.getTime() - date.getTime();
610
+ if (diff < 6e4) return "just now";
611
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
612
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
613
+ if (diff < 6048e5) return `${Math.floor(diff / 864e5)}d ago`;
614
+ return date.toLocaleDateString();
615
+ }
616
+ function wrapText(text, width = 70) {
617
+ const words = text.split(" ");
618
+ const lines = [];
619
+ let currentLine = "";
620
+ for (const word of words) {
621
+ if (currentLine.length + word.length + 1 <= width) {
622
+ currentLine += (currentLine ? " " : "") + word;
623
+ } else {
624
+ if (currentLine) lines.push(currentLine);
625
+ currentLine = word;
626
+ }
627
+ }
628
+ if (currentLine) lines.push(currentLine);
629
+ return lines;
630
+ }
631
+ function printMessage(content, role, indent = " ") {
632
+ const roleColors = {
633
+ user: "\x1B[34m",
634
+ // Blue
635
+ assistant: "\x1B[32m",
636
+ // Green
637
+ system: "\x1B[90m"
638
+ // Gray
639
+ };
640
+ const resetColor = "\x1B[0m";
641
+ const color = roleColors[role] || resetColor;
642
+ const label = role === "user" ? "You" : role === "assistant" ? "Assistant" : "System";
643
+ console.log(`${indent}${color}[${label}]${resetColor}`);
644
+ const lines = content.split("\n");
645
+ for (const line of lines) {
646
+ const wrapped = wrapText(line, 70);
647
+ for (const wrappedLine of wrapped) {
648
+ console.log(`${indent} ${wrappedLine}`);
649
+ }
650
+ }
651
+ console.log("");
652
+ }
653
+ function clearScreen() {
654
+ process.stdout.write("\x1B[2J\x1B[H");
655
+ }
656
+
657
+ // src/cli-utils/agui.ts
658
+ var AGUIClient = class {
659
+ endpointUrl;
660
+ accessToken;
661
+ debug;
662
+ model;
663
+ constructor(options) {
664
+ this.endpointUrl = options.endpointUrl ?? `${options.baseUrl}/api/v2/w/${options.workspaceUuid}/web-chat/${options.chatId}/chat/agui`;
665
+ this.accessToken = options.accessToken;
666
+ this.debug = options.debug ?? false;
667
+ this.model = "model" in options ? options.model : "manager";
668
+ }
669
+ /**
670
+ * Send messages and stream the response
671
+ */
672
+ async sendMessage(messages, onEvent, options) {
673
+ const body = JSON.stringify({
674
+ ...this.model ? { model: this.model } : {},
675
+ messages,
676
+ ...options?.autoApprove ? { context: { autoApprove: true } } : {}
677
+ });
678
+ if (this.debug) {
679
+ console.log(`
680
+ [DEBUG] POST ${this.endpointUrl}`);
681
+ console.log(`[DEBUG] Body: ${messages.length} message(s)`);
682
+ }
683
+ const response = await fetch(this.endpointUrl, {
684
+ method: "POST",
685
+ headers: {
686
+ Authorization: `Bearer ${this.accessToken}`,
687
+ "Content-Type": "application/json",
688
+ Accept: "text/event-stream"
689
+ },
690
+ body
691
+ });
692
+ if (this.debug) {
693
+ console.log(`[DEBUG] Response status: ${response.status}`);
694
+ }
695
+ if (!response.ok) {
696
+ const errorText = await response.text().catch(() => "");
697
+ if (this.debug) {
698
+ console.log(`[DEBUG] Error response: ${errorText}`);
699
+ }
700
+ throw new Error(`Chat request failed: ${response.status}`);
701
+ }
702
+ const reader = response.body.getReader();
703
+ const decoder = new TextDecoder();
704
+ let buffer = "";
705
+ while (true) {
706
+ const { done, value } = await reader.read();
707
+ if (done) break;
708
+ buffer += decoder.decode(value, { stream: true });
709
+ const lines = buffer.split("\n");
710
+ buffer = lines.pop() || "";
711
+ for (const line of lines) {
712
+ if (line.startsWith("data: ")) {
713
+ const data = line.slice(6);
714
+ if (data === "[DONE]") {
715
+ onEvent({ type: "done" });
716
+ continue;
717
+ }
718
+ try {
719
+ const event = JSON.parse(data);
720
+ onEvent(event);
721
+ } catch {
722
+ }
723
+ }
724
+ }
725
+ }
726
+ }
727
+ };
728
+ function isTextContentEvent(event) {
729
+ const type = event.type.toLowerCase();
730
+ return type === "text_message_content";
731
+ }
732
+ function isErrorEvent(event) {
733
+ const type = event.type.toLowerCase();
734
+ return type === "run_error";
735
+ }
736
+ function getTextDelta(event) {
737
+ if (isTextContentEvent(event) && event.delta) {
738
+ return event.delta;
739
+ }
740
+ return null;
741
+ }
742
+ function getErrorMessage(event) {
743
+ if (isErrorEvent(event)) {
744
+ return event.message || event.code || "Unknown error";
745
+ }
746
+ return null;
747
+ }
748
+
749
+ export {
750
+ ENVIRONMENTS,
751
+ CALLBACK_PORT,
752
+ CALLBACK_PATH,
753
+ REDIRECT_URI,
754
+ getEnvironmentConfig,
755
+ CredentialsManager,
756
+ generatePKCE,
757
+ openBrowser,
758
+ startCallbackServer,
759
+ performOAuthFlow,
760
+ refreshAccessToken,
761
+ selectFromList,
762
+ printTable,
763
+ formatTimestamp,
764
+ wrapText,
765
+ printMessage,
766
+ clearScreen,
767
+ AGUIClient,
768
+ isTextContentEvent,
769
+ isErrorEvent,
770
+ getTextDelta,
771
+ getErrorMessage
772
+ };
773
+ //# sourceMappingURL=chunk-OIFV7TAT.js.map