launchpd 1.0.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Launchpd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Launchpd
2
+
3
+ Deploy static sites instantly to a live URL. No config, no complexity.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npm install -g launchpd
9
+ launchpd deploy ./my-site
10
+ ```
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install -g launchpd
16
+ ```
17
+
18
+ Requires Node.js 20 or higher.
19
+
20
+ ## Usage
21
+
22
+ ### Deploy a folder
23
+
24
+ ```bash
25
+ launchpd deploy ./my-folder
26
+ ```
27
+
28
+ ### Use a custom subdomain
29
+
30
+ ```bash
31
+ launchpd deploy ./my-folder --name my-project
32
+ ```
33
+
34
+ ### Set expiration time
35
+
36
+ ```bash
37
+ launchpd deploy ./my-folder --expires 2h
38
+ # Auto-deletes after 2 hours
39
+ ```
40
+
41
+
42
+
43
+ ### List your deployments
44
+
45
+ ```bash
46
+ launchpd list
47
+ ```
48
+
49
+ ### View version history
50
+
51
+ ```bash
52
+ launchpd versions my-subdomain
53
+ ```
54
+
55
+ ### Rollback to previous version
56
+
57
+ ```bash
58
+ launchpd rollback my-subdomain
59
+ launchpd rollback my-subdomain --to 2
60
+ ```
61
+
62
+ ## Authentication
63
+
64
+ ### Register for a free account
65
+
66
+ ```bash
67
+ launchpd register
68
+ ```
69
+
70
+ ### Login with your API key
71
+
72
+ ```bash
73
+ launchpd login
74
+ ```
75
+
76
+ ### Check current user and quota
77
+
78
+ ```bash
79
+ launchpd whoami
80
+ launchpd quota
81
+ ```
82
+
83
+ ### Logout
84
+
85
+ ```bash
86
+ launchpd logout
87
+ ```
88
+
89
+ ## Support
90
+
91
+ - [Report issues](https://github.com/kents00/launchpd/issues)
92
+ - [Documentation](https://launchpd.cloud/docs)
93
+
94
+ ## License
95
+
96
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { deploy } from '../src/commands/deploy.js';
5
+ import { list } from '../src/commands/list.js';
6
+ import { rollback } from '../src/commands/rollback.js';
7
+ import { versions } from '../src/commands/versions.js';
8
+ import { login, logout, register, whoami, quota } from '../src/commands/auth.js';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('launchpd')
14
+ .description('Deploy static sites instantly to a live URL')
15
+ .version('0.1.12');
16
+
17
+ program
18
+ .command('deploy')
19
+ .description('Deploy a folder to a live URL')
20
+ .argument('<folder>', 'Path to the folder to deploy')
21
+ .option('--name <subdomain>', 'Use a custom subdomain (optional)')
22
+ .option('--expires <time>', 'Auto-delete after time (e.g., 30m, 2h, 1d). Minimum: 30m')
23
+ .option('--verbose', 'Show detailed error information')
24
+ .action(async (folder, options) => {
25
+ await deploy(folder, options);
26
+ });
27
+
28
+ program
29
+ .command('list')
30
+ .description('List your past deployments')
31
+ .option('--json', 'Output as JSON')
32
+ .option('--local', 'Only show local deployments')
33
+ .option('--verbose', 'Show detailed error information')
34
+ .action(async (options) => {
35
+ await list(options);
36
+ });
37
+
38
+ program
39
+ .command('versions')
40
+ .description('List all versions for a subdomain')
41
+ .argument('<subdomain>', 'The subdomain to list versions for')
42
+ .option('--json', 'Output as JSON')
43
+ .option('--verbose', 'Show detailed error information')
44
+ .action(async (subdomain, options) => {
45
+ await versions(subdomain, options);
46
+ });
47
+
48
+ program
49
+ .command('rollback')
50
+ .description('Rollback a subdomain to a previous version')
51
+ .argument('<subdomain>', 'The subdomain to rollback')
52
+ .option('--to <n>', 'Specific version number to rollback to')
53
+ .option('--verbose', 'Show detailed error information')
54
+ .action(async (subdomain, options) => {
55
+ await rollback(subdomain, options);
56
+ });
57
+
58
+ // Authentication commands
59
+ program
60
+ .command('login')
61
+ .description('Login with your API key')
62
+ .action(async () => {
63
+ await login();
64
+ });
65
+
66
+ program
67
+ .command('logout')
68
+ .description('Clear stored credentials')
69
+ .action(async () => {
70
+ await logout();
71
+ });
72
+
73
+ program
74
+ .command('register')
75
+ .description('Open browser to create a new account')
76
+ .action(async () => {
77
+ await register();
78
+ });
79
+
80
+ program
81
+ .command('whoami')
82
+ .description('Show current user info and quota status')
83
+ .action(async () => {
84
+ await whoami();
85
+ });
86
+
87
+ program
88
+ .command('quota')
89
+ .description('Check current quota and usage')
90
+ .action(async () => {
91
+ await quota();
92
+ });
93
+
94
+ program.parseAsync();
package/bin/setup.js ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { config } from '../src/config.js';
4
+ import { info, success } from '../src/utils/logger.js';
5
+ import chalk from 'chalk';
6
+
7
+ /**
8
+ * Setup script to display CLI information
9
+ */
10
+ async function setup() {
11
+ console.log('\n' + chalk.bold.blue('═══════════════════════════════════════'));
12
+ console.log(chalk.bold.blue(' Launchpd CLI'));
13
+ console.log(chalk.bold.blue('═══════════════════════════════════════\n'));
14
+
15
+ info('Launchpd is ready to use!\n');
16
+
17
+ console.log(chalk.bold('Configuration:'));
18
+ console.log(chalk.gray('─'.repeat(50)));
19
+ console.log(chalk.cyan(' Domain: '), config.domain);
20
+ console.log(chalk.cyan(' API: '), config.apiUrl);
21
+ console.log(chalk.cyan(' Version: '), config.version);
22
+ console.log(chalk.gray('─'.repeat(50)) + '\n');
23
+
24
+ console.log(chalk.bold('Quick Start:'));
25
+ console.log(chalk.gray(' Deploy your first site:'));
26
+ console.log(chalk.cyan(' launchpd deploy ./your-folder\n'));
27
+
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!');
35
+ }
36
+
37
+ setup().catch(err => {
38
+ console.error(`Setup failed: ${err.message}`);
39
+ process.exit(1);
40
+ });
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "launchpd",
3
+ "version": "1.0.0",
4
+ "description": "Deploy static sites instantly to a live URL",
5
+ "keywords": [
6
+ "static",
7
+ "hosting",
8
+ "deploy",
9
+ "cli",
10
+ "cdn",
11
+ "website",
12
+ "publish"
13
+ ],
14
+ "homepage": "https://launchpd.cloud",
15
+ "bugs": {
16
+ "url": "https://github.com/kents00/launchpd/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/kents00/launchpd.git",
21
+ "directory": "cli"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Kent John Edoloverio",
25
+ "type": "module",
26
+ "exports": {
27
+ ".": "./src/commands/index.js",
28
+ "./utils": "./src/utils/index.js"
29
+ },
30
+ "main": "./src/commands/index.js",
31
+ "bin": {
32
+ "launchpd": "bin/cli.js"
33
+ },
34
+ "directories": {
35
+ "test": "tests"
36
+ },
37
+ "files": [
38
+ "bin",
39
+ "src"
40
+ ],
41
+ "scripts": {
42
+ "start": "node bin/cli.js",
43
+ "dev": "node bin/cli.js deploy ../examples/test-site",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "test:coverage": "vitest run --coverage",
47
+ "lint": "eslint src bin --ext .js",
48
+ "lint:fix": "eslint src bin --ext .js --fix",
49
+ "prepublishOnly": "npm run lint && npm run test"
50
+ },
51
+ "dependencies": {
52
+ "chalk": "^5.4.0",
53
+ "commander": "^14.0.0",
54
+ "mime-types": "^2.1.35",
55
+ "nanoid": "^5.1.0",
56
+ "ora": "^8.0.1"
57
+ },
58
+ "devDependencies": {
59
+ "@eslint/js": "^9.39.2",
60
+ "@vitest/coverage-v8": "^2.1.0",
61
+ "eslint": "^9.0.0",
62
+ "vitest": "^2.1.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=20.0.0"
66
+ }
67
+ }
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Authentication commands for StaticLaunch CLI
3
+ * login, logout, register, whoami
4
+ */
5
+
6
+ import { exec } from 'node:child_process';
7
+ import { promptSecret } from '../utils/prompt.js';
8
+ import { config } from '../config.js';
9
+ import { getCredentials, saveCredentials, clearCredentials, isLoggedIn } from '../utils/credentials.js';
10
+ import { success, error, errorWithSuggestions, info, warning, spinner } from '../utils/logger.js';
11
+ import { formatBytes } from '../utils/quota.js';
12
+ import chalk from 'chalk';
13
+
14
+ const API_BASE_URL = config.apiUrl;
15
+ const REGISTER_URL = `https://${config.domain}/`;
16
+
17
+
18
+ /**
19
+ * Validate API key with the server
20
+ */
21
+ async function validateApiKey(apiKey) {
22
+ try {
23
+ const response = await fetch(`${API_BASE_URL}/api/quota`, {
24
+ headers: {
25
+ 'X-API-Key': apiKey,
26
+ },
27
+ });
28
+
29
+ if (!response.ok) {
30
+ return null;
31
+ }
32
+
33
+ const data = await response.json();
34
+ if (data.authenticated) {
35
+ return data;
36
+ }
37
+ return null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Background update credentials if new data (like apiSecret) is available
45
+ */
46
+ async function updateCredentialsIfNeeded(creds, result) {
47
+ if (result.user?.api_secret && !creds.apiSecret) {
48
+ await saveCredentials({
49
+ ...creds,
50
+ apiSecret: result.user.api_secret,
51
+ userId: result.user.id || creds.userId,
52
+ email: result.user.email || creds.email,
53
+ });
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Login command - prompts for API key and validates it
59
+ */
60
+ export async function login() {
61
+ // Check if already logged in
62
+ if (await isLoggedIn()) {
63
+ const creds = await getCredentials();
64
+ warning(`Already logged in as ${chalk.cyan(creds.email || creds.userId)}`);
65
+ info('Run "launchpd logout" to switch accounts');
66
+ return;
67
+ }
68
+
69
+ console.log('\nLaunchpd Login\n');
70
+ console.log('Enter your API key from the dashboard.');
71
+ console.log(`Don't have one? Run ${chalk.cyan('"launchpd register"')} first.\n`);
72
+
73
+ const apiKey = await promptSecret('API Key: ');
74
+
75
+ if (!apiKey) {
76
+ errorWithSuggestions('API key is required', [
77
+ 'Get your API key from the dashboard',
78
+ `Visit: https://${config.domain}/settings`,
79
+ 'Run "launchpd register" if you don\'t have an account',
80
+ ]);
81
+ process.exit(1);
82
+ }
83
+
84
+ const validateSpinner = spinner('Validating API key...');
85
+
86
+ const result = await validateApiKey(apiKey);
87
+
88
+ if (!result) {
89
+ validateSpinner.fail('Invalid API key');
90
+ errorWithSuggestions('Please check and try again.', [
91
+ `Get your API key at: https://portal.${config.domain}/api-keys`,
92
+ 'Make sure you copied the full key',
93
+ 'API keys start with "lpd_"',
94
+ ]);
95
+ process.exit(1);
96
+ }
97
+
98
+ // Save credentials
99
+ await saveCredentials({
100
+ apiKey,
101
+ apiSecret: result.user?.api_secret,
102
+ userId: result.user?.id,
103
+ email: result.user?.email,
104
+ tier: result.tier,
105
+ });
106
+
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('');
114
+ }
115
+
116
+ /**
117
+ * Logout command - clears stored credentials
118
+ */
119
+ export async function logout() {
120
+ const loggedIn = await isLoggedIn();
121
+
122
+ if (!loggedIn) {
123
+ warning('Not currently logged in');
124
+ return;
125
+ }
126
+
127
+ const creds = await getCredentials();
128
+ await clearCredentials();
129
+
130
+ success('Logged out successfully');
131
+ if (creds?.email) {
132
+ info(`Was logged in as: ${chalk.cyan(creds.email)}`);
133
+ }
134
+ console.log(`\nYou can still deploy anonymously (limited to ${chalk.yellow('3 sites')}, ${chalk.yellow('50MB')}).`);
135
+ }
136
+
137
+ /**
138
+ * Register command - opens browser to registration page
139
+ */
140
+ export async function register() {
141
+ console.log('\nRegister for Launchpd\n');
142
+ console.log(`Opening registration page: ${chalk.cyan(REGISTER_URL)}\n`);
143
+
144
+ // Open browser based on platform
145
+ const platform = process.platform;
146
+ let cmd;
147
+
148
+ if (platform === 'darwin') {
149
+ cmd = `open "${REGISTER_URL}"`;
150
+ } else if (platform === 'win32') {
151
+ cmd = `start "" "${REGISTER_URL}"`;
152
+ } else {
153
+ cmd = `xdg-open "${REGISTER_URL}"`;
154
+ }
155
+
156
+ exec(cmd, (err) => {
157
+ if (err) {
158
+ console.log(`Please open this URL in your browser:\n ${chalk.cyan(REGISTER_URL)}\n`);
159
+ }
160
+ });
161
+
162
+ console.log('After registering:');
163
+ console.log(` 1. Get your API key from the dashboard`);
164
+ console.log(` 2. Run: ${chalk.cyan('launchpd login')}`);
165
+ console.log('');
166
+
167
+ info('Registration benefits:');
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')}`);
172
+ console.log('');
173
+ }
174
+
175
+ /**
176
+ * Whoami command - shows current user info and quota status
177
+ */
178
+ export async function whoami() {
179
+ const creds = await getCredentials();
180
+
181
+ if (!creds) {
182
+ console.log('\n👤 Not logged in (anonymous mode)\n');
183
+ console.log('Anonymous limits:');
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`);
190
+ return;
191
+ }
192
+
193
+ info('Fetching account status...');
194
+
195
+ // Validate and get current quota
196
+ const result = await validateApiKey(creds.apiKey);
197
+
198
+ if (!result) {
199
+ warning('Session expired or API key invalid');
200
+ await clearCredentials();
201
+ error('Please login again with: launchpd login');
202
+ process.exit(1);
203
+ }
204
+
205
+ // Background upgrade to apiSecret if missing
206
+ await updateCredentialsIfNeeded(creds, result);
207
+
208
+ console.log(`\nLogged in as: ${result.user?.email || result.user?.id}\n`);
209
+
210
+ console.log('Account Info:');
211
+ console.log(` User ID: ${result.user?.id}`);
212
+ console.log(` Email: ${result.user?.email || 'Not set'} ${result.user?.email_verified ? chalk.green('(Verified)') : chalk.yellow('(Unverified)')}`);
213
+ console.log(` 2FA: ${result.user?.is_2fa_enabled ? chalk.green('Enabled') : chalk.gray('Disabled')}`);
214
+ console.log(` Tier: ${result.tier}`);
215
+ console.log('');
216
+
217
+ console.log('Usage:');
218
+ console.log(` Sites: ${result.usage?.siteCount || 0} / ${result.limits?.maxSites}`);
219
+ console.log(` Storage: ${result.usage?.storageUsedMB || 0}MB / ${result.limits?.maxStorageMB}MB`);
220
+ console.log(` Sites remaining: ${result.usage?.sitesRemaining || 0}`);
221
+ console.log(` Storage remaining: ${result.usage?.storageRemainingMB || 0}MB`);
222
+ console.log('');
223
+
224
+ console.log('Limits:');
225
+ console.log(` Max versions per site: ${result.limits?.maxVersionsPerSite}`);
226
+ console.log(` Retention: ${result.limits?.retentionDays} days`);
227
+ console.log('');
228
+
229
+ // Show warnings
230
+ if (result.warnings && result.warnings.length > 0) {
231
+ console.log('⚠️ Warnings:');
232
+ result.warnings.forEach(w => console.log(` ${w}`));
233
+ console.log('');
234
+ }
235
+
236
+ if (!result.canCreateNewSite) {
237
+ warning('You cannot create new sites (limit reached)');
238
+ info('You can still update existing sites');
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Quota command - shows detailed quota information
244
+ */
245
+ export async function quota() {
246
+ const creds = await getCredentials();
247
+
248
+ if (!creds) {
249
+ console.log(`\n${chalk.bold('Anonymous Quota Status')}\n`);
250
+ console.log(chalk.gray('You are not logged in.'));
251
+ console.log('');
252
+ console.log(chalk.bold('Anonymous tier limits:'));
253
+ console.log(chalk.gray(' ┌─────────────────────────────────┐'));
254
+ console.log(chalk.gray(' │') + ` Sites: ${chalk.white('3 maximum')} ` + chalk.gray('│'));
255
+ console.log(chalk.gray(' │') + ` Storage: ${chalk.white('50MB total')} ` + chalk.gray('│'));
256
+ console.log(chalk.gray(' │') + ` Retention: ${chalk.white('7 days')} ` + chalk.gray('│'));
257
+ console.log(chalk.gray(' │') + ` Versions: ${chalk.white('1 per site')} ` + chalk.gray('│'));
258
+ console.log(chalk.gray(' └─────────────────────────────────┘'));
259
+ console.log('');
260
+ console.log(`${chalk.cyan('Register for FREE')} to unlock more:`);
261
+ console.log(` ${chalk.green('→')} ${chalk.white('10 sites')}`);
262
+ console.log(` ${chalk.green('→')} ${chalk.white('100MB storage')}`);
263
+ console.log(` ${chalk.green('→')} ${chalk.white('30-day retention')}`);
264
+ console.log(` ${chalk.green('→')} ${chalk.white('10 versions per site')}`);
265
+ console.log('');
266
+ console.log(`Run: ${chalk.cyan('launchpd register')}`);
267
+ console.log('');
268
+ return;
269
+ }
270
+
271
+ const fetchSpinner = spinner('Fetching quota status...');
272
+
273
+ const result = await validateApiKey(creds.apiKey);
274
+
275
+ if (!result) {
276
+ fetchSpinner.fail('Failed to fetch quota');
277
+ errorWithSuggestions('API key may be invalid.', [
278
+ 'Run "launchpd login" to re-authenticate',
279
+ 'Check your internet connection',
280
+ ]);
281
+ process.exit(1);
282
+ }
283
+
284
+ // Background upgrade to apiSecret if missing
285
+ await updateCredentialsIfNeeded(creds, result);
286
+
287
+ fetchSpinner.succeed('Quota fetched');
288
+ console.log(`\n${chalk.bold('Quota Status for:')} ${chalk.cyan(result.user?.email || creds.email)}\n`);
289
+
290
+ // Sites usage
291
+ const sitesUsed = result.usage?.siteCount || 0;
292
+ const sitesMax = result.limits?.maxSites || 10;
293
+ const sitesPercent = Math.round((sitesUsed / sitesMax) * 100);
294
+ const sitesBar = createProgressBar(sitesUsed, sitesMax);
295
+
296
+ console.log(`${chalk.gray('Sites:')} ${sitesBar} ${chalk.white(sitesUsed)}/${sitesMax} (${getPercentColor(sitesPercent)})`);
297
+
298
+ // Storage usage
299
+ const storageBytes = result.usage?.storageUsed || 0;
300
+ const storageMaxBytes = result.limits?.maxStorageBytes || (result.limits?.maxStorageMB || 100) * 1024 * 1024;
301
+ const storagePercent = Math.round((storageBytes / storageMaxBytes) * 100);
302
+ const storageBar = createProgressBar(storageBytes, storageMaxBytes);
303
+
304
+ console.log(`${chalk.gray('Storage:')} ${storageBar} ${chalk.white(formatBytes(storageBytes))}/${formatBytes(storageMaxBytes)} (${getPercentColor(storagePercent)})`);
305
+
306
+ console.log('');
307
+ console.log(`${chalk.gray('Tier:')} ${chalk.green(result.tier || 'free')}`);
308
+ console.log(`${chalk.gray('Retention:')} ${chalk.white(result.limits?.retentionDays || 30)} days`);
309
+ console.log(`${chalk.gray('Max versions:')} ${chalk.white(result.limits?.maxVersionsPerSite || 10)} per site`);
310
+ console.log('');
311
+
312
+ // Status indicators
313
+ if (result.canCreateNewSite === false) {
314
+ warning('Site limit reached - cannot create new sites');
315
+ }
316
+
317
+ if (storagePercent > 80) {
318
+ warning(`Storage ${storagePercent}% used - consider cleaning up old deployments`);
319
+ }
320
+
321
+ console.log('');
322
+ }
323
+
324
+ /**
325
+ * Create a simple progress bar with color coding
326
+ */
327
+ function createProgressBar(current, max, width = 20) {
328
+ const filled = Math.round((current / max) * width);
329
+ const empty = width - filled;
330
+ const percent = (current / max) * 100;
331
+
332
+ const filledChar = '█';
333
+ let barColor;
334
+
335
+ if (percent >= 90) {
336
+ barColor = chalk.red;
337
+ } else if (percent >= 70) {
338
+ barColor = chalk.yellow;
339
+ } else {
340
+ barColor = chalk.green;
341
+ }
342
+
343
+ const bar = barColor(filledChar.repeat(filled)) + chalk.gray('░'.repeat(empty));
344
+ return `[${bar}]`;
345
+ }
346
+
347
+ /**
348
+ * Get colored percentage text
349
+ */
350
+ function getPercentColor(percent) {
351
+ if (percent >= 90) {
352
+ return chalk.red(`${percent}%`);
353
+ } else if (percent >= 70) {
354
+ return chalk.yellow(`${percent}%`);
355
+ }
356
+ return chalk.green(`${percent}%`);
357
+ }