launchpd 1.0.2 → 1.0.3
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/bin/cli.js +15 -2
- package/bin/setup.js +4 -4
- package/package.json +4 -2
- package/src/commands/auth.js +329 -92
- package/src/commands/deploy.js +105 -31
- package/src/commands/index.js +1 -2
- package/src/commands/list.js +11 -11
- package/src/commands/rollback.js +4 -4
- package/src/commands/status.js +10 -10
- package/src/commands/versions.js +11 -11
- package/src/config.js +18 -1
- package/src/utils/api.js +116 -14
- package/src/utils/errors.js +124 -0
- package/src/utils/index.js +1 -0
- package/src/utils/logger.js +38 -17
- package/src/utils/machineId.js +2 -1
- package/src/utils/metadata.js +9 -9
- package/src/utils/projectConfig.js +3 -2
- package/src/utils/prompt.js +10 -0
- package/src/utils/quota.js +73 -51
- package/src/utils/upload.js +2 -1
package/bin/cli.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import fs from 'node:fs';
|
|
3
4
|
import { Command } from 'commander';
|
|
4
|
-
import
|
|
5
|
+
import updateNotifier from 'update-notifier';
|
|
6
|
+
import { deploy, list, rollback, versions, init, status, login, logout, register, whoami, quota, resendEmailVerification } from '../src/commands/index.js';
|
|
7
|
+
|
|
8
|
+
const packageJson = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
|
|
9
|
+
updateNotifier({ pkg: packageJson }).notify();
|
|
5
10
|
|
|
6
11
|
|
|
7
12
|
const program = new Command();
|
|
@@ -9,7 +14,7 @@ const program = new Command();
|
|
|
9
14
|
program
|
|
10
15
|
.name('launchpd')
|
|
11
16
|
.description('Deploy static sites instantly to a live URL')
|
|
12
|
-
.version(
|
|
17
|
+
.version(packageJson.version);
|
|
13
18
|
|
|
14
19
|
program
|
|
15
20
|
.command('deploy')
|
|
@@ -22,6 +27,7 @@ program
|
|
|
22
27
|
.option('--force', 'Force deployment even with warnings')
|
|
23
28
|
.option('-o, --open', 'Open the site URL in the default browser after deployment')
|
|
24
29
|
.option('--verbose', 'Show detailed error information')
|
|
30
|
+
.option('--qr', 'Show QR code for deployment')
|
|
25
31
|
.action(async (folder, options) => {
|
|
26
32
|
await deploy(folder || '.', options);
|
|
27
33
|
});
|
|
@@ -108,4 +114,11 @@ program
|
|
|
108
114
|
await quota();
|
|
109
115
|
});
|
|
110
116
|
|
|
117
|
+
program
|
|
118
|
+
.command('verify')
|
|
119
|
+
.description('Resend email verification')
|
|
120
|
+
.action(async () => {
|
|
121
|
+
await resendEmailVerification();
|
|
122
|
+
});
|
|
123
|
+
|
|
111
124
|
program.parseAsync();
|
package/bin/setup.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
import { config } from '../src/config.js';
|
|
4
2
|
import { info, success } from '../src/utils/logger.js';
|
|
5
3
|
import chalk from 'chalk';
|
|
@@ -34,7 +32,9 @@ async function setup() {
|
|
|
34
32
|
success('No configuration needed - just deploy!');
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
try {
|
|
36
|
+
await setup();
|
|
37
|
+
} catch (err) {
|
|
38
38
|
console.error(`Setup failed: ${err.message}`);
|
|
39
39
|
process.exit(1);
|
|
40
|
-
}
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "launchpd",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Deploy static sites instantly to a live URL",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"static",
|
|
@@ -54,7 +54,9 @@
|
|
|
54
54
|
"launchpd": "^1.0.0",
|
|
55
55
|
"mime-types": "^2.1.35",
|
|
56
56
|
"nanoid": "^5.1.0",
|
|
57
|
-
"ora": "^8.0.1"
|
|
57
|
+
"ora": "^8.0.1",
|
|
58
|
+
"qrcode": "^1.5.4",
|
|
59
|
+
"update-notifier": "^7.3.1"
|
|
58
60
|
},
|
|
59
61
|
"devDependencies": {
|
|
60
62
|
"@eslint/js": "^9.39.2",
|
package/src/commands/auth.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { exec } from 'node:child_process';
|
|
7
|
-
import { promptSecret } from '../utils/prompt.js';
|
|
7
|
+
import { promptSecret, prompt } from '../utils/prompt.js';
|
|
8
8
|
import { config } from '../config.js';
|
|
9
9
|
import { getCredentials, saveCredentials, clearCredentials, isLoggedIn } from '../utils/credentials.js';
|
|
10
|
-
import { success, error, errorWithSuggestions, info, warning, spinner } from '../utils/logger.js';
|
|
10
|
+
import { success, error, errorWithSuggestions, info, warning, spinner, log } from '../utils/logger.js';
|
|
11
11
|
import { formatBytes } from '../utils/quota.js';
|
|
12
|
+
import { handleCommonError, MaintenanceError } from '../utils/errors.js';
|
|
13
|
+
import { serverLogout, resendVerification } from '../utils/api.js';
|
|
12
14
|
import chalk from 'chalk';
|
|
13
15
|
|
|
14
16
|
const API_BASE_URL = config.apiUrl;
|
|
@@ -55,20 +57,116 @@ async function updateCredentialsIfNeeded(creds, result) {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
|
-
* Login
|
|
60
|
+
* Login with email and password (supports 2FA)
|
|
59
61
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
async function loginWithEmailPassword() {
|
|
63
|
+
const email = await prompt('Email: ');
|
|
64
|
+
if (!email) {
|
|
65
|
+
error('Email is required');
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const password = await promptSecret('Password: ');
|
|
70
|
+
if (!password) {
|
|
71
|
+
error('Password is required');
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const loginSpinner = spinner('Authenticating...');
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// First login attempt
|
|
79
|
+
let response = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({ email, password }),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Handle maintenance mode
|
|
86
|
+
if (response.status === 503) {
|
|
87
|
+
const data = await response.json().catch(() => ({}));
|
|
88
|
+
if (data.maintenance_mode) {
|
|
89
|
+
loginSpinner.fail('Service unavailable');
|
|
90
|
+
throw new MaintenanceError(data.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let data = await response.json();
|
|
95
|
+
|
|
96
|
+
// Handle 2FA requirement
|
|
97
|
+
if (data.requires_2fa) {
|
|
98
|
+
loginSpinner.stop();
|
|
99
|
+
|
|
100
|
+
const codeType = data.two_factor_type === 'email'
|
|
101
|
+
? 'email verification code'
|
|
102
|
+
: 'authenticator code';
|
|
103
|
+
|
|
104
|
+
if (data.two_factor_type === 'email') {
|
|
105
|
+
log('');
|
|
106
|
+
info('📧 A verification code has been sent to your email');
|
|
107
|
+
} else {
|
|
108
|
+
log('');
|
|
109
|
+
info('🔐 Two-factor authentication required');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const twoFactorCode = await prompt(`Enter ${codeType}: `);
|
|
113
|
+
|
|
114
|
+
if (!twoFactorCode) {
|
|
115
|
+
error('Verification code is required');
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const verifySpinner = spinner('Verifying code...');
|
|
120
|
+
|
|
121
|
+
// Retry with 2FA code
|
|
122
|
+
response = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
email,
|
|
127
|
+
password,
|
|
128
|
+
two_factor_code: twoFactorCode
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
data = await response.json();
|
|
133
|
+
|
|
134
|
+
if (!data.success) {
|
|
135
|
+
verifySpinner.fail('Verification failed');
|
|
136
|
+
error(data.message || 'Invalid verification code');
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
verifySpinner.succeed('Verified!');
|
|
141
|
+
} else if (data.success) {
|
|
142
|
+
loginSpinner.succeed('Authenticated!');
|
|
143
|
+
} else {
|
|
144
|
+
loginSpinner.fail('Authentication failed');
|
|
145
|
+
error(data.message || 'Invalid email or password');
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return data;
|
|
150
|
+
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (handleCommonError(err, { error, info, warning })) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (err.message.includes('fetch failed') || err.message.includes('ENOTFOUND')) {
|
|
156
|
+
error('Unable to connect to LaunchPd servers');
|
|
157
|
+
info('Check your internet connection');
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
throw err;
|
|
67
161
|
}
|
|
162
|
+
}
|
|
68
163
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Login with API key (original method)
|
|
166
|
+
*/
|
|
167
|
+
async function loginWithApiKey() {
|
|
168
|
+
log('Enter your API key from the dashboard.');
|
|
169
|
+
log(`Don't have one? Run ${chalk.cyan('"launchpd register"')} first.\n`);
|
|
72
170
|
|
|
73
171
|
const apiKey = await promptSecret('API Key: ');
|
|
74
172
|
|
|
@@ -78,7 +176,7 @@ export async function login() {
|
|
|
78
176
|
`Visit: https://${config.domain}/settings`,
|
|
79
177
|
'Run "launchpd register" if you don\'t have an account',
|
|
80
178
|
]);
|
|
81
|
-
|
|
179
|
+
return null;
|
|
82
180
|
}
|
|
83
181
|
|
|
84
182
|
const validateSpinner = spinner('Validating API key...');
|
|
@@ -92,7 +190,53 @@ export async function login() {
|
|
|
92
190
|
'Make sure you copied the full key',
|
|
93
191
|
'API keys start with "lpd_"',
|
|
94
192
|
]);
|
|
95
|
-
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
validateSpinner.succeed('Logged in successfully!');
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
...result,
|
|
200
|
+
apiKey, // Include the API key for saving
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Login command - prompts for API key and validates it
|
|
206
|
+
*/
|
|
207
|
+
export async function login() {
|
|
208
|
+
// Check if already logged in
|
|
209
|
+
if (await isLoggedIn()) {
|
|
210
|
+
const creds = await getCredentials();
|
|
211
|
+
warning(`Already logged in as ${chalk.cyan(creds.email || creds.userId)}`);
|
|
212
|
+
info('Run "launchpd logout" to switch accounts');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
log('\nLaunchpd Login\n');
|
|
217
|
+
log('Choose login method:');
|
|
218
|
+
log(` ${chalk.cyan('1.')} API Key ${chalk.gray('(from dashboard)')}`);
|
|
219
|
+
log(` ${chalk.cyan('2.')} Email & Password ${chalk.gray('(supports 2FA)')}`);
|
|
220
|
+
log('');
|
|
221
|
+
|
|
222
|
+
const choice = await prompt('Enter choice (1 or 2): ');
|
|
223
|
+
|
|
224
|
+
let result;
|
|
225
|
+
let apiKey;
|
|
226
|
+
|
|
227
|
+
if (choice === '2') {
|
|
228
|
+
result = await loginWithEmailPassword();
|
|
229
|
+
if (!result) {
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
// Extract API key from user data
|
|
233
|
+
apiKey = result.user?.api_key;
|
|
234
|
+
} else {
|
|
235
|
+
result = await loginWithApiKey();
|
|
236
|
+
if (!result) {
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
apiKey = result.apiKey;
|
|
96
240
|
}
|
|
97
241
|
|
|
98
242
|
// Save credentials
|
|
@@ -104,17 +248,26 @@ export async function login() {
|
|
|
104
248
|
tier: result.tier,
|
|
105
249
|
});
|
|
106
250
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
251
|
+
log('');
|
|
252
|
+
log(` ${chalk.gray('Email:')} ${chalk.cyan(result.user?.email || 'N/A')}`);
|
|
253
|
+
log(` ${chalk.gray('Tier:')} ${chalk.green(result.tier || 'registered')}`);
|
|
254
|
+
if (result.usage) {
|
|
255
|
+
log(` ${chalk.gray('Sites:')} ${result.usage?.siteCount || 0}/${result.limits?.maxSites || '?'}`);
|
|
256
|
+
log(` ${chalk.gray('Storage:')} ${result.usage?.storageUsedMB || 0}MB/${result.limits?.maxStorageMB || '?'}MB`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Warn if email not verified
|
|
260
|
+
if (result.user?.email && !result.user?.email_verified) {
|
|
261
|
+
log('');
|
|
262
|
+
warning('⚠️ Your email is not verified');
|
|
263
|
+
info(`Verify at: https://${config.domain}/auth/verify-pending`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
log('');
|
|
114
267
|
}
|
|
115
268
|
|
|
116
269
|
/**
|
|
117
|
-
* Logout command - clears stored credentials
|
|
270
|
+
* Logout command - clears stored credentials and invalidates server session
|
|
118
271
|
*/
|
|
119
272
|
export async function logout() {
|
|
120
273
|
const loggedIn = await isLoggedIn();
|
|
@@ -125,21 +278,29 @@ export async function logout() {
|
|
|
125
278
|
}
|
|
126
279
|
|
|
127
280
|
const creds = await getCredentials();
|
|
281
|
+
|
|
282
|
+
// Try server-side logout (best effort - don't fail if it doesn't work)
|
|
283
|
+
try {
|
|
284
|
+
await serverLogout();
|
|
285
|
+
} catch {
|
|
286
|
+
// Ignore server logout errors - we'll clear local creds anyway
|
|
287
|
+
}
|
|
288
|
+
|
|
128
289
|
await clearCredentials();
|
|
129
290
|
|
|
130
291
|
success('Logged out successfully');
|
|
131
292
|
if (creds?.email) {
|
|
132
293
|
info(`Was logged in as: ${chalk.cyan(creds.email)}`);
|
|
133
294
|
}
|
|
134
|
-
|
|
295
|
+
log(`\nYou can still deploy anonymously (limited to ${chalk.yellow('3 sites')}, ${chalk.yellow('50MB')}).`);
|
|
135
296
|
}
|
|
136
297
|
|
|
137
298
|
/**
|
|
138
299
|
* Register command - opens browser to registration page
|
|
139
300
|
*/
|
|
140
301
|
export async function register() {
|
|
141
|
-
|
|
142
|
-
|
|
302
|
+
log('\nRegister for Launchpd\n');
|
|
303
|
+
log(`Opening registration page: ${chalk.cyan(REGISTER_URL)}\n`);
|
|
143
304
|
|
|
144
305
|
// Open browser based on platform
|
|
145
306
|
const platform = process.platform;
|
|
@@ -155,21 +316,21 @@ export async function register() {
|
|
|
155
316
|
|
|
156
317
|
exec(cmd, (err) => {
|
|
157
318
|
if (err) {
|
|
158
|
-
|
|
319
|
+
log(`Please open this URL in your browser:\n ${chalk.cyan(REGISTER_URL)}\n`);
|
|
159
320
|
}
|
|
160
321
|
});
|
|
161
322
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
323
|
+
log('After registering:');
|
|
324
|
+
log(` 1. Get your API key from the dashboard`);
|
|
325
|
+
log(` 2. Run: ${chalk.cyan('launchpd login')}`);
|
|
326
|
+
log('');
|
|
166
327
|
|
|
167
328
|
info('Registration benefits:');
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
329
|
+
log(` ${chalk.green('✓')} ${chalk.white('10 sites')} ${chalk.gray('(instead of 3)')}`);
|
|
330
|
+
log(` ${chalk.green('✓')} ${chalk.white('100MB storage')} ${chalk.gray('(instead of 50MB)')}`);
|
|
331
|
+
log(` ${chalk.green('✓')} ${chalk.white('30-day retention')} ${chalk.gray('(instead of 7 days)')}`);
|
|
332
|
+
log(` ${chalk.green('✓')} ${chalk.white('10 versions per site')}`);
|
|
333
|
+
log('');
|
|
173
334
|
}
|
|
174
335
|
|
|
175
336
|
/**
|
|
@@ -179,14 +340,14 @@ export async function whoami() {
|
|
|
179
340
|
const creds = await getCredentials();
|
|
180
341
|
|
|
181
342
|
if (!creds) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
343
|
+
log('\n👤 Not logged in (anonymous mode)\n');
|
|
344
|
+
log('Anonymous limits:');
|
|
345
|
+
log(` • ${chalk.white('3 sites')} maximum`);
|
|
346
|
+
log(` • ${chalk.white('50MB')} total storage`);
|
|
347
|
+
log(` • ${chalk.white('7-day')} retention`);
|
|
348
|
+
log(` • ${chalk.white('1 version')} per site`);
|
|
349
|
+
log(`\nRun ${chalk.cyan('"launchpd login"')} to authenticate`);
|
|
350
|
+
log(`Run ${chalk.cyan('"launchpd register"')} to create an account\n`);
|
|
190
351
|
return;
|
|
191
352
|
}
|
|
192
353
|
|
|
@@ -205,38 +366,73 @@ export async function whoami() {
|
|
|
205
366
|
// Background upgrade to apiSecret if missing
|
|
206
367
|
await updateCredentialsIfNeeded(creds, result);
|
|
207
368
|
|
|
208
|
-
|
|
369
|
+
log(`\nLogged in as: ${result.user?.email || result.user?.id}\n`);
|
|
370
|
+
|
|
371
|
+
log('Account Info:');
|
|
372
|
+
log(` User ID: ${result.user?.id}`);
|
|
373
|
+
|
|
374
|
+
// Email with verification status
|
|
375
|
+
const emailStatus = result.user?.email_verified
|
|
376
|
+
? chalk.green('✓ Verified')
|
|
377
|
+
: chalk.yellow('⚠ Unverified');
|
|
378
|
+
log(` Email: ${result.user?.email || 'Not set'} ${emailStatus}`);
|
|
379
|
+
|
|
380
|
+
// Enhanced 2FA display
|
|
381
|
+
const hasTOTP = result.user?.is_2fa_enabled;
|
|
382
|
+
const hasEmail2FA = result.user?.is_email_2fa_enabled;
|
|
209
383
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
384
|
+
if (hasTOTP && hasEmail2FA) {
|
|
385
|
+
log(` 2FA: ${chalk.green('✓ Enabled')} ${chalk.gray('(App + Email)')}`);
|
|
386
|
+
} else if (hasTOTP) {
|
|
387
|
+
log(` 2FA: ${chalk.green('✓ Enabled')} ${chalk.gray('(Authenticator App)')}`);
|
|
388
|
+
} else if (hasEmail2FA) {
|
|
389
|
+
log(` 2FA: ${chalk.green('✓ Enabled')} ${chalk.gray('(Email)')}`);
|
|
390
|
+
} else {
|
|
391
|
+
log(` 2FA: ${chalk.gray('Not enabled')}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
log(` Tier: ${result.tier}`);
|
|
395
|
+
log('');
|
|
216
396
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
397
|
+
log('Usage:');
|
|
398
|
+
log(` Sites: ${result.usage?.siteCount || 0} / ${result.limits?.maxSites}`);
|
|
399
|
+
log(` Storage: ${result.usage?.storageUsedMB || 0}MB / ${result.limits?.maxStorageMB}MB`);
|
|
400
|
+
log(` Sites remaining: ${result.usage?.sitesRemaining || 0}`);
|
|
401
|
+
log(` Storage remaining: ${result.usage?.storageRemainingMB || 0}MB`);
|
|
402
|
+
log('');
|
|
223
403
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
404
|
+
log('Limits:');
|
|
405
|
+
log(` Max versions per site: ${result.limits?.maxVersionsPerSite}`);
|
|
406
|
+
log(` Retention: ${result.limits?.retentionDays} days`);
|
|
407
|
+
log('');
|
|
228
408
|
|
|
229
409
|
// Show warnings
|
|
230
410
|
if (result.warnings && result.warnings.length > 0) {
|
|
231
|
-
|
|
232
|
-
result.warnings.forEach(w =>
|
|
233
|
-
|
|
411
|
+
log('⚠️ Warnings:');
|
|
412
|
+
result.warnings.forEach(w => log(` ${w}`));
|
|
413
|
+
log('');
|
|
234
414
|
}
|
|
235
415
|
|
|
236
416
|
if (!result.canCreateNewSite) {
|
|
237
417
|
warning('You cannot create new sites (limit reached)');
|
|
238
418
|
info('You can still update existing sites');
|
|
239
419
|
}
|
|
420
|
+
|
|
421
|
+
// Email verification warning
|
|
422
|
+
if (result.user?.email && !result.user?.email_verified) {
|
|
423
|
+
log('');
|
|
424
|
+
warning('⚠️ Your email is not verified');
|
|
425
|
+
info(`Verify at: https://${config.domain}/auth/verify-pending`);
|
|
426
|
+
info('Some features may be limited until verified');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 2FA recommendation if not enabled
|
|
430
|
+
if (!result.user?.is_2fa_enabled && !result.user?.is_email_2fa_enabled) {
|
|
431
|
+
log('');
|
|
432
|
+
info('💡 Tip: Enable 2FA for better security');
|
|
433
|
+
const securityUrl = `https://${config.domain}/settings/security`;
|
|
434
|
+
log(` ${chalk.gray('Visit: ' + securityUrl)}`);
|
|
435
|
+
}
|
|
240
436
|
}
|
|
241
437
|
|
|
242
438
|
/**
|
|
@@ -246,25 +442,25 @@ export async function quota() {
|
|
|
246
442
|
const creds = await getCredentials();
|
|
247
443
|
|
|
248
444
|
if (!creds) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
445
|
+
log(`\n${chalk.bold('Anonymous Quota Status')}\n`);
|
|
446
|
+
log(chalk.gray('You are not logged in.'));
|
|
447
|
+
log('');
|
|
448
|
+
log(chalk.bold('Anonymous tier limits:'));
|
|
449
|
+
log(chalk.gray(' ┌─────────────────────────────────┐'));
|
|
450
|
+
log(chalk.gray(' │') + ` Sites: ${chalk.white('3 maximum')} ` + chalk.gray('│'));
|
|
451
|
+
log(chalk.gray(' │') + ` Storage: ${chalk.white('50MB total')} ` + chalk.gray('│'));
|
|
452
|
+
log(chalk.gray(' │') + ` Retention: ${chalk.white('7 days')} ` + chalk.gray('│'));
|
|
453
|
+
log(chalk.gray(' │') + ` Versions: ${chalk.white('1 per site')} ` + chalk.gray('│'));
|
|
454
|
+
log(chalk.gray(' └─────────────────────────────────┘'));
|
|
455
|
+
log('');
|
|
456
|
+
log(`${chalk.cyan('Register for FREE')} to unlock more:`);
|
|
457
|
+
log(` ${chalk.green('→')} ${chalk.white('10 sites')}`);
|
|
458
|
+
log(` ${chalk.green('→')} ${chalk.white('100MB storage')}`);
|
|
459
|
+
log(` ${chalk.green('→')} ${chalk.white('30-day retention')}`);
|
|
460
|
+
log(` ${chalk.green('→')} ${chalk.white('10 versions per site')}`);
|
|
461
|
+
log('');
|
|
462
|
+
log(`Run: ${chalk.cyan('launchpd register')}`);
|
|
463
|
+
log('');
|
|
268
464
|
return;
|
|
269
465
|
}
|
|
270
466
|
|
|
@@ -285,7 +481,7 @@ export async function quota() {
|
|
|
285
481
|
await updateCredentialsIfNeeded(creds, result);
|
|
286
482
|
|
|
287
483
|
fetchSpinner.succeed('Quota fetched');
|
|
288
|
-
|
|
484
|
+
log(`\n${chalk.bold('Quota Status for:')} ${chalk.cyan(result.user?.email || creds.email)}\n`);
|
|
289
485
|
|
|
290
486
|
// Sites usage
|
|
291
487
|
const sitesUsed = result.usage?.siteCount || 0;
|
|
@@ -293,7 +489,7 @@ export async function quota() {
|
|
|
293
489
|
const sitesPercent = Math.round((sitesUsed / sitesMax) * 100);
|
|
294
490
|
const sitesBar = createProgressBar(sitesUsed, sitesMax);
|
|
295
491
|
|
|
296
|
-
|
|
492
|
+
log(`${chalk.gray('Sites:')} ${sitesBar} ${chalk.white(sitesUsed)}/${sitesMax} (${getPercentColor(sitesPercent)})`);
|
|
297
493
|
|
|
298
494
|
// Storage usage
|
|
299
495
|
const storageBytes = result.usage?.storageUsed || 0;
|
|
@@ -301,13 +497,13 @@ export async function quota() {
|
|
|
301
497
|
const storagePercent = Math.round((storageBytes / storageMaxBytes) * 100);
|
|
302
498
|
const storageBar = createProgressBar(storageBytes, storageMaxBytes);
|
|
303
499
|
|
|
304
|
-
|
|
500
|
+
log(`${chalk.gray('Storage:')} ${storageBar} ${chalk.white(formatBytes(storageBytes))}/${formatBytes(storageMaxBytes)} (${getPercentColor(storagePercent)})`);
|
|
305
501
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
502
|
+
log('');
|
|
503
|
+
log(`${chalk.gray('Tier:')} ${chalk.green(result.tier || 'free')}`);
|
|
504
|
+
log(`${chalk.gray('Retention:')} ${chalk.white(result.limits?.retentionDays || 30)} days`);
|
|
505
|
+
log(`${chalk.gray('Max versions:')} ${chalk.white(result.limits?.maxVersionsPerSite || 10)} per site`);
|
|
506
|
+
log('');
|
|
311
507
|
|
|
312
508
|
// Status indicators
|
|
313
509
|
if (result.canCreateNewSite === false) {
|
|
@@ -318,7 +514,7 @@ export async function quota() {
|
|
|
318
514
|
warning(`Storage ${storagePercent}% used - consider cleaning up old deployments`);
|
|
319
515
|
}
|
|
320
516
|
|
|
321
|
-
|
|
517
|
+
log('');
|
|
322
518
|
}
|
|
323
519
|
|
|
324
520
|
/**
|
|
@@ -355,3 +551,44 @@ function getPercentColor(percent) {
|
|
|
355
551
|
}
|
|
356
552
|
return chalk.green(`${percent}%`);
|
|
357
553
|
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Resend email verification command
|
|
557
|
+
*/
|
|
558
|
+
export async function resendEmailVerification() {
|
|
559
|
+
const loggedIn = await isLoggedIn();
|
|
560
|
+
|
|
561
|
+
if (!loggedIn) {
|
|
562
|
+
error('Not logged in');
|
|
563
|
+
info(`Run ${chalk.cyan('"launchpd login"')} first`);
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const sendSpinner = spinner('Requesting verification email...');
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
const result = await resendVerification();
|
|
571
|
+
|
|
572
|
+
if (result.success) {
|
|
573
|
+
sendSpinner.succeed('Verification email sent!');
|
|
574
|
+
info('Check your inbox and spam folder');
|
|
575
|
+
} else {
|
|
576
|
+
sendSpinner.fail('Failed to send verification email');
|
|
577
|
+
error(result.message || 'Unknown error');
|
|
578
|
+
if (result.seconds_remaining) {
|
|
579
|
+
info(`Please wait ${result.seconds_remaining} seconds before trying again`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
} catch (err) {
|
|
583
|
+
sendSpinner.fail('Request failed');
|
|
584
|
+
if (handleCommonError(err, { error, info, warning })) {
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
if (err.message?.includes('already verified')) {
|
|
588
|
+
success('Your email is already verified!');
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
error(err.message || 'Failed to resend verification email');
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
}
|