rushdeploy 1.0.4 → 1.0.5
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/README.md +1 -0
- package/bin/index.js +1269 -1245
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -1,1245 +1,1269 @@
|
|
|
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 tokens & UI utilities
|
|
32
|
-
const colors = {
|
|
33
|
-
reset: '\x1b[0m',
|
|
34
|
-
bold: '\x1b[1m',
|
|
35
|
-
dim: '\x1b[2m',
|
|
36
|
-
italic: '\x1b[3m',
|
|
37
|
-
underline: '\x1b[4m',
|
|
38
|
-
gray: '\x1b[90m',
|
|
39
|
-
white: '\x1b[37m',
|
|
40
|
-
brightWhite: '\x1b[97m',
|
|
41
|
-
green: '\x1b[32m',
|
|
42
|
-
brightGreen: '\x1b[92m',
|
|
43
|
-
red: '\x1b[31m',
|
|
44
|
-
brightRed: '\x1b[91m',
|
|
45
|
-
yellow: '\x1b[33m',
|
|
46
|
-
brightYellow: '\x1b[93m',
|
|
47
|
-
cyan: '\x1b[36m',
|
|
48
|
-
brightCyan: '\x1b[96m',
|
|
49
|
-
blue: '\x1b[34m',
|
|
50
|
-
magenta: '\x1b[35m'
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
function stripAnsi(str) {
|
|
54
|
-
return String(str || '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const ui = {
|
|
58
|
-
step(msg) {
|
|
59
|
-
console.log(`${colors.green}✓${colors.reset} ${msg}`);
|
|
60
|
-
},
|
|
61
|
-
stepInfo(msg) {
|
|
62
|
-
console.log(`${colors.cyan}❯${colors.reset} ${msg}`);
|
|
63
|
-
},
|
|
64
|
-
stepWarn(msg) {
|
|
65
|
-
console.log(`${colors.yellow}⚠${colors.reset} ${msg}`);
|
|
66
|
-
},
|
|
67
|
-
stepError(msg) {
|
|
68
|
-
console.log(`${colors.red}✗${colors.reset} ${msg}`);
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
badge(statusStr) {
|
|
72
|
-
if (!statusStr) return '';
|
|
73
|
-
const upper = String(statusStr).toUpperCase();
|
|
74
|
-
if (['RUNNING', 'ONLINE', 'ACTIVE', 'SUCCESS', 'AUTHENTICATED', 'OK'].includes(upper)) {
|
|
75
|
-
return `${colors.green}${colors.bold}${upper}${colors.reset}`;
|
|
76
|
-
}
|
|
77
|
-
if (['BUILDING', 'PENDING', 'QUEUED', 'STARTING', 'ON', 'WARN'].includes(upper)) {
|
|
78
|
-
return `${colors.yellow}${colors.bold}${upper}${colors.reset}`;
|
|
79
|
-
}
|
|
80
|
-
if (['FAILED', 'OFFLINE', 'STOPPED', 'OFF', 'ERROR'].includes(upper)) {
|
|
81
|
-
return `${colors.red}${colors.bold}${upper}${colors.reset}`;
|
|
82
|
-
}
|
|
83
|
-
return `${colors.cyan}${colors.bold}${upper}${colors.reset}`;
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
progressBar(percent, width = 16) {
|
|
87
|
-
const pct = Math.max(0, Math.min(100, parseFloat(percent) || 0));
|
|
88
|
-
const filled = Math.round((pct / 100) * width);
|
|
89
|
-
const empty = width - filled;
|
|
90
|
-
const bar = `${colors.green}${'█'.repeat(filled)}${colors.gray}${'░'.repeat(empty)}${colors.reset}`;
|
|
91
|
-
return `${bar} ${pct.toFixed(1)}%`;
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
box(fields, options = {}) {
|
|
95
|
-
const fieldPairs = Array.isArray(fields)
|
|
96
|
-
? fields
|
|
97
|
-
: Object.entries(fields).map(([label, value]) => ({ label, value }));
|
|
98
|
-
|
|
99
|
-
const labelWidth = 14;
|
|
100
|
-
const leftIndent = 3;
|
|
101
|
-
const rightMargin = 3;
|
|
102
|
-
const gap = 2;
|
|
103
|
-
|
|
104
|
-
let maxValLen = 30;
|
|
105
|
-
fieldPairs.forEach(f => {
|
|
106
|
-
const rawVal = stripAnsi(f.value !== undefined ? String(f.value) : '');
|
|
107
|
-
if (rawVal.length > maxValLen) maxValLen = rawVal.length;
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const innerWidth = Math.max(options.minWidth || 66, leftIndent + labelWidth + gap + maxValLen + rightMargin);
|
|
111
|
-
|
|
112
|
-
const topBorder = `┌${'─'.repeat(innerWidth)}┐`;
|
|
113
|
-
const emptyRow = `│${' '.repeat(innerWidth)}│`;
|
|
114
|
-
const bottomBorder = `└${'─'.repeat(innerWidth)}┘`;
|
|
115
|
-
|
|
116
|
-
console.log(`\n${colors.gray}${topBorder}${colors.reset}`);
|
|
117
|
-
console.log(`${colors.gray}${emptyRow}${colors.reset}`);
|
|
118
|
-
|
|
119
|
-
fieldPairs.forEach(f => {
|
|
120
|
-
const label = f.label || '';
|
|
121
|
-
const valStr = f.value !== undefined ? String(f.value) : '';
|
|
122
|
-
const rawVal = stripAnsi(valStr);
|
|
123
|
-
|
|
124
|
-
const labelPadded = label.padEnd(labelWidth);
|
|
125
|
-
const padLen = innerWidth - (leftIndent + labelWidth + gap + rawVal.length + rightMargin);
|
|
126
|
-
const padRight = ' '.repeat(Math.max(0, padLen));
|
|
127
|
-
|
|
128
|
-
console.log(
|
|
129
|
-
`${colors.gray}│${colors.reset}` +
|
|
130
|
-
' '.repeat(leftIndent) +
|
|
131
|
-
`${colors.gray}${labelPadded}${colors.reset}` +
|
|
132
|
-
' '.repeat(gap) +
|
|
133
|
-
`${valStr}` +
|
|
134
|
-
padRight +
|
|
135
|
-
' '.repeat(rightMargin) +
|
|
136
|
-
`${colors.gray}│${colors.reset}`
|
|
137
|
-
);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
console.log(`${colors.gray}${emptyRow}${colors.reset}`);
|
|
141
|
-
console.log(`${colors.gray}${bottomBorder}${colors.reset}\n`);
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
table(headers, rows) {
|
|
147
|
-
if (!rows || rows.length === 0) return;
|
|
148
|
-
|
|
149
|
-
const colWidths = headers.map(h => h.label.length);
|
|
150
|
-
rows.forEach(row => {
|
|
151
|
-
row.forEach((cell, i) => {
|
|
152
|
-
const raw = stripAnsi(cell !== undefined ? String(cell) : '');
|
|
153
|
-
if (raw.length > (colWidths[i] || 0)) {
|
|
154
|
-
colWidths[i] = raw.length;
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const headerLine = headers.map((h, i) => {
|
|
160
|
-
const label = h.label.toUpperCase();
|
|
161
|
-
return colors.gray + colors.bold + label.padEnd(colWidths[i] + (h.pad || 3)) + colors.reset;
|
|
162
|
-
}).join('');
|
|
163
|
-
|
|
164
|
-
const totalLen = colWidths.reduce((a, b) => a + b, 0) + (headers.length * 3);
|
|
165
|
-
const divider = colors.gray + '─'.repeat(Math.max(64, totalLen)) + colors.reset;
|
|
166
|
-
|
|
167
|
-
console.log(`\n${headerLine}`);
|
|
168
|
-
console.log(divider);
|
|
169
|
-
|
|
170
|
-
rows.forEach(row => {
|
|
171
|
-
const rowLine = row.map((cell, i) => {
|
|
172
|
-
const str = cell !== undefined ? String(cell) : '';
|
|
173
|
-
const raw = stripAnsi(str);
|
|
174
|
-
const padLen = (colWidths[i] || 0) + (headers[i]?.pad || 3) - raw.length;
|
|
175
|
-
return str + ' '.repeat(Math.max(0, padLen));
|
|
176
|
-
}).join('');
|
|
177
|
-
console.log(rowLine);
|
|
178
|
-
});
|
|
179
|
-
console.log();
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
function loadConfig() {
|
|
184
|
-
if (!fs.existsSync(CONFIG_FILE)) return null;
|
|
185
|
-
try {
|
|
186
|
-
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
187
|
-
} catch (err) {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function saveConfig(server, token) {
|
|
193
|
-
try {
|
|
194
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
195
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
196
|
-
}
|
|
197
|
-
const cleanServer = (server || DEFAULT_SERVER).replace(/\/$/, '');
|
|
198
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ server: cleanServer, token }, null, 2), 'utf8');
|
|
199
|
-
return true;
|
|
200
|
-
} catch (err) {
|
|
201
|
-
ui.stepError(`Error saving configuration: ${err.message}`);
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function makeRequest(apiPath, method = 'GET', data = null) {
|
|
207
|
-
return new Promise((resolve, reject) => {
|
|
208
|
-
const config = loadConfig();
|
|
209
|
-
const isLoginPath = apiPath.endsWith('/auth/me') && method === 'GET';
|
|
210
|
-
const isPlansPath = apiPath.endsWith('/subscriptions/plans') && method === 'GET';
|
|
211
|
-
|
|
212
|
-
if (!config && !isLoginPath && !isPlansPath) {
|
|
213
|
-
ui.stepError(`You are not logged in. Run 'rushdeploy login <token>' first.`);
|
|
214
|
-
process.exit(1);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const serverUrl = getServerUrl();
|
|
218
|
-
const token = config ? config.token : null;
|
|
219
|
-
|
|
220
|
-
const parsedUrl = new URL(serverUrl);
|
|
221
|
-
const options = {
|
|
222
|
-
hostname: parsedUrl.hostname,
|
|
223
|
-
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
224
|
-
path: `/api/v1${apiPath}`,
|
|
225
|
-
method: method,
|
|
226
|
-
headers: {
|
|
227
|
-
'Accept': 'application/json',
|
|
228
|
-
'Content-Type': 'application/json'
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
if (token) {
|
|
233
|
-
options.headers['Authorization'] = `Bearer ${token}`;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const client = parsedUrl.protocol === 'https:' ? https : http;
|
|
237
|
-
|
|
238
|
-
const req = client.request(options, (res) => {
|
|
239
|
-
let body = '';
|
|
240
|
-
res.on('data', (chunk) => body += chunk);
|
|
241
|
-
res.on('end', () => {
|
|
242
|
-
let jsonResponse;
|
|
243
|
-
try {
|
|
244
|
-
jsonResponse = JSON.parse(body);
|
|
245
|
-
} catch (e) {
|
|
246
|
-
jsonResponse = body;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
250
|
-
resolve(jsonResponse);
|
|
251
|
-
} else {
|
|
252
|
-
let errorMsg = res.statusMessage || 'Request failed';
|
|
253
|
-
if (jsonResponse && jsonResponse.error) {
|
|
254
|
-
errorMsg = jsonResponse.error.message || errorMsg;
|
|
255
|
-
} else if (jsonResponse && jsonResponse.detail) {
|
|
256
|
-
errorMsg = typeof jsonResponse.detail === 'object' ? jsonResponse.detail.message : jsonResponse.detail;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (res.statusCode === 401) {
|
|
260
|
-
ui.stepError(`Unauthorized (401). Your token may have expired or been revoked.`);
|
|
261
|
-
console.log(` Please log in again using: ${colors.green}rushdeploy login <token>${colors.reset}`);
|
|
262
|
-
} else if (res.statusCode === 429) {
|
|
263
|
-
ui.stepError(`Rate Limit Exceeded (429): You are sending commands too quickly.`);
|
|
264
|
-
console.log(` Please wait a minute before running more CLI commands.`);
|
|
265
|
-
} else {
|
|
266
|
-
ui.stepError(`Error (${res.statusCode}): ${errorMsg}`);
|
|
267
|
-
}
|
|
268
|
-
process.exit(1);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
req.on('error', (err) => {
|
|
274
|
-
ui.stepError(`Connection Error: Unable to connect to server at ${serverUrl}`);
|
|
275
|
-
console.log(` Details: ${colors.gray}${err.message}${colors.reset}`);
|
|
276
|
-
process.exit(1);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
if (data) {
|
|
280
|
-
req.write(JSON.stringify(data));
|
|
281
|
-
}
|
|
282
|
-
req.end();
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function formatBytes(bytes) {
|
|
287
|
-
if (bytes === undefined || bytes === null || isNaN(bytes)) return '0 B';
|
|
288
|
-
if (bytes === 0) return '0 B';
|
|
289
|
-
const k = 1024;
|
|
290
|
-
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
291
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
292
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function formatMemory(val) {
|
|
296
|
-
if (val === undefined || val === null || isNaN(val)) return '0 B';
|
|
297
|
-
const num = Number(val);
|
|
298
|
-
if (num === 0) return '0 B';
|
|
299
|
-
const bytes = num < 1000000 ? num * 1024 * 1024 : num;
|
|
300
|
-
return formatBytes(bytes);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function formatDate(isoStr) {
|
|
304
|
-
if (!isoStr) return 'Never';
|
|
305
|
-
try {
|
|
306
|
-
const d = new Date(isoStr);
|
|
307
|
-
return d.toLocaleString();
|
|
308
|
-
} catch (e) {
|
|
309
|
-
return isoStr;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function openBrowser(url) {
|
|
314
|
-
let command;
|
|
315
|
-
switch (process.platform) {
|
|
316
|
-
case 'darwin':
|
|
317
|
-
command = `open "${url}"`;
|
|
318
|
-
break;
|
|
319
|
-
case 'win32':
|
|
320
|
-
command = `start "" "${url}"`;
|
|
321
|
-
break;
|
|
322
|
-
default:
|
|
323
|
-
command = `xdg-open "${url}"`;
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
326
|
-
exec(command, (err) => {
|
|
327
|
-
if (err) {
|
|
328
|
-
ui.stepError(`Failed to open browser: ${err.message}`);
|
|
329
|
-
} else {
|
|
330
|
-
ui.step(`Opening browser: ${colors.brightCyan}${url}${colors.reset}`);
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function extractRepoName(url) {
|
|
336
|
-
try {
|
|
337
|
-
const cleanUrl = url.replace(/\/$/, '').replace(/\.git$/, '');
|
|
338
|
-
const parts = cleanUrl.split('/');
|
|
339
|
-
return parts[parts.length - 1] || 'my-app';
|
|
340
|
-
} catch (e) {
|
|
341
|
-
return 'my-app';
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async function cmdLogin(token, serverArg) {
|
|
346
|
-
const server = serverArg ||
|
|
347
|
-
ui.stepInfo(`Connecting to ${colors.brightWhite}${server}${colors.reset}...`);
|
|
348
|
-
|
|
349
|
-
saveConfig(server, token);
|
|
350
|
-
|
|
351
|
-
try {
|
|
352
|
-
ui.stepInfo(`Validating authentication token...`);
|
|
353
|
-
const res = await makeRequest('/auth/me');
|
|
354
|
-
if (res.success) {
|
|
355
|
-
const user = res.user;
|
|
356
|
-
ui.step(`Authentication successful!`);
|
|
357
|
-
|
|
358
|
-
ui.box([
|
|
359
|
-
{ label: 'Account', value: `${colors.bold}${user.name}${colors.reset} (${colors.gray}${user.email}${colors.reset})` },
|
|
360
|
-
{ label: 'Role', value: user.role.toUpperCase() },
|
|
361
|
-
{ label: 'Status', value: ui.badge('AUTHENTICATED') },
|
|
362
|
-
{ label: 'Server', value: colors.gray + server + colors.reset }
|
|
363
|
-
]);
|
|
364
|
-
}
|
|
365
|
-
} catch (err) {
|
|
366
|
-
if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
|
|
367
|
-
ui.stepError(`Authentication failed. Check your token or server URL.`);
|
|
368
|
-
process.exit(1);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function cmdLogout() {
|
|
373
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
374
|
-
fs.unlinkSync(CONFIG_FILE);
|
|
375
|
-
ui.step(`Successfully logged out. Local token configuration cleared.`);
|
|
376
|
-
} else {
|
|
377
|
-
ui.stepInfo(`You are already logged out.`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async function cmdWhoami() {
|
|
382
|
-
const config = loadConfig();
|
|
383
|
-
if (!config) {
|
|
384
|
-
ui.stepWarn(`Not logged in. Run 'rushdeploy login <token>' to log in.`);
|
|
385
|
-
process.exit(0);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const res = await makeRequest('/auth/me');
|
|
389
|
-
const user = res.user;
|
|
390
|
-
ui.box([
|
|
391
|
-
{ label: 'Name', value: colors.bold + user.name + colors.reset },
|
|
392
|
-
{ label: 'Email', value: user.email },
|
|
393
|
-
{ label: 'Role', value: user.role.toUpperCase() },
|
|
394
|
-
{ label: 'Server', value: colors.gray + getServerUrl() + colors.reset }
|
|
395
|
-
], { title: 'USER PROFILE' });
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
async function cmdPlans() {
|
|
399
|
-
ui.stepInfo(`Fetching RushDeploy hosting plans...`);
|
|
400
|
-
const res = await makeRequest('/subscriptions/plans');
|
|
401
|
-
const plans = res.plans || [];
|
|
402
|
-
|
|
403
|
-
plans.forEach(p => {
|
|
404
|
-
const price = p.base_price_monthly ? `$${p.base_price_monthly}/mo` : 'Free';
|
|
405
|
-
const fields = [
|
|
406
|
-
{ label: 'Monthly Price', value: colors.green + colors.bold + price + colors.reset },
|
|
407
|
-
{ label: 'Description', value: colors.gray + (p.description || 'N/A') + colors.reset }
|
|
408
|
-
];
|
|
409
|
-
if (p.limits) {
|
|
410
|
-
fields.push(
|
|
411
|
-
{ label: 'Projects Limit', value: String(p.limits.max_projects || 'Unlimited') },
|
|
412
|
-
{ label: 'Deployments', value: String(p.limits.max_deployments || 'Unlimited') },
|
|
413
|
-
{ label: 'Bandwidth', value: p.limits.max_bandwidth_bytes ? formatBytes(p.limits.max_bandwidth_bytes) : 'Unlimited' },
|
|
414
|
-
{ label: 'Build Minutes', value: p.limits.max_build_minutes ? `${p.limits.max_build_minutes} mins` : 'Unlimited' }
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
ui.box(fields, { title: p.name.toUpperCase() });
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function drawCards(cards, cardsPerRow = 2) {
|
|
422
|
-
const cardWidth = 34;
|
|
423
|
-
const rows = [];
|
|
424
|
-
|
|
425
|
-
for (let i = 0; i < cards.length; i += cardsPerRow) {
|
|
426
|
-
rows.push(cards.slice(i, i + cardsPerRow));
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
rows.forEach(row => {
|
|
430
|
-
let topBorder = '';
|
|
431
|
-
let labelValLine = '';
|
|
432
|
-
let sepLine = '';
|
|
433
|
-
let hintLine = '';
|
|
434
|
-
let bottomBorder = '';
|
|
435
|
-
|
|
436
|
-
row.forEach((card, idx) => {
|
|
437
|
-
const space = idx > 0 ? ' ' : ''; // Gap between cards
|
|
438
|
-
|
|
439
|
-
topBorder += space + colors.gray + '┌' + '─'.repeat(cardWidth - 2) + '┐' + colors.reset;
|
|
440
|
-
|
|
441
|
-
const label = card.label || '';
|
|
442
|
-
let value = card.value !== undefined ? String(card.value) : '';
|
|
443
|
-
|
|
444
|
-
let valColor = colors.bold;
|
|
445
|
-
if (card.status === 'online') valColor = colors.green + colors.bold;
|
|
446
|
-
else if (card.status === 'offline') valColor = colors.red + colors.bold;
|
|
447
|
-
else if (card.status === 'warn') valColor = colors.yellow + colors.bold;
|
|
448
|
-
|
|
449
|
-
const valStr = `${valColor}${value}${colors.reset}`;
|
|
450
|
-
const rawVal = stripAnsi(value);
|
|
451
|
-
|
|
452
|
-
const textLen = label.length + rawVal.length;
|
|
453
|
-
const padLen = cardWidth - 6 - textLen;
|
|
454
|
-
const pad = ' '.repeat(Math.max(1, padLen));
|
|
455
|
-
|
|
456
|
-
labelValLine += space + `${colors.gray}│${colors.reset} ${colors.brightCyan}${label}${colors.reset}${pad}${valStr} ${colors.gray}│${colors.reset}`;
|
|
457
|
-
sepLine += space + colors.gray + '├' + '─'.repeat(cardWidth - 2) + '┤' + colors.reset;
|
|
458
|
-
|
|
459
|
-
const hint = card.hint || '';
|
|
460
|
-
const rawHint = stripAnsi(hint);
|
|
461
|
-
const hintPadLen = cardWidth - 6 - rawHint.length;
|
|
462
|
-
const hintPad = ' '.repeat(Math.max(0, hintPadLen));
|
|
463
|
-
|
|
464
|
-
hintLine += space + `${colors.gray}│${colors.reset} ${colors.gray}${hint}${colors.reset}${hintPad} ${colors.gray}│${colors.reset}`;
|
|
465
|
-
bottomBorder += space + colors.gray + '└' + '─'.repeat(cardWidth - 2) + '┘' + colors.reset;
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
console.log(topBorder);
|
|
469
|
-
console.log(labelValLine);
|
|
470
|
-
console.log(sepLine);
|
|
471
|
-
console.log(hintLine);
|
|
472
|
-
console.log(bottomBorder);
|
|
473
|
-
console.log();
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
async function cmdStatus() {
|
|
478
|
-
const userRes = await makeRequest('/auth/me');
|
|
479
|
-
const user = userRes.user;
|
|
480
|
-
|
|
481
|
-
let workspaceRes = null;
|
|
482
|
-
try {
|
|
483
|
-
workspaceRes = await makeRequest('/workspaces/current');
|
|
484
|
-
} catch (err) {
|
|
485
|
-
// Ignore / fallback
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (user.role === 'super_admin') {
|
|
489
|
-
try {
|
|
490
|
-
ui.stepInfo(`Fetching system metrics (Admin overview)...`);
|
|
491
|
-
const [overviewRes, serverRes] = await Promise.all([
|
|
492
|
-
makeRequest('/admin/overview').catch(() => null),
|
|
493
|
-
makeRequest('/admin/server').catch(() => null)
|
|
494
|
-
]);
|
|
495
|
-
|
|
496
|
-
if (overviewRes && overviewRes.success && serverRes && serverRes.success) {
|
|
497
|
-
const stats = overviewRes.stats || {};
|
|
498
|
-
const server = serverRes.server || {};
|
|
499
|
-
const byStatus = stats.projects_by_status || {};
|
|
500
|
-
const celery = stats.celery || {};
|
|
501
|
-
const workerOnline = (celery.workers ?? 0) > 0;
|
|
502
|
-
|
|
503
|
-
const activeBuildingProjects = (byStatus.building || byStatus.BUILDING || 0) + (byStatus.pending || byStatus.PENDING || 0);
|
|
504
|
-
const activeTasksCount = (celery.active && celery.active > 0) ? celery.active : activeBuildingProjects;
|
|
505
|
-
const pendingInQueue = stats.redis_queue_len ?? celery.queued ?? 0;
|
|
506
|
-
|
|
507
|
-
const cpuVal = server.cpu_percent ? `${Number(server.cpu_percent).toFixed(1)}%` : 'Normal';
|
|
508
|
-
const ramVal = server.memory ? `${Number(server.memory.used_percent).toFixed(1)}%` : 'Healthy';
|
|
509
|
-
const diskVal = server.disk ? `${Number(server.disk.used_percent).toFixed(1)}%` : 'Optimal';
|
|
510
|
-
|
|
511
|
-
const runCount = byStatus.running || byStatus.RUNNING || 0;
|
|
512
|
-
const buildCount = activeBuildingProjects;
|
|
513
|
-
const failCount = byStatus.failed || byStatus.FAILED || 0;
|
|
514
|
-
|
|
515
|
-
ui.step(`System Overview metrics retrieved`);
|
|
516
|
-
|
|
517
|
-
const adminCards = [
|
|
518
|
-
{ label: 'Users', value: stats.users ?? 0, hint: 'Total registered users' },
|
|
519
|
-
{ label: 'Databases', value: stats.databases ?? 0, hint: 'Total workspace databases' },
|
|
520
|
-
{
|
|
521
|
-
label: 'Projects',
|
|
522
|
-
value: stats.projects ?? 0,
|
|
523
|
-
hint: `${colors.green}${runCount} run${colors.reset} · ${colors.yellow}${buildCount} build${colors.reset} · ${colors.red}${failCount} fail${colors.reset}`
|
|
524
|
-
},
|
|
525
|
-
{
|
|
526
|
-
label: 'App Containers',
|
|
527
|
-
value: stats.running_containers ?? 0,
|
|
528
|
-
status: stats.docker_available ? 'online' : 'offline',
|
|
529
|
-
hint: stats.docker_available ? 'Active running containers' : 'Engine offline'
|
|
530
|
-
},
|
|
531
|
-
{ label: 'CPU Usage', value: cpuVal, hint: 'Host CPU load' },
|
|
532
|
-
{ label: 'RAM Usage', value: ramVal, hint: 'Host memory allocation' },
|
|
533
|
-
{ label: 'Disk Usage', value: diskVal, hint: 'Host storage capacity' },
|
|
534
|
-
{
|
|
535
|
-
label: 'Task Queue',
|
|
536
|
-
value: workerOnline ? 'Online' : 'Offline',
|
|
537
|
-
status: workerOnline ? 'online' : 'offline',
|
|
538
|
-
hint: `${activeTasksCount} active · ${pendingInQueue} pending`
|
|
539
|
-
}
|
|
540
|
-
];
|
|
541
|
-
|
|
542
|
-
drawCards(adminCards, 2);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
} catch (err) {
|
|
546
|
-
// Fall through to workspace status
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Fallback / Standard workspace view
|
|
551
|
-
if (workspaceRes && workspaceRes.success) {
|
|
552
|
-
const workspace = workspaceRes.workspace;
|
|
553
|
-
const usage = workspaceRes.usage;
|
|
554
|
-
const limits = workspaceRes.limits;
|
|
555
|
-
|
|
556
|
-
const projLimit = limits.max_projects || 'Unlimited';
|
|
557
|
-
const bwLimit = limits.max_bandwidth_bytes ? formatBytes(limits.max_bandwidth_bytes) : 'Unlimited';
|
|
558
|
-
const buildLimit = limits.max_build_minutes ? `${limits.max_build_minutes} mins` : 'Unlimited';
|
|
559
|
-
|
|
560
|
-
ui.step(`Workspace metrics retrieved`);
|
|
561
|
-
|
|
562
|
-
const workspaceCards = [
|
|
563
|
-
{ label: 'Workspace', value: workspace.name, hint: `Tier Plan: ${workspace.tier.toUpperCase()}` },
|
|
564
|
-
{ label: 'Projects Quota', value: `${usage.projects_count} / ${projLimit}`, hint: 'Active project slots' },
|
|
565
|
-
{ label: 'Bandwidth Limit', value: `${formatBytes(usage.bandwidth_bytes)} / ${bwLimit}`, hint: 'Monthly data usage' },
|
|
566
|
-
{ label: 'Build Duration', value: `${(usage.build_seconds / 60).toFixed(1)}m / ${buildLimit}`, hint: 'Monthly build duration' }
|
|
567
|
-
];
|
|
568
|
-
|
|
569
|
-
drawCards(workspaceCards, 2);
|
|
570
|
-
} else {
|
|
571
|
-
ui.stepError(`Could not retrieve status details.`);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
async function cmdProjects() {
|
|
576
|
-
ui.stepInfo(`Fetching projects list...`);
|
|
577
|
-
const res = await makeRequest('/projects/');
|
|
578
|
-
const projects = res.projects || [];
|
|
579
|
-
|
|
580
|
-
if (projects.length === 0) {
|
|
581
|
-
ui.stepInfo('No projects found in active workspace.');
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
ui.step(`Retrieved ${projects.length} project(s)`);
|
|
586
|
-
|
|
587
|
-
const headers = [
|
|
588
|
-
{ label: 'ID', pad: 4 },
|
|
589
|
-
{ label: 'Project Name', pad: 4 },
|
|
590
|
-
{ label: 'Status', pad: 4 },
|
|
591
|
-
{ label: 'Public URL', pad: 2 }
|
|
592
|
-
];
|
|
593
|
-
|
|
594
|
-
const rows = projects.map(p => [
|
|
595
|
-
String(p.id),
|
|
596
|
-
colors.bold + p.name + colors.reset,
|
|
597
|
-
ui.badge(p.status),
|
|
598
|
-
colors.brightCyan + p.url + colors.reset
|
|
599
|
-
]);
|
|
600
|
-
|
|
601
|
-
ui.table(headers, rows);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
async function cmdRepos() {
|
|
605
|
-
try {
|
|
606
|
-
ui.stepInfo(`Fetching connected GitHub repositories...`);
|
|
607
|
-
const res = await makeRequest('/auth/github/repos');
|
|
608
|
-
const repos = res.repos || [];
|
|
609
|
-
|
|
610
|
-
if (repos.length === 0) {
|
|
611
|
-
ui.stepInfo('No GitHub repositories found. Try connecting to GitHub first.');
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
ui.step(`Found ${repos.length} repository/repositories`);
|
|
616
|
-
|
|
617
|
-
const headers = [
|
|
618
|
-
{ label: 'Repository Name', pad: 4 },
|
|
619
|
-
{ label: 'Default Branch', pad: 4 },
|
|
620
|
-
{ label: 'Visibility', pad: 2 }
|
|
621
|
-
];
|
|
622
|
-
|
|
623
|
-
const rows = repos.map(r => [
|
|
624
|
-
colors.bold + r.full_name + colors.reset,
|
|
625
|
-
colors.gray + String(r.default_branch || 'main') + colors.reset,
|
|
626
|
-
r.private ? ui.badge('PRIVATE') : ui.badge('PUBLIC')
|
|
627
|
-
]);
|
|
628
|
-
|
|
629
|
-
ui.table(headers, rows);
|
|
630
|
-
} catch (err) {
|
|
631
|
-
ui.stepWarn(`Please connect your GitHub account via Settings in the Web Dashboard first.`);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
async function cmdDeploy(type, args) {
|
|
636
|
-
if (type === 'git') {
|
|
637
|
-
const repoUrl = args[0];
|
|
638
|
-
if (!repoUrl) {
|
|
639
|
-
ui.stepError(`Please specify the Git repository URL.`);
|
|
640
|
-
console.log(` Usage: ${colors.green}rushdeploy deploy git <repo_url> [--branch <branch>] [--name <name>] [--type <type>]${colors.reset}`);
|
|
641
|
-
process.exit(1);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
let branch = 'main';
|
|
645
|
-
let name = extractRepoName(repoUrl);
|
|
646
|
-
let ptype = 'auto';
|
|
647
|
-
|
|
648
|
-
const branchIdx = args.indexOf('--branch');
|
|
649
|
-
if (branchIdx !== -1 && args[branchIdx + 1]) branch = args[branchIdx + 1];
|
|
650
|
-
|
|
651
|
-
const nameIdx = args.indexOf('--name');
|
|
652
|
-
if (nameIdx !== -1 && args[nameIdx + 1]) name = args[nameIdx + 1];
|
|
653
|
-
|
|
654
|
-
const typeIdx = args.indexOf('--type');
|
|
655
|
-
if (typeIdx !== -1 && args[typeIdx + 1]) ptype = args[typeIdx + 1];
|
|
656
|
-
|
|
657
|
-
ui.step(`Connecting to repository`);
|
|
658
|
-
ui.step(`Creating project ${name}`);
|
|
659
|
-
ui.step(`Triggering deployment pipeline for ${branch} branch`);
|
|
660
|
-
|
|
661
|
-
const res = await makeRequest('/projects/deploy', 'POST', {
|
|
662
|
-
name,
|
|
663
|
-
repo_url: repoUrl,
|
|
664
|
-
branch,
|
|
665
|
-
type: ptype
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
if (res.success) {
|
|
669
|
-
ui.step(`Deployment queued successfully!`);
|
|
670
|
-
ui.box([
|
|
671
|
-
{ label: 'Project', value: colors.bold + name + colors.reset },
|
|
672
|
-
{ label: 'State', value: ui.badge('QUEUED') },
|
|
673
|
-
{ label: 'Project ID', value: String(res.project_id) },
|
|
674
|
-
{ label: 'Deployment ID', value: String(res.deployment_id) },
|
|
675
|
-
{ label: 'Repository', value: colors.gray + repoUrl + colors.reset },
|
|
676
|
-
{ label: 'Public URL', value: colors.brightCyan + colors.bold + res.url + colors.reset }
|
|
677
|
-
]);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
} else if (type === 'repo') {
|
|
681
|
-
const ownerRepo = args[0];
|
|
682
|
-
if (!ownerRepo || !ownerRepo.includes('/')) {
|
|
683
|
-
ui.stepError(`Please specify the repository in 'owner/repo' format.`);
|
|
684
|
-
console.log(` Usage: ${colors.green}rushdeploy deploy repo <owner/repo> [--branch <branch>] [--name <name>] [--type <type>]${colors.reset}`);
|
|
685
|
-
process.exit(1);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const repoUrl = `https://github.com/${ownerRepo}.git`;
|
|
689
|
-
let branch = 'main';
|
|
690
|
-
let name = ownerRepo.split('/')[1];
|
|
691
|
-
let ptype = 'auto';
|
|
692
|
-
|
|
693
|
-
const branchIdx = args.indexOf('--branch');
|
|
694
|
-
if (branchIdx !== -1 && args[branchIdx + 1]) branch = args[branchIdx + 1];
|
|
695
|
-
|
|
696
|
-
const nameIdx = args.indexOf('--name');
|
|
697
|
-
if (nameIdx !== -1 && args[nameIdx + 1]) name = args[nameIdx + 1];
|
|
698
|
-
|
|
699
|
-
const typeIdx = args.indexOf('--type');
|
|
700
|
-
if (typeIdx !== -1 && args[typeIdx + 1]) ptype = args[typeIdx + 1];
|
|
701
|
-
|
|
702
|
-
ui.step(`Connecting to GitHub repository ${ownerRepo}`);
|
|
703
|
-
ui.step(`Creating project ${name}`);
|
|
704
|
-
ui.step(`Triggering deployment pipeline for ${branch} branch`);
|
|
705
|
-
|
|
706
|
-
const res = await makeRequest('/projects/deploy', 'POST', {
|
|
707
|
-
name,
|
|
708
|
-
repo_url: repoUrl,
|
|
709
|
-
branch,
|
|
710
|
-
type: ptype
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
if (res.success) {
|
|
714
|
-
ui.step(`Deployment queued successfully!`);
|
|
715
|
-
ui.box([
|
|
716
|
-
{ label: 'Project', value: colors.bold + name + colors.reset },
|
|
717
|
-
{ label: 'State', value: ui.badge('QUEUED') },
|
|
718
|
-
{ label: 'Project ID', value: String(res.project_id) },
|
|
719
|
-
{ label: 'Deployment ID', value: String(res.deployment_id) },
|
|
720
|
-
{ label: 'Repository', value: colors.gray + ownerRepo + colors.reset },
|
|
721
|
-
{ label: 'Public URL', value: colors.brightCyan + colors.bold + res.url + colors.reset }
|
|
722
|
-
]);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
} else if (type === 'wordpress') {
|
|
726
|
-
const name = args[0];
|
|
727
|
-
if (!name) {
|
|
728
|
-
ui.stepError(`Please specify the site/project name.`);
|
|
729
|
-
console.log(` Usage: ${colors.green}rushdeploy deploy wordpress <name> --email <email> [options]${colors.reset}`);
|
|
730
|
-
process.exit(1);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const emailIdx = args.indexOf('--email');
|
|
734
|
-
if (emailIdx === -1 || !args[emailIdx + 1]) {
|
|
735
|
-
ui.stepError(`--email <email> option is required for WordPress deployment.`);
|
|
736
|
-
process.exit(1);
|
|
737
|
-
}
|
|
738
|
-
const adminEmail = args[emailIdx + 1];
|
|
739
|
-
|
|
740
|
-
let siteTitle = `${name} Site`;
|
|
741
|
-
const titleIdx = args.indexOf('--title');
|
|
742
|
-
if (titleIdx !== -1 && args[titleIdx + 1]) siteTitle = args[titleIdx + 1];
|
|
743
|
-
|
|
744
|
-
let adminUser = 'admin';
|
|
745
|
-
const userIdx = args.indexOf('--user');
|
|
746
|
-
if (userIdx !== -1 && args[userIdx + 1]) adminUser = args[userIdx + 1];
|
|
747
|
-
|
|
748
|
-
let adminPassword = null;
|
|
749
|
-
const passIdx = args.indexOf('--password');
|
|
750
|
-
if (passIdx !== -1 && args[passIdx + 1]) adminPassword = args[passIdx + 1];
|
|
751
|
-
|
|
752
|
-
ui.step(`Preparing WordPress environment for ${name}`);
|
|
753
|
-
ui.step(`Configuring database and web server container`);
|
|
754
|
-
ui.step(`Starting WordPress auto-installation`);
|
|
755
|
-
|
|
756
|
-
const res = await makeRequest('/projects/wordpress', 'POST', {
|
|
757
|
-
name,
|
|
758
|
-
admin_email: adminEmail,
|
|
759
|
-
site_title: siteTitle,
|
|
760
|
-
admin_user: adminUser,
|
|
761
|
-
admin_password: adminPassword
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
if (res.success) {
|
|
765
|
-
ui.step(`WordPress deployment started successfully!`);
|
|
766
|
-
ui.box([
|
|
767
|
-
{ label: 'Site Name', value: colors.bold + name + colors.reset },
|
|
768
|
-
{ label: 'State', value: ui.badge('BUILDING') },
|
|
769
|
-
{ label: 'Admin Email', value: adminEmail },
|
|
770
|
-
{ label: 'Project ID', value: String(res.project_id) },
|
|
771
|
-
{ label: 'Deployment ID', value: String(res.deployment_id) },
|
|
772
|
-
{ label: 'Public URL', value: colors.brightCyan + colors.bold + res.url + colors.reset }
|
|
773
|
-
]);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
} else {
|
|
777
|
-
ui.stepError(`Invalid deployment source type. Must be 'git', 'repo', or 'wordpress'.`);
|
|
778
|
-
console.log(`\nUsage:`);
|
|
779
|
-
console.log(` rushdeploy deploy git <repo_url> [options]`);
|
|
780
|
-
console.log(` rushdeploy deploy repo <owner/repo> [options]`);
|
|
781
|
-
console.log(` rushdeploy deploy wordpress <name> --email <email> [options]`);
|
|
782
|
-
process.exit(1);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
async function resolveProjectId(idOrSlug) {
|
|
787
|
-
if (/^\d+$/.test(idOrSlug)) {
|
|
788
|
-
return parseInt(idOrSlug, 10);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
const res = await makeRequest('/projects/');
|
|
792
|
-
const projects = res.projects || [];
|
|
793
|
-
const matched = projects.find(p => p.slug === idOrSlug || p.name.toLowerCase() === idOrSlug.toLowerCase());
|
|
794
|
-
if (matched) {
|
|
795
|
-
return matched.id;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
ui.stepError(`No project found matching name or slug "${idOrSlug}".`);
|
|
799
|
-
process.exit(1);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
async function cmdProject(idOrSlug, subcommand, subArgs) {
|
|
803
|
-
const id = await resolveProjectId(idOrSlug);
|
|
804
|
-
|
|
805
|
-
switch (subcommand) {
|
|
806
|
-
case 'start': {
|
|
807
|
-
ui.stepInfo(`Starting project ${id}...`);
|
|
808
|
-
const res = await makeRequest(`/projects/${id}/start`, 'POST');
|
|
809
|
-
if (res.success) {
|
|
810
|
-
ui.step(`Project container started successfully.`);
|
|
811
|
-
}
|
|
812
|
-
break;
|
|
813
|
-
}
|
|
814
|
-
case 'stop': {
|
|
815
|
-
ui.stepInfo(`Stopping project ${id}...`);
|
|
816
|
-
const res = await makeRequest(`/projects/${id}/stop`, 'POST');
|
|
817
|
-
if (res.success) {
|
|
818
|
-
ui.step(`Project container stopped successfully.`);
|
|
819
|
-
}
|
|
820
|
-
break;
|
|
821
|
-
}
|
|
822
|
-
case 'redeploy': {
|
|
823
|
-
ui.stepInfo(`Triggering project redeployment...`);
|
|
824
|
-
const res = await makeRequest(`/projects/${id}/redeploy`, 'POST');
|
|
825
|
-
if (res.success) {
|
|
826
|
-
ui.step(`Redeployment triggered successfully.`);
|
|
827
|
-
ui.stepInfo(`Deployment ID: ${res.deployment_id}`);
|
|
828
|
-
}
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
case '
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
if (
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
ui.
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
}
|
|
967
|
-
break;
|
|
968
|
-
}
|
|
969
|
-
case '
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
${colors.
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
${colors.green}
|
|
1155
|
-
${colors.green}
|
|
1156
|
-
${colors.green}
|
|
1157
|
-
${colors.green}
|
|
1158
|
-
|
|
1159
|
-
${colors.
|
|
1160
|
-
${colors.green}
|
|
1161
|
-
${colors.green}
|
|
1162
|
-
${colors.green}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
${colors.green}
|
|
1166
|
-
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
await
|
|
1218
|
-
break;
|
|
1219
|
-
}
|
|
1220
|
-
case '
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
case '
|
|
1233
|
-
await
|
|
1234
|
-
break;
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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 tokens & UI utilities
|
|
32
|
+
const colors = {
|
|
33
|
+
reset: '\x1b[0m',
|
|
34
|
+
bold: '\x1b[1m',
|
|
35
|
+
dim: '\x1b[2m',
|
|
36
|
+
italic: '\x1b[3m',
|
|
37
|
+
underline: '\x1b[4m',
|
|
38
|
+
gray: '\x1b[90m',
|
|
39
|
+
white: '\x1b[37m',
|
|
40
|
+
brightWhite: '\x1b[97m',
|
|
41
|
+
green: '\x1b[32m',
|
|
42
|
+
brightGreen: '\x1b[92m',
|
|
43
|
+
red: '\x1b[31m',
|
|
44
|
+
brightRed: '\x1b[91m',
|
|
45
|
+
yellow: '\x1b[33m',
|
|
46
|
+
brightYellow: '\x1b[93m',
|
|
47
|
+
cyan: '\x1b[36m',
|
|
48
|
+
brightCyan: '\x1b[96m',
|
|
49
|
+
blue: '\x1b[34m',
|
|
50
|
+
magenta: '\x1b[35m'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function stripAnsi(str) {
|
|
54
|
+
return String(str || '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ui = {
|
|
58
|
+
step(msg) {
|
|
59
|
+
console.log(`${colors.green}✓${colors.reset} ${msg}`);
|
|
60
|
+
},
|
|
61
|
+
stepInfo(msg) {
|
|
62
|
+
console.log(`${colors.cyan}❯${colors.reset} ${msg}`);
|
|
63
|
+
},
|
|
64
|
+
stepWarn(msg) {
|
|
65
|
+
console.log(`${colors.yellow}⚠${colors.reset} ${msg}`);
|
|
66
|
+
},
|
|
67
|
+
stepError(msg) {
|
|
68
|
+
console.log(`${colors.red}✗${colors.reset} ${msg}`);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
badge(statusStr) {
|
|
72
|
+
if (!statusStr) return '';
|
|
73
|
+
const upper = String(statusStr).toUpperCase();
|
|
74
|
+
if (['RUNNING', 'ONLINE', 'ACTIVE', 'SUCCESS', 'AUTHENTICATED', 'OK'].includes(upper)) {
|
|
75
|
+
return `${colors.green}${colors.bold}${upper}${colors.reset}`;
|
|
76
|
+
}
|
|
77
|
+
if (['BUILDING', 'PENDING', 'QUEUED', 'STARTING', 'ON', 'WARN'].includes(upper)) {
|
|
78
|
+
return `${colors.yellow}${colors.bold}${upper}${colors.reset}`;
|
|
79
|
+
}
|
|
80
|
+
if (['FAILED', 'OFFLINE', 'STOPPED', 'OFF', 'ERROR'].includes(upper)) {
|
|
81
|
+
return `${colors.red}${colors.bold}${upper}${colors.reset}`;
|
|
82
|
+
}
|
|
83
|
+
return `${colors.cyan}${colors.bold}${upper}${colors.reset}`;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
progressBar(percent, width = 16) {
|
|
87
|
+
const pct = Math.max(0, Math.min(100, parseFloat(percent) || 0));
|
|
88
|
+
const filled = Math.round((pct / 100) * width);
|
|
89
|
+
const empty = width - filled;
|
|
90
|
+
const bar = `${colors.green}${'█'.repeat(filled)}${colors.gray}${'░'.repeat(empty)}${colors.reset}`;
|
|
91
|
+
return `${bar} ${pct.toFixed(1)}%`;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
box(fields, options = {}) {
|
|
95
|
+
const fieldPairs = Array.isArray(fields)
|
|
96
|
+
? fields
|
|
97
|
+
: Object.entries(fields).map(([label, value]) => ({ label, value }));
|
|
98
|
+
|
|
99
|
+
const labelWidth = 14;
|
|
100
|
+
const leftIndent = 3;
|
|
101
|
+
const rightMargin = 3;
|
|
102
|
+
const gap = 2;
|
|
103
|
+
|
|
104
|
+
let maxValLen = 30;
|
|
105
|
+
fieldPairs.forEach(f => {
|
|
106
|
+
const rawVal = stripAnsi(f.value !== undefined ? String(f.value) : '');
|
|
107
|
+
if (rawVal.length > maxValLen) maxValLen = rawVal.length;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const innerWidth = Math.max(options.minWidth || 66, leftIndent + labelWidth + gap + maxValLen + rightMargin);
|
|
111
|
+
|
|
112
|
+
const topBorder = `┌${'─'.repeat(innerWidth)}┐`;
|
|
113
|
+
const emptyRow = `│${' '.repeat(innerWidth)}│`;
|
|
114
|
+
const bottomBorder = `└${'─'.repeat(innerWidth)}┘`;
|
|
115
|
+
|
|
116
|
+
console.log(`\n${colors.gray}${topBorder}${colors.reset}`);
|
|
117
|
+
console.log(`${colors.gray}${emptyRow}${colors.reset}`);
|
|
118
|
+
|
|
119
|
+
fieldPairs.forEach(f => {
|
|
120
|
+
const label = f.label || '';
|
|
121
|
+
const valStr = f.value !== undefined ? String(f.value) : '';
|
|
122
|
+
const rawVal = stripAnsi(valStr);
|
|
123
|
+
|
|
124
|
+
const labelPadded = label.padEnd(labelWidth);
|
|
125
|
+
const padLen = innerWidth - (leftIndent + labelWidth + gap + rawVal.length + rightMargin);
|
|
126
|
+
const padRight = ' '.repeat(Math.max(0, padLen));
|
|
127
|
+
|
|
128
|
+
console.log(
|
|
129
|
+
`${colors.gray}│${colors.reset}` +
|
|
130
|
+
' '.repeat(leftIndent) +
|
|
131
|
+
`${colors.gray}${labelPadded}${colors.reset}` +
|
|
132
|
+
' '.repeat(gap) +
|
|
133
|
+
`${valStr}` +
|
|
134
|
+
padRight +
|
|
135
|
+
' '.repeat(rightMargin) +
|
|
136
|
+
`${colors.gray}│${colors.reset}`
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(`${colors.gray}${emptyRow}${colors.reset}`);
|
|
141
|
+
console.log(`${colors.gray}${bottomBorder}${colors.reset}\n`);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
table(headers, rows) {
|
|
147
|
+
if (!rows || rows.length === 0) return;
|
|
148
|
+
|
|
149
|
+
const colWidths = headers.map(h => h.label.length);
|
|
150
|
+
rows.forEach(row => {
|
|
151
|
+
row.forEach((cell, i) => {
|
|
152
|
+
const raw = stripAnsi(cell !== undefined ? String(cell) : '');
|
|
153
|
+
if (raw.length > (colWidths[i] || 0)) {
|
|
154
|
+
colWidths[i] = raw.length;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const headerLine = headers.map((h, i) => {
|
|
160
|
+
const label = h.label.toUpperCase();
|
|
161
|
+
return colors.gray + colors.bold + label.padEnd(colWidths[i] + (h.pad || 3)) + colors.reset;
|
|
162
|
+
}).join('');
|
|
163
|
+
|
|
164
|
+
const totalLen = colWidths.reduce((a, b) => a + b, 0) + (headers.length * 3);
|
|
165
|
+
const divider = colors.gray + '─'.repeat(Math.max(64, totalLen)) + colors.reset;
|
|
166
|
+
|
|
167
|
+
console.log(`\n${headerLine}`);
|
|
168
|
+
console.log(divider);
|
|
169
|
+
|
|
170
|
+
rows.forEach(row => {
|
|
171
|
+
const rowLine = row.map((cell, i) => {
|
|
172
|
+
const str = cell !== undefined ? String(cell) : '';
|
|
173
|
+
const raw = stripAnsi(str);
|
|
174
|
+
const padLen = (colWidths[i] || 0) + (headers[i]?.pad || 3) - raw.length;
|
|
175
|
+
return str + ' '.repeat(Math.max(0, padLen));
|
|
176
|
+
}).join('');
|
|
177
|
+
console.log(rowLine);
|
|
178
|
+
});
|
|
179
|
+
console.log();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
function loadConfig() {
|
|
184
|
+
if (!fs.existsSync(CONFIG_FILE)) return null;
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
187
|
+
} catch (err) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function saveConfig(server, token) {
|
|
193
|
+
try {
|
|
194
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
195
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
196
|
+
}
|
|
197
|
+
const cleanServer = (server || DEFAULT_SERVER).replace(/\/$/, '');
|
|
198
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ server: cleanServer, token }, null, 2), 'utf8');
|
|
199
|
+
return true;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
ui.stepError(`Error saving configuration: ${err.message}`);
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function makeRequest(apiPath, method = 'GET', data = null) {
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
const config = loadConfig();
|
|
209
|
+
const isLoginPath = apiPath.endsWith('/auth/me') && method === 'GET';
|
|
210
|
+
const isPlansPath = apiPath.endsWith('/subscriptions/plans') && method === 'GET';
|
|
211
|
+
|
|
212
|
+
if (!config && !isLoginPath && !isPlansPath) {
|
|
213
|
+
ui.stepError(`You are not logged in. Run 'rushdeploy login <token>' first.`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const serverUrl = getServerUrl();
|
|
218
|
+
const token = config ? config.token : null;
|
|
219
|
+
|
|
220
|
+
const parsedUrl = new URL(serverUrl);
|
|
221
|
+
const options = {
|
|
222
|
+
hostname: parsedUrl.hostname,
|
|
223
|
+
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
224
|
+
path: `/api/v1${apiPath}`,
|
|
225
|
+
method: method,
|
|
226
|
+
headers: {
|
|
227
|
+
'Accept': 'application/json',
|
|
228
|
+
'Content-Type': 'application/json'
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (token) {
|
|
233
|
+
options.headers['Authorization'] = `Bearer ${token}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const client = parsedUrl.protocol === 'https:' ? https : http;
|
|
237
|
+
|
|
238
|
+
const req = client.request(options, (res) => {
|
|
239
|
+
let body = '';
|
|
240
|
+
res.on('data', (chunk) => body += chunk);
|
|
241
|
+
res.on('end', () => {
|
|
242
|
+
let jsonResponse;
|
|
243
|
+
try {
|
|
244
|
+
jsonResponse = JSON.parse(body);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
jsonResponse = body;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
250
|
+
resolve(jsonResponse);
|
|
251
|
+
} else {
|
|
252
|
+
let errorMsg = res.statusMessage || 'Request failed';
|
|
253
|
+
if (jsonResponse && jsonResponse.error) {
|
|
254
|
+
errorMsg = jsonResponse.error.message || errorMsg;
|
|
255
|
+
} else if (jsonResponse && jsonResponse.detail) {
|
|
256
|
+
errorMsg = typeof jsonResponse.detail === 'object' ? jsonResponse.detail.message : jsonResponse.detail;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (res.statusCode === 401) {
|
|
260
|
+
ui.stepError(`Unauthorized (401). Your token may have expired or been revoked.`);
|
|
261
|
+
console.log(` Please log in again using: ${colors.green}rushdeploy login <token>${colors.reset}`);
|
|
262
|
+
} else if (res.statusCode === 429) {
|
|
263
|
+
ui.stepError(`Rate Limit Exceeded (429): You are sending commands too quickly.`);
|
|
264
|
+
console.log(` Please wait a minute before running more CLI commands.`);
|
|
265
|
+
} else {
|
|
266
|
+
ui.stepError(`Error (${res.statusCode}): ${errorMsg}`);
|
|
267
|
+
}
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
req.on('error', (err) => {
|
|
274
|
+
ui.stepError(`Connection Error: Unable to connect to server at ${serverUrl}`);
|
|
275
|
+
console.log(` Details: ${colors.gray}${err.message}${colors.reset}`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (data) {
|
|
280
|
+
req.write(JSON.stringify(data));
|
|
281
|
+
}
|
|
282
|
+
req.end();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function formatBytes(bytes) {
|
|
287
|
+
if (bytes === undefined || bytes === null || isNaN(bytes)) return '0 B';
|
|
288
|
+
if (bytes === 0) return '0 B';
|
|
289
|
+
const k = 1024;
|
|
290
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
291
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
292
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function formatMemory(val) {
|
|
296
|
+
if (val === undefined || val === null || isNaN(val)) return '0 B';
|
|
297
|
+
const num = Number(val);
|
|
298
|
+
if (num === 0) return '0 B';
|
|
299
|
+
const bytes = num < 1000000 ? num * 1024 * 1024 : num;
|
|
300
|
+
return formatBytes(bytes);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function formatDate(isoStr) {
|
|
304
|
+
if (!isoStr) return 'Never';
|
|
305
|
+
try {
|
|
306
|
+
const d = new Date(isoStr);
|
|
307
|
+
return d.toLocaleString();
|
|
308
|
+
} catch (e) {
|
|
309
|
+
return isoStr;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function openBrowser(url) {
|
|
314
|
+
let command;
|
|
315
|
+
switch (process.platform) {
|
|
316
|
+
case 'darwin':
|
|
317
|
+
command = `open "${url}"`;
|
|
318
|
+
break;
|
|
319
|
+
case 'win32':
|
|
320
|
+
command = `start "" "${url}"`;
|
|
321
|
+
break;
|
|
322
|
+
default:
|
|
323
|
+
command = `xdg-open "${url}"`;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
exec(command, (err) => {
|
|
327
|
+
if (err) {
|
|
328
|
+
ui.stepError(`Failed to open browser: ${err.message}`);
|
|
329
|
+
} else {
|
|
330
|
+
ui.step(`Opening browser: ${colors.brightCyan}${url}${colors.reset}`);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function extractRepoName(url) {
|
|
336
|
+
try {
|
|
337
|
+
const cleanUrl = url.replace(/\/$/, '').replace(/\.git$/, '');
|
|
338
|
+
const parts = cleanUrl.split('/');
|
|
339
|
+
return parts[parts.length - 1] || 'my-app';
|
|
340
|
+
} catch (e) {
|
|
341
|
+
return 'my-app';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function cmdLogin(token, serverArg) {
|
|
346
|
+
const server = serverArg || getServerUrl();
|
|
347
|
+
ui.stepInfo(`Connecting to ${colors.brightWhite}${server}${colors.reset}...`);
|
|
348
|
+
|
|
349
|
+
saveConfig(server, token);
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
ui.stepInfo(`Validating authentication token...`);
|
|
353
|
+
const res = await makeRequest('/auth/me');
|
|
354
|
+
if (res.success) {
|
|
355
|
+
const user = res.user;
|
|
356
|
+
ui.step(`Authentication successful!`);
|
|
357
|
+
|
|
358
|
+
ui.box([
|
|
359
|
+
{ label: 'Account', value: `${colors.bold}${user.name}${colors.reset} (${colors.gray}${user.email}${colors.reset})` },
|
|
360
|
+
{ label: 'Role', value: user.role.toUpperCase() },
|
|
361
|
+
{ label: 'Status', value: ui.badge('AUTHENTICATED') },
|
|
362
|
+
{ label: 'Server', value: colors.gray + server + colors.reset }
|
|
363
|
+
]);
|
|
364
|
+
}
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
|
|
367
|
+
ui.stepError(`Authentication failed. Check your token or server URL.`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function cmdLogout() {
|
|
373
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
374
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
375
|
+
ui.step(`Successfully logged out. Local token configuration cleared.`);
|
|
376
|
+
} else {
|
|
377
|
+
ui.stepInfo(`You are already logged out.`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function cmdWhoami() {
|
|
382
|
+
const config = loadConfig();
|
|
383
|
+
if (!config) {
|
|
384
|
+
ui.stepWarn(`Not logged in. Run 'rushdeploy login <token>' to log in.`);
|
|
385
|
+
process.exit(0);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const res = await makeRequest('/auth/me');
|
|
389
|
+
const user = res.user;
|
|
390
|
+
ui.box([
|
|
391
|
+
{ label: 'Name', value: colors.bold + user.name + colors.reset },
|
|
392
|
+
{ label: 'Email', value: user.email },
|
|
393
|
+
{ label: 'Role', value: user.role.toUpperCase() },
|
|
394
|
+
{ label: 'Server', value: colors.gray + getServerUrl() + colors.reset }
|
|
395
|
+
], { title: 'USER PROFILE' });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function cmdPlans() {
|
|
399
|
+
ui.stepInfo(`Fetching RushDeploy hosting plans...`);
|
|
400
|
+
const res = await makeRequest('/subscriptions/plans');
|
|
401
|
+
const plans = res.plans || [];
|
|
402
|
+
|
|
403
|
+
plans.forEach(p => {
|
|
404
|
+
const price = p.base_price_monthly ? `$${p.base_price_monthly}/mo` : 'Free';
|
|
405
|
+
const fields = [
|
|
406
|
+
{ label: 'Monthly Price', value: colors.green + colors.bold + price + colors.reset },
|
|
407
|
+
{ label: 'Description', value: colors.gray + (p.description || 'N/A') + colors.reset }
|
|
408
|
+
];
|
|
409
|
+
if (p.limits) {
|
|
410
|
+
fields.push(
|
|
411
|
+
{ label: 'Projects Limit', value: String(p.limits.max_projects || 'Unlimited') },
|
|
412
|
+
{ label: 'Deployments', value: String(p.limits.max_deployments || 'Unlimited') },
|
|
413
|
+
{ label: 'Bandwidth', value: p.limits.max_bandwidth_bytes ? formatBytes(p.limits.max_bandwidth_bytes) : 'Unlimited' },
|
|
414
|
+
{ label: 'Build Minutes', value: p.limits.max_build_minutes ? `${p.limits.max_build_minutes} mins` : 'Unlimited' }
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
ui.box(fields, { title: p.name.toUpperCase() });
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function drawCards(cards, cardsPerRow = 2) {
|
|
422
|
+
const cardWidth = 34;
|
|
423
|
+
const rows = [];
|
|
424
|
+
|
|
425
|
+
for (let i = 0; i < cards.length; i += cardsPerRow) {
|
|
426
|
+
rows.push(cards.slice(i, i + cardsPerRow));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
rows.forEach(row => {
|
|
430
|
+
let topBorder = '';
|
|
431
|
+
let labelValLine = '';
|
|
432
|
+
let sepLine = '';
|
|
433
|
+
let hintLine = '';
|
|
434
|
+
let bottomBorder = '';
|
|
435
|
+
|
|
436
|
+
row.forEach((card, idx) => {
|
|
437
|
+
const space = idx > 0 ? ' ' : ''; // Gap between cards
|
|
438
|
+
|
|
439
|
+
topBorder += space + colors.gray + '┌' + '─'.repeat(cardWidth - 2) + '┐' + colors.reset;
|
|
440
|
+
|
|
441
|
+
const label = card.label || '';
|
|
442
|
+
let value = card.value !== undefined ? String(card.value) : '';
|
|
443
|
+
|
|
444
|
+
let valColor = colors.bold;
|
|
445
|
+
if (card.status === 'online') valColor = colors.green + colors.bold;
|
|
446
|
+
else if (card.status === 'offline') valColor = colors.red + colors.bold;
|
|
447
|
+
else if (card.status === 'warn') valColor = colors.yellow + colors.bold;
|
|
448
|
+
|
|
449
|
+
const valStr = `${valColor}${value}${colors.reset}`;
|
|
450
|
+
const rawVal = stripAnsi(value);
|
|
451
|
+
|
|
452
|
+
const textLen = label.length + rawVal.length;
|
|
453
|
+
const padLen = cardWidth - 6 - textLen;
|
|
454
|
+
const pad = ' '.repeat(Math.max(1, padLen));
|
|
455
|
+
|
|
456
|
+
labelValLine += space + `${colors.gray}│${colors.reset} ${colors.brightCyan}${label}${colors.reset}${pad}${valStr} ${colors.gray}│${colors.reset}`;
|
|
457
|
+
sepLine += space + colors.gray + '├' + '─'.repeat(cardWidth - 2) + '┤' + colors.reset;
|
|
458
|
+
|
|
459
|
+
const hint = card.hint || '';
|
|
460
|
+
const rawHint = stripAnsi(hint);
|
|
461
|
+
const hintPadLen = cardWidth - 6 - rawHint.length;
|
|
462
|
+
const hintPad = ' '.repeat(Math.max(0, hintPadLen));
|
|
463
|
+
|
|
464
|
+
hintLine += space + `${colors.gray}│${colors.reset} ${colors.gray}${hint}${colors.reset}${hintPad} ${colors.gray}│${colors.reset}`;
|
|
465
|
+
bottomBorder += space + colors.gray + '└' + '─'.repeat(cardWidth - 2) + '┘' + colors.reset;
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
console.log(topBorder);
|
|
469
|
+
console.log(labelValLine);
|
|
470
|
+
console.log(sepLine);
|
|
471
|
+
console.log(hintLine);
|
|
472
|
+
console.log(bottomBorder);
|
|
473
|
+
console.log();
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function cmdStatus() {
|
|
478
|
+
const userRes = await makeRequest('/auth/me');
|
|
479
|
+
const user = userRes.user;
|
|
480
|
+
|
|
481
|
+
let workspaceRes = null;
|
|
482
|
+
try {
|
|
483
|
+
workspaceRes = await makeRequest('/workspaces/current');
|
|
484
|
+
} catch (err) {
|
|
485
|
+
// Ignore / fallback
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (user.role === 'super_admin') {
|
|
489
|
+
try {
|
|
490
|
+
ui.stepInfo(`Fetching system metrics (Admin overview)...`);
|
|
491
|
+
const [overviewRes, serverRes] = await Promise.all([
|
|
492
|
+
makeRequest('/admin/overview').catch(() => null),
|
|
493
|
+
makeRequest('/admin/server').catch(() => null)
|
|
494
|
+
]);
|
|
495
|
+
|
|
496
|
+
if (overviewRes && overviewRes.success && serverRes && serverRes.success) {
|
|
497
|
+
const stats = overviewRes.stats || {};
|
|
498
|
+
const server = serverRes.server || {};
|
|
499
|
+
const byStatus = stats.projects_by_status || {};
|
|
500
|
+
const celery = stats.celery || {};
|
|
501
|
+
const workerOnline = (celery.workers ?? 0) > 0;
|
|
502
|
+
|
|
503
|
+
const activeBuildingProjects = (byStatus.building || byStatus.BUILDING || 0) + (byStatus.pending || byStatus.PENDING || 0);
|
|
504
|
+
const activeTasksCount = (celery.active && celery.active > 0) ? celery.active : activeBuildingProjects;
|
|
505
|
+
const pendingInQueue = stats.redis_queue_len ?? celery.queued ?? 0;
|
|
506
|
+
|
|
507
|
+
const cpuVal = server.cpu_percent ? `${Number(server.cpu_percent).toFixed(1)}%` : 'Normal';
|
|
508
|
+
const ramVal = server.memory ? `${Number(server.memory.used_percent).toFixed(1)}%` : 'Healthy';
|
|
509
|
+
const diskVal = server.disk ? `${Number(server.disk.used_percent).toFixed(1)}%` : 'Optimal';
|
|
510
|
+
|
|
511
|
+
const runCount = byStatus.running || byStatus.RUNNING || 0;
|
|
512
|
+
const buildCount = activeBuildingProjects;
|
|
513
|
+
const failCount = byStatus.failed || byStatus.FAILED || 0;
|
|
514
|
+
|
|
515
|
+
ui.step(`System Overview metrics retrieved`);
|
|
516
|
+
|
|
517
|
+
const adminCards = [
|
|
518
|
+
{ label: 'Users', value: stats.users ?? 0, hint: 'Total registered users' },
|
|
519
|
+
{ label: 'Databases', value: stats.databases ?? 0, hint: 'Total workspace databases' },
|
|
520
|
+
{
|
|
521
|
+
label: 'Projects',
|
|
522
|
+
value: stats.projects ?? 0,
|
|
523
|
+
hint: `${colors.green}${runCount} run${colors.reset} · ${colors.yellow}${buildCount} build${colors.reset} · ${colors.red}${failCount} fail${colors.reset}`
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
label: 'App Containers',
|
|
527
|
+
value: stats.running_containers ?? 0,
|
|
528
|
+
status: stats.docker_available ? 'online' : 'offline',
|
|
529
|
+
hint: stats.docker_available ? 'Active running containers' : 'Engine offline'
|
|
530
|
+
},
|
|
531
|
+
{ label: 'CPU Usage', value: cpuVal, hint: 'Host CPU load' },
|
|
532
|
+
{ label: 'RAM Usage', value: ramVal, hint: 'Host memory allocation' },
|
|
533
|
+
{ label: 'Disk Usage', value: diskVal, hint: 'Host storage capacity' },
|
|
534
|
+
{
|
|
535
|
+
label: 'Task Queue',
|
|
536
|
+
value: workerOnline ? 'Online' : 'Offline',
|
|
537
|
+
status: workerOnline ? 'online' : 'offline',
|
|
538
|
+
hint: `${activeTasksCount} active · ${pendingInQueue} pending`
|
|
539
|
+
}
|
|
540
|
+
];
|
|
541
|
+
|
|
542
|
+
drawCards(adminCards, 2);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
} catch (err) {
|
|
546
|
+
// Fall through to workspace status
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Fallback / Standard workspace view
|
|
551
|
+
if (workspaceRes && workspaceRes.success) {
|
|
552
|
+
const workspace = workspaceRes.workspace;
|
|
553
|
+
const usage = workspaceRes.usage;
|
|
554
|
+
const limits = workspaceRes.limits;
|
|
555
|
+
|
|
556
|
+
const projLimit = limits.max_projects || 'Unlimited';
|
|
557
|
+
const bwLimit = limits.max_bandwidth_bytes ? formatBytes(limits.max_bandwidth_bytes) : 'Unlimited';
|
|
558
|
+
const buildLimit = limits.max_build_minutes ? `${limits.max_build_minutes} mins` : 'Unlimited';
|
|
559
|
+
|
|
560
|
+
ui.step(`Workspace metrics retrieved`);
|
|
561
|
+
|
|
562
|
+
const workspaceCards = [
|
|
563
|
+
{ label: 'Workspace', value: workspace.name, hint: `Tier Plan: ${workspace.tier.toUpperCase()}` },
|
|
564
|
+
{ label: 'Projects Quota', value: `${usage.projects_count} / ${projLimit}`, hint: 'Active project slots' },
|
|
565
|
+
{ label: 'Bandwidth Limit', value: `${formatBytes(usage.bandwidth_bytes)} / ${bwLimit}`, hint: 'Monthly data usage' },
|
|
566
|
+
{ label: 'Build Duration', value: `${(usage.build_seconds / 60).toFixed(1)}m / ${buildLimit}`, hint: 'Monthly build duration' }
|
|
567
|
+
];
|
|
568
|
+
|
|
569
|
+
drawCards(workspaceCards, 2);
|
|
570
|
+
} else {
|
|
571
|
+
ui.stepError(`Could not retrieve status details.`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function cmdProjects() {
|
|
576
|
+
ui.stepInfo(`Fetching projects list...`);
|
|
577
|
+
const res = await makeRequest('/projects/');
|
|
578
|
+
const projects = res.projects || [];
|
|
579
|
+
|
|
580
|
+
if (projects.length === 0) {
|
|
581
|
+
ui.stepInfo('No projects found in active workspace.');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
ui.step(`Retrieved ${projects.length} project(s)`);
|
|
586
|
+
|
|
587
|
+
const headers = [
|
|
588
|
+
{ label: 'ID', pad: 4 },
|
|
589
|
+
{ label: 'Project Name', pad: 4 },
|
|
590
|
+
{ label: 'Status', pad: 4 },
|
|
591
|
+
{ label: 'Public URL', pad: 2 }
|
|
592
|
+
];
|
|
593
|
+
|
|
594
|
+
const rows = projects.map(p => [
|
|
595
|
+
String(p.id),
|
|
596
|
+
colors.bold + p.name + colors.reset,
|
|
597
|
+
ui.badge(p.status),
|
|
598
|
+
colors.brightCyan + p.url + colors.reset
|
|
599
|
+
]);
|
|
600
|
+
|
|
601
|
+
ui.table(headers, rows);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function cmdRepos() {
|
|
605
|
+
try {
|
|
606
|
+
ui.stepInfo(`Fetching connected GitHub repositories...`);
|
|
607
|
+
const res = await makeRequest('/auth/github/repos');
|
|
608
|
+
const repos = res.repos || [];
|
|
609
|
+
|
|
610
|
+
if (repos.length === 0) {
|
|
611
|
+
ui.stepInfo('No GitHub repositories found. Try connecting to GitHub first.');
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
ui.step(`Found ${repos.length} repository/repositories`);
|
|
616
|
+
|
|
617
|
+
const headers = [
|
|
618
|
+
{ label: 'Repository Name', pad: 4 },
|
|
619
|
+
{ label: 'Default Branch', pad: 4 },
|
|
620
|
+
{ label: 'Visibility', pad: 2 }
|
|
621
|
+
];
|
|
622
|
+
|
|
623
|
+
const rows = repos.map(r => [
|
|
624
|
+
colors.bold + r.full_name + colors.reset,
|
|
625
|
+
colors.gray + String(r.default_branch || 'main') + colors.reset,
|
|
626
|
+
r.private ? ui.badge('PRIVATE') : ui.badge('PUBLIC')
|
|
627
|
+
]);
|
|
628
|
+
|
|
629
|
+
ui.table(headers, rows);
|
|
630
|
+
} catch (err) {
|
|
631
|
+
ui.stepWarn(`Please connect your GitHub account via Settings in the Web Dashboard first.`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function cmdDeploy(type, args) {
|
|
636
|
+
if (type === 'git') {
|
|
637
|
+
const repoUrl = args[0];
|
|
638
|
+
if (!repoUrl) {
|
|
639
|
+
ui.stepError(`Please specify the Git repository URL.`);
|
|
640
|
+
console.log(` Usage: ${colors.green}rushdeploy deploy git <repo_url> [--branch <branch>] [--name <name>] [--type <type>]${colors.reset}`);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
let branch = 'main';
|
|
645
|
+
let name = extractRepoName(repoUrl);
|
|
646
|
+
let ptype = 'auto';
|
|
647
|
+
|
|
648
|
+
const branchIdx = args.indexOf('--branch');
|
|
649
|
+
if (branchIdx !== -1 && args[branchIdx + 1]) branch = args[branchIdx + 1];
|
|
650
|
+
|
|
651
|
+
const nameIdx = args.indexOf('--name');
|
|
652
|
+
if (nameIdx !== -1 && args[nameIdx + 1]) name = args[nameIdx + 1];
|
|
653
|
+
|
|
654
|
+
const typeIdx = args.indexOf('--type');
|
|
655
|
+
if (typeIdx !== -1 && args[typeIdx + 1]) ptype = args[typeIdx + 1];
|
|
656
|
+
|
|
657
|
+
ui.step(`Connecting to repository`);
|
|
658
|
+
ui.step(`Creating project ${name}`);
|
|
659
|
+
ui.step(`Triggering deployment pipeline for ${branch} branch`);
|
|
660
|
+
|
|
661
|
+
const res = await makeRequest('/projects/deploy', 'POST', {
|
|
662
|
+
name,
|
|
663
|
+
repo_url: repoUrl,
|
|
664
|
+
branch,
|
|
665
|
+
type: ptype
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
if (res.success) {
|
|
669
|
+
ui.step(`Deployment queued successfully!`);
|
|
670
|
+
ui.box([
|
|
671
|
+
{ label: 'Project', value: colors.bold + name + colors.reset },
|
|
672
|
+
{ label: 'State', value: ui.badge('QUEUED') },
|
|
673
|
+
{ label: 'Project ID', value: String(res.project_id) },
|
|
674
|
+
{ label: 'Deployment ID', value: String(res.deployment_id) },
|
|
675
|
+
{ label: 'Repository', value: colors.gray + repoUrl + colors.reset },
|
|
676
|
+
{ label: 'Public URL', value: colors.brightCyan + colors.bold + res.url + colors.reset }
|
|
677
|
+
]);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
} else if (type === 'repo') {
|
|
681
|
+
const ownerRepo = args[0];
|
|
682
|
+
if (!ownerRepo || !ownerRepo.includes('/')) {
|
|
683
|
+
ui.stepError(`Please specify the repository in 'owner/repo' format.`);
|
|
684
|
+
console.log(` Usage: ${colors.green}rushdeploy deploy repo <owner/repo> [--branch <branch>] [--name <name>] [--type <type>]${colors.reset}`);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const repoUrl = `https://github.com/${ownerRepo}.git`;
|
|
689
|
+
let branch = 'main';
|
|
690
|
+
let name = ownerRepo.split('/')[1];
|
|
691
|
+
let ptype = 'auto';
|
|
692
|
+
|
|
693
|
+
const branchIdx = args.indexOf('--branch');
|
|
694
|
+
if (branchIdx !== -1 && args[branchIdx + 1]) branch = args[branchIdx + 1];
|
|
695
|
+
|
|
696
|
+
const nameIdx = args.indexOf('--name');
|
|
697
|
+
if (nameIdx !== -1 && args[nameIdx + 1]) name = args[nameIdx + 1];
|
|
698
|
+
|
|
699
|
+
const typeIdx = args.indexOf('--type');
|
|
700
|
+
if (typeIdx !== -1 && args[typeIdx + 1]) ptype = args[typeIdx + 1];
|
|
701
|
+
|
|
702
|
+
ui.step(`Connecting to GitHub repository ${ownerRepo}`);
|
|
703
|
+
ui.step(`Creating project ${name}`);
|
|
704
|
+
ui.step(`Triggering deployment pipeline for ${branch} branch`);
|
|
705
|
+
|
|
706
|
+
const res = await makeRequest('/projects/deploy', 'POST', {
|
|
707
|
+
name,
|
|
708
|
+
repo_url: repoUrl,
|
|
709
|
+
branch,
|
|
710
|
+
type: ptype
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
if (res.success) {
|
|
714
|
+
ui.step(`Deployment queued successfully!`);
|
|
715
|
+
ui.box([
|
|
716
|
+
{ label: 'Project', value: colors.bold + name + colors.reset },
|
|
717
|
+
{ label: 'State', value: ui.badge('QUEUED') },
|
|
718
|
+
{ label: 'Project ID', value: String(res.project_id) },
|
|
719
|
+
{ label: 'Deployment ID', value: String(res.deployment_id) },
|
|
720
|
+
{ label: 'Repository', value: colors.gray + ownerRepo + colors.reset },
|
|
721
|
+
{ label: 'Public URL', value: colors.brightCyan + colors.bold + res.url + colors.reset }
|
|
722
|
+
]);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
} else if (type === 'wordpress') {
|
|
726
|
+
const name = args[0];
|
|
727
|
+
if (!name) {
|
|
728
|
+
ui.stepError(`Please specify the site/project name.`);
|
|
729
|
+
console.log(` Usage: ${colors.green}rushdeploy deploy wordpress <name> --email <email> [options]${colors.reset}`);
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const emailIdx = args.indexOf('--email');
|
|
734
|
+
if (emailIdx === -1 || !args[emailIdx + 1]) {
|
|
735
|
+
ui.stepError(`--email <email> option is required for WordPress deployment.`);
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
const adminEmail = args[emailIdx + 1];
|
|
739
|
+
|
|
740
|
+
let siteTitle = `${name} Site`;
|
|
741
|
+
const titleIdx = args.indexOf('--title');
|
|
742
|
+
if (titleIdx !== -1 && args[titleIdx + 1]) siteTitle = args[titleIdx + 1];
|
|
743
|
+
|
|
744
|
+
let adminUser = 'admin';
|
|
745
|
+
const userIdx = args.indexOf('--user');
|
|
746
|
+
if (userIdx !== -1 && args[userIdx + 1]) adminUser = args[userIdx + 1];
|
|
747
|
+
|
|
748
|
+
let adminPassword = null;
|
|
749
|
+
const passIdx = args.indexOf('--password');
|
|
750
|
+
if (passIdx !== -1 && args[passIdx + 1]) adminPassword = args[passIdx + 1];
|
|
751
|
+
|
|
752
|
+
ui.step(`Preparing WordPress environment for ${name}`);
|
|
753
|
+
ui.step(`Configuring database and web server container`);
|
|
754
|
+
ui.step(`Starting WordPress auto-installation`);
|
|
755
|
+
|
|
756
|
+
const res = await makeRequest('/projects/wordpress', 'POST', {
|
|
757
|
+
name,
|
|
758
|
+
admin_email: adminEmail,
|
|
759
|
+
site_title: siteTitle,
|
|
760
|
+
admin_user: adminUser,
|
|
761
|
+
admin_password: adminPassword
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
if (res.success) {
|
|
765
|
+
ui.step(`WordPress deployment started successfully!`);
|
|
766
|
+
ui.box([
|
|
767
|
+
{ label: 'Site Name', value: colors.bold + name + colors.reset },
|
|
768
|
+
{ label: 'State', value: ui.badge('BUILDING') },
|
|
769
|
+
{ label: 'Admin Email', value: adminEmail },
|
|
770
|
+
{ label: 'Project ID', value: String(res.project_id) },
|
|
771
|
+
{ label: 'Deployment ID', value: String(res.deployment_id) },
|
|
772
|
+
{ label: 'Public URL', value: colors.brightCyan + colors.bold + res.url + colors.reset }
|
|
773
|
+
]);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
} else {
|
|
777
|
+
ui.stepError(`Invalid deployment source type. Must be 'git', 'repo', or 'wordpress'.`);
|
|
778
|
+
console.log(`\nUsage:`);
|
|
779
|
+
console.log(` rushdeploy deploy git <repo_url> [options]`);
|
|
780
|
+
console.log(` rushdeploy deploy repo <owner/repo> [options]`);
|
|
781
|
+
console.log(` rushdeploy deploy wordpress <name> --email <email> [options]`);
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function resolveProjectId(idOrSlug) {
|
|
787
|
+
if (/^\d+$/.test(idOrSlug)) {
|
|
788
|
+
return parseInt(idOrSlug, 10);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const res = await makeRequest('/projects/');
|
|
792
|
+
const projects = res.projects || [];
|
|
793
|
+
const matched = projects.find(p => p.slug === idOrSlug || p.name.toLowerCase() === idOrSlug.toLowerCase());
|
|
794
|
+
if (matched) {
|
|
795
|
+
return matched.id;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
ui.stepError(`No project found matching name or slug "${idOrSlug}".`);
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function cmdProject(idOrSlug, subcommand, subArgs) {
|
|
803
|
+
const id = await resolveProjectId(idOrSlug);
|
|
804
|
+
|
|
805
|
+
switch (subcommand) {
|
|
806
|
+
case 'start': {
|
|
807
|
+
ui.stepInfo(`Starting project ${id}...`);
|
|
808
|
+
const res = await makeRequest(`/projects/${id}/start`, 'POST');
|
|
809
|
+
if (res.success) {
|
|
810
|
+
ui.step(`Project container started successfully.`);
|
|
811
|
+
}
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
case 'stop': {
|
|
815
|
+
ui.stepInfo(`Stopping project ${id}...`);
|
|
816
|
+
const res = await makeRequest(`/projects/${id}/stop`, 'POST');
|
|
817
|
+
if (res.success) {
|
|
818
|
+
ui.step(`Project container stopped successfully.`);
|
|
819
|
+
}
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
case 'redeploy': {
|
|
823
|
+
ui.stepInfo(`Triggering project redeployment...`);
|
|
824
|
+
const res = await makeRequest(`/projects/${id}/redeploy`, 'POST');
|
|
825
|
+
if (res.success) {
|
|
826
|
+
ui.step(`Redeployment triggered successfully.`);
|
|
827
|
+
ui.stepInfo(`Deployment ID: ${res.deployment_id}`);
|
|
828
|
+
}
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
case 'rollback': {
|
|
832
|
+
let targetDeploymentId = subArgs[0];
|
|
833
|
+
if (!targetDeploymentId) {
|
|
834
|
+
const projRes = await makeRequest(`/projects/${id}`);
|
|
835
|
+
const deployments = projRes.deployments || [];
|
|
836
|
+
const successful = deployments.filter(d => d.status === 'success');
|
|
837
|
+
if (successful.length === 0) {
|
|
838
|
+
ui.stepError(`No successful past deployments found to rollback to.`);
|
|
839
|
+
ui.stepInfo(`Specify a target deployment ID: rushdeploy project ${idOrSlug} rollback <deployment_id>`);
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
842
|
+
targetDeploymentId = successful[0].id;
|
|
843
|
+
ui.stepInfo(`No deployment ID specified. Auto-selected latest successful build #${targetDeploymentId}.`);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
ui.stepInfo(`Triggering rollback to deployment #${targetDeploymentId}...`);
|
|
847
|
+
const res = await makeRequest(`/projects/${id}/rollback/${targetDeploymentId}`, 'POST');
|
|
848
|
+
if (res.success) {
|
|
849
|
+
ui.step(`Rollback queued successfully to build #${targetDeploymentId}.`);
|
|
850
|
+
ui.stepInfo(`New Deployment ID: ${res.deployment_id}`);
|
|
851
|
+
}
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
case 'visit': {
|
|
855
|
+
const res = await makeRequest(`/projects/${id}`);
|
|
856
|
+
const url = res.project.url;
|
|
857
|
+
openBrowser(url);
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
case 'stats':
|
|
861
|
+
case 'metrics': {
|
|
862
|
+
const statsRes = await makeRequest(`/projects/${id}/stats`);
|
|
863
|
+
const info = await makeRequest(`/projects/${id}/status`);
|
|
864
|
+
const stats = statsRes.stats;
|
|
865
|
+
|
|
866
|
+
ui.stepInfo(`Resource metrics for Project ${id}`);
|
|
867
|
+
|
|
868
|
+
const fields = [
|
|
869
|
+
{ label: 'Current Status', value: ui.badge(info.status) }
|
|
870
|
+
];
|
|
871
|
+
|
|
872
|
+
if (stats) {
|
|
873
|
+
const cpuPct = stats.cpu_usage !== undefined ? stats.cpu_usage : 0;
|
|
874
|
+
const rawUsage = stats.memory_usage ?? 0;
|
|
875
|
+
const rawLimit = stats.memory_limit || stats.memory_limit_mb || 0;
|
|
876
|
+
|
|
877
|
+
const rawUsageMb = rawUsage < 1000000 ? rawUsage : rawUsage / (1024 * 1024);
|
|
878
|
+
const rawLimitMb = rawLimit > 0 ? (rawLimit < 1000000 ? rawLimit : rawLimit / (1024 * 1024)) : 0;
|
|
879
|
+
|
|
880
|
+
const memUsed = formatMemory(rawUsageMb);
|
|
881
|
+
const memLimit = rawLimitMb > 0 ? formatMemory(rawLimitMb) : 'Unlimited';
|
|
882
|
+
const memPct = rawLimitMb > 0 ? (rawUsageMb / rawLimitMb) * 100 : (stats.memory_percent || 0);
|
|
883
|
+
|
|
884
|
+
fields.push(
|
|
885
|
+
{ label: 'CPU Load', value: ui.progressBar(cpuPct) },
|
|
886
|
+
{ label: 'Memory Load', value: `${memUsed} / ${memLimit} (${ui.progressBar(memPct)})` }
|
|
887
|
+
);
|
|
888
|
+
} else {
|
|
889
|
+
fields.push({ label: 'Metrics Status', value: colors.gray + 'Container metrics inactive or stopped' + colors.reset });
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
ui.box(fields, { title: `METRICS: PROJECT ${id}` });
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
case 'actions': {
|
|
896
|
+
ui.stepInfo(`Fetching recent deployment actions for Project ${id}...`);
|
|
897
|
+
const res = await makeRequest(`/projects/${id}`);
|
|
898
|
+
const deployments = res.deployments || [];
|
|
899
|
+
|
|
900
|
+
if (deployments.length === 0) {
|
|
901
|
+
ui.stepInfo('No recent deployments found.');
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const headers = [
|
|
906
|
+
{ label: 'ID', pad: 4 },
|
|
907
|
+
{ label: 'Status', pad: 4 },
|
|
908
|
+
{ label: 'Branch', pad: 4 },
|
|
909
|
+
{ label: 'Duration', pad: 4 },
|
|
910
|
+
{ label: 'Created At', pad: 2 }
|
|
911
|
+
];
|
|
912
|
+
|
|
913
|
+
const rows = deployments.slice(0, 10).map(d => [
|
|
914
|
+
String(d.id),
|
|
915
|
+
ui.badge(d.status),
|
|
916
|
+
colors.gray + (d.branch || 'main') + colors.reset,
|
|
917
|
+
d.duration_ms ? `${(d.duration_ms / 1000).toFixed(1)}s` : 'N/A',
|
|
918
|
+
formatDate(d.created_at)
|
|
919
|
+
]);
|
|
920
|
+
|
|
921
|
+
ui.table(headers, rows);
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
case 'autoscale': {
|
|
925
|
+
const enableArg = subArgs[0];
|
|
926
|
+
if (!enableArg || (enableArg !== 'on' && enableArg !== 'off')) {
|
|
927
|
+
const res = await makeRequest(`/projects/${id}`);
|
|
928
|
+
const p = res.project;
|
|
929
|
+
ui.box([
|
|
930
|
+
{ label: 'Autoscale State', value: p.autoscale_enabled ? ui.badge('ON') : ui.badge('OFF') },
|
|
931
|
+
{ label: 'Min/Max Replicas', value: `${p.min_replicas} (min) / ${p.max_replicas} (max)` },
|
|
932
|
+
{ label: 'CPU Trigger', value: `${p.cpu_threshold_percent}% CPU load` }
|
|
933
|
+
], { title: `AUTOSCALE: PROJECT ${id}` });
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const enabled = (enableArg === 'on');
|
|
938
|
+
let min_replicas = 1;
|
|
939
|
+
let max_replicas = 5;
|
|
940
|
+
let cpu_threshold_percent = 80.0;
|
|
941
|
+
|
|
942
|
+
const minIdx = subArgs.indexOf('--min');
|
|
943
|
+
if (minIdx !== -1 && subArgs[minIdx + 1]) min_replicas = parseInt(subArgs[minIdx + 1], 10);
|
|
944
|
+
|
|
945
|
+
const maxIdx = subArgs.indexOf('--max');
|
|
946
|
+
if (maxIdx !== -1 && subArgs[maxIdx + 1]) max_replicas = parseInt(subArgs[maxIdx + 1], 10);
|
|
947
|
+
|
|
948
|
+
const cpuIdx = subArgs.indexOf('--cpu');
|
|
949
|
+
if (cpuIdx !== -1 && subArgs[cpuIdx + 1]) cpu_threshold_percent = parseFloat(subArgs[cpuIdx + 1]);
|
|
950
|
+
|
|
951
|
+
ui.stepInfo(`Updating autoscale settings...`);
|
|
952
|
+
const res = await makeRequest(`/projects/${id}/autoscale`, 'PUT', {
|
|
953
|
+
autoscale_enabled: enabled,
|
|
954
|
+
min_replicas,
|
|
955
|
+
max_replicas,
|
|
956
|
+
cpu_threshold_percent
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
if (res.success) {
|
|
960
|
+
ui.step(`Autoscale configuration updated successfully.`);
|
|
961
|
+
ui.box([
|
|
962
|
+
{ label: 'Autoscale State', value: res.autoscale.autoscale_enabled ? ui.badge('ON') : ui.badge('OFF') },
|
|
963
|
+
{ label: 'Replica Range', value: `${res.autoscale.min_replicas} - ${res.autoscale.max_replicas}` },
|
|
964
|
+
{ label: 'Trigger Threshold', value: `${res.autoscale.cpu_threshold_percent}% CPU` }
|
|
965
|
+
]);
|
|
966
|
+
}
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
case 'autodeploy': {
|
|
970
|
+
const enableArg = subArgs[0];
|
|
971
|
+
if (!enableArg || (enableArg !== 'on' && enableArg !== 'off')) {
|
|
972
|
+
const res = await makeRequest(`/projects/${id}`);
|
|
973
|
+
ui.stepInfo(`Auto-deploy is currently: ${res.project.auto_deploy ? ui.badge('ON') : ui.badge('OFF')}`);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (enableArg === 'on') {
|
|
978
|
+
ui.stepInfo(`Enabling auto-deploy (webhook configuration)...`);
|
|
979
|
+
const res = await makeRequest(`/projects/${id}/auto-deploy/enable`, 'POST');
|
|
980
|
+
if (res.success) {
|
|
981
|
+
ui.step(`GitHub Auto-Deploy enabled.`);
|
|
982
|
+
}
|
|
983
|
+
} else {
|
|
984
|
+
ui.stepInfo(`Disabling auto-deploy...`);
|
|
985
|
+
const res = await makeRequest(`/projects/${id}/auto-deploy/disable`, 'POST');
|
|
986
|
+
if (res.success) {
|
|
987
|
+
ui.step(`GitHub Auto-Deploy disabled.`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
case 'logs': {
|
|
993
|
+
let linesLimit = 50;
|
|
994
|
+
const linesIdx = subArgs.indexOf('--lines');
|
|
995
|
+
if (linesIdx !== -1 && subArgs[linesIdx + 1]) {
|
|
996
|
+
linesLimit = parseInt(subArgs[linesIdx + 1], 10);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const res = await makeRequest(`/projects/${id}/logs`);
|
|
1000
|
+
const logLines = res.lines || [];
|
|
1001
|
+
ui.stepInfo(`Showing last ${Math.min(linesLimit, logLines.length)} log lines for project ${id}:`);
|
|
1002
|
+
console.log(colors.gray + '─'.repeat(80) + colors.reset);
|
|
1003
|
+
logLines.slice(-linesLimit).forEach(ln => {
|
|
1004
|
+
let levelColor = colors.reset;
|
|
1005
|
+
if (ln.level === 'error') levelColor = colors.red;
|
|
1006
|
+
console.log(`[${colors.gray}${formatDate(ln.timestamp)}${colors.reset}] ${levelColor}${ln.message}${colors.reset}`);
|
|
1007
|
+
});
|
|
1008
|
+
console.log();
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
case 'env': {
|
|
1012
|
+
const action = subArgs[0];
|
|
1013
|
+
|
|
1014
|
+
if (!action) {
|
|
1015
|
+
const res = await makeRequest(`/projects/${id}/env`);
|
|
1016
|
+
const env = res.env || {};
|
|
1017
|
+
ui.stepInfo(`Environment Variables for Project ${id}`);
|
|
1018
|
+
const headers = [
|
|
1019
|
+
{ label: 'Key', pad: 4 },
|
|
1020
|
+
{ label: 'Value', pad: 2 }
|
|
1021
|
+
];
|
|
1022
|
+
const rows = Object.entries(env).map(([k, v]) => [
|
|
1023
|
+
colors.green + colors.bold + k + colors.reset,
|
|
1024
|
+
colors.gray + String(v) + colors.reset
|
|
1025
|
+
]);
|
|
1026
|
+
ui.table(headers, rows);
|
|
1027
|
+
} else if (action === 'set') {
|
|
1028
|
+
const setPairs = subArgs.slice(1);
|
|
1029
|
+
if (setPairs.length === 0) {
|
|
1030
|
+
ui.stepError(`Please specify KEY=VALUE pairs to set.`);
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const currentRes = await makeRequest(`/projects/${id}/env`);
|
|
1035
|
+
const env = currentRes.env || {};
|
|
1036
|
+
|
|
1037
|
+
setPairs.forEach(pair => {
|
|
1038
|
+
const parts = pair.split('=');
|
|
1039
|
+
const k = parts[0];
|
|
1040
|
+
const v = parts.slice(1).join('=');
|
|
1041
|
+
if (k) env[k] = v;
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
const res = await makeRequest(`/projects/${id}/env`, 'PUT', { env });
|
|
1045
|
+
if (res.success) {
|
|
1046
|
+
ui.step(`Environment variables updated successfully.`);
|
|
1047
|
+
ui.stepInfo(`Run 'rushdeploy project ${idOrSlug} redeploy' to apply changes.`);
|
|
1048
|
+
}
|
|
1049
|
+
} else if (action === 'remove') {
|
|
1050
|
+
const removeKeys = subArgs.slice(1);
|
|
1051
|
+
if (removeKeys.length === 0) {
|
|
1052
|
+
ui.stepError(`Please specify keys to remove.`);
|
|
1053
|
+
process.exit(1);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const currentRes = await makeRequest(`/projects/${id}/env`);
|
|
1057
|
+
const env = currentRes.env || {};
|
|
1058
|
+
|
|
1059
|
+
removeKeys.forEach(k => {
|
|
1060
|
+
delete env[k];
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
const res = await makeRequest(`/projects/${id}/env`, 'PUT', { env });
|
|
1064
|
+
if (res.success) {
|
|
1065
|
+
ui.step(`Environment variables updated successfully.`);
|
|
1066
|
+
ui.stepInfo(`Run 'rushdeploy project ${idOrSlug} redeploy' to apply changes.`);
|
|
1067
|
+
}
|
|
1068
|
+
} else {
|
|
1069
|
+
ui.stepError(`Unknown env subcommand: "${action}". Use "set" or "remove".`);
|
|
1070
|
+
}
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
default: {
|
|
1074
|
+
const [res, statsRes] = await Promise.all([
|
|
1075
|
+
makeRequest(`/projects/${id}`),
|
|
1076
|
+
makeRequest(`/projects/${id}/stats`).catch(() => null)
|
|
1077
|
+
]);
|
|
1078
|
+
const p = res.project;
|
|
1079
|
+
const stats = statsRes ? statsRes.stats : null;
|
|
1080
|
+
|
|
1081
|
+
const fields = [
|
|
1082
|
+
{ label: 'Project Name', value: colors.bold + p.name + colors.reset },
|
|
1083
|
+
{ label: 'ID / Slug', value: `${p.id} (${p.slug})` },
|
|
1084
|
+
{ label: 'State', value: ui.badge(p.status) },
|
|
1085
|
+
{ label: 'Public URL', value: colors.brightCyan + colors.bold + p.url + colors.reset },
|
|
1086
|
+
{ label: 'Repository', value: colors.gray + `${p.repo_url} (${p.branch})` + colors.reset }
|
|
1087
|
+
];
|
|
1088
|
+
|
|
1089
|
+
if (stats && p.status === 'running') {
|
|
1090
|
+
const cpuPct = stats.cpu_usage !== undefined ? stats.cpu_usage : 0;
|
|
1091
|
+
const rawUsage = stats.memory_usage ?? 0;
|
|
1092
|
+
const rawLimit = stats.memory_limit || stats.memory_limit_mb || p.memory_mb || 0;
|
|
1093
|
+
|
|
1094
|
+
const rawUsageMb = rawUsage < 1000000 ? rawUsage : rawUsage / (1024 * 1024);
|
|
1095
|
+
const rawLimitMb = rawLimit > 0 ? (rawLimit < 1000000 ? rawLimit : rawLimit / (1024 * 1024)) : (p.memory_mb || 512);
|
|
1096
|
+
|
|
1097
|
+
const memUsed = formatMemory(rawUsageMb);
|
|
1098
|
+
const memLimit = formatMemory(rawLimitMb);
|
|
1099
|
+
const memPct = rawLimitMb > 0 ? (rawUsageMb / rawLimitMb) * 100 : (stats.memory_percent || 0);
|
|
1100
|
+
|
|
1101
|
+
fields.push(
|
|
1102
|
+
{ label: 'CPU Usage', value: ui.progressBar(cpuPct) },
|
|
1103
|
+
{ label: 'Memory Usage', value: `${memUsed} / ${memLimit} (${ui.progressBar(memPct)})` }
|
|
1104
|
+
);
|
|
1105
|
+
} else {
|
|
1106
|
+
fields.push(
|
|
1107
|
+
{ label: 'Resource Alloc', value: `${p.memory_mb} MB RAM · ${p.cpu_cores} Core(s)` }
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
fields.push(
|
|
1112
|
+
{ label: 'Autoscale', value: p.autoscale_enabled ? `${ui.badge('ENABLED')} (${p.min_replicas}-${p.max_replicas} reps)` : ui.badge('DISABLED') },
|
|
1113
|
+
{ label: 'Created At', value: formatDate(p.created_at) }
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
ui.box(fields, { title: p.name.toUpperCase() });
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
async function cmdWorkspace() {
|
|
1122
|
+
ui.stepInfo(`Fetching workspace quotas & usage metrics...`);
|
|
1123
|
+
const wsRes = await makeRequest('/workspaces/current');
|
|
1124
|
+
const workspace = wsRes.workspace;
|
|
1125
|
+
const usage = wsRes.usage;
|
|
1126
|
+
const limits = wsRes.limits;
|
|
1127
|
+
|
|
1128
|
+
const projLimit = limits.max_projects || 'Unlimited';
|
|
1129
|
+
const depLimit = limits.max_deployments || 'Unlimited';
|
|
1130
|
+
const bwLimit = limits.max_bandwidth_bytes ? formatBytes(limits.max_bandwidth_bytes) : 'Unlimited';
|
|
1131
|
+
const buildLimit = limits.max_build_minutes ? `${limits.max_build_minutes} mins` : 'Unlimited';
|
|
1132
|
+
|
|
1133
|
+
ui.step(`Retrieved workspace quotas`);
|
|
1134
|
+
|
|
1135
|
+
ui.box([
|
|
1136
|
+
{ label: 'Workspace', value: `${colors.bold}${workspace.name}${colors.reset} (${workspace.slug})` },
|
|
1137
|
+
{ label: 'Tier Plan', value: colors.magenta + colors.bold + workspace.tier.toUpperCase() + colors.reset },
|
|
1138
|
+
{ label: 'Billing Period', value: colors.gray + usage.billing_period + colors.reset },
|
|
1139
|
+
{ label: 'Projects Slot', value: `${usage.projects_count} / ${projLimit}` },
|
|
1140
|
+
{ label: 'Deployments', value: `${usage.deployments_count} / ${depLimit}` },
|
|
1141
|
+
{ label: 'Bandwidth Used', value: `${formatBytes(usage.bandwidth_bytes)} / ${bwLimit}` },
|
|
1142
|
+
{ label: 'Build Duration', value: `${(usage.build_seconds / 60).toFixed(1)} mins / ${buildLimit}` }
|
|
1143
|
+
], { title: 'WORKSPACE QUOTAS' });
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function printHelp() {
|
|
1147
|
+
console.log(`
|
|
1148
|
+
${colors.bold}${colors.brightWhite}RushDeploy CLI${colors.reset} ${colors.gray}v1.0.4${colors.reset} - Modern PaaS Command Line Interface
|
|
1149
|
+
|
|
1150
|
+
${colors.bold}USAGE:${colors.reset}
|
|
1151
|
+
${colors.brightCyan}rushdeploy${colors.reset} <command> [arguments] [options]
|
|
1152
|
+
|
|
1153
|
+
${colors.bold}GLOBAL COMMANDS:${colors.reset}
|
|
1154
|
+
${colors.green}login <token>${colors.reset} Authenticate with your access token
|
|
1155
|
+
${colors.green}logout${colors.reset} Clear your stored CLI session token
|
|
1156
|
+
${colors.green}whoami${colors.reset} Display active profile details
|
|
1157
|
+
${colors.green}plans${colors.reset} List subscription plans and limits
|
|
1158
|
+
${colors.green}status${colors.reset} Show system & workspace status metrics
|
|
1159
|
+
${colors.green}projects${colors.reset} List all projects in current workspace
|
|
1160
|
+
${colors.green}repos${colors.reset} List repositories from connected GitHub account
|
|
1161
|
+
${colors.green}workspace${colors.reset} Display workspace resource usage & quotas
|
|
1162
|
+
${colors.green}help${colors.reset} Display this help manual
|
|
1163
|
+
|
|
1164
|
+
${colors.bold}DEPLOYMENT COMMANDS:${colors.reset}
|
|
1165
|
+
${colors.green}deploy git <repo_url>${colors.reset} Deploy a project from a Git repository URL
|
|
1166
|
+
Options: ${colors.gray}--name <name> --branch <branch> --type <type>${colors.reset}
|
|
1167
|
+
${colors.green}deploy repo <owner/repo>${colors.reset} Deploy from connected GitHub account
|
|
1168
|
+
Options: ${colors.gray}--name <name> --branch <branch> --type <type>${colors.reset}
|
|
1169
|
+
${colors.green}deploy wordpress <name>${colors.reset} Deploy a new one-click WordPress site
|
|
1170
|
+
Options: ${colors.gray}--email <e> [--title <t>] [--user <u>] [--password <p>]${colors.reset}
|
|
1171
|
+
|
|
1172
|
+
${colors.bold}PROJECT CONTROL:${colors.reset}
|
|
1173
|
+
${colors.green}project <slug|id>${colors.reset} Show project overview
|
|
1174
|
+
${colors.green}project <slug|id> start${colors.reset} Start project container
|
|
1175
|
+
${colors.green}project <slug|id> stop${colors.reset} Stop project container
|
|
1176
|
+
${colors.green}project <slug|id> redeploy${colors.reset} Rebuild and redeploy project
|
|
1177
|
+
${colors.green}project <slug|id> rollback [dep_id]${colors.reset} Rollback project to previous deployment build
|
|
1178
|
+
${colors.green}project <slug|id> visit${colors.reset} Open project in default browser
|
|
1179
|
+
${colors.green}project <slug|id> stats${colors.reset} Show container CPU & RAM metrics meter
|
|
1180
|
+
${colors.green}project <slug|id> actions${colors.reset} Show recent deployment history
|
|
1181
|
+
${colors.green}project <slug|id> logs${colors.reset} View project container logs (${colors.gray}--lines <N>${colors.reset})
|
|
1182
|
+
|
|
1183
|
+
${colors.bold}PROJECT CONFIGURATION:${colors.reset}
|
|
1184
|
+
${colors.green}project <slug|id> autoscale${colors.reset} Show auto-scaling configuration
|
|
1185
|
+
${colors.green}project <slug|id> autoscale <on|off>${colors.reset} Configure auto-scaling limits (${colors.gray}--min <N> --max <N> --cpu <%>${colors.reset})
|
|
1186
|
+
${colors.green}project <slug|id> autodeploy <on|off>${colors.reset} Toggle auto-deploy webhooks
|
|
1187
|
+
${colors.green}project <slug|id> env${colors.reset} List environment variables
|
|
1188
|
+
${colors.green}project <slug|id> env set K=V${colors.reset} Set environment variable(s)
|
|
1189
|
+
${colors.green}project <slug|id> env remove K${colors.reset} Remove environment variable(s)
|
|
1190
|
+
`);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async function main() {
|
|
1194
|
+
const args = process.argv.slice(2);
|
|
1195
|
+
const command = args[0];
|
|
1196
|
+
|
|
1197
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
1198
|
+
printHelp();
|
|
1199
|
+
process.exit(0);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
switch (command) {
|
|
1203
|
+
case 'login': {
|
|
1204
|
+
const token = args[1];
|
|
1205
|
+
if (!token) {
|
|
1206
|
+
ui.stepError(`Please provide your access token.`);
|
|
1207
|
+
console.log(` Usage: ${colors.green}rushdeploy login <token> [--server <url>]${colors.reset}`);
|
|
1208
|
+
process.exit(1);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
let server = null;
|
|
1212
|
+
const serverIndex = args.indexOf('--server');
|
|
1213
|
+
if (serverIndex !== -1 && args[serverIndex + 1]) {
|
|
1214
|
+
server = args[serverIndex + 1];
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
await cmdLogin(token, server);
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
case 'logout':
|
|
1221
|
+
cmdLogout();
|
|
1222
|
+
break;
|
|
1223
|
+
case 'whoami':
|
|
1224
|
+
await cmdWhoami();
|
|
1225
|
+
break;
|
|
1226
|
+
case 'plans':
|
|
1227
|
+
await cmdPlans();
|
|
1228
|
+
break;
|
|
1229
|
+
case 'status':
|
|
1230
|
+
await cmdStatus();
|
|
1231
|
+
break;
|
|
1232
|
+
case 'projects':
|
|
1233
|
+
await cmdProjects();
|
|
1234
|
+
break;
|
|
1235
|
+
case 'repos':
|
|
1236
|
+
await cmdRepos();
|
|
1237
|
+
break;
|
|
1238
|
+
case 'deploy': {
|
|
1239
|
+
const type = args[1];
|
|
1240
|
+
const deployArgs = args.slice(2);
|
|
1241
|
+
await cmdDeploy(type, deployArgs);
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
case 'project': {
|
|
1245
|
+
const idOrSlug = args[1];
|
|
1246
|
+
if (!idOrSlug) {
|
|
1247
|
+
ui.stepError(`Please provide a project ID or slug.`);
|
|
1248
|
+
console.log(` Usage: ${colors.green}rushdeploy project <slug|id> [subcommand] [arguments]${colors.reset}`);
|
|
1249
|
+
process.exit(1);
|
|
1250
|
+
}
|
|
1251
|
+
const subcommand = args[2];
|
|
1252
|
+
const subArgs = args.slice(3);
|
|
1253
|
+
await cmdProject(idOrSlug, subcommand, subArgs);
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
case 'workspace':
|
|
1257
|
+
await cmdWorkspace();
|
|
1258
|
+
break;
|
|
1259
|
+
default:
|
|
1260
|
+
ui.stepError(`Unknown command: "${command}"`);
|
|
1261
|
+
printHelp();
|
|
1262
|
+
process.exit(1);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
main().catch(err => {
|
|
1267
|
+
ui.stepError(`Fatal Error: ${err.message}`);
|
|
1268
|
+
process.exit(1);
|
|
1269
|
+
});
|