rushdeploy 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/bin/index.js +1023 -0
- package/package.json +20 -0
package/bin/index.js
ADDED
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* RushDeploy CLI
|
|
4
|
+
* A zero-dependency Node.js CLI utility to manage your RushDeploy account.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const http = require('http');
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const { exec } = require('child_process');
|
|
13
|
+
|
|
14
|
+
const CONFIG_DIR = path.join(os.homedir(), '.rushdeploy');
|
|
15
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
16
|
+
|
|
17
|
+
// Domain Hardcoding: Always default to rushdeploy.com
|
|
18
|
+
const DEFAULT_SERVER = 'https://rushdeploy.com';
|
|
19
|
+
|
|
20
|
+
function getServerUrl() {
|
|
21
|
+
if (process.env.RUSHDEPLOY_SERVER) {
|
|
22
|
+
return process.env.RUSHDEPLOY_SERVER;
|
|
23
|
+
}
|
|
24
|
+
const config = loadConfig();
|
|
25
|
+
if (config && config.server) {
|
|
26
|
+
return config.server;
|
|
27
|
+
}
|
|
28
|
+
return DEFAULT_SERVER;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ANSI terminal color helpers
|
|
32
|
+
const colors = {
|
|
33
|
+
reset: '\x1b[0m',
|
|
34
|
+
bold: '\x1b[1m',
|
|
35
|
+
green: '\x1b[32m',
|
|
36
|
+
red: '\x1b[31m',
|
|
37
|
+
cyan: '\x1b[36m',
|
|
38
|
+
yellow: '\x1b[33m',
|
|
39
|
+
blue: '\x1b[34m',
|
|
40
|
+
magenta: '\x1b[35m'
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function loadConfig() {
|
|
44
|
+
if (!fs.existsSync(CONFIG_FILE)) return null;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function saveConfig(server, token) {
|
|
53
|
+
try {
|
|
54
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
55
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
const cleanServer = (server || DEFAULT_SERVER).replace(/\/$/, '');
|
|
58
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ server: cleanServer, token }, null, 2), 'utf8');
|
|
59
|
+
return true;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(`${colors.red}Error saving configuration:${colors.reset}`, err.message);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeRequest(apiPath, method = 'GET', data = null) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const config = loadConfig();
|
|
69
|
+
const isLoginPath = apiPath.endsWith('/auth/me') && method === 'GET';
|
|
70
|
+
const isPlansPath = apiPath.endsWith('/subscriptions/plans') && method === 'GET';
|
|
71
|
+
|
|
72
|
+
if (!config && !isLoginPath && !isPlansPath) {
|
|
73
|
+
console.error(`${colors.red}Error: You are not logged in. Run 'rushdeploy login <token>' first.${colors.reset}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const serverUrl = getServerUrl();
|
|
78
|
+
const token = config ? config.token : null;
|
|
79
|
+
|
|
80
|
+
const parsedUrl = new URL(serverUrl);
|
|
81
|
+
const options = {
|
|
82
|
+
hostname: parsedUrl.hostname,
|
|
83
|
+
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
84
|
+
path: `/api/v1${apiPath}`,
|
|
85
|
+
method: method,
|
|
86
|
+
headers: {
|
|
87
|
+
'Accept': 'application/json',
|
|
88
|
+
'Content-Type': 'application/json'
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (token) {
|
|
93
|
+
options.headers['Authorization'] = `Bearer ${token}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const client = parsedUrl.protocol === 'https:' ? https : http;
|
|
97
|
+
|
|
98
|
+
const req = client.request(options, (res) => {
|
|
99
|
+
let body = '';
|
|
100
|
+
res.on('data', (chunk) => body += chunk);
|
|
101
|
+
res.on('end', () => {
|
|
102
|
+
let jsonResponse;
|
|
103
|
+
try {
|
|
104
|
+
jsonResponse = JSON.parse(body);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
jsonResponse = body;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
110
|
+
resolve(jsonResponse);
|
|
111
|
+
} else {
|
|
112
|
+
let errorMsg = res.statusMessage || 'Request failed';
|
|
113
|
+
if (jsonResponse && jsonResponse.error) {
|
|
114
|
+
errorMsg = jsonResponse.error.message || errorMsg;
|
|
115
|
+
} else if (jsonResponse && jsonResponse.detail) {
|
|
116
|
+
errorMsg = typeof jsonResponse.detail === 'object' ? jsonResponse.detail.message : jsonResponse.detail;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (res.statusCode === 401) {
|
|
120
|
+
console.error(`${colors.red}Error (401): Unauthorized. Your token may have expired or been revoked.${colors.reset}`);
|
|
121
|
+
console.error('Please log in again using `rushdeploy login <token>`');
|
|
122
|
+
} else {
|
|
123
|
+
console.error(`${colors.red}Error (${res.statusCode}): ${errorMsg}${colors.reset}`);
|
|
124
|
+
}
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
req.on('error', (err) => {
|
|
131
|
+
console.error(`${colors.red}Connection Error: Unable to connect to server at ${serverUrl}.${colors.reset}`);
|
|
132
|
+
console.error('Details:', err.message);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (data) {
|
|
137
|
+
req.write(JSON.stringify(data));
|
|
138
|
+
}
|
|
139
|
+
req.end();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatBytes(bytes) {
|
|
144
|
+
if (bytes === undefined || bytes === null || isNaN(bytes)) return '0 B';
|
|
145
|
+
if (bytes === 0) return '0 B';
|
|
146
|
+
const k = 1024;
|
|
147
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
148
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
149
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatDate(isoStr) {
|
|
153
|
+
if (!isoStr) return 'Never';
|
|
154
|
+
try {
|
|
155
|
+
const d = new Date(isoStr);
|
|
156
|
+
return d.toLocaleString();
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return isoStr;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function openBrowser(url) {
|
|
163
|
+
let command;
|
|
164
|
+
switch (process.platform) {
|
|
165
|
+
case 'darwin':
|
|
166
|
+
command = `open "${url}"`;
|
|
167
|
+
break;
|
|
168
|
+
case 'win32':
|
|
169
|
+
command = `start "" "${url}"`;
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
command = `xdg-open "${url}"`;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
exec(command, (err) => {
|
|
176
|
+
if (err) {
|
|
177
|
+
console.error(`${colors.red}Failed to open browser:${colors.reset}`, err.message);
|
|
178
|
+
} else {
|
|
179
|
+
console.log(`${colors.green}Opening browser to: ${url}${colors.reset}`);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function extractRepoName(url) {
|
|
185
|
+
try {
|
|
186
|
+
const cleanUrl = url.replace(/\/$/, '').replace(/\.git$/, '');
|
|
187
|
+
const parts = cleanUrl.split('/');
|
|
188
|
+
return parts[parts.length - 1] || 'my-app';
|
|
189
|
+
} catch (e) {
|
|
190
|
+
return 'my-app';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function cmdLogin(token, serverArg) {
|
|
195
|
+
const server = serverArg || 'https://rushdeploy.com';
|
|
196
|
+
console.log(`${colors.cyan}Connecting to ${server}...${colors.reset}`);
|
|
197
|
+
|
|
198
|
+
saveConfig(server, token);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const res = await makeRequest('/auth/me');
|
|
202
|
+
if (res.success) {
|
|
203
|
+
const user = res.user;
|
|
204
|
+
console.log(`\n${colors.green}${colors.bold}✓ Login successful!${colors.reset}`);
|
|
205
|
+
console.log(`Account: ${colors.bold}${user.name} (${user.email})${colors.reset}`);
|
|
206
|
+
console.log(`Role: ${user.role.toUpperCase()}`);
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
|
|
210
|
+
console.error(`${colors.red}Authentication failed. Check your token or server URL.${colors.reset}`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function cmdLogout() {
|
|
216
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
217
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
218
|
+
console.log(`${colors.green}Successfully logged out. Local token configuration cleared.${colors.reset}`);
|
|
219
|
+
} else {
|
|
220
|
+
console.log('You are already logged out.');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function cmdWhoami() {
|
|
225
|
+
const config = loadConfig();
|
|
226
|
+
if (!config) {
|
|
227
|
+
console.log('Not logged in.');
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const res = await makeRequest('/auth/me');
|
|
232
|
+
const user = res.user;
|
|
233
|
+
console.log(`\n${colors.bold}Profile Details:${colors.reset}`);
|
|
234
|
+
console.log(` Name: ${colors.bold}${user.name}${colors.reset}`);
|
|
235
|
+
console.log(` Email: ${user.email}`);
|
|
236
|
+
console.log(` Role: ${user.role.toUpperCase()}`);
|
|
237
|
+
console.log(` Server: ${SERVER_URL}`);
|
|
238
|
+
console.log();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function cmdPlans() {
|
|
242
|
+
const res = await makeRequest('/subscriptions/plans');
|
|
243
|
+
const plans = res.plans || [];
|
|
244
|
+
|
|
245
|
+
console.log(`\n${colors.cyan}${colors.bold}====================================================${colors.reset}`);
|
|
246
|
+
console.log(`${colors.bold} RUSHDEPLOY HOSTING PLANS ${colors.reset}`);
|
|
247
|
+
console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}`);
|
|
248
|
+
|
|
249
|
+
plans.forEach(p => {
|
|
250
|
+
const price = p.base_price_monthly ? `$${p.base_price_monthly}/mo` : 'Free';
|
|
251
|
+
console.log(`${colors.bold}${p.name.toUpperCase()}${colors.reset} - ${colors.green}${price}${colors.reset}`);
|
|
252
|
+
console.log(`Description: ${p.description || 'No description'}`);
|
|
253
|
+
if (p.limits) {
|
|
254
|
+
console.log(`Limits:`);
|
|
255
|
+
console.log(` - Projects: ${p.limits.max_projects || 'Unlimited'}`);
|
|
256
|
+
console.log(` - Deployments: ${p.limits.max_deployments || 'Unlimited'}`);
|
|
257
|
+
console.log(` - Bandwidth Limit: ${p.limits.max_bandwidth_bytes ? formatBytes(p.limits.max_bandwidth_bytes) : 'Unlimited'}`);
|
|
258
|
+
console.log(` - Build Minutes: ${p.limits.max_build_minutes || 'Unlimited'} mins`);
|
|
259
|
+
}
|
|
260
|
+
console.log(`----------------------------------------------------`);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function stripAnsi(str) {
|
|
265
|
+
return String(str).replace(/\x1b\[[0-9;]*m/g, '');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function drawCards(cards, cardsPerRow = 2) {
|
|
269
|
+
const cardWidth = 34;
|
|
270
|
+
const rows = [];
|
|
271
|
+
|
|
272
|
+
for (let i = 0; i < cards.length; i += cardsPerRow) {
|
|
273
|
+
rows.push(cards.slice(i, i + cardsPerRow));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
rows.forEach(row => {
|
|
277
|
+
let topBorder = '';
|
|
278
|
+
let labelValLine = '';
|
|
279
|
+
let sepLine = '';
|
|
280
|
+
let hintLine = '';
|
|
281
|
+
let bottomBorder = '';
|
|
282
|
+
|
|
283
|
+
row.forEach((card, idx) => {
|
|
284
|
+
const space = idx > 0 ? ' ' : ''; // Gap between cards
|
|
285
|
+
|
|
286
|
+
topBorder += space + '┌' + '─'.repeat(cardWidth - 2) + '┐';
|
|
287
|
+
|
|
288
|
+
const label = card.label || '';
|
|
289
|
+
let value = card.value !== undefined ? String(card.value) : '';
|
|
290
|
+
|
|
291
|
+
let valColor = colors.bold;
|
|
292
|
+
if (card.status === 'online') valColor = colors.green + colors.bold;
|
|
293
|
+
else if (card.status === 'offline') valColor = colors.red + colors.bold;
|
|
294
|
+
else if (card.status === 'warn') valColor = colors.yellow + colors.bold;
|
|
295
|
+
|
|
296
|
+
const valStr = `${valColor}${value}${colors.reset}`;
|
|
297
|
+
const rawVal = stripAnsi(value);
|
|
298
|
+
|
|
299
|
+
const textLen = label.length + rawVal.length;
|
|
300
|
+
const padLen = cardWidth - 6 - textLen; // accounts for borders and padding spaces
|
|
301
|
+
const pad = ' '.repeat(Math.max(1, padLen));
|
|
302
|
+
|
|
303
|
+
labelValLine += space + `│ ${colors.cyan}${label}${colors.reset}${pad}${valStr} │`;
|
|
304
|
+
sepLine += space + '├' + '─'.repeat(cardWidth - 2) + '┤';
|
|
305
|
+
|
|
306
|
+
const hint = card.hint || '';
|
|
307
|
+
const rawHint = stripAnsi(hint);
|
|
308
|
+
const hintPadLen = cardWidth - 6 - rawHint.length;
|
|
309
|
+
const hintPad = ' '.repeat(Math.max(0, hintPadLen));
|
|
310
|
+
|
|
311
|
+
hintLine += space + `│ ${hint}${hintPad} │`;
|
|
312
|
+
bottomBorder += space + '└' + '─'.repeat(cardWidth - 2) + '┘';
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
console.log(topBorder);
|
|
316
|
+
console.log(labelValLine);
|
|
317
|
+
console.log(sepLine);
|
|
318
|
+
console.log(hintLine);
|
|
319
|
+
console.log(bottomBorder);
|
|
320
|
+
console.log();
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function cmdStatus() {
|
|
325
|
+
const userRes = await makeRequest('/auth/me');
|
|
326
|
+
const user = userRes.user;
|
|
327
|
+
|
|
328
|
+
let workspaceRes = null;
|
|
329
|
+
try {
|
|
330
|
+
workspaceRes = await makeRequest('/workspaces/current');
|
|
331
|
+
} catch (err) {
|
|
332
|
+
// Ignore / fallback
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (user.role === 'super_admin') {
|
|
336
|
+
try {
|
|
337
|
+
console.log(`${colors.cyan}Fetching system metrics (Admin overview)...${colors.reset}`);
|
|
338
|
+
const [overviewRes, serverRes] = await Promise.all([
|
|
339
|
+
makeRequest('/admin/overview').catch(() => null),
|
|
340
|
+
makeRequest('/admin/server').catch(() => null)
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
if (overviewRes && overviewRes.success && serverRes && serverRes.success) {
|
|
344
|
+
const stats = overviewRes.stats || {};
|
|
345
|
+
const server = serverRes.server || {};
|
|
346
|
+
const byStatus = stats.projects_by_status || {};
|
|
347
|
+
const celery = stats.celery || {};
|
|
348
|
+
const workerOnline = (celery.workers ?? 0) > 0;
|
|
349
|
+
|
|
350
|
+
const activeBuildingProjects = (byStatus.building || byStatus.BUILDING || 0) + (byStatus.pending || byStatus.PENDING || 0);
|
|
351
|
+
const activeTasksCount = (celery.active && celery.active > 0) ? celery.active : activeBuildingProjects;
|
|
352
|
+
const pendingInQueue = stats.redis_queue_len ?? celery.queued ?? 0;
|
|
353
|
+
|
|
354
|
+
const cpuVal = server.cpu_percent ? `${Number(server.cpu_percent).toFixed(1)}%` : 'Normal';
|
|
355
|
+
const ramVal = server.memory ? `${Number(server.memory.used_percent).toFixed(1)}%` : 'Healthy';
|
|
356
|
+
const diskVal = server.disk ? `${Number(server.disk.used_percent).toFixed(1)}%` : 'Optimal';
|
|
357
|
+
|
|
358
|
+
const runCount = byStatus.running || byStatus.RUNNING || 0;
|
|
359
|
+
const buildCount = activeBuildingProjects;
|
|
360
|
+
const failCount = byStatus.failed || byStatus.FAILED || 0;
|
|
361
|
+
|
|
362
|
+
console.log(`\n${colors.cyan}${colors.bold}========================================================================${colors.reset}`);
|
|
363
|
+
console.log(`${colors.bold} RUSHDEPLOY SYSTEM STATUS (ADMIN) ${colors.reset}`);
|
|
364
|
+
console.log(`${colors.cyan}${colors.bold}========================================================================${colors.reset}\n`);
|
|
365
|
+
|
|
366
|
+
const adminCards = [
|
|
367
|
+
{ label: 'Users', value: stats.users ?? 0, hint: 'Total registered users' },
|
|
368
|
+
{ label: 'Databases', value: stats.databases ?? 0, hint: 'Total workspace databases' },
|
|
369
|
+
{
|
|
370
|
+
label: 'Projects',
|
|
371
|
+
value: stats.projects ?? 0,
|
|
372
|
+
hint: `${colors.green}${runCount} run${colors.reset} · ${colors.yellow}${buildCount} build${colors.reset} · ${colors.red}${failCount} fail${colors.reset}`
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
label: 'App Containers',
|
|
376
|
+
value: stats.running_containers ?? 0,
|
|
377
|
+
status: stats.docker_available ? 'online' : 'offline',
|
|
378
|
+
hint: stats.docker_available ? 'Active running containers' : 'Engine offline'
|
|
379
|
+
},
|
|
380
|
+
{ label: 'CPU Usage', value: cpuVal, hint: 'Host CPU load' },
|
|
381
|
+
{ label: 'RAM Usage', value: ramVal, hint: 'Host memory allocation' },
|
|
382
|
+
{ label: 'Disk Usage', value: diskVal, hint: 'Host storage capacity' },
|
|
383
|
+
{
|
|
384
|
+
label: 'Task Queue',
|
|
385
|
+
value: workerOnline ? 'Online' : 'Offline',
|
|
386
|
+
status: workerOnline ? 'online' : 'offline',
|
|
387
|
+
hint: `${activeTasksCount} active · ${pendingInQueue} pending`
|
|
388
|
+
}
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
drawCards(adminCards, 2);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
// Fall through to workspace status
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Fallback / Standard workspace view
|
|
400
|
+
if (workspaceRes && workspaceRes.success) {
|
|
401
|
+
const workspace = workspaceRes.workspace;
|
|
402
|
+
const usage = workspaceRes.usage;
|
|
403
|
+
const limits = workspaceRes.limits;
|
|
404
|
+
|
|
405
|
+
const projLimit = limits.max_projects || 'Unlimited';
|
|
406
|
+
const bwLimit = limits.max_bandwidth_bytes ? formatBytes(limits.max_bandwidth_bytes) : 'Unlimited';
|
|
407
|
+
const buildLimit = limits.max_build_minutes ? `${limits.max_build_minutes} mins` : 'Unlimited';
|
|
408
|
+
|
|
409
|
+
console.log(`\n${colors.cyan}${colors.bold}========================================================================${colors.reset}`);
|
|
410
|
+
console.log(`${colors.bold} RUSHDEPLOY ACCOUNT STATUS (WORKSPACE) ${colors.reset}`);
|
|
411
|
+
console.log(`${colors.cyan}${colors.bold}========================================================================${colors.reset}\n`);
|
|
412
|
+
|
|
413
|
+
const workspaceCards = [
|
|
414
|
+
{ label: 'Workspace', value: workspace.name, hint: `Tier Plan: ${workspace.tier.toUpperCase()}` },
|
|
415
|
+
{ label: 'Projects Quota', value: `${usage.projects_count} / ${projLimit}`, hint: 'Active project slots' },
|
|
416
|
+
{ label: 'Bandwidth Limit', value: `${formatBytes(usage.bandwidth_bytes)} / ${bwLimit}`, hint: 'Monthly data usage' },
|
|
417
|
+
{ label: 'Build Duration', value: `${(usage.build_seconds / 60).toFixed(1)}m / ${buildLimit}`, hint: 'Monthly build duration' }
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
drawCards(workspaceCards, 2);
|
|
421
|
+
} else {
|
|
422
|
+
console.error(`${colors.red}Error: Could not retrieve status details.${colors.reset}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function cmdProjects() {
|
|
427
|
+
const res = await makeRequest('/projects/');
|
|
428
|
+
const projects = res.projects || [];
|
|
429
|
+
|
|
430
|
+
if (projects.length === 0) {
|
|
431
|
+
console.log('No projects found in active workspace.');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log(`\n${colors.bold}${'ID'.padEnd(6)} | ${'PROJECT NAME'.padEnd(20)} | ${'STATUS'.padEnd(10)} | ${'PUBLIC URL'}${colors.reset}`);
|
|
436
|
+
console.log('-'.repeat(80));
|
|
437
|
+
projects.forEach((p) => {
|
|
438
|
+
let statusColor = colors.reset;
|
|
439
|
+
if (p.status === 'running') statusColor = colors.green;
|
|
440
|
+
if (p.status === 'failed') statusColor = colors.red;
|
|
441
|
+
if (p.status === 'pending') statusColor = colors.yellow;
|
|
442
|
+
|
|
443
|
+
console.log(`${String(p.id).padEnd(6)} | ${p.name.substring(0, 20).padEnd(20)} | ${statusColor}${p.status.toUpperCase().padEnd(10)}${colors.reset} | ${p.url}`);
|
|
444
|
+
});
|
|
445
|
+
console.log();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function cmdRepos() {
|
|
449
|
+
try {
|
|
450
|
+
const res = await makeRequest('/auth/github/repos');
|
|
451
|
+
const repos = res.repos || [];
|
|
452
|
+
|
|
453
|
+
if (repos.length === 0) {
|
|
454
|
+
console.log('No GitHub repositories found. Try connecting to GitHub first.');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
console.log(`\n${colors.bold}${'REPOSITORY NAME'.padEnd(35)} | ${'DEFAULT BRANCH'.padEnd(16)} | ${'VISIBILITY'}${colors.reset}`);
|
|
459
|
+
console.log('-'.repeat(70));
|
|
460
|
+
repos.forEach((r) => {
|
|
461
|
+
const vis = r.private ? `${colors.yellow}PRIVATE${colors.reset}` : `${colors.green}PUBLIC${colors.reset}`;
|
|
462
|
+
console.log(`${r.full_name.substring(0, 35).padEnd(35)} | ${String(r.default_branch || 'main').padEnd(16)} | ${vis}`);
|
|
463
|
+
});
|
|
464
|
+
console.log();
|
|
465
|
+
} catch (err) {
|
|
466
|
+
// If user has not connected GitHub OAuth in settings yet
|
|
467
|
+
console.error(`${colors.yellow}Notice: Please connect your GitHub account via Settings in the Web Dashboard first.${colors.reset}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function cmdDeploy(type, args) {
|
|
472
|
+
if (type === 'git') {
|
|
473
|
+
// Deploy from URI: deploy git <repo_url> [--branch <branch>] [--name <name>] [--type <type>]
|
|
474
|
+
const repoUrl = args[0];
|
|
475
|
+
if (!repoUrl) {
|
|
476
|
+
console.error(`${colors.red}Error: Please specify the Git repository URL.${colors.reset}`);
|
|
477
|
+
console.log('Usage: rushdeploy deploy git <repo_url> [--branch <branch>] [--name <name>] [--type <type>]');
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let branch = 'main';
|
|
482
|
+
let name = extractRepoName(repoUrl);
|
|
483
|
+
let ptype = 'auto';
|
|
484
|
+
|
|
485
|
+
const branchIdx = args.indexOf('--branch');
|
|
486
|
+
if (branchIdx !== -1 && args[branchIdx + 1]) branch = args[branchIdx + 1];
|
|
487
|
+
|
|
488
|
+
const nameIdx = args.indexOf('--name');
|
|
489
|
+
if (nameIdx !== -1 && args[nameIdx + 1]) name = args[nameIdx + 1];
|
|
490
|
+
|
|
491
|
+
const typeIdx = args.indexOf('--type');
|
|
492
|
+
if (typeIdx !== -1 && args[typeIdx + 1]) ptype = args[typeIdx + 1];
|
|
493
|
+
|
|
494
|
+
console.log(`${colors.cyan}Triggering deployment for Git repo: ${repoUrl} (${branch})...${colors.reset}`);
|
|
495
|
+
const res = await makeRequest('/projects/deploy', 'POST', {
|
|
496
|
+
name,
|
|
497
|
+
repo_url: repoUrl,
|
|
498
|
+
branch,
|
|
499
|
+
type: ptype
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
if (res.success) {
|
|
503
|
+
console.log(`\n${colors.green}${colors.bold}✓ Deployment queued successfully!${colors.reset}`);
|
|
504
|
+
console.log(`Project ID: ${res.project_id}`);
|
|
505
|
+
console.log(`Deployment ID: ${res.deployment_id}`);
|
|
506
|
+
console.log(`Public URL: ${colors.bold}${res.url}${colors.reset}\n`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
} else if (type === 'repo') {
|
|
510
|
+
// Deploy from Github Account: deploy repo <owner/repo> [--branch <branch>] [--name <name>] [--type <type>]
|
|
511
|
+
const ownerRepo = args[0];
|
|
512
|
+
if (!ownerRepo || !ownerRepo.includes('/')) {
|
|
513
|
+
console.error(`${colors.red}Error: Please specify the repository in 'owner/repo' format.${colors.reset}`);
|
|
514
|
+
console.log('Usage: rushdeploy deploy repo <owner/repo> [--branch <branch>] [--name <name>] [--type <type>]');
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const repoUrl = `https://github.com/${ownerRepo}.git`;
|
|
519
|
+
let branch = 'main';
|
|
520
|
+
let name = ownerRepo.split('/')[1];
|
|
521
|
+
let ptype = 'auto';
|
|
522
|
+
|
|
523
|
+
const branchIdx = args.indexOf('--branch');
|
|
524
|
+
if (branchIdx !== -1 && args[branchIdx + 1]) branch = args[branchIdx + 1];
|
|
525
|
+
|
|
526
|
+
const nameIdx = args.indexOf('--name');
|
|
527
|
+
if (nameIdx !== -1 && args[nameIdx + 1]) name = args[nameIdx + 1];
|
|
528
|
+
|
|
529
|
+
const typeIdx = args.indexOf('--type');
|
|
530
|
+
if (typeIdx !== -1 && args[typeIdx + 1]) ptype = args[typeIdx + 1];
|
|
531
|
+
|
|
532
|
+
console.log(`${colors.cyan}Triggering deployment for GitHub repo: ${ownerRepo} (${branch})...${colors.reset}`);
|
|
533
|
+
const res = await makeRequest('/projects/deploy', 'POST', {
|
|
534
|
+
name,
|
|
535
|
+
repo_url: repoUrl,
|
|
536
|
+
branch,
|
|
537
|
+
type: ptype
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
if (res.success) {
|
|
541
|
+
console.log(`\n${colors.green}${colors.bold}✓ Deployment queued successfully!${colors.reset}`);
|
|
542
|
+
console.log(`Project ID: ${res.project_id}`);
|
|
543
|
+
console.log(`Deployment ID: ${res.deployment_id}`);
|
|
544
|
+
console.log(`Public URL: ${colors.bold}${res.url}${colors.reset}\n`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
} else if (type === 'wordpress') {
|
|
548
|
+
// Deploy wordpress <name> --email <email> [--title <title>] [--user <user>] [--password <pass>]
|
|
549
|
+
const name = args[0];
|
|
550
|
+
if (!name) {
|
|
551
|
+
console.error(`${colors.red}Error: Please specify the site/project name.${colors.reset}`);
|
|
552
|
+
console.log('Usage: rushdeploy deploy wordpress <name> --email <email> [--title <title>] [--user <user>] [--password <pass>]');
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const emailIdx = args.indexOf('--email');
|
|
557
|
+
if (emailIdx === -1 || !args[emailIdx + 1]) {
|
|
558
|
+
console.error(`${colors.red}Error: --email <email> option is required for WordPress deployment.${colors.reset}`);
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
const adminEmail = args[emailIdx + 1];
|
|
562
|
+
|
|
563
|
+
let siteTitle = `${name} Site`;
|
|
564
|
+
const titleIdx = args.indexOf('--title');
|
|
565
|
+
if (titleIdx !== -1 && args[titleIdx + 1]) siteTitle = args[titleIdx + 1];
|
|
566
|
+
|
|
567
|
+
let adminUser = 'admin';
|
|
568
|
+
const userIdx = args.indexOf('--user');
|
|
569
|
+
if (userIdx !== -1 && args[userIdx + 1]) adminUser = args[userIdx + 1];
|
|
570
|
+
|
|
571
|
+
let adminPassword = null;
|
|
572
|
+
const passIdx = args.indexOf('--password');
|
|
573
|
+
if (passIdx !== -1 && args[passIdx + 1]) adminPassword = args[passIdx + 1];
|
|
574
|
+
|
|
575
|
+
console.log(`${colors.cyan}Triggering WordPress deployment: ${name}...${colors.reset}`);
|
|
576
|
+
const res = await makeRequest('/projects/wordpress', 'POST', {
|
|
577
|
+
name,
|
|
578
|
+
admin_email: adminEmail,
|
|
579
|
+
site_title: siteTitle,
|
|
580
|
+
admin_user: adminUser,
|
|
581
|
+
admin_password: adminPassword
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
if (res.success) {
|
|
585
|
+
console.log(`\n${colors.green}${colors.bold}✓ WordPress deployment started successfully!${colors.reset}`);
|
|
586
|
+
console.log(`Project ID: ${res.project_id}`);
|
|
587
|
+
console.log(`Deployment ID: ${res.deployment_id}`);
|
|
588
|
+
console.log(`Public URL: ${colors.bold}${res.url}${colors.reset}\n`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
} else {
|
|
592
|
+
console.error(`${colors.red}Error: Invalid deployment source type. Must be 'git', 'repo', or 'wordpress'.${colors.reset}`);
|
|
593
|
+
console.log('Usage:');
|
|
594
|
+
console.log(' rushdeploy deploy git <repo_url> [options]');
|
|
595
|
+
console.log(' rushdeploy deploy repo <owner/repo> [options]');
|
|
596
|
+
console.log(' rushdeploy deploy wordpress <name> --email <email> [options]');
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function resolveProjectId(idOrSlug) {
|
|
602
|
+
if (/^\d+$/.test(idOrSlug)) {
|
|
603
|
+
return parseInt(idOrSlug, 10);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const res = await makeRequest('/projects/');
|
|
607
|
+
const projects = res.projects || [];
|
|
608
|
+
const matched = projects.find(p => p.slug === idOrSlug || p.name.toLowerCase() === idOrSlug.toLowerCase());
|
|
609
|
+
if (matched) {
|
|
610
|
+
return matched.id;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
console.error(`${colors.red}Error: No project found matching name or slug "${idOrSlug}".${colors.reset}`);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function cmdProject(idOrSlug, subcommand, subArgs) {
|
|
618
|
+
const id = await resolveProjectId(idOrSlug);
|
|
619
|
+
|
|
620
|
+
switch (subcommand) {
|
|
621
|
+
case 'start': {
|
|
622
|
+
console.log(`${colors.cyan}Starting project ${id}...${colors.reset}`);
|
|
623
|
+
const res = await makeRequest(`/projects/${id}/start`, 'POST');
|
|
624
|
+
if (res.success) {
|
|
625
|
+
console.log(`${colors.green}${colors.bold}✓ Project container started successfully.${colors.reset}`);
|
|
626
|
+
}
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
case 'stop': {
|
|
630
|
+
console.log(`${colors.cyan}Stopping project ${id}...${colors.reset}`);
|
|
631
|
+
const res = await makeRequest(`/projects/${id}/stop`, 'POST');
|
|
632
|
+
if (res.success) {
|
|
633
|
+
console.log(`${colors.green}${colors.bold}✓ Project container stopped successfully.${colors.reset}`);
|
|
634
|
+
}
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
case 'redeploy': {
|
|
638
|
+
console.log(`${colors.cyan}Triggering project redeployment...${colors.reset}`);
|
|
639
|
+
const res = await makeRequest(`/projects/${id}/redeploy`, 'POST');
|
|
640
|
+
if (res.success) {
|
|
641
|
+
console.log(`${colors.green}${colors.bold}✓ Redeployment triggered successfully.${colors.reset}`);
|
|
642
|
+
console.log(`Deployment ID: ${res.deployment_id}`);
|
|
643
|
+
}
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
case 'visit': {
|
|
647
|
+
const res = await makeRequest(`/projects/${id}`);
|
|
648
|
+
const url = res.project.url;
|
|
649
|
+
openBrowser(url);
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
case 'stats':
|
|
653
|
+
case 'metrics': {
|
|
654
|
+
const statsRes = await makeRequest(`/projects/${id}/stats`);
|
|
655
|
+
const info = await makeRequest(`/projects/${id}/status`);
|
|
656
|
+
const stats = statsRes.stats;
|
|
657
|
+
|
|
658
|
+
console.log(`\n${colors.cyan}${colors.bold}====================================================${colors.reset}`);
|
|
659
|
+
console.log(`${colors.bold} RESOURCE METRICS: PROJECT ${id} ${colors.reset}`);
|
|
660
|
+
console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}`);
|
|
661
|
+
console.log(`Current Status: ${info.status.toUpperCase()}`);
|
|
662
|
+
|
|
663
|
+
if (stats) {
|
|
664
|
+
const cpu = stats.cpu_usage !== undefined ? stats.cpu_usage.toFixed(2) : '0.00';
|
|
665
|
+
const memUsed = formatBytes(stats.memory_usage);
|
|
666
|
+
const memLimit = formatBytes(stats.memory_limit);
|
|
667
|
+
const memPct = stats.memory_limit ? ((stats.memory_usage / stats.memory_limit) * 100).toFixed(2) : '0.00';
|
|
668
|
+
|
|
669
|
+
console.log(`CPU Usage: ${colors.green}${cpu}%${colors.reset}`);
|
|
670
|
+
console.log(`Memory Usage: ${memUsed} / ${memLimit} (${colors.yellow}${memPct}%${colors.reset})`);
|
|
671
|
+
} else {
|
|
672
|
+
console.log('Container stats not active. Is the project stopped?');
|
|
673
|
+
}
|
|
674
|
+
console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}\n`);
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
case 'actions': {
|
|
678
|
+
const res = await makeRequest(`/projects/${id}`);
|
|
679
|
+
const deployments = res.deployments || [];
|
|
680
|
+
|
|
681
|
+
if (deployments.length === 0) {
|
|
682
|
+
console.log('No recent deployments found.');
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
console.log(`\n${colors.cyan}${colors.bold}====================================================${colors.reset}`);
|
|
687
|
+
console.log(`${colors.bold} RECENT DEPLOYMENT ACTIONS: PROJECT ${id} ${colors.reset}`);
|
|
688
|
+
console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}`);
|
|
689
|
+
deployments.slice(0, 10).forEach(d => {
|
|
690
|
+
const dur = d.duration_ms ? `${(d.duration_ms / 1000).toFixed(1)}s` : 'N/A';
|
|
691
|
+
let statColor = colors.reset;
|
|
692
|
+
if (d.status === 'success') statColor = colors.green;
|
|
693
|
+
if (d.status === 'failed') statColor = colors.red;
|
|
694
|
+
|
|
695
|
+
console.log(`ID: ${d.id} (${statColor}${d.status.toUpperCase()}${colors.reset})`);
|
|
696
|
+
console.log(`Branch: ${d.branch} | Duration: ${dur}`);
|
|
697
|
+
console.log(`Created: ${formatDate(d.created_at)}`);
|
|
698
|
+
console.log(`----------------------------------------------------`);
|
|
699
|
+
});
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case 'autoscale': {
|
|
703
|
+
const enableArg = subArgs[0];
|
|
704
|
+
if (!enableArg || (enableArg !== 'on' && enableArg !== 'off')) {
|
|
705
|
+
const res = await makeRequest(`/projects/${id}`);
|
|
706
|
+
const p = res.project;
|
|
707
|
+
console.log(`\n${colors.bold}Autoscale Configuration for project ${id}:${colors.reset}`);
|
|
708
|
+
console.log(` Autoscale Enabled: ${p.autoscale_enabled ? colors.green + 'ON' : colors.red + 'OFF'}${colors.reset}`);
|
|
709
|
+
console.log(` Min/Max Replicas: ${p.min_replicas} (min) / ${p.max_replicas} (max)`);
|
|
710
|
+
console.log(` CPU Threshold trigger: ${p.cpu_threshold_percent}%`);
|
|
711
|
+
console.log();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const enabled = (enableArg === 'on');
|
|
716
|
+
let min_replicas = 1;
|
|
717
|
+
let max_replicas = 5;
|
|
718
|
+
let cpu_threshold_percent = 80.0;
|
|
719
|
+
|
|
720
|
+
const minIdx = subArgs.indexOf('--min');
|
|
721
|
+
if (minIdx !== -1 && subArgs[minIdx + 1]) min_replicas = parseInt(subArgs[minIdx + 1], 10);
|
|
722
|
+
|
|
723
|
+
const maxIdx = subArgs.indexOf('--max');
|
|
724
|
+
if (maxIdx !== -1 && subArgs[maxIdx + 1]) max_replicas = parseInt(subArgs[maxIdx + 1], 10);
|
|
725
|
+
|
|
726
|
+
const cpuIdx = subArgs.indexOf('--cpu');
|
|
727
|
+
if (cpuIdx !== -1 && subArgs[cpuIdx + 1]) cpu_threshold_percent = parseFloat(subArgs[cpuIdx + 1]);
|
|
728
|
+
|
|
729
|
+
console.log(`${colors.cyan}Updating autoscale configuration...${colors.reset}`);
|
|
730
|
+
const res = await makeRequest(`/projects/${id}/autoscale`, 'PUT', {
|
|
731
|
+
autoscale_enabled: enabled,
|
|
732
|
+
min_replicas,
|
|
733
|
+
max_replicas,
|
|
734
|
+
cpu_threshold_percent
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
if (res.success) {
|
|
738
|
+
console.log(`${colors.green}${colors.bold}✓ Autoscale settings updated successfully.${colors.reset}`);
|
|
739
|
+
console.log(` Enabled: ${res.autoscale.autoscale_enabled ? 'ON' : 'OFF'}`);
|
|
740
|
+
console.log(` Replicas: ${res.autoscale.min_replicas} - ${res.autoscale.max_replicas}`);
|
|
741
|
+
console.log(` Trigger: ${res.autoscale.cpu_threshold_percent}% CPU`);
|
|
742
|
+
}
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
case 'autodeploy': {
|
|
746
|
+
const enableArg = subArgs[0];
|
|
747
|
+
if (!enableArg || (enableArg !== 'on' && enableArg !== 'off')) {
|
|
748
|
+
const res = await makeRequest(`/projects/${id}`);
|
|
749
|
+
console.log(`Auto-deploy is currently: ${res.project.auto_deploy ? colors.green + 'ON' : colors.red + 'OFF'}${colors.reset}`);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (enableArg === 'on') {
|
|
754
|
+
console.log(`${colors.cyan}Enabling auto-deploy (webhook configuration)...${colors.reset}`);
|
|
755
|
+
const res = await makeRequest(`/projects/${id}/auto-deploy/enable`, 'POST');
|
|
756
|
+
if (res.success) {
|
|
757
|
+
console.log(`${colors.green}${colors.bold}✓ GitHub Auto-Deploy enabled.${colors.reset}`);
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
console.log(`${colors.cyan}Disabling auto-deploy...${colors.reset}`);
|
|
761
|
+
const res = await makeRequest(`/projects/${id}/auto-deploy/disable`, 'POST');
|
|
762
|
+
if (res.success) {
|
|
763
|
+
console.log(`${colors.green}${colors.bold}✓ GitHub Auto-Deploy disabled.${colors.reset}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
case 'logs': {
|
|
769
|
+
let linesLimit = 50;
|
|
770
|
+
const linesIdx = subArgs.indexOf('--lines');
|
|
771
|
+
if (linesIdx !== -1 && subArgs[linesIdx + 1]) {
|
|
772
|
+
linesLimit = parseInt(subArgs[linesIdx + 1], 10);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const res = await makeRequest(`/projects/${id}/logs`);
|
|
776
|
+
const logLines = res.lines || [];
|
|
777
|
+
console.log(`\n${colors.cyan}${colors.bold}Showing last ${Math.min(linesLimit, logLines.length)} log lines for project ${id}:${colors.reset}`);
|
|
778
|
+
console.log('-'.repeat(80));
|
|
779
|
+
logLines.slice(-linesLimit).forEach(ln => {
|
|
780
|
+
let levelColor = colors.reset;
|
|
781
|
+
if (ln.level === 'error') levelColor = colors.red;
|
|
782
|
+
console.log(`[${formatDate(ln.timestamp)}] ${levelColor}${ln.message}${colors.reset}`);
|
|
783
|
+
});
|
|
784
|
+
console.log();
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
case 'env': {
|
|
788
|
+
const action = subArgs[0];
|
|
789
|
+
|
|
790
|
+
if (!action) {
|
|
791
|
+
const res = await makeRequest(`/projects/${id}/env`);
|
|
792
|
+
const env = res.env || {};
|
|
793
|
+
console.log(`\n${colors.bold}Environment Variables for project ${id}:${colors.reset}`);
|
|
794
|
+
console.log('-'.repeat(50));
|
|
795
|
+
Object.entries(env).forEach(([k, v]) => {
|
|
796
|
+
console.log(`${colors.green}${k}${colors.reset}=${v}`);
|
|
797
|
+
});
|
|
798
|
+
console.log();
|
|
799
|
+
} else if (action === 'set') {
|
|
800
|
+
const setPairs = subArgs.slice(1);
|
|
801
|
+
if (setPairs.length === 0) {
|
|
802
|
+
console.error(`${colors.red}Error: Please specify KEY=VALUE pairs to set.${colors.reset}`);
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const currentRes = await makeRequest(`/projects/${id}/env`);
|
|
807
|
+
const env = currentRes.env || {};
|
|
808
|
+
|
|
809
|
+
setPairs.forEach(pair => {
|
|
810
|
+
const parts = pair.split('=');
|
|
811
|
+
const k = parts[0];
|
|
812
|
+
const v = parts.slice(1).join('=');
|
|
813
|
+
if (k) env[k] = v;
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
const res = await makeRequest(`/projects/${id}/env`, 'PUT', { env });
|
|
817
|
+
if (res.success) {
|
|
818
|
+
console.log(`${colors.green}${colors.bold}✓ Environment variables updated successfully.${colors.reset}`);
|
|
819
|
+
console.log('Run `rushdeploy project <slug> redeploy` to apply changes.');
|
|
820
|
+
}
|
|
821
|
+
} else if (action === 'remove') {
|
|
822
|
+
const removeKeys = subArgs.slice(1);
|
|
823
|
+
if (removeKeys.length === 0) {
|
|
824
|
+
console.error(`${colors.red}Error: Please specify keys to remove.${colors.reset}`);
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const currentRes = await makeRequest(`/projects/${id}/env`);
|
|
829
|
+
const env = currentRes.env || {};
|
|
830
|
+
|
|
831
|
+
removeKeys.forEach(k => {
|
|
832
|
+
delete env[k];
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
const res = await makeRequest(`/projects/${id}/env`, 'PUT', { env });
|
|
836
|
+
if (res.success) {
|
|
837
|
+
console.log(`${colors.green}${colors.bold}✓ Environment variables updated successfully.${colors.reset}`);
|
|
838
|
+
console.log('Run `rushdeploy project <slug> redeploy` to apply changes.');
|
|
839
|
+
}
|
|
840
|
+
} else {
|
|
841
|
+
console.error(`${colors.red}Unknown env subcommand: "${action}". Use "set" or "remove".${colors.reset}`);
|
|
842
|
+
}
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
default: {
|
|
846
|
+
const res = await makeRequest(`/projects/${id}`);
|
|
847
|
+
const p = res.project;
|
|
848
|
+
|
|
849
|
+
console.log(`\n${colors.cyan}${colors.bold}====================================================${colors.reset}`);
|
|
850
|
+
console.log(`${colors.bold} PROJECT: ${p.name.toUpperCase()} ${colors.reset}`);
|
|
851
|
+
console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}`);
|
|
852
|
+
console.log(`ID: ${p.id}`);
|
|
853
|
+
console.log(`Slug: ${p.slug}`);
|
|
854
|
+
console.log(`Status: ${p.status.toUpperCase()}`);
|
|
855
|
+
console.log(`Public URL: ${colors.bold}${p.url}${colors.reset}`);
|
|
856
|
+
console.log(`Repository: ${p.repo_url} (${p.branch})`);
|
|
857
|
+
console.log(`----------------------------------------------------`);
|
|
858
|
+
console.log(`Memory Limit: ${p.memory_mb} MB`);
|
|
859
|
+
console.log(`CPU Alloc: ${p.cpu_cores} Cores`);
|
|
860
|
+
console.log(`Autoscale: ${p.autoscale_enabled ? 'Enabled' : 'Disabled'}`);
|
|
861
|
+
if (p.autoscale_enabled) {
|
|
862
|
+
console.log(` Min/Max Reps: ${p.min_replicas} / ${p.max_replicas}`);
|
|
863
|
+
console.log(` Current Reps: ${p.current_replicas}`);
|
|
864
|
+
}
|
|
865
|
+
console.log(`Created At: ${formatDate(p.created_at)}`);
|
|
866
|
+
console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}\n`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function cmdWorkspace() {
|
|
872
|
+
const wsRes = await makeRequest('/workspaces/current');
|
|
873
|
+
const workspace = wsRes.workspace;
|
|
874
|
+
const usage = wsRes.usage;
|
|
875
|
+
const limits = wsRes.limits;
|
|
876
|
+
|
|
877
|
+
console.log(`\n${colors.cyan}${colors.bold}====================================================${colors.reset}`);
|
|
878
|
+
console.log(`${colors.bold} WORKSPACE QUOTAS & METRICS ${colors.reset}`);
|
|
879
|
+
console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}`);
|
|
880
|
+
console.log(`Workspace: ${workspace.name} (${workspace.slug})`);
|
|
881
|
+
console.log(`Billing Period: ${usage.billing_period}`);
|
|
882
|
+
console.log(`Tier: ${colors.magenta}${workspace.tier.toUpperCase()}${colors.reset}`);
|
|
883
|
+
console.log(`----------------------------------------------------`);
|
|
884
|
+
console.log(`Resource Usage:`);
|
|
885
|
+
|
|
886
|
+
const projLimit = limits.max_projects || 'Unlimited';
|
|
887
|
+
const depLimit = limits.max_deployments || 'Unlimited';
|
|
888
|
+
const bwLimit = limits.max_bandwidth_bytes ? formatBytes(limits.max_bandwidth_bytes) : 'Unlimited';
|
|
889
|
+
const buildLimit = limits.max_build_minutes ? `${limits.max_build_minutes} mins` : 'Unlimited';
|
|
890
|
+
|
|
891
|
+
console.log(` Projects Created: ${usage.projects_count} / ${projLimit}`);
|
|
892
|
+
console.log(` Deployments: ${usage.deployments_count} / ${depLimit}`);
|
|
893
|
+
console.log(` Bandwidth Used: ${formatBytes(usage.bandwidth_bytes)} / ${bwLimit}`);
|
|
894
|
+
console.log(` Build Duration: ${(usage.build_seconds / 60).toFixed(1)} mins / ${buildLimit}`);
|
|
895
|
+
console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}\n`);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function printHelp() {
|
|
899
|
+
console.log(`
|
|
900
|
+
${colors.bold}RushDeploy CLI${colors.reset} - Command Line Interface for RushDeploy PaaS.
|
|
901
|
+
|
|
902
|
+
${colors.bold}Usage:${colors.reset}
|
|
903
|
+
rushdeploy <command> [arguments]
|
|
904
|
+
|
|
905
|
+
${colors.bold}Global Commands:${colors.reset}
|
|
906
|
+
${colors.green}login <token>${colors.reset} Authenticate with your personal access token.
|
|
907
|
+
(Uses production domain: ${SERVER_URL})
|
|
908
|
+
${colors.green}logout${colors.reset} Clear your stored CLI session token.
|
|
909
|
+
${colors.green}whoami${colors.reset} Show details of the current logged-in user.
|
|
910
|
+
${colors.green}plans${colors.reset} List the subscription tiers and resource quotas.
|
|
911
|
+
${colors.green}status${colors.reset} Display workspace metrics and summary.
|
|
912
|
+
${colors.green}projects${colors.reset} List all projects in your active workspace.
|
|
913
|
+
${colors.green}repos${colors.reset} List repositories in your connected GitHub account.
|
|
914
|
+
${colors.green}workspace${colors.reset} Display resource usage limits and quotas.
|
|
915
|
+
${colors.green}help${colors.reset} Show this help dialog.
|
|
916
|
+
|
|
917
|
+
${colors.bold}Deployment Commands:${colors.reset}
|
|
918
|
+
${colors.green}deploy git <repo_url> [opts]${colors.reset} Deploy a project directly from a Git URL.
|
|
919
|
+
Options: --name <name> --branch <branch> --type <type>
|
|
920
|
+
${colors.green}deploy repo <owner/repo> [opts]${colors.reset}Deploy a project from your connected GitHub account.
|
|
921
|
+
Options: --name <name> --branch <branch> --type <type>
|
|
922
|
+
${colors.green}deploy wordpress <name> --email <e>${colors.reset} Deploy a new one-click WordPress instance.
|
|
923
|
+
Options: --title <title> --user <user> --password <pass>
|
|
924
|
+
|
|
925
|
+
${colors.bold}Project Control Commands:${colors.reset}
|
|
926
|
+
${colors.green}project <slug|id> start${colors.reset} Start the project container.
|
|
927
|
+
${colors.green}project <slug|id> stop${colors.reset} Stop the project container.
|
|
928
|
+
${colors.green}project <slug|id> redeploy${colors.reset} Trigger a project rebuild and redeploy.
|
|
929
|
+
${colors.green}project <slug|id> visit${colors.reset} Open the project's public URL in the browser.
|
|
930
|
+
${colors.green}project <slug|id> stats${colors.reset} Show real-time container CPU & Memory metrics.
|
|
931
|
+
${colors.green}project <slug|id> metrics${colors.reset} Show CPU & memory metrics (alias of stats).
|
|
932
|
+
${colors.green}project <slug|id> actions${colors.reset} Show recent deployment history.
|
|
933
|
+
${colors.green}project <slug|id> logs${colors.reset} Fetch recent container and build logs.
|
|
934
|
+
Use --lines <N> to customize output length.
|
|
935
|
+
|
|
936
|
+
${colors.bold}Project Configuration Commands:${colors.reset}
|
|
937
|
+
${colors.green}project <slug|id> autoscale${colors.reset} Show auto-scaling config.
|
|
938
|
+
${colors.green}project <slug|id> autoscale <on|off>${colors.reset} Enable/disable scaling.
|
|
939
|
+
Optional: --min <N> --max <N> --cpu <%>
|
|
940
|
+
${colors.green}project <slug|id> autodeploy <on|off>${colors.reset} Enable/disable webhook pushes.
|
|
941
|
+
${colors.green}project <slug|id> env${colors.reset} List environment variables.
|
|
942
|
+
${colors.green}project <slug|id> env set KEY=VAL [K2=V2]${colors.reset} Set environment variables.
|
|
943
|
+
${colors.green}project <slug|id> env remove KEY [KEY2]${colors.reset} Remove environment variables.
|
|
944
|
+
`);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
async function main() {
|
|
948
|
+
const args = process.argv.slice(2);
|
|
949
|
+
const command = args[0];
|
|
950
|
+
|
|
951
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
952
|
+
printHelp();
|
|
953
|
+
process.exit(0);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
switch (command) {
|
|
957
|
+
case 'login': {
|
|
958
|
+
const token = args[1];
|
|
959
|
+
if (!token) {
|
|
960
|
+
console.error(`${colors.red}Error: Please provide your access token.${colors.reset}`);
|
|
961
|
+
console.log('Usage: rushdeploy login <token> [--server <url>]');
|
|
962
|
+
process.exit(1);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
let server = null;
|
|
966
|
+
const serverIndex = args.indexOf('--server');
|
|
967
|
+
if (serverIndex !== -1 && args[serverIndex + 1]) {
|
|
968
|
+
server = args[serverIndex + 1];
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
await cmdLogin(token, server);
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
case 'logout':
|
|
975
|
+
cmdLogout();
|
|
976
|
+
break;
|
|
977
|
+
case 'whoami':
|
|
978
|
+
await cmdWhoami();
|
|
979
|
+
break;
|
|
980
|
+
case 'plans':
|
|
981
|
+
await cmdPlans();
|
|
982
|
+
break;
|
|
983
|
+
case 'status':
|
|
984
|
+
await cmdStatus();
|
|
985
|
+
break;
|
|
986
|
+
case 'projects':
|
|
987
|
+
await cmdProjects();
|
|
988
|
+
break;
|
|
989
|
+
case 'repos':
|
|
990
|
+
await cmdRepos();
|
|
991
|
+
break;
|
|
992
|
+
case 'deploy': {
|
|
993
|
+
const type = args[1];
|
|
994
|
+
const deployArgs = args.slice(2);
|
|
995
|
+
await cmdDeploy(type, deployArgs);
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
case 'project': {
|
|
999
|
+
const idOrSlug = args[1];
|
|
1000
|
+
if (!idOrSlug) {
|
|
1001
|
+
console.error(`${colors.red}Error: Please provide a project ID or slug.${colors.reset}`);
|
|
1002
|
+
console.log('Usage: rushdeploy project <slug|id> [subcommand] [arguments]');
|
|
1003
|
+
process.exit(1);
|
|
1004
|
+
}
|
|
1005
|
+
const subcommand = args[2];
|
|
1006
|
+
const subArgs = args.slice(3);
|
|
1007
|
+
await cmdProject(idOrSlug, subcommand, subArgs);
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
case 'workspace':
|
|
1011
|
+
await cmdWorkspace();
|
|
1012
|
+
break;
|
|
1013
|
+
default:
|
|
1014
|
+
console.error(`${colors.red}Unknown command: "${command}"${colors.reset}`);
|
|
1015
|
+
printHelp();
|
|
1016
|
+
process.exit(1);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
main().catch(err => {
|
|
1021
|
+
console.error(`${colors.red}Fatal Error:${colors.reset}`, err.message);
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rushdeploy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool to manage your self-hosted RushDeploy PaaS account.",
|
|
5
|
+
"main": "bin/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rushdeploy": "bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"rushdeploy",
|
|
15
|
+
"paas",
|
|
16
|
+
"cli"
|
|
17
|
+
],
|
|
18
|
+
"author": "RushDeploy Team",
|
|
19
|
+
"license": "MIT"
|
|
20
|
+
}
|