@glash/cli 0.2.29 → 0.2.31

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 CHANGED
@@ -104,6 +104,8 @@ glash deploy --asset-mode offload
104
104
  glash deploy --asset-mode auto --asset-threshold 100mb
105
105
  ```
106
106
 
107
+ Every deploy prints a live monitor link. Open it to watch the black-screen deployment flow while the CLI is packaging, uploading, building, routing, and checking the live URL.
108
+
107
109
  ## Commands
108
110
 
109
111
  | Command | Purpose |
@@ -174,6 +176,7 @@ The commands require PostgreSQL client tools locally: `pg_dump` for dumps,
174
176
  | Variable | Default | Notes |
175
177
  | --- | --- | --- |
176
178
  | `GLASH_API_URL` | `https://api.glashdb.com/api` | Override the API base (self-hosted, staging) |
179
+ | `GLASH_APP_URL` | `https://glashdb.com` | Override the dashboard base used for live monitor links |
177
180
  | `GLASH_TOKEN` | — | Use this token instead of `~/.glash/credentials.json` (useful in CI) |
178
181
  | `GLASH_ASSET_OFFLOAD_THRESHOLD` | `100mb` | Default threshold for `glash deploy --asset-mode auto` |
179
182
 
@@ -134,10 +134,13 @@ export async function deploy({ flags }) {
134
134
  const commitHash = flags.commit || (remoteDeploy ? undefined : currentCommit());
135
135
  const gitBranch = flags.branch || currentBranch();
136
136
  const target = flags.prod || flags.production ? 'production' : (flags.target || 'production');
137
+ const requestedAssetMode = assetMode(flags);
138
+ const monitorStartedAt = Date.now();
139
+ const initialStatusUrl = deployMonitorUrl({ projectId, after: monitorStartedAt });
137
140
 
138
141
  let dep;
139
142
  let envSummary = { count: 0, skipped: Boolean(flags['no-env']) };
140
- let assetSummary = { mode: assetMode(flags), offloaded: false, count: 0, bytes: 0, skipped: 0 };
143
+ let assetSummary = { mode: requestedAssetMode, status: requestedAssetMode === 'inline' ? 'inline' : 'pending', offloaded: false, count: 0, bytes: 0, skipped: 0 };
141
144
  let currentPhase = 'doing';
142
145
  const start = Date.now();
143
146
  const indicator = createDeployIndicator();
@@ -153,9 +156,11 @@ export async function deploy({ flags }) {
153
156
  envSummary,
154
157
  assetSummary,
155
158
  liveUrl: '',
159
+ statusUrl: initialStatusUrl,
156
160
  error: '',
157
161
  start,
158
162
  };
163
+ info(`Live status: ${terminalLink(initialStatusUrl)}`);
159
164
  indicator.start(currentPhase);
160
165
 
161
166
  if (!flags['no-env']) {
@@ -177,8 +182,11 @@ export async function deploy({ flags }) {
177
182
  assetSummary = await offloadPublicAssetsBeforeDeploy(projectId, flags);
178
183
  report.assetSummary = assetSummary;
179
184
  } catch (e) {
185
+ const failedAssetSummary = e.assetSummary ?? { ...assetSummary, status: 'failed', error: e.message };
186
+ assetSummary = failedAssetSummary;
187
+ report.assetSummary = failedAssetSummary;
180
188
  indicator.fail('failed');
181
- printDeployReport({ ...report, assetSummary, status: 'failed', error: `Public asset offload failed: ${e.message}` });
189
+ printDeployReport({ ...report, assetSummary: failedAssetSummary, status: 'failed', error: `Public asset offload failed: ${e.message}` });
182
190
  return fail(`Public asset offload failed: ${e.message}`);
183
191
  }
184
192
  } else if (assetSummary.mode !== 'inline') {
@@ -207,6 +215,7 @@ export async function deploy({ flags }) {
207
215
  indicator.setPhase('glashing');
208
216
  dep = await apiPost('/deployments/upload', formData);
209
217
  report.deploymentId = dep?.id ?? '';
218
+ report.statusUrl = deployMonitorUrl({ deploymentId: dep?.id, projectId, after: monitorStartedAt });
210
219
  report.packagesUploaded = 1;
211
220
  await rm(zipPath, { force: true }).catch(() => {});
212
221
  } catch (e) {
@@ -220,6 +229,7 @@ export async function deploy({ flags }) {
220
229
  try {
221
230
  dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target });
222
231
  report.deploymentId = dep?.id ?? '';
232
+ report.statusUrl = deployMonitorUrl({ deploymentId: dep?.id, projectId, after: monitorStartedAt });
223
233
  } catch (e) {
224
234
  indicator.fail('failed');
225
235
  printDeployReport({ ...report, status: 'failed', error: e.message });
@@ -231,6 +241,7 @@ export async function deploy({ flags }) {
231
241
  indicator.fail('queued');
232
242
  printDeployReport({ ...report, status: 'queued' });
233
243
  info(`Queued: ${color.dim(dep.id)}`);
244
+ info(`Watch live: ${terminalLink(report.statusUrl)}`);
234
245
  info(`Track it with: ${color.cyan(`glash status ${dep.id}`)}`);
235
246
  return;
236
247
  }
@@ -335,16 +346,16 @@ async function syncEnvBeforeDeploy(projectId, target, flags) {
335
346
 
336
347
  async function offloadPublicAssetsBeforeDeploy(projectId, flags) {
337
348
  const mode = assetMode(flags);
338
- const empty = { mode, offloaded: false, count: 0, bytes: 0, skipped: 0 };
349
+ const empty = { mode, status: mode === 'inline' ? 'inline' : 'pending', offloaded: false, count: 0, bytes: 0, skipped: 0 };
339
350
  if (mode === 'inline') return empty;
340
351
 
341
352
  const files = await publicAssetFiles(process.cwd());
342
- if (!files.length) return empty;
353
+ if (!files.length) return { ...empty, status: 'no-public-assets' };
343
354
 
344
355
  const totalBytes = files.reduce((sum, file) => sum + file.sizeBytes, 0);
345
356
  const threshold = assetOffloadThresholdBytes(flags);
346
357
  if (mode === 'auto' && totalBytes < threshold) {
347
- return { ...empty, bytes: totalBytes };
358
+ return { ...empty, status: 'below-threshold', count: files.length, bytes: totalBytes, thresholdBytes: threshold };
348
359
  }
349
360
 
350
361
  const assets = [];
@@ -360,7 +371,12 @@ async function offloadPublicAssetsBeforeDeploy(projectId, flags) {
360
371
  let uploaded = 0;
361
372
  let skipped = 0;
362
373
  for (const batch of chunk(assets, 100)) {
363
- const prepared = await apiPost(`/projects/${projectId}/public-assets/prepare`, { assets: batch });
374
+ let prepared;
375
+ try {
376
+ prepared = await apiPost(`/projects/${projectId}/public-assets/prepare`, { assets: batch });
377
+ } catch (e) {
378
+ throw assetOffloadFailure(e, { mode, status: 'prepare-failed', offloaded: false, count: assets.length, uploaded, skipped, bytes: totalBytes });
379
+ }
364
380
  for (const asset of prepared.assets ?? []) {
365
381
  if (asset.skipped) {
366
382
  skipped += 1;
@@ -377,15 +393,23 @@ async function offloadPublicAssetsBeforeDeploy(projectId, flags) {
377
393
  });
378
394
  if (!res.ok) {
379
395
  const detail = await res.text().catch(() => '');
380
- throw new Error(`Upload ${asset.path} failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`);
396
+ throw assetOffloadFailure(
397
+ new Error(`Upload ${asset.path} failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`),
398
+ { mode, status: 'upload-failed', offloaded: false, count: assets.length, uploaded, skipped, bytes: totalBytes },
399
+ );
381
400
  }
382
401
  uploaded += 1;
383
402
  }
384
- await apiPost(`/projects/${projectId}/public-assets/complete`, { assets: batch });
403
+ try {
404
+ await apiPost(`/projects/${projectId}/public-assets/complete`, { assets: batch });
405
+ } catch (e) {
406
+ throw assetOffloadFailure(e, { mode, status: 'complete-failed', offloaded: false, count: assets.length, uploaded, skipped, bytes: totalBytes });
407
+ }
385
408
  }
386
409
 
387
410
  return {
388
411
  mode,
412
+ status: 'offloaded',
389
413
  offloaded: true,
390
414
  count: assets.length,
391
415
  uploaded,
@@ -394,6 +418,26 @@ async function offloadPublicAssetsBeforeDeploy(projectId, flags) {
394
418
  };
395
419
  }
396
420
 
421
+ function assetOffloadFailure(error, assetSummary) {
422
+ const message = friendlyAssetOffloadError(error);
423
+ const wrapped = new Error(message);
424
+ wrapped.cause = error;
425
+ wrapped.status = error?.status;
426
+ wrapped.assetSummary = { ...assetSummary, error: message };
427
+ return wrapped;
428
+ }
429
+
430
+ function friendlyAssetOffloadError(error) {
431
+ const message = error?.message || String(error);
432
+ if (error?.status === 404 && /public-assets\/prepare|public-assets/i.test(message)) {
433
+ return 'This GlashDB API does not support public asset offload yet. Deploy the backend public-assets route and router update, then retry.';
434
+ }
435
+ if (error?.status === 503 && /Public asset storage is not configured|storage is not configured/i.test(message)) {
436
+ return 'Public asset storage is not configured on this GlashDB API. Set the R2 asset storage secrets, redeploy the backend, then retry.';
437
+ }
438
+ return message;
439
+ }
440
+
397
441
  function assetMode(flags) {
398
442
  const raw = String(flags['asset-mode'] ?? flags.assets ?? flags['public-assets'] ?? 'inline').toLowerCase();
399
443
  if (['inline', 'offload', 'auto'].includes(raw)) return raw;
@@ -630,11 +674,29 @@ function normalizeLiveUrl(url) {
630
674
  return url ? `https://${String(url).replace(/^https?:\/\//, '')}` : '';
631
675
  }
632
676
 
677
+ function deployMonitorUrl({ deploymentId, projectId, after } = {}) {
678
+ const base = appBaseUrl();
679
+ if (deploymentId) return `${base}/app/deploy/live/${encodeURIComponent(deploymentId)}`;
680
+ const url = new URL(`${base}/app/deploy/live`);
681
+ if (projectId) url.searchParams.set('projectId', projectId);
682
+ if (after) url.searchParams.set('after', String(after));
683
+ return url.toString();
684
+ }
685
+
686
+ function appBaseUrl() {
687
+ const configured = process.env.GLASH_APP_URL?.trim().replace(/\/$/, '');
688
+ if (configured) return configured;
689
+ const api = process.env.GLASH_API_URL || '';
690
+ if (/^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])/i.test(api)) return 'http://localhost:3000';
691
+ return 'https://glashdb.com';
692
+ }
693
+
633
694
  function printDeployReport(report) {
634
695
  const elapsedMs = Date.now() - report.start;
635
696
  const liveUrl = report.liveUrl ? terminalLink(report.liveUrl) : '-';
636
697
  const rows = [
637
698
  ['Status', formatReportStatus(report.status)],
699
+ ['Project ID', report.projectId || '-'],
638
700
  ['Deployment ID', report.deploymentId || '-'],
639
701
  ['Target', report.target || 'production'],
640
702
  ['Source', report.source || '-'],
@@ -642,6 +704,7 @@ function printDeployReport(report) {
642
704
  ['Public assets', formatAssetSummary(report.assetSummary)],
643
705
  ['Deployment size', formatDeploymentSize(report)],
644
706
  ['Total time spent', formatMinutes(elapsedMs)],
707
+ ['Status link', report.statusUrl ? terminalLink(report.statusUrl) : '-'],
645
708
  ['Live URL', liveUrl],
646
709
  ['Environment', formatEnvSummary(report.envSummary)],
647
710
  ];
@@ -673,14 +736,29 @@ function formatPackagesUploaded(report) {
673
736
  }
674
737
 
675
738
  function formatAssetSummary(assetSummary) {
676
- if (!assetSummary || assetSummary.mode === 'inline') return 'inline';
739
+ if (!assetSummary || assetSummary.mode === 'inline') return 'inline (bundled with source)';
740
+ if (assetSummary.status === 'pending') return `${assetSummary.mode} requested`;
741
+ if (assetSummary.status === 'no-public-assets') return `${assetSummary.mode}, no public assets found`;
742
+ if (assetSummary.status === 'below-threshold') {
743
+ return `auto kept inline (${formatBytes(assetSummary.bytes)} below ${formatBytes(assetSummary.thresholdBytes)})`;
744
+ }
745
+ if (assetSummary.status === 'prepare-failed') {
746
+ return `prepare failed (${(assetSummary.count ?? 0).toLocaleString()} assets, ${formatBytes(assetSummary.bytes)})`;
747
+ }
748
+ if (assetSummary.status === 'upload-failed') {
749
+ return `upload failed (${(assetSummary.uploaded ?? 0).toLocaleString()}/${(assetSummary.count ?? 0).toLocaleString()} uploaded, ${formatBytes(assetSummary.bytes)})`;
750
+ }
751
+ if (assetSummary.status === 'complete-failed') {
752
+ return `activation failed (${(assetSummary.uploaded ?? 0).toLocaleString()} uploaded, ${formatBytes(assetSummary.bytes)})`;
753
+ }
754
+ if (assetSummary.status === 'failed') return `failed${assetSummary.error ? ` (${assetSummary.error})` : ''}`;
677
755
  if (!assetSummary.offloaded) {
678
756
  const size = assetSummary.bytes ? ` (${formatBytes(assetSummary.bytes)})` : '';
679
757
  return `${assetSummary.mode}, no offload${size}`;
680
758
  }
681
759
  const uploaded = assetSummary.uploaded ?? Math.max(0, (assetSummary.count ?? 0) - (assetSummary.skipped ?? 0));
682
760
  const skipped = assetSummary.skipped ? `, ${assetSummary.skipped.toLocaleString()} reused` : '';
683
- return `${assetSummary.count.toLocaleString()} offloaded (${formatBytes(assetSummary.bytes)}; ${uploaded.toLocaleString()} uploaded${skipped})`;
761
+ return `${assetSummary.count.toLocaleString()} assets offloaded (${formatBytes(assetSummary.bytes)}; ${uploaded.toLocaleString()} uploaded${skipped})`;
684
762
  }
685
763
 
686
764
  function formatDeploymentSize(report) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glash/cli",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
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",