@glash/cli 0.2.32 → 0.2.34

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
@@ -68,6 +68,7 @@ Common Node and frontend targets:
68
68
 
69
69
  | Framework or runtime | Notes |
70
70
  | --- | --- |
71
+ | glashjs | Native glashDB framework support; the CLI detects `glashjs`/`glash.config.*` and packages build/start scripts when missing |
71
72
  | Next.js | Built and started with the Next runtime when no custom start script is present |
72
73
  | React | Works with Vite, Create React App, Gatsby, Remix, and custom React servers |
73
74
  | Vue | Works with Vue apps and Nuxt projects |
@@ -93,6 +94,7 @@ Deployment checklist:
93
94
  - Set required secrets with `glash env:add` or the dashboard before deploying.
94
95
  - Make sure the app listens on the expected runtime port.
95
96
  - For large `public/` folders, deploy with `--asset-mode offload` to upload public assets to glashDB object storage/CDN and keep the source bundle small.
97
+ - Source bundles over 90 MB automatically use chunked source upload, keeping each request below Cloudflare Free's 100 MB upload limit.
96
98
 
97
99
  Large public folders:
98
100
 
@@ -107,6 +109,21 @@ glash deploy --asset-mode auto --asset-threshold 100mb
107
109
  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
110
  Public asset offload prepares assets in small batches and uses multipart uploads for large files, so image/video-heavy `public/` folders can upload without forcing the source bundle over the platform limit.
109
111
 
112
+ Large source bundles:
113
+
114
+ ```bash
115
+ # Default: direct upload below 90 MB, chunked upload above it
116
+ glash deploy
117
+
118
+ # Force chunked upload even for a smaller bundle
119
+ glash deploy --source-upload chunked
120
+
121
+ # Tune the auto-chunk threshold
122
+ glash deploy --source-threshold 75mb
123
+ ```
124
+
125
+ For glashjs projects, `glash deploy` detects `glashjs` or `glash.config.mjs`, marks the project as `glashjs`, keeps deploy-safe `.glash` output, and adds missing `build` / `start` scripts inside the deployment bundle without editing your local `package.json`.
126
+
110
127
  ## Commands
111
128
 
112
129
  | Command | Purpose |
@@ -123,6 +140,7 @@ Public asset offload prepares assets in small batches and uses multipart uploads
123
140
  | `glash open` | Open the linked project's primary domain |
124
141
  | `glash deploy [--prod] [--detach]` | Deploy the linked project; streams the pipeline until live |
125
142
  | `glash deploy --asset-mode offload` | Upload `public/` assets to object storage/CDN before zipping source |
143
+ | `glash deploy --source-upload chunked` | Stage the source zip through multipart object storage to bypass Cloudflare's single-request cap |
126
144
  | `glash deployments` | List recent deployments |
127
145
  | `glash status <id>` | Show a deployment's pipeline + events |
128
146
  | `glash logs <id>` | Alias of `status` |
@@ -181,6 +199,8 @@ The commands require PostgreSQL client tools locally: `pg_dump` for dumps,
181
199
  | `GLASH_TOKEN` | — | Use this token instead of `~/.glash/credentials.json` (useful in CI) |
182
200
  | `GLASH_ASSET_OFFLOAD_THRESHOLD` | `100mb` | Default threshold for `glash deploy --asset-mode auto` |
183
201
  | `GLASH_ASSET_BATCH_SIZE` | `50` | Number of public assets prepared per request during `--asset-mode offload` |
202
+ | `GLASH_SOURCE_UPLOAD_MODE` | `auto` | Source upload mode: `auto`, `direct`, or `chunked` |
203
+ | `GLASH_SOURCE_UPLOAD_THRESHOLD` | `90mb` | Default threshold for automatic chunked source upload |
184
204
 
185
205
  ## Requirements
186
206
 
package/bin/glash.mjs CHANGED
@@ -100,6 +100,10 @@ ${color.bold('FLAGS')}
100
100
  --no-env Skip automatic env sync during deploy
101
101
  --asset-mode <mode> Public asset handling: inline, auto, offload
102
102
  --asset-threshold <size> Auto-offload threshold (default: 100mb)
103
+ --source-upload <mode> Source upload: auto, direct, chunked
104
+ --source-threshold <size> Auto-chunk source threshold (default: 90mb)
105
+ --source-concurrency <n> Parallel chunk uploads for large source bundles (default: 4)
106
+ --framework <name> Override framework detection (glashjs, next, vite, etc.)
103
107
  --help, -h Show this help
104
108
  --version, -v Show CLI version
105
109
 
@@ -118,9 +118,10 @@ async function createNewProjectFromCurrentDirectory() {
118
118
  const dirName = basename(process.cwd());
119
119
  const name = (await prompt(`What's your project's name? [${dirName}] `)) || dirName;
120
120
  const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
121
+ const profile = await detectProjectProfile(process.cwd());
121
122
 
122
123
  info(color.dim(`Creating project "${name}"...`));
123
- const p = await apiPost('/projects', { name, slug });
124
+ const p = await apiPost('/projects', { name, slug, framework: profile.framework });
124
125
  await writeProjectLink({ projectId: p.id, slug: p.slug, name: p.name });
125
126
  ok(`Created and linked to ${p.name} (${p.slug})`);
126
127
  return p.id;
@@ -135,6 +136,7 @@ export async function deploy({ flags }) {
135
136
  const gitBranch = flags.branch || currentBranch();
136
137
  const target = flags.prod || flags.production ? 'production' : (flags.target || 'production');
137
138
  const requestedAssetMode = assetMode(flags);
139
+ const projectProfile = remoteDeploy ? { framework: frameworkHint(flags), label: frameworkHint(flags) || 'auto' } : await detectProjectProfile(process.cwd(), flags);
138
140
  const monitorStartedAt = Date.now();
139
141
  const initialStatusUrl = deployMonitorUrl({ projectId, after: monitorStartedAt });
140
142
 
@@ -150,6 +152,8 @@ export async function deploy({ flags }) {
150
152
  deploymentId: '',
151
153
  target,
152
154
  source: remoteDeploy ? 'remote git deploy' : 'local upload',
155
+ framework: projectProfile.framework,
156
+ sourceUpload: { mode: remoteDeploy ? 'remote' : 'pending', parts: 0 },
153
157
  packagesUploaded: 0,
154
158
  filesIncluded: 0,
155
159
  deploymentSizeBytes: 0,
@@ -200,23 +204,46 @@ export async function deploy({ flags }) {
200
204
  indicator.setPhase('packaging');
201
205
  const zipPath = join(tmpdir(), `glash-deploy-${Date.now()}.zip`);
202
206
  try {
203
- const bundle = await zipDirectory(process.cwd(), zipPath, { ignore: assetSummary.offloaded ? ['public/**'] : [] });
207
+ const packageOverrides = await packageOverridesForProfile(process.cwd(), projectProfile);
208
+ const bundle = await zipDirectory(process.cwd(), zipPath, {
209
+ ignore: assetSummary.offloaded ? ['public/**'] : [],
210
+ includeGlashOutput: projectProfile.framework === 'glashjs',
211
+ overrides: packageOverrides,
212
+ });
204
213
  const zipStat = await stat(zipPath);
205
214
  report.filesIncluded = bundle.fileCount ?? 0;
206
215
  report.deploymentSizeBytes = zipStat.size || bundle.bytes || 0;
207
- const formData = new FormData();
208
- formData.append('projectId', projectId);
209
- formData.append('file', await openAsBlob(zipPath, { type: 'application/zip' }), 'project.zip');
210
-
211
- formData.append('commitHash', commitHash || 'local-upload');
212
- formData.append('gitBranch', gitBranch || '');
213
- formData.append('target', target);
214
-
215
- indicator.setPhase('glashing');
216
- dep = await apiPost('/deployments/upload', formData);
216
+ const uploadMeta = {
217
+ commitHash: commitHash || 'local-upload',
218
+ gitBranch: gitBranch || '',
219
+ target,
220
+ framework: projectProfile.framework,
221
+ flags,
222
+ };
223
+
224
+ if (shouldUseChunkedSourceUpload(zipStat.size, flags)) {
225
+ indicator.setPhase('uploading');
226
+ const staged = await uploadSourceBundleStaged(projectId, zipPath, zipStat, uploadMeta, indicator);
227
+ dep = staged.deployment;
228
+ report.source = 'local upload (chunked)';
229
+ report.sourceUpload = { mode: 'chunked', parts: staged.partsUploaded, sha256: staged.sha256 };
230
+ report.packagesUploaded = staged.partsUploaded;
231
+ } else {
232
+ const formData = new FormData();
233
+ formData.append('projectId', projectId);
234
+ formData.append('file', await openAsBlob(zipPath, { type: 'application/zip' }), 'project.zip');
235
+ formData.append('commitHash', uploadMeta.commitHash);
236
+ formData.append('gitBranch', uploadMeta.gitBranch);
237
+ formData.append('target', uploadMeta.target);
238
+ if (uploadMeta.framework) formData.append('framework', uploadMeta.framework);
239
+
240
+ indicator.setPhase('glashing');
241
+ dep = await apiPost('/deployments/upload', formData);
242
+ report.sourceUpload = { mode: 'direct', parts: 1 };
243
+ report.packagesUploaded = 1;
244
+ }
217
245
  report.deploymentId = dep?.id ?? '';
218
246
  report.statusUrl = deployMonitorUrl({ deploymentId: dep?.id, projectId, after: monitorStartedAt });
219
- report.packagesUploaded = 1;
220
247
  await rm(zipPath, { force: true }).catch(() => {});
221
248
  } catch (e) {
222
249
  await rm(zipPath, { force: true }).catch(() => {});
@@ -227,7 +254,7 @@ export async function deploy({ flags }) {
227
254
  } else {
228
255
  indicator.setPhase('hatching');
229
256
  try {
230
- dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target });
257
+ dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target, framework: projectProfile.framework });
231
258
  report.deploymentId = dep?.id ?? '';
232
259
  report.statusUrl = deployMonitorUrl({ deploymentId: dep?.id, projectId, after: monitorStartedAt });
233
260
  } catch (e) {
@@ -470,6 +497,119 @@ async function uploadMultipartPublicAsset(asset, local) {
470
497
  }
471
498
  }
472
499
 
500
+ async function uploadSourceBundleStaged(projectId, zipPath, zipStat, meta, indicator) {
501
+ const sizeBytes = zipStat.size;
502
+ const sha256 = await hashFile(zipPath);
503
+ let prepared;
504
+ try {
505
+ prepared = await apiPost('/deployments/source-upload/prepare', {
506
+ projectId,
507
+ fileName: 'project.zip',
508
+ sizeBytes,
509
+ sha256,
510
+ contentType: 'application/zip',
511
+ framework: meta.framework,
512
+ }, { timeoutMs: 120_000 });
513
+ } catch (e) {
514
+ throw sourceUploadFailure(e);
515
+ }
516
+
517
+ if (!prepared?.uploadPartUrl || !prepared?.completeUrl || !prepared?.uploadId) {
518
+ throw new Error('Chunked source upload was not prepared by this GlashDB API.');
519
+ }
520
+
521
+ const partSize = sourceUploadPartSize(prepared.partSize);
522
+ const blob = await openAsBlob(zipPath, { type: 'application/zip' });
523
+ const chunks = [];
524
+ for (let offset = 0, partNumber = 1; offset < sizeBytes; offset += partSize, partNumber += 1) {
525
+ chunks.push({
526
+ partNumber,
527
+ offset,
528
+ end: Math.min(offset + partSize, sizeBytes),
529
+ });
530
+ }
531
+
532
+ const parts = [];
533
+ let uploadedBytes = 0;
534
+ const concurrency = sourceUploadConcurrency(meta.flags);
535
+ await mapWithConcurrency(chunks, concurrency, async ({ partNumber, offset, end }) => {
536
+ const partUrl = withQueryParam(prepared.uploadPartUrl, 'partNumber', String(partNumber));
537
+ const res = await fetchWithRetries(partUrl, {
538
+ method: 'PUT',
539
+ headers: { 'Content-Type': 'application/octet-stream' },
540
+ body: blob.slice(offset, end, 'application/octet-stream'),
541
+ }, { timeoutMs: uploadTimeoutMsFor(end - offset) });
542
+ if (!res.ok) {
543
+ const detail = await res.text().catch(() => '');
544
+ throw new Error(`Chunked source upload part ${partNumber} failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`);
545
+ }
546
+ let data;
547
+ try { data = await res.json(); } catch { data = {}; }
548
+ const part = data?.part;
549
+ if (!part?.etag || !Number.isInteger(Number(part.partNumber))) {
550
+ throw new Error(`Chunked source upload part ${partNumber} did not return a multipart ETag`);
551
+ }
552
+ parts.push({ partNumber: Number(part.partNumber), etag: String(part.etag) });
553
+ uploadedBytes += (end - offset);
554
+ const percent = 28 + Math.round((uploadedBytes / sizeBytes) * 14);
555
+ indicator?.setProgress?.(percent, 'uploading');
556
+ });
557
+ parts.sort((a, b) => a.partNumber - b.partNumber);
558
+
559
+ const completed = await fetchWithRetries(prepared.completeUrl, {
560
+ method: 'POST',
561
+ headers: { 'Content-Type': 'application/json' },
562
+ body: JSON.stringify({ parts }),
563
+ }, { timeoutMs: 120_000 });
564
+ if (!completed.ok) {
565
+ const detail = await completed.text().catch(() => '');
566
+ throw new Error(`Completing chunked source upload failed with HTTP ${completed.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`);
567
+ }
568
+ let completion;
569
+ try { completion = await completed.json(); } catch { completion = {}; }
570
+ if (!completion?.sourceUrl) {
571
+ throw new Error('Chunked source upload completed without a staged source URL.');
572
+ }
573
+
574
+ indicator?.setProgress?.(43, 'hatching');
575
+ const deployment = await apiPost('/deployments/source-upload/commit', {
576
+ projectId,
577
+ sourceUrl: completion.sourceUrl,
578
+ sha256,
579
+ sizeBytes: String(sizeBytes),
580
+ commitHash: meta.commitHash,
581
+ gitBranch: meta.gitBranch,
582
+ target: meta.target,
583
+ framework: meta.framework,
584
+ }, { timeoutMs: 120_000 });
585
+
586
+ return { deployment, partsUploaded: parts.length, sha256 };
587
+ }
588
+
589
+ async function mapWithConcurrency(items, concurrency, worker) {
590
+ const limit = Math.max(1, Math.min(Number(concurrency) || 1, items.length || 1));
591
+ let next = 0;
592
+ const runners = Array.from({ length: limit }, async () => {
593
+ while (next < items.length) {
594
+ const item = items[next];
595
+ next += 1;
596
+ await worker(item);
597
+ }
598
+ });
599
+ await Promise.all(runners);
600
+ }
601
+
602
+ function sourceUploadFailure(error) {
603
+ const message = error?.message || String(error);
604
+ if (error?.status === 404 && /source-upload|Cannot POST/i.test(message)) {
605
+ return new Error('This GlashDB edge does not support chunked source upload yet. Deploy the router/backend source-upload update, then retry.');
606
+ }
607
+ if (error?.status === 503 && /storage is not configured/i.test(message)) {
608
+ return new Error('Chunked source upload storage is not configured on this GlashDB API.');
609
+ }
610
+ return error instanceof Error ? error : new Error(message);
611
+ }
612
+
473
613
  async function fetchWithRetries(url, init, { retries = 3, timeoutMs = 120_000 } = {}) {
474
614
  let attempt = 0;
475
615
  // eslint-disable-next-line no-constant-condition
@@ -513,7 +653,7 @@ function uploadTimeoutMsFor(bytes) {
513
653
  function publicAssetPartSize(value) {
514
654
  const parsed = Number(value);
515
655
  if (Number.isFinite(parsed) && parsed >= 5 * 1024 * 1024) return Math.round(parsed);
516
- return 16 * 1024 * 1024;
656
+ return 80 * 1024 * 1024;
517
657
  }
518
658
 
519
659
  function assetOffloadFailure(error, assetSummary) {
@@ -542,6 +682,103 @@ function assetMode(flags) {
542
682
  throw new Error(`Unsupported --asset-mode "${raw}". Use inline, auto, or offload.`);
543
683
  }
544
684
 
685
+ function sourceUploadMode(flags) {
686
+ const raw = String(flags['source-upload'] ?? process.env.GLASH_SOURCE_UPLOAD_MODE ?? 'auto').toLowerCase();
687
+ if (['auto', 'direct', 'chunked'].includes(raw)) return raw;
688
+ throw new Error(`Unsupported --source-upload "${raw}". Use auto, direct, or chunked.`);
689
+ }
690
+
691
+ function shouldUseChunkedSourceUpload(sizeBytes, flags) {
692
+ const mode = sourceUploadMode(flags);
693
+ if (mode === 'direct') return false;
694
+ if (mode === 'chunked') return true;
695
+ return Number(sizeBytes) >= sourceUploadThresholdBytes(flags);
696
+ }
697
+
698
+ function sourceUploadThresholdBytes(flags) {
699
+ return parseByteSize(flags['source-threshold'] ?? process.env.GLASH_SOURCE_UPLOAD_THRESHOLD ?? '90mb');
700
+ }
701
+
702
+ function sourceUploadPartSize(value) {
703
+ const parsed = Number(value);
704
+ if (Number.isFinite(parsed) && parsed >= 5 * 1024 * 1024) return Math.round(parsed);
705
+ return 80 * 1024 * 1024;
706
+ }
707
+
708
+ function sourceUploadConcurrency(flags = {}) {
709
+ const raw = flags['source-concurrency'] ?? process.env.GLASH_SOURCE_UPLOAD_CONCURRENCY ?? '4';
710
+ const parsed = Number(raw);
711
+ if (!Number.isFinite(parsed) || parsed < 1) return 4;
712
+ return Math.max(1, Math.min(8, Math.floor(parsed)));
713
+ }
714
+
715
+ async function detectProjectProfile(cwd, flags = {}) {
716
+ const forced = frameworkHint(flags);
717
+ if (forced) return { framework: forced, label: forced };
718
+ const pkg = await readPackageJson(cwd);
719
+ const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
720
+ const scripts = pkg?.scripts ?? {};
721
+ const hasNextConfig = await existsAny(cwd, ['next.config.js', 'next.config.mjs', 'next.config.cjs', 'next.config.ts']);
722
+ const hasGlashConfig = await existsAny(cwd, ['glash.config.mjs', 'glash.config.js', 'glash.config.cjs']);
723
+ const scriptText = Object.values(scripts).join('\n');
724
+ if (deps.next || hasNextConfig || /\bnext\s+(build|dev|start)\b/i.test(scriptText)) return { framework: 'next', label: 'Next.js' };
725
+ if (deps.glashjs || hasGlashConfig || /\bglash\s+(build|serve|dev)\b/i.test(scriptText)) return { framework: 'glashjs', label: 'glashjs' };
726
+ if (deps.nuxt || deps.nuxt3) return { framework: 'nuxt', label: 'Nuxt' };
727
+ if (deps.vite) return { framework: 'vite', label: 'Vite' };
728
+ if (deps['@remix-run/dev'] || deps.remix) return { framework: 'remix', label: 'Remix' };
729
+ if (deps.astro) return { framework: 'astro', label: 'Astro' };
730
+ if (deps.svelte || deps['@sveltejs/kit']) return { framework: 'sveltekit', label: 'SvelteKit' };
731
+ if (deps.express || deps.fastify || deps['@nestjs/core']) return { framework: 'node', label: 'Node.js' };
732
+ if (pkg) return { framework: 'node', label: 'Node.js' };
733
+ return { framework: 'static', label: 'Static' };
734
+ }
735
+
736
+ async function packageOverridesForProfile(cwd, profile) {
737
+ if (profile?.framework !== 'glashjs') return {};
738
+ const pkg = await readPackageJson(cwd);
739
+ if (!pkg) return {};
740
+ const scripts = { ...(pkg.scripts ?? {}) };
741
+ let changed = false;
742
+ if (!scripts.build) {
743
+ scripts.build = 'glash build';
744
+ changed = true;
745
+ }
746
+ if (!scripts.start) {
747
+ scripts.start = 'glash serve --port ${PORT:-3000}';
748
+ changed = true;
749
+ }
750
+ if (!changed) return {};
751
+ return {
752
+ 'package.json': `${JSON.stringify({ ...pkg, scripts }, null, 2)}\n`,
753
+ };
754
+ }
755
+
756
+ function frameworkHint(flags = {}) {
757
+ const raw = String(flags.framework ?? flags.runtime ?? '').trim().toLowerCase();
758
+ if (!raw) return '';
759
+ const aliases = { nextjs: 'next', glash: 'glashjs', glashdb: 'glashjs', react: 'vite' };
760
+ const normalized = aliases[raw] ?? raw;
761
+ return /^[a-z0-9._-]{1,40}$/.test(normalized) ? normalized : '';
762
+ }
763
+
764
+ async function readPackageJson(cwd) {
765
+ try {
766
+ return JSON.parse(await readFile(join(cwd, 'package.json'), 'utf8'));
767
+ } catch {
768
+ return null;
769
+ }
770
+ }
771
+
772
+ async function existsAny(cwd, files) {
773
+ for (const file of files) {
774
+ try {
775
+ await access(join(cwd, file));
776
+ return true;
777
+ } catch {}
778
+ }
779
+ return false;
780
+ }
781
+
545
782
  async function publicAssetFiles(cwd) {
546
783
  const entries = await fg('public/**/*', {
547
784
  cwd,
@@ -620,7 +857,7 @@ function parseByteSize(value) {
620
857
  if (typeof value === 'number') return value;
621
858
  const raw = String(value || '').trim().toLowerCase();
622
859
  const match = raw.match(/^(\d+(?:\.\d+)?)\s*(b|kb|kib|mb|mib|gb|gib)?$/);
623
- if (!match) throw new Error(`Invalid asset threshold "${value}"`);
860
+ if (!match) throw new Error(`Invalid size "${value}"`);
624
861
  const amount = Number(match[1]);
625
862
  const unit = match[2] || 'b';
626
863
  const multiplier = unit === 'gb' || unit === 'gib'
@@ -805,6 +1042,7 @@ function printDeployReport(report) {
805
1042
  ['Deployment ID', report.deploymentId || '-'],
806
1043
  ['Target', report.target || 'production'],
807
1044
  ['Source', report.source || '-'],
1045
+ ['Framework', report.framework || 'auto'],
808
1046
  ['Packages uploaded', formatPackagesUploaded(report)],
809
1047
  ['Public assets', formatAssetSummary(report.assetSummary)],
810
1048
  ['Deployment size', formatDeploymentSize(report)],
@@ -833,6 +1071,14 @@ function formatReportStatus(status) {
833
1071
 
834
1072
  function formatPackagesUploaded(report) {
835
1073
  const count = report.packagesUploaded ?? 0;
1074
+ if (report.sourceUpload?.mode === 'chunked') {
1075
+ const files = report.filesIncluded ? ` (${report.filesIncluded.toLocaleString()} files included)` : '';
1076
+ return `${count.toLocaleString()} source chunk${count === 1 ? '' : 's'}${files}`;
1077
+ }
1078
+ if (report.sourceUpload?.mode === 'direct') {
1079
+ const files = report.filesIncluded ? ` (${report.filesIncluded.toLocaleString()} files included)` : '';
1080
+ return `1 source bundle${files}`;
1081
+ }
836
1082
  const label = count === 1 ? 'source bundle' : 'source bundles';
837
1083
  const files = report.filesIncluded
838
1084
  ? ` (${report.filesIncluded.toLocaleString()} files included)`
@@ -949,6 +1195,9 @@ function createDeployIndicator() {
949
1195
  if (!nextPhase) return;
950
1196
  setProgress(progressFromPhase(nextPhase), nextPhase);
951
1197
  },
1198
+ setProgress(nextProgress, nextPhase = phase) {
1199
+ setProgress(nextProgress, nextPhase);
1200
+ },
952
1201
  finish(finalPhase = 'complete') {
953
1202
  stopTimer();
954
1203
  phase = finalPhase;
@@ -978,6 +1227,7 @@ function progressFromPhase(phase) {
978
1227
  syncing: 8,
979
1228
  assets: 15,
980
1229
  packaging: 22,
1230
+ uploading: 28,
981
1231
  glashing: 35,
982
1232
  hatching: 45,
983
1233
  waiting: 48,
package/lib/zip.mjs CHANGED
@@ -4,7 +4,7 @@ import { createWriteStream } from 'node:fs';
4
4
  import { join, relative } from 'node:path';
5
5
  import fs from 'fs-extra';
6
6
 
7
- export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
7
+ export async function zipDirectory(sourceDir, outPath, { ignore = [], includeGlashOutput = false, overrides = {} } = {}) {
8
8
  // Default ignores
9
9
  const defaultIgnore = [
10
10
  'node_modules/**',
@@ -32,7 +32,7 @@ export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
32
32
  'build/**',
33
33
  '**/build/**',
34
34
  'out/**',
35
- '**/out/**',
35
+ ...(includeGlashOutput ? [] : ['**/out/**']),
36
36
  'coverage/**',
37
37
  '**/coverage/**',
38
38
  '.turbo/**',
@@ -43,8 +43,19 @@ export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
43
43
  '**/.parcel-cache/**',
44
44
  '.vite/**',
45
45
  '**/.vite/**',
46
- '.glash/**',
47
- '**/.glash/**',
46
+ ...(includeGlashOutput
47
+ ? [
48
+ '.glash/project.json',
49
+ '**/.glash/project.json',
50
+ '.glash/credentials.json',
51
+ '**/.glash/credentials.json',
52
+ '.glash/cache/**',
53
+ '**/.glash/cache/**',
54
+ ]
55
+ : [
56
+ '.glash/**',
57
+ '**/.glash/**',
58
+ ]),
48
59
  '*.log',
49
60
  '**/*.log',
50
61
  'npm-debug.log*',
@@ -63,20 +74,21 @@ export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
63
74
  '**/.env.development',
64
75
  ];
65
76
  const allIgnore = [...new Set([...defaultIgnore, ...loadIgnorePatterns(sourceDir), ...ignore])];
66
- const files = await fg('**/*', {
77
+ const overridePaths = new Set(Object.keys(overrides));
78
+ const files = (await fg('**/*', {
67
79
  cwd: sourceDir,
68
80
  ignore: allIgnore,
69
81
  dot: true,
70
82
  onlyFiles: true,
71
83
  followSymbolicLinks: false,
72
- });
84
+ })).filter((file) => !overridePaths.has(file));
73
85
 
74
86
  return new Promise((resolve, reject) => {
75
87
  const zip = new yazl.ZipFile();
76
88
  const output = createWriteStream(outPath);
77
89
  let bytes = 0;
78
90
 
79
- output.on('close', () => resolve({ bytes, fileCount: files.length, files }));
91
+ output.on('close', () => resolve({ bytes, fileCount: files.length + overridePaths.size, files: [...files, ...overridePaths] }));
80
92
  output.on('error', reject);
81
93
  zip.outputStream.on('data', (chunk) => { bytes += chunk.length; });
82
94
  zip.outputStream.on('error', reject);
@@ -85,6 +97,9 @@ export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
85
97
  for (const file of files) {
86
98
  zip.addFile(join(sourceDir, file), file);
87
99
  }
100
+ for (const [file, content] of Object.entries(overrides)) {
101
+ zip.addBuffer(Buffer.isBuffer(content) ? content : Buffer.from(String(content)), file);
102
+ }
88
103
  zip.end();
89
104
  });
90
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glash/cli",
3
- "version": "0.2.32",
3
+ "version": "0.2.34",
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",