@glash/cli 0.2.28 → 0.2.29
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 +194 -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
|
|
|
@@ -135,6 +137,7 @@ export async function deploy({ flags }) {
|
|
|
135
137
|
|
|
136
138
|
let dep;
|
|
137
139
|
let envSummary = { count: 0, skipped: Boolean(flags['no-env']) };
|
|
140
|
+
let assetSummary = { mode: assetMode(flags), offloaded: false, count: 0, bytes: 0, skipped: 0 };
|
|
138
141
|
let currentPhase = 'doing';
|
|
139
142
|
const start = Date.now();
|
|
140
143
|
const indicator = createDeployIndicator();
|
|
@@ -148,6 +151,7 @@ export async function deploy({ flags }) {
|
|
|
148
151
|
filesIncluded: 0,
|
|
149
152
|
deploymentSizeBytes: 0,
|
|
150
153
|
envSummary,
|
|
154
|
+
assetSummary,
|
|
151
155
|
liveUrl: '',
|
|
152
156
|
error: '',
|
|
153
157
|
start,
|
|
@@ -166,12 +170,29 @@ export async function deploy({ flags }) {
|
|
|
166
170
|
return fail(`Env sync failed: ${e.message}`);
|
|
167
171
|
}
|
|
168
172
|
}
|
|
173
|
+
|
|
174
|
+
if (!remoteDeploy && assetSummary.mode !== 'inline') {
|
|
175
|
+
indicator.setPhase('assets');
|
|
176
|
+
try {
|
|
177
|
+
assetSummary = await offloadPublicAssetsBeforeDeploy(projectId, flags);
|
|
178
|
+
report.assetSummary = assetSummary;
|
|
179
|
+
} catch (e) {
|
|
180
|
+
indicator.fail('failed');
|
|
181
|
+
printDeployReport({ ...report, assetSummary, status: 'failed', error: `Public asset offload failed: ${e.message}` });
|
|
182
|
+
return fail(`Public asset offload failed: ${e.message}`);
|
|
183
|
+
}
|
|
184
|
+
} else if (assetSummary.mode !== 'inline') {
|
|
185
|
+
indicator.fail('failed');
|
|
186
|
+
const message = '--asset-mode is only available for local source uploads, not remote git deploys.';
|
|
187
|
+
printDeployReport({ ...report, status: 'failed', error: message });
|
|
188
|
+
return fail(message);
|
|
189
|
+
}
|
|
169
190
|
|
|
170
191
|
if (!remoteDeploy) {
|
|
171
192
|
indicator.setPhase('packaging');
|
|
172
193
|
const zipPath = join(tmpdir(), `glash-deploy-${Date.now()}.zip`);
|
|
173
194
|
try {
|
|
174
|
-
const bundle = await zipDirectory(process.cwd(), zipPath);
|
|
195
|
+
const bundle = await zipDirectory(process.cwd(), zipPath, { ignore: assetSummary.offloaded ? ['public/**'] : [] });
|
|
175
196
|
const zipStat = await stat(zipPath);
|
|
176
197
|
report.filesIncluded = bundle.fileCount ?? 0;
|
|
177
198
|
report.deploymentSizeBytes = zipStat.size || bundle.bytes || 0;
|
|
@@ -312,6 +333,163 @@ async function syncEnvBeforeDeploy(projectId, target, flags) {
|
|
|
312
333
|
return { count: vars.length, files };
|
|
313
334
|
}
|
|
314
335
|
|
|
336
|
+
async function offloadPublicAssetsBeforeDeploy(projectId, flags) {
|
|
337
|
+
const mode = assetMode(flags);
|
|
338
|
+
const empty = { mode, offloaded: false, count: 0, bytes: 0, skipped: 0 };
|
|
339
|
+
if (mode === 'inline') return empty;
|
|
340
|
+
|
|
341
|
+
const files = await publicAssetFiles(process.cwd());
|
|
342
|
+
if (!files.length) return empty;
|
|
343
|
+
|
|
344
|
+
const totalBytes = files.reduce((sum, file) => sum + file.sizeBytes, 0);
|
|
345
|
+
const threshold = assetOffloadThresholdBytes(flags);
|
|
346
|
+
if (mode === 'auto' && totalBytes < threshold) {
|
|
347
|
+
return { ...empty, bytes: totalBytes };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const assets = [];
|
|
351
|
+
for (const file of files) {
|
|
352
|
+
assets.push({
|
|
353
|
+
path: file.path,
|
|
354
|
+
sizeBytes: file.sizeBytes,
|
|
355
|
+
sha256: await hashFile(file.absolutePath),
|
|
356
|
+
contentType: contentTypeFor(file.path),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let uploaded = 0;
|
|
361
|
+
let skipped = 0;
|
|
362
|
+
for (const batch of chunk(assets, 100)) {
|
|
363
|
+
const prepared = await apiPost(`/projects/${projectId}/public-assets/prepare`, { assets: batch });
|
|
364
|
+
for (const asset of prepared.assets ?? []) {
|
|
365
|
+
if (asset.skipped) {
|
|
366
|
+
skipped += 1;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (!asset.uploadUrl) throw new Error(`No upload URL returned for ${asset.path}`);
|
|
370
|
+
const local = assets.find((item) => item.path === asset.path);
|
|
371
|
+
if (!local) throw new Error(`Prepared unknown asset ${asset.path}`);
|
|
372
|
+
const blob = await openAsBlob(join(process.cwd(), 'public', asset.path), { type: local.contentType });
|
|
373
|
+
const res = await fetch(asset.uploadUrl, {
|
|
374
|
+
method: asset.method || 'PUT',
|
|
375
|
+
headers: { 'Content-Type': local.contentType },
|
|
376
|
+
body: blob,
|
|
377
|
+
});
|
|
378
|
+
if (!res.ok) {
|
|
379
|
+
const detail = await res.text().catch(() => '');
|
|
380
|
+
throw new Error(`Upload ${asset.path} failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`);
|
|
381
|
+
}
|
|
382
|
+
uploaded += 1;
|
|
383
|
+
}
|
|
384
|
+
await apiPost(`/projects/${projectId}/public-assets/complete`, { assets: batch });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
mode,
|
|
389
|
+
offloaded: true,
|
|
390
|
+
count: assets.length,
|
|
391
|
+
uploaded,
|
|
392
|
+
skipped,
|
|
393
|
+
bytes: totalBytes,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function assetMode(flags) {
|
|
398
|
+
const raw = String(flags['asset-mode'] ?? flags.assets ?? flags['public-assets'] ?? 'inline').toLowerCase();
|
|
399
|
+
if (['inline', 'offload', 'auto'].includes(raw)) return raw;
|
|
400
|
+
throw new Error(`Unsupported --asset-mode "${raw}". Use inline, auto, or offload.`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function publicAssetFiles(cwd) {
|
|
404
|
+
const entries = await fg('public/**/*', {
|
|
405
|
+
cwd,
|
|
406
|
+
onlyFiles: true,
|
|
407
|
+
dot: true,
|
|
408
|
+
followSymbolicLinks: false,
|
|
409
|
+
});
|
|
410
|
+
const out = [];
|
|
411
|
+
for (const entry of entries) {
|
|
412
|
+
const absolutePath = join(cwd, entry);
|
|
413
|
+
const s = await stat(absolutePath);
|
|
414
|
+
out.push({
|
|
415
|
+
path: entry.replace(/^public\//, ''),
|
|
416
|
+
absolutePath,
|
|
417
|
+
sizeBytes: s.size,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function hashFile(file) {
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
425
|
+
const hash = createHash('sha256');
|
|
426
|
+
const stream = createReadStream(file);
|
|
427
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
428
|
+
stream.on('error', reject);
|
|
429
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function contentTypeFor(path) {
|
|
434
|
+
const ext = extname(path).toLowerCase();
|
|
435
|
+
const types = {
|
|
436
|
+
'.avif': 'image/avif',
|
|
437
|
+
'.css': 'text/css; charset=utf-8',
|
|
438
|
+
'.gif': 'image/gif',
|
|
439
|
+
'.html': 'text/html; charset=utf-8',
|
|
440
|
+
'.ico': 'image/x-icon',
|
|
441
|
+
'.jpeg': 'image/jpeg',
|
|
442
|
+
'.jpg': 'image/jpeg',
|
|
443
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
444
|
+
'.json': 'application/json',
|
|
445
|
+
'.m4v': 'video/x-m4v',
|
|
446
|
+
'.map': 'application/json',
|
|
447
|
+
'.mp3': 'audio/mpeg',
|
|
448
|
+
'.mp4': 'video/mp4',
|
|
449
|
+
'.ogg': 'audio/ogg',
|
|
450
|
+
'.otf': 'font/otf',
|
|
451
|
+
'.pdf': 'application/pdf',
|
|
452
|
+
'.png': 'image/png',
|
|
453
|
+
'.svg': 'image/svg+xml',
|
|
454
|
+
'.ttf': 'font/ttf',
|
|
455
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
456
|
+
'.wav': 'audio/wav',
|
|
457
|
+
'.webm': 'video/webm',
|
|
458
|
+
'.webp': 'image/webp',
|
|
459
|
+
'.woff': 'font/woff',
|
|
460
|
+
'.woff2': 'font/woff2',
|
|
461
|
+
'.xml': 'application/xml',
|
|
462
|
+
};
|
|
463
|
+
return types[ext] || 'application/octet-stream';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function assetOffloadThresholdBytes(flags) {
|
|
467
|
+
return parseByteSize(flags['asset-threshold'] ?? process.env.GLASH_ASSET_OFFLOAD_THRESHOLD ?? '100mb');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function parseByteSize(value) {
|
|
471
|
+
if (typeof value === 'number') return value;
|
|
472
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
473
|
+
const match = raw.match(/^(\d+(?:\.\d+)?)\s*(b|kb|kib|mb|mib|gb|gib)?$/);
|
|
474
|
+
if (!match) throw new Error(`Invalid asset threshold "${value}"`);
|
|
475
|
+
const amount = Number(match[1]);
|
|
476
|
+
const unit = match[2] || 'b';
|
|
477
|
+
const multiplier = unit === 'gb' || unit === 'gib'
|
|
478
|
+
? 1024 ** 3
|
|
479
|
+
: unit === 'mb' || unit === 'mib'
|
|
480
|
+
? 1024 ** 2
|
|
481
|
+
: unit === 'kb' || unit === 'kib'
|
|
482
|
+
? 1024
|
|
483
|
+
: 1;
|
|
484
|
+
return Math.round(amount * multiplier);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function chunk(items, size) {
|
|
488
|
+
const out = [];
|
|
489
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
|
|
315
493
|
async function resolveEnvFiles(target, flags) {
|
|
316
494
|
if (flags['env-file']) {
|
|
317
495
|
const files = String(flags['env-file']).split(',').map((f) => f.trim()).filter(Boolean);
|
|
@@ -461,6 +639,7 @@ function printDeployReport(report) {
|
|
|
461
639
|
['Target', report.target || 'production'],
|
|
462
640
|
['Source', report.source || '-'],
|
|
463
641
|
['Packages uploaded', formatPackagesUploaded(report)],
|
|
642
|
+
['Public assets', formatAssetSummary(report.assetSummary)],
|
|
464
643
|
['Deployment size', formatDeploymentSize(report)],
|
|
465
644
|
['Total time spent', formatMinutes(elapsedMs)],
|
|
466
645
|
['Live URL', liveUrl],
|
|
@@ -493,6 +672,17 @@ function formatPackagesUploaded(report) {
|
|
|
493
672
|
return `${count.toLocaleString()} ${label}${files}`;
|
|
494
673
|
}
|
|
495
674
|
|
|
675
|
+
function formatAssetSummary(assetSummary) {
|
|
676
|
+
if (!assetSummary || assetSummary.mode === 'inline') return 'inline';
|
|
677
|
+
if (!assetSummary.offloaded) {
|
|
678
|
+
const size = assetSummary.bytes ? ` (${formatBytes(assetSummary.bytes)})` : '';
|
|
679
|
+
return `${assetSummary.mode}, no offload${size}`;
|
|
680
|
+
}
|
|
681
|
+
const uploaded = assetSummary.uploaded ?? Math.max(0, (assetSummary.count ?? 0) - (assetSummary.skipped ?? 0));
|
|
682
|
+
const skipped = assetSummary.skipped ? `, ${assetSummary.skipped.toLocaleString()} reused` : '';
|
|
683
|
+
return `${assetSummary.count.toLocaleString()} offloaded (${formatBytes(assetSummary.bytes)}; ${uploaded.toLocaleString()} uploaded${skipped})`;
|
|
684
|
+
}
|
|
685
|
+
|
|
496
686
|
function formatDeploymentSize(report) {
|
|
497
687
|
if (report.deploymentSizeBytes) return formatBytes(report.deploymentSizeBytes);
|
|
498
688
|
return report.source === 'remote git deploy' ? 'not measured for remote deploy' : '-';
|
|
@@ -603,6 +793,7 @@ function progressFromPhase(phase) {
|
|
|
603
793
|
const progressByPhase = {
|
|
604
794
|
doing: 2,
|
|
605
795
|
syncing: 8,
|
|
796
|
+
assets: 15,
|
|
606
797
|
packaging: 22,
|
|
607
798
|
glashing: 35,
|
|
608
799
|
hatching: 45,
|
package/package.json
CHANGED