@glash/cli 0.2.34 → 0.2.36

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
@@ -104,6 +104,7 @@ ${color.bold('FLAGS')}
104
104
  --source-threshold <size> Auto-chunk source threshold (default: 90mb)
105
105
  --source-concurrency <n> Parallel chunk uploads for large source bundles (default: 4)
106
106
  --framework <name> Override framework detection (glashjs, next, vite, etc.)
107
+ --ignore-framework-mismatch Continue when detected framework and runtime scripts differ
107
108
  --help, -h Show this help
108
109
  --version, -v Show CLI version
109
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,
@@ -713,30 +718,194 @@ function sourceUploadConcurrency(flags = {}) {
713
718
  }
714
719
 
715
720
  async function detectProjectProfile(cwd, flags = {}) {
721
+ const runtime = await inspectProjectRuntime(cwd);
716
722
  const forced = frameworkHint(flags);
717
- 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) {
718
730
  const pkg = await readPackageJson(cwd);
719
731
  const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
720
732
  const scripts = pkg?.scripts ?? {};
733
+ const scriptText = Object.values(scripts).join('\n');
734
+ const hasDockerfile = await existsAny(cwd, ['Dockerfile', 'dockerfile']);
721
735
  const hasNextConfig = await existsAny(cwd, ['next.config.js', 'next.config.mjs', 'next.config.cjs', 'next.config.ts']);
722
736
  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' };
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' };
726
764
  if (deps.nuxt || deps.nuxt3) return { framework: 'nuxt', label: 'Nuxt' };
727
765
  if (deps.vite) return { framework: 'vite', label: 'Vite' };
728
766
  if (deps['@remix-run/dev'] || deps.remix) return { framework: 'remix', label: 'Remix' };
729
767
  if (deps.astro) return { framework: 'astro', label: 'Astro' };
730
768
  if (deps.svelte || deps['@sveltejs/kit']) return { framework: 'sveltekit', label: 'SvelteKit' };
731
769
  if (deps.express || deps.fastify || deps['@nestjs/core']) return { framework: 'node', label: 'Node.js' };
732
- if (pkg) return { framework: 'node', label: 'Node.js' };
770
+ if (runtime.pkg) return { framework: 'node', label: 'Node.js' };
733
771
  return { framework: 'static', label: 'Static' };
734
772
  }
735
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
+
736
904
  async function packageOverridesForProfile(cwd, profile) {
737
- if (profile?.framework !== 'glashjs') return {};
905
+ const overrides = await glashRuntimeOverrides(cwd);
906
+ if (profile?.framework !== 'glashjs') return overrides;
738
907
  const pkg = await readPackageJson(cwd);
739
- if (!pkg) return {};
908
+ if (!pkg) return overrides;
740
909
  const scripts = { ...(pkg.scripts ?? {}) };
741
910
  let changed = false;
742
911
  if (!scripts.build) {
@@ -747,17 +916,42 @@ async function packageOverridesForProfile(cwd, profile) {
747
916
  scripts.start = 'glash serve --port ${PORT:-3000}';
748
917
  changed = true;
749
918
  }
750
- if (!changed) return {};
919
+ if (!changed) return overrides;
751
920
  return {
921
+ ...overrides,
752
922
  'package.json': `${JSON.stringify({ ...pkg, scripts }, null, 2)}\n`,
753
923
  };
754
924
  }
755
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
+
756
951
  function frameworkHint(flags = {}) {
757
952
  const raw = String(flags.framework ?? flags.runtime ?? '').trim().toLowerCase();
758
953
  if (!raw) return '';
759
- const aliases = { nextjs: 'next', glash: 'glashjs', glashdb: 'glashjs', react: 'vite' };
760
- const normalized = aliases[raw] ?? raw;
954
+ const normalized = normalizeFrameworkName(raw === 'react' ? 'vite' : raw);
761
955
  return /^[a-z0-9._-]{1,40}$/.test(normalized) ? normalized : '';
762
956
  }
763
957
 
@@ -1043,6 +1237,7 @@ function printDeployReport(report) {
1043
1237
  ['Target', report.target || 'production'],
1044
1238
  ['Source', report.source || '-'],
1045
1239
  ['Framework', report.framework || 'auto'],
1240
+ ['Runtime', report.runtime || '-'],
1046
1241
  ['Packages uploaded', formatPackagesUploaded(report)],
1047
1242
  ['Public assets', formatAssetSummary(report.assetSummary)],
1048
1243
  ['Deployment size', formatDeploymentSize(report)],
@@ -1,14 +1,107 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { platform } from 'node:process';
3
+ import { hostname } from 'node:os';
1
4
  import { apiPost } from '../lib/api.mjs';
2
5
  import { writeCreds } from '../lib/config.mjs';
3
- import { prompt, ok, fail } from '../lib/ui.mjs';
6
+ import { prompt, ok, fail, info, warn, color } from '../lib/ui.mjs';
7
+
8
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
9
+
10
+ function openBrowser(url) {
11
+ try {
12
+ const opener = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
13
+ const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
14
+ spawn(opener, args, { stdio: 'ignore', detached: true }).unref();
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
4
20
 
5
21
  export async function login({ flags }) {
22
+ // Explicit password login (headless/CI). Otherwise use browser-based approval.
23
+ if (flags.email || flags.password) {
24
+ return passwordLogin(flags);
25
+ }
26
+ return browserLogin(flags);
27
+ }
28
+
29
+ async function browserLogin(flags) {
30
+ let start;
31
+ try {
32
+ start = await apiPost('/auth/cli/device/start', { deviceName: hostname() });
33
+ } catch (e) {
34
+ // Older backend without device auth — fall back to password login.
35
+ warn('Browser login is unavailable; falling back to email/password.');
36
+ return passwordLogin(flags);
37
+ }
38
+
39
+ const { deviceCode, userCode, verificationUri, verificationUriComplete, interval = 5, expiresIn = 600 } = start;
40
+ const approveUrl = verificationUriComplete || verificationUri;
41
+
42
+ info('');
43
+ info(` To sign in, approve this request in your browser:`);
44
+ info(` ${color.cyan(approveUrl)}`);
45
+ info(` Verification code: ${color.bold(userCode)}`);
46
+ info('');
47
+
48
+ if (flags['no-browser']) {
49
+ info(' Open the link above to continue.');
50
+ } else if (openBrowser(approveUrl)) {
51
+ info(color.dim(' Opened your browser… waiting for approval.'));
52
+ } else {
53
+ info(' Could not open a browser automatically — open the link above.');
54
+ }
55
+
56
+ const deadline = Date.now() + expiresIn * 1000;
57
+ let delay = Math.max(2, interval) * 1000;
58
+ try {
59
+ while (Date.now() < deadline) {
60
+ await sleep(delay);
61
+ let r;
62
+ try {
63
+ r = await apiPost('/auth/cli/device/poll', { deviceCode });
64
+ } catch (e) {
65
+ return fail(e.message);
66
+ }
67
+ if (r?.status === 'approved' && r.accessToken) {
68
+ await writeCreds({ token: r.accessToken, email: r.user?.email });
69
+ return ok(`Signed in as ${r.user?.email ?? 'your account'}`);
70
+ }
71
+ if (r?.status === 'denied') return fail('Sign-in was denied in the browser.');
72
+ if (r?.status === 'expired') return fail('Sign-in request expired. Run `glash login` again.');
73
+ // status === 'pending' → keep polling.
74
+ }
75
+ return fail('Timed out waiting for approval. Run `glash login` again.');
76
+ } catch (e) {
77
+ return fail(e.message);
78
+ }
79
+ }
80
+
81
+ async function passwordLogin(flags) {
6
82
  const email = flags.email || (await prompt('Email: '));
7
83
  const password = flags.password || (await prompt('Password: ', { mask: true }));
8
84
  if (!email || !password) return fail('email and password required');
9
85
 
10
86
  try {
11
- const r = await apiPost('/auth/login', { email, password });
87
+ let r = await apiPost('/auth/login', { email, password });
88
+
89
+ // Second factor is always required. Complete the email-OTP challenge.
90
+ if (!r?.accessToken && r?.secondFactorRequired && !r?.challengeId) {
91
+ r = await apiPost('/auth/login', { email, password, secondFactorMethod: 'email_otp' });
92
+ }
93
+ if (!r?.accessToken && (r?.otpRequired || r?.challengeId)) {
94
+ const code = flags.otp || (await prompt('Enter the 6-digit code sent to your email: '));
95
+ if (!code) throw new Error('A verification code is required to sign in.');
96
+ r = await apiPost('/auth/login', {
97
+ email,
98
+ password,
99
+ emailOtpCode: String(code).trim(),
100
+ loginOtpChallengeId: r.challengeId,
101
+ secondFactorMethod: 'email_otp',
102
+ });
103
+ }
104
+
12
105
  if (!r?.accessToken || !r?.user?.email) {
13
106
  throw new Error('Login response was missing a session token. Please try again.');
14
107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glash/cli",
3
- "version": "0.2.34",
3
+ "version": "0.2.36",
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",