@husar.ai/cli 0.4.1 → 0.4.2
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/AGENTS.md +835 -0
- package/dist/auth/api.d.ts +15 -0
- package/dist/auth/api.js +86 -0
- package/dist/auth/api.js.map +1 -0
- package/dist/auth/config.d.ts +32 -0
- package/dist/auth/config.js +95 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/login.d.ts +30 -0
- package/dist/auth/login.js +450 -0
- package/dist/auth/login.js.map +1 -0
- package/dist/cli.js +83 -3
- package/dist/cli.js.map +1 -1
- package/dist/functions/create.d.ts +6 -0
- package/dist/functions/create.js +311 -0
- package/dist/functions/create.js.map +1 -0
- package/dist/mcp.js +20 -14
- package/dist/mcp.js.map +1 -1
- package/dist/types/config.d.ts +3 -1
- package/dist/types/config.js +12 -1
- package/dist/types/config.js.map +1 -1
- package/dist/zeus/const.js +635 -289
- package/dist/zeus/const.js.map +1 -1
- package/dist/zeus/index.d.ts +3079 -1601
- package/dist/zeus/index.js +150 -2
- package/dist/zeus/index.js.map +1 -1
- package/package.json +3 -3
- package/src/auth/api.ts +133 -0
- package/src/auth/config.ts +198 -0
- package/src/auth/login.ts +631 -0
- package/src/cli.ts +96 -4
- package/src/functions/create.ts +489 -0
- package/src/mcp.ts +47 -27
- package/src/types/config.ts +32 -1
- package/src/zeus/const.ts +641 -289
- package/src/zeus/index.ts +2996 -1465
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login flows for Husar CLI
|
|
3
|
+
*
|
|
4
|
+
* Two separate authentication flows:
|
|
5
|
+
* 1. Cloud login - OAuth via husar.ai, returns JWT for listing projects
|
|
6
|
+
* 2. Panel CMS login - SUPERADMIN credentials, returns adminToken for CMS operations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createServer, IncomingMessage, ServerResponse } from 'node:http';
|
|
10
|
+
import { URL } from 'node:url';
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
12
|
+
import { saveCloudAuth, saveProjectAuth, ProjectAuth, CloudAuth } from './config.js';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CALLBACK_PORT = 9876;
|
|
15
|
+
const CLOUD_AUTH_URL = process.env.HUSAR_AUTH_URL || 'https://husar.ai';
|
|
16
|
+
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
// SECURITY UTILITIES
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Escape HTML special characters to prevent XSS attacks
|
|
23
|
+
* Used when interpolating user-provided data into HTML responses
|
|
24
|
+
*/
|
|
25
|
+
function escapeHtml(str: string): string {
|
|
26
|
+
return str
|
|
27
|
+
.replace(/&/g, '&')
|
|
28
|
+
.replace(/</g, '<')
|
|
29
|
+
.replace(/>/g, '>')
|
|
30
|
+
.replace(/"/g, '"')
|
|
31
|
+
.replace(/'/g, ''');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate a cryptographically secure random state parameter for OAuth
|
|
36
|
+
* This prevents CSRF attacks in the OAuth flow
|
|
37
|
+
*/
|
|
38
|
+
function generateState(): string {
|
|
39
|
+
return randomBytes(32).toString('hex');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
43
|
+
// TYPES
|
|
44
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
45
|
+
|
|
46
|
+
export interface CloudLoginResult {
|
|
47
|
+
success: boolean;
|
|
48
|
+
email?: string;
|
|
49
|
+
accessToken?: string;
|
|
50
|
+
error?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PanelLoginResult {
|
|
54
|
+
success: boolean;
|
|
55
|
+
project?: ProjectAuth;
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @deprecated Use CloudLoginResult or PanelLoginResult instead */
|
|
60
|
+
export interface LoginResult {
|
|
61
|
+
success: boolean;
|
|
62
|
+
email?: string;
|
|
63
|
+
project?: ProjectAuth;
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
68
|
+
// CLOUD LOGIN FLOW
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Start cloud login flow - OAuth via husar.ai
|
|
73
|
+
* Returns JWT access token for cloud API operations (listing projects, etc.)
|
|
74
|
+
*
|
|
75
|
+
* Flow:
|
|
76
|
+
* 1. Opens browser to husar.ai/app/cli-auth
|
|
77
|
+
* 2. User logs in via OAuth (Google/GitHub/email)
|
|
78
|
+
* 3. Callback returns: access_token, user_email, state
|
|
79
|
+
*
|
|
80
|
+
* Security: Uses state parameter to prevent CSRF attacks
|
|
81
|
+
*/
|
|
82
|
+
export async function startCloudLoginFlow(options?: { port?: number; timeout?: number }): Promise<CloudLoginResult> {
|
|
83
|
+
const port = options?.port ?? DEFAULT_CALLBACK_PORT;
|
|
84
|
+
const timeout = options?.timeout ?? 300000; // 5 minutes default
|
|
85
|
+
|
|
86
|
+
// Generate state parameter for CSRF protection
|
|
87
|
+
const expectedState = generateState();
|
|
88
|
+
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
let resolved = false;
|
|
91
|
+
|
|
92
|
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
93
|
+
if (resolved) {
|
|
94
|
+
res.writeHead(200);
|
|
95
|
+
res.end('Already processed');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
100
|
+
|
|
101
|
+
if (url.pathname === '/callback') {
|
|
102
|
+
// Parse callback parameters (cloud auth returns JWT)
|
|
103
|
+
const accessToken = url.searchParams.get('access_token');
|
|
104
|
+
const userEmail = url.searchParams.get('user_email');
|
|
105
|
+
const returnedState = url.searchParams.get('state');
|
|
106
|
+
const error = url.searchParams.get('error');
|
|
107
|
+
|
|
108
|
+
if (error) {
|
|
109
|
+
resolved = true;
|
|
110
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
111
|
+
res.end(getErrorHtml(error));
|
|
112
|
+
server.close();
|
|
113
|
+
resolve({ success: false, error });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Validate state parameter to prevent CSRF
|
|
118
|
+
if (returnedState !== expectedState) {
|
|
119
|
+
resolved = true;
|
|
120
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
121
|
+
res.end(getErrorHtml('Invalid state parameter. This may be a CSRF attack.'));
|
|
122
|
+
server.close();
|
|
123
|
+
resolve({ success: false, error: 'State parameter mismatch - possible CSRF attack' });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!accessToken) {
|
|
128
|
+
resolved = true;
|
|
129
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
130
|
+
res.end(getErrorHtml('Missing access token'));
|
|
131
|
+
server.close();
|
|
132
|
+
resolve({ success: false, error: 'Missing access token from cloud auth' });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Success - save cloud auth and close server
|
|
137
|
+
resolved = true;
|
|
138
|
+
|
|
139
|
+
const cloudAuth: CloudAuth = {
|
|
140
|
+
email: userEmail ?? undefined,
|
|
141
|
+
accessToken,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Send success response before processing
|
|
145
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
146
|
+
res.end(getCloudSuccessHtml(userEmail ?? 'user'));
|
|
147
|
+
|
|
148
|
+
// Save cloud auth
|
|
149
|
+
saveCloudAuth(cloudAuth)
|
|
150
|
+
.then(() => {
|
|
151
|
+
server.close();
|
|
152
|
+
resolve({
|
|
153
|
+
success: true,
|
|
154
|
+
email: userEmail ?? undefined,
|
|
155
|
+
accessToken,
|
|
156
|
+
});
|
|
157
|
+
})
|
|
158
|
+
.catch((err: Error) => {
|
|
159
|
+
server.close();
|
|
160
|
+
resolve({ success: false, error: err.message });
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
res.writeHead(404);
|
|
164
|
+
res.end('Not found');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
server.listen(port, () => {
|
|
169
|
+
// Server is ready, open browser (include state parameter)
|
|
170
|
+
const authUrl = `${CLOUD_AUTH_URL}/en/app/cli-auth?callback_port=${port}&state=${expectedState}`;
|
|
171
|
+
console.log(`\n🔐 Opening browser for cloud authentication...`);
|
|
172
|
+
console.log(` If the browser doesn't open, visit: ${authUrl}\n`);
|
|
173
|
+
openBrowser(authUrl);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
server.on('error', (err) => {
|
|
177
|
+
if (!resolved) {
|
|
178
|
+
resolved = true;
|
|
179
|
+
resolve({ success: false, error: `Server error: ${err.message}` });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Timeout after specified duration
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
if (!resolved) {
|
|
186
|
+
resolved = true;
|
|
187
|
+
server.close();
|
|
188
|
+
resolve({ success: false, error: 'Cloud login timed out. Please try again.' });
|
|
189
|
+
}
|
|
190
|
+
}, timeout);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
195
|
+
// PANEL CMS LOGIN FLOW
|
|
196
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Start panel CMS login flow - SUPERADMIN credentials
|
|
200
|
+
* Returns adminToken for CMS operations
|
|
201
|
+
*
|
|
202
|
+
* Flow:
|
|
203
|
+
* 1. Opens browser to {panelHost}/connect-device
|
|
204
|
+
* 2. User enters SUPERADMIN username/password (from email)
|
|
205
|
+
* 3. Panel verifies credentials and returns adminToken
|
|
206
|
+
* 4. Callback returns: admin_token, host, project_name, state
|
|
207
|
+
*
|
|
208
|
+
* Security: Uses state parameter to prevent CSRF attacks
|
|
209
|
+
*/
|
|
210
|
+
export async function startPanelLoginFlow(
|
|
211
|
+
panelHost: string,
|
|
212
|
+
options?: { port?: number; timeout?: number },
|
|
213
|
+
): Promise<PanelLoginResult> {
|
|
214
|
+
const port = options?.port ?? DEFAULT_CALLBACK_PORT;
|
|
215
|
+
const timeout = options?.timeout ?? 300000; // 5 minutes default
|
|
216
|
+
|
|
217
|
+
// Normalize host URL
|
|
218
|
+
const normalizedHost = panelHost.endsWith('/') ? panelHost.slice(0, -1) : panelHost;
|
|
219
|
+
|
|
220
|
+
// Generate state parameter for CSRF protection
|
|
221
|
+
const expectedState = generateState();
|
|
222
|
+
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
let resolved = false;
|
|
225
|
+
|
|
226
|
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
227
|
+
if (resolved) {
|
|
228
|
+
res.writeHead(200);
|
|
229
|
+
res.end('Already processed');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
234
|
+
|
|
235
|
+
if (url.pathname === '/callback') {
|
|
236
|
+
// Parse callback parameters (panel returns adminToken)
|
|
237
|
+
const adminToken = url.searchParams.get('admin_token');
|
|
238
|
+
const host = url.searchParams.get('host');
|
|
239
|
+
const projectName = url.searchParams.get('project_name');
|
|
240
|
+
const returnedState = url.searchParams.get('state');
|
|
241
|
+
const error = url.searchParams.get('error');
|
|
242
|
+
|
|
243
|
+
if (error) {
|
|
244
|
+
resolved = true;
|
|
245
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
246
|
+
res.end(getErrorHtml(error));
|
|
247
|
+
server.close();
|
|
248
|
+
resolve({ success: false, error });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Validate state parameter to prevent CSRF
|
|
253
|
+
if (returnedState !== expectedState) {
|
|
254
|
+
resolved = true;
|
|
255
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
256
|
+
res.end(getErrorHtml('Invalid state parameter. This may be a CSRF attack.'));
|
|
257
|
+
server.close();
|
|
258
|
+
resolve({ success: false, error: 'State parameter mismatch - possible CSRF attack' });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!adminToken || !host || !projectName) {
|
|
263
|
+
resolved = true;
|
|
264
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
265
|
+
res.end(getErrorHtml('Missing required parameters'));
|
|
266
|
+
server.close();
|
|
267
|
+
resolve({ success: false, error: 'Missing admin_token, host, or project_name from panel' });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Success - save project auth and close server
|
|
272
|
+
resolved = true;
|
|
273
|
+
|
|
274
|
+
const project: ProjectAuth = {
|
|
275
|
+
projectName,
|
|
276
|
+
host,
|
|
277
|
+
adminToken,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Send success response before processing
|
|
281
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
282
|
+
res.end(getPanelSuccessHtml(projectName));
|
|
283
|
+
|
|
284
|
+
// Save project auth
|
|
285
|
+
saveProjectAuth(project)
|
|
286
|
+
.then(() => {
|
|
287
|
+
server.close();
|
|
288
|
+
resolve({
|
|
289
|
+
success: true,
|
|
290
|
+
project,
|
|
291
|
+
});
|
|
292
|
+
})
|
|
293
|
+
.catch((err) => {
|
|
294
|
+
server.close();
|
|
295
|
+
resolve({ success: false, error: err.message });
|
|
296
|
+
});
|
|
297
|
+
} else {
|
|
298
|
+
res.writeHead(404);
|
|
299
|
+
res.end('Not found');
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
server.listen(port, () => {
|
|
304
|
+
// Server is ready, open browser (include state parameter)
|
|
305
|
+
const authUrl = `${normalizedHost}/connect-device?callback_port=${port}&state=${expectedState}`;
|
|
306
|
+
console.log(`\n🔐 Opening browser for CMS authentication...`);
|
|
307
|
+
console.log(` You'll need your SUPERADMIN credentials (from the welcome email)`);
|
|
308
|
+
console.log(` If the browser doesn't open, visit: ${authUrl}\n`);
|
|
309
|
+
openBrowser(authUrl);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
server.on('error', (err) => {
|
|
313
|
+
if (!resolved) {
|
|
314
|
+
resolved = true;
|
|
315
|
+
resolve({ success: false, error: `Server error: ${err.message}` });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Timeout after specified duration
|
|
320
|
+
setTimeout(() => {
|
|
321
|
+
if (!resolved) {
|
|
322
|
+
resolved = true;
|
|
323
|
+
server.close();
|
|
324
|
+
resolve({ success: false, error: 'Panel login timed out. Please try again.' });
|
|
325
|
+
}
|
|
326
|
+
}, timeout);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
331
|
+
// LEGACY COMBINED LOGIN FLOW (for backward compatibility)
|
|
332
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* @deprecated Use startCloudLoginFlow() or startPanelLoginFlow() instead
|
|
336
|
+
*
|
|
337
|
+
* Start local callback server and initiate browser login flow
|
|
338
|
+
* This is the old combined flow that opens husar.ai/app/cli-auth
|
|
339
|
+
* and expects both project selection AND admin token generation
|
|
340
|
+
*/
|
|
341
|
+
export async function startLoginFlow(options?: { port?: number; timeout?: number }): Promise<LoginResult> {
|
|
342
|
+
const port = options?.port ?? DEFAULT_CALLBACK_PORT;
|
|
343
|
+
const timeout = options?.timeout ?? 300000; // 5 minutes default
|
|
344
|
+
|
|
345
|
+
return new Promise((resolve) => {
|
|
346
|
+
let resolved = false;
|
|
347
|
+
|
|
348
|
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
349
|
+
if (resolved) {
|
|
350
|
+
res.writeHead(200);
|
|
351
|
+
res.end('Already processed');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
356
|
+
|
|
357
|
+
if (url.pathname === '/callback') {
|
|
358
|
+
// Parse callback parameters
|
|
359
|
+
const host = url.searchParams.get('host');
|
|
360
|
+
const adminToken = url.searchParams.get('admin_token');
|
|
361
|
+
const projectName = url.searchParams.get('project_name');
|
|
362
|
+
const userEmail = url.searchParams.get('user_email');
|
|
363
|
+
const error = url.searchParams.get('error');
|
|
364
|
+
|
|
365
|
+
if (error) {
|
|
366
|
+
resolved = true;
|
|
367
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
368
|
+
res.end(getErrorHtml(error));
|
|
369
|
+
server.close();
|
|
370
|
+
resolve({ success: false, error });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (!host || !adminToken || !projectName) {
|
|
375
|
+
resolved = true;
|
|
376
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
377
|
+
res.end(getErrorHtml('Missing required parameters'));
|
|
378
|
+
server.close();
|
|
379
|
+
resolve({ success: false, error: 'Missing required parameters' });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Success - save credentials and close server
|
|
384
|
+
resolved = true;
|
|
385
|
+
|
|
386
|
+
const project: ProjectAuth = {
|
|
387
|
+
projectName,
|
|
388
|
+
host,
|
|
389
|
+
adminToken,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Send success response before processing
|
|
393
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
394
|
+
res.end(getPanelSuccessHtml(projectName, userEmail ?? 'user'));
|
|
395
|
+
|
|
396
|
+
// Save credentials
|
|
397
|
+
Promise.all([
|
|
398
|
+
userEmail ? saveCloudAuth({ email: userEmail, accessToken: adminToken }) : Promise.resolve(),
|
|
399
|
+
saveProjectAuth(project),
|
|
400
|
+
])
|
|
401
|
+
.then(() => {
|
|
402
|
+
server.close();
|
|
403
|
+
resolve({
|
|
404
|
+
success: true,
|
|
405
|
+
email: userEmail ?? undefined,
|
|
406
|
+
project,
|
|
407
|
+
});
|
|
408
|
+
})
|
|
409
|
+
.catch((err) => {
|
|
410
|
+
server.close();
|
|
411
|
+
resolve({ success: false, error: err.message });
|
|
412
|
+
});
|
|
413
|
+
} else {
|
|
414
|
+
res.writeHead(404);
|
|
415
|
+
res.end('Not found');
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
server.listen(port, () => {
|
|
420
|
+
// Server is ready, open browser
|
|
421
|
+
const authUrl = `${CLOUD_AUTH_URL}/en/app/cli-auth?callback_port=${port}`;
|
|
422
|
+
console.log(`\n🔐 Opening browser for authentication...`);
|
|
423
|
+
console.log(` If the browser doesn't open, visit: ${authUrl}\n`);
|
|
424
|
+
openBrowser(authUrl);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
server.on('error', (err) => {
|
|
428
|
+
if (!resolved) {
|
|
429
|
+
resolved = true;
|
|
430
|
+
resolve({ success: false, error: `Server error: ${err.message}` });
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Timeout after specified duration
|
|
435
|
+
setTimeout(() => {
|
|
436
|
+
if (!resolved) {
|
|
437
|
+
resolved = true;
|
|
438
|
+
server.close();
|
|
439
|
+
resolve({ success: false, error: 'Login timed out. Please try again.' });
|
|
440
|
+
}
|
|
441
|
+
}, timeout);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
446
|
+
// HELPERS
|
|
447
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Open URL in default browser
|
|
451
|
+
*/
|
|
452
|
+
function openBrowser(url: string): void {
|
|
453
|
+
const { exec } = require('node:child_process');
|
|
454
|
+
const platform = process.platform;
|
|
455
|
+
|
|
456
|
+
let command: string;
|
|
457
|
+
if (platform === 'darwin') {
|
|
458
|
+
command = `open "${url}"`;
|
|
459
|
+
} else if (platform === 'win32') {
|
|
460
|
+
command = `start "" "${url}"`;
|
|
461
|
+
} else {
|
|
462
|
+
// Linux and others
|
|
463
|
+
command = `xdg-open "${url}"`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
exec(command, (err: Error | null) => {
|
|
467
|
+
if (err) {
|
|
468
|
+
console.error('Could not open browser automatically.');
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Generate cloud auth success HTML page
|
|
475
|
+
*/
|
|
476
|
+
function getCloudSuccessHtml(email: string): string {
|
|
477
|
+
return `
|
|
478
|
+
<!DOCTYPE html>
|
|
479
|
+
<html>
|
|
480
|
+
<head>
|
|
481
|
+
<meta charset="utf-8">
|
|
482
|
+
<title>Husar CLI - Cloud Login Successful</title>
|
|
483
|
+
<style>
|
|
484
|
+
body {
|
|
485
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
486
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
487
|
+
color: white;
|
|
488
|
+
min-height: 100vh;
|
|
489
|
+
display: flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
justify-content: center;
|
|
492
|
+
margin: 0;
|
|
493
|
+
}
|
|
494
|
+
.container {
|
|
495
|
+
text-align: center;
|
|
496
|
+
padding: 40px;
|
|
497
|
+
background: rgba(255,255,255,0.05);
|
|
498
|
+
border-radius: 16px;
|
|
499
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
500
|
+
}
|
|
501
|
+
.success-icon {
|
|
502
|
+
font-size: 64px;
|
|
503
|
+
margin-bottom: 20px;
|
|
504
|
+
}
|
|
505
|
+
h1 { margin: 0 0 10px; }
|
|
506
|
+
p { color: #a0a0a0; margin: 5px 0; }
|
|
507
|
+
.email { color: #60a5fa; font-weight: 600; }
|
|
508
|
+
.close-note {
|
|
509
|
+
margin-top: 30px;
|
|
510
|
+
font-size: 14px;
|
|
511
|
+
color: #666;
|
|
512
|
+
}
|
|
513
|
+
</style>
|
|
514
|
+
</head>
|
|
515
|
+
<body>
|
|
516
|
+
<div class="container">
|
|
517
|
+
<div class="success-icon">✅</div>
|
|
518
|
+
<h1>Cloud Login Successful!</h1>
|
|
519
|
+
<p>Logged in as <span class="email">${escapeHtml(email)}</span></p>
|
|
520
|
+
<p class="close-note">You can close this window and return to the terminal.</p>
|
|
521
|
+
</div>
|
|
522
|
+
</body>
|
|
523
|
+
</html>
|
|
524
|
+
`.trim();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Generate panel auth success HTML page
|
|
529
|
+
*/
|
|
530
|
+
function getPanelSuccessHtml(projectName: string, email?: string): string {
|
|
531
|
+
const emailLine = email ? `<p>Logged in as <strong>${escapeHtml(email)}</strong></p>` : '';
|
|
532
|
+
|
|
533
|
+
return `
|
|
534
|
+
<!DOCTYPE html>
|
|
535
|
+
<html>
|
|
536
|
+
<head>
|
|
537
|
+
<meta charset="utf-8">
|
|
538
|
+
<title>Husar CLI - CMS Connected</title>
|
|
539
|
+
<style>
|
|
540
|
+
body {
|
|
541
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
542
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
543
|
+
color: white;
|
|
544
|
+
min-height: 100vh;
|
|
545
|
+
display: flex;
|
|
546
|
+
align-items: center;
|
|
547
|
+
justify-content: center;
|
|
548
|
+
margin: 0;
|
|
549
|
+
}
|
|
550
|
+
.container {
|
|
551
|
+
text-align: center;
|
|
552
|
+
padding: 40px;
|
|
553
|
+
background: rgba(255,255,255,0.05);
|
|
554
|
+
border-radius: 16px;
|
|
555
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
556
|
+
}
|
|
557
|
+
.success-icon {
|
|
558
|
+
font-size: 64px;
|
|
559
|
+
margin-bottom: 20px;
|
|
560
|
+
}
|
|
561
|
+
h1 { margin: 0 0 10px; }
|
|
562
|
+
p { color: #a0a0a0; margin: 5px 0; }
|
|
563
|
+
.project { color: #60a5fa; font-weight: 600; }
|
|
564
|
+
.close-note {
|
|
565
|
+
margin-top: 30px;
|
|
566
|
+
font-size: 14px;
|
|
567
|
+
color: #666;
|
|
568
|
+
}
|
|
569
|
+
</style>
|
|
570
|
+
</head>
|
|
571
|
+
<body>
|
|
572
|
+
<div class="container">
|
|
573
|
+
<div class="success-icon">✅</div>
|
|
574
|
+
<h1>CMS Connected!</h1>
|
|
575
|
+
${emailLine}
|
|
576
|
+
<p>Connected to project <span class="project">${escapeHtml(projectName)}</span></p>
|
|
577
|
+
<p class="close-note">You can close this window and return to the terminal.</p>
|
|
578
|
+
</div>
|
|
579
|
+
</body>
|
|
580
|
+
</html>
|
|
581
|
+
`.trim();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Generate error HTML page
|
|
586
|
+
*/
|
|
587
|
+
function getErrorHtml(error: string): string {
|
|
588
|
+
return `
|
|
589
|
+
<!DOCTYPE html>
|
|
590
|
+
<html>
|
|
591
|
+
<head>
|
|
592
|
+
<meta charset="utf-8">
|
|
593
|
+
<title>Husar CLI - Error</title>
|
|
594
|
+
<style>
|
|
595
|
+
body {
|
|
596
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
597
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
598
|
+
color: white;
|
|
599
|
+
min-height: 100vh;
|
|
600
|
+
display: flex;
|
|
601
|
+
align-items: center;
|
|
602
|
+
justify-content: center;
|
|
603
|
+
margin: 0;
|
|
604
|
+
}
|
|
605
|
+
.container {
|
|
606
|
+
text-align: center;
|
|
607
|
+
padding: 40px;
|
|
608
|
+
background: rgba(255,255,255,0.05);
|
|
609
|
+
border-radius: 16px;
|
|
610
|
+
border: 1px solid rgba(239,68,68,0.3);
|
|
611
|
+
}
|
|
612
|
+
.error-icon {
|
|
613
|
+
font-size: 64px;
|
|
614
|
+
margin-bottom: 20px;
|
|
615
|
+
}
|
|
616
|
+
h1 { margin: 0 0 10px; color: #ef4444; }
|
|
617
|
+
p { color: #a0a0a0; margin: 5px 0; }
|
|
618
|
+
.error-msg { color: #fca5a5; }
|
|
619
|
+
</style>
|
|
620
|
+
</head>
|
|
621
|
+
<body>
|
|
622
|
+
<div class="container">
|
|
623
|
+
<div class="error-icon">❌</div>
|
|
624
|
+
<h1>Authorization Failed</h1>
|
|
625
|
+
<p class="error-msg">${escapeHtml(error)}</p>
|
|
626
|
+
<p>Please try again or contact support.</p>
|
|
627
|
+
</div>
|
|
628
|
+
</body>
|
|
629
|
+
</html>
|
|
630
|
+
`.trim();
|
|
631
|
+
}
|