@glash/cli 0.2.26 → 0.2.28

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.
@@ -138,14 +138,31 @@ export async function deploy({ flags }) {
138
138
  let currentPhase = 'doing';
139
139
  const start = Date.now();
140
140
  const indicator = createDeployIndicator();
141
+ const report = {
142
+ status: 'running',
143
+ projectId,
144
+ deploymentId: '',
145
+ target,
146
+ source: remoteDeploy ? 'remote git deploy' : 'local upload',
147
+ packagesUploaded: 0,
148
+ filesIncluded: 0,
149
+ deploymentSizeBytes: 0,
150
+ envSummary,
151
+ liveUrl: '',
152
+ error: '',
153
+ start,
154
+ };
141
155
  indicator.start(currentPhase);
142
156
 
143
157
  if (!flags['no-env']) {
144
158
  indicator.setPhase('syncing');
145
159
  try {
146
160
  envSummary = await syncEnvBeforeDeploy(projectId, target, flags);
161
+ report.envSummary = envSummary;
147
162
  } catch (e) {
148
- indicator.stop();
163
+ indicator.fail('failed');
164
+ report.envSummary = envSummary;
165
+ printDeployReport({ ...report, status: 'failed', error: `Env sync failed: ${e.message}` });
149
166
  return fail(`Env sync failed: ${e.message}`);
150
167
  }
151
168
  }
@@ -154,8 +171,10 @@ export async function deploy({ flags }) {
154
171
  indicator.setPhase('packaging');
155
172
  const zipPath = join(tmpdir(), `glash-deploy-${Date.now()}.zip`);
156
173
  try {
157
- await zipDirectory(process.cwd(), zipPath);
158
- await stat(zipPath);
174
+ const bundle = await zipDirectory(process.cwd(), zipPath);
175
+ const zipStat = await stat(zipPath);
176
+ report.filesIncluded = bundle.fileCount ?? 0;
177
+ report.deploymentSizeBytes = zipStat.size || bundle.bytes || 0;
159
178
  const formData = new FormData();
160
179
  formData.append('projectId', projectId);
161
180
  formData.append('file', await openAsBlob(zipPath, { type: 'application/zip' }), 'project.zip');
@@ -166,24 +185,30 @@ export async function deploy({ flags }) {
166
185
 
167
186
  indicator.setPhase('glashing');
168
187
  dep = await apiPost('/deployments/upload', formData);
188
+ report.deploymentId = dep?.id ?? '';
189
+ report.packagesUploaded = 1;
169
190
  await rm(zipPath, { force: true }).catch(() => {});
170
191
  } catch (e) {
171
192
  await rm(zipPath, { force: true }).catch(() => {});
172
- indicator.stop();
193
+ indicator.fail('failed');
194
+ printDeployReport({ ...report, status: 'failed', error: `Upload failed: ${e.message}` });
173
195
  return fail(`Upload failed: ${e.message}`);
174
196
  }
175
197
  } else {
176
198
  indicator.setPhase('hatching');
177
199
  try {
178
200
  dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target });
201
+ report.deploymentId = dep?.id ?? '';
179
202
  } catch (e) {
180
- indicator.stop();
203
+ indicator.fail('failed');
204
+ printDeployReport({ ...report, status: 'failed', error: e.message });
181
205
  return fail(e.message);
182
206
  }
183
207
  }
184
208
 
185
209
  if (flags.detach || flags.D) {
186
- indicator.stop();
210
+ indicator.fail('queued');
211
+ printDeployReport({ ...report, status: 'queued' });
187
212
  info(`Queued: ${color.dim(dep.id)}`);
188
213
  info(`Track it with: ${color.cyan(`glash status ${dep.id}`)}`);
189
214
  return;
@@ -203,7 +228,7 @@ export async function deploy({ flags }) {
203
228
  if (e.status === 429) {
204
229
  const wait = Math.min(Math.max(e.retryAfterMs ?? pollDelayMs * 2, 10000), 60000);
205
230
  pollDelayMs = wait;
206
- await sleep(wait);
231
+ await indicator.wait(wait);
207
232
  continue;
208
233
  }
209
234
  if (Date.now() - lastProgressAt > 60_000) {
@@ -211,7 +236,7 @@ export async function deploy({ flags }) {
211
236
  lastProgressAt = Date.now();
212
237
  }
213
238
  pollDelayMs = Math.min(Math.max(pollDelayMs * 1.5, 5000), 30000);
214
- await sleep(pollDelayMs);
239
+ await indicator.wait(pollDelayMs);
215
240
  continue;
216
241
  }
217
242
 
@@ -229,28 +254,34 @@ export async function deploy({ flags }) {
229
254
  const url = pipe.hostings?.[0]?.domains?.[0]?.domain;
230
255
  if (url) {
231
256
  indicator.setPhase('checking');
232
- const problem = await waitForLiveUrlProblem(`https://${url.replace(/^https?:\/\//, '')}`);
233
- indicator.stop();
257
+ report.liveUrl = normalizeLiveUrl(url);
258
+ const problem = await waitForLiveUrlProblem(report.liveUrl);
234
259
  if (problem) {
260
+ indicator.fail('failed');
261
+ printDeployReport({ ...report, status: 'failed', error: `Live URL check failed: ${problem}` });
235
262
  return fail(`Deployment finished, but the hostname is still not serving your app: ${problem}. Run \`glash logs ${dep.id}\` for details.`);
236
263
  }
237
264
  } else {
238
- indicator.stop();
265
+ report.liveUrl = '';
239
266
  }
240
- printDeploySuccess({ url, envSummary });
267
+ indicator.finish('complete');
268
+ printDeployReport({ ...report, status: 'successful' });
269
+ ok('Deployment live');
241
270
  return;
242
271
  }
243
272
  if (status === 'placeholder') {
244
- indicator.stop();
245
273
  const lastPlaceholder = [...(pipe.events ?? [])].reverse().find((ev) => ev.status === 'placeholder' || ev.step === 'deploy');
246
274
  const detail = lastPlaceholder?.message ? `: ${lastPlaceholder.message}` : '';
275
+ indicator.fail('failed');
276
+ printDeployReport({ ...report, status: 'failed', error: `Deployment reached the glashDB parked page, not your app${detail}` });
247
277
  return fail(`Deployment reached the glashDB parked page, not your app${detail}. Run \`glash logs ${dep.id}\` for details.`);
248
278
  }
249
279
  if (['error', 'failed'].includes(status)) {
250
- indicator.stop();
251
280
  const lastError = [...(pipe.events ?? [])].reverse().find((ev) => ev.status === 'error' || ev.status === 'failed');
252
281
  const rawDetail = lastError?.message ? `: ${lastError.message}` : '';
253
282
  const detail = `${rawDetail}${deploymentFailureHint(lastError?.message)}`;
283
+ indicator.fail('failed');
284
+ printDeployReport({ ...report, status: 'failed', error: `Deployment failed (${status})${rawDetail}` });
254
285
  return fail(`Deployment failed (${status})${detail}. Run \`glash logs ${dep.id}\` for details.`);
255
286
  }
256
287
  currentPhase = phaseFromPipeline(pipe, currentPhase);
@@ -258,7 +289,8 @@ export async function deploy({ flags }) {
258
289
  pollDelayMs = nextPollDelay(pipe);
259
290
  await indicator.wait(pollDelayMs);
260
291
  }
261
- indicator.stop();
292
+ indicator.fail('timed out');
293
+ printDeployReport({ ...report, status: 'timed out', error: `Waited ${formatDuration(timeoutMs)} for deployment to finish` });
262
294
  warn(`Timed out after ${formatDuration(timeoutMs)} waiting for deployment. Check \`glash status ${dep.id}\`.`);
263
295
  }
264
296
 
@@ -416,12 +448,74 @@ function isTransientLiveUrlProblem(problem) {
416
448
  return /no nginx route|parked page|HTTP 5\d\d|live URL check failed/i.test(problem);
417
449
  }
418
450
 
419
- function printDeploySuccess({ url, envSummary }) {
420
- const liveUrl = url ? `https://${url.replace(/^https?:\/\//, '')}` : '';
421
- if (liveUrl) info(`Live: ${terminalLink(liveUrl)}`);
422
- const envText = envSummary?.skipped ? 'skipped' : `${envSummary?.count ?? 0} moved to glash`;
423
- info(`Env: ${envText}`);
424
- ok('Success');
451
+ function normalizeLiveUrl(url) {
452
+ return url ? `https://${String(url).replace(/^https?:\/\//, '')}` : '';
453
+ }
454
+
455
+ function printDeployReport(report) {
456
+ const elapsedMs = Date.now() - report.start;
457
+ const liveUrl = report.liveUrl ? terminalLink(report.liveUrl) : '-';
458
+ const rows = [
459
+ ['Status', formatReportStatus(report.status)],
460
+ ['Deployment ID', report.deploymentId || '-'],
461
+ ['Target', report.target || 'production'],
462
+ ['Source', report.source || '-'],
463
+ ['Packages uploaded', formatPackagesUploaded(report)],
464
+ ['Deployment size', formatDeploymentSize(report)],
465
+ ['Total time spent', formatMinutes(elapsedMs)],
466
+ ['Live URL', liveUrl],
467
+ ['Environment', formatEnvSummary(report.envSummary)],
468
+ ];
469
+ if (report.error) rows.push(['Error', report.error]);
470
+
471
+ const width = rows.reduce((max, [label]) => Math.max(max, label.length), 0);
472
+ info('');
473
+ info(color.bold('Deployment report'));
474
+ for (const [label, value] of rows) {
475
+ info(` ${color.dim(label.padEnd(width))} ${value}`);
476
+ }
477
+ }
478
+
479
+ function formatReportStatus(status) {
480
+ if (status === 'successful') return color.green('successful');
481
+ if (status === 'queued') return color.yellow('queued');
482
+ if (status === 'timed out') return color.yellow('timed out');
483
+ if (status === 'failed') return color.red('failed');
484
+ return status || '-';
485
+ }
486
+
487
+ function formatPackagesUploaded(report) {
488
+ const count = report.packagesUploaded ?? 0;
489
+ const label = count === 1 ? 'source bundle' : 'source bundles';
490
+ const files = report.filesIncluded
491
+ ? ` (${report.filesIncluded.toLocaleString()} files included)`
492
+ : (report.source === 'remote git deploy' ? ' (remote source)' : '');
493
+ return `${count.toLocaleString()} ${label}${files}`;
494
+ }
495
+
496
+ function formatDeploymentSize(report) {
497
+ if (report.deploymentSizeBytes) return formatBytes(report.deploymentSizeBytes);
498
+ return report.source === 'remote git deploy' ? 'not measured for remote deploy' : '-';
499
+ }
500
+
501
+ function formatEnvSummary(envSummary) {
502
+ if (envSummary?.skipped) return 'skipped';
503
+ const count = envSummary?.count ?? 0;
504
+ return `${count.toLocaleString()} variable${count === 1 ? '' : 's'} synced`;
505
+ }
506
+
507
+ function formatMinutes(ms) {
508
+ const minutes = ms / 60_000;
509
+ const rounded = minutes < 10 ? minutes.toFixed(2) : minutes.toFixed(1);
510
+ return `${rounded} min (${formatDuration(ms)})`;
511
+ }
512
+
513
+ function formatBytes(bytes) {
514
+ if (!Number.isFinite(bytes) || bytes <= 0) return '-';
515
+ if (bytes < 1024) return `${bytes} B`;
516
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
517
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
518
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
425
519
  }
426
520
 
427
521
  function terminalLink(url) {
@@ -432,42 +526,71 @@ function terminalLink(url) {
432
526
 
433
527
  function createDeployIndicator() {
434
528
  const interactive = Boolean(stdout.isTTY);
435
- let frame = 0;
436
529
  let timer;
437
530
  let phase = 'doing';
438
- let printedPhase = '';
439
- const frames = ['.', '..', '...'];
531
+ let progress = 0;
532
+ let lastPrinted = '';
440
533
 
441
534
  function message() {
442
- const dots = frames[frame++ % frames.length].padEnd(3, ' ');
443
- return color.dim(`${phase}${dots}`);
535
+ const width = 24;
536
+ const filled = Math.round((progress / 100) * width);
537
+ const bar = `${'#'.repeat(filled)}${'-'.repeat(width - filled)}`;
538
+ const percent = `${String(Math.round(progress)).padStart(3, ' ')}%`;
539
+ return `${color.dim(`[${bar}]`)} ${percent} ${phase}`;
444
540
  }
445
541
 
446
542
  function render({ force = false } = {}) {
543
+ const next = message();
447
544
  if (!interactive) {
448
- if (force && phase !== printedPhase) {
449
- printedPhase = phase;
450
- info(message().trimEnd());
545
+ if (force && next !== lastPrinted) {
546
+ lastPrinted = next;
547
+ info(next);
451
548
  }
452
549
  return;
453
550
  }
454
- stdout.write(`\r\x1b[2K${message()}`);
551
+ stdout.write(`\r\x1b[2K${next}`);
552
+ }
553
+
554
+ function stopTimer() {
555
+ if (timer) clearInterval(timer);
556
+ timer = undefined;
557
+ }
558
+
559
+ function setProgress(nextProgress, nextPhase = phase) {
560
+ const bounded = Math.max(0, Math.min(100, Number(nextProgress) || 0));
561
+ const advanced = Math.max(progress, bounded);
562
+ const changed = nextPhase !== phase || advanced !== progress;
563
+ phase = nextPhase;
564
+ progress = advanced;
565
+ if (changed) render({ force: true });
455
566
  }
456
567
 
457
568
  return {
458
569
  start(initialPhase = 'doing') {
459
570
  phase = initialPhase;
460
- render();
571
+ progress = 0;
572
+ render({ force: true });
461
573
  timer = setInterval(render, 1000);
462
574
  },
463
575
  setPhase(nextPhase) {
464
- if (!nextPhase || nextPhase === phase) return;
465
- phase = nextPhase;
576
+ if (!nextPhase) return;
577
+ setProgress(progressFromPhase(nextPhase), nextPhase);
578
+ },
579
+ finish(finalPhase = 'complete') {
580
+ stopTimer();
581
+ phase = finalPhase;
582
+ progress = 100;
583
+ render({ force: true });
584
+ if (interactive) stdout.write('\n');
585
+ },
586
+ fail(finalPhase = 'failed') {
587
+ stopTimer();
588
+ phase = finalPhase;
466
589
  render({ force: true });
590
+ if (interactive) stdout.write('\n');
467
591
  },
468
592
  stop() {
469
- if (timer) clearInterval(timer);
470
- timer = undefined;
593
+ stopTimer();
471
594
  if (interactive) stdout.write('\r\x1b[2K');
472
595
  },
473
596
  wait(ms) {
@@ -476,4 +599,20 @@ function createDeployIndicator() {
476
599
  };
477
600
  }
478
601
 
602
+ function progressFromPhase(phase) {
603
+ const progressByPhase = {
604
+ doing: 2,
605
+ syncing: 8,
606
+ packaging: 22,
607
+ glashing: 35,
608
+ hatching: 45,
609
+ waiting: 48,
610
+ building: 65,
611
+ deploying: 82,
612
+ routing: 90,
613
+ checking: 96,
614
+ };
615
+ return progressByPhase[phase] ?? 50;
616
+ }
617
+
479
618
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
package/lib/zip.mjs CHANGED
@@ -76,7 +76,7 @@ export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
76
76
  const output = createWriteStream(outPath);
77
77
  let bytes = 0;
78
78
 
79
- output.on('close', () => resolve(bytes));
79
+ output.on('close', () => resolve({ bytes, fileCount: files.length, files }));
80
80
  output.on('error', reject);
81
81
  zip.outputStream.on('data', (chunk) => { bytes += chunk.length; });
82
82
  zip.outputStream.on('error', reject);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glash/cli",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "glashDB command-line interface — deploy projects, pull source + env, manage env vars.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://glashdb.com",