launchpd 0.1.2 → 0.1.5
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 +4 -0
- package/README.md +1 -1
- package/bin/cli.js +5 -0
- package/bin/setup.js +18 -40
- package/package.json +3 -6
- package/src/commands/auth.js +95 -65
- package/src/commands/deploy.js +71 -38
- package/src/commands/list.js +37 -15
- package/src/commands/rollback.js +29 -12
- package/src/commands/versions.js +34 -10
- package/src/config.js +8 -30
- package/src/utils/logger.js +124 -1
- package/src/utils/metadata.js +96 -249
- package/src/utils/upload.js +94 -25
package/LICENSE
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
+
<<<<<<< HEAD
|
|
3
4
|
Copyright (c) 2026 Launchpd
|
|
5
|
+
=======
|
|
6
|
+
Copyright (c) 2026 Kent Edoloverio
|
|
7
|
+
>>>>>>> 12ada1bf2dcc63674c3b6955e525a51209308d45
|
|
4
8
|
|
|
5
9
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
10
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
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/bin/setup.js
CHANGED
|
@@ -1,62 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { config
|
|
4
|
-
import { info, success
|
|
3
|
+
import { config } from '../src/config.js';
|
|
4
|
+
import { info, success } from '../src/utils/logger.js';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Setup script to
|
|
8
|
+
* Setup script to display CLI information
|
|
9
9
|
*/
|
|
10
10
|
async function setup() {
|
|
11
11
|
console.log('\n' + chalk.bold.blue('═══════════════════════════════════════'));
|
|
12
|
-
console.log(chalk.bold.blue(' Launchpd
|
|
12
|
+
console.log(chalk.bold.blue(' Launchpd CLI'));
|
|
13
13
|
console.log(chalk.bold.blue('═══════════════════════════════════════\n'));
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
info('Checking configuration...\n');
|
|
15
|
+
info('Launchpd is ready to use!\n');
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!validation.valid) {
|
|
21
|
-
error(`Missing required environment variables:`);
|
|
22
|
-
for (const missing of validation.missing) {
|
|
23
|
-
console.log(chalk.red(` ✗ ${missing}`));
|
|
24
|
-
}
|
|
25
|
-
console.log('\n' + chalk.yellow('Setup Instructions:'));
|
|
26
|
-
console.log(' 1. Copy .env.example to .env:');
|
|
27
|
-
console.log(chalk.gray(' cp .env.example .env'));
|
|
28
|
-
console.log(' 2. Edit .env and add your Cloudflare R2 credentials');
|
|
29
|
-
console.log(' 3. Get credentials from: https://dash.cloudflare.com → R2 → API Tokens\n');
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Display current configuration
|
|
34
|
-
console.log(chalk.green('✓ All required environment variables set\n'));
|
|
35
|
-
|
|
36
|
-
console.log(chalk.bold('Current Configuration:'));
|
|
17
|
+
console.log(chalk.bold('Configuration:'));
|
|
37
18
|
console.log(chalk.gray('─'.repeat(50)));
|
|
38
19
|
console.log(chalk.cyan(' Domain: '), config.domain);
|
|
39
|
-
console.log(chalk.cyan('
|
|
40
|
-
console.log(chalk.cyan('
|
|
41
|
-
console.log(chalk.cyan(' Access Key: '), config.r2.accessKeyId ? '✓ Set' : '✗ Missing');
|
|
42
|
-
console.log(chalk.cyan(' Secret Key: '), config.r2.secretAccessKey ? '✓ Set' : '✗ Missing');
|
|
20
|
+
console.log(chalk.cyan(' API: '), config.apiUrl);
|
|
21
|
+
console.log(chalk.cyan(' Version: '), config.version);
|
|
43
22
|
console.log(chalk.gray('─'.repeat(50)) + '\n');
|
|
44
23
|
|
|
45
|
-
|
|
46
|
-
console.log(chalk.
|
|
47
|
-
console.log(chalk.gray(' 1. Test dry-run deployment:'));
|
|
48
|
-
console.log(chalk.cyan(' npm run dev'));
|
|
49
|
-
console.log(chalk.gray(' 2. Deploy Worker:'));
|
|
50
|
-
console.log(chalk.cyan(' cd worker && wrangler deploy'));
|
|
51
|
-
console.log(chalk.gray(' 3. Verify DNS settings in Cloudflare Dashboard:'));
|
|
52
|
-
console.log(chalk.cyan(` DNS → Records → Add A record * → 192.0.2.1 (Proxied)`));
|
|
53
|
-
console.log(chalk.gray(' 4. Deploy your first site:'));
|
|
24
|
+
console.log(chalk.bold('Quick Start:'));
|
|
25
|
+
console.log(chalk.gray(' Deploy your first site:'));
|
|
54
26
|
console.log(chalk.cyan(' launchpd deploy ./your-folder\n'));
|
|
55
27
|
|
|
56
|
-
|
|
28
|
+
console.log(chalk.gray(' Login for more quota:'));
|
|
29
|
+
console.log(chalk.cyan(' launchpd login\n'));
|
|
30
|
+
|
|
31
|
+
console.log(chalk.gray(' List your deployments:'));
|
|
32
|
+
console.log(chalk.cyan(' launchpd list\n'));
|
|
33
|
+
|
|
34
|
+
success('No configuration needed - just deploy!');
|
|
57
35
|
}
|
|
58
36
|
|
|
59
37
|
setup().catch(err => {
|
|
60
|
-
error(`Setup failed: ${err.message}`);
|
|
38
|
+
console.error(`Setup failed: ${err.message}`);
|
|
61
39
|
process.exit(1);
|
|
62
40
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "launchpd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Deploy static sites instantly to a live URL",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "node bin/cli.js",
|
|
23
23
|
"dev": "node bin/cli.js deploy ../examples/test-site --dry-run",
|
|
24
|
-
"setup": "node bin/setup.js",
|
|
25
24
|
"test": "vitest run",
|
|
26
25
|
"test:watch": "vitest",
|
|
27
26
|
"test:coverage": "vitest run --coverage",
|
|
@@ -35,7 +34,6 @@
|
|
|
35
34
|
"deploy",
|
|
36
35
|
"cli",
|
|
37
36
|
"cloudflare",
|
|
38
|
-
"r2",
|
|
39
37
|
"cdn",
|
|
40
38
|
"website",
|
|
41
39
|
"publish"
|
|
@@ -52,12 +50,11 @@
|
|
|
52
50
|
},
|
|
53
51
|
"homepage": "https://launchpd.cloud",
|
|
54
52
|
"dependencies": {
|
|
55
|
-
"@aws-sdk/client-s3": "^3.700.0",
|
|
56
53
|
"chalk": "^5.4.0",
|
|
57
54
|
"commander": "^14.0.0",
|
|
58
|
-
"dotenv": "^16.4.0",
|
|
59
55
|
"mime-types": "^2.1.35",
|
|
60
|
-
"nanoid": "^5.1.0"
|
|
56
|
+
"nanoid": "^5.1.0",
|
|
57
|
+
"ora": "^8.0.1"
|
|
61
58
|
},
|
|
62
59
|
"devDependencies": {
|
|
63
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
|
+
}
|