launchpd 1.0.3 → 1.0.6
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/LICENSE +21 -21
- package/README.md +61 -39
- package/bin/cli.js +149 -124
- package/bin/setup.js +42 -40
- package/package.json +4 -4
- package/src/commands/auth.js +516 -522
- package/src/commands/deploy.js +745 -386
- package/src/commands/index.js +14 -7
- package/src/commands/init.js +95 -72
- package/src/commands/list.js +120 -122
- package/src/commands/rollback.js +139 -102
- package/src/commands/status.js +75 -51
- package/src/commands/versions.js +153 -113
- package/src/config.js +32 -31
- package/src/utils/api.js +220 -195
- package/src/utils/credentials.js +88 -85
- package/src/utils/endpoint.js +58 -0
- package/src/utils/errors.js +79 -69
- package/src/utils/expiration.js +49 -47
- package/src/utils/id.js +5 -5
- package/src/utils/ignore.js +35 -36
- package/src/utils/index.js +10 -11
- package/src/utils/localConfig.js +39 -43
- package/src/utils/logger.js +113 -106
- package/src/utils/machineId.js +15 -19
- package/src/utils/metadata.js +113 -87
- package/src/utils/projectConfig.js +48 -45
- package/src/utils/prompt.js +91 -82
- package/src/utils/quota.js +261 -225
- package/src/utils/remoteSource.js +680 -0
- package/src/utils/upload.js +197 -127
- package/src/utils/validator.js +116 -68
package/src/commands/auth.js
CHANGED
|
@@ -3,592 +3,586 @@
|
|
|
3
3
|
* login, logout, register, whoami
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { promptSecret
|
|
8
|
-
import { config } from '../config.js'
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
import { execFile } from 'node:child_process'
|
|
7
|
+
import { promptSecret } from '../utils/prompt.js'
|
|
8
|
+
import { config } from '../config.js'
|
|
9
|
+
import {
|
|
10
|
+
getCredentials,
|
|
11
|
+
saveCredentials,
|
|
12
|
+
clearCredentials,
|
|
13
|
+
isLoggedIn
|
|
14
|
+
} from '../utils/credentials.js'
|
|
15
|
+
import {
|
|
16
|
+
success,
|
|
17
|
+
error,
|
|
18
|
+
errorWithSuggestions,
|
|
19
|
+
info,
|
|
20
|
+
warning,
|
|
21
|
+
spinner,
|
|
22
|
+
log
|
|
23
|
+
} from '../utils/logger.js'
|
|
24
|
+
import { formatBytes } from '../utils/quota.js'
|
|
25
|
+
import { handleCommonError } from '../utils/errors.js'
|
|
26
|
+
import {
|
|
27
|
+
resendVerification,
|
|
28
|
+
createFetchTimeout,
|
|
29
|
+
API_TIMEOUT_MS
|
|
30
|
+
} from '../utils/api.js'
|
|
31
|
+
import chalk from 'chalk'
|
|
32
|
+
|
|
33
|
+
const API_BASE_URL = config.apiUrl
|
|
34
|
+
const REGISTER_URL = `https://${config.domain}/`
|
|
19
35
|
|
|
20
36
|
/**
|
|
21
|
-
* Validate API key
|
|
37
|
+
* Validate API key format
|
|
38
|
+
* Returns true if the key matches expected format: lpd_ followed by alphanumeric/special chars
|
|
22
39
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const data = await response.json();
|
|
36
|
-
if (data.authenticated) {
|
|
37
|
-
return data;
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
} catch {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
40
|
+
function isValidApiKeyFormat (apiKey) {
|
|
41
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
// API keys must start with lpd_ and contain only safe characters
|
|
45
|
+
// This validation ensures we don't send arbitrary file data to the network
|
|
46
|
+
return /^lpd_[a-zA-Z0-9_-]{16,64}$/.test(apiKey)
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
|
-
*
|
|
50
|
+
* Validate API key with the server
|
|
47
51
|
*/
|
|
48
|
-
async function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
async function validateApiKey (apiKey) {
|
|
53
|
+
// Validate API key format before sending to network
|
|
54
|
+
// This ensures we only send properly formatted keys, not arbitrary file data
|
|
55
|
+
if (!isValidApiKeyFormat(apiKey)) {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { signal, clear } = createFetchTimeout(API_TIMEOUT_MS)
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(`${API_BASE_URL}/api/quota`, {
|
|
62
|
+
headers: {
|
|
63
|
+
'X-API-Key': apiKey
|
|
64
|
+
},
|
|
65
|
+
signal
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (response.status === 401) {
|
|
69
|
+
const data = await response.json().catch(() => ({}))
|
|
70
|
+
if (data.requires_2fa) {
|
|
71
|
+
return { requires_2fa: true, two_factor_type: data.two_factor_type }
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
56
74
|
}
|
|
57
|
-
}
|
|
58
75
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*/
|
|
62
|
-
async function loginWithEmailPassword() {
|
|
63
|
-
const email = await prompt('Email: ');
|
|
64
|
-
if (!email) {
|
|
65
|
-
error('Email is required');
|
|
66
|
-
return null;
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
return null
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
return null;
|
|
80
|
+
const data = await response.json()
|
|
81
|
+
if (data.authenticated) {
|
|
82
|
+
return data
|
|
73
83
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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;
|
|
84
|
+
return null
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err.name === 'AbortError') {
|
|
87
|
+
return { timeout: true }
|
|
161
88
|
}
|
|
89
|
+
return null
|
|
90
|
+
} finally {
|
|
91
|
+
clear()
|
|
92
|
+
}
|
|
162
93
|
}
|
|
163
94
|
|
|
164
95
|
/**
|
|
165
|
-
*
|
|
96
|
+
* Background update credentials if new data (like apiSecret) is available
|
|
166
97
|
*/
|
|
167
|
-
async function
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
'Run "launchpd register" if you don\'t have an account',
|
|
178
|
-
]);
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const validateSpinner = spinner('Validating API key...');
|
|
183
|
-
|
|
184
|
-
const result = await validateApiKey(apiKey);
|
|
185
|
-
|
|
186
|
-
if (!result) {
|
|
187
|
-
validateSpinner.fail('Invalid API key');
|
|
188
|
-
errorWithSuggestions('Please check and try again.', [
|
|
189
|
-
`Get your API key at: https://portal.${config.domain}/api-keys`,
|
|
190
|
-
'Make sure you copied the full key',
|
|
191
|
-
'API keys start with "lpd_"',
|
|
192
|
-
]);
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
validateSpinner.succeed('Logged in successfully!');
|
|
98
|
+
async function updateCredentialsIfNeeded (creds, result) {
|
|
99
|
+
if (result.user?.api_secret && !creds.apiSecret) {
|
|
100
|
+
await saveCredentials({
|
|
101
|
+
...creds,
|
|
102
|
+
apiSecret: result.user.api_secret,
|
|
103
|
+
userId: result.user.id || creds.userId,
|
|
104
|
+
email: result.user.email || creds.email
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
197
108
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Login with API key (original method)
|
|
111
|
+
*/
|
|
112
|
+
async function loginWithApiKey () {
|
|
113
|
+
log('Enter your API key from the dashboard.')
|
|
114
|
+
log(`Don't have one? Run ${chalk.cyan('"launchpd register"')} first.\n`)
|
|
115
|
+
|
|
116
|
+
const apiKey = await promptSecret('API Key: ')
|
|
117
|
+
|
|
118
|
+
if (!apiKey) {
|
|
119
|
+
errorWithSuggestions('API key is required', [
|
|
120
|
+
'Get your API key from the dashboard',
|
|
121
|
+
`Visit: https://${config.domain}/settings`,
|
|
122
|
+
'Run "launchpd register" if you don\'t have an account'
|
|
123
|
+
])
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const validateSpinner = spinner('Validating API key...')
|
|
128
|
+
|
|
129
|
+
const result = await validateApiKey(apiKey)
|
|
130
|
+
|
|
131
|
+
if (!result) {
|
|
132
|
+
validateSpinner.fail('Invalid API key')
|
|
133
|
+
errorWithSuggestions('Please check and try again.', [
|
|
134
|
+
`Get your API key at: https://portal.${config.domain}/api-keys`,
|
|
135
|
+
'Make sure you copied the full key',
|
|
136
|
+
'API keys start with "lpd_"'
|
|
137
|
+
])
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (result.timeout) {
|
|
142
|
+
validateSpinner.fail('Request timed out')
|
|
143
|
+
errorWithSuggestions('The server did not respond in time.', [
|
|
144
|
+
'Check your internet connection',
|
|
145
|
+
'Try again later',
|
|
146
|
+
'If the problem persists, check https://status.launchpd.cloud'
|
|
147
|
+
])
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (result.requires_2fa) {
|
|
152
|
+
validateSpinner.fail('2FA Required')
|
|
153
|
+
info(
|
|
154
|
+
'2FA is required for your account. Please log in via the browser or use an authenticator app.'
|
|
155
|
+
)
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
validateSpinner.succeed('Logged in successfully!')
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
...result,
|
|
163
|
+
apiKey // Include the API key for saving
|
|
164
|
+
}
|
|
202
165
|
}
|
|
203
166
|
|
|
204
167
|
/**
|
|
205
168
|
* Login command - prompts for API key and validates it
|
|
206
169
|
*/
|
|
207
|
-
export async function login() {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
log(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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('');
|
|
170
|
+
export async function login () {
|
|
171
|
+
// Check if already logged in
|
|
172
|
+
if (await isLoggedIn()) {
|
|
173
|
+
const creds = await getCredentials()
|
|
174
|
+
warning(`Already logged in as ${chalk.cyan(creds.email || creds.userId)}`)
|
|
175
|
+
info('Run "launchpd logout" to switch accounts')
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
log('\nLaunchpd Login\n')
|
|
180
|
+
|
|
181
|
+
const result = await loginWithApiKey()
|
|
182
|
+
if (!result) {
|
|
183
|
+
process.exit(1)
|
|
184
|
+
}
|
|
185
|
+
const apiKey = result.apiKey
|
|
186
|
+
|
|
187
|
+
// Save credentials
|
|
188
|
+
await saveCredentials({
|
|
189
|
+
apiKey,
|
|
190
|
+
apiSecret: result.user?.api_secret,
|
|
191
|
+
userId: result.user?.id,
|
|
192
|
+
email: result.user?.email,
|
|
193
|
+
tier: result.tier
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
log('')
|
|
197
|
+
log(` ${chalk.gray('Email:')} ${chalk.cyan(result.user?.email || 'N/A')}`)
|
|
198
|
+
log(` ${chalk.gray('Tier:')} ${chalk.green(result.tier || 'registered')}`)
|
|
199
|
+
if (result.usage) {
|
|
200
|
+
log(
|
|
201
|
+
` ${chalk.gray('Sites:')} ${result.usage?.siteCount || 0}/${result.limits?.maxSites || '?'}`
|
|
202
|
+
)
|
|
203
|
+
const storageUsed =
|
|
204
|
+
result.usage?.storageUsed ||
|
|
205
|
+
(result.usage?.storageUsedMB || 0) * 1024 * 1024
|
|
206
|
+
const storageMax =
|
|
207
|
+
result.limits?.maxStorageBytes ||
|
|
208
|
+
(result.limits?.maxStorageMB || 0) * 1024 * 1024
|
|
209
|
+
log(
|
|
210
|
+
` ${chalk.gray('Storage:')} ${formatBytes(storageUsed)}/${formatBytes(storageMax)}`
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Warn if email not verified
|
|
215
|
+
if (result.user?.email && !result.user?.email_verified) {
|
|
216
|
+
log('')
|
|
217
|
+
warning('Your email is not verified')
|
|
218
|
+
info(`Verify at: https://${config.domain}/auth/verify-pending`)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
log('')
|
|
267
222
|
}
|
|
268
223
|
|
|
269
224
|
/**
|
|
270
225
|
* Logout command - clears stored credentials and invalidates server session
|
|
271
226
|
*/
|
|
272
|
-
export async function logout() {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (!loggedIn) {
|
|
276
|
-
warning('Not currently logged in');
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
227
|
+
export async function logout () {
|
|
228
|
+
const loggedIn = await isLoggedIn()
|
|
279
229
|
|
|
280
|
-
|
|
230
|
+
if (!loggedIn) {
|
|
231
|
+
warning('Not currently logged in')
|
|
232
|
+
return
|
|
233
|
+
}
|
|
281
234
|
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
await serverLogout();
|
|
285
|
-
} catch {
|
|
286
|
-
// Ignore server logout errors - we'll clear local creds anyway
|
|
287
|
-
}
|
|
235
|
+
const creds = await getCredentials()
|
|
288
236
|
|
|
289
|
-
|
|
237
|
+
await clearCredentials()
|
|
290
238
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
239
|
+
success('Logged out successfully')
|
|
240
|
+
if (creds?.email) {
|
|
241
|
+
info(`Was logged in as: ${chalk.cyan(creds.email)}`)
|
|
242
|
+
}
|
|
243
|
+
log(
|
|
244
|
+
`\nYou can still deploy anonymously (limited to ${chalk.yellow('3 sites')}, ${chalk.yellow('50MB')}).`
|
|
245
|
+
)
|
|
296
246
|
}
|
|
297
247
|
|
|
298
248
|
/**
|
|
299
249
|
* Register command - opens browser to registration page
|
|
300
250
|
*/
|
|
301
|
-
export
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
251
|
+
export function register () {
|
|
252
|
+
log('\nRegister for Launchpd\n')
|
|
253
|
+
log(`Opening registration page: ${chalk.cyan(REGISTER_URL)}\n`)
|
|
254
|
+
|
|
255
|
+
// Open browser based on platform
|
|
256
|
+
const platform = process.platform
|
|
257
|
+
let command = 'xdg-open'
|
|
258
|
+
let args = [REGISTER_URL]
|
|
259
|
+
|
|
260
|
+
if (platform === 'darwin') {
|
|
261
|
+
command = 'open'
|
|
262
|
+
} else if (platform === 'win32') {
|
|
263
|
+
command = 'cmd'
|
|
264
|
+
args = ['/c', 'start', '', REGISTER_URL]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
execFile(command, args, (err) => {
|
|
268
|
+
if (err) {
|
|
269
|
+
log(
|
|
270
|
+
`Please open this URL in your browser:\n ${chalk.cyan(REGISTER_URL)}\n`
|
|
271
|
+
)
|
|
315
272
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
log('After registering:')
|
|
276
|
+
log(' 1. Get your API key from the dashboard')
|
|
277
|
+
log(` 2. Run: ${chalk.cyan('launchpd login')}`)
|
|
278
|
+
log('')
|
|
279
|
+
|
|
280
|
+
info('Registration benefits:')
|
|
281
|
+
log(
|
|
282
|
+
` ${chalk.green('✓')} ${chalk.white('10 sites')} ${chalk.gray('(instead of 3)')}`
|
|
283
|
+
)
|
|
284
|
+
log(
|
|
285
|
+
` ${chalk.green('✓')} ${chalk.white('100MB storage')} ${chalk.gray('(instead of 50MB)')}`
|
|
286
|
+
)
|
|
287
|
+
log(
|
|
288
|
+
` ${chalk.green('✓')} ${chalk.white('30-day retention')} ${chalk.gray('(instead of 7 days)')}`
|
|
289
|
+
)
|
|
290
|
+
log(` ${chalk.green('✓')} ${chalk.white('10 versions per site')}`)
|
|
291
|
+
log('')
|
|
334
292
|
}
|
|
335
293
|
|
|
336
294
|
/**
|
|
337
295
|
* Whoami command - shows current user info and quota status
|
|
338
296
|
*/
|
|
339
|
-
export async function whoami() {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
297
|
+
export async function whoami () {
|
|
298
|
+
const creds = await getCredentials()
|
|
299
|
+
|
|
300
|
+
if (!creds) {
|
|
301
|
+
log('\n👤 Not logged in (anonymous mode)\n')
|
|
302
|
+
log('Anonymous limits:')
|
|
303
|
+
log(` • ${chalk.white('3 sites')} maximum`)
|
|
304
|
+
log(` • ${chalk.white('50MB')} total storage`)
|
|
305
|
+
log(` • ${chalk.white('7-day')} retention`)
|
|
306
|
+
log(` • ${chalk.white('1 version')} per site`)
|
|
307
|
+
log(`\nRun ${chalk.cyan('"launchpd login"')} to authenticate`)
|
|
308
|
+
log(`Run ${chalk.cyan('"launchpd register"')} to create an account\n`)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
info('Fetching account status...')
|
|
313
|
+
|
|
314
|
+
// Validate and get current quota
|
|
315
|
+
const result = await validateApiKey(creds.apiKey)
|
|
316
|
+
|
|
317
|
+
if (!result) {
|
|
318
|
+
warning('Session expired or API key invalid')
|
|
319
|
+
await clearCredentials()
|
|
320
|
+
error('Please login again with: launchpd login')
|
|
321
|
+
process.exit(1)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Background upgrade to apiSecret if missing
|
|
325
|
+
await updateCredentialsIfNeeded(creds, result)
|
|
326
|
+
|
|
327
|
+
log(`\nLogged in as: ${result.user?.email || result.user?.id}\n`)
|
|
328
|
+
|
|
329
|
+
log('Account Info:')
|
|
330
|
+
log(` User ID: ${result.user?.id}`)
|
|
331
|
+
|
|
332
|
+
// Email with verification status
|
|
333
|
+
const emailStatus = result.user?.email_verified
|
|
334
|
+
? chalk.green('✓ Verified')
|
|
335
|
+
: chalk.yellow('⚠ Unverified')
|
|
336
|
+
log(` Email: ${result.user?.email || 'Not set'} ${emailStatus}`)
|
|
337
|
+
|
|
338
|
+
// Enhanced 2FA display
|
|
339
|
+
const hasTOTP = result.user?.is_2fa_enabled
|
|
340
|
+
const hasEmail2FA = result.user?.is_email_2fa_enabled
|
|
341
|
+
|
|
342
|
+
if (hasTOTP && hasEmail2FA) {
|
|
343
|
+
log(` 2FA: ${chalk.green('✓ Enabled')} ${chalk.gray('(App + Email)')}`)
|
|
344
|
+
} else if (hasTOTP) {
|
|
345
|
+
log(
|
|
346
|
+
` 2FA: ${chalk.green('✓ Enabled')} ${chalk.gray('(Authenticator App)')}`
|
|
347
|
+
)
|
|
348
|
+
} else if (hasEmail2FA) {
|
|
349
|
+
log(` 2FA: ${chalk.green('✓ Enabled')} ${chalk.gray('(Email)')}`)
|
|
350
|
+
} else {
|
|
351
|
+
log(` 2FA: ${chalk.gray('Not enabled')}`)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
log(` Tier: ${result.tier}`)
|
|
355
|
+
log('')
|
|
356
|
+
|
|
357
|
+
log('Usage:')
|
|
358
|
+
log(` Sites: ${result.usage?.siteCount || 0} / ${result.limits?.maxSites}`)
|
|
359
|
+
const storageUsed =
|
|
360
|
+
result.usage?.storageUsed ||
|
|
361
|
+
(result.usage?.storageUsedMB || 0) * 1024 * 1024
|
|
362
|
+
const storageMax =
|
|
363
|
+
result.limits?.maxStorageBytes ||
|
|
364
|
+
(result.limits?.maxStorageMB || 0) * 1024 * 1024
|
|
365
|
+
log(` Storage: ${formatBytes(storageUsed)} / ${formatBytes(storageMax)}`)
|
|
366
|
+
log(` Sites remaining: ${result.usage?.sitesRemaining || 0}`)
|
|
367
|
+
const storageRemaining =
|
|
368
|
+
result.usage?.storageRemaining ||
|
|
369
|
+
(result.usage?.storageRemainingMB || 0) * 1024 * 1024
|
|
370
|
+
log(` Storage remaining: ${formatBytes(storageRemaining)}`)
|
|
371
|
+
log('')
|
|
372
|
+
|
|
373
|
+
log('Limits:')
|
|
374
|
+
log(` Max versions per site: ${result.limits?.maxVersionsPerSite}`)
|
|
375
|
+
log(` Retention: ${result.limits?.retentionDays} days`)
|
|
376
|
+
log('')
|
|
377
|
+
|
|
378
|
+
// Show warnings
|
|
379
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
380
|
+
log('Warnings:')
|
|
381
|
+
result.warnings.forEach((w) => log(` ${w}`))
|
|
382
|
+
log('')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!result.canCreateNewSite) {
|
|
386
|
+
warning('You cannot create new sites (limit reached)')
|
|
387
|
+
info('You can still update existing sites')
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Email verification warning
|
|
391
|
+
if (result.user?.email && !result.user?.email_verified) {
|
|
392
|
+
log('')
|
|
393
|
+
warning('Your email is not verified')
|
|
394
|
+
info(`Verify at: https://${config.domain}/auth/verify-pending`)
|
|
395
|
+
info('Some features may be limited until verified')
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 2FA recommendation if not enabled
|
|
399
|
+
if (!result.user?.is_2fa_enabled && !result.user?.is_email_2fa_enabled) {
|
|
400
|
+
log('')
|
|
401
|
+
info('Tip: Enable 2FA for better security')
|
|
402
|
+
const securityUrl = `https://${config.domain}/settings/security`
|
|
403
|
+
const securityUrlMsg = `Visit: ${securityUrl}`
|
|
404
|
+
log(` ${chalk.gray(securityUrlMsg)}`)
|
|
405
|
+
}
|
|
436
406
|
}
|
|
437
407
|
|
|
438
408
|
/**
|
|
439
409
|
* Quota command - shows detailed quota information
|
|
440
410
|
*/
|
|
441
|
-
export async function quota() {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
|
|
411
|
+
export async function quota () {
|
|
412
|
+
const creds = await getCredentials()
|
|
413
|
+
|
|
414
|
+
if (!creds) {
|
|
415
|
+
log(`\n${chalk.bold('Anonymous Quota Status')}\n`)
|
|
416
|
+
log(chalk.gray('You are not logged in.'))
|
|
417
|
+
log('')
|
|
418
|
+
log(chalk.bold('Anonymous tier limits:'))
|
|
419
|
+
log(chalk.gray(' ┌─────────────────────────────────┐'))
|
|
420
|
+
log(
|
|
421
|
+
`${chalk.gray(' │')} Sites: ${chalk.white('3 maximum')} ${chalk.gray('│')}`
|
|
422
|
+
)
|
|
423
|
+
log(
|
|
424
|
+
`${chalk.gray(' │')} Storage: ${chalk.white('50MB total')} ${chalk.gray('│')}`
|
|
425
|
+
)
|
|
426
|
+
log(
|
|
427
|
+
`${chalk.gray(' │')} Retention: ${chalk.white('7 days')} ${chalk.gray('│')}`
|
|
428
|
+
)
|
|
429
|
+
log(
|
|
430
|
+
`${chalk.gray(' │')} Versions: ${chalk.white('1 per site')} ${chalk.gray('│')}`
|
|
431
|
+
)
|
|
432
|
+
log(chalk.gray(' └─────────────────────────────────┘'))
|
|
433
|
+
log('')
|
|
434
|
+
log(`${chalk.cyan('Register for FREE')} to unlock more:`)
|
|
435
|
+
log(` ${chalk.green('→')} ${chalk.white('10 sites')}`)
|
|
436
|
+
log(` ${chalk.green('→')} ${chalk.white('100MB storage')}`)
|
|
437
|
+
log(` ${chalk.green('→')} ${chalk.white('30-day retention')}`)
|
|
438
|
+
log(` ${chalk.green('→')} ${chalk.white('10 versions per site')}`)
|
|
439
|
+
log('')
|
|
440
|
+
log(`Run: ${chalk.cyan('launchpd register')}`)
|
|
441
|
+
log('')
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const fetchSpinner = spinner('Fetching quota status...')
|
|
446
|
+
|
|
447
|
+
const result = await validateApiKey(creds.apiKey)
|
|
448
|
+
|
|
449
|
+
if (!result) {
|
|
450
|
+
fetchSpinner.fail('Failed to fetch quota')
|
|
451
|
+
errorWithSuggestions('API key may be invalid.', [
|
|
452
|
+
'Run "launchpd login" to re-authenticate',
|
|
453
|
+
'Check your internet connection'
|
|
454
|
+
])
|
|
455
|
+
process.exit(1)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Background upgrade to apiSecret if missing
|
|
459
|
+
await updateCredentialsIfNeeded(creds, result)
|
|
460
|
+
|
|
461
|
+
fetchSpinner.succeed('Quota fetched')
|
|
462
|
+
log(
|
|
463
|
+
`\n${chalk.bold('Quota Status for:')} ${chalk.cyan(result.user?.email || creds.email)}\n`
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
// Sites usage
|
|
467
|
+
const sitesUsed = result.usage?.siteCount || 0
|
|
468
|
+
const sitesMax = result.limits?.maxSites || 10
|
|
469
|
+
const sitesPercent = Math.round((sitesUsed / sitesMax) * 100)
|
|
470
|
+
const sitesBar = createProgressBar(sitesUsed, sitesMax)
|
|
471
|
+
|
|
472
|
+
log(
|
|
473
|
+
`${chalk.gray('Sites:')} ${sitesBar} ${chalk.white(sitesUsed)}/${sitesMax} (${getPercentColor(sitesPercent)})`
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
// Storage usage
|
|
477
|
+
const storageBytes = result.usage?.storageUsed || 0
|
|
478
|
+
const storageMaxBytes =
|
|
479
|
+
result.limits?.maxStorageBytes ||
|
|
480
|
+
(result.limits?.maxStorageMB || 100) * 1024 * 1024
|
|
481
|
+
const storagePercent = Math.round((storageBytes / storageMaxBytes) * 100)
|
|
482
|
+
const storageBar = createProgressBar(storageBytes, storageMaxBytes)
|
|
483
|
+
|
|
484
|
+
log(
|
|
485
|
+
`${chalk.gray('Storage:')} ${storageBar} ${chalk.white(formatBytes(storageBytes))}/${formatBytes(storageMaxBytes)} (${getPercentColor(storagePercent)})`
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
log('')
|
|
489
|
+
log(`${chalk.gray('Tier:')} ${chalk.green(result.tier || 'free')}`)
|
|
490
|
+
log(
|
|
491
|
+
`${chalk.gray('Retention:')} ${chalk.white(result.limits?.retentionDays || 30)} days`
|
|
492
|
+
)
|
|
493
|
+
log(
|
|
494
|
+
`${chalk.gray('Max versions:')} ${chalk.white(result.limits?.maxVersionsPerSite || 10)} per site`
|
|
495
|
+
)
|
|
496
|
+
log('')
|
|
497
|
+
|
|
498
|
+
// Status indicators
|
|
499
|
+
if (result.canCreateNewSite === false) {
|
|
500
|
+
warning('Site limit reached - cannot create new sites')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (storagePercent > 80) {
|
|
504
|
+
warning(
|
|
505
|
+
`Storage ${storagePercent}% used - consider cleaning up old deployments`
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
log('')
|
|
518
510
|
}
|
|
519
511
|
|
|
520
512
|
/**
|
|
521
513
|
* Create a simple progress bar with color coding
|
|
522
514
|
*/
|
|
523
|
-
function createProgressBar(current, max, width = 20) {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
515
|
+
function createProgressBar (current, max, width = 20) {
|
|
516
|
+
const filled = Math.round((current / max) * width)
|
|
517
|
+
const empty = width - filled
|
|
518
|
+
const percent = (current / max) * 100
|
|
519
|
+
|
|
520
|
+
const filledChar = '█'
|
|
521
|
+
let barColor
|
|
522
|
+
|
|
523
|
+
if (percent >= 90) {
|
|
524
|
+
barColor = chalk.red
|
|
525
|
+
} else if (percent >= 70) {
|
|
526
|
+
barColor = chalk.yellow
|
|
527
|
+
} else {
|
|
528
|
+
barColor = chalk.green
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const bar = `${barColor(filledChar.repeat(filled))}${chalk.gray('░'.repeat(empty))}`
|
|
532
|
+
return `[${bar}]`
|
|
541
533
|
}
|
|
542
534
|
|
|
543
535
|
/**
|
|
544
536
|
* Get colored percentage text
|
|
545
537
|
*/
|
|
546
|
-
function getPercentColor(percent) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
538
|
+
function getPercentColor (percent) {
|
|
539
|
+
if (percent >= 90) {
|
|
540
|
+
return chalk.red(`${percent}%`)
|
|
541
|
+
} else if (percent >= 70) {
|
|
542
|
+
return chalk.yellow(`${percent}%`)
|
|
543
|
+
}
|
|
544
|
+
return chalk.green(`${percent}%`)
|
|
553
545
|
}
|
|
554
546
|
|
|
555
547
|
/**
|
|
556
548
|
* Resend email verification command
|
|
557
549
|
*/
|
|
558
|
-
export async function resendEmailVerification() {
|
|
559
|
-
|
|
550
|
+
export async function resendEmailVerification () {
|
|
551
|
+
const loggedIn = await isLoggedIn()
|
|
560
552
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
553
|
+
if (!loggedIn) {
|
|
554
|
+
error('Not logged in')
|
|
555
|
+
info(`Run ${chalk.cyan('"launchpd login"')} first`)
|
|
556
|
+
process.exit(1)
|
|
557
|
+
}
|
|
566
558
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
process.exit(1);
|
|
559
|
+
const sendSpinner = spinner('Requesting verification email...')
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
const result = await resendVerification()
|
|
563
|
+
|
|
564
|
+
if (result.success) {
|
|
565
|
+
sendSpinner.succeed('Verification email sent!')
|
|
566
|
+
info('Check your inbox and spam folder')
|
|
567
|
+
} else {
|
|
568
|
+
sendSpinner.fail('Failed to send verification email')
|
|
569
|
+
error(result.message || 'Unknown error')
|
|
570
|
+
if (result.seconds_remaining) {
|
|
571
|
+
info(
|
|
572
|
+
`Please wait ${result.seconds_remaining} seconds before trying again`
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
} catch (err) {
|
|
577
|
+
sendSpinner.fail('Request failed')
|
|
578
|
+
if (handleCommonError(err, { error, info, warning })) {
|
|
579
|
+
process.exit(1)
|
|
580
|
+
}
|
|
581
|
+
if (err.message?.includes('already verified')) {
|
|
582
|
+
success('Your email is already verified!')
|
|
583
|
+
return
|
|
593
584
|
}
|
|
585
|
+
error(err.message || 'Failed to resend verification email')
|
|
586
|
+
process.exit(1)
|
|
587
|
+
}
|
|
594
588
|
}
|