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.
Potentially problematic release.
This version of launchpd might be problematic. Click here for more details.
- 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/src/commands/deploy.js
CHANGED
|
@@ -2,11 +2,11 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { readdir } from 'node:fs/promises';
|
|
3
3
|
import { resolve, basename, join } from 'node:path';
|
|
4
4
|
import { generateSubdomain } from '../utils/id.js';
|
|
5
|
-
import { uploadFolder } from '../utils/upload.js';
|
|
6
|
-
import {
|
|
5
|
+
import { uploadFolder, finalizeUpload } from '../utils/upload.js';
|
|
6
|
+
import { getNextVersion } from '../utils/metadata.js';
|
|
7
7
|
import { saveLocalDeployment } from '../utils/localConfig.js';
|
|
8
|
-
import {
|
|
9
|
-
import { success,
|
|
8
|
+
import { getNextVersionFromAPI } from '../utils/api.js';
|
|
9
|
+
import { success, errorWithSuggestions, info, warning, spinner, formatSize } from '../utils/logger.js';
|
|
10
10
|
import { calculateExpiresAt, formatTimeRemaining } from '../utils/expiration.js';
|
|
11
11
|
import { checkQuota, displayQuotaWarnings } from '../utils/quota.js';
|
|
12
12
|
import { getCredentials } from '../utils/credentials.js';
|
|
@@ -42,9 +42,11 @@ async function calculateFolderSize(folderPath) {
|
|
|
42
42
|
* @param {boolean} options.dryRun - Skip actual upload
|
|
43
43
|
* @param {string} options.name - Custom subdomain
|
|
44
44
|
* @param {string} options.expires - Expiration time (e.g., "30m", "2h", "1d")
|
|
45
|
+
* @param {boolean} options.verbose - Show verbose error details
|
|
45
46
|
*/
|
|
46
47
|
export async function deploy(folder, options) {
|
|
47
48
|
const folderPath = resolve(folder);
|
|
49
|
+
const verbose = options.verbose || false;
|
|
48
50
|
|
|
49
51
|
// Parse expiration if provided
|
|
50
52
|
let expiresAt = null;
|
|
@@ -52,41 +54,58 @@ export async function deploy(folder, options) {
|
|
|
52
54
|
try {
|
|
53
55
|
expiresAt = calculateExpiresAt(options.expires);
|
|
54
56
|
} catch (err) {
|
|
55
|
-
|
|
57
|
+
errorWithSuggestions(err.message, [
|
|
58
|
+
'Use format like: 30m, 2h, 1d, 7d',
|
|
59
|
+
'Minimum expiration is 30 minutes',
|
|
60
|
+
'Examples: --expires 1h, --expires 2d',
|
|
61
|
+
], { verbose, cause: err });
|
|
56
62
|
process.exit(1);
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
// Validate folder exists
|
|
61
67
|
if (!existsSync(folderPath)) {
|
|
62
|
-
|
|
68
|
+
errorWithSuggestions(`Folder not found: ${folderPath}`, [
|
|
69
|
+
'Check the path is correct',
|
|
70
|
+
'Use an absolute path or path relative to current directory',
|
|
71
|
+
`Current directory: ${process.cwd()}`,
|
|
72
|
+
], { verbose });
|
|
63
73
|
process.exit(1);
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
// Check folder is not empty
|
|
77
|
+
const scanSpinner = spinner('Scanning folder...');
|
|
67
78
|
const files = await readdir(folderPath, { recursive: true, withFileTypes: true });
|
|
68
79
|
const fileCount = files.filter(f => f.isFile()).length;
|
|
69
80
|
|
|
70
81
|
if (fileCount === 0) {
|
|
71
|
-
|
|
82
|
+
scanSpinner.fail('Folder is empty');
|
|
83
|
+
errorWithSuggestions('Nothing to deploy.', [
|
|
84
|
+
'Add some files to your folder',
|
|
85
|
+
'Make sure index.html exists for static sites',
|
|
86
|
+
], { verbose });
|
|
72
87
|
process.exit(1);
|
|
73
88
|
}
|
|
89
|
+
scanSpinner.succeed(`Found ${fileCount} file(s)`);
|
|
74
90
|
|
|
75
91
|
// Generate or use provided subdomain
|
|
76
92
|
const subdomain = options.name || generateSubdomain();
|
|
77
93
|
const url = `https://${subdomain}.launchpd.cloud`;
|
|
78
94
|
|
|
79
95
|
// Calculate estimated upload size
|
|
96
|
+
const sizeSpinner = spinner('Calculating folder size...');
|
|
80
97
|
const estimatedBytes = await calculateFolderSize(folderPath);
|
|
98
|
+
sizeSpinner.succeed(`Size: ${formatSize(estimatedBytes)}`);
|
|
81
99
|
|
|
82
100
|
// Check quota before deploying
|
|
83
|
-
|
|
101
|
+
const quotaSpinner = spinner('Checking quota...');
|
|
84
102
|
const quotaCheck = await checkQuota(subdomain, estimatedBytes);
|
|
85
103
|
|
|
86
104
|
if (!quotaCheck.allowed) {
|
|
87
|
-
|
|
105
|
+
quotaSpinner.fail('Deployment blocked due to quota limits');
|
|
88
106
|
process.exit(1);
|
|
89
107
|
}
|
|
108
|
+
quotaSpinner.succeed('Quota check passed');
|
|
90
109
|
|
|
91
110
|
// Display any warnings
|
|
92
111
|
displayQuotaWarnings(quotaCheck.warnings);
|
|
@@ -101,7 +120,6 @@ export async function deploy(folder, options) {
|
|
|
101
120
|
|
|
102
121
|
info(`Deploying ${fileCount} file(s) from ${folderPath}`);
|
|
103
122
|
info(`Target: ${url}`);
|
|
104
|
-
info(`Size: ${(estimatedBytes / 1024 / 1024).toFixed(2)}MB`);
|
|
105
123
|
|
|
106
124
|
if (options.dryRun) {
|
|
107
125
|
warning('Dry run mode - skipping upload');
|
|
@@ -127,7 +145,7 @@ export async function deploy(folder, options) {
|
|
|
127
145
|
? (quotaCheck.quota.usage?.siteCount || 0) + 1
|
|
128
146
|
: quotaCheck.quota.usage?.siteCount || 0;
|
|
129
147
|
console.log(` Sites: ${sitesAfter}/${quotaCheck.quota.limits.maxSites}`);
|
|
130
|
-
console.log(` Storage: ${(storageAfter
|
|
148
|
+
console.log(` Storage: ${formatSize(storageAfter)}/${quotaCheck.quota.limits.maxStorageMB}MB`);
|
|
131
149
|
console.log('');
|
|
132
150
|
}
|
|
133
151
|
return;
|
|
@@ -135,42 +153,37 @@ export async function deploy(folder, options) {
|
|
|
135
153
|
|
|
136
154
|
// Perform actual upload
|
|
137
155
|
try {
|
|
138
|
-
// Get next version number for this subdomain (try API first, fallback to
|
|
156
|
+
// Get next version number for this subdomain (try API first, fallback to local)
|
|
157
|
+
const versionSpinner = spinner('Fetching version info...');
|
|
139
158
|
let version = await getNextVersionFromAPI(subdomain);
|
|
140
159
|
if (version === null) {
|
|
141
160
|
version = await getNextVersion(subdomain);
|
|
142
161
|
}
|
|
143
|
-
|
|
162
|
+
versionSpinner.succeed(`Deploying as version ${version}`);
|
|
144
163
|
|
|
145
|
-
|
|
164
|
+
// Upload all files via API proxy
|
|
165
|
+
const folderName = basename(folderPath);
|
|
166
|
+
const uploadSpinner = spinner(`Uploading files... 0/${fileCount}`);
|
|
146
167
|
|
|
147
|
-
|
|
148
|
-
|
|
168
|
+
const { totalBytes } = await uploadFolder(folderPath, subdomain, version, (uploaded, total, fileName) => {
|
|
169
|
+
uploadSpinner.update(`Uploading files... ${uploaded}/${total} (${fileName})`);
|
|
170
|
+
});
|
|
149
171
|
|
|
150
|
-
|
|
151
|
-
info('Recording deployment metadata...');
|
|
172
|
+
uploadSpinner.succeed(`Uploaded ${fileCount} files (${formatSize(totalBytes)})`);
|
|
152
173
|
|
|
153
|
-
//
|
|
154
|
-
const
|
|
155
|
-
|
|
174
|
+
// Finalize upload: set active version and record metadata
|
|
175
|
+
const finalizeSpinner = spinner('Finalizing deployment...');
|
|
176
|
+
await finalizeUpload(
|
|
156
177
|
subdomain,
|
|
157
|
-
|
|
178
|
+
version,
|
|
158
179
|
fileCount,
|
|
159
180
|
totalBytes,
|
|
160
|
-
|
|
161
|
-
expiresAt
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (apiResult) {
|
|
165
|
-
// API succeeded - deployment is centrally tracked
|
|
166
|
-
info('Deployment recorded to central API');
|
|
167
|
-
} else {
|
|
168
|
-
// API unavailable - fallback to R2 metadata
|
|
169
|
-
warning('Central API unavailable, using local fallback');
|
|
170
|
-
await recordMetadata(subdomain, folderPath, fileCount, totalBytes, version, expiresAt);
|
|
171
|
-
}
|
|
181
|
+
folderName,
|
|
182
|
+
expiresAt?.toISOString() || null
|
|
183
|
+
);
|
|
184
|
+
finalizeSpinner.succeed('Deployment finalized');
|
|
172
185
|
|
|
173
|
-
//
|
|
186
|
+
// Save locally for quick access
|
|
174
187
|
await saveLocalDeployment({
|
|
175
188
|
subdomain,
|
|
176
189
|
folderName,
|
|
@@ -182,13 +195,33 @@ export async function deploy(folder, options) {
|
|
|
182
195
|
});
|
|
183
196
|
|
|
184
197
|
success(`Deployed successfully! (v${version})`);
|
|
185
|
-
console.log(`\n
|
|
198
|
+
console.log(`\n${url}`);
|
|
186
199
|
if (expiresAt) {
|
|
187
|
-
warning(`
|
|
200
|
+
warning(`Expires: ${formatTimeRemaining(expiresAt)}`);
|
|
188
201
|
}
|
|
189
202
|
console.log('');
|
|
190
203
|
} catch (err) {
|
|
191
|
-
|
|
204
|
+
const suggestions = [];
|
|
205
|
+
|
|
206
|
+
// Provide context-specific suggestions
|
|
207
|
+
if (err.message.includes('fetch failed') || err.message.includes('ENOTFOUND')) {
|
|
208
|
+
suggestions.push('Check your internet connection');
|
|
209
|
+
suggestions.push('The API server may be temporarily unavailable');
|
|
210
|
+
} else if (err.message.includes('401') || err.message.includes('Unauthorized')) {
|
|
211
|
+
suggestions.push('Run "launchpd login" to authenticate');
|
|
212
|
+
suggestions.push('Your API key may have expired');
|
|
213
|
+
} else if (err.message.includes('413') || err.message.includes('too large')) {
|
|
214
|
+
suggestions.push('Try deploying fewer or smaller files');
|
|
215
|
+
suggestions.push('Check your storage quota with "launchpd quota"');
|
|
216
|
+
} else if (err.message.includes('429') || err.message.includes('rate limit')) {
|
|
217
|
+
suggestions.push('Wait a few minutes and try again');
|
|
218
|
+
suggestions.push('You may be deploying too frequently');
|
|
219
|
+
} else {
|
|
220
|
+
suggestions.push('Try running with --verbose for more details');
|
|
221
|
+
suggestions.push('Check https://status.launchpd.cloud for service status');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
errorWithSuggestions(`Upload failed: ${err.message}`, suggestions, { verbose, cause: err });
|
|
192
225
|
process.exit(1);
|
|
193
226
|
}
|
|
194
227
|
}
|
package/src/commands/list.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getLocalDeployments } from '../utils/localConfig.js';
|
|
2
2
|
import { listDeployments as listFromAPI } from '../utils/api.js';
|
|
3
|
-
import {
|
|
3
|
+
import { errorWithSuggestions, info, spinner, formatSize } from '../utils/logger.js';
|
|
4
4
|
import { formatTimeRemaining, isExpired } from '../utils/expiration.js';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
|
|
@@ -9,12 +9,17 @@ import chalk from 'chalk';
|
|
|
9
9
|
* @param {object} options - Command options
|
|
10
10
|
* @param {boolean} options.json - Output as JSON
|
|
11
11
|
* @param {boolean} options.local - Only show local deployments
|
|
12
|
+
* @param {boolean} options.verbose - Show verbose error details
|
|
12
13
|
*/
|
|
13
14
|
export async function list(options) {
|
|
15
|
+
const verbose = options.verbose || false;
|
|
16
|
+
|
|
14
17
|
try {
|
|
15
18
|
let deployments = [];
|
|
16
19
|
let source = 'local';
|
|
17
20
|
|
|
21
|
+
const fetchSpinner = spinner('Fetching deployments...');
|
|
22
|
+
|
|
18
23
|
// Try API first unless --local flag is set
|
|
19
24
|
if (!options.local) {
|
|
20
25
|
const apiResult = await listFromAPI();
|
|
@@ -40,11 +45,13 @@ export async function list(options) {
|
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
if (deployments.length === 0) {
|
|
43
|
-
|
|
44
|
-
info('Deploy a folder with: launchpd deploy ./my-folder');
|
|
48
|
+
fetchSpinner.warn('No deployments found');
|
|
49
|
+
info('Deploy a folder with: ' + chalk.cyan('launchpd deploy ./my-folder'));
|
|
45
50
|
return;
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
fetchSpinner.succeed(`Found ${deployments.length} deployment(s)`);
|
|
54
|
+
|
|
48
55
|
if (options.json) {
|
|
49
56
|
console.log(JSON.stringify(deployments, null, 2));
|
|
50
57
|
return;
|
|
@@ -53,7 +60,7 @@ export async function list(options) {
|
|
|
53
60
|
// Display as table
|
|
54
61
|
console.log('');
|
|
55
62
|
console.log(chalk.bold('Your Deployments:'));
|
|
56
|
-
console.log(chalk.gray('─'.repeat(
|
|
63
|
+
console.log(chalk.gray('─'.repeat(100)));
|
|
57
64
|
|
|
58
65
|
// Header
|
|
59
66
|
console.log(
|
|
@@ -61,43 +68,58 @@ export async function list(options) {
|
|
|
61
68
|
padRight('URL', 40) +
|
|
62
69
|
padRight('Folder', 15) +
|
|
63
70
|
padRight('Files', 7) +
|
|
71
|
+
padRight('Size', 12) +
|
|
64
72
|
padRight('Date', 12) +
|
|
65
73
|
'Status'
|
|
66
74
|
)
|
|
67
75
|
);
|
|
68
|
-
console.log(chalk.gray('─'.repeat(
|
|
76
|
+
console.log(chalk.gray('─'.repeat(100)));
|
|
69
77
|
|
|
70
78
|
// Rows (most recent first)
|
|
71
79
|
const sorted = [...deployments].reverse();
|
|
72
80
|
for (const dep of sorted) {
|
|
73
81
|
const url = `https://${dep.subdomain}.launchpd.cloud`;
|
|
74
82
|
const date = new Date(dep.timestamp).toLocaleDateString();
|
|
83
|
+
const size = dep.totalBytes ? formatSize(dep.totalBytes) : '-';
|
|
75
84
|
|
|
76
|
-
// Determine status
|
|
77
|
-
let status
|
|
85
|
+
// Determine status with colors
|
|
86
|
+
let status;
|
|
78
87
|
if (dep.expiresAt) {
|
|
79
88
|
if (isExpired(dep.expiresAt)) {
|
|
80
|
-
status = chalk.red('expired');
|
|
89
|
+
status = chalk.red.bold('● expired');
|
|
81
90
|
} else {
|
|
82
|
-
status = chalk.yellow(formatTimeRemaining(dep.expiresAt));
|
|
91
|
+
status = chalk.yellow(`⏱ ${formatTimeRemaining(dep.expiresAt)}`);
|
|
83
92
|
}
|
|
93
|
+
} else {
|
|
94
|
+
status = chalk.green.bold('● active');
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
// Version badge
|
|
98
|
+
const versionBadge = chalk.magenta(`v${dep.version || 1}`);
|
|
99
|
+
|
|
86
100
|
console.log(
|
|
87
101
|
chalk.cyan(padRight(url, 40)) +
|
|
88
|
-
padRight(dep.folderName || '-', 15) +
|
|
89
|
-
padRight(String(dep.fileCount), 7) +
|
|
102
|
+
chalk.white(padRight(dep.folderName || '-', 15)) +
|
|
103
|
+
chalk.white(padRight(String(dep.fileCount), 7)) +
|
|
104
|
+
chalk.white(padRight(size, 12)) +
|
|
90
105
|
chalk.gray(padRight(date, 12)) +
|
|
91
|
-
status
|
|
106
|
+
status + ' ' + versionBadge
|
|
92
107
|
);
|
|
93
108
|
}
|
|
94
109
|
|
|
95
|
-
console.log(chalk.gray('─'.repeat(
|
|
96
|
-
|
|
110
|
+
console.log(chalk.gray('─'.repeat(100)));
|
|
111
|
+
const syncStatus = source === 'api'
|
|
112
|
+
? chalk.green(' ✓ synced')
|
|
113
|
+
: chalk.yellow(' ⚠ local only');
|
|
114
|
+
console.log(chalk.gray(`Total: ${deployments.length} deployment(s)`) + syncStatus);
|
|
97
115
|
console.log('');
|
|
98
116
|
|
|
99
117
|
} catch (err) {
|
|
100
|
-
|
|
118
|
+
errorWithSuggestions(`Failed to list deployments: ${err.message}`, [
|
|
119
|
+
'Check your internet connection',
|
|
120
|
+
'Use --local flag to show local deployments only',
|
|
121
|
+
'Try running with --verbose for more details',
|
|
122
|
+
], { verbose, cause: err });
|
|
101
123
|
process.exit(1);
|
|
102
124
|
}
|
|
103
125
|
}
|
package/src/commands/rollback.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { getVersionsForSubdomain, setActiveVersion, getActiveVersion } from '../utils/metadata.js';
|
|
2
2
|
import { getVersions as getVersionsFromAPI, rollbackVersion as rollbackViaAPI } from '../utils/api.js';
|
|
3
|
-
import {
|
|
3
|
+
import { error, errorWithSuggestions, info, warning, spinner } from '../utils/logger.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Rollback a subdomain to a previous version
|
|
7
8
|
* @param {string} subdomain - Subdomain to rollback
|
|
8
9
|
* @param {object} options - Command options
|
|
9
10
|
* @param {number} options.to - Specific version to rollback to (optional)
|
|
11
|
+
* @param {boolean} options.verbose - Show verbose error details
|
|
10
12
|
*/
|
|
11
13
|
export async function rollback(subdomain, options) {
|
|
14
|
+
const verbose = options.verbose || false;
|
|
15
|
+
|
|
12
16
|
try {
|
|
13
|
-
|
|
17
|
+
const fetchSpinner = spinner(`Checking versions for ${subdomain}...`);
|
|
14
18
|
|
|
15
19
|
// Get all versions for this subdomain (try API first)
|
|
16
20
|
let versions = [];
|
|
@@ -33,16 +37,22 @@ export async function rollback(subdomain, options) {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
if (versions.length === 0) {
|
|
36
|
-
|
|
40
|
+
fetchSpinner.fail('No deployments found');
|
|
41
|
+
errorWithSuggestions(`No deployments found for subdomain: ${subdomain}`, [
|
|
42
|
+
'Check the subdomain name is correct',
|
|
43
|
+
'Run "launchpd list" to see your deployments',
|
|
44
|
+
], { verbose });
|
|
37
45
|
process.exit(1);
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
if (versions.length === 1) {
|
|
41
|
-
|
|
49
|
+
fetchSpinner.warn('Only one version exists');
|
|
50
|
+
warning('Nothing to rollback to.');
|
|
42
51
|
process.exit(1);
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
fetchSpinner.succeed(`Found ${versions.length} versions`);
|
|
55
|
+
info(`Current active version: ${chalk.cyan(`v${currentActive}`)}`);
|
|
46
56
|
|
|
47
57
|
// Determine target version
|
|
48
58
|
let targetVersion;
|
|
@@ -51,9 +61,12 @@ export async function rollback(subdomain, options) {
|
|
|
51
61
|
const versionExists = versions.some(v => v.version === targetVersion);
|
|
52
62
|
if (!versionExists) {
|
|
53
63
|
error(`Version ${targetVersion} does not exist.`);
|
|
64
|
+
console.log('');
|
|
54
65
|
info('Available versions:');
|
|
55
66
|
versions.forEach(v => {
|
|
56
|
-
|
|
67
|
+
const isActive = v.version === currentActive;
|
|
68
|
+
const marker = isActive ? chalk.green(' (active)') : '';
|
|
69
|
+
console.log(` ${chalk.cyan(`v${v.version}`)} - ${chalk.gray(v.timestamp)}${marker}`);
|
|
57
70
|
});
|
|
58
71
|
process.exit(1);
|
|
59
72
|
}
|
|
@@ -69,18 +82,18 @@ export async function rollback(subdomain, options) {
|
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
if (targetVersion === currentActive) {
|
|
72
|
-
warning(`Version ${targetVersion} is already active.`);
|
|
85
|
+
warning(`Version ${chalk.cyan(`v${targetVersion}`)} is already active.`);
|
|
73
86
|
process.exit(0);
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
|
|
89
|
+
const rollbackSpinner = spinner(`Rolling back from v${currentActive} to v${targetVersion}...`);
|
|
77
90
|
|
|
78
91
|
// Set the target version as active
|
|
79
92
|
if (useAPI) {
|
|
80
93
|
// Use API for centralized rollback (updates both D1 and R2)
|
|
81
94
|
const result = await rollbackViaAPI(subdomain, targetVersion);
|
|
82
95
|
if (!result) {
|
|
83
|
-
|
|
96
|
+
rollbackSpinner.warn('API unavailable, using local rollback');
|
|
84
97
|
await setActiveVersion(subdomain, targetVersion);
|
|
85
98
|
}
|
|
86
99
|
} else {
|
|
@@ -90,12 +103,16 @@ export async function rollback(subdomain, options) {
|
|
|
90
103
|
// Find the target version's deployment record for file count
|
|
91
104
|
const targetDeployment = versions.find(v => v.version === targetVersion);
|
|
92
105
|
|
|
93
|
-
|
|
106
|
+
rollbackSpinner.succeed(`Rolled back to ${chalk.cyan(`v${targetVersion}`)}`);
|
|
94
107
|
console.log(`\n 🔄 https://${subdomain}.launchpd.cloud\n`);
|
|
95
|
-
info(`Restored deployment from: ${targetDeployment?.timestamp || 'unknown'}`);
|
|
108
|
+
info(`Restored deployment from: ${chalk.gray(targetDeployment?.timestamp || 'unknown')}`);
|
|
96
109
|
|
|
97
110
|
} catch (err) {
|
|
98
|
-
|
|
111
|
+
errorWithSuggestions(`Rollback failed: ${err.message}`, [
|
|
112
|
+
'Check your internet connection',
|
|
113
|
+
'Verify the subdomain and version exist',
|
|
114
|
+
'Run "launchpd versions <subdomain>" to see available versions',
|
|
115
|
+
], { verbose, cause: err });
|
|
99
116
|
process.exit(1);
|
|
100
117
|
}
|
|
101
118
|
}
|
package/src/commands/versions.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { getVersionsForSubdomain, getActiveVersion } from '../utils/metadata.js';
|
|
2
2
|
import { getVersions as getVersionsFromAPI } from '../utils/api.js';
|
|
3
|
-
import { success,
|
|
3
|
+
import { success, errorWithSuggestions, info, spinner, formatSize } from '../utils/logger.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* List all versions for a subdomain
|
|
7
8
|
* @param {string} subdomain - Subdomain to list versions for
|
|
8
9
|
* @param {object} options - Command options
|
|
9
10
|
* @param {boolean} options.json - Output as JSON
|
|
11
|
+
* @param {boolean} options.verbose - Show verbose error details
|
|
10
12
|
*/
|
|
11
13
|
export async function versions(subdomain, options) {
|
|
14
|
+
const verbose = options.verbose || false;
|
|
15
|
+
|
|
12
16
|
try {
|
|
13
|
-
|
|
17
|
+
const fetchSpinner = spinner(`Fetching versions for ${subdomain}...`);
|
|
14
18
|
|
|
15
19
|
let versionList = [];
|
|
16
20
|
let activeVersion = 1;
|
|
@@ -32,10 +36,17 @@ export async function versions(subdomain, options) {
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
if (versionList.length === 0) {
|
|
35
|
-
|
|
39
|
+
fetchSpinner.fail(`No deployments found for: ${subdomain}`);
|
|
40
|
+
errorWithSuggestions(`No deployments found for subdomain: ${subdomain}`, [
|
|
41
|
+
'Check the subdomain name is correct',
|
|
42
|
+
'Run "launchpd list" to see your deployments',
|
|
43
|
+
'Deploy a new site with "launchpd deploy ./folder"',
|
|
44
|
+
], { verbose });
|
|
36
45
|
process.exit(1);
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
fetchSpinner.succeed(`Found ${versionList.length} version(s)`);
|
|
49
|
+
|
|
39
50
|
if (options.json) {
|
|
40
51
|
console.log(JSON.stringify({
|
|
41
52
|
subdomain,
|
|
@@ -52,24 +63,37 @@ export async function versions(subdomain, options) {
|
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
console.log('');
|
|
55
|
-
success(`Versions for ${subdomain}.launchpd.cloud:`);
|
|
66
|
+
success(`Versions for ${chalk.cyan(subdomain)}.launchpd.cloud:`);
|
|
56
67
|
console.log('');
|
|
57
68
|
|
|
69
|
+
// Table header
|
|
70
|
+
console.log(chalk.gray(' Version Date Files Size Status'));
|
|
71
|
+
console.log(chalk.gray(' ' + '─'.repeat(70)));
|
|
72
|
+
|
|
58
73
|
for (const v of versionList) {
|
|
59
74
|
const isActive = v.version === activeVersion;
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
75
|
+
const versionStr = chalk.bold.cyan(`v${v.version}`);
|
|
76
|
+
const date = chalk.gray(new Date(v.timestamp).toLocaleString());
|
|
77
|
+
const files = chalk.white(`${v.fileCount} files`);
|
|
78
|
+
const size = v.totalBytes ? chalk.white(formatSize(v.totalBytes)) : chalk.gray('unknown');
|
|
79
|
+
const status = isActive
|
|
80
|
+
? chalk.green.bold('● active')
|
|
81
|
+
: chalk.gray('○ inactive');
|
|
63
82
|
|
|
64
|
-
console.log(`
|
|
83
|
+
console.log(` ${versionStr.padEnd(18)}${date.padEnd(30)}${files.padEnd(12)}${size.padEnd(14)}${status}`);
|
|
65
84
|
}
|
|
66
85
|
|
|
86
|
+
console.log(chalk.gray(' ' + '─'.repeat(70)));
|
|
67
87
|
console.log('');
|
|
68
|
-
info(`Use
|
|
88
|
+
info(`Use ${chalk.cyan(`launchpd rollback ${subdomain} --to <n>`)} to restore a version.`);
|
|
69
89
|
console.log('');
|
|
70
90
|
|
|
71
91
|
} catch (err) {
|
|
72
|
-
|
|
92
|
+
errorWithSuggestions(`Failed to list versions: ${err.message}`, [
|
|
93
|
+
'Check your internet connection',
|
|
94
|
+
'Verify the subdomain exists',
|
|
95
|
+
'Try running with --verbose for more details',
|
|
96
|
+
], { verbose, cause: err });
|
|
73
97
|
process.exit(1);
|
|
74
98
|
}
|
|
75
99
|
}
|
package/src/config.js
CHANGED
|
@@ -1,36 +1,14 @@
|
|
|
1
|
-
import 'dotenv/config';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* Application configuration
|
|
2
|
+
* Application configuration for Launchpd CLI
|
|
3
|
+
* No credentials needed - uploads go through the API proxy
|
|
5
4
|
*/
|
|
6
5
|
export const config = {
|
|
7
|
-
r2: {
|
|
8
|
-
accountId: process.env.R2_ACCOUNT_ID || '',
|
|
9
|
-
accessKeyId: process.env.R2_ACCESS_KEY_ID || '',
|
|
10
|
-
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || '',
|
|
11
|
-
bucketName: process.env.R2_BUCKET_NAME || 'launchpd',
|
|
12
|
-
},
|
|
13
|
-
|
|
14
6
|
// Base domain for deployments
|
|
15
|
-
domain:
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Validate required configuration
|
|
20
|
-
* @returns {boolean} true if all required config is present
|
|
21
|
-
*/
|
|
22
|
-
export function validateConfig() {
|
|
23
|
-
const required = [
|
|
24
|
-
'R2_ACCOUNT_ID',
|
|
25
|
-
'R2_ACCESS_KEY_ID',
|
|
26
|
-
'R2_SECRET_ACCESS_KEY',
|
|
27
|
-
];
|
|
7
|
+
domain: 'launchpd.cloud',
|
|
28
8
|
|
|
29
|
-
|
|
9
|
+
// API endpoint
|
|
10
|
+
apiUrl: 'https://api.launchpd.cloud',
|
|
30
11
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return { valid: true, missing: [] };
|
|
36
|
-
}
|
|
12
|
+
// CLI version
|
|
13
|
+
version: '0.1.1',
|
|
14
|
+
};
|