launchpd 1.0.0 → 1.0.1
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/README.md +66 -63
- package/bin/cli.js +17 -5
- package/package.json +4 -3
- package/src/commands/deploy.js +40 -6
- package/src/commands/index.js +2 -0
- package/src/commands/init.js +71 -0
- package/src/commands/rollback.js +6 -1
- package/src/commands/status.js +55 -0
- package/src/commands/versions.js +13 -9
- package/src/utils/api.js +2 -1
- package/src/utils/projectConfig.js +59 -0
- package/src/utils/upload.js +4 -3
- package/src/utils/validator.js +84 -0
package/README.md
CHANGED
|
@@ -1,96 +1,99 @@
|
|
|
1
1
|
# Launchpd
|
|
2
2
|
|
|
3
|
-
Deploy static sites instantly to a live URL. No config, no complexity
|
|
3
|
+
**Deploy static sites instantly to a live URL. No config, no complexity.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
npm install -g launchpd
|
|
9
|
-
launchpd deploy ./my-site
|
|
10
|
-
```
|
|
5
|
+
[](https://www.npmjs.com/package/launchpd)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://github.com/kents00/launchpd)
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
npm install -g launchpd
|
|
16
|
-
```
|
|
9
|
+
---
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
## Features
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
* **Blazing Fast**: Deploy folders in seconds with a single command.
|
|
14
|
+
* **Project-Based**: Link local folders to subdomains once and deploy without re-typing names.
|
|
15
|
+
* **Zero Config**: No complex setup; optionally use `.launchpd.json` for project persistence.
|
|
16
|
+
* **Version Control**: Every deployment is versioned with messages. Roll back instantly.
|
|
17
|
+
* **Static-Only Security**: Strict validation ensures only high-performance static assets are deployed.
|
|
18
|
+
* **Secure**: Private uploads with API key authentication or safe anonymous testing.
|
|
19
|
+
* **Auto-Expiration**: Set temporary deployments that delete themselves automatically.
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
launchpd deploy ./my-folder
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Use a custom subdomain
|
|
21
|
+
## Quick Start
|
|
29
22
|
|
|
30
23
|
```bash
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
### Set expiration time
|
|
24
|
+
# Install globally
|
|
25
|
+
npm install -g launchpd
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
launchpd deploy
|
|
38
|
-
# Auto-deletes after 2 hours
|
|
27
|
+
# Deploy your current folder
|
|
28
|
+
launchpd deploy .
|
|
39
29
|
```
|
|
40
30
|
|
|
31
|
+
---
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
### List your deployments
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
launchpd list
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### View version history
|
|
33
|
+
## Installation
|
|
50
34
|
|
|
51
35
|
```bash
|
|
52
|
-
|
|
36
|
+
npm install -g launchpd
|
|
53
37
|
```
|
|
38
|
+
*Requires **Node.js 20** or higher.*
|
|
54
39
|
|
|
55
|
-
|
|
40
|
+
---
|
|
56
41
|
|
|
57
|
-
|
|
58
|
-
launchpd rollback my-subdomain
|
|
59
|
-
launchpd rollback my-subdomain --to 2
|
|
60
|
-
```
|
|
42
|
+
## Command Reference
|
|
61
43
|
|
|
62
|
-
|
|
44
|
+
### Deployment
|
|
45
|
+
| Command | Description |
|
|
46
|
+
| :--- | :--- |
|
|
47
|
+
| `launchpd init` | Link current folder to a subdomain (persisted in `.launchpd.json`) |
|
|
48
|
+
| `launchpd deploy <folder>` | Deploy a local folder (uses linked subdomain if available) |
|
|
49
|
+
| `launchpd deploy . -m "Fix layout"` | Deploy with a message (like a git commit) |
|
|
50
|
+
| `launchpd deploy . --name site` | Deploy with a custom subdomain explicitly |
|
|
51
|
+
| `launchpd deploy . --expires 2h` | Set auto-deletion (e.g., `30m`, `1d`, `7d`) |
|
|
63
52
|
|
|
64
|
-
###
|
|
53
|
+
### Management
|
|
54
|
+
| Command | Description |
|
|
55
|
+
| :--- | :--- |
|
|
56
|
+
| `launchpd status` | Show linked subdomain and latest deployment info |
|
|
57
|
+
| `launchpd list` | View your active deployments |
|
|
58
|
+
| `launchpd versions <subdomain>` | See version history with messages |
|
|
59
|
+
| `launchpd rollback <subdomain>` | Rollback to the previous version |
|
|
60
|
+
| `launchpd rollback <id> --to <v>` | Rollback to a specific version number |
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
### Identity & Auth
|
|
63
|
+
| Command | Description |
|
|
64
|
+
| :--- | :--- |
|
|
65
|
+
| `launchpd register` | Open the dashboard to create an account |
|
|
66
|
+
| `launchpd login` | Authenticate with your API key |
|
|
67
|
+
| `launchpd whoami` | Show current account status |
|
|
68
|
+
| `launchpd quota` | View storage and site limits |
|
|
69
|
+
| `launchpd logout` | Remove stored credentials |
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
---
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
launchpd login
|
|
74
|
-
```
|
|
73
|
+
## Why Register?
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
While anonymous deployments are great for testing, registered users get more power:
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
| Feature | Anonymous | Registered (Free) |
|
|
78
|
+
| :--- | :--- | :--- |
|
|
79
|
+
| **Max Sites** | 3 | 10+ |
|
|
80
|
+
| **Storage** | 50MB | 100MB+ |
|
|
81
|
+
| **Custom Names** | No | **Yes** |
|
|
82
|
+
| **Retention** | 7 Days | **Permanent** |
|
|
83
|
+
| **Versions** | 1 per site | 10 per site |
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
Run `launchpd register` to unlock these benefits!
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
launchpd logout
|
|
87
|
-
```
|
|
87
|
+
---
|
|
88
88
|
|
|
89
89
|
## Support
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
* **Bugs & Feedback**: [GitHub Issues](https://github.com/kents00/launchpd/issues)
|
|
92
|
+
* **Website**: [launchpd.cloud](https://launchpd.cloud)
|
|
93
|
+
* **Docs**: [launchpd.cloud/docs](https://launchpd.cloud/docs)
|
|
94
|
+
|
|
95
|
+
---
|
|
93
96
|
|
|
94
97
|
## License
|
|
95
98
|
|
|
96
|
-
MIT
|
|
99
|
+
[MIT](LICENSE) © [Kent Edoloverio](https://github.com/kents00)
|
package/bin/cli.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import { deploy } from '../src/commands/
|
|
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';
|
|
4
|
+
import { deploy, list, rollback, versions, init, status, login, logout, register, whoami, quota } from '../src/commands/index.js';
|
|
9
5
|
|
|
10
6
|
const program = new Command();
|
|
11
7
|
|
|
@@ -19,6 +15,7 @@ program
|
|
|
19
15
|
.description('Deploy a folder to a live URL')
|
|
20
16
|
.argument('<folder>', 'Path to the folder to deploy')
|
|
21
17
|
.option('--name <subdomain>', 'Use a custom subdomain (optional)')
|
|
18
|
+
.option('-m, --message <text>', 'Deployment message (optional)')
|
|
22
19
|
.option('--expires <time>', 'Auto-delete after time (e.g., 30m, 2h, 1d). Minimum: 30m')
|
|
23
20
|
.option('--verbose', 'Show detailed error information')
|
|
24
21
|
.action(async (folder, options) => {
|
|
@@ -55,6 +52,21 @@ program
|
|
|
55
52
|
await rollback(subdomain, options);
|
|
56
53
|
});
|
|
57
54
|
|
|
55
|
+
program
|
|
56
|
+
.command('init')
|
|
57
|
+
.description('Initialize a new project in the current directory')
|
|
58
|
+
.option('--name <subdomain>', 'Subdomain to link to')
|
|
59
|
+
.action(async (options) => {
|
|
60
|
+
await init(options);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
program
|
|
64
|
+
.command('status')
|
|
65
|
+
.description('Show current project status')
|
|
66
|
+
.action(async () => {
|
|
67
|
+
await status();
|
|
68
|
+
});
|
|
69
|
+
|
|
58
70
|
// Authentication commands
|
|
59
71
|
program
|
|
60
72
|
.command('login')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "launchpd",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Deploy static sites instantly to a live URL",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"static",
|
|
@@ -51,15 +51,16 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"chalk": "^5.4.0",
|
|
53
53
|
"commander": "^14.0.0",
|
|
54
|
+
"launchpd": "^1.0.0",
|
|
54
55
|
"mime-types": "^2.1.35",
|
|
55
56
|
"nanoid": "^5.1.0",
|
|
56
57
|
"ora": "^8.0.1"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@eslint/js": "^9.39.2",
|
|
60
|
-
"@vitest/coverage-v8": "^
|
|
61
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
61
62
|
"eslint": "^9.0.0",
|
|
62
|
-
"vitest": "^
|
|
63
|
+
"vitest": "^4.0.17"
|
|
63
64
|
},
|
|
64
65
|
"engines": {
|
|
65
66
|
"node": ">=20.0.0"
|
package/src/commands/deploy.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import { readdir } from 'node:fs/promises';
|
|
3
4
|
import { resolve, basename, join } from 'node:path';
|
|
4
5
|
import { generateSubdomain } from '../utils/id.js';
|
|
5
6
|
import { uploadFolder, finalizeUpload } from '../utils/upload.js';
|
|
6
7
|
import { getNextVersion } from '../utils/metadata.js';
|
|
7
8
|
import { saveLocalDeployment } from '../utils/localConfig.js';
|
|
8
|
-
import { getNextVersionFromAPI } from '../utils/api.js';
|
|
9
|
+
import { getNextVersionFromAPI, checkSubdomainAvailable, listSubdomains } from '../utils/api.js';
|
|
10
|
+
import { getProjectConfig, findProjectRoot } from '../utils/projectConfig.js';
|
|
9
11
|
import { success, errorWithSuggestions, info, warning, spinner, formatSize } from '../utils/logger.js';
|
|
10
12
|
import { calculateExpiresAt, formatTimeRemaining } from '../utils/expiration.js';
|
|
11
13
|
import { checkQuota, displayQuotaWarnings } from '../utils/quota.js';
|
|
12
14
|
import { getCredentials } from '../utils/credentials.js';
|
|
15
|
+
import { validateStaticOnly } from '../utils/validator.js';
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Calculate total size of a folder
|
|
@@ -87,6 +90,22 @@ export async function deploy(folder, options) {
|
|
|
87
90
|
}
|
|
88
91
|
scanSpinner.succeed(`Found ${fileCount} file(s)`);
|
|
89
92
|
|
|
93
|
+
// Static-Only Validation
|
|
94
|
+
const validationSpinner = spinner('Validating files...');
|
|
95
|
+
const validation = await validateStaticOnly(folderPath);
|
|
96
|
+
if (!validation.success) {
|
|
97
|
+
validationSpinner.fail('Deployment blocked: Non-static files detected');
|
|
98
|
+
errorWithSuggestions('Your project contains files that are not allowed.', [
|
|
99
|
+
'Launchpd only supports static files (HTML, CSS, JS, images, etc.)',
|
|
100
|
+
'Remove framework files, backend code, and build metadata:',
|
|
101
|
+
...validation.violations.map(v => ` - ${v}`).slice(0, 10),
|
|
102
|
+
validation.violations.length > 10 ? ` - ...and ${validation.violations.length - 10} more` : '',
|
|
103
|
+
'If you use a framework (React, Vue, etc.), deploy the "dist" or "build" folder instead.'
|
|
104
|
+
], { verbose });
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
validationSpinner.succeed('Project validated (Static files only)');
|
|
108
|
+
|
|
90
109
|
// Generate or use provided subdomain
|
|
91
110
|
// Anonymous users cannot use custom subdomains
|
|
92
111
|
const creds = await getCredentials();
|
|
@@ -97,14 +116,28 @@ export async function deploy(folder, options) {
|
|
|
97
116
|
console.log('');
|
|
98
117
|
}
|
|
99
118
|
|
|
100
|
-
|
|
119
|
+
// Detect project config if no name provided
|
|
120
|
+
let subdomain = (options.name && creds?.email) ? options.name.toLowerCase() : null;
|
|
121
|
+
|
|
122
|
+
if (!subdomain) {
|
|
123
|
+
const projectRoot = findProjectRoot(folderPath);
|
|
124
|
+
const config = await getProjectConfig(projectRoot);
|
|
125
|
+
if (config?.subdomain) {
|
|
126
|
+
subdomain = config.subdomain;
|
|
127
|
+
info(`Using project subdomain: ${chalk.bold(subdomain)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!subdomain) {
|
|
132
|
+
subdomain = generateSubdomain();
|
|
133
|
+
}
|
|
134
|
+
|
|
101
135
|
const url = `https://${subdomain}.launchpd.cloud`;
|
|
102
136
|
|
|
103
|
-
// Check if custom subdomain is taken
|
|
104
|
-
if (options.name
|
|
137
|
+
// Check if custom subdomain is taken (only if explicitly provided or new)
|
|
138
|
+
if (options.name || !subdomain) {
|
|
105
139
|
const checkSpinner = spinner('Checking subdomain availability...');
|
|
106
140
|
try {
|
|
107
|
-
const { checkSubdomainAvailable, listSubdomains } = await import('../utils/api.js');
|
|
108
141
|
const isAvailable = await checkSubdomainAvailable(subdomain);
|
|
109
142
|
|
|
110
143
|
if (!isAvailable) {
|
|
@@ -183,7 +216,8 @@ export async function deploy(folder, options) {
|
|
|
183
216
|
fileCount,
|
|
184
217
|
totalBytes,
|
|
185
218
|
folderName,
|
|
186
|
-
expiresAt?.toISOString() || null
|
|
219
|
+
expiresAt?.toISOString() || null,
|
|
220
|
+
options.message || null
|
|
187
221
|
);
|
|
188
222
|
finalizeSpinner.succeed('Deployment finalized');
|
|
189
223
|
|
package/src/commands/index.js
CHANGED
|
@@ -6,4 +6,6 @@ export { deploy } from './deploy.js';
|
|
|
6
6
|
export { list } from './list.js';
|
|
7
7
|
export { rollback } from './rollback.js';
|
|
8
8
|
export { versions } from './versions.js';
|
|
9
|
+
export { init } from './init.js';
|
|
10
|
+
export { status } from './status.js';
|
|
9
11
|
export { login, logout, register, whoami, quota } from './auth.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { initProjectConfig, findProjectRoot } from '../utils/projectConfig.js';
|
|
2
|
+
import { checkSubdomainAvailable, reserveSubdomain } from '../utils/api.js';
|
|
3
|
+
import { isLoggedIn } from '../utils/credentials.js';
|
|
4
|
+
import { success, errorWithSuggestions, info, spinner, warning } from '../utils/logger.js';
|
|
5
|
+
import readline from 'node:readline/promises';
|
|
6
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Initialize a new project in the current directory
|
|
10
|
+
* @param {object} options - Command options
|
|
11
|
+
* @param {string} options.name - Optional subdomain name
|
|
12
|
+
*/
|
|
13
|
+
export async function init(options) {
|
|
14
|
+
const projectRoot = findProjectRoot();
|
|
15
|
+
if (projectRoot) {
|
|
16
|
+
warning('This directory is already part of a Launchpd project.');
|
|
17
|
+
info(`Project root: ${projectRoot}`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!await isLoggedIn()) {
|
|
22
|
+
errorWithSuggestions('You must be logged in to initialize a project.', [
|
|
23
|
+
'Run "launchpd login" to log in',
|
|
24
|
+
'Run "launchpd register" to create an account'
|
|
25
|
+
]);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rl = readline.createInterface({ input, output });
|
|
30
|
+
let subdomain = options.name;
|
|
31
|
+
|
|
32
|
+
if (!subdomain) {
|
|
33
|
+
info('Linking this directory to a Launchpd subdomain...');
|
|
34
|
+
subdomain = await rl.question('Enter subdomain name (e.g. my-awesome-site): ');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!subdomain || !/^[a-z0-9-]+$/.test(subdomain)) {
|
|
38
|
+
rl.close();
|
|
39
|
+
errorWithSuggestions('Invalid subdomain. Use lowercase alphanumeric and hyphens only.', [
|
|
40
|
+
'Example: my-site-123',
|
|
41
|
+
'No spaces or special characters'
|
|
42
|
+
]);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const checkSpinner = spinner(`Checking if "${subdomain}" is available...`);
|
|
47
|
+
try {
|
|
48
|
+
const isAvailable = await checkSubdomainAvailable(subdomain);
|
|
49
|
+
if (!isAvailable) {
|
|
50
|
+
checkSpinner.fail(`Subdomain "${subdomain}" is already taken.`);
|
|
51
|
+
rl.close();
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
checkSpinner.succeed(`Subdomain "${subdomain}" is available!`);
|
|
55
|
+
|
|
56
|
+
const reserveStatus = await reserveSubdomain(subdomain);
|
|
57
|
+
if (reserveStatus) {
|
|
58
|
+
await initProjectConfig(subdomain);
|
|
59
|
+
success(`Project initialized! Linked to: ${subdomain}.launchpd.cloud`);
|
|
60
|
+
info('Now you can run "launchpd deploy" without specifying a name.');
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
checkSpinner.fail('Failed to initialize project');
|
|
64
|
+
errorWithSuggestions(err.message, [
|
|
65
|
+
'Check your internet connection',
|
|
66
|
+
'Try a different subdomain name'
|
|
67
|
+
]);
|
|
68
|
+
} finally {
|
|
69
|
+
rl.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/commands/rollback.js
CHANGED
|
@@ -28,6 +28,7 @@ export async function rollback(subdomainInput, options) {
|
|
|
28
28
|
version: v.version,
|
|
29
29
|
timestamp: v.created_at,
|
|
30
30
|
fileCount: v.file_count,
|
|
31
|
+
message: v.message,
|
|
31
32
|
}));
|
|
32
33
|
currentActive = apiResult.activeVersion || 1;
|
|
33
34
|
useAPI = true;
|
|
@@ -67,7 +68,8 @@ export async function rollback(subdomainInput, options) {
|
|
|
67
68
|
versions.forEach(v => {
|
|
68
69
|
const isActive = v.version === currentActive;
|
|
69
70
|
const marker = isActive ? chalk.green(' (active)') : '';
|
|
70
|
-
|
|
71
|
+
const message = v.message ? ` - "${v.message}"` : '';
|
|
72
|
+
console.log(` ${chalk.cyan(`v${v.version}`)}${message} - ${chalk.gray(v.timestamp)}${marker}`);
|
|
71
73
|
});
|
|
72
74
|
process.exit(1);
|
|
73
75
|
}
|
|
@@ -106,6 +108,9 @@ export async function rollback(subdomainInput, options) {
|
|
|
106
108
|
|
|
107
109
|
rollbackSpinner.succeed(`Rolled back to ${chalk.cyan(`v${targetVersion}`)}`);
|
|
108
110
|
console.log(`\n 🔄 https://${subdomain}.launchpd.cloud\n`);
|
|
111
|
+
if (targetDeployment?.message) {
|
|
112
|
+
info(`Version message: "${chalk.italic(targetDeployment.message)}"`);
|
|
113
|
+
}
|
|
109
114
|
info(`Restored deployment from: ${chalk.gray(targetDeployment?.timestamp || 'unknown')}`);
|
|
110
115
|
|
|
111
116
|
} catch (err) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getProjectConfig, findProjectRoot } from '../utils/projectConfig.js';
|
|
2
|
+
import { getDeployment } from '../utils/api.js';
|
|
3
|
+
import { errorWithSuggestions, info, spinner, warning, formatSize } from '../utils/logger.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Show current project status
|
|
8
|
+
*/
|
|
9
|
+
export async function status(_options) {
|
|
10
|
+
const projectRoot = findProjectRoot();
|
|
11
|
+
if (!projectRoot) {
|
|
12
|
+
warning('Not a Launchpd project (no .launchpd.json found)');
|
|
13
|
+
info('Run "launchpd init" to link this directory to a subdomain.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const config = await getProjectConfig(projectRoot);
|
|
18
|
+
if (!config || !config.subdomain) {
|
|
19
|
+
errorWithSuggestions('Invalid project configuration.', [
|
|
20
|
+
'Try deleting .launchpd.json and running "launchpd init" again'
|
|
21
|
+
]);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
info(`Project root: ${chalk.cyan(projectRoot)}`);
|
|
26
|
+
info(`Linked subdomain: ${chalk.bold.green(config.subdomain)}.launchpd.cloud`);
|
|
27
|
+
|
|
28
|
+
const statusSpinner = spinner('Fetching latest deployment info...');
|
|
29
|
+
try {
|
|
30
|
+
const deploymentData = await getDeployment(config.subdomain);
|
|
31
|
+
statusSpinner.stop();
|
|
32
|
+
|
|
33
|
+
if (deploymentData && deploymentData.versions && deploymentData.versions.length > 0) {
|
|
34
|
+
const active = deploymentData.versions.find(v => v.version === deploymentData.activeVersion) || deploymentData.versions[0];
|
|
35
|
+
|
|
36
|
+
console.log('\nDeployment Status:');
|
|
37
|
+
console.log(` Active Version: ${chalk.cyan(`v${active.version}`)}`);
|
|
38
|
+
console.log(` Deployed At: ${new Date(active.created_at || active.timestamp).toLocaleString()}`);
|
|
39
|
+
if (active.message) {
|
|
40
|
+
console.log(` Message: ${chalk.italic(active.message)}`);
|
|
41
|
+
}
|
|
42
|
+
console.log(` File Count: ${active.file_count || active.fileCount}`);
|
|
43
|
+
console.log(` Total Size: ${formatSize(active.total_bytes || active.totalBytes)}`);
|
|
44
|
+
console.log(` URL: ${chalk.underline.blue(`https://${config.subdomain}.launchpd.cloud`)}`);
|
|
45
|
+
console.log('');
|
|
46
|
+
} else {
|
|
47
|
+
warning('\nNo deployments found for this project yet.');
|
|
48
|
+
info('Run "launchpd deploy <folder>" to push your first version.');
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
statusSpinner.fail('Failed to fetch deployment status');
|
|
52
|
+
info(`Subdomain: ${config.subdomain}`);
|
|
53
|
+
// Don't exit, just show what we have
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/commands/versions.js
CHANGED
|
@@ -34,9 +34,10 @@ export async function versions(subdomainInput, options) {
|
|
|
34
34
|
if (apiResult && apiResult.versions) {
|
|
35
35
|
versionList = apiResult.versions.map(v => ({
|
|
36
36
|
version: v.version,
|
|
37
|
-
timestamp: v.created_at,
|
|
38
|
-
fileCount: v.file_count,
|
|
39
|
-
totalBytes: v.total_bytes,
|
|
37
|
+
timestamp: v.created_at || v.timestamp,
|
|
38
|
+
fileCount: v.file_count || v.fileCount,
|
|
39
|
+
totalBytes: v.total_bytes || v.totalBytes,
|
|
40
|
+
message: v.message || '',
|
|
40
41
|
}));
|
|
41
42
|
activeVersion = apiResult.activeVersion || 1;
|
|
42
43
|
} else {
|
|
@@ -67,6 +68,7 @@ export async function versions(subdomainInput, options) {
|
|
|
67
68
|
fileCount: v.fileCount,
|
|
68
69
|
totalBytes: v.totalBytes,
|
|
69
70
|
isActive: v.version === activeVersion,
|
|
71
|
+
message: v.message,
|
|
70
72
|
})),
|
|
71
73
|
}, null, 2));
|
|
72
74
|
return;
|
|
@@ -77,8 +79,8 @@ export async function versions(subdomainInput, options) {
|
|
|
77
79
|
console.log('');
|
|
78
80
|
|
|
79
81
|
// Table header
|
|
80
|
-
console.log(chalk.gray(' Version Date Files Size Status'));
|
|
81
|
-
console.log(chalk.gray(' ' + '─'.repeat(
|
|
82
|
+
console.log(chalk.gray(' Version Date Files Size Status Message'));
|
|
83
|
+
console.log(chalk.gray(' ' + '─'.repeat(100)));
|
|
82
84
|
|
|
83
85
|
for (const v of versionList) {
|
|
84
86
|
const isActive = v.version === activeVersion;
|
|
@@ -95,13 +97,15 @@ export async function versions(subdomainInput, options) {
|
|
|
95
97
|
const filesStr = chalk.white(filesRaw.padEnd(10));
|
|
96
98
|
const sizeStr = chalk.white(sizeRaw.padEnd(12));
|
|
97
99
|
const statusStr = isActive
|
|
98
|
-
? chalk.green.bold('● active')
|
|
99
|
-
: chalk.gray('○ inactive');
|
|
100
|
+
? chalk.green.bold('● active'.padEnd(12))
|
|
101
|
+
: chalk.gray('○ inactive'.padEnd(12));
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
const messageStr = chalk.italic.gray(v.message || '');
|
|
104
|
+
|
|
105
|
+
console.log(` ${versionStr}${dateStr}${filesStr}${sizeStr}${statusStr}${messageStr}`);
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
console.log(chalk.gray(' ' + '─'.repeat(
|
|
108
|
+
console.log(chalk.gray(' ' + '─'.repeat(100)));
|
|
105
109
|
console.log('');
|
|
106
110
|
info(`Use ${chalk.cyan(`launchpd rollback ${subdomain} --to <n>`)} to restore a version.`);
|
|
107
111
|
console.log('');
|
package/src/utils/api.js
CHANGED
|
@@ -82,7 +82,7 @@ export async function getNextVersionFromAPI(subdomain) {
|
|
|
82
82
|
* Record a new deployment in the API
|
|
83
83
|
*/
|
|
84
84
|
export async function recordDeployment(deploymentData) {
|
|
85
|
-
const { subdomain, folderName, fileCount, totalBytes, version, expiresAt } = deploymentData;
|
|
85
|
+
const { subdomain, folderName, fileCount, totalBytes, version, expiresAt, message } = deploymentData;
|
|
86
86
|
|
|
87
87
|
return apiRequest('/api/deployments', {
|
|
88
88
|
method: 'POST',
|
|
@@ -94,6 +94,7 @@ export async function recordDeployment(deploymentData) {
|
|
|
94
94
|
version,
|
|
95
95
|
cliVersion: '0.1.0',
|
|
96
96
|
expiresAt,
|
|
97
|
+
message,
|
|
97
98
|
}),
|
|
98
99
|
});
|
|
99
100
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const PROJECT_CONFIG_FILE = '.launchpd.json';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find project root by looking for .launchpd.json upwards
|
|
9
|
+
*/
|
|
10
|
+
export function findProjectRoot(startDir = process.cwd()) {
|
|
11
|
+
let current = resolve(startDir);
|
|
12
|
+
while (true) {
|
|
13
|
+
if (existsSync(join(current, PROJECT_CONFIG_FILE))) {
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
const parent = resolve(current, '..');
|
|
17
|
+
if (parent === current) return null;
|
|
18
|
+
current = parent;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get project configuration
|
|
24
|
+
*/
|
|
25
|
+
export async function getProjectConfig(projectDir = findProjectRoot()) {
|
|
26
|
+
if (!projectDir) return null;
|
|
27
|
+
|
|
28
|
+
const configPath = join(projectDir, PROJECT_CONFIG_FILE);
|
|
29
|
+
try {
|
|
30
|
+
if (existsSync(configPath)) {
|
|
31
|
+
const content = await readFile(configPath, 'utf8');
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Silently fail or handle corrupted config
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Save project configuration
|
|
42
|
+
*/
|
|
43
|
+
export async function saveProjectConfig(config, projectDir = process.cwd()) {
|
|
44
|
+
const configPath = join(projectDir, PROJECT_CONFIG_FILE);
|
|
45
|
+
await writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
46
|
+
return configPath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize a new project config
|
|
51
|
+
*/
|
|
52
|
+
export async function initProjectConfig(subdomain, projectDir = process.cwd()) {
|
|
53
|
+
const config = {
|
|
54
|
+
subdomain,
|
|
55
|
+
createdAt: new Date().toISOString(),
|
|
56
|
+
updatedAt: new Date().toISOString()
|
|
57
|
+
};
|
|
58
|
+
return await saveProjectConfig(config, projectDir);
|
|
59
|
+
}
|
package/src/utils/upload.js
CHANGED
|
@@ -72,7 +72,7 @@ async function uploadFile(content, subdomain, version, filePath, contentType) {
|
|
|
72
72
|
* @param {string} folderName - Original folder name
|
|
73
73
|
* @param {string|null} expiresAt - ISO expiration timestamp
|
|
74
74
|
*/
|
|
75
|
-
async function completeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt) {
|
|
75
|
+
async function completeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt, message) {
|
|
76
76
|
const apiKey = await getApiKey();
|
|
77
77
|
const apiSecret = await getApiSecret();
|
|
78
78
|
const headers = {
|
|
@@ -87,6 +87,7 @@ async function completeUpload(subdomain, version, fileCount, totalBytes, folderN
|
|
|
87
87
|
totalBytes,
|
|
88
88
|
folderName,
|
|
89
89
|
expiresAt,
|
|
90
|
+
message,
|
|
90
91
|
});
|
|
91
92
|
|
|
92
93
|
if (apiSecret) {
|
|
@@ -176,6 +177,6 @@ export async function uploadFolder(localPath, subdomain, version = 1, onProgress
|
|
|
176
177
|
* @param {string} folderName - Folder name
|
|
177
178
|
* @param {string|null} expiresAt - Expiration ISO timestamp
|
|
178
179
|
*/
|
|
179
|
-
export async function finalizeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt = null) {
|
|
180
|
-
return completeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt);
|
|
180
|
+
export async function finalizeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt = null, message = null) {
|
|
181
|
+
return completeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt, message);
|
|
181
182
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { extname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Allowed static file extensions
|
|
5
|
+
const ALLOWED_EXTENSIONS = new Set([
|
|
6
|
+
'.html', '.htm',
|
|
7
|
+
'.css', '.scss', '.sass',
|
|
8
|
+
'.js', '.mjs', '.cjs',
|
|
9
|
+
'.json', '.jsonld',
|
|
10
|
+
'.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.avif',
|
|
11
|
+
'.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
12
|
+
'.mp4', '.webm', '.ogg', '.mp3', '.wav', '.flac',
|
|
13
|
+
'.pdf', '.txt', '.md', '.xml', '.yaml', '.yml'
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
// Forbidden indicators (frameworks, build tools, backend code)
|
|
17
|
+
const FORBIDDEN_INDICATORS = [
|
|
18
|
+
// Build systems & Frameworks
|
|
19
|
+
'package.json',
|
|
20
|
+
'package-lock.json',
|
|
21
|
+
'yarn.lock',
|
|
22
|
+
'pnpm-lock.yaml',
|
|
23
|
+
'node_modules',
|
|
24
|
+
'composer.json',
|
|
25
|
+
'vendor',
|
|
26
|
+
'requirements.txt',
|
|
27
|
+
'Gemfile',
|
|
28
|
+
'Makefile',
|
|
29
|
+
'tsconfig.json',
|
|
30
|
+
'next.config.js',
|
|
31
|
+
'nuxt.config.js',
|
|
32
|
+
'svelte.config.js',
|
|
33
|
+
'vite.config.js',
|
|
34
|
+
'webpack.config.js',
|
|
35
|
+
'rollup.config.js',
|
|
36
|
+
'angular.json',
|
|
37
|
+
|
|
38
|
+
// Backend/Source
|
|
39
|
+
'.jsx', '.tsx', '.ts', '.vue', '.svelte', '.php', '.py', '.rb', '.go', '.rs', '.java', '.cs', '.cpp', '.c',
|
|
40
|
+
'.env', '.env.local', '.env.production',
|
|
41
|
+
'.dockerfile', 'docker-compose.yml',
|
|
42
|
+
|
|
43
|
+
// Hidden/System
|
|
44
|
+
'.git', '.svn', '.hg'
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validates that a folder contains ONLY static files.
|
|
49
|
+
* @param {string} folderPath
|
|
50
|
+
* @returns {Promise<{success: boolean, violations: string[]}>}
|
|
51
|
+
*/
|
|
52
|
+
export async function validateStaticOnly(folderPath) {
|
|
53
|
+
const violations = [];
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const files = await readdir(folderPath, { recursive: true, withFileTypes: true });
|
|
57
|
+
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const fileName = file.name.toLowerCase();
|
|
60
|
+
const ext = extname(fileName);
|
|
61
|
+
|
|
62
|
+
// 1. Check if the file/dir itself is a forbidden indicator
|
|
63
|
+
if (FORBIDDEN_INDICATORS.includes(fileName) || FORBIDDEN_INDICATORS.includes(ext)) {
|
|
64
|
+
violations.push(file.name);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Check extension for non-allowed types (only for files)
|
|
69
|
+
if (file.isFile()) {
|
|
70
|
+
// Ignore files without extensions or if they start with a dot (but handle indicators above)
|
|
71
|
+
if (ext && !ALLOWED_EXTENSIONS.has(ext)) {
|
|
72
|
+
violations.push(file.name);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
success: violations.length === 0,
|
|
79
|
+
violations: [...new Set(violations)] // Deduplicate
|
|
80
|
+
};
|
|
81
|
+
} catch (err) {
|
|
82
|
+
throw new Error(`Failed to validate folder: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|