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 +21 -0
- package/README.md +96 -0
- package/bin/cli.js +94 -0
- package/bin/setup.js +40 -0
- package/package.json +67 -0
- package/src/commands/auth.js +357 -0
- package/src/commands/deploy.js +242 -0
- package/src/commands/index.js +9 -0
- package/src/commands/list.js +133 -0
- package/src/commands/rollback.js +119 -0
- package/src/commands/versions.js +117 -0
- package/src/config.js +14 -0
- package/src/utils/api.js +182 -0
- package/src/utils/credentials.js +153 -0
- package/src/utils/expiration.js +89 -0
- package/src/utils/id.js +17 -0
- package/src/utils/index.js +14 -0
- package/src/utils/localConfig.js +85 -0
- package/src/utils/logger.js +152 -0
- package/src/utils/machineId.js +28 -0
- package/src/utils/metadata.js +201 -0
- package/src/utils/prompt.js +87 -0
- package/src/utils/quota.js +231 -0
- package/src/utils/upload.js +181 -0
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
|
+
}
|