@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 +3 -0
- package/commands/deploy.mjs +88 -10
- package/package.json +1 -1
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
|
|
package/commands/deploy.mjs
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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