rushdeploy 1.0.1 → 1.0.4

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.
Files changed (2) hide show
  1. package/bin/index.js +478 -256
  2. package/package.json +6 -2
package/bin/index.js CHANGED
@@ -28,18 +28,158 @@ function getServerUrl() {
28
28
  return DEFAULT_SERVER;
29
29
  }
30
30
 
31
- // ANSI terminal color helpers
31
+ // ANSI terminal color tokens & UI utilities
32
32
  const colors = {
33
33
  reset: '\x1b[0m',
34
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',
35
41
  green: '\x1b[32m',
42
+ brightGreen: '\x1b[92m',
36
43
  red: '\x1b[31m',
37
- cyan: '\x1b[36m',
44
+ brightRed: '\x1b[91m',
38
45
  yellow: '\x1b[33m',
46
+ brightYellow: '\x1b[93m',
47
+ cyan: '\x1b[36m',
48
+ brightCyan: '\x1b[96m',
39
49
  blue: '\x1b[34m',
40
50
  magenta: '\x1b[35m'
41
51
  };
42
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
+
43
183
  function loadConfig() {
44
184
  if (!fs.existsSync(CONFIG_FILE)) return null;
45
185
  try {
@@ -58,7 +198,7 @@ function saveConfig(server, token) {
58
198
  fs.writeFileSync(CONFIG_FILE, JSON.stringify({ server: cleanServer, token }, null, 2), 'utf8');
59
199
  return true;
60
200
  } catch (err) {
61
- console.error(`${colors.red}Error saving configuration:${colors.reset}`, err.message);
201
+ ui.stepError(`Error saving configuration: ${err.message}`);
62
202
  return false;
63
203
  }
64
204
  }
@@ -70,7 +210,7 @@ function makeRequest(apiPath, method = 'GET', data = null) {
70
210
  const isPlansPath = apiPath.endsWith('/subscriptions/plans') && method === 'GET';
71
211
 
72
212
  if (!config && !isLoginPath && !isPlansPath) {
73
- console.error(`${colors.red}Error: You are not logged in. Run 'rushdeploy login <token>' first.${colors.reset}`);
213
+ ui.stepError(`You are not logged in. Run 'rushdeploy login <token>' first.`);
74
214
  process.exit(1);
75
215
  }
76
216
 
@@ -117,10 +257,13 @@ function makeRequest(apiPath, method = 'GET', data = null) {
117
257
  }
118
258
 
119
259
  if (res.statusCode === 401) {
120
- console.error(`${colors.red}Error (401): Unauthorized. Your token may have expired or been revoked.${colors.reset}`);
121
- console.error('Please log in again using `rushdeploy login <token>`');
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.`);
122
265
  } else {
123
- console.error(`${colors.red}Error (${res.statusCode}): ${errorMsg}${colors.reset}`);
266
+ ui.stepError(`Error (${res.statusCode}): ${errorMsg}`);
124
267
  }
125
268
  process.exit(1);
126
269
  }
@@ -128,8 +271,8 @@ function makeRequest(apiPath, method = 'GET', data = null) {
128
271
  });
129
272
 
130
273
  req.on('error', (err) => {
131
- console.error(`${colors.red}Connection Error: Unable to connect to server at ${serverUrl}.${colors.reset}`);
132
- console.error('Details:', err.message);
274
+ ui.stepError(`Connection Error: Unable to connect to server at ${serverUrl}`);
275
+ console.log(` Details: ${colors.gray}${err.message}${colors.reset}`);
133
276
  process.exit(1);
134
277
  });
135
278
 
@@ -149,6 +292,14 @@ function formatBytes(bytes) {
149
292
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
150
293
  }
151
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
+
152
303
  function formatDate(isoStr) {
153
304
  if (!isoStr) return 'Never';
154
305
  try {
@@ -174,9 +325,9 @@ function openBrowser(url) {
174
325
  }
175
326
  exec(command, (err) => {
176
327
  if (err) {
177
- console.error(`${colors.red}Failed to open browser:${colors.reset}`, err.message);
328
+ ui.stepError(`Failed to open browser: ${err.message}`);
178
329
  } else {
179
- console.log(`${colors.green}Opening browser to: ${url}${colors.reset}`);
330
+ ui.step(`Opening browser: ${colors.brightCyan}${url}${colors.reset}`);
180
331
  }
181
332
  });
182
333
  }
@@ -192,22 +343,28 @@ function extractRepoName(url) {
192
343
  }
193
344
 
194
345
  async function cmdLogin(token, serverArg) {
195
- const server = serverArg || 'https://rushdeploy.com';
196
- console.log(`${colors.cyan}Connecting to ${server}...${colors.reset}`);
346
+ const server = serverArg || process.env.RUSHDEPLOY_SERVER || DEFAULT_SERVER;
347
+ ui.stepInfo(`Connecting to ${colors.brightWhite}${server}${colors.reset}...`);
197
348
 
198
349
  saveConfig(server, token);
199
350
 
200
351
  try {
352
+ ui.stepInfo(`Validating authentication token...`);
201
353
  const res = await makeRequest('/auth/me');
202
354
  if (res.success) {
203
355
  const user = res.user;
204
- console.log(`\n${colors.green}${colors.bold}✓ Login successful!${colors.reset}`);
205
- console.log(`Account: ${colors.bold}${user.name} (${user.email})${colors.reset}`);
206
- console.log(`Role: ${user.role.toUpperCase()}`);
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
+ ]);
207
364
  }
208
365
  } catch (err) {
209
366
  if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
210
- console.error(`${colors.red}Authentication failed. Check your token or server URL.${colors.reset}`);
367
+ ui.stepError(`Authentication failed. Check your token or server URL.`);
211
368
  process.exit(1);
212
369
  }
213
370
  }
@@ -215,56 +372,52 @@ async function cmdLogin(token, serverArg) {
215
372
  function cmdLogout() {
216
373
  if (fs.existsSync(CONFIG_FILE)) {
217
374
  fs.unlinkSync(CONFIG_FILE);
218
- console.log(`${colors.green}Successfully logged out. Local token configuration cleared.${colors.reset}`);
375
+ ui.step(`Successfully logged out. Local token configuration cleared.`);
219
376
  } else {
220
- console.log('You are already logged out.');
377
+ ui.stepInfo(`You are already logged out.`);
221
378
  }
222
379
  }
223
380
 
224
381
  async function cmdWhoami() {
225
382
  const config = loadConfig();
226
383
  if (!config) {
227
- console.log('Not logged in.');
384
+ ui.stepWarn(`Not logged in. Run 'rushdeploy login <token>' to log in.`);
228
385
  process.exit(0);
229
386
  }
230
387
 
231
388
  const res = await makeRequest('/auth/me');
232
389
  const user = res.user;
233
- console.log(`\n${colors.bold}Profile Details:${colors.reset}`);
234
- console.log(` Name: ${colors.bold}${user.name}${colors.reset}`);
235
- console.log(` Email: ${user.email}`);
236
- console.log(` Role: ${user.role.toUpperCase()}`);
237
- console.log(` Server: ${SERVER_URL}`);
238
- console.log();
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' });
239
396
  }
240
397
 
241
398
  async function cmdPlans() {
399
+ ui.stepInfo(`Fetching RushDeploy hosting plans...`);
242
400
  const res = await makeRequest('/subscriptions/plans');
243
401
  const plans = res.plans || [];
244
402
 
245
- console.log(`\n${colors.cyan}${colors.bold}====================================================${colors.reset}`);
246
- console.log(`${colors.bold} RUSHDEPLOY HOSTING PLANS ${colors.reset}`);
247
- console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}`);
248
-
249
403
  plans.forEach(p => {
250
404
  const price = p.base_price_monthly ? `$${p.base_price_monthly}/mo` : 'Free';
251
- console.log(`${colors.bold}${p.name.toUpperCase()}${colors.reset} - ${colors.green}${price}${colors.reset}`);
252
- console.log(`Description: ${p.description || 'No description'}`);
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
+ ];
253
409
  if (p.limits) {
254
- console.log(`Limits:`);
255
- console.log(` - Projects: ${p.limits.max_projects || 'Unlimited'}`);
256
- console.log(` - Deployments: ${p.limits.max_deployments || 'Unlimited'}`);
257
- console.log(` - Bandwidth Limit: ${p.limits.max_bandwidth_bytes ? formatBytes(p.limits.max_bandwidth_bytes) : 'Unlimited'}`);
258
- console.log(` - Build Minutes: ${p.limits.max_build_minutes || 'Unlimited'} mins`);
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
+ );
259
416
  }
260
- console.log(`----------------------------------------------------`);
417
+ ui.box(fields, { title: p.name.toUpperCase() });
261
418
  });
262
419
  }
263
420
 
264
- function stripAnsi(str) {
265
- return String(str).replace(/\x1b\[[0-9;]*m/g, '');
266
- }
267
-
268
421
  function drawCards(cards, cardsPerRow = 2) {
269
422
  const cardWidth = 34;
270
423
  const rows = [];
@@ -283,7 +436,7 @@ function drawCards(cards, cardsPerRow = 2) {
283
436
  row.forEach((card, idx) => {
284
437
  const space = idx > 0 ? ' ' : ''; // Gap between cards
285
438
 
286
- topBorder += space + '┌' + '─'.repeat(cardWidth - 2) + '┐';
439
+ topBorder += space + colors.gray + '┌' + '─'.repeat(cardWidth - 2) + '┐' + colors.reset;
287
440
 
288
441
  const label = card.label || '';
289
442
  let value = card.value !== undefined ? String(card.value) : '';
@@ -297,19 +450,19 @@ function drawCards(cards, cardsPerRow = 2) {
297
450
  const rawVal = stripAnsi(value);
298
451
 
299
452
  const textLen = label.length + rawVal.length;
300
- const padLen = cardWidth - 6 - textLen; // accounts for borders and padding spaces
453
+ const padLen = cardWidth - 6 - textLen;
301
454
  const pad = ' '.repeat(Math.max(1, padLen));
302
455
 
303
- labelValLine += space + `│ ${colors.cyan}${label}${colors.reset}${pad}${valStr} │`;
304
- sepLine += space + '├' + '─'.repeat(cardWidth - 2) + '┤';
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;
305
458
 
306
459
  const hint = card.hint || '';
307
460
  const rawHint = stripAnsi(hint);
308
461
  const hintPadLen = cardWidth - 6 - rawHint.length;
309
462
  const hintPad = ' '.repeat(Math.max(0, hintPadLen));
310
463
 
311
- hintLine += space + `│ ${hint}${hintPad} │`;
312
- bottomBorder += space + '└' + '─'.repeat(cardWidth - 2) + '┘';
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;
313
466
  });
314
467
 
315
468
  console.log(topBorder);
@@ -334,7 +487,7 @@ async function cmdStatus() {
334
487
 
335
488
  if (user.role === 'super_admin') {
336
489
  try {
337
- console.log(`${colors.cyan}Fetching system metrics (Admin overview)...${colors.reset}`);
490
+ ui.stepInfo(`Fetching system metrics (Admin overview)...`);
338
491
  const [overviewRes, serverRes] = await Promise.all([
339
492
  makeRequest('/admin/overview').catch(() => null),
340
493
  makeRequest('/admin/server').catch(() => null)
@@ -359,9 +512,7 @@ async function cmdStatus() {
359
512
  const buildCount = activeBuildingProjects;
360
513
  const failCount = byStatus.failed || byStatus.FAILED || 0;
361
514
 
362
- console.log(`\n${colors.cyan}${colors.bold}========================================================================${colors.reset}`);
363
- console.log(`${colors.bold} RUSHDEPLOY SYSTEM STATUS (ADMIN) ${colors.reset}`);
364
- console.log(`${colors.cyan}${colors.bold}========================================================================${colors.reset}\n`);
515
+ ui.step(`System Overview metrics retrieved`);
365
516
 
366
517
  const adminCards = [
367
518
  { label: 'Users', value: stats.users ?? 0, hint: 'Total registered users' },
@@ -406,9 +557,7 @@ async function cmdStatus() {
406
557
  const bwLimit = limits.max_bandwidth_bytes ? formatBytes(limits.max_bandwidth_bytes) : 'Unlimited';
407
558
  const buildLimit = limits.max_build_minutes ? `${limits.max_build_minutes} mins` : 'Unlimited';
408
559
 
409
- console.log(`\n${colors.cyan}${colors.bold}========================================================================${colors.reset}`);
410
- console.log(`${colors.bold} RUSHDEPLOY ACCOUNT STATUS (WORKSPACE) ${colors.reset}`);
411
- console.log(`${colors.cyan}${colors.bold}========================================================================${colors.reset}\n`);
560
+ ui.step(`Workspace metrics retrieved`);
412
561
 
413
562
  const workspaceCards = [
414
563
  { label: 'Workspace', value: workspace.name, hint: `Tier Plan: ${workspace.tier.toUpperCase()}` },
@@ -419,62 +568,76 @@ async function cmdStatus() {
419
568
 
420
569
  drawCards(workspaceCards, 2);
421
570
  } else {
422
- console.error(`${colors.red}Error: Could not retrieve status details.${colors.reset}`);
571
+ ui.stepError(`Could not retrieve status details.`);
423
572
  }
424
573
  }
425
574
 
426
575
  async function cmdProjects() {
576
+ ui.stepInfo(`Fetching projects list...`);
427
577
  const res = await makeRequest('/projects/');
428
578
  const projects = res.projects || [];
429
579
 
430
580
  if (projects.length === 0) {
431
- console.log('No projects found in active workspace.');
581
+ ui.stepInfo('No projects found in active workspace.');
432
582
  return;
433
583
  }
434
584
 
435
- console.log(`\n${colors.bold}${'ID'.padEnd(6)} | ${'PROJECT NAME'.padEnd(20)} | ${'STATUS'.padEnd(10)} | ${'PUBLIC URL'}${colors.reset}`);
436
- console.log('-'.repeat(80));
437
- projects.forEach((p) => {
438
- let statusColor = colors.reset;
439
- if (p.status === 'running') statusColor = colors.green;
440
- if (p.status === 'failed') statusColor = colors.red;
441
- if (p.status === 'pending') statusColor = colors.yellow;
585
+ ui.step(`Retrieved ${projects.length} project(s)`);
442
586
 
443
- console.log(`${String(p.id).padEnd(6)} | ${p.name.substring(0, 20).padEnd(20)} | ${statusColor}${p.status.toUpperCase().padEnd(10)}${colors.reset} | ${p.url}`);
444
- });
445
- console.log();
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);
446
602
  }
447
603
 
448
604
  async function cmdRepos() {
449
605
  try {
606
+ ui.stepInfo(`Fetching connected GitHub repositories...`);
450
607
  const res = await makeRequest('/auth/github/repos');
451
608
  const repos = res.repos || [];
452
609
 
453
610
  if (repos.length === 0) {
454
- console.log('No GitHub repositories found. Try connecting to GitHub first.');
611
+ ui.stepInfo('No GitHub repositories found. Try connecting to GitHub first.');
455
612
  return;
456
613
  }
457
614
 
458
- console.log(`\n${colors.bold}${'REPOSITORY NAME'.padEnd(35)} | ${'DEFAULT BRANCH'.padEnd(16)} | ${'VISIBILITY'}${colors.reset}`);
459
- console.log('-'.repeat(70));
460
- repos.forEach((r) => {
461
- const vis = r.private ? `${colors.yellow}PRIVATE${colors.reset}` : `${colors.green}PUBLIC${colors.reset}`;
462
- console.log(`${r.full_name.substring(0, 35).padEnd(35)} | ${String(r.default_branch || 'main').padEnd(16)} | ${vis}`);
463
- });
464
- console.log();
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);
465
630
  } catch (err) {
466
- // If user has not connected GitHub OAuth in settings yet
467
- console.error(`${colors.yellow}Notice: Please connect your GitHub account via Settings in the Web Dashboard first.${colors.reset}`);
631
+ ui.stepWarn(`Please connect your GitHub account via Settings in the Web Dashboard first.`);
468
632
  }
469
633
  }
470
634
 
471
635
  async function cmdDeploy(type, args) {
472
636
  if (type === 'git') {
473
- // Deploy from URI: deploy git <repo_url> [--branch <branch>] [--name <name>] [--type <type>]
474
637
  const repoUrl = args[0];
475
638
  if (!repoUrl) {
476
- console.error(`${colors.red}Error: Please specify the Git repository URL.${colors.reset}`);
477
- console.log('Usage: rushdeploy deploy git <repo_url> [--branch <branch>] [--name <name>] [--type <type>]');
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}`);
478
641
  process.exit(1);
479
642
  }
480
643
 
@@ -491,7 +654,10 @@ async function cmdDeploy(type, args) {
491
654
  const typeIdx = args.indexOf('--type');
492
655
  if (typeIdx !== -1 && args[typeIdx + 1]) ptype = args[typeIdx + 1];
493
656
 
494
- console.log(`${colors.cyan}Triggering deployment for Git repo: ${repoUrl} (${branch})...${colors.reset}`);
657
+ ui.step(`Connecting to repository`);
658
+ ui.step(`Creating project ${name}`);
659
+ ui.step(`Triggering deployment pipeline for ${branch} branch`);
660
+
495
661
  const res = await makeRequest('/projects/deploy', 'POST', {
496
662
  name,
497
663
  repo_url: repoUrl,
@@ -500,18 +666,22 @@ async function cmdDeploy(type, args) {
500
666
  });
501
667
 
502
668
  if (res.success) {
503
- console.log(`\n${colors.green}${colors.bold}✓ Deployment queued successfully!${colors.reset}`);
504
- console.log(`Project ID: ${res.project_id}`);
505
- console.log(`Deployment ID: ${res.deployment_id}`);
506
- console.log(`Public URL: ${colors.bold}${res.url}${colors.reset}\n`);
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
+ ]);
507
678
  }
508
679
 
509
680
  } else if (type === 'repo') {
510
- // Deploy from Github Account: deploy repo <owner/repo> [--branch <branch>] [--name <name>] [--type <type>]
511
681
  const ownerRepo = args[0];
512
682
  if (!ownerRepo || !ownerRepo.includes('/')) {
513
- console.error(`${colors.red}Error: Please specify the repository in 'owner/repo' format.${colors.reset}`);
514
- console.log('Usage: rushdeploy deploy repo <owner/repo> [--branch <branch>] [--name <name>] [--type <type>]');
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}`);
515
685
  process.exit(1);
516
686
  }
517
687
 
@@ -529,7 +699,10 @@ async function cmdDeploy(type, args) {
529
699
  const typeIdx = args.indexOf('--type');
530
700
  if (typeIdx !== -1 && args[typeIdx + 1]) ptype = args[typeIdx + 1];
531
701
 
532
- console.log(`${colors.cyan}Triggering deployment for GitHub repo: ${ownerRepo} (${branch})...${colors.reset}`);
702
+ ui.step(`Connecting to GitHub repository ${ownerRepo}`);
703
+ ui.step(`Creating project ${name}`);
704
+ ui.step(`Triggering deployment pipeline for ${branch} branch`);
705
+
533
706
  const res = await makeRequest('/projects/deploy', 'POST', {
534
707
  name,
535
708
  repo_url: repoUrl,
@@ -538,24 +711,28 @@ async function cmdDeploy(type, args) {
538
711
  });
539
712
 
540
713
  if (res.success) {
541
- console.log(`\n${colors.green}${colors.bold}✓ Deployment queued successfully!${colors.reset}`);
542
- console.log(`Project ID: ${res.project_id}`);
543
- console.log(`Deployment ID: ${res.deployment_id}`);
544
- console.log(`Public URL: ${colors.bold}${res.url}${colors.reset}\n`);
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
+ ]);
545
723
  }
546
724
 
547
725
  } else if (type === 'wordpress') {
548
- // Deploy wordpress <name> --email <email> [--title <title>] [--user <user>] [--password <pass>]
549
726
  const name = args[0];
550
727
  if (!name) {
551
- console.error(`${colors.red}Error: Please specify the site/project name.${colors.reset}`);
552
- console.log('Usage: rushdeploy deploy wordpress <name> --email <email> [--title <title>] [--user <user>] [--password <pass>]');
728
+ ui.stepError(`Please specify the site/project name.`);
729
+ console.log(` Usage: ${colors.green}rushdeploy deploy wordpress <name> --email <email> [options]${colors.reset}`);
553
730
  process.exit(1);
554
731
  }
555
732
 
556
733
  const emailIdx = args.indexOf('--email');
557
734
  if (emailIdx === -1 || !args[emailIdx + 1]) {
558
- console.error(`${colors.red}Error: --email <email> option is required for WordPress deployment.${colors.reset}`);
735
+ ui.stepError(`--email <email> option is required for WordPress deployment.`);
559
736
  process.exit(1);
560
737
  }
561
738
  const adminEmail = args[emailIdx + 1];
@@ -572,7 +749,10 @@ async function cmdDeploy(type, args) {
572
749
  const passIdx = args.indexOf('--password');
573
750
  if (passIdx !== -1 && args[passIdx + 1]) adminPassword = args[passIdx + 1];
574
751
 
575
- console.log(`${colors.cyan}Triggering WordPress deployment: ${name}...${colors.reset}`);
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
+
576
756
  const res = await makeRequest('/projects/wordpress', 'POST', {
577
757
  name,
578
758
  admin_email: adminEmail,
@@ -582,18 +762,23 @@ async function cmdDeploy(type, args) {
582
762
  });
583
763
 
584
764
  if (res.success) {
585
- console.log(`\n${colors.green}${colors.bold}✓ WordPress deployment started successfully!${colors.reset}`);
586
- console.log(`Project ID: ${res.project_id}`);
587
- console.log(`Deployment ID: ${res.deployment_id}`);
588
- console.log(`Public URL: ${colors.bold}${res.url}${colors.reset}\n`);
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
+ ]);
589
774
  }
590
775
 
591
776
  } else {
592
- console.error(`${colors.red}Error: Invalid deployment source type. Must be 'git', 'repo', or 'wordpress'.${colors.reset}`);
593
- console.log('Usage:');
594
- console.log(' rushdeploy deploy git <repo_url> [options]');
595
- console.log(' rushdeploy deploy repo <owner/repo> [options]');
596
- console.log(' rushdeploy deploy wordpress <name> --email <email> [options]');
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]`);
597
782
  process.exit(1);
598
783
  }
599
784
  }
@@ -610,7 +795,7 @@ async function resolveProjectId(idOrSlug) {
610
795
  return matched.id;
611
796
  }
612
797
 
613
- console.error(`${colors.red}Error: No project found matching name or slug "${idOrSlug}".${colors.reset}`);
798
+ ui.stepError(`No project found matching name or slug "${idOrSlug}".`);
614
799
  process.exit(1);
615
800
  }
616
801
 
@@ -619,27 +804,27 @@ async function cmdProject(idOrSlug, subcommand, subArgs) {
619
804
 
620
805
  switch (subcommand) {
621
806
  case 'start': {
622
- console.log(`${colors.cyan}Starting project ${id}...${colors.reset}`);
807
+ ui.stepInfo(`Starting project ${id}...`);
623
808
  const res = await makeRequest(`/projects/${id}/start`, 'POST');
624
809
  if (res.success) {
625
- console.log(`${colors.green}${colors.bold}✓ Project container started successfully.${colors.reset}`);
810
+ ui.step(`Project container started successfully.`);
626
811
  }
627
812
  break;
628
813
  }
629
814
  case 'stop': {
630
- console.log(`${colors.cyan}Stopping project ${id}...${colors.reset}`);
815
+ ui.stepInfo(`Stopping project ${id}...`);
631
816
  const res = await makeRequest(`/projects/${id}/stop`, 'POST');
632
817
  if (res.success) {
633
- console.log(`${colors.green}${colors.bold}✓ Project container stopped successfully.${colors.reset}`);
818
+ ui.step(`Project container stopped successfully.`);
634
819
  }
635
820
  break;
636
821
  }
637
822
  case 'redeploy': {
638
- console.log(`${colors.cyan}Triggering project redeployment...${colors.reset}`);
823
+ ui.stepInfo(`Triggering project redeployment...`);
639
824
  const res = await makeRequest(`/projects/${id}/redeploy`, 'POST');
640
825
  if (res.success) {
641
- console.log(`${colors.green}${colors.bold}✓ Redeployment triggered successfully.${colors.reset}`);
642
- console.log(`Deployment ID: ${res.deployment_id}`);
826
+ ui.step(`Redeployment triggered successfully.`);
827
+ ui.stepInfo(`Deployment ID: ${res.deployment_id}`);
643
828
  }
644
829
  break;
645
830
  }
@@ -655,48 +840,62 @@ async function cmdProject(idOrSlug, subcommand, subArgs) {
655
840
  const info = await makeRequest(`/projects/${id}/status`);
656
841
  const stats = statsRes.stats;
657
842
 
658
- console.log(`\n${colors.cyan}${colors.bold}====================================================${colors.reset}`);
659
- console.log(`${colors.bold} RESOURCE METRICS: PROJECT ${id} ${colors.reset}`);
660
- console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}`);
661
- console.log(`Current Status: ${info.status.toUpperCase()}`);
843
+ ui.stepInfo(`Resource metrics for Project ${id}`);
662
844
 
845
+ const fields = [
846
+ { label: 'Current Status', value: ui.badge(info.status) }
847
+ ];
848
+
663
849
  if (stats) {
664
- const cpu = stats.cpu_usage !== undefined ? stats.cpu_usage.toFixed(2) : '0.00';
665
- const memUsed = formatBytes(stats.memory_usage);
666
- const memLimit = formatBytes(stats.memory_limit);
667
- const memPct = stats.memory_limit ? ((stats.memory_usage / stats.memory_limit) * 100).toFixed(2) : '0.00';
850
+ const cpuPct = stats.cpu_usage !== undefined ? stats.cpu_usage : 0;
851
+ const rawUsage = stats.memory_usage ?? 0;
852
+ const rawLimit = stats.memory_limit || stats.memory_limit_mb || 0;
668
853
 
669
- console.log(`CPU Usage: ${colors.green}${cpu}%${colors.reset}`);
670
- console.log(`Memory Usage: ${memUsed} / ${memLimit} (${colors.yellow}${memPct}%${colors.reset})`);
854
+ const rawUsageMb = rawUsage < 1000000 ? rawUsage : rawUsage / (1024 * 1024);
855
+ const rawLimitMb = rawLimit > 0 ? (rawLimit < 1000000 ? rawLimit : rawLimit / (1024 * 1024)) : 0;
856
+
857
+ const memUsed = formatMemory(rawUsageMb);
858
+ const memLimit = rawLimitMb > 0 ? formatMemory(rawLimitMb) : 'Unlimited';
859
+ const memPct = rawLimitMb > 0 ? (rawUsageMb / rawLimitMb) * 100 : (stats.memory_percent || 0);
860
+
861
+ fields.push(
862
+ { label: 'CPU Load', value: ui.progressBar(cpuPct) },
863
+ { label: 'Memory Load', value: `${memUsed} / ${memLimit} (${ui.progressBar(memPct)})` }
864
+ );
671
865
  } else {
672
- console.log('Container stats not active. Is the project stopped?');
866
+ fields.push({ label: 'Metrics Status', value: colors.gray + 'Container metrics inactive or stopped' + colors.reset });
673
867
  }
674
- console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}\n`);
868
+
869
+ ui.box(fields, { title: `METRICS: PROJECT ${id}` });
675
870
  break;
676
871
  }
677
872
  case 'actions': {
873
+ ui.stepInfo(`Fetching recent deployment actions for Project ${id}...`);
678
874
  const res = await makeRequest(`/projects/${id}`);
679
875
  const deployments = res.deployments || [];
680
876
 
681
877
  if (deployments.length === 0) {
682
- console.log('No recent deployments found.');
878
+ ui.stepInfo('No recent deployments found.');
683
879
  return;
684
880
  }
685
881
 
686
- console.log(`\n${colors.cyan}${colors.bold}====================================================${colors.reset}`);
687
- console.log(`${colors.bold} RECENT DEPLOYMENT ACTIONS: PROJECT ${id} ${colors.reset}`);
688
- console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}`);
689
- deployments.slice(0, 10).forEach(d => {
690
- const dur = d.duration_ms ? `${(d.duration_ms / 1000).toFixed(1)}s` : 'N/A';
691
- let statColor = colors.reset;
692
- if (d.status === 'success') statColor = colors.green;
693
- if (d.status === 'failed') statColor = colors.red;
694
-
695
- console.log(`ID: ${d.id} (${statColor}${d.status.toUpperCase()}${colors.reset})`);
696
- console.log(`Branch: ${d.branch} | Duration: ${dur}`);
697
- console.log(`Created: ${formatDate(d.created_at)}`);
698
- console.log(`----------------------------------------------------`);
699
- });
882
+ const headers = [
883
+ { label: 'ID', pad: 4 },
884
+ { label: 'Status', pad: 4 },
885
+ { label: 'Branch', pad: 4 },
886
+ { label: 'Duration', pad: 4 },
887
+ { label: 'Created At', pad: 2 }
888
+ ];
889
+
890
+ const rows = deployments.slice(0, 10).map(d => [
891
+ String(d.id),
892
+ ui.badge(d.status),
893
+ colors.gray + (d.branch || 'main') + colors.reset,
894
+ d.duration_ms ? `${(d.duration_ms / 1000).toFixed(1)}s` : 'N/A',
895
+ formatDate(d.created_at)
896
+ ]);
897
+
898
+ ui.table(headers, rows);
700
899
  break;
701
900
  }
702
901
  case 'autoscale': {
@@ -704,11 +903,11 @@ async function cmdProject(idOrSlug, subcommand, subArgs) {
704
903
  if (!enableArg || (enableArg !== 'on' && enableArg !== 'off')) {
705
904
  const res = await makeRequest(`/projects/${id}`);
706
905
  const p = res.project;
707
- console.log(`\n${colors.bold}Autoscale Configuration for project ${id}:${colors.reset}`);
708
- console.log(` Autoscale Enabled: ${p.autoscale_enabled ? colors.green + 'ON' : colors.red + 'OFF'}${colors.reset}`);
709
- console.log(` Min/Max Replicas: ${p.min_replicas} (min) / ${p.max_replicas} (max)`);
710
- console.log(` CPU Threshold trigger: ${p.cpu_threshold_percent}%`);
711
- console.log();
906
+ ui.box([
907
+ { label: 'Autoscale State', value: p.autoscale_enabled ? ui.badge('ON') : ui.badge('OFF') },
908
+ { label: 'Min/Max Replicas', value: `${p.min_replicas} (min) / ${p.max_replicas} (max)` },
909
+ { label: 'CPU Trigger', value: `${p.cpu_threshold_percent}% CPU load` }
910
+ ], { title: `AUTOSCALE: PROJECT ${id}` });
712
911
  return;
713
912
  }
714
913
 
@@ -726,7 +925,7 @@ async function cmdProject(idOrSlug, subcommand, subArgs) {
726
925
  const cpuIdx = subArgs.indexOf('--cpu');
727
926
  if (cpuIdx !== -1 && subArgs[cpuIdx + 1]) cpu_threshold_percent = parseFloat(subArgs[cpuIdx + 1]);
728
927
 
729
- console.log(`${colors.cyan}Updating autoscale configuration...${colors.reset}`);
928
+ ui.stepInfo(`Updating autoscale settings...`);
730
929
  const res = await makeRequest(`/projects/${id}/autoscale`, 'PUT', {
731
930
  autoscale_enabled: enabled,
732
931
  min_replicas,
@@ -735,10 +934,12 @@ async function cmdProject(idOrSlug, subcommand, subArgs) {
735
934
  });
736
935
 
737
936
  if (res.success) {
738
- console.log(`${colors.green}${colors.bold}✓ Autoscale settings updated successfully.${colors.reset}`);
739
- console.log(` Enabled: ${res.autoscale.autoscale_enabled ? 'ON' : 'OFF'}`);
740
- console.log(` Replicas: ${res.autoscale.min_replicas} - ${res.autoscale.max_replicas}`);
741
- console.log(` Trigger: ${res.autoscale.cpu_threshold_percent}% CPU`);
937
+ ui.step(`Autoscale configuration updated successfully.`);
938
+ ui.box([
939
+ { label: 'Autoscale State', value: res.autoscale.autoscale_enabled ? ui.badge('ON') : ui.badge('OFF') },
940
+ { label: 'Replica Range', value: `${res.autoscale.min_replicas} - ${res.autoscale.max_replicas}` },
941
+ { label: 'Trigger Threshold', value: `${res.autoscale.cpu_threshold_percent}% CPU` }
942
+ ]);
742
943
  }
743
944
  break;
744
945
  }
@@ -746,21 +947,21 @@ async function cmdProject(idOrSlug, subcommand, subArgs) {
746
947
  const enableArg = subArgs[0];
747
948
  if (!enableArg || (enableArg !== 'on' && enableArg !== 'off')) {
748
949
  const res = await makeRequest(`/projects/${id}`);
749
- console.log(`Auto-deploy is currently: ${res.project.auto_deploy ? colors.green + 'ON' : colors.red + 'OFF'}${colors.reset}`);
950
+ ui.stepInfo(`Auto-deploy is currently: ${res.project.auto_deploy ? ui.badge('ON') : ui.badge('OFF')}`);
750
951
  return;
751
952
  }
752
953
 
753
954
  if (enableArg === 'on') {
754
- console.log(`${colors.cyan}Enabling auto-deploy (webhook configuration)...${colors.reset}`);
955
+ ui.stepInfo(`Enabling auto-deploy (webhook configuration)...`);
755
956
  const res = await makeRequest(`/projects/${id}/auto-deploy/enable`, 'POST');
756
957
  if (res.success) {
757
- console.log(`${colors.green}${colors.bold}✓ GitHub Auto-Deploy enabled.${colors.reset}`);
958
+ ui.step(`GitHub Auto-Deploy enabled.`);
758
959
  }
759
960
  } else {
760
- console.log(`${colors.cyan}Disabling auto-deploy...${colors.reset}`);
961
+ ui.stepInfo(`Disabling auto-deploy...`);
761
962
  const res = await makeRequest(`/projects/${id}/auto-deploy/disable`, 'POST');
762
963
  if (res.success) {
763
- console.log(`${colors.green}${colors.bold}✓ GitHub Auto-Deploy disabled.${colors.reset}`);
964
+ ui.step(`GitHub Auto-Deploy disabled.`);
764
965
  }
765
966
  }
766
967
  break;
@@ -774,12 +975,12 @@ async function cmdProject(idOrSlug, subcommand, subArgs) {
774
975
 
775
976
  const res = await makeRequest(`/projects/${id}/logs`);
776
977
  const logLines = res.lines || [];
777
- console.log(`\n${colors.cyan}${colors.bold}Showing last ${Math.min(linesLimit, logLines.length)} log lines for project ${id}:${colors.reset}`);
778
- console.log('-'.repeat(80));
978
+ ui.stepInfo(`Showing last ${Math.min(linesLimit, logLines.length)} log lines for project ${id}:`);
979
+ console.log(colors.gray + ''.repeat(80) + colors.reset);
779
980
  logLines.slice(-linesLimit).forEach(ln => {
780
981
  let levelColor = colors.reset;
781
982
  if (ln.level === 'error') levelColor = colors.red;
782
- console.log(`[${formatDate(ln.timestamp)}] ${levelColor}${ln.message}${colors.reset}`);
983
+ console.log(`[${colors.gray}${formatDate(ln.timestamp)}${colors.reset}] ${levelColor}${ln.message}${colors.reset}`);
783
984
  });
784
985
  console.log();
785
986
  break;
@@ -790,16 +991,20 @@ async function cmdProject(idOrSlug, subcommand, subArgs) {
790
991
  if (!action) {
791
992
  const res = await makeRequest(`/projects/${id}/env`);
792
993
  const env = res.env || {};
793
- console.log(`\n${colors.bold}Environment Variables for project ${id}:${colors.reset}`);
794
- console.log('-'.repeat(50));
795
- Object.entries(env).forEach(([k, v]) => {
796
- console.log(`${colors.green}${k}${colors.reset}=${v}`);
797
- });
798
- console.log();
994
+ ui.stepInfo(`Environment Variables for Project ${id}`);
995
+ const headers = [
996
+ { label: 'Key', pad: 4 },
997
+ { label: 'Value', pad: 2 }
998
+ ];
999
+ const rows = Object.entries(env).map(([k, v]) => [
1000
+ colors.green + colors.bold + k + colors.reset,
1001
+ colors.gray + String(v) + colors.reset
1002
+ ]);
1003
+ ui.table(headers, rows);
799
1004
  } else if (action === 'set') {
800
1005
  const setPairs = subArgs.slice(1);
801
1006
  if (setPairs.length === 0) {
802
- console.error(`${colors.red}Error: Please specify KEY=VALUE pairs to set.${colors.reset}`);
1007
+ ui.stepError(`Please specify KEY=VALUE pairs to set.`);
803
1008
  process.exit(1);
804
1009
  }
805
1010
 
@@ -815,13 +1020,13 @@ async function cmdProject(idOrSlug, subcommand, subArgs) {
815
1020
 
816
1021
  const res = await makeRequest(`/projects/${id}/env`, 'PUT', { env });
817
1022
  if (res.success) {
818
- console.log(`${colors.green}${colors.bold}✓ Environment variables updated successfully.${colors.reset}`);
819
- console.log('Run `rushdeploy project <slug> redeploy` to apply changes.');
1023
+ ui.step(`Environment variables updated successfully.`);
1024
+ ui.stepInfo(`Run 'rushdeploy project ${idOrSlug} redeploy' to apply changes.`);
820
1025
  }
821
1026
  } else if (action === 'remove') {
822
1027
  const removeKeys = subArgs.slice(1);
823
1028
  if (removeKeys.length === 0) {
824
- console.error(`${colors.red}Error: Please specify keys to remove.${colors.reset}`);
1029
+ ui.stepError(`Please specify keys to remove.`);
825
1030
  process.exit(1);
826
1031
  }
827
1032
 
@@ -834,113 +1039,130 @@ async function cmdProject(idOrSlug, subcommand, subArgs) {
834
1039
 
835
1040
  const res = await makeRequest(`/projects/${id}/env`, 'PUT', { env });
836
1041
  if (res.success) {
837
- console.log(`${colors.green}${colors.bold}✓ Environment variables updated successfully.${colors.reset}`);
838
- console.log('Run `rushdeploy project <slug> redeploy` to apply changes.');
1042
+ ui.step(`Environment variables updated successfully.`);
1043
+ ui.stepInfo(`Run 'rushdeploy project ${idOrSlug} redeploy' to apply changes.`);
839
1044
  }
840
1045
  } else {
841
- console.error(`${colors.red}Unknown env subcommand: "${action}". Use "set" or "remove".${colors.reset}`);
1046
+ ui.stepError(`Unknown env subcommand: "${action}". Use "set" or "remove".`);
842
1047
  }
843
1048
  break;
844
1049
  }
845
1050
  default: {
846
- const res = await makeRequest(`/projects/${id}`);
1051
+ const [res, statsRes] = await Promise.all([
1052
+ makeRequest(`/projects/${id}`),
1053
+ makeRequest(`/projects/${id}/stats`).catch(() => null)
1054
+ ]);
847
1055
  const p = res.project;
848
-
849
- console.log(`\n${colors.cyan}${colors.bold}====================================================${colors.reset}`);
850
- console.log(`${colors.bold} PROJECT: ${p.name.toUpperCase()} ${colors.reset}`);
851
- console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}`);
852
- console.log(`ID: ${p.id}`);
853
- console.log(`Slug: ${p.slug}`);
854
- console.log(`Status: ${p.status.toUpperCase()}`);
855
- console.log(`Public URL: ${colors.bold}${p.url}${colors.reset}`);
856
- console.log(`Repository: ${p.repo_url} (${p.branch})`);
857
- console.log(`----------------------------------------------------`);
858
- console.log(`Memory Limit: ${p.memory_mb} MB`);
859
- console.log(`CPU Alloc: ${p.cpu_cores} Cores`);
860
- console.log(`Autoscale: ${p.autoscale_enabled ? 'Enabled' : 'Disabled'}`);
861
- if (p.autoscale_enabled) {
862
- console.log(` Min/Max Reps: ${p.min_replicas} / ${p.max_replicas}`);
863
- console.log(` Current Reps: ${p.current_replicas}`);
1056
+ const stats = statsRes ? statsRes.stats : null;
1057
+
1058
+ const fields = [
1059
+ { label: 'Project Name', value: colors.bold + p.name + colors.reset },
1060
+ { label: 'ID / Slug', value: `${p.id} (${p.slug})` },
1061
+ { label: 'State', value: ui.badge(p.status) },
1062
+ { label: 'Public URL', value: colors.brightCyan + colors.bold + p.url + colors.reset },
1063
+ { label: 'Repository', value: colors.gray + `${p.repo_url} (${p.branch})` + colors.reset }
1064
+ ];
1065
+
1066
+ if (stats && p.status === 'running') {
1067
+ const cpuPct = stats.cpu_usage !== undefined ? stats.cpu_usage : 0;
1068
+ const rawUsage = stats.memory_usage ?? 0;
1069
+ const rawLimit = stats.memory_limit || stats.memory_limit_mb || p.memory_mb || 0;
1070
+
1071
+ const rawUsageMb = rawUsage < 1000000 ? rawUsage : rawUsage / (1024 * 1024);
1072
+ const rawLimitMb = rawLimit > 0 ? (rawLimit < 1000000 ? rawLimit : rawLimit / (1024 * 1024)) : (p.memory_mb || 512);
1073
+
1074
+ const memUsed = formatMemory(rawUsageMb);
1075
+ const memLimit = formatMemory(rawLimitMb);
1076
+ const memPct = rawLimitMb > 0 ? (rawUsageMb / rawLimitMb) * 100 : (stats.memory_percent || 0);
1077
+
1078
+ fields.push(
1079
+ { label: 'CPU Usage', value: ui.progressBar(cpuPct) },
1080
+ { label: 'Memory Usage', value: `${memUsed} / ${memLimit} (${ui.progressBar(memPct)})` }
1081
+ );
1082
+ } else {
1083
+ fields.push(
1084
+ { label: 'Resource Alloc', value: `${p.memory_mb} MB RAM · ${p.cpu_cores} Core(s)` }
1085
+ );
864
1086
  }
865
- console.log(`Created At: ${formatDate(p.created_at)}`);
866
- console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}\n`);
1087
+
1088
+ fields.push(
1089
+ { label: 'Autoscale', value: p.autoscale_enabled ? `${ui.badge('ENABLED')} (${p.min_replicas}-${p.max_replicas} reps)` : ui.badge('DISABLED') },
1090
+ { label: 'Created At', value: formatDate(p.created_at) }
1091
+ );
1092
+
1093
+ ui.box(fields, { title: p.name.toUpperCase() });
867
1094
  }
868
1095
  }
869
1096
  }
870
1097
 
871
1098
  async function cmdWorkspace() {
1099
+ ui.stepInfo(`Fetching workspace quotas & usage metrics...`);
872
1100
  const wsRes = await makeRequest('/workspaces/current');
873
1101
  const workspace = wsRes.workspace;
874
1102
  const usage = wsRes.usage;
875
1103
  const limits = wsRes.limits;
876
1104
 
877
- console.log(`\n${colors.cyan}${colors.bold}====================================================${colors.reset}`);
878
- console.log(`${colors.bold} WORKSPACE QUOTAS & METRICS ${colors.reset}`);
879
- console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}`);
880
- console.log(`Workspace: ${workspace.name} (${workspace.slug})`);
881
- console.log(`Billing Period: ${usage.billing_period}`);
882
- console.log(`Tier: ${colors.magenta}${workspace.tier.toUpperCase()}${colors.reset}`);
883
- console.log(`----------------------------------------------------`);
884
- console.log(`Resource Usage:`);
885
-
886
1105
  const projLimit = limits.max_projects || 'Unlimited';
887
1106
  const depLimit = limits.max_deployments || 'Unlimited';
888
1107
  const bwLimit = limits.max_bandwidth_bytes ? formatBytes(limits.max_bandwidth_bytes) : 'Unlimited';
889
1108
  const buildLimit = limits.max_build_minutes ? `${limits.max_build_minutes} mins` : 'Unlimited';
890
1109
 
891
- console.log(` Projects Created: ${usage.projects_count} / ${projLimit}`);
892
- console.log(` Deployments: ${usage.deployments_count} / ${depLimit}`);
893
- console.log(` Bandwidth Used: ${formatBytes(usage.bandwidth_bytes)} / ${bwLimit}`);
894
- console.log(` Build Duration: ${(usage.build_seconds / 60).toFixed(1)} mins / ${buildLimit}`);
895
- console.log(`${colors.cyan}${colors.bold}====================================================${colors.reset}\n`);
1110
+ ui.step(`Retrieved workspace quotas`);
1111
+
1112
+ ui.box([
1113
+ { label: 'Workspace', value: `${colors.bold}${workspace.name}${colors.reset} (${workspace.slug})` },
1114
+ { label: 'Tier Plan', value: colors.magenta + colors.bold + workspace.tier.toUpperCase() + colors.reset },
1115
+ { label: 'Billing Period', value: colors.gray + usage.billing_period + colors.reset },
1116
+ { label: 'Projects Slot', value: `${usage.projects_count} / ${projLimit}` },
1117
+ { label: 'Deployments', value: `${usage.deployments_count} / ${depLimit}` },
1118
+ { label: 'Bandwidth Used', value: `${formatBytes(usage.bandwidth_bytes)} / ${bwLimit}` },
1119
+ { label: 'Build Duration', value: `${(usage.build_seconds / 60).toFixed(1)} mins / ${buildLimit}` }
1120
+ ], { title: 'WORKSPACE QUOTAS' });
896
1121
  }
897
1122
 
898
1123
  function printHelp() {
899
1124
  console.log(`
900
- ${colors.bold}RushDeploy CLI${colors.reset} - Command Line Interface for RushDeploy PaaS.
901
-
902
- ${colors.bold}Usage:${colors.reset}
903
- rushdeploy <command> [arguments]
904
-
905
- ${colors.bold}Global Commands:${colors.reset}
906
- ${colors.green}login <token>${colors.reset} Authenticate with your personal access token.
907
- (Uses production domain: ${SERVER_URL})
908
- ${colors.green}logout${colors.reset} Clear your stored CLI session token.
909
- ${colors.green}whoami${colors.reset} Show details of the current logged-in user.
910
- ${colors.green}plans${colors.reset} List the subscription tiers and resource quotas.
911
- ${colors.green}status${colors.reset} Display workspace metrics and summary.
912
- ${colors.green}projects${colors.reset} List all projects in your active workspace.
913
- ${colors.green}repos${colors.reset} List repositories in your connected GitHub account.
914
- ${colors.green}workspace${colors.reset} Display resource usage limits and quotas.
915
- ${colors.green}help${colors.reset} Show this help dialog.
916
-
917
- ${colors.bold}Deployment Commands:${colors.reset}
918
- ${colors.green}deploy git <repo_url> [opts]${colors.reset} Deploy a project directly from a Git URL.
919
- Options: --name <name> --branch <branch> --type <type>
920
- ${colors.green}deploy repo <owner/repo> [opts]${colors.reset}Deploy a project from your connected GitHub account.
921
- Options: --name <name> --branch <branch> --type <type>
922
- ${colors.green}deploy wordpress <name> --email <e>${colors.reset} Deploy a new one-click WordPress instance.
923
- Options: --title <title> --user <user> --password <pass>
924
-
925
- ${colors.bold}Project Control Commands:${colors.reset}
926
- ${colors.green}project <slug|id> start${colors.reset} Start the project container.
927
- ${colors.green}project <slug|id> stop${colors.reset} Stop the project container.
928
- ${colors.green}project <slug|id> redeploy${colors.reset} Trigger a project rebuild and redeploy.
929
- ${colors.green}project <slug|id> visit${colors.reset} Open the project's public URL in the browser.
930
- ${colors.green}project <slug|id> stats${colors.reset} Show real-time container CPU & Memory metrics.
931
- ${colors.green}project <slug|id> metrics${colors.reset} Show CPU & memory metrics (alias of stats).
932
- ${colors.green}project <slug|id> actions${colors.reset} Show recent deployment history.
933
- ${colors.green}project <slug|id> logs${colors.reset} Fetch recent container and build logs.
934
- Use --lines <N> to customize output length.
935
-
936
- ${colors.bold}Project Configuration Commands:${colors.reset}
937
- ${colors.green}project <slug|id> autoscale${colors.reset} Show auto-scaling config.
938
- ${colors.green}project <slug|id> autoscale <on|off>${colors.reset} Enable/disable scaling.
939
- Optional: --min <N> --max <N> --cpu <%>
940
- ${colors.green}project <slug|id> autodeploy <on|off>${colors.reset} Enable/disable webhook pushes.
941
- ${colors.green}project <slug|id> env${colors.reset} List environment variables.
942
- ${colors.green}project <slug|id> env set KEY=VAL [K2=V2]${colors.reset} Set environment variables.
943
- ${colors.green}project <slug|id> env remove KEY [KEY2]${colors.reset} Remove environment variables.
1125
+ ${colors.bold}${colors.brightWhite}RushDeploy CLI${colors.reset} ${colors.gray}v1.0.2${colors.reset} - Modern PaaS Command Line Interface
1126
+
1127
+ ${colors.bold}USAGE:${colors.reset}
1128
+ ${colors.brightCyan}rushdeploy${colors.reset} <command> [arguments] [options]
1129
+
1130
+ ${colors.bold}GLOBAL COMMANDS:${colors.reset}
1131
+ ${colors.green}login <token>${colors.reset} Authenticate with your access token
1132
+ ${colors.green}logout${colors.reset} Clear your stored CLI session token
1133
+ ${colors.green}whoami${colors.reset} Display active profile details
1134
+ ${colors.green}plans${colors.reset} List subscription plans and limits
1135
+ ${colors.green}status${colors.reset} Show system & workspace status metrics
1136
+ ${colors.green}projects${colors.reset} List all projects in current workspace
1137
+ ${colors.green}repos${colors.reset} List repositories from connected GitHub account
1138
+ ${colors.green}workspace${colors.reset} Display workspace resource usage & quotas
1139
+ ${colors.green}help${colors.reset} Display this help manual
1140
+
1141
+ ${colors.bold}DEPLOYMENT COMMANDS:${colors.reset}
1142
+ ${colors.green}deploy git <repo_url>${colors.reset} Deploy a project from a Git repository URL
1143
+ Options: ${colors.gray}--name <name> --branch <branch> --type <type>${colors.reset}
1144
+ ${colors.green}deploy repo <owner/repo>${colors.reset} Deploy from connected GitHub account
1145
+ Options: ${colors.gray}--name <name> --branch <branch> --type <type>${colors.reset}
1146
+ ${colors.green}deploy wordpress <name>${colors.reset} Deploy a new one-click WordPress site
1147
+ Options: ${colors.gray}--email <e> [--title <t>] [--user <u>] [--password <p>]${colors.reset}
1148
+
1149
+ ${colors.bold}PROJECT CONTROL:${colors.reset}
1150
+ ${colors.green}project <slug|id>${colors.reset} Show project overview
1151
+ ${colors.green}project <slug|id> start${colors.reset} Start project container
1152
+ ${colors.green}project <slug|id> stop${colors.reset} Stop project container
1153
+ ${colors.green}project <slug|id> redeploy${colors.reset} Rebuild and redeploy project
1154
+ ${colors.green}project <slug|id> visit${colors.reset} Open project in default browser
1155
+ ${colors.green}project <slug|id> stats${colors.reset} Show container CPU & RAM metrics meter
1156
+ ${colors.green}project <slug|id> actions${colors.reset} Show recent deployment history
1157
+ ${colors.green}project <slug|id> logs${colors.reset} View project container logs (${colors.gray}--lines <N>${colors.reset})
1158
+
1159
+ ${colors.bold}PROJECT CONFIGURATION:${colors.reset}
1160
+ ${colors.green}project <slug|id> autoscale${colors.reset} Show auto-scaling configuration
1161
+ ${colors.green}project <slug|id> autoscale <on|off>${colors.reset} Configure auto-scaling limits (${colors.gray}--min <N> --max <N> --cpu <%>${colors.reset})
1162
+ ${colors.green}project <slug|id> autodeploy <on|off>${colors.reset} Toggle auto-deploy webhooks
1163
+ ${colors.green}project <slug|id> env${colors.reset} List environment variables
1164
+ ${colors.green}project <slug|id> env set K=V${colors.reset} Set environment variable(s)
1165
+ ${colors.green}project <slug|id> env remove K${colors.reset} Remove environment variable(s)
944
1166
  `);
945
1167
  }
946
1168
 
@@ -957,8 +1179,8 @@ async function main() {
957
1179
  case 'login': {
958
1180
  const token = args[1];
959
1181
  if (!token) {
960
- console.error(`${colors.red}Error: Please provide your access token.${colors.reset}`);
961
- console.log('Usage: rushdeploy login <token> [--server <url>]');
1182
+ ui.stepError(`Please provide your access token.`);
1183
+ console.log(` Usage: ${colors.green}rushdeploy login <token> [--server <url>]${colors.reset}`);
962
1184
  process.exit(1);
963
1185
  }
964
1186
 
@@ -998,8 +1220,8 @@ async function main() {
998
1220
  case 'project': {
999
1221
  const idOrSlug = args[1];
1000
1222
  if (!idOrSlug) {
1001
- console.error(`${colors.red}Error: Please provide a project ID or slug.${colors.reset}`);
1002
- console.log('Usage: rushdeploy project <slug|id> [subcommand] [arguments]');
1223
+ ui.stepError(`Please provide a project ID or slug.`);
1224
+ console.log(` Usage: ${colors.green}rushdeploy project <slug|id> [subcommand] [arguments]${colors.reset}`);
1003
1225
  process.exit(1);
1004
1226
  }
1005
1227
  const subcommand = args[2];
@@ -1011,13 +1233,13 @@ async function main() {
1011
1233
  await cmdWorkspace();
1012
1234
  break;
1013
1235
  default:
1014
- console.error(`${colors.red}Unknown command: "${command}"${colors.reset}`);
1236
+ ui.stepError(`Unknown command: "${command}"`);
1015
1237
  printHelp();
1016
1238
  process.exit(1);
1017
1239
  }
1018
1240
  }
1019
1241
 
1020
1242
  main().catch(err => {
1021
- console.error(`${colors.red}Fatal Error:${colors.reset}`, err.message);
1243
+ ui.stepError(`Fatal Error: ${err.message}`);
1022
1244
  process.exit(1);
1023
1245
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rushdeploy",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "CLI tool to manage your self-hosted RushDeploy PaaS account.",
5
5
  "main": "bin/index.js",
6
6
  "bin": {
@@ -16,5 +16,9 @@
16
16
  "cli"
17
17
  ],
18
18
  "author": "RushDeploy Team",
19
- "license": "MIT"
19
+ "license": "MIT",
20
+ "files": [
21
+ "bin",
22
+ "README.md"
23
+ ]
20
24
  }