@vibedhost/cli 1.0.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.
package/src/index.ts ADDED
@@ -0,0 +1,1028 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import {
6
+ API_BASE_URL,
7
+ readGlobalConfig,
8
+ writeGlobalConfig,
9
+ clearGlobalConfig,
10
+ readProjectConfig,
11
+ writeProjectConfig,
12
+ openBrowser,
13
+ scanDirectory,
14
+ createZipBuffer,
15
+ MAX_PAYLOAD_BYTES,
16
+ askQuestion,
17
+ inferAppName,
18
+ computeBufferSha256
19
+ } from "./utils";
20
+
21
+ const args = process.argv.slice(2);
22
+ const command = args[0] || "help";
23
+
24
+ async function main() {
25
+ switch (command) {
26
+ case "login":
27
+ await handleLogin();
28
+ break;
29
+ case "logout":
30
+ await handleLogout();
31
+ break;
32
+ case "whoami":
33
+ await handleWhoami();
34
+ break;
35
+ case "deploy":
36
+ await handleDeploy();
37
+ break;
38
+ case "status":
39
+ await handleStatus();
40
+ break;
41
+ case "logs":
42
+ await handleLogs();
43
+ break;
44
+ case "db":
45
+ await handleDb();
46
+ break;
47
+ case "env":
48
+ await handleEnv();
49
+ break;
50
+ case "port":
51
+ await handlePort();
52
+ break;
53
+ case "help":
54
+ case "--help":
55
+ case "-h":
56
+ default:
57
+ printHelp();
58
+ break;
59
+ }
60
+ }
61
+
62
+ function printHelp() {
63
+ console.log(`
64
+ VibedHost Cloud Engine — Command Line Interface
65
+ --------------------------------------------------------------------------
66
+ Deploy web applications and manage dedicated cloud workspaces directly from
67
+ your terminal, Claude Code, Cursor, or automated CI/CD pipelines.
68
+
69
+ Website: https://www.vibedhost.com
70
+ Dashboard: https://www.vibedhost.com/dashboard/workspaces
71
+
72
+ Usage:
73
+ npx vibed [command] [options]
74
+
75
+ Getting Started:
76
+ 1. Create account & workspace: https://www.vibedhost.com/register
77
+ 2. Authenticate terminal: npx vibed login
78
+ 3. Deploy local project: npx vibed deploy
79
+
80
+ Commands:
81
+ login Authenticate this terminal with your VibedHost account
82
+ logout Disconnect active account credentials from this machine
83
+ whoami Display authenticated account and workspace inventory
84
+ deploy [dir] Package and deploy local project to dedicated workspace
85
+ status [app] Check real-time application deployment and routing status
86
+ logs [app] Stream live application logs or container build output
87
+ db <create|list|link> Manage dedicated databases and link to applications
88
+ env <get|set> Manage container environment variables
89
+ port set <port> Update container listening port (e.g. 3000, 8080)
90
+
91
+ Deployment Options:
92
+ --workspace <id> Specify target workspace ID (skips interactive selection)
93
+ --name <name> Override service name for this deployment
94
+ --port <port> Specify internal container port (e.g. 3000, 80)
95
+ --domain <domain> Specify deployment domain suffix (e.g. vibedhost.online)
96
+ --force, -f Force rebuild and bypass zero-change detection
97
+
98
+ Inspection Options:
99
+ --ai Format latest error snippet for AI coding assistants
100
+
101
+ Examples:
102
+ npx vibed login
103
+ npx vibed deploy
104
+ npx vibed deploy ./dist --name frontend-spa
105
+ npx vibed status
106
+ npx vibed logs my-app
107
+ npx vibed logs my-app --ai
108
+ npx vibed env set DATABASE_URL=postgresql://...
109
+ `);
110
+ }
111
+
112
+ async function handleLogin() {
113
+ console.log("Initializing secure authentication session...");
114
+
115
+ try {
116
+ const res = await fetch(`${API_BASE_URL}/api/cli/auth/session`, {
117
+ method: "POST"
118
+ });
119
+ const data = (await res.json()) as any;
120
+
121
+ if (!res.ok || !data.sessionCode) {
122
+ console.error(`Authentication error: ${data.error || "Failed to initialize session."}`);
123
+ process.exit(1);
124
+ }
125
+
126
+ const { sessionCode, userCode, authUrl } = data;
127
+
128
+ if (userCode) {
129
+ console.log(`\nTerminal Pairing Code: ${userCode}`);
130
+ }
131
+ console.log("Opening browser for authentication...");
132
+ console.log(`If your browser does not open automatically, visit:\n${authUrl}\n`);
133
+ console.log("Verify that the pairing code matches in your browser (timeout in 15 minutes)...");
134
+
135
+ openBrowser(authUrl);
136
+
137
+ // Poll for authorization completion
138
+ const startTime = Date.now();
139
+ const timeoutMs = 15 * 60 * 1000;
140
+
141
+ while (Date.now() - startTime < timeoutMs) {
142
+ await new Promise((r) => setTimeout(r, 1800));
143
+
144
+ const pollRes = await fetch(`${API_BASE_URL}/api/cli/auth/poll?sessionCode=${sessionCode}`);
145
+ const pollData = (await pollRes.json()) as any;
146
+
147
+ if (pollRes.ok && pollData.status === "approved" && pollData.token) {
148
+ writeGlobalConfig({
149
+ token: pollData.token,
150
+ email: pollData.user?.email,
151
+ name: pollData.user?.name
152
+ });
153
+
154
+ console.log(`\nAuthentication successful!`);
155
+ console.log(`Logged in as: ${pollData.user?.email || "Authenticated User"}`);
156
+ console.log(`Run 'npx vibed deploy' inside any project directory to deploy.`);
157
+ return;
158
+ }
159
+
160
+ if (pollData.status === "expired" || pollData.status === "invalid") {
161
+ console.error("\nSession expired or rejected. Please run 'npx vibed login' again.");
162
+ process.exit(1);
163
+ }
164
+ }
165
+
166
+ console.error("\nAuthentication timed out. Please try again.");
167
+ process.exit(1);
168
+ } catch (err: any) {
169
+ console.error(`\nConnection error: ${err.message}`);
170
+ process.exit(1);
171
+ }
172
+ }
173
+
174
+ function handleLogout() {
175
+ clearGlobalConfig();
176
+ console.log("Logged out. Local authentication credentials cleared.");
177
+ }
178
+
179
+ async function handleWhoami() {
180
+ const config = readGlobalConfig();
181
+ if (!config?.token) {
182
+ console.log("Not authenticated. Run 'npx vibed login' to connect your account.");
183
+ return;
184
+ }
185
+
186
+ try {
187
+ const res = await fetch(`${API_BASE_URL}/api/cli/workspaces`, {
188
+ headers: { Authorization: `Bearer ${config.token}` }
189
+ });
190
+ const data = (await res.json()) as any;
191
+
192
+ if (!res.ok) {
193
+ console.error(`Error: ${data.error || "Failed to retrieve account details."}`);
194
+ return;
195
+ }
196
+
197
+ console.log("\nVibedHost Account Overview");
198
+ console.log("------------------------------------------------------------");
199
+ console.log(`User: ${data.userEmail || config.email || "Unknown"}`);
200
+ console.log(`Wallet Balance: €${(data.walletBalance || 0).toFixed(2)}`);
201
+ console.log(`Workspaces: ${data.workspaces?.length || 0} active`);
202
+
203
+ if (data.workspaces?.length > 0) {
204
+ console.log("\nDedicated Cloud Workspaces:");
205
+ data.workspaces.forEach((ws: any, idx: number) => {
206
+ const ramStr = ws.ramTotalGB ? `${(ws.memoryUsageGB || 0.4).toFixed(1)} GB / ${ws.ramTotalGB} GB` : "Allocated";
207
+ const diskStr = ws.storageTotalGB ? `${(ws.storageUsageGB || 2.0).toFixed(1)} GB / ${ws.storageTotalGB} GB` : "NVMe";
208
+ const bwLimit = ws.bandwidthLimitGB > 0 ? `${(ws.bandwidthLimitGB / 1000).toFixed(0)} TB` : "Unmetered";
209
+ const bwStr = `${(ws.bandwidthUsedGB || 0).toFixed(1)} GB / ${bwLimit}`;
210
+
211
+ console.log(` [${idx + 1}] ${ws.name} (ID: ${ws.id})`);
212
+ console.log(` Tier: ${ws.planName} • Status: ${ws.status}`);
213
+ console.log(` Telemetry: RAM: ${ramStr} • Disk: ${diskStr} • Traffic: ${bwStr}`);
214
+ console.log(` Deployed Services: ${ws.apps?.length || 0} active`);
215
+ });
216
+ } else {
217
+ console.log("\nNo workspaces provisioned yet.");
218
+ console.log("Launch a workspace on the web: https://www.vibedhost.com/dashboard/workspaces");
219
+ }
220
+ console.log("");
221
+ } catch (err: any) {
222
+ console.error(`Connection error: ${err.message}`);
223
+ }
224
+ }
225
+
226
+ async function handleDeploy() {
227
+ const config = readGlobalConfig();
228
+ if (!config?.token) {
229
+ console.log(`
230
+ VibedHost Cloud Engine — Deployment Gateway
231
+ ------------------------------------------------------------
232
+ You are not currently authenticated.
233
+
234
+ Get Started:
235
+ 1. Create an account: https://www.vibedhost.com/register
236
+ 2. Authenticate: npx vibed login
237
+ `);
238
+ process.exit(1);
239
+ }
240
+
241
+ // Parse command options
242
+ let targetDir = process.cwd();
243
+ const dirArg = args[1] && !args[1].startsWith("--") ? args[1] : null;
244
+ if (dirArg) {
245
+ targetDir = path.resolve(process.cwd(), dirArg);
246
+ }
247
+
248
+ const workspaceFlagIndex = args.indexOf("--workspace");
249
+ const nameFlagIndex = args.indexOf("--name");
250
+ const portFlagIndex = args.indexOf("--port");
251
+ const domainFlagIndex = args.indexOf("--domain");
252
+
253
+ const explicitWorkspaceId = workspaceFlagIndex !== -1 ? args[workspaceFlagIndex + 1] : null;
254
+ const explicitAppName = nameFlagIndex !== -1 ? args[nameFlagIndex + 1] : null;
255
+ const explicitPort = portFlagIndex !== -1 ? args[portFlagIndex + 1] : null;
256
+ const explicitDomain = domainFlagIndex !== -1 ? args[domainFlagIndex + 1] : null;
257
+
258
+ if (!fs.existsSync(targetDir)) {
259
+ console.error(`Directory not found: ${targetDir}`);
260
+ process.exit(1);
261
+ }
262
+
263
+ const projectConfig = readProjectConfig(targetDir);
264
+
265
+ // 1. Fetch available workspaces
266
+ console.log("Verifying account and active workspaces...");
267
+ let workspaces: any[] = [];
268
+ try {
269
+ const wsRes = await fetch(`${API_BASE_URL}/api/cli/workspaces`, {
270
+ headers: { Authorization: `Bearer ${config.token}` }
271
+ });
272
+ const wsData = (await wsRes.json()) as any;
273
+
274
+ if (!wsRes.ok) {
275
+ console.error(`Authentication error: ${wsData.error || "Invalid token. Please run 'npx vibed login'."}`);
276
+ process.exit(1);
277
+ }
278
+
279
+ workspaces = (wsData.workspaces || []).filter((w: any) => w.status === "running");
280
+
281
+ if (workspaces.length === 0) {
282
+ console.log(`
283
+ No active dedicated workspaces found.
284
+ Wallet Balance: €${(wsData.walletBalance || 0).toFixed(2)}
285
+
286
+ To deploy applications, launch a dedicated cloud workspace:
287
+ https://www.vibedhost.com/dashboard/workspaces
288
+
289
+ Once provisioned, run 'npx vibed deploy' again.
290
+ `);
291
+ process.exit(1);
292
+ }
293
+ } catch (err: any) {
294
+ console.error(`Failed to reach VibedHost API: ${err.message}`);
295
+ process.exit(1);
296
+ }
297
+
298
+ // 2. Resolve target workspace ID (with interactive prompt if unlinked)
299
+ let targetWorkspaceId = explicitWorkspaceId || projectConfig?.workspaceId;
300
+
301
+ if (!targetWorkspaceId && process.stdin.isTTY && !explicitWorkspaceId) {
302
+ if (workspaces.length === 1) {
303
+ const confirmWs = await askQuestion(
304
+ `Target Workspace: ${workspaces[0].name} (${workspaces[0].planName}) [Y/n]`,
305
+ "y"
306
+ );
307
+ if (confirmWs.toLowerCase() === "n" || confirmWs.toLowerCase() === "no") {
308
+ console.log("Deployment cancelled.");
309
+ process.exit(0);
310
+ }
311
+ targetWorkspaceId = workspaces[0].id;
312
+ } else {
313
+ console.log("\nSelect target workspace for this deployment:");
314
+ workspaces.forEach((w: any, idx: number) => {
315
+ console.log(` [${idx + 1}] ${w.name} (${w.planName})`);
316
+ });
317
+ const chosenIdx = await askQuestion("Enter number", "1");
318
+ const num = parseInt(chosenIdx, 10);
319
+ const selected = workspaces[num - 1] || workspaces[0];
320
+ targetWorkspaceId = selected.id;
321
+ }
322
+ } else if (!targetWorkspaceId) {
323
+ targetWorkspaceId = workspaces[0].id;
324
+ }
325
+
326
+ const targetWs = workspaces.find((w) => w.id === targetWorkspaceId) || workspaces[0];
327
+
328
+ // 3. Resolve service name
329
+ let defaultAppName = projectConfig?.appName || inferAppName(targetDir);
330
+ let appName = explicitAppName;
331
+
332
+ if (!appName && !projectConfig?.appName && process.stdin.isTTY) {
333
+ appName = await askQuestion("Service Name", defaultAppName);
334
+ } else if (!appName) {
335
+ appName = defaultAppName;
336
+ }
337
+
338
+ appName = String(appName)
339
+ .toLowerCase()
340
+ .replace(/[^a-z0-9-]/g, "-")
341
+ .replace(/(^-|-$)+/g, "");
342
+
343
+ const hasForceFlag = args.includes("--force") || args.includes("-f");
344
+
345
+ // Linked Project Confirmation Guard
346
+ if (projectConfig?.workspaceId && process.stdin.isTTY && !explicitWorkspaceId && !hasForceFlag) {
347
+ const confirmLinked = await askQuestion(
348
+ `Deploying updates to existing service '${appName}' (Workspace: ${targetWs.name}) [Y/n]`,
349
+ "y"
350
+ );
351
+ if (confirmLinked.toLowerCase() === "n" || confirmLinked.toLowerCase() === "no") {
352
+ console.log("Deployment cancelled.");
353
+ process.exit(0);
354
+ }
355
+ }
356
+
357
+ // 3.5 Resolve deployment domain
358
+ let targetDomainSuffix = explicitDomain || projectConfig?.domain;
359
+ if (!targetDomainSuffix && process.stdin.isTTY) {
360
+ try {
361
+ const domRes = await fetch(`${API_BASE_URL}/api/cli/domains`, {
362
+ headers: { Authorization: `Bearer ${config.token}` }
363
+ });
364
+ const domainsList = ((await domRes.json()) as any[]) || [];
365
+ if (domainsList.length > 1) {
366
+ console.log("\nSelect deployment domain:");
367
+ domainsList.forEach((d: any, idx: number) => {
368
+ console.log(` [${idx + 1}] .${d.domain}${d.isDefault ? " (Default)" : ""}`);
369
+ });
370
+ const chosenDomIdx = await askQuestion("Enter number", "1");
371
+ const dNum = parseInt(chosenDomIdx, 10);
372
+ const selectedDom = domainsList[dNum - 1] || domainsList[0];
373
+ targetDomainSuffix = selectedDom.domain;
374
+ } else if (domainsList.length === 1) {
375
+ targetDomainSuffix = domainsList[0].domain;
376
+ }
377
+ } catch {}
378
+ }
379
+
380
+ // 4. Scan Directory & Create In-Memory ZIP
381
+ console.log(`\nScanning project directory (${path.basename(targetDir)})...`);
382
+ const { files, bloatReport } = scanDirectory(targetDir);
383
+
384
+ if (files.length === 0) {
385
+ console.error("Error: No deployable source files found in this directory.");
386
+ process.exit(1);
387
+ }
388
+
389
+ console.log(`Packaging ${files.length} project files in memory...`);
390
+ const zipBuffer = createZipBuffer(files);
391
+ const zipSizeBytes = zipBuffer.length;
392
+ const zipSizeMb = (zipSizeBytes / (1024 * 1024)).toFixed(2);
393
+
394
+ // Pre-flight Size Boundary Check
395
+ if (zipSizeBytes > MAX_PAYLOAD_BYTES) {
396
+ console.error(`
397
+ Error: Project archive size (${zipSizeMb} MB) exceeds the 50.0 MB limit.
398
+
399
+ Bloat directories detected:`);
400
+ Object.entries(bloatReport).forEach(([dir, size]) => {
401
+ console.error(` - ${dir}/ (${(size / (1024 * 1024)).toFixed(1)} MB)`);
402
+ });
403
+ console.error(`
404
+ Resolution:
405
+ 1. Ensure 'node_modules', '.next', '.venv', and build caches are excluded.
406
+ 2. Run 'npx vibed deploy' again.
407
+ `);
408
+ process.exit(1);
409
+ }
410
+
411
+ // Zero-Change Guard: Prevent accidental re-deploys when files are unchanged
412
+ const currentZipHash = computeBufferSha256(zipBuffer);
413
+ if (
414
+ projectConfig?.lastDeployHash &&
415
+ projectConfig.lastDeployHash === currentZipHash &&
416
+ process.stdin.isTTY &&
417
+ !hasForceFlag
418
+ ) {
419
+ const confirmRedeploy = await askQuestion(
420
+ `No local code changes detected since last deployment (${appName}).\nDo you want to re-deploy anyway? [y/N]`,
421
+ "n"
422
+ );
423
+ if (confirmRedeploy.toLowerCase() !== "y" && confirmRedeploy.toLowerCase() !== "yes") {
424
+ console.log("Deployment cancelled (no changes detected).");
425
+ process.exit(0);
426
+ }
427
+ }
428
+
429
+ console.log(`Archive packaged (${zipSizeMb} MB). Uploading to workspace [${targetWs.name}]...`);
430
+
431
+ // 5. Send Multipart Deployment Request
432
+ try {
433
+ const boundary = `----VibedFormBoundary${Date.now()}`;
434
+ const preBuffer = Buffer.from(
435
+ `--${boundary}\r\n` +
436
+ `Content-Disposition: form-data; name="file"; filename="project.zip"\r\n` +
437
+ `Content-Type: application/zip\r\n\r\n`
438
+ );
439
+
440
+ const midBuffer = Buffer.from(
441
+ `\r\n--${boundary}\r\n` +
442
+ `Content-Disposition: form-data; name="appName"\r\n\r\n${appName}\r\n` +
443
+ `--${boundary}\r\n` +
444
+ `Content-Disposition: form-data; name="workspaceId"\r\n\r\n${targetWs.id}\r\n` +
445
+ (explicitPort ? `--${boundary}\r\nContent-Disposition: form-data; name="containerPort"\r\n\r\n${explicitPort}\r\n` : "") +
446
+ (targetDomainSuffix ? `--${boundary}\r\nContent-Disposition: form-data; name="deploymentDomain"\r\n\r\n${targetDomainSuffix}\r\n` : "") +
447
+ `--${boundary}--\r\n`
448
+ );
449
+
450
+ const payload = Buffer.concat([preBuffer, zipBuffer, midBuffer]);
451
+
452
+ const deployRes = await fetch(`${API_BASE_URL}/api/cli/deploy`, {
453
+ method: "POST",
454
+ headers: {
455
+ Authorization: `Bearer ${config.token}`,
456
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
457
+ "Content-Length": payload.length.toString()
458
+ },
459
+ body: payload
460
+ });
461
+
462
+ const rawText = await deployRes.text();
463
+ let result: any = {};
464
+ try {
465
+ result = JSON.parse(rawText);
466
+ } catch {
467
+ throw new Error(`Gateway returned unexpected response: ${rawText.slice(0, 100)}`);
468
+ }
469
+
470
+ if (deployRes.status === 429) {
471
+ console.error(`
472
+ Deployment In Progress / Rate Limited
473
+ ------------------------------------------------------------
474
+ Service: ${appName}
475
+ Workspace: ${targetWs.name}
476
+ Status: ${result.error || "A build is already in progress for this application."}
477
+
478
+ Troubleshooting:
479
+ - Stream live logs: npx vibed logs ${appName}
480
+ - Check status: npx vibed status ${appName}
481
+ - Format for AI debug: npx vibed logs ${appName} --ai
482
+ `);
483
+ process.exit(1);
484
+ }
485
+
486
+ if (!deployRes.ok || !result.success) {
487
+ console.error(`
488
+ Deployment Unsuccessful
489
+ ------------------------------------------------------------
490
+ Service: ${appName}
491
+ Workspace: ${targetWs.name}
492
+ Error: ${result.error || "Build failed. Please check your configuration."}
493
+
494
+ Troubleshooting:
495
+ - Stream logs: npx vibed logs ${appName}
496
+ - Format for AI debug: npx vibed logs ${appName} --ai
497
+ - Adjust port: npx vibed port set 3000
498
+ `);
499
+ process.exit(1);
500
+ }
501
+
502
+ // Save project configuration locally immediately
503
+ writeProjectConfig(
504
+ {
505
+ workspaceId: targetWs.id,
506
+ appName: result.appName || appName,
507
+ domain: result.domain,
508
+ lastDeployHash: currentZipHash
509
+ },
510
+ targetDir
511
+ );
512
+
513
+ console.log("Building application container in background...");
514
+
515
+ // Progressive status polling loop (up to 120 seconds)
516
+ const startTime = Date.now();
517
+ let isLive = result.status === "active";
518
+ let isFailed = false;
519
+ const finalAppName = result.appName || appName;
520
+
521
+ while (!isLive && !isFailed && Date.now() - startTime < 120000) {
522
+ await new Promise((r) => setTimeout(r, 2500));
523
+ const elapsedSec = Math.round((Date.now() - startTime) / 1000);
524
+ try {
525
+ const checkRes = await fetch(
526
+ `${API_BASE_URL}/api/cli/logs?appName=${encodeURIComponent(finalAppName)}`,
527
+ { headers: { Authorization: `Bearer ${config.token}` } }
528
+ );
529
+ const checkData = (await checkRes.json()) as any;
530
+
531
+ if (checkData.status === "active") {
532
+ isLive = true;
533
+ break;
534
+ } else if (checkData.status === "failed") {
535
+ isFailed = true;
536
+ break;
537
+ } else {
538
+ process.stdout.write(`\rBuilding application container in background... (${elapsedSec}s)`);
539
+ }
540
+ } catch {}
541
+ }
542
+
543
+ process.stdout.write("\r" + " ".repeat(65) + "\r");
544
+
545
+ if (isLive) {
546
+ console.log(`
547
+ Deployment Complete!
548
+ ------------------------------------------------------------
549
+ Service: ${finalAppName}
550
+ Workspace: ${targetWs.name}
551
+ Status: Active
552
+
553
+ Live URL: https://${result.domain}
554
+ ------------------------------------------------------------
555
+ `);
556
+ } else if (isFailed) {
557
+ console.error(`
558
+ Deployment Unsuccessful
559
+ ------------------------------------------------------------
560
+ Service: ${finalAppName}
561
+ Workspace: ${targetWs.name}
562
+ Status: Failed
563
+
564
+ Build unsuccessful.
565
+ Run 'npx vibed logs ${finalAppName}' to view container error logs.
566
+ ------------------------------------------------------------
567
+ `);
568
+ process.exit(1);
569
+ } else {
570
+ console.log(`
571
+ Deployment In Progress
572
+ ------------------------------------------------------------
573
+ Service: ${finalAppName}
574
+ Workspace: ${targetWs.name}
575
+ Status: Building
576
+
577
+ Your application is continuing to build in the background.
578
+ Run 'npx vibed status' to check status.
579
+ Run 'npx vibed logs' to view live build logs.
580
+ ------------------------------------------------------------
581
+ `);
582
+ }
583
+ } catch (err: any) {
584
+ console.error(`\nDeployment error: ${err.message}`);
585
+ process.exit(1);
586
+ }
587
+ }
588
+
589
+ async function handleStatus() {
590
+ const config = readGlobalConfig();
591
+ if (!config?.token) {
592
+ console.error("Not authenticated. Please run 'npx vibed login' first.");
593
+ process.exit(1);
594
+ }
595
+
596
+ const projectConfig = readProjectConfig();
597
+ const appName = args[1] && !args[1].startsWith("--") ? args[1] : (projectConfig?.appName || inferAppName(process.cwd()));
598
+
599
+ try {
600
+ // If an app name is provided or detected in this directory, show app status + telemetry
601
+ if (appName) {
602
+ const res = await fetch(`${API_BASE_URL}/api/cli/logs?appName=${encodeURIComponent(appName)}`, {
603
+ headers: { Authorization: `Bearer ${config.token}` }
604
+ });
605
+ const data = (await res.json()) as any;
606
+
607
+ if (!res.ok) {
608
+ console.error(`Error: ${data.error || "Failed to retrieve status."}`);
609
+ process.exit(1);
610
+ }
611
+
612
+ const currentStatus = data.status || "unknown";
613
+
614
+ if (currentStatus === "active") {
615
+ console.log(`
616
+ VibedHost Service Status
617
+ ------------------------------------------------------------
618
+ Service: ${appName}
619
+ Workspace: ${data.workspaceName || "Workspace"}
620
+ Status: Online & Active
621
+
622
+ Live URL: https://${data.domain || projectConfig?.domain}
623
+ ------------------------------------------------------------
624
+ `);
625
+ } else if (currentStatus === "failed") {
626
+ console.log(`
627
+ VibedHost Service Status
628
+ ------------------------------------------------------------
629
+ Service: ${appName}
630
+ Workspace: ${data.workspaceName || "Workspace"}
631
+ Status: Failed
632
+
633
+ Build unsuccessful.
634
+ Run 'npx vibed logs ${appName}' to view error logs.
635
+ ------------------------------------------------------------
636
+ `);
637
+ } else {
638
+ console.log(`
639
+ VibedHost Service Status
640
+ ------------------------------------------------------------
641
+ Service: ${appName}
642
+ Workspace: ${data.workspaceName || "Workspace"}
643
+ Status: Building
644
+
645
+ Building application container in background.
646
+ Run 'npx vibed logs ${appName}' to view build progress.
647
+ ------------------------------------------------------------
648
+ `);
649
+ }
650
+ return;
651
+ }
652
+
653
+ // If no app name specified, display workspace fleet health & resource telemetry
654
+ const wsRes = await fetch(`${API_BASE_URL}/api/cli/workspaces`, {
655
+ headers: { Authorization: `Bearer ${config.token}` }
656
+ });
657
+ const wsData = (await wsRes.json()) as any;
658
+
659
+ if (!wsRes.ok || !wsData.workspaces || wsData.workspaces.length === 0) {
660
+ console.log("\nNo active dedicated workspaces found. Launch one on the web console:\nhttps://www.vibedhost.com/dashboard/workspaces\n");
661
+ return;
662
+ }
663
+
664
+ console.log("\nVibedHost Workspace Health & Telemetry");
665
+ console.log("------------------------------------------------------------");
666
+ wsData.workspaces.forEach((ws: any, idx: number) => {
667
+ const ramStr = ws.ramTotalGB ? `${(ws.memoryUsageGB || 0.4).toFixed(1)} GB / ${ws.ramTotalGB} GB` : "Allocated";
668
+ const diskStr = ws.storageTotalGB ? `${(ws.storageUsageGB || 2.0).toFixed(1)} GB / ${ws.storageTotalGB} GB` : "NVMe";
669
+ const bwLimit = ws.bandwidthLimitGB > 0 ? `${(ws.bandwidthLimitGB / 1000).toFixed(0)} TB` : "Unmetered";
670
+ const bwStr = `${(ws.bandwidthUsedGB || 0).toFixed(1)} GB / ${bwLimit}`;
671
+
672
+ console.log(`Workspace: ${ws.name}`);
673
+ console.log(`Plan: ${ws.planName}`);
674
+ console.log(`Status: ${ws.status === 'running' ? 'Online & Active' : ws.status}`);
675
+ console.log(`CPU Load: ${ws.computeUsage || 4}%`);
676
+ console.log(`Memory: ${ramStr}`);
677
+ console.log(`NVMe Disk: ${diskStr}`);
678
+ console.log(`Bandwidth: ${bwStr}`);
679
+
680
+ if (ws.apps && ws.apps.length > 0) {
681
+ console.log(`\nActive Services (${ws.apps.length}):`);
682
+ ws.apps.forEach((a: any) => {
683
+ const urlStr = a.domain ? `https://${a.domain}` : (a.type === 'DATABASE' ? 'Internal Network' : 'Ready');
684
+ console.log(` • ${a.name} (${a.status}) ➔ ${urlStr}`);
685
+ });
686
+ }
687
+ if (idx < wsData.workspaces.length - 1) {
688
+ console.log("------------------------------------------------------------");
689
+ }
690
+ });
691
+ console.log("------------------------------------------------------------\n");
692
+ } catch (err: any) {
693
+ console.error(`Failed to check service status: ${err.message}`);
694
+ }
695
+ }
696
+
697
+ async function handleLogs() {
698
+ const config = readGlobalConfig();
699
+ if (!config?.token) {
700
+ console.error("Not authenticated. Please run 'npx vibed login' first.");
701
+ process.exit(1);
702
+ }
703
+
704
+ const isAiFormat = args.includes("--ai");
705
+ const filteredArgs = args.slice(1).filter((a) => a !== "--ai");
706
+ const projectConfig = readProjectConfig();
707
+ const appName = filteredArgs[0] || projectConfig?.appName || inferAppName(process.cwd());
708
+
709
+ if (!appName) {
710
+ console.error("Please specify application name: npx vibed logs <appName>");
711
+ process.exit(1);
712
+ }
713
+
714
+ try {
715
+ const res = await fetch(`${API_BASE_URL}/api/cli/logs?appName=${encodeURIComponent(appName)}`, {
716
+ headers: { Authorization: `Bearer ${config.token}` }
717
+ });
718
+ const data = (await res.json()) as any;
719
+
720
+ if (!res.ok) {
721
+ console.error(`Error: ${data.error || "Failed to retrieve logs."}`);
722
+ process.exit(1);
723
+ }
724
+
725
+ if (isAiFormat) {
726
+ console.log(`
727
+ ============================================================
728
+ AI ASSISTANT DEBUG PROMPT (COPY AND PASTE TO CLAUDE / CURSOR)
729
+ ============================================================
730
+ I am deploying application "${appName}" on VibedHost PaaS.
731
+ The service encountered an error. Here are the container logs:
732
+
733
+ -------------------- BEGIN LOG STREAM --------------------
734
+ ${(data.logs || "").slice(-1500)}
735
+ --------------------- END LOG STREAM ---------------------
736
+
737
+ Please analyze these logs and tell me how to resolve the error.
738
+ ============================================================
739
+ `);
740
+ } else {
741
+ console.log(`\nLog stream for '${appName}' (${data.workspaceName || "Workspace"}):`);
742
+ console.log("------------------------------------------------------------");
743
+ console.log(data.logs || "No logs returned.");
744
+ console.log("------------------------------------------------------------\n");
745
+ }
746
+ } catch (err: any) {
747
+ console.error(`Failed to stream logs: ${err.message}`);
748
+ }
749
+ }
750
+
751
+ async function handleEnv() {
752
+ const config = readGlobalConfig();
753
+ if (!config?.token) {
754
+ console.error("Not authenticated. Run 'npx vibed login'.");
755
+ process.exit(1);
756
+ }
757
+
758
+ const subCommand = args[1];
759
+ const projectConfig = readProjectConfig();
760
+ const appName = projectConfig?.appName || inferAppName(process.cwd());
761
+
762
+ if (!appName) {
763
+ console.error("No linked project found in this folder. Run inside a project directory or run 'npx vibed deploy' first.");
764
+ process.exit(1);
765
+ }
766
+
767
+ if (subCommand === "set") {
768
+ const keyVal = args[2];
769
+ if (!keyVal || !keyVal.includes("=")) {
770
+ console.error("Usage: npx vibed env set KEY=VALUE");
771
+ process.exit(1);
772
+ }
773
+ const idx = keyVal.indexOf("=");
774
+ const key = keyVal.substring(0, idx).trim();
775
+ const value = keyVal.substring(idx + 1).trim();
776
+
777
+ try {
778
+ const postRes = await fetch(`${API_BASE_URL}/api/cli/env`, {
779
+ method: "POST",
780
+ headers: {
781
+ Authorization: `Bearer ${config.token}`,
782
+ "Content-Type": "application/json"
783
+ },
784
+ body: JSON.stringify({
785
+ appName,
786
+ key,
787
+ value
788
+ })
789
+ });
790
+
791
+ if (postRes.ok) {
792
+ console.log(`Environment variable ${key} updated. Service restarted.`);
793
+ } else {
794
+ console.error("Failed to update environment variable.");
795
+ }
796
+ } catch (err: any) {
797
+ console.error(`Connection error: ${err.message}`);
798
+ }
799
+ } else {
800
+ // List vars
801
+ try {
802
+ const res = await fetch(`${API_BASE_URL}/api/cli/env?appName=${encodeURIComponent(appName)}`, {
803
+ headers: { Authorization: `Bearer ${config.token}` }
804
+ });
805
+ const data = (await res.json()) as any;
806
+ if (res.ok) {
807
+ console.log(`\nEnvironment Variables for '${appName}':`);
808
+ console.log("------------------------------------------------------------");
809
+ (data.envVars || []).forEach((v: any) => {
810
+ const masked = v.value.length > 8 ? `${v.value.slice(0, 4)}••••${v.value.slice(-3)}` : "••••";
811
+ console.log(` ${v.key}=${masked}`);
812
+ });
813
+ console.log(` Internal Port: ${data.port || 80}`);
814
+ console.log("------------------------------------------------------------\n");
815
+ }
816
+ } catch (err: any) {
817
+ console.error(`Error: ${err.message}`);
818
+ }
819
+ }
820
+ }
821
+
822
+ async function handlePort() {
823
+ const config = readGlobalConfig();
824
+ if (!config?.token) {
825
+ console.error("Not authenticated. Run 'npx vibed login'.");
826
+ process.exit(1);
827
+ }
828
+
829
+ const subCommand = args[1];
830
+ const portVal = parseInt(args[2], 10);
831
+ const projectConfig = readProjectConfig();
832
+ const appName = projectConfig?.appName || inferAppName(process.cwd());
833
+
834
+ if (!appName) {
835
+ console.error("No linked project found in this folder.");
836
+ process.exit(1);
837
+ }
838
+
839
+ if (subCommand !== "set" || isNaN(portVal) || portVal < 1 || portVal > 65535) {
840
+ console.error("Usage: npx vibed port set <1-65535>");
841
+ process.exit(1);
842
+ }
843
+
844
+ try {
845
+ const res = await fetch(`${API_BASE_URL}/api/cli/env`, {
846
+ method: "POST",
847
+ headers: {
848
+ Authorization: `Bearer ${config.token}`,
849
+ "Content-Type": "application/json"
850
+ },
851
+ body: JSON.stringify({
852
+ appName,
853
+ port: portVal
854
+ })
855
+ });
856
+
857
+ if (res.ok) {
858
+ console.log(`Routing port for '${appName}' updated to ${portVal}. Network routes synchronized.`);
859
+ } else {
860
+ console.error("Failed to update container port.");
861
+ }
862
+ } catch (err: any) {
863
+ console.error(`Connection error: ${err.message}`);
864
+ }
865
+ }
866
+
867
+ async function handleDb() {
868
+ const config = readGlobalConfig();
869
+ if (!config?.token) {
870
+ console.error("Not authenticated. Please run 'npx vibed login' first.");
871
+ process.exit(1);
872
+ }
873
+
874
+ const subCommand = args[1] || "list";
875
+ const projectConfig = readProjectConfig();
876
+
877
+ // 1. DATABASE LIST
878
+ if (subCommand === "list") {
879
+ try {
880
+ const res = await fetch(`${API_BASE_URL}/api/cli/db`, {
881
+ headers: { Authorization: `Bearer ${config.token}` }
882
+ });
883
+ const data = (await res.json()) as any;
884
+
885
+ if (!res.ok) {
886
+ console.error(`Error: ${data.error || "Failed to fetch databases."}`);
887
+ process.exit(1);
888
+ }
889
+
890
+ const isReveal = args.includes("--reveal");
891
+ console.log("\nManaged Databases:");
892
+ console.log("------------------------------------------------------------");
893
+
894
+ if (!data.databases || data.databases.length === 0) {
895
+ console.log("No databases provisioned yet.");
896
+ console.log("Run 'npx vibed db create <name>' to launch a database.\n");
897
+ return;
898
+ }
899
+
900
+ data.databases.forEach((d: any, idx: number) => {
901
+ console.log(`[${idx + 1}] ${d.name} (${d.engine}) • Workspace: ${d.workspaceName}`);
902
+ console.log(` Status: ${d.status}`);
903
+ console.log(` URI: ${isReveal ? d.internalUri : d.maskedUri}`);
904
+ });
905
+ console.log("------------------------------------------------------------\n");
906
+ } catch (err: any) {
907
+ console.error(`Connection error: ${err.message}`);
908
+ }
909
+ return;
910
+ }
911
+
912
+ // 2. DATABASE CREATE
913
+ if (subCommand === "create") {
914
+ let dbName = args[2] && !args[2].startsWith("--") ? args[2] : "";
915
+ const engineIndex = args.indexOf("--engine");
916
+ let explicitEngine = engineIndex !== -1 ? args[engineIndex + 1] : "";
917
+
918
+ if (!dbName && process.stdin.isTTY) {
919
+ dbName = await askQuestion("Database Service Name", "my-postgres");
920
+ } else if (!dbName) {
921
+ dbName = "my-postgres";
922
+ }
923
+
924
+ if (!explicitEngine && process.stdin.isTTY) {
925
+ console.log("\nSelect Database Engine:");
926
+ console.log(" [1] PostgreSQL 15 (Default)");
927
+ console.log(" [2] Supabase Postgres");
928
+ console.log(" [3] MySQL 8.0");
929
+ console.log(" [4] Redis 7 (In-Memory)");
930
+ console.log(" [5] MongoDB 6");
931
+ const chosen = await askQuestion("Enter number", "1");
932
+ const map: Record<string, string> = {
933
+ "1": "POSTGRES",
934
+ "2": "SUPABASE_POSTGRES",
935
+ "3": "MYSQL",
936
+ "4": "REDIS",
937
+ "5": "MONGODB"
938
+ };
939
+ explicitEngine = map[chosen] || "POSTGRES";
940
+ }
941
+
942
+ const cleanEngine = (explicitEngine || "POSTGRES").toUpperCase();
943
+ console.log(`\nProvisioning ${cleanEngine} database '${dbName}' on dedicated NVMe storage...`);
944
+
945
+ try {
946
+ const res = await fetch(`${API_BASE_URL}/api/cli/db`, {
947
+ method: "POST",
948
+ headers: {
949
+ Authorization: `Bearer ${config.token}`,
950
+ "Content-Type": "application/json"
951
+ },
952
+ body: JSON.stringify({
953
+ action: "CREATE_DB",
954
+ workspaceId: projectConfig?.workspaceId,
955
+ name: dbName,
956
+ engine: cleanEngine
957
+ })
958
+ });
959
+
960
+ const data = (await res.json()) as any;
961
+
962
+ if (!res.ok || !data.success) {
963
+ console.error(`Database launch error: ${data.error || "Failed to create database."}`);
964
+ process.exit(1);
965
+ }
966
+
967
+ console.log(`\nDatabase Ready!`);
968
+ console.log("------------------------------------------------------------");
969
+ console.log(`Name: ${data.name}`);
970
+ console.log(`Engine: ${data.engine}`);
971
+ console.log(`Workspace: ${data.workspaceName}`);
972
+ console.log(`URI: ${data.internalUri}`);
973
+ console.log("------------------------------------------------------------");
974
+ console.log(`To link to your app, run: npx vibed db link ${data.name}\n`);
975
+ } catch (err: any) {
976
+ console.error(`Connection error: ${err.message}`);
977
+ }
978
+ return;
979
+ }
980
+
981
+ // 3. DATABASE LINK
982
+ if (subCommand === "link") {
983
+ const dbTargetName = args[2] && !args[2].startsWith("--") ? args[2] : "";
984
+ const targetAppName = projectConfig?.appName || inferAppName(process.cwd());
985
+
986
+ if (!dbTargetName) {
987
+ console.error("Usage: npx vibed db link <database-name>");
988
+ process.exit(1);
989
+ }
990
+
991
+ console.log(`Linking database '${dbTargetName}' to application '${targetAppName}'...`);
992
+
993
+ try {
994
+ const res = await fetch(`${API_BASE_URL}/api/cli/db`, {
995
+ method: "POST",
996
+ headers: {
997
+ Authorization: `Bearer ${config.token}`,
998
+ "Content-Type": "application/json"
999
+ },
1000
+ body: JSON.stringify({
1001
+ action: "LINK_DB",
1002
+ appName: targetAppName,
1003
+ dbName: dbTargetName
1004
+ })
1005
+ });
1006
+
1007
+ const data = (await res.json()) as any;
1008
+
1009
+ if (!res.ok || !data.success) {
1010
+ console.error(`Link error: ${data.error || "Failed to link database."}`);
1011
+ process.exit(1);
1012
+ }
1013
+
1014
+ console.log(`\nSuccessfully linked '${data.dbName}' to '${data.appName}'!`);
1015
+ console.log(`Injected ${data.keyUsed} into environment variables. Container restarted.\n`);
1016
+ } catch (err: any) {
1017
+ console.error(`Connection error: ${err.message}`);
1018
+ }
1019
+ return;
1020
+ }
1021
+
1022
+ console.log("Usage: npx vibed db [create|list|link]");
1023
+ }
1024
+
1025
+ main().catch((err) => {
1026
+ console.error(`Execution error: ${err.message}`);
1027
+ process.exit(1);
1028
+ });