@glash/cli 0.2.35 → 0.2.37

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.
@@ -711,9 +711,12 @@ function sourceUploadPartSize(value) {
711
711
  }
712
712
 
713
713
  function sourceUploadConcurrency(flags = {}) {
714
- const raw = flags['source-concurrency'] ?? process.env.GLASH_SOURCE_UPLOAD_CONCURRENCY ?? '4';
714
+ // Default 3: fewer simultaneous part uploads means each connection gets more of
715
+ // a constrained uplink and finishes before it can be reset mid-stream. Override
716
+ // with --source-concurrency or GLASH_SOURCE_UPLOAD_CONCURRENCY.
717
+ const raw = flags['source-concurrency'] ?? process.env.GLASH_SOURCE_UPLOAD_CONCURRENCY ?? '3';
715
718
  const parsed = Number(raw);
716
- if (!Number.isFinite(parsed) || parsed < 1) return 4;
719
+ if (!Number.isFinite(parsed) || parsed < 1) return 3;
717
720
  return Math.max(1, Math.min(8, Math.floor(parsed)));
718
721
  }
719
722
 
@@ -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.35",
3
+ "version": "0.2.37",
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",