launchpd 0.1.4 ā 0.1.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/bin/cli.js +5 -0
- package/package.json +3 -2
- package/src/commands/auth.js +95 -65
- package/src/commands/deploy.js +60 -14
- package/src/commands/list.js +37 -15
- package/src/commands/rollback.js +29 -12
- package/src/commands/versions.js +34 -10
- package/src/utils/logger.js +124 -1
- package/src/utils/upload.js +7 -3
package/bin/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ program
|
|
|
21
21
|
.option('--dry-run', 'Simulate deployment without uploading to R2')
|
|
22
22
|
.option('--name <subdomain>', 'Use a custom subdomain (optional)')
|
|
23
23
|
.option('--expires <time>', 'Auto-delete after time (e.g., 30m, 2h, 1d). Minimum: 30m')
|
|
24
|
+
.option('--verbose', 'Show detailed error information')
|
|
24
25
|
.action(async (folder, options) => {
|
|
25
26
|
await deploy(folder, options);
|
|
26
27
|
});
|
|
@@ -29,6 +30,8 @@ program
|
|
|
29
30
|
.command('list')
|
|
30
31
|
.description('List your past deployments')
|
|
31
32
|
.option('--json', 'Output as JSON')
|
|
33
|
+
.option('--local', 'Only show local deployments')
|
|
34
|
+
.option('--verbose', 'Show detailed error information')
|
|
32
35
|
.action(async (options) => {
|
|
33
36
|
await list(options);
|
|
34
37
|
});
|
|
@@ -38,6 +41,7 @@ program
|
|
|
38
41
|
.description('List all versions for a subdomain')
|
|
39
42
|
.argument('<subdomain>', 'The subdomain to list versions for')
|
|
40
43
|
.option('--json', 'Output as JSON')
|
|
44
|
+
.option('--verbose', 'Show detailed error information')
|
|
41
45
|
.action(async (subdomain, options) => {
|
|
42
46
|
await versions(subdomain, options);
|
|
43
47
|
});
|
|
@@ -47,6 +51,7 @@ program
|
|
|
47
51
|
.description('Rollback a subdomain to a previous version')
|
|
48
52
|
.argument('<subdomain>', 'The subdomain to rollback')
|
|
49
53
|
.option('--to <n>', 'Specific version number to rollback to')
|
|
54
|
+
.option('--verbose', 'Show detailed error information')
|
|
50
55
|
.action(async (subdomain, options) => {
|
|
51
56
|
await rollback(subdomain, options);
|
|
52
57
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "launchpd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Deploy static sites instantly to a live URL",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"chalk": "^5.4.0",
|
|
54
54
|
"commander": "^14.0.0",
|
|
55
55
|
"mime-types": "^2.1.35",
|
|
56
|
-
"nanoid": "^5.1.0"
|
|
56
|
+
"nanoid": "^5.1.0",
|
|
57
|
+
"ora": "^8.0.1"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@eslint/js": "^9.39.2",
|
package/src/commands/auth.js
CHANGED
|
@@ -7,7 +7,8 @@ import { createInterface } from 'node:readline';
|
|
|
7
7
|
import { exec } from 'node:child_process';
|
|
8
8
|
import { config } from '../config.js';
|
|
9
9
|
import { getCredentials, saveCredentials, clearCredentials, isLoggedIn } from '../utils/credentials.js';
|
|
10
|
-
import { success, error, info, warning } from '../utils/logger.js';
|
|
10
|
+
import { success, error, errorWithSuggestions, info, warning, spinner } from '../utils/logger.js';
|
|
11
|
+
import chalk from 'chalk';
|
|
11
12
|
|
|
12
13
|
const API_BASE_URL = `https://api.${config.domain}`;
|
|
13
14
|
const REGISTER_URL = `https://portal.${config.domain}/auth/register`;
|
|
@@ -49,8 +50,7 @@ async function validateApiKey(apiKey) {
|
|
|
49
50
|
return data;
|
|
50
51
|
}
|
|
51
52
|
return null;
|
|
52
|
-
} catch
|
|
53
|
-
error(`Failed to validate API key: ${err.message}`);
|
|
53
|
+
} catch {
|
|
54
54
|
return null;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -62,29 +62,37 @@ export async function login() {
|
|
|
62
62
|
// Check if already logged in
|
|
63
63
|
if (await isLoggedIn()) {
|
|
64
64
|
const creds = await getCredentials();
|
|
65
|
-
warning(`Already logged in as ${creds.email || creds.userId}`);
|
|
65
|
+
warning(`Already logged in as ${chalk.cyan(creds.email || creds.userId)}`);
|
|
66
66
|
info('Run "launchpd logout" to switch accounts');
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
console.log('\
|
|
70
|
+
console.log('\nLaunchpd Login\n');
|
|
71
71
|
console.log('Enter your API key from the dashboard.');
|
|
72
|
-
console.log(`Don't have one? Run "launchpd register" first.\n`);
|
|
72
|
+
console.log(`Don't have one? Run ${chalk.cyan('"launchpd register"')} first.\n`);
|
|
73
73
|
|
|
74
74
|
const apiKey = await prompt('API Key: ');
|
|
75
75
|
|
|
76
76
|
if (!apiKey) {
|
|
77
|
-
|
|
77
|
+
errorWithSuggestions('API key is required', [
|
|
78
|
+
'Get your API key from the dashboard',
|
|
79
|
+
`Visit: https://portal.${config.domain}/api-keys`,
|
|
80
|
+
'Run "launchpd register" if you don\'t have an account',
|
|
81
|
+
]);
|
|
78
82
|
process.exit(1);
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
const validateSpinner = spinner('Validating API key...');
|
|
82
86
|
|
|
83
87
|
const result = await validateApiKey(apiKey);
|
|
84
88
|
|
|
85
89
|
if (!result) {
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
validateSpinner.fail('Invalid API key');
|
|
91
|
+
errorWithSuggestions('Please check and try again.', [
|
|
92
|
+
`Get your API key at: https://portal.${config.domain}/api-keys`,
|
|
93
|
+
'Make sure you copied the full key',
|
|
94
|
+
'API keys start with "lpd_"',
|
|
95
|
+
]);
|
|
88
96
|
process.exit(1);
|
|
89
97
|
}
|
|
90
98
|
|
|
@@ -96,11 +104,13 @@ export async function login() {
|
|
|
96
104
|
tier: result.tier,
|
|
97
105
|
});
|
|
98
106
|
|
|
99
|
-
|
|
100
|
-
console.log(
|
|
101
|
-
console.log(`
|
|
102
|
-
console.log(`
|
|
103
|
-
console.log(`
|
|
107
|
+
validateSpinner.succeed('Logged in successfully!');
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(` ${chalk.gray('Email:')} ${chalk.cyan(result.user?.email || 'N/A')}`);
|
|
110
|
+
console.log(` ${chalk.gray('Tier:')} ${chalk.green(result.tier)}`);
|
|
111
|
+
console.log(` ${chalk.gray('Sites:')} ${result.usage?.siteCount || 0}/${result.limits?.maxSites || '?'}`);
|
|
112
|
+
console.log(` ${chalk.gray('Storage:')} ${result.usage?.storageUsedMB || 0}MB/${result.limits?.maxStorageMB || '?'}MB`);
|
|
113
|
+
console.log('');
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
/**
|
|
@@ -117,19 +127,19 @@ export async function logout() {
|
|
|
117
127
|
const creds = await getCredentials();
|
|
118
128
|
await clearCredentials();
|
|
119
129
|
|
|
120
|
-
success(
|
|
130
|
+
success('Logged out successfully');
|
|
121
131
|
if (creds?.email) {
|
|
122
|
-
info(`Was logged in as: ${creds.email}`);
|
|
132
|
+
info(`Was logged in as: ${chalk.cyan(creds.email)}`);
|
|
123
133
|
}
|
|
124
|
-
console.log(`\nYou can still deploy anonymously (limited to 3 sites, 50MB).`);
|
|
134
|
+
console.log(`\nYou can still deploy anonymously (limited to ${chalk.yellow('3 sites')}, ${chalk.yellow('50MB')}).`);
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
/**
|
|
128
138
|
* Register command - opens browser to registration page
|
|
129
139
|
*/
|
|
130
140
|
export async function register() {
|
|
131
|
-
console.log('\
|
|
132
|
-
console.log(`Opening registration page: ${REGISTER_URL}\n`);
|
|
141
|
+
console.log('\nRegister for Launchpd\n');
|
|
142
|
+
console.log(`Opening registration page: ${chalk.cyan(REGISTER_URL)}\n`);
|
|
133
143
|
|
|
134
144
|
// Open browser based on platform
|
|
135
145
|
const platform = process.platform;
|
|
@@ -145,20 +155,20 @@ export async function register() {
|
|
|
145
155
|
|
|
146
156
|
exec(cmd, (err) => {
|
|
147
157
|
if (err) {
|
|
148
|
-
console.log(`Please open this URL in your browser:\n ${REGISTER_URL}\n`);
|
|
158
|
+
console.log(`Please open this URL in your browser:\n ${chalk.cyan(REGISTER_URL)}\n`);
|
|
149
159
|
}
|
|
150
160
|
});
|
|
151
161
|
|
|
152
162
|
console.log('After registering:');
|
|
153
|
-
console.log(
|
|
154
|
-
console.log(
|
|
163
|
+
console.log(` 1. Get your API key from the dashboard`);
|
|
164
|
+
console.log(` 2. Run: ${chalk.cyan('launchpd login')}`);
|
|
155
165
|
console.log('');
|
|
156
166
|
|
|
157
167
|
info('Registration benefits:');
|
|
158
|
-
console.log('
|
|
159
|
-
console.log('
|
|
160
|
-
console.log('
|
|
161
|
-
console.log('
|
|
168
|
+
console.log(` ${chalk.green('ā')} ${chalk.white('10 sites')} ${chalk.gray('(instead of 3)')}`);
|
|
169
|
+
console.log(` ${chalk.green('ā')} ${chalk.white('100MB storage')} ${chalk.gray('(instead of 50MB)')}`);
|
|
170
|
+
console.log(` ${chalk.green('ā')} ${chalk.white('30-day retention')} ${chalk.gray('(instead of 7 days)')}`);
|
|
171
|
+
console.log(` ${chalk.green('ā')} ${chalk.white('10 versions per site')}`);
|
|
162
172
|
console.log('');
|
|
163
173
|
}
|
|
164
174
|
|
|
@@ -171,12 +181,12 @@ export async function whoami() {
|
|
|
171
181
|
if (!creds) {
|
|
172
182
|
console.log('\nš¤ Not logged in (anonymous mode)\n');
|
|
173
183
|
console.log('Anonymous limits:');
|
|
174
|
-
console.log(
|
|
175
|
-
console.log(
|
|
176
|
-
console.log(
|
|
177
|
-
console.log(
|
|
178
|
-
console.log(`\nRun "launchpd login" to authenticate`);
|
|
179
|
-
console.log(`Run "launchpd register" to create an account\n`);
|
|
184
|
+
console.log(` ⢠${chalk.white('3 sites')} maximum`);
|
|
185
|
+
console.log(` ⢠${chalk.white('50MB')} total storage`);
|
|
186
|
+
console.log(` ⢠${chalk.white('7-day')} retention`);
|
|
187
|
+
console.log(` ⢠${chalk.white('1 version')} per site`);
|
|
188
|
+
console.log(`\nRun ${chalk.cyan('"launchpd login"')} to authenticate`);
|
|
189
|
+
console.log(`Run ${chalk.cyan('"launchpd register"')} to create an account\n`);
|
|
180
190
|
return;
|
|
181
191
|
}
|
|
182
192
|
|
|
@@ -192,7 +202,7 @@ export async function whoami() {
|
|
|
192
202
|
process.exit(1);
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
console.log(`\
|
|
205
|
+
console.log(`\nLogged in as: ${result.user?.email || result.user?.id}\n`);
|
|
196
206
|
|
|
197
207
|
console.log('Account Info:');
|
|
198
208
|
console.log(` User ID: ${result.user?.id}`);
|
|
@@ -232,38 +242,43 @@ export async function quota() {
|
|
|
232
242
|
const creds = await getCredentials();
|
|
233
243
|
|
|
234
244
|
if (!creds) {
|
|
235
|
-
console.log('
|
|
236
|
-
console.log('You are not logged in.');
|
|
245
|
+
console.log(`\n${chalk.bold('Anonymous Quota Status')}\n`);
|
|
246
|
+
console.log(chalk.gray('You are not logged in.'));
|
|
237
247
|
console.log('');
|
|
238
|
-
console.log('Anonymous tier limits:');
|
|
239
|
-
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
240
|
-
console.log(' ā Sites: 3 maximum ā');
|
|
241
|
-
console.log(' ā Storage: 50MB total ā');
|
|
242
|
-
console.log(' ā Retention: 7 days ā');
|
|
243
|
-
console.log(' ā Versions: 1 per site ā');
|
|
244
|
-
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
248
|
+
console.log(chalk.bold('Anonymous tier limits:'));
|
|
249
|
+
console.log(chalk.gray(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
250
|
+
console.log(chalk.gray(' ā') + ` Sites: ${chalk.white('3 maximum')} ` + chalk.gray('ā'));
|
|
251
|
+
console.log(chalk.gray(' ā') + ` Storage: ${chalk.white('50MB total')} ` + chalk.gray('ā'));
|
|
252
|
+
console.log(chalk.gray(' ā') + ` Retention: ${chalk.white('7 days')} ` + chalk.gray('ā'));
|
|
253
|
+
console.log(chalk.gray(' ā') + ` Versions: ${chalk.white('1 per site')} ` + chalk.gray('ā'));
|
|
254
|
+
console.log(chalk.gray(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
245
255
|
console.log('');
|
|
246
|
-
console.log('
|
|
247
|
-
console.log('
|
|
248
|
-
console.log('
|
|
249
|
-
console.log('
|
|
250
|
-
console.log('
|
|
256
|
+
console.log(`${chalk.cyan('Register for FREE')} to unlock more:`);
|
|
257
|
+
console.log(` ${chalk.green('ā')} ${chalk.white('10 sites')}`);
|
|
258
|
+
console.log(` ${chalk.green('ā')} ${chalk.white('100MB storage')}`);
|
|
259
|
+
console.log(` ${chalk.green('ā')} ${chalk.white('30-day retention')}`);
|
|
260
|
+
console.log(` ${chalk.green('ā')} ${chalk.white('10 versions per site')}`);
|
|
251
261
|
console.log('');
|
|
252
|
-
console.log(
|
|
262
|
+
console.log(`Run: ${chalk.cyan('launchpd register')}`);
|
|
253
263
|
console.log('');
|
|
254
264
|
return;
|
|
255
265
|
}
|
|
256
266
|
|
|
257
|
-
|
|
267
|
+
const fetchSpinner = spinner('Fetching quota status...');
|
|
258
268
|
|
|
259
269
|
const result = await validateApiKey(creds.apiKey);
|
|
260
270
|
|
|
261
271
|
if (!result) {
|
|
262
|
-
|
|
272
|
+
fetchSpinner.fail('Failed to fetch quota');
|
|
273
|
+
errorWithSuggestions('API key may be invalid.', [
|
|
274
|
+
'Run "launchpd login" to re-authenticate',
|
|
275
|
+
'Check your internet connection',
|
|
276
|
+
]);
|
|
263
277
|
process.exit(1);
|
|
264
278
|
}
|
|
265
279
|
|
|
266
|
-
|
|
280
|
+
fetchSpinner.succeed('Quota fetched');
|
|
281
|
+
console.log(`\n${chalk.bold('Quota Status for:')} ${chalk.cyan(result.user?.email || creds.email)}\n`);
|
|
267
282
|
|
|
268
283
|
// Sites usage
|
|
269
284
|
const sitesUsed = result.usage?.siteCount || 0;
|
|
@@ -271,7 +286,7 @@ export async function quota() {
|
|
|
271
286
|
const sitesPercent = Math.round((sitesUsed / sitesMax) * 100);
|
|
272
287
|
const sitesBar = createProgressBar(sitesUsed, sitesMax);
|
|
273
288
|
|
|
274
|
-
console.log(
|
|
289
|
+
console.log(`${chalk.gray('Sites:')} ${sitesBar} ${chalk.white(sitesUsed)}/${sitesMax} (${getPercentColor(sitesPercent)})`);
|
|
275
290
|
|
|
276
291
|
// Storage usage
|
|
277
292
|
const storageMB = result.usage?.storageUsedMB || 0;
|
|
@@ -279,46 +294,61 @@ export async function quota() {
|
|
|
279
294
|
const storagePercent = Math.round((storageMB / storageMaxMB) * 100);
|
|
280
295
|
const storageBar = createProgressBar(storageMB, storageMaxMB);
|
|
281
296
|
|
|
282
|
-
console.log(
|
|
297
|
+
console.log(`${chalk.gray('Storage:')} ${storageBar} ${chalk.white(storageMB)}MB/${storageMaxMB}MB (${getPercentColor(storagePercent)})`);
|
|
283
298
|
|
|
284
299
|
console.log('');
|
|
285
|
-
console.log(
|
|
286
|
-
console.log(
|
|
287
|
-
console.log(
|
|
300
|
+
console.log(`${chalk.gray('Tier:')} ${chalk.green(result.tier || 'free')}`);
|
|
301
|
+
console.log(`${chalk.gray('Retention:')} ${chalk.white(result.limits?.retentionDays || 30)} days`);
|
|
302
|
+
console.log(`${chalk.gray('Max versions:')} ${chalk.white(result.limits?.maxVersionsPerSite || 10)} per site`);
|
|
288
303
|
console.log('');
|
|
289
304
|
|
|
290
305
|
// Status indicators
|
|
291
306
|
if (result.canCreateNewSite === false) {
|
|
292
|
-
warning('
|
|
307
|
+
warning('Site limit reached - cannot create new sites');
|
|
293
308
|
}
|
|
294
309
|
|
|
295
310
|
if (storagePercent > 80) {
|
|
296
|
-
warning(
|
|
311
|
+
warning(`Storage ${storagePercent}% used - consider cleaning up old deployments`);
|
|
297
312
|
}
|
|
298
313
|
|
|
299
314
|
if (result.tier === 'free') {
|
|
300
315
|
console.log('');
|
|
301
|
-
info(
|
|
316
|
+
info(`Upgrade to ${chalk.magenta('Pro')} for 50 sites, 1GB storage, and 50 versions`);
|
|
302
317
|
}
|
|
303
318
|
console.log('');
|
|
304
319
|
}
|
|
305
320
|
|
|
306
321
|
/**
|
|
307
|
-
* Create a simple progress bar
|
|
322
|
+
* Create a simple progress bar with color coding
|
|
308
323
|
*/
|
|
309
324
|
function createProgressBar(current, max, width = 20) {
|
|
310
325
|
const filled = Math.round((current / max) * width);
|
|
311
326
|
const empty = width - filled;
|
|
312
327
|
const percent = (current / max) * 100;
|
|
313
328
|
|
|
314
|
-
|
|
329
|
+
const filledChar = 'ā';
|
|
330
|
+
let barColor;
|
|
331
|
+
|
|
315
332
|
if (percent >= 90) {
|
|
316
|
-
|
|
333
|
+
barColor = chalk.red;
|
|
317
334
|
} else if (percent >= 70) {
|
|
318
|
-
|
|
335
|
+
barColor = chalk.yellow;
|
|
319
336
|
} else {
|
|
320
|
-
|
|
337
|
+
barColor = chalk.green;
|
|
321
338
|
}
|
|
322
339
|
|
|
340
|
+
const bar = barColor(filledChar.repeat(filled)) + chalk.gray('ā'.repeat(empty));
|
|
323
341
|
return `[${bar}]`;
|
|
324
342
|
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get colored percentage text
|
|
346
|
+
*/
|
|
347
|
+
function getPercentColor(percent) {
|
|
348
|
+
if (percent >= 90) {
|
|
349
|
+
return chalk.red(`${percent}%`);
|
|
350
|
+
} else if (percent >= 70) {
|
|
351
|
+
return chalk.yellow(`${percent}%`);
|
|
352
|
+
}
|
|
353
|
+
return chalk.green(`${percent}%`);
|
|
354
|
+
}
|
package/src/commands/deploy.js
CHANGED
|
@@ -6,7 +6,7 @@ import { uploadFolder, finalizeUpload } from '../utils/upload.js';
|
|
|
6
6
|
import { getNextVersion } from '../utils/metadata.js';
|
|
7
7
|
import { saveLocalDeployment } from '../utils/localConfig.js';
|
|
8
8
|
import { getNextVersionFromAPI } from '../utils/api.js';
|
|
9
|
-
import { success,
|
|
9
|
+
import { success, errorWithSuggestions, info, warning, spinner, formatSize } from '../utils/logger.js';
|
|
10
10
|
import { calculateExpiresAt, formatTimeRemaining } from '../utils/expiration.js';
|
|
11
11
|
import { checkQuota, displayQuotaWarnings } from '../utils/quota.js';
|
|
12
12
|
import { getCredentials } from '../utils/credentials.js';
|
|
@@ -42,9 +42,11 @@ async function calculateFolderSize(folderPath) {
|
|
|
42
42
|
* @param {boolean} options.dryRun - Skip actual upload
|
|
43
43
|
* @param {string} options.name - Custom subdomain
|
|
44
44
|
* @param {string} options.expires - Expiration time (e.g., "30m", "2h", "1d")
|
|
45
|
+
* @param {boolean} options.verbose - Show verbose error details
|
|
45
46
|
*/
|
|
46
47
|
export async function deploy(folder, options) {
|
|
47
48
|
const folderPath = resolve(folder);
|
|
49
|
+
const verbose = options.verbose || false;
|
|
48
50
|
|
|
49
51
|
// Parse expiration if provided
|
|
50
52
|
let expiresAt = null;
|
|
@@ -52,41 +54,58 @@ export async function deploy(folder, options) {
|
|
|
52
54
|
try {
|
|
53
55
|
expiresAt = calculateExpiresAt(options.expires);
|
|
54
56
|
} catch (err) {
|
|
55
|
-
|
|
57
|
+
errorWithSuggestions(err.message, [
|
|
58
|
+
'Use format like: 30m, 2h, 1d, 7d',
|
|
59
|
+
'Minimum expiration is 30 minutes',
|
|
60
|
+
'Examples: --expires 1h, --expires 2d',
|
|
61
|
+
], { verbose, cause: err });
|
|
56
62
|
process.exit(1);
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
// Validate folder exists
|
|
61
67
|
if (!existsSync(folderPath)) {
|
|
62
|
-
|
|
68
|
+
errorWithSuggestions(`Folder not found: ${folderPath}`, [
|
|
69
|
+
'Check the path is correct',
|
|
70
|
+
'Use an absolute path or path relative to current directory',
|
|
71
|
+
`Current directory: ${process.cwd()}`,
|
|
72
|
+
], { verbose });
|
|
63
73
|
process.exit(1);
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
// Check folder is not empty
|
|
77
|
+
const scanSpinner = spinner('Scanning folder...');
|
|
67
78
|
const files = await readdir(folderPath, { recursive: true, withFileTypes: true });
|
|
68
79
|
const fileCount = files.filter(f => f.isFile()).length;
|
|
69
80
|
|
|
70
81
|
if (fileCount === 0) {
|
|
71
|
-
|
|
82
|
+
scanSpinner.fail('Folder is empty');
|
|
83
|
+
errorWithSuggestions('Nothing to deploy.', [
|
|
84
|
+
'Add some files to your folder',
|
|
85
|
+
'Make sure index.html exists for static sites',
|
|
86
|
+
], { verbose });
|
|
72
87
|
process.exit(1);
|
|
73
88
|
}
|
|
89
|
+
scanSpinner.succeed(`Found ${fileCount} file(s)`);
|
|
74
90
|
|
|
75
91
|
// Generate or use provided subdomain
|
|
76
92
|
const subdomain = options.name || generateSubdomain();
|
|
77
93
|
const url = `https://${subdomain}.launchpd.cloud`;
|
|
78
94
|
|
|
79
95
|
// Calculate estimated upload size
|
|
96
|
+
const sizeSpinner = spinner('Calculating folder size...');
|
|
80
97
|
const estimatedBytes = await calculateFolderSize(folderPath);
|
|
98
|
+
sizeSpinner.succeed(`Size: ${formatSize(estimatedBytes)}`);
|
|
81
99
|
|
|
82
100
|
// Check quota before deploying
|
|
83
|
-
|
|
101
|
+
const quotaSpinner = spinner('Checking quota...');
|
|
84
102
|
const quotaCheck = await checkQuota(subdomain, estimatedBytes);
|
|
85
103
|
|
|
86
104
|
if (!quotaCheck.allowed) {
|
|
87
|
-
|
|
105
|
+
quotaSpinner.fail('Deployment blocked due to quota limits');
|
|
88
106
|
process.exit(1);
|
|
89
107
|
}
|
|
108
|
+
quotaSpinner.succeed('Quota check passed');
|
|
90
109
|
|
|
91
110
|
// Display any warnings
|
|
92
111
|
displayQuotaWarnings(quotaCheck.warnings);
|
|
@@ -101,7 +120,6 @@ export async function deploy(folder, options) {
|
|
|
101
120
|
|
|
102
121
|
info(`Deploying ${fileCount} file(s) from ${folderPath}`);
|
|
103
122
|
info(`Target: ${url}`);
|
|
104
|
-
info(`Size: ${(estimatedBytes / 1024 / 1024).toFixed(2)}MB`);
|
|
105
123
|
|
|
106
124
|
if (options.dryRun) {
|
|
107
125
|
warning('Dry run mode - skipping upload');
|
|
@@ -127,7 +145,7 @@ export async function deploy(folder, options) {
|
|
|
127
145
|
? (quotaCheck.quota.usage?.siteCount || 0) + 1
|
|
128
146
|
: quotaCheck.quota.usage?.siteCount || 0;
|
|
129
147
|
console.log(` Sites: ${sitesAfter}/${quotaCheck.quota.limits.maxSites}`);
|
|
130
|
-
console.log(` Storage: ${(storageAfter
|
|
148
|
+
console.log(` Storage: ${formatSize(storageAfter)}/${quotaCheck.quota.limits.maxStorageMB}MB`);
|
|
131
149
|
console.log('');
|
|
132
150
|
}
|
|
133
151
|
return;
|
|
@@ -136,18 +154,25 @@ export async function deploy(folder, options) {
|
|
|
136
154
|
// Perform actual upload
|
|
137
155
|
try {
|
|
138
156
|
// Get next version number for this subdomain (try API first, fallback to local)
|
|
157
|
+
const versionSpinner = spinner('Fetching version info...');
|
|
139
158
|
let version = await getNextVersionFromAPI(subdomain);
|
|
140
159
|
if (version === null) {
|
|
141
160
|
version = await getNextVersion(subdomain);
|
|
142
161
|
}
|
|
143
|
-
|
|
162
|
+
versionSpinner.succeed(`Deploying as version ${version}`);
|
|
144
163
|
|
|
145
164
|
// Upload all files via API proxy
|
|
146
165
|
const folderName = basename(folderPath);
|
|
147
|
-
const
|
|
166
|
+
const uploadSpinner = spinner(`Uploading files... 0/${fileCount}`);
|
|
167
|
+
|
|
168
|
+
const { totalBytes } = await uploadFolder(folderPath, subdomain, version, (uploaded, total, fileName) => {
|
|
169
|
+
uploadSpinner.update(`Uploading files... ${uploaded}/${total} (${fileName})`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
uploadSpinner.succeed(`Uploaded ${fileCount} files (${formatSize(totalBytes)})`);
|
|
148
173
|
|
|
149
174
|
// Finalize upload: set active version and record metadata
|
|
150
|
-
|
|
175
|
+
const finalizeSpinner = spinner('Finalizing deployment...');
|
|
151
176
|
await finalizeUpload(
|
|
152
177
|
subdomain,
|
|
153
178
|
version,
|
|
@@ -156,6 +181,7 @@ export async function deploy(folder, options) {
|
|
|
156
181
|
folderName,
|
|
157
182
|
expiresAt?.toISOString() || null
|
|
158
183
|
);
|
|
184
|
+
finalizeSpinner.succeed('Deployment finalized');
|
|
159
185
|
|
|
160
186
|
// Save locally for quick access
|
|
161
187
|
await saveLocalDeployment({
|
|
@@ -169,13 +195,33 @@ export async function deploy(folder, options) {
|
|
|
169
195
|
});
|
|
170
196
|
|
|
171
197
|
success(`Deployed successfully! (v${version})`);
|
|
172
|
-
console.log(`\n
|
|
198
|
+
console.log(`\n${url}`);
|
|
173
199
|
if (expiresAt) {
|
|
174
|
-
warning(`
|
|
200
|
+
warning(`Expires: ${formatTimeRemaining(expiresAt)}`);
|
|
175
201
|
}
|
|
176
202
|
console.log('');
|
|
177
203
|
} catch (err) {
|
|
178
|
-
|
|
204
|
+
const suggestions = [];
|
|
205
|
+
|
|
206
|
+
// Provide context-specific suggestions
|
|
207
|
+
if (err.message.includes('fetch failed') || err.message.includes('ENOTFOUND')) {
|
|
208
|
+
suggestions.push('Check your internet connection');
|
|
209
|
+
suggestions.push('The API server may be temporarily unavailable');
|
|
210
|
+
} else if (err.message.includes('401') || err.message.includes('Unauthorized')) {
|
|
211
|
+
suggestions.push('Run "launchpd login" to authenticate');
|
|
212
|
+
suggestions.push('Your API key may have expired');
|
|
213
|
+
} else if (err.message.includes('413') || err.message.includes('too large')) {
|
|
214
|
+
suggestions.push('Try deploying fewer or smaller files');
|
|
215
|
+
suggestions.push('Check your storage quota with "launchpd quota"');
|
|
216
|
+
} else if (err.message.includes('429') || err.message.includes('rate limit')) {
|
|
217
|
+
suggestions.push('Wait a few minutes and try again');
|
|
218
|
+
suggestions.push('You may be deploying too frequently');
|
|
219
|
+
} else {
|
|
220
|
+
suggestions.push('Try running with --verbose for more details');
|
|
221
|
+
suggestions.push('Check https://status.launchpd.cloud for service status');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
errorWithSuggestions(`Upload failed: ${err.message}`, suggestions, { verbose, cause: err });
|
|
179
225
|
process.exit(1);
|
|
180
226
|
}
|
|
181
227
|
}
|
package/src/commands/list.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getLocalDeployments } from '../utils/localConfig.js';
|
|
2
2
|
import { listDeployments as listFromAPI } from '../utils/api.js';
|
|
3
|
-
import {
|
|
3
|
+
import { errorWithSuggestions, info, spinner, formatSize } from '../utils/logger.js';
|
|
4
4
|
import { formatTimeRemaining, isExpired } from '../utils/expiration.js';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
|
|
@@ -9,12 +9,17 @@ import chalk from 'chalk';
|
|
|
9
9
|
* @param {object} options - Command options
|
|
10
10
|
* @param {boolean} options.json - Output as JSON
|
|
11
11
|
* @param {boolean} options.local - Only show local deployments
|
|
12
|
+
* @param {boolean} options.verbose - Show verbose error details
|
|
12
13
|
*/
|
|
13
14
|
export async function list(options) {
|
|
15
|
+
const verbose = options.verbose || false;
|
|
16
|
+
|
|
14
17
|
try {
|
|
15
18
|
let deployments = [];
|
|
16
19
|
let source = 'local';
|
|
17
20
|
|
|
21
|
+
const fetchSpinner = spinner('Fetching deployments...');
|
|
22
|
+
|
|
18
23
|
// Try API first unless --local flag is set
|
|
19
24
|
if (!options.local) {
|
|
20
25
|
const apiResult = await listFromAPI();
|
|
@@ -40,11 +45,13 @@ export async function list(options) {
|
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
if (deployments.length === 0) {
|
|
43
|
-
|
|
44
|
-
info('Deploy a folder with: launchpd deploy ./my-folder');
|
|
48
|
+
fetchSpinner.warn('No deployments found');
|
|
49
|
+
info('Deploy a folder with: ' + chalk.cyan('launchpd deploy ./my-folder'));
|
|
45
50
|
return;
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
fetchSpinner.succeed(`Found ${deployments.length} deployment(s)`);
|
|
54
|
+
|
|
48
55
|
if (options.json) {
|
|
49
56
|
console.log(JSON.stringify(deployments, null, 2));
|
|
50
57
|
return;
|
|
@@ -53,7 +60,7 @@ export async function list(options) {
|
|
|
53
60
|
// Display as table
|
|
54
61
|
console.log('');
|
|
55
62
|
console.log(chalk.bold('Your Deployments:'));
|
|
56
|
-
console.log(chalk.gray('ā'.repeat(
|
|
63
|
+
console.log(chalk.gray('ā'.repeat(100)));
|
|
57
64
|
|
|
58
65
|
// Header
|
|
59
66
|
console.log(
|
|
@@ -61,43 +68,58 @@ export async function list(options) {
|
|
|
61
68
|
padRight('URL', 40) +
|
|
62
69
|
padRight('Folder', 15) +
|
|
63
70
|
padRight('Files', 7) +
|
|
71
|
+
padRight('Size', 12) +
|
|
64
72
|
padRight('Date', 12) +
|
|
65
73
|
'Status'
|
|
66
74
|
)
|
|
67
75
|
);
|
|
68
|
-
console.log(chalk.gray('ā'.repeat(
|
|
76
|
+
console.log(chalk.gray('ā'.repeat(100)));
|
|
69
77
|
|
|
70
78
|
// Rows (most recent first)
|
|
71
79
|
const sorted = [...deployments].reverse();
|
|
72
80
|
for (const dep of sorted) {
|
|
73
81
|
const url = `https://${dep.subdomain}.launchpd.cloud`;
|
|
74
82
|
const date = new Date(dep.timestamp).toLocaleDateString();
|
|
83
|
+
const size = dep.totalBytes ? formatSize(dep.totalBytes) : '-';
|
|
75
84
|
|
|
76
|
-
// Determine status
|
|
77
|
-
let status
|
|
85
|
+
// Determine status with colors
|
|
86
|
+
let status;
|
|
78
87
|
if (dep.expiresAt) {
|
|
79
88
|
if (isExpired(dep.expiresAt)) {
|
|
80
|
-
status = chalk.red('expired');
|
|
89
|
+
status = chalk.red.bold('ā expired');
|
|
81
90
|
} else {
|
|
82
|
-
status = chalk.yellow(formatTimeRemaining(dep.expiresAt));
|
|
91
|
+
status = chalk.yellow(`ā± ${formatTimeRemaining(dep.expiresAt)}`);
|
|
83
92
|
}
|
|
93
|
+
} else {
|
|
94
|
+
status = chalk.green.bold('ā active');
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
// Version badge
|
|
98
|
+
const versionBadge = chalk.magenta(`v${dep.version || 1}`);
|
|
99
|
+
|
|
86
100
|
console.log(
|
|
87
101
|
chalk.cyan(padRight(url, 40)) +
|
|
88
|
-
padRight(dep.folderName || '-', 15) +
|
|
89
|
-
padRight(String(dep.fileCount), 7) +
|
|
102
|
+
chalk.white(padRight(dep.folderName || '-', 15)) +
|
|
103
|
+
chalk.white(padRight(String(dep.fileCount), 7)) +
|
|
104
|
+
chalk.white(padRight(size, 12)) +
|
|
90
105
|
chalk.gray(padRight(date, 12)) +
|
|
91
|
-
status
|
|
106
|
+
status + ' ' + versionBadge
|
|
92
107
|
);
|
|
93
108
|
}
|
|
94
109
|
|
|
95
|
-
console.log(chalk.gray('ā'.repeat(
|
|
96
|
-
|
|
110
|
+
console.log(chalk.gray('ā'.repeat(100)));
|
|
111
|
+
const syncStatus = source === 'api'
|
|
112
|
+
? chalk.green(' ā synced')
|
|
113
|
+
: chalk.yellow(' ā local only');
|
|
114
|
+
console.log(chalk.gray(`Total: ${deployments.length} deployment(s)`) + syncStatus);
|
|
97
115
|
console.log('');
|
|
98
116
|
|
|
99
117
|
} catch (err) {
|
|
100
|
-
|
|
118
|
+
errorWithSuggestions(`Failed to list deployments: ${err.message}`, [
|
|
119
|
+
'Check your internet connection',
|
|
120
|
+
'Use --local flag to show local deployments only',
|
|
121
|
+
'Try running with --verbose for more details',
|
|
122
|
+
], { verbose, cause: err });
|
|
101
123
|
process.exit(1);
|
|
102
124
|
}
|
|
103
125
|
}
|
package/src/commands/rollback.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { getVersionsForSubdomain, setActiveVersion, getActiveVersion } from '../utils/metadata.js';
|
|
2
2
|
import { getVersions as getVersionsFromAPI, rollbackVersion as rollbackViaAPI } from '../utils/api.js';
|
|
3
|
-
import {
|
|
3
|
+
import { error, errorWithSuggestions, info, warning, spinner } from '../utils/logger.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Rollback a subdomain to a previous version
|
|
7
8
|
* @param {string} subdomain - Subdomain to rollback
|
|
8
9
|
* @param {object} options - Command options
|
|
9
10
|
* @param {number} options.to - Specific version to rollback to (optional)
|
|
11
|
+
* @param {boolean} options.verbose - Show verbose error details
|
|
10
12
|
*/
|
|
11
13
|
export async function rollback(subdomain, options) {
|
|
14
|
+
const verbose = options.verbose || false;
|
|
15
|
+
|
|
12
16
|
try {
|
|
13
|
-
|
|
17
|
+
const fetchSpinner = spinner(`Checking versions for ${subdomain}...`);
|
|
14
18
|
|
|
15
19
|
// Get all versions for this subdomain (try API first)
|
|
16
20
|
let versions = [];
|
|
@@ -33,16 +37,22 @@ export async function rollback(subdomain, options) {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
if (versions.length === 0) {
|
|
36
|
-
|
|
40
|
+
fetchSpinner.fail('No deployments found');
|
|
41
|
+
errorWithSuggestions(`No deployments found for subdomain: ${subdomain}`, [
|
|
42
|
+
'Check the subdomain name is correct',
|
|
43
|
+
'Run "launchpd list" to see your deployments',
|
|
44
|
+
], { verbose });
|
|
37
45
|
process.exit(1);
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
if (versions.length === 1) {
|
|
41
|
-
|
|
49
|
+
fetchSpinner.warn('Only one version exists');
|
|
50
|
+
warning('Nothing to rollback to.');
|
|
42
51
|
process.exit(1);
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
fetchSpinner.succeed(`Found ${versions.length} versions`);
|
|
55
|
+
info(`Current active version: ${chalk.cyan(`v${currentActive}`)}`);
|
|
46
56
|
|
|
47
57
|
// Determine target version
|
|
48
58
|
let targetVersion;
|
|
@@ -51,9 +61,12 @@ export async function rollback(subdomain, options) {
|
|
|
51
61
|
const versionExists = versions.some(v => v.version === targetVersion);
|
|
52
62
|
if (!versionExists) {
|
|
53
63
|
error(`Version ${targetVersion} does not exist.`);
|
|
64
|
+
console.log('');
|
|
54
65
|
info('Available versions:');
|
|
55
66
|
versions.forEach(v => {
|
|
56
|
-
|
|
67
|
+
const isActive = v.version === currentActive;
|
|
68
|
+
const marker = isActive ? chalk.green(' (active)') : '';
|
|
69
|
+
console.log(` ${chalk.cyan(`v${v.version}`)} - ${chalk.gray(v.timestamp)}${marker}`);
|
|
57
70
|
});
|
|
58
71
|
process.exit(1);
|
|
59
72
|
}
|
|
@@ -69,18 +82,18 @@ export async function rollback(subdomain, options) {
|
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
if (targetVersion === currentActive) {
|
|
72
|
-
warning(`Version ${targetVersion} is already active.`);
|
|
85
|
+
warning(`Version ${chalk.cyan(`v${targetVersion}`)} is already active.`);
|
|
73
86
|
process.exit(0);
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
|
|
89
|
+
const rollbackSpinner = spinner(`Rolling back from v${currentActive} to v${targetVersion}...`);
|
|
77
90
|
|
|
78
91
|
// Set the target version as active
|
|
79
92
|
if (useAPI) {
|
|
80
93
|
// Use API for centralized rollback (updates both D1 and R2)
|
|
81
94
|
const result = await rollbackViaAPI(subdomain, targetVersion);
|
|
82
95
|
if (!result) {
|
|
83
|
-
|
|
96
|
+
rollbackSpinner.warn('API unavailable, using local rollback');
|
|
84
97
|
await setActiveVersion(subdomain, targetVersion);
|
|
85
98
|
}
|
|
86
99
|
} else {
|
|
@@ -90,12 +103,16 @@ export async function rollback(subdomain, options) {
|
|
|
90
103
|
// Find the target version's deployment record for file count
|
|
91
104
|
const targetDeployment = versions.find(v => v.version === targetVersion);
|
|
92
105
|
|
|
93
|
-
|
|
106
|
+
rollbackSpinner.succeed(`Rolled back to ${chalk.cyan(`v${targetVersion}`)}`);
|
|
94
107
|
console.log(`\n š https://${subdomain}.launchpd.cloud\n`);
|
|
95
|
-
info(`Restored deployment from: ${targetDeployment?.timestamp || 'unknown'}`);
|
|
108
|
+
info(`Restored deployment from: ${chalk.gray(targetDeployment?.timestamp || 'unknown')}`);
|
|
96
109
|
|
|
97
110
|
} catch (err) {
|
|
98
|
-
|
|
111
|
+
errorWithSuggestions(`Rollback failed: ${err.message}`, [
|
|
112
|
+
'Check your internet connection',
|
|
113
|
+
'Verify the subdomain and version exist',
|
|
114
|
+
'Run "launchpd versions <subdomain>" to see available versions',
|
|
115
|
+
], { verbose, cause: err });
|
|
99
116
|
process.exit(1);
|
|
100
117
|
}
|
|
101
118
|
}
|
package/src/commands/versions.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { getVersionsForSubdomain, getActiveVersion } from '../utils/metadata.js';
|
|
2
2
|
import { getVersions as getVersionsFromAPI } from '../utils/api.js';
|
|
3
|
-
import { success,
|
|
3
|
+
import { success, errorWithSuggestions, info, spinner, formatSize } from '../utils/logger.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* List all versions for a subdomain
|
|
7
8
|
* @param {string} subdomain - Subdomain to list versions for
|
|
8
9
|
* @param {object} options - Command options
|
|
9
10
|
* @param {boolean} options.json - Output as JSON
|
|
11
|
+
* @param {boolean} options.verbose - Show verbose error details
|
|
10
12
|
*/
|
|
11
13
|
export async function versions(subdomain, options) {
|
|
14
|
+
const verbose = options.verbose || false;
|
|
15
|
+
|
|
12
16
|
try {
|
|
13
|
-
|
|
17
|
+
const fetchSpinner = spinner(`Fetching versions for ${subdomain}...`);
|
|
14
18
|
|
|
15
19
|
let versionList = [];
|
|
16
20
|
let activeVersion = 1;
|
|
@@ -32,10 +36,17 @@ export async function versions(subdomain, options) {
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
if (versionList.length === 0) {
|
|
35
|
-
|
|
39
|
+
fetchSpinner.fail(`No deployments found for: ${subdomain}`);
|
|
40
|
+
errorWithSuggestions(`No deployments found for subdomain: ${subdomain}`, [
|
|
41
|
+
'Check the subdomain name is correct',
|
|
42
|
+
'Run "launchpd list" to see your deployments',
|
|
43
|
+
'Deploy a new site with "launchpd deploy ./folder"',
|
|
44
|
+
], { verbose });
|
|
36
45
|
process.exit(1);
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
fetchSpinner.succeed(`Found ${versionList.length} version(s)`);
|
|
49
|
+
|
|
39
50
|
if (options.json) {
|
|
40
51
|
console.log(JSON.stringify({
|
|
41
52
|
subdomain,
|
|
@@ -52,24 +63,37 @@ export async function versions(subdomain, options) {
|
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
console.log('');
|
|
55
|
-
success(`Versions for ${subdomain}.launchpd.cloud:`);
|
|
66
|
+
success(`Versions for ${chalk.cyan(subdomain)}.launchpd.cloud:`);
|
|
56
67
|
console.log('');
|
|
57
68
|
|
|
69
|
+
// Table header
|
|
70
|
+
console.log(chalk.gray(' Version Date Files Size Status'));
|
|
71
|
+
console.log(chalk.gray(' ' + 'ā'.repeat(70)));
|
|
72
|
+
|
|
58
73
|
for (const v of versionList) {
|
|
59
74
|
const isActive = v.version === activeVersion;
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
75
|
+
const versionStr = chalk.bold.cyan(`v${v.version}`);
|
|
76
|
+
const date = chalk.gray(new Date(v.timestamp).toLocaleString());
|
|
77
|
+
const files = chalk.white(`${v.fileCount} files`);
|
|
78
|
+
const size = v.totalBytes ? chalk.white(formatSize(v.totalBytes)) : chalk.gray('unknown');
|
|
79
|
+
const status = isActive
|
|
80
|
+
? chalk.green.bold('ā active')
|
|
81
|
+
: chalk.gray('ā inactive');
|
|
63
82
|
|
|
64
|
-
console.log(`
|
|
83
|
+
console.log(` ${versionStr.padEnd(18)}${date.padEnd(30)}${files.padEnd(12)}${size.padEnd(14)}${status}`);
|
|
65
84
|
}
|
|
66
85
|
|
|
86
|
+
console.log(chalk.gray(' ' + 'ā'.repeat(70)));
|
|
67
87
|
console.log('');
|
|
68
|
-
info(`Use
|
|
88
|
+
info(`Use ${chalk.cyan(`launchpd rollback ${subdomain} --to <n>`)} to restore a version.`);
|
|
69
89
|
console.log('');
|
|
70
90
|
|
|
71
91
|
} catch (err) {
|
|
72
|
-
|
|
92
|
+
errorWithSuggestions(`Failed to list versions: ${err.message}`, [
|
|
93
|
+
'Check your internet connection',
|
|
94
|
+
'Verify the subdomain exists',
|
|
95
|
+
'Try running with --verbose for more details',
|
|
96
|
+
], { verbose, cause: err });
|
|
73
97
|
process.exit(1);
|
|
74
98
|
}
|
|
75
99
|
}
|
package/src/utils/logger.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
|
|
4
|
+
// Store active spinner reference
|
|
5
|
+
let activeSpinner = null;
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Log a success message
|
|
@@ -11,9 +15,16 @@ export function success(message) {
|
|
|
11
15
|
/**
|
|
12
16
|
* Log an error message
|
|
13
17
|
* @param {string} message
|
|
18
|
+
* @param {object} options - Optional error details
|
|
19
|
+
* @param {boolean} options.verbose - Show verbose error details
|
|
20
|
+
* @param {Error} options.cause - Original error for verbose mode
|
|
14
21
|
*/
|
|
15
|
-
export function error(message) {
|
|
22
|
+
export function error(message, options = {}) {
|
|
16
23
|
console.error(chalk.red.bold('ā'), chalk.red(message));
|
|
24
|
+
if (options.verbose && options.cause) {
|
|
25
|
+
console.error(chalk.gray(' Stack trace:'));
|
|
26
|
+
console.error(chalk.gray(` ${options.cause.stack || options.cause.message}`));
|
|
27
|
+
}
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
/**
|
|
@@ -31,3 +42,115 @@ export function info(message) {
|
|
|
31
42
|
export function warning(message) {
|
|
32
43
|
console.log(chalk.yellow.bold('ā '), chalk.yellow(message));
|
|
33
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create and start a spinner
|
|
48
|
+
* @param {string} text - Initial spinner text
|
|
49
|
+
* @returns {object} - Spinner instance with helper methods
|
|
50
|
+
*/
|
|
51
|
+
export function spinner(text) {
|
|
52
|
+
activeSpinner = ora({
|
|
53
|
+
text,
|
|
54
|
+
color: 'cyan',
|
|
55
|
+
spinner: 'dots',
|
|
56
|
+
}).start();
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
/**
|
|
60
|
+
* Update spinner text
|
|
61
|
+
* @param {string} newText
|
|
62
|
+
*/
|
|
63
|
+
update(newText) {
|
|
64
|
+
if (activeSpinner) {
|
|
65
|
+
activeSpinner.text = newText;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Mark spinner as successful and stop
|
|
71
|
+
* @param {string} text - Success message
|
|
72
|
+
*/
|
|
73
|
+
succeed(text) {
|
|
74
|
+
if (activeSpinner) {
|
|
75
|
+
activeSpinner.succeed(chalk.green(text));
|
|
76
|
+
activeSpinner = null;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Mark spinner as failed and stop
|
|
82
|
+
* @param {string} text - Failure message
|
|
83
|
+
*/
|
|
84
|
+
fail(text) {
|
|
85
|
+
if (activeSpinner) {
|
|
86
|
+
activeSpinner.fail(chalk.red(text));
|
|
87
|
+
activeSpinner = null;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Stop spinner with info message
|
|
93
|
+
* @param {string} text - Info message
|
|
94
|
+
*/
|
|
95
|
+
info(text) {
|
|
96
|
+
if (activeSpinner) {
|
|
97
|
+
activeSpinner.info(chalk.blue(text));
|
|
98
|
+
activeSpinner = null;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Stop spinner with warning
|
|
104
|
+
* @param {string} text - Warning message
|
|
105
|
+
*/
|
|
106
|
+
warn(text) {
|
|
107
|
+
if (activeSpinner) {
|
|
108
|
+
activeSpinner.warn(chalk.yellow(text));
|
|
109
|
+
activeSpinner = null;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Stop spinner without any symbol
|
|
115
|
+
*/
|
|
116
|
+
stop() {
|
|
117
|
+
if (activeSpinner) {
|
|
118
|
+
activeSpinner.stop();
|
|
119
|
+
activeSpinner = null;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Format bytes to human readable size (KB/MB/GB)
|
|
127
|
+
* @param {number} bytes - Size in bytes
|
|
128
|
+
* @param {number} decimals - Number of decimal places
|
|
129
|
+
* @returns {string} - Formatted size string
|
|
130
|
+
*/
|
|
131
|
+
export function formatSize(bytes, decimals = 2) {
|
|
132
|
+
if (bytes === 0) return '0 Bytes';
|
|
133
|
+
|
|
134
|
+
const k = 1024;
|
|
135
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
136
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
137
|
+
|
|
138
|
+
return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Log helpful error with suggestions
|
|
143
|
+
* @param {string} message - Error message
|
|
144
|
+
* @param {string[]} suggestions - Array of suggested actions
|
|
145
|
+
* @param {object} options - Error options
|
|
146
|
+
*/
|
|
147
|
+
export function errorWithSuggestions(message, suggestions = [], options = {}) {
|
|
148
|
+
error(message, options);
|
|
149
|
+
if (suggestions.length > 0) {
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(chalk.yellow('š” Suggestions:'));
|
|
152
|
+
suggestions.forEach(suggestion => {
|
|
153
|
+
console.log(chalk.gray(` ⢠${suggestion}`));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/utils/upload.js
CHANGED
|
@@ -2,7 +2,6 @@ import { readdir, readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join, relative, posix, sep } from 'node:path';
|
|
3
3
|
import mime from 'mime-types';
|
|
4
4
|
import { config } from '../config.js';
|
|
5
|
-
import { info } from './logger.js';
|
|
6
5
|
|
|
7
6
|
const API_BASE_URL = `https://api.${config.domain}`;
|
|
8
7
|
|
|
@@ -91,8 +90,9 @@ async function completeUpload(subdomain, version, fileCount, totalBytes, folderN
|
|
|
91
90
|
* @param {string} localPath - Local folder path
|
|
92
91
|
* @param {string} subdomain - Subdomain to use as bucket prefix
|
|
93
92
|
* @param {number} version - Version number for this deployment
|
|
93
|
+
* @param {function} onProgress - Progress callback (uploaded, total, fileName)
|
|
94
94
|
*/
|
|
95
|
-
export async function uploadFolder(localPath, subdomain, version = 1) {
|
|
95
|
+
export async function uploadFolder(localPath, subdomain, version = 1, onProgress = null) {
|
|
96
96
|
const files = await readdir(localPath, { recursive: true, withFileTypes: true });
|
|
97
97
|
|
|
98
98
|
let uploaded = 0;
|
|
@@ -119,7 +119,11 @@ export async function uploadFolder(localPath, subdomain, version = 1) {
|
|
|
119
119
|
await uploadFile(body, subdomain, version, posixPath, contentType);
|
|
120
120
|
|
|
121
121
|
uploaded++;
|
|
122
|
-
|
|
122
|
+
|
|
123
|
+
// Call progress callback if provided
|
|
124
|
+
if (onProgress) {
|
|
125
|
+
onProgress(uploaded, total, posixPath);
|
|
126
|
+
}
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
return { uploaded, subdomain, totalBytes };
|