@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 +4 -0
- package/bin/glash.mjs +1 -0
- package/commands/deploy.mjs +206 -11
- package/commands/login.mjs +95 -2
- package/package.json +1 -1
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
|
|
package/commands/deploy.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
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
|
|
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)],
|
package/commands/login.mjs
CHANGED
|
@@ -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
|
-
|
|
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