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