@glash/cli 0.2.28 → 0.2.30
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 +46 -0
- package/bin/glash.mjs +2 -0
- package/commands/deploy.mjs +247 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,50 @@ source ~/.zshrc
|
|
|
60
60
|
npm install -g glashdb
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
## Deployable frameworks
|
|
64
|
+
|
|
65
|
+
`glash deploy` works with more than Next.js. For Node projects, glashDB can infer a build from `package.json` and your lockfile. For anything else, include a `Dockerfile` and glashDB will build and run the container.
|
|
66
|
+
|
|
67
|
+
Common Node and frontend targets:
|
|
68
|
+
|
|
69
|
+
| Framework or runtime | Notes |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| Next.js | Built and started with the Next runtime when no custom start script is present |
|
|
72
|
+
| React | Works with Vite, Create React App, Gatsby, Remix, and custom React servers |
|
|
73
|
+
| Vue | Works with Vue apps and Nuxt projects |
|
|
74
|
+
| Svelte | Works with Svelte and SvelteKit projects |
|
|
75
|
+
| Astro | Works for static or server-rendered Astro projects when the start command is configured |
|
|
76
|
+
| Static sites | Plain HTML/CSS/JS, generated static output, and Hugo-style sites |
|
|
77
|
+
| Node servers | Express, Fastify, NestJS, Hono, and other servers that expose a start script |
|
|
78
|
+
|
|
79
|
+
Containerized targets:
|
|
80
|
+
|
|
81
|
+
| Framework or runtime | Requirement |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| Django, FastAPI, Flask | Add a `Dockerfile` that starts the Python app |
|
|
84
|
+
| Laravel, Rails | Add a `Dockerfile` with the app server and required runtime setup |
|
|
85
|
+
| Go, Rust, Java, Spring | Add a `Dockerfile` that builds and runs the service |
|
|
86
|
+
| Any custom runtime | Add a valid `Dockerfile` and expose the listening port |
|
|
87
|
+
|
|
88
|
+
Deployment checklist:
|
|
89
|
+
|
|
90
|
+
- Add a `build` script if your app needs a production build.
|
|
91
|
+
- Add a `start` script for Node apps, or provide `server.js` / `index.js`.
|
|
92
|
+
- Add a `Dockerfile` for non-Node apps or custom runtime needs.
|
|
93
|
+
- Set required secrets with `glash env:add` or the dashboard before deploying.
|
|
94
|
+
- Make sure the app listens on the expected runtime port.
|
|
95
|
+
- For large `public/` folders, deploy with `--asset-mode offload` to upload public assets to glashDB object storage/CDN and keep the source bundle small.
|
|
96
|
+
|
|
97
|
+
Large public folders:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Always offload public/ before building
|
|
101
|
+
glash deploy --asset-mode offload
|
|
102
|
+
|
|
103
|
+
# Offload only when public/ exceeds the threshold
|
|
104
|
+
glash deploy --asset-mode auto --asset-threshold 100mb
|
|
105
|
+
```
|
|
106
|
+
|
|
63
107
|
## Commands
|
|
64
108
|
|
|
65
109
|
| Command | Purpose |
|
|
@@ -75,6 +119,7 @@ npm install -g glashdb
|
|
|
75
119
|
| `glash link <slug>` | Link the current directory to an existing project |
|
|
76
120
|
| `glash open` | Open the linked project's primary domain |
|
|
77
121
|
| `glash deploy [--prod] [--detach]` | Deploy the linked project; streams the pipeline until live |
|
|
122
|
+
| `glash deploy --asset-mode offload` | Upload `public/` assets to object storage/CDN before zipping source |
|
|
78
123
|
| `glash deployments` | List recent deployments |
|
|
79
124
|
| `glash status <id>` | Show a deployment's pipeline + events |
|
|
80
125
|
| `glash logs <id>` | Alias of `status` |
|
|
@@ -130,6 +175,7 @@ The commands require PostgreSQL client tools locally: `pg_dump` for dumps,
|
|
|
130
175
|
| --- | --- | --- |
|
|
131
176
|
| `GLASH_API_URL` | `https://api.glashdb.com/api` | Override the API base (self-hosted, staging) |
|
|
132
177
|
| `GLASH_TOKEN` | — | Use this token instead of `~/.glash/credentials.json` (useful in CI) |
|
|
178
|
+
| `GLASH_ASSET_OFFLOAD_THRESHOLD` | `100mb` | Default threshold for `glash deploy --asset-mode auto` |
|
|
133
179
|
|
|
134
180
|
## Requirements
|
|
135
181
|
|
package/bin/glash.mjs
CHANGED
|
@@ -98,6 +98,8 @@ ${color.bold('FLAGS')}
|
|
|
98
98
|
--environment <name> Env-var environment (default: production)
|
|
99
99
|
--env-file <file[,file]> Env file(s) to sync before deploy
|
|
100
100
|
--no-env Skip automatic env sync during deploy
|
|
101
|
+
--asset-mode <mode> Public asset handling: inline, auto, offload
|
|
102
|
+
--asset-threshold <size> Auto-offload threshold (default: 100mb)
|
|
101
103
|
--help, -h Show this help
|
|
102
104
|
--version, -v Show CLI version
|
|
103
105
|
|
package/commands/deploy.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { apiGet, apiPost } from '../lib/api.mjs';
|
|
2
2
|
import { readProjectLink, writeProjectLink } from '../lib/config.mjs';
|
|
3
3
|
import { ok, fail, info, warn, color, prompt } from '../lib/ui.mjs';
|
|
4
|
+
import fg from 'fast-glob';
|
|
4
5
|
import { execSync } from 'node:child_process';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
5
7
|
import { zipDirectory } from '../lib/zip.mjs';
|
|
6
|
-
import { join, basename } from 'node:path';
|
|
8
|
+
import { join, basename, extname } from 'node:path';
|
|
7
9
|
import { tmpdir } from 'node:os';
|
|
8
|
-
import { openAsBlob } from 'node:fs';
|
|
10
|
+
import { createReadStream, openAsBlob } from 'node:fs';
|
|
9
11
|
import { access, readFile, rm, stat } from 'node:fs/promises';
|
|
10
12
|
import { stdout } from 'node:process';
|
|
11
13
|
|
|
@@ -132,9 +134,11 @@ export async function deploy({ flags }) {
|
|
|
132
134
|
const commitHash = flags.commit || (remoteDeploy ? undefined : currentCommit());
|
|
133
135
|
const gitBranch = flags.branch || currentBranch();
|
|
134
136
|
const target = flags.prod || flags.production ? 'production' : (flags.target || 'production');
|
|
137
|
+
const requestedAssetMode = assetMode(flags);
|
|
135
138
|
|
|
136
139
|
let dep;
|
|
137
140
|
let envSummary = { count: 0, skipped: Boolean(flags['no-env']) };
|
|
141
|
+
let assetSummary = { mode: requestedAssetMode, status: requestedAssetMode === 'inline' ? 'inline' : 'pending', offloaded: false, count: 0, bytes: 0, skipped: 0 };
|
|
138
142
|
let currentPhase = 'doing';
|
|
139
143
|
const start = Date.now();
|
|
140
144
|
const indicator = createDeployIndicator();
|
|
@@ -148,6 +152,7 @@ export async function deploy({ flags }) {
|
|
|
148
152
|
filesIncluded: 0,
|
|
149
153
|
deploymentSizeBytes: 0,
|
|
150
154
|
envSummary,
|
|
155
|
+
assetSummary,
|
|
151
156
|
liveUrl: '',
|
|
152
157
|
error: '',
|
|
153
158
|
start,
|
|
@@ -166,12 +171,32 @@ export async function deploy({ flags }) {
|
|
|
166
171
|
return fail(`Env sync failed: ${e.message}`);
|
|
167
172
|
}
|
|
168
173
|
}
|
|
174
|
+
|
|
175
|
+
if (!remoteDeploy && assetSummary.mode !== 'inline') {
|
|
176
|
+
indicator.setPhase('assets');
|
|
177
|
+
try {
|
|
178
|
+
assetSummary = await offloadPublicAssetsBeforeDeploy(projectId, flags);
|
|
179
|
+
report.assetSummary = assetSummary;
|
|
180
|
+
} catch (e) {
|
|
181
|
+
const failedAssetSummary = e.assetSummary ?? { ...assetSummary, status: 'failed', error: e.message };
|
|
182
|
+
assetSummary = failedAssetSummary;
|
|
183
|
+
report.assetSummary = failedAssetSummary;
|
|
184
|
+
indicator.fail('failed');
|
|
185
|
+
printDeployReport({ ...report, assetSummary: failedAssetSummary, status: 'failed', error: `Public asset offload failed: ${e.message}` });
|
|
186
|
+
return fail(`Public asset offload failed: ${e.message}`);
|
|
187
|
+
}
|
|
188
|
+
} else if (assetSummary.mode !== 'inline') {
|
|
189
|
+
indicator.fail('failed');
|
|
190
|
+
const message = '--asset-mode is only available for local source uploads, not remote git deploys.';
|
|
191
|
+
printDeployReport({ ...report, status: 'failed', error: message });
|
|
192
|
+
return fail(message);
|
|
193
|
+
}
|
|
169
194
|
|
|
170
195
|
if (!remoteDeploy) {
|
|
171
196
|
indicator.setPhase('packaging');
|
|
172
197
|
const zipPath = join(tmpdir(), `glash-deploy-${Date.now()}.zip`);
|
|
173
198
|
try {
|
|
174
|
-
const bundle = await zipDirectory(process.cwd(), zipPath);
|
|
199
|
+
const bundle = await zipDirectory(process.cwd(), zipPath, { ignore: assetSummary.offloaded ? ['public/**'] : [] });
|
|
175
200
|
const zipStat = await stat(zipPath);
|
|
176
201
|
report.filesIncluded = bundle.fileCount ?? 0;
|
|
177
202
|
report.deploymentSizeBytes = zipStat.size || bundle.bytes || 0;
|
|
@@ -312,6 +337,196 @@ async function syncEnvBeforeDeploy(projectId, target, flags) {
|
|
|
312
337
|
return { count: vars.length, files };
|
|
313
338
|
}
|
|
314
339
|
|
|
340
|
+
async function offloadPublicAssetsBeforeDeploy(projectId, flags) {
|
|
341
|
+
const mode = assetMode(flags);
|
|
342
|
+
const empty = { mode, status: mode === 'inline' ? 'inline' : 'pending', offloaded: false, count: 0, bytes: 0, skipped: 0 };
|
|
343
|
+
if (mode === 'inline') return empty;
|
|
344
|
+
|
|
345
|
+
const files = await publicAssetFiles(process.cwd());
|
|
346
|
+
if (!files.length) return { ...empty, status: 'no-public-assets' };
|
|
347
|
+
|
|
348
|
+
const totalBytes = files.reduce((sum, file) => sum + file.sizeBytes, 0);
|
|
349
|
+
const threshold = assetOffloadThresholdBytes(flags);
|
|
350
|
+
if (mode === 'auto' && totalBytes < threshold) {
|
|
351
|
+
return { ...empty, status: 'below-threshold', count: files.length, bytes: totalBytes, thresholdBytes: threshold };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const assets = [];
|
|
355
|
+
for (const file of files) {
|
|
356
|
+
assets.push({
|
|
357
|
+
path: file.path,
|
|
358
|
+
sizeBytes: file.sizeBytes,
|
|
359
|
+
sha256: await hashFile(file.absolutePath),
|
|
360
|
+
contentType: contentTypeFor(file.path),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let uploaded = 0;
|
|
365
|
+
let skipped = 0;
|
|
366
|
+
for (const batch of chunk(assets, 100)) {
|
|
367
|
+
let prepared;
|
|
368
|
+
try {
|
|
369
|
+
prepared = await apiPost(`/projects/${projectId}/public-assets/prepare`, { assets: batch });
|
|
370
|
+
} catch (e) {
|
|
371
|
+
throw assetOffloadFailure(e, { mode, status: 'prepare-failed', offloaded: false, count: assets.length, uploaded, skipped, bytes: totalBytes });
|
|
372
|
+
}
|
|
373
|
+
for (const asset of prepared.assets ?? []) {
|
|
374
|
+
if (asset.skipped) {
|
|
375
|
+
skipped += 1;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (!asset.uploadUrl) throw new Error(`No upload URL returned for ${asset.path}`);
|
|
379
|
+
const local = assets.find((item) => item.path === asset.path);
|
|
380
|
+
if (!local) throw new Error(`Prepared unknown asset ${asset.path}`);
|
|
381
|
+
const blob = await openAsBlob(join(process.cwd(), 'public', asset.path), { type: local.contentType });
|
|
382
|
+
const res = await fetch(asset.uploadUrl, {
|
|
383
|
+
method: asset.method || 'PUT',
|
|
384
|
+
headers: { 'Content-Type': local.contentType },
|
|
385
|
+
body: blob,
|
|
386
|
+
});
|
|
387
|
+
if (!res.ok) {
|
|
388
|
+
const detail = await res.text().catch(() => '');
|
|
389
|
+
throw assetOffloadFailure(
|
|
390
|
+
new Error(`Upload ${asset.path} failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`),
|
|
391
|
+
{ mode, status: 'upload-failed', offloaded: false, count: assets.length, uploaded, skipped, bytes: totalBytes },
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
uploaded += 1;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
await apiPost(`/projects/${projectId}/public-assets/complete`, { assets: batch });
|
|
398
|
+
} catch (e) {
|
|
399
|
+
throw assetOffloadFailure(e, { mode, status: 'complete-failed', offloaded: false, count: assets.length, uploaded, skipped, bytes: totalBytes });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
mode,
|
|
405
|
+
status: 'offloaded',
|
|
406
|
+
offloaded: true,
|
|
407
|
+
count: assets.length,
|
|
408
|
+
uploaded,
|
|
409
|
+
skipped,
|
|
410
|
+
bytes: totalBytes,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function assetOffloadFailure(error, assetSummary) {
|
|
415
|
+
const message = friendlyAssetOffloadError(error);
|
|
416
|
+
const wrapped = new Error(message);
|
|
417
|
+
wrapped.cause = error;
|
|
418
|
+
wrapped.status = error?.status;
|
|
419
|
+
wrapped.assetSummary = { ...assetSummary, error: message };
|
|
420
|
+
return wrapped;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function friendlyAssetOffloadError(error) {
|
|
424
|
+
const message = error?.message || String(error);
|
|
425
|
+
if (error?.status === 404 && /public-assets\/prepare|public-assets/i.test(message)) {
|
|
426
|
+
return 'This GlashDB API does not support public asset offload yet. Deploy the backend public-assets route and router update, then retry.';
|
|
427
|
+
}
|
|
428
|
+
if (error?.status === 503 && /Public asset storage is not configured|storage is not configured/i.test(message)) {
|
|
429
|
+
return 'Public asset storage is not configured on this GlashDB API. Set the R2 asset storage secrets, redeploy the backend, then retry.';
|
|
430
|
+
}
|
|
431
|
+
return message;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function assetMode(flags) {
|
|
435
|
+
const raw = String(flags['asset-mode'] ?? flags.assets ?? flags['public-assets'] ?? 'inline').toLowerCase();
|
|
436
|
+
if (['inline', 'offload', 'auto'].includes(raw)) return raw;
|
|
437
|
+
throw new Error(`Unsupported --asset-mode "${raw}". Use inline, auto, or offload.`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function publicAssetFiles(cwd) {
|
|
441
|
+
const entries = await fg('public/**/*', {
|
|
442
|
+
cwd,
|
|
443
|
+
onlyFiles: true,
|
|
444
|
+
dot: true,
|
|
445
|
+
followSymbolicLinks: false,
|
|
446
|
+
});
|
|
447
|
+
const out = [];
|
|
448
|
+
for (const entry of entries) {
|
|
449
|
+
const absolutePath = join(cwd, entry);
|
|
450
|
+
const s = await stat(absolutePath);
|
|
451
|
+
out.push({
|
|
452
|
+
path: entry.replace(/^public\//, ''),
|
|
453
|
+
absolutePath,
|
|
454
|
+
sizeBytes: s.size,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function hashFile(file) {
|
|
461
|
+
return new Promise((resolve, reject) => {
|
|
462
|
+
const hash = createHash('sha256');
|
|
463
|
+
const stream = createReadStream(file);
|
|
464
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
465
|
+
stream.on('error', reject);
|
|
466
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function contentTypeFor(path) {
|
|
471
|
+
const ext = extname(path).toLowerCase();
|
|
472
|
+
const types = {
|
|
473
|
+
'.avif': 'image/avif',
|
|
474
|
+
'.css': 'text/css; charset=utf-8',
|
|
475
|
+
'.gif': 'image/gif',
|
|
476
|
+
'.html': 'text/html; charset=utf-8',
|
|
477
|
+
'.ico': 'image/x-icon',
|
|
478
|
+
'.jpeg': 'image/jpeg',
|
|
479
|
+
'.jpg': 'image/jpeg',
|
|
480
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
481
|
+
'.json': 'application/json',
|
|
482
|
+
'.m4v': 'video/x-m4v',
|
|
483
|
+
'.map': 'application/json',
|
|
484
|
+
'.mp3': 'audio/mpeg',
|
|
485
|
+
'.mp4': 'video/mp4',
|
|
486
|
+
'.ogg': 'audio/ogg',
|
|
487
|
+
'.otf': 'font/otf',
|
|
488
|
+
'.pdf': 'application/pdf',
|
|
489
|
+
'.png': 'image/png',
|
|
490
|
+
'.svg': 'image/svg+xml',
|
|
491
|
+
'.ttf': 'font/ttf',
|
|
492
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
493
|
+
'.wav': 'audio/wav',
|
|
494
|
+
'.webm': 'video/webm',
|
|
495
|
+
'.webp': 'image/webp',
|
|
496
|
+
'.woff': 'font/woff',
|
|
497
|
+
'.woff2': 'font/woff2',
|
|
498
|
+
'.xml': 'application/xml',
|
|
499
|
+
};
|
|
500
|
+
return types[ext] || 'application/octet-stream';
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function assetOffloadThresholdBytes(flags) {
|
|
504
|
+
return parseByteSize(flags['asset-threshold'] ?? process.env.GLASH_ASSET_OFFLOAD_THRESHOLD ?? '100mb');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function parseByteSize(value) {
|
|
508
|
+
if (typeof value === 'number') return value;
|
|
509
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
510
|
+
const match = raw.match(/^(\d+(?:\.\d+)?)\s*(b|kb|kib|mb|mib|gb|gib)?$/);
|
|
511
|
+
if (!match) throw new Error(`Invalid asset threshold "${value}"`);
|
|
512
|
+
const amount = Number(match[1]);
|
|
513
|
+
const unit = match[2] || 'b';
|
|
514
|
+
const multiplier = unit === 'gb' || unit === 'gib'
|
|
515
|
+
? 1024 ** 3
|
|
516
|
+
: unit === 'mb' || unit === 'mib'
|
|
517
|
+
? 1024 ** 2
|
|
518
|
+
: unit === 'kb' || unit === 'kib'
|
|
519
|
+
? 1024
|
|
520
|
+
: 1;
|
|
521
|
+
return Math.round(amount * multiplier);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function chunk(items, size) {
|
|
525
|
+
const out = [];
|
|
526
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
527
|
+
return out;
|
|
528
|
+
}
|
|
529
|
+
|
|
315
530
|
async function resolveEnvFiles(target, flags) {
|
|
316
531
|
if (flags['env-file']) {
|
|
317
532
|
const files = String(flags['env-file']).split(',').map((f) => f.trim()).filter(Boolean);
|
|
@@ -457,10 +672,12 @@ function printDeployReport(report) {
|
|
|
457
672
|
const liveUrl = report.liveUrl ? terminalLink(report.liveUrl) : '-';
|
|
458
673
|
const rows = [
|
|
459
674
|
['Status', formatReportStatus(report.status)],
|
|
675
|
+
['Project ID', report.projectId || '-'],
|
|
460
676
|
['Deployment ID', report.deploymentId || '-'],
|
|
461
677
|
['Target', report.target || 'production'],
|
|
462
678
|
['Source', report.source || '-'],
|
|
463
679
|
['Packages uploaded', formatPackagesUploaded(report)],
|
|
680
|
+
['Public assets', formatAssetSummary(report.assetSummary)],
|
|
464
681
|
['Deployment size', formatDeploymentSize(report)],
|
|
465
682
|
['Total time spent', formatMinutes(elapsedMs)],
|
|
466
683
|
['Live URL', liveUrl],
|
|
@@ -493,6 +710,32 @@ function formatPackagesUploaded(report) {
|
|
|
493
710
|
return `${count.toLocaleString()} ${label}${files}`;
|
|
494
711
|
}
|
|
495
712
|
|
|
713
|
+
function formatAssetSummary(assetSummary) {
|
|
714
|
+
if (!assetSummary || assetSummary.mode === 'inline') return 'inline (bundled with source)';
|
|
715
|
+
if (assetSummary.status === 'pending') return `${assetSummary.mode} requested`;
|
|
716
|
+
if (assetSummary.status === 'no-public-assets') return `${assetSummary.mode}, no public assets found`;
|
|
717
|
+
if (assetSummary.status === 'below-threshold') {
|
|
718
|
+
return `auto kept inline (${formatBytes(assetSummary.bytes)} below ${formatBytes(assetSummary.thresholdBytes)})`;
|
|
719
|
+
}
|
|
720
|
+
if (assetSummary.status === 'prepare-failed') {
|
|
721
|
+
return `prepare failed (${(assetSummary.count ?? 0).toLocaleString()} assets, ${formatBytes(assetSummary.bytes)})`;
|
|
722
|
+
}
|
|
723
|
+
if (assetSummary.status === 'upload-failed') {
|
|
724
|
+
return `upload failed (${(assetSummary.uploaded ?? 0).toLocaleString()}/${(assetSummary.count ?? 0).toLocaleString()} uploaded, ${formatBytes(assetSummary.bytes)})`;
|
|
725
|
+
}
|
|
726
|
+
if (assetSummary.status === 'complete-failed') {
|
|
727
|
+
return `activation failed (${(assetSummary.uploaded ?? 0).toLocaleString()} uploaded, ${formatBytes(assetSummary.bytes)})`;
|
|
728
|
+
}
|
|
729
|
+
if (assetSummary.status === 'failed') return `failed${assetSummary.error ? ` (${assetSummary.error})` : ''}`;
|
|
730
|
+
if (!assetSummary.offloaded) {
|
|
731
|
+
const size = assetSummary.bytes ? ` (${formatBytes(assetSummary.bytes)})` : '';
|
|
732
|
+
return `${assetSummary.mode}, no offload${size}`;
|
|
733
|
+
}
|
|
734
|
+
const uploaded = assetSummary.uploaded ?? Math.max(0, (assetSummary.count ?? 0) - (assetSummary.skipped ?? 0));
|
|
735
|
+
const skipped = assetSummary.skipped ? `, ${assetSummary.skipped.toLocaleString()} reused` : '';
|
|
736
|
+
return `${assetSummary.count.toLocaleString()} assets offloaded (${formatBytes(assetSummary.bytes)}; ${uploaded.toLocaleString()} uploaded${skipped})`;
|
|
737
|
+
}
|
|
738
|
+
|
|
496
739
|
function formatDeploymentSize(report) {
|
|
497
740
|
if (report.deploymentSizeBytes) return formatBytes(report.deploymentSizeBytes);
|
|
498
741
|
return report.source === 'remote git deploy' ? 'not measured for remote deploy' : '-';
|
|
@@ -603,6 +846,7 @@ function progressFromPhase(phase) {
|
|
|
603
846
|
const progressByPhase = {
|
|
604
847
|
doing: 2,
|
|
605
848
|
syncing: 8,
|
|
849
|
+
assets: 15,
|
|
606
850
|
packaging: 22,
|
|
607
851
|
glashing: 35,
|
|
608
852
|
hatching: 45,
|
package/package.json
CHANGED