@glash/cli 0.2.33 → 0.2.35

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
@@ -109,6 +109,10 @@ glash deploy --asset-mode auto --asset-threshold 100mb
109
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.
110
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.
111
111
 
112
+ Framework/runtime checks:
113
+
114
+ `glash deploy` detects both the project framework and the runtime scripts in `package.json`. If they disagree, for example the project looks like Next.js but `start` calls `glash serve`, the CLI stops before upload and asks you to align the scripts or explicitly continue. Use `--ignore-framework-mismatch` only when the mismatch is intentional.
115
+
112
116
  Large source bundles:
113
117
 
114
118
  ```bash
package/bin/glash.mjs CHANGED
@@ -102,7 +102,9 @@ ${color.bold('FLAGS')}
102
102
  --asset-threshold <size> Auto-offload threshold (default: 100mb)
103
103
  --source-upload <mode> Source upload: auto, direct, chunked
104
104
  --source-threshold <size> Auto-chunk source threshold (default: 90mb)
105
+ --source-concurrency <n> Parallel chunk uploads for large source bundles (default: 4)
105
106
  --framework <name> Override framework detection (glashjs, next, vite, etc.)
107
+ --ignore-framework-mismatch Continue when detected framework and runtime scripts differ
106
108
  --help, -h Show this help
107
109
  --version, -v Show CLI version
108
110
 
@@ -9,7 +9,7 @@ import { join, basename, extname } from 'node:path';
9
9
  import { tmpdir } from 'node:os';
10
10
  import { createReadStream, openAsBlob } from 'node:fs';
11
11
  import { access, readFile, rm, stat } from 'node:fs/promises';
12
- import { stdout } from 'node:process';
12
+ import { stdin, stdout } from 'node:process';
13
13
 
14
14
  function currentCommit() {
15
15
  try {
@@ -137,6 +137,10 @@ export async function deploy({ flags }) {
137
137
  const target = flags.prod || flags.production ? 'production' : (flags.target || 'production');
138
138
  const requestedAssetMode = assetMode(flags);
139
139
  const projectProfile = remoteDeploy ? { framework: frameworkHint(flags), label: frameworkHint(flags) || 'auto' } : await detectProjectProfile(process.cwd(), flags);
140
+ if (!remoteDeploy) {
141
+ const compatible = await confirmFrameworkRuntimeCompatibility(projectProfile, flags);
142
+ if (!compatible) return fail('Deployment cancelled. Align the framework/runtime scripts or rerun with --ignore-framework-mismatch.');
143
+ }
140
144
  const monitorStartedAt = Date.now();
141
145
  const initialStatusUrl = deployMonitorUrl({ projectId, after: monitorStartedAt });
142
146
 
@@ -153,6 +157,7 @@ export async function deploy({ flags }) {
153
157
  target,
154
158
  source: remoteDeploy ? 'remote git deploy' : 'local upload',
155
159
  framework: projectProfile.framework,
160
+ runtime: projectProfile.runtime?.summary || '',
156
161
  sourceUpload: { mode: remoteDeploy ? 'remote' : 'pending', parts: 0 },
157
162
  packagesUploaded: 0,
158
163
  filesIncluded: 0,
@@ -218,6 +223,7 @@ export async function deploy({ flags }) {
218
223
  gitBranch: gitBranch || '',
219
224
  target,
220
225
  framework: projectProfile.framework,
226
+ flags,
221
227
  };
222
228
 
223
229
  if (shouldUseChunkedSourceUpload(zipStat.size, flags)) {
@@ -519,10 +525,19 @@ async function uploadSourceBundleStaged(projectId, zipPath, zipStat, meta, indic
519
525
 
520
526
  const partSize = sourceUploadPartSize(prepared.partSize);
521
527
  const blob = await openAsBlob(zipPath, { type: 'application/zip' });
528
+ const chunks = [];
529
+ for (let offset = 0, partNumber = 1; offset < sizeBytes; offset += partSize, partNumber += 1) {
530
+ chunks.push({
531
+ partNumber,
532
+ offset,
533
+ end: Math.min(offset + partSize, sizeBytes),
534
+ });
535
+ }
536
+
522
537
  const parts = [];
523
- let partNumber = 1;
524
- for (let offset = 0; offset < sizeBytes; offset += partSize) {
525
- const end = Math.min(offset + partSize, sizeBytes);
538
+ let uploadedBytes = 0;
539
+ const concurrency = sourceUploadConcurrency(meta.flags);
540
+ await mapWithConcurrency(chunks, concurrency, async ({ partNumber, offset, end }) => {
526
541
  const partUrl = withQueryParam(prepared.uploadPartUrl, 'partNumber', String(partNumber));
527
542
  const res = await fetchWithRetries(partUrl, {
528
543
  method: 'PUT',
@@ -540,10 +555,11 @@ async function uploadSourceBundleStaged(projectId, zipPath, zipStat, meta, indic
540
555
  throw new Error(`Chunked source upload part ${partNumber} did not return a multipart ETag`);
541
556
  }
542
557
  parts.push({ partNumber: Number(part.partNumber), etag: String(part.etag) });
543
- const percent = 28 + Math.round((end / sizeBytes) * 14);
558
+ uploadedBytes += (end - offset);
559
+ const percent = 28 + Math.round((uploadedBytes / sizeBytes) * 14);
544
560
  indicator?.setProgress?.(percent, 'uploading');
545
- partNumber += 1;
546
- }
561
+ });
562
+ parts.sort((a, b) => a.partNumber - b.partNumber);
547
563
 
548
564
  const completed = await fetchWithRetries(prepared.completeUrl, {
549
565
  method: 'POST',
@@ -575,6 +591,19 @@ async function uploadSourceBundleStaged(projectId, zipPath, zipStat, meta, indic
575
591
  return { deployment, partsUploaded: parts.length, sha256 };
576
592
  }
577
593
 
594
+ async function mapWithConcurrency(items, concurrency, worker) {
595
+ const limit = Math.max(1, Math.min(Number(concurrency) || 1, items.length || 1));
596
+ let next = 0;
597
+ const runners = Array.from({ length: limit }, async () => {
598
+ while (next < items.length) {
599
+ const item = items[next];
600
+ next += 1;
601
+ await worker(item);
602
+ }
603
+ });
604
+ await Promise.all(runners);
605
+ }
606
+
578
607
  function sourceUploadFailure(error) {
579
608
  const message = error?.message || String(error);
580
609
  if (error?.status === 404 && /source-upload|Cannot POST/i.test(message)) {
@@ -629,7 +658,7 @@ function uploadTimeoutMsFor(bytes) {
629
658
  function publicAssetPartSize(value) {
630
659
  const parsed = Number(value);
631
660
  if (Number.isFinite(parsed) && parsed >= 5 * 1024 * 1024) return Math.round(parsed);
632
- return 16 * 1024 * 1024;
661
+ return 80 * 1024 * 1024;
633
662
  }
634
663
 
635
664
  function assetOffloadFailure(error, assetSummary) {
@@ -678,31 +707,205 @@ function sourceUploadThresholdBytes(flags) {
678
707
  function sourceUploadPartSize(value) {
679
708
  const parsed = Number(value);
680
709
  if (Number.isFinite(parsed) && parsed >= 5 * 1024 * 1024) return Math.round(parsed);
681
- return 16 * 1024 * 1024;
710
+ return 80 * 1024 * 1024;
711
+ }
712
+
713
+ function sourceUploadConcurrency(flags = {}) {
714
+ const raw = flags['source-concurrency'] ?? process.env.GLASH_SOURCE_UPLOAD_CONCURRENCY ?? '4';
715
+ const parsed = Number(raw);
716
+ if (!Number.isFinite(parsed) || parsed < 1) return 4;
717
+ return Math.max(1, Math.min(8, Math.floor(parsed)));
682
718
  }
683
719
 
684
720
  async function detectProjectProfile(cwd, flags = {}) {
721
+ const runtime = await inspectProjectRuntime(cwd);
685
722
  const forced = frameworkHint(flags);
686
- if (forced) return { framework: forced, label: forced };
723
+ const profile = forced
724
+ ? { framework: forced, label: frameworkLabel(forced), forced: true }
725
+ : inferFrameworkFromRuntime(runtime);
726
+ return { ...profile, runtime: summarizeRuntimeCompatibility(profile, runtime) };
727
+ }
728
+
729
+ async function inspectProjectRuntime(cwd) {
687
730
  const pkg = await readPackageJson(cwd);
688
731
  const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
732
+ const scripts = pkg?.scripts ?? {};
733
+ const scriptText = Object.values(scripts).join('\n');
734
+ const hasDockerfile = await existsAny(cwd, ['Dockerfile', 'dockerfile']);
735
+ const hasNextConfig = await existsAny(cwd, ['next.config.js', 'next.config.mjs', 'next.config.cjs', 'next.config.ts']);
689
736
  const hasGlashConfig = await existsAny(cwd, ['glash.config.mjs', 'glash.config.js', 'glash.config.cjs']);
690
- if (deps.glashjs || hasGlashConfig) return { framework: 'glashjs', label: 'glashjs' };
691
- if (deps.next) return { framework: 'next', label: 'Next.js' };
737
+ const scriptCalls = {
738
+ build: scriptFrameworkCalls(scripts.build),
739
+ start: scriptFrameworkCalls(scripts.start),
740
+ dev: scriptFrameworkCalls(scripts.dev),
741
+ };
742
+ return {
743
+ pkg,
744
+ deps,
745
+ scripts,
746
+ scriptText,
747
+ hasDockerfile,
748
+ hasNextConfig,
749
+ hasGlashConfig,
750
+ buildFramework: scriptCalls.build[0] || '',
751
+ startFramework: scriptCalls.start[0] || '',
752
+ devFramework: scriptCalls.dev[0] || '',
753
+ buildCalls: scriptCalls.build,
754
+ startCalls: scriptCalls.start,
755
+ devCalls: scriptCalls.dev,
756
+ };
757
+ }
758
+
759
+ function inferFrameworkFromRuntime(runtime) {
760
+ const { deps, scriptText } = runtime;
761
+ if (runtime.hasDockerfile) return { framework: 'dockerfile', label: 'Dockerfile' };
762
+ if (deps.next || runtime.hasNextConfig || /\bnext\s+(build|dev|start)\b/i.test(scriptText)) return { framework: 'next', label: 'Next.js' };
763
+ if (deps.glashjs || runtime.hasGlashConfig || /\bglash(?:db)?\s+(build|serve|dev|start)\b/i.test(scriptText)) return { framework: 'glashjs', label: 'glashjs' };
692
764
  if (deps.nuxt || deps.nuxt3) return { framework: 'nuxt', label: 'Nuxt' };
693
765
  if (deps.vite) return { framework: 'vite', label: 'Vite' };
694
766
  if (deps['@remix-run/dev'] || deps.remix) return { framework: 'remix', label: 'Remix' };
695
767
  if (deps.astro) return { framework: 'astro', label: 'Astro' };
696
768
  if (deps.svelte || deps['@sveltejs/kit']) return { framework: 'sveltekit', label: 'SvelteKit' };
697
769
  if (deps.express || deps.fastify || deps['@nestjs/core']) return { framework: 'node', label: 'Node.js' };
698
- if (pkg) return { framework: 'node', label: 'Node.js' };
770
+ if (runtime.pkg) return { framework: 'node', label: 'Node.js' };
699
771
  return { framework: 'static', label: 'Static' };
700
772
  }
701
773
 
774
+ function summarizeRuntimeCompatibility(profile, runtime) {
775
+ const runtimeFramework = runtime.startFramework || runtime.buildFramework || runtime.devFramework || (runtime.hasDockerfile ? 'dockerfile' : '');
776
+ const buildFramework = runtime.buildFramework || '';
777
+ const summary = runtimeFramework
778
+ ? `${frameworkLabel(runtimeFramework)}${runtime.startFramework ? ' start' : runtime.buildFramework ? ' build' : runtime.devFramework ? ' dev' : ''}`
779
+ : runtime.hasDockerfile ? 'Dockerfile' : 'auto';
780
+ const mismatch = frameworkRuntimeMismatch(profile.framework, runtimeFramework, buildFramework, runtime);
781
+ return {
782
+ ...runtime,
783
+ runtimeFramework,
784
+ summary,
785
+ mismatch,
786
+ };
787
+ }
788
+
789
+ function frameworkRuntimeMismatch(framework, runtimeFramework, buildFramework, runtime) {
790
+ if (!framework || framework === 'auto' || framework === 'dockerfile' || framework === 'static') return null;
791
+ if (!runtimeFramework || runtimeFramework === 'dockerfile') return null;
792
+ if (frameworksCompatible(framework, runtimeFramework)) return null;
793
+ const buildStillCallsFramework = buildFramework && frameworksCompatible(framework, buildFramework);
794
+ if (runtime.startFramework === 'node' && buildStillCallsFramework) return null;
795
+ if (!runtime.startFramework && buildStillCallsFramework) return null;
796
+ return {
797
+ framework,
798
+ runtimeFramework,
799
+ buildFramework,
800
+ buildScript: runtime.scripts?.build || '',
801
+ startScript: runtime.scripts?.start || '',
802
+ devScript: runtime.scripts?.dev || '',
803
+ };
804
+ }
805
+
806
+ function frameworksCompatible(a, b) {
807
+ const left = normalizeFrameworkName(a);
808
+ const right = normalizeFrameworkName(b);
809
+ if (!left || !right) return false;
810
+ if (left === right) return true;
811
+ if (left === 'sveltekit' && right === 'vite') return true;
812
+ if (left === 'vite' && right === 'sveltekit') return true;
813
+ return false;
814
+ }
815
+
816
+ function scriptFrameworkCalls(command = '') {
817
+ const text = String(command || '');
818
+ if (!text.trim()) return [];
819
+ const patterns = [
820
+ ['next', /\bnext\s+(?:build|start|dev|export)\b/ig],
821
+ ['glashjs', /\bglash(?:db)?\s+(?:build|serve|dev|start)\b/ig],
822
+ ['nuxt', /\b(?:nuxt|nuxi)\s*(?:build|start|preview|dev|generate)?\b/ig],
823
+ ['astro', /\bastro\s+(?:build|dev|preview)\b/ig],
824
+ ['remix', /\b(?:remix-serve|remix)\b/ig],
825
+ ['sveltekit', /\bsvelte-kit\s+(?:build|dev|preview)\b/ig],
826
+ ['vite', /\bvite(?:\s+(?:build|preview|dev|--host))?\b/ig],
827
+ ['node', /\b(?:node|tsx|ts-node)\b/ig],
828
+ ['node', /\bnest\s+start\b/ig],
829
+ ];
830
+ const matches = [];
831
+ for (const [framework, regex] of patterns) {
832
+ for (const match of text.matchAll(regex)) {
833
+ matches.push({ framework, index: match.index ?? 0 });
834
+ }
835
+ }
836
+ matches.sort((a, b) => a.index - b.index);
837
+ const out = [];
838
+ for (const match of matches) {
839
+ if (!out.includes(match.framework)) out.push(match.framework);
840
+ }
841
+ return out;
842
+ }
843
+
844
+ function normalizeFrameworkName(value) {
845
+ const raw = String(value || '').trim().toLowerCase();
846
+ const aliases = {
847
+ glash: 'glashjs',
848
+ glashdb: 'glashjs',
849
+ nextjs: 'next',
850
+ svelte: 'sveltekit',
851
+ docker: 'dockerfile',
852
+ dockerfile: 'dockerfile',
853
+ };
854
+ return aliases[raw] ?? raw;
855
+ }
856
+
857
+ function frameworkLabel(value) {
858
+ const labels = {
859
+ next: 'Next.js',
860
+ glashjs: 'glashjs',
861
+ nuxt: 'Nuxt',
862
+ vite: 'Vite',
863
+ remix: 'Remix',
864
+ astro: 'Astro',
865
+ sveltekit: 'SvelteKit',
866
+ node: 'Node.js',
867
+ static: 'Static',
868
+ dockerfile: 'Dockerfile',
869
+ auto: 'auto',
870
+ };
871
+ return labels[normalizeFrameworkName(value)] ?? String(value || 'auto');
872
+ }
873
+
874
+ async function confirmFrameworkRuntimeCompatibility(profile, flags = {}) {
875
+ const mismatch = profile?.runtime?.mismatch;
876
+ if (!mismatch) return true;
877
+
878
+ const detected = frameworkLabel(mismatch.framework);
879
+ const runtime = frameworkLabel(mismatch.runtimeFramework);
880
+ warn('');
881
+ warn('Framework/runtime mismatch detected');
882
+ warn(`Your framework is ${color.bold(detected)} while your runtime calls on ${color.bold(runtime)}.`);
883
+ if (mismatch.buildScript) warn(`build: ${color.dim(mismatch.buildScript)}`);
884
+ if (mismatch.startScript) warn(`start: ${color.dim(mismatch.startScript)}`);
885
+ warn('Consider moving to one framework, or ignore this warning to continue building.');
886
+
887
+ if (ignoreFrameworkMismatch(flags)) {
888
+ warn('Continuing because --ignore-framework-mismatch was set.');
889
+ return true;
890
+ }
891
+ if (!stdin.isTTY) return false;
892
+ const answer = await prompt('Ignore mismatch and continue building? [y/N] ');
893
+ return ['y', 'yes', 'ignore', 'continue'].includes(answer.toLowerCase());
894
+ }
895
+
896
+ function ignoreFrameworkMismatch(flags = {}) {
897
+ return Boolean(
898
+ flags['ignore-framework-mismatch']
899
+ || flags['ignore-runtime-mismatch']
900
+ || String(flags['framework-mismatch'] || '').toLowerCase() === 'ignore'
901
+ );
902
+ }
903
+
702
904
  async function packageOverridesForProfile(cwd, profile) {
703
- if (profile?.framework !== 'glashjs') return {};
905
+ const overrides = await glashRuntimeOverrides(cwd);
906
+ if (profile?.framework !== 'glashjs') return overrides;
704
907
  const pkg = await readPackageJson(cwd);
705
- if (!pkg) return {};
908
+ if (!pkg) return overrides;
706
909
  const scripts = { ...(pkg.scripts ?? {}) };
707
910
  let changed = false;
708
911
  if (!scripts.build) {
@@ -713,17 +916,42 @@ async function packageOverridesForProfile(cwd, profile) {
713
916
  scripts.start = 'glash serve --port ${PORT:-3000}';
714
917
  changed = true;
715
918
  }
716
- if (!changed) return {};
919
+ if (!changed) return overrides;
717
920
  return {
921
+ ...overrides,
718
922
  'package.json': `${JSON.stringify({ ...pkg, scripts }, null, 2)}\n`,
719
923
  };
720
924
  }
721
925
 
926
+ const GLASH_PUBLIC_RUNTIME_FILES = [
927
+ 'glash-sw.js',
928
+ 'glash-offline.mjs',
929
+ 'glash-favicon.mjs',
930
+ 'glash-assets.manifest.json',
931
+ 'manifest.webmanifest',
932
+ 'favicon.svg',
933
+ 'favicon-animated.svg',
934
+ ];
935
+
936
+ async function glashRuntimeOverrides(cwd) {
937
+ const overrides = {};
938
+ for (const name of GLASH_PUBLIC_RUNTIME_FILES) {
939
+ const publicPath = `public/${name}`;
940
+ const candidates = [publicPath, `.glash/out/${name}`];
941
+ for (const candidate of candidates) {
942
+ try {
943
+ overrides[publicPath] = await readFile(join(cwd, candidate));
944
+ break;
945
+ } catch {}
946
+ }
947
+ }
948
+ return overrides;
949
+ }
950
+
722
951
  function frameworkHint(flags = {}) {
723
952
  const raw = String(flags.framework ?? flags.runtime ?? '').trim().toLowerCase();
724
953
  if (!raw) return '';
725
- const aliases = { nextjs: 'next', glash: 'glashjs', glashdb: 'glashjs', react: 'vite' };
726
- const normalized = aliases[raw] ?? raw;
954
+ const normalized = normalizeFrameworkName(raw === 'react' ? 'vite' : raw);
727
955
  return /^[a-z0-9._-]{1,40}$/.test(normalized) ? normalized : '';
728
956
  }
729
957
 
@@ -1009,6 +1237,7 @@ function printDeployReport(report) {
1009
1237
  ['Target', report.target || 'production'],
1010
1238
  ['Source', report.source || '-'],
1011
1239
  ['Framework', report.framework || 'auto'],
1240
+ ['Runtime', report.runtime || '-'],
1012
1241
  ['Packages uploaded', formatPackagesUploaded(report)],
1013
1242
  ['Public assets', formatAssetSummary(report.assetSummary)],
1014
1243
  ['Deployment size', formatDeploymentSize(report)],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glash/cli",
3
- "version": "0.2.33",
3
+ "version": "0.2.35",
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",