launchpd 0.1.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.
Potentially problematic release.
This version of launchpd might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/bin/cli.js +90 -0
- package/bin/setup.js +62 -0
- package/package.json +68 -0
- package/src/commands/auth.js +324 -0
- package/src/commands/deploy.js +194 -0
- package/src/commands/index.js +9 -0
- package/src/commands/list.js +111 -0
- package/src/commands/rollback.js +101 -0
- package/src/commands/versions.js +75 -0
- package/src/config.js +36 -0
- package/src/utils/api.js +158 -0
- package/src/utils/credentials.js +143 -0
- package/src/utils/expiration.js +89 -0
- package/src/utils/id.js +17 -0
- package/src/utils/index.js +13 -0
- package/src/utils/localConfig.js +85 -0
- package/src/utils/logger.js +33 -0
- package/src/utils/metadata.js +354 -0
- package/src/utils/quota.js +229 -0
- package/src/utils/upload.js +74 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication commands for StaticLaunch CLI
|
|
3
|
+
* login, logout, register, whoami
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { exec } from 'node:child_process';
|
|
8
|
+
import { config } from '../config.js';
|
|
9
|
+
import { getCredentials, saveCredentials, clearCredentials, isLoggedIn } from '../utils/credentials.js';
|
|
10
|
+
import { success, error, info, warning } from '../utils/logger.js';
|
|
11
|
+
|
|
12
|
+
const API_BASE_URL = `https://api.${config.domain}`;
|
|
13
|
+
const REGISTER_URL = `https://portal.${config.domain}/auth/register`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Prompt for user input
|
|
17
|
+
*/
|
|
18
|
+
function prompt(question) {
|
|
19
|
+
const rl = createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
rl.question(question, (answer) => {
|
|
26
|
+
rl.close();
|
|
27
|
+
resolve(answer.trim());
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate API key with the server
|
|
34
|
+
*/
|
|
35
|
+
async function validateApiKey(apiKey) {
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(`${API_BASE_URL}/api/quota`, {
|
|
38
|
+
headers: {
|
|
39
|
+
'X-API-Key': apiKey,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
if (data.authenticated) {
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
error(`Failed to validate API key: ${err.message}`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Login command - prompts for API key and validates it
|
|
60
|
+
*/
|
|
61
|
+
export async function login() {
|
|
62
|
+
// Check if already logged in
|
|
63
|
+
if (await isLoggedIn()) {
|
|
64
|
+
const creds = await getCredentials();
|
|
65
|
+
warning(`Already logged in as ${creds.email || creds.userId}`);
|
|
66
|
+
info('Run "launchpd logout" to switch accounts');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log('\nš Launchpd Login\n');
|
|
71
|
+
console.log('Enter your API key from the dashboard.');
|
|
72
|
+
console.log(`Don't have one? Run "launchpd register" first.\n`);
|
|
73
|
+
|
|
74
|
+
const apiKey = await prompt('API Key: ');
|
|
75
|
+
|
|
76
|
+
if (!apiKey) {
|
|
77
|
+
error('API key is required');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
info('Validating API key...');
|
|
82
|
+
|
|
83
|
+
const result = await validateApiKey(apiKey);
|
|
84
|
+
|
|
85
|
+
if (!result) {
|
|
86
|
+
error('Invalid API key. Please check and try again.');
|
|
87
|
+
console.log(`\nGet your API key at: https://portal.${config.domain}/api-keys`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Save credentials
|
|
92
|
+
await saveCredentials({
|
|
93
|
+
apiKey,
|
|
94
|
+
userId: result.user?.id,
|
|
95
|
+
email: result.user?.email,
|
|
96
|
+
tier: result.tier,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
success(`Logged in successfully!`);
|
|
100
|
+
console.log(`\n Email: ${result.user?.email || 'N/A'}`);
|
|
101
|
+
console.log(` Tier: ${result.tier}`);
|
|
102
|
+
console.log(` Sites: ${result.usage?.siteCount || 0}/${result.limits?.maxSites || '?'}`);
|
|
103
|
+
console.log(` Storage: ${result.usage?.storageUsedMB || 0}MB/${result.limits?.maxStorageMB || '?'}MB\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Logout command - clears stored credentials
|
|
108
|
+
*/
|
|
109
|
+
export async function logout() {
|
|
110
|
+
const loggedIn = await isLoggedIn();
|
|
111
|
+
|
|
112
|
+
if (!loggedIn) {
|
|
113
|
+
warning('Not currently logged in');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const creds = await getCredentials();
|
|
118
|
+
await clearCredentials();
|
|
119
|
+
|
|
120
|
+
success(`Logged out successfully`);
|
|
121
|
+
if (creds?.email) {
|
|
122
|
+
info(`Was logged in as: ${creds.email}`);
|
|
123
|
+
}
|
|
124
|
+
console.log(`\nYou can still deploy anonymously (limited to 3 sites, 50MB).`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Register command - opens browser to registration page
|
|
129
|
+
*/
|
|
130
|
+
export async function register() {
|
|
131
|
+
console.log('\nš Register for Launchpd\n');
|
|
132
|
+
console.log(`Opening registration page: ${REGISTER_URL}\n`);
|
|
133
|
+
|
|
134
|
+
// Open browser based on platform
|
|
135
|
+
const platform = process.platform;
|
|
136
|
+
let cmd;
|
|
137
|
+
|
|
138
|
+
if (platform === 'darwin') {
|
|
139
|
+
cmd = `open "${REGISTER_URL}"`;
|
|
140
|
+
} else if (platform === 'win32') {
|
|
141
|
+
cmd = `start "" "${REGISTER_URL}"`;
|
|
142
|
+
} else {
|
|
143
|
+
cmd = `xdg-open "${REGISTER_URL}"`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
exec(cmd, (err) => {
|
|
147
|
+
if (err) {
|
|
148
|
+
console.log(`Please open this URL in your browser:\n ${REGISTER_URL}\n`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
console.log('After registering:');
|
|
153
|
+
console.log(' 1. Get your API key from the dashboard');
|
|
154
|
+
console.log(' 2. Run: launchpd login');
|
|
155
|
+
console.log('');
|
|
156
|
+
|
|
157
|
+
info('Registration benefits:');
|
|
158
|
+
console.log(' ā 10 sites (instead of 3)');
|
|
159
|
+
console.log(' ā 100MB storage (instead of 50MB)');
|
|
160
|
+
console.log(' ā 30-day retention (instead of 7 days)');
|
|
161
|
+
console.log(' ā 10 versions per site');
|
|
162
|
+
console.log('');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Whoami command - shows current user info and quota status
|
|
167
|
+
*/
|
|
168
|
+
export async function whoami() {
|
|
169
|
+
const creds = await getCredentials();
|
|
170
|
+
|
|
171
|
+
if (!creds) {
|
|
172
|
+
console.log('\nš¤ Not logged in (anonymous mode)\n');
|
|
173
|
+
console.log('Anonymous limits:');
|
|
174
|
+
console.log(' ⢠3 sites maximum');
|
|
175
|
+
console.log(' ⢠50MB total storage');
|
|
176
|
+
console.log(' ⢠7-day retention');
|
|
177
|
+
console.log(' ⢠1 version per site');
|
|
178
|
+
console.log(`\nRun "launchpd login" to authenticate`);
|
|
179
|
+
console.log(`Run "launchpd register" to create an account\n`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
info('Fetching account status...');
|
|
184
|
+
|
|
185
|
+
// Validate and get current quota
|
|
186
|
+
const result = await validateApiKey(creds.apiKey);
|
|
187
|
+
|
|
188
|
+
if (!result) {
|
|
189
|
+
warning('Session expired or API key invalid');
|
|
190
|
+
await clearCredentials();
|
|
191
|
+
error('Please login again with: launchpd login');
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log(`\nš¤ Logged in as: ${result.user?.email || result.user?.id}\n`);
|
|
196
|
+
|
|
197
|
+
console.log('Account Info:');
|
|
198
|
+
console.log(` User ID: ${result.user?.id}`);
|
|
199
|
+
console.log(` Email: ${result.user?.email || 'Not set'}`);
|
|
200
|
+
console.log(` Tier: ${result.tier}`);
|
|
201
|
+
console.log('');
|
|
202
|
+
|
|
203
|
+
console.log('Usage:');
|
|
204
|
+
console.log(` Sites: ${result.usage?.siteCount || 0} / ${result.limits?.maxSites}`);
|
|
205
|
+
console.log(` Storage: ${result.usage?.storageUsedMB || 0}MB / ${result.limits?.maxStorageMB}MB`);
|
|
206
|
+
console.log(` Sites remaining: ${result.usage?.sitesRemaining || 0}`);
|
|
207
|
+
console.log(` Storage remaining: ${result.usage?.storageRemainingMB || 0}MB`);
|
|
208
|
+
console.log('');
|
|
209
|
+
|
|
210
|
+
console.log('Limits:');
|
|
211
|
+
console.log(` Max versions per site: ${result.limits?.maxVersionsPerSite}`);
|
|
212
|
+
console.log(` Retention: ${result.limits?.retentionDays} days`);
|
|
213
|
+
console.log('');
|
|
214
|
+
|
|
215
|
+
// Show warnings
|
|
216
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
217
|
+
console.log('ā ļø Warnings:');
|
|
218
|
+
result.warnings.forEach(w => console.log(` ${w}`));
|
|
219
|
+
console.log('');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!result.canCreateNewSite) {
|
|
223
|
+
warning('You cannot create new sites (limit reached)');
|
|
224
|
+
info('You can still update existing sites');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Quota command - shows detailed quota information
|
|
230
|
+
*/
|
|
231
|
+
export async function quota() {
|
|
232
|
+
const creds = await getCredentials();
|
|
233
|
+
|
|
234
|
+
if (!creds) {
|
|
235
|
+
console.log('\nš Anonymous Quota Status\n');
|
|
236
|
+
console.log('You are not logged in.');
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log('Anonymous tier limits:');
|
|
239
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
240
|
+
console.log(' ā Sites: 3 maximum ā');
|
|
241
|
+
console.log(' ā Storage: 50MB total ā');
|
|
242
|
+
console.log(' ā Retention: 7 days ā');
|
|
243
|
+
console.log(' ā Versions: 1 per site ā');
|
|
244
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
245
|
+
console.log('');
|
|
246
|
+
console.log('š” Register for FREE to unlock more:');
|
|
247
|
+
console.log(' ā 10 sites');
|
|
248
|
+
console.log(' ā 100MB storage');
|
|
249
|
+
console.log(' ā 30-day retention');
|
|
250
|
+
console.log(' ā 10 versions per site');
|
|
251
|
+
console.log('');
|
|
252
|
+
console.log('Run: launchpd register');
|
|
253
|
+
console.log('');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
info('Fetching quota status...');
|
|
258
|
+
|
|
259
|
+
const result = await validateApiKey(creds.apiKey);
|
|
260
|
+
|
|
261
|
+
if (!result) {
|
|
262
|
+
error('Failed to fetch quota. API key may be invalid.');
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log(`\nš Quota Status for: ${result.user?.email || creds.email}\n`);
|
|
267
|
+
|
|
268
|
+
// Sites usage
|
|
269
|
+
const sitesUsed = result.usage?.siteCount || 0;
|
|
270
|
+
const sitesMax = result.limits?.maxSites || 10;
|
|
271
|
+
const sitesPercent = Math.round((sitesUsed / sitesMax) * 100);
|
|
272
|
+
const sitesBar = createProgressBar(sitesUsed, sitesMax);
|
|
273
|
+
|
|
274
|
+
console.log(`Sites: ${sitesBar} ${sitesUsed}/${sitesMax} (${sitesPercent}%)`);
|
|
275
|
+
|
|
276
|
+
// Storage usage
|
|
277
|
+
const storageMB = result.usage?.storageUsedMB || 0;
|
|
278
|
+
const storageMaxMB = result.limits?.maxStorageMB || 100;
|
|
279
|
+
const storagePercent = Math.round((storageMB / storageMaxMB) * 100);
|
|
280
|
+
const storageBar = createProgressBar(storageMB, storageMaxMB);
|
|
281
|
+
|
|
282
|
+
console.log(`Storage: ${storageBar} ${storageMB}MB/${storageMaxMB}MB (${storagePercent}%)`);
|
|
283
|
+
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log(`Tier: ${result.tier || 'free'}`);
|
|
286
|
+
console.log(`Retention: ${result.limits?.retentionDays || 30} days`);
|
|
287
|
+
console.log(`Max versions: ${result.limits?.maxVersionsPerSite || 10} per site`);
|
|
288
|
+
console.log('');
|
|
289
|
+
|
|
290
|
+
// Status indicators
|
|
291
|
+
if (result.canCreateNewSite === false) {
|
|
292
|
+
warning('ā ļø Site limit reached - cannot create new sites');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (storagePercent > 80) {
|
|
296
|
+
warning(`ā ļø Storage ${storagePercent}% used - consider cleaning up old deployments`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (result.tier === 'free') {
|
|
300
|
+
console.log('');
|
|
301
|
+
info('š Upgrade to Pro for 50 sites, 1GB storage, and 50 versions');
|
|
302
|
+
}
|
|
303
|
+
console.log('');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Create a simple progress bar
|
|
308
|
+
*/
|
|
309
|
+
function createProgressBar(current, max, width = 20) {
|
|
310
|
+
const filled = Math.round((current / max) * width);
|
|
311
|
+
const empty = width - filled;
|
|
312
|
+
const percent = (current / max) * 100;
|
|
313
|
+
|
|
314
|
+
let bar = '';
|
|
315
|
+
if (percent >= 90) {
|
|
316
|
+
bar = 'ā'.repeat(filled) + 'ā'.repeat(empty);
|
|
317
|
+
} else if (percent >= 70) {
|
|
318
|
+
bar = 'ā'.repeat(filled) + 'ā'.repeat(empty);
|
|
319
|
+
} else {
|
|
320
|
+
bar = 'ā'.repeat(filled) + 'ā'.repeat(empty);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return `[${bar}]`;
|
|
324
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import { resolve, basename, join } from 'node:path';
|
|
4
|
+
import { generateSubdomain } from '../utils/id.js';
|
|
5
|
+
import { uploadFolder } from '../utils/upload.js';
|
|
6
|
+
import { recordDeployment as recordMetadata, getNextVersion, setActiveVersion } from '../utils/metadata.js';
|
|
7
|
+
import { saveLocalDeployment } from '../utils/localConfig.js';
|
|
8
|
+
import { recordDeployment as recordToAPI, getNextVersionFromAPI } from '../utils/api.js';
|
|
9
|
+
import { success, error, info, warning } from '../utils/logger.js';
|
|
10
|
+
import { calculateExpiresAt, formatTimeRemaining } from '../utils/expiration.js';
|
|
11
|
+
import { checkQuota, displayQuotaWarnings } from '../utils/quota.js';
|
|
12
|
+
import { getCredentials } from '../utils/credentials.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculate total size of a folder
|
|
16
|
+
*/
|
|
17
|
+
async function calculateFolderSize(folderPath) {
|
|
18
|
+
const files = await readdir(folderPath, { recursive: true, withFileTypes: true });
|
|
19
|
+
let totalSize = 0;
|
|
20
|
+
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
if (file.isFile()) {
|
|
23
|
+
const fullPath = file.parentPath
|
|
24
|
+
? join(file.parentPath, file.name)
|
|
25
|
+
: join(folderPath, file.name);
|
|
26
|
+
try {
|
|
27
|
+
const stats = statSync(fullPath);
|
|
28
|
+
totalSize += stats.size;
|
|
29
|
+
} catch {
|
|
30
|
+
// File may have been deleted
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return totalSize;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Deploy a local folder to StaticLaunch
|
|
40
|
+
* @param {string} folder - Path to folder to deploy
|
|
41
|
+
* @param {object} options - Command options
|
|
42
|
+
* @param {boolean} options.dryRun - Skip actual upload
|
|
43
|
+
* @param {string} options.name - Custom subdomain
|
|
44
|
+
* @param {string} options.expires - Expiration time (e.g., "30m", "2h", "1d")
|
|
45
|
+
*/
|
|
46
|
+
export async function deploy(folder, options) {
|
|
47
|
+
const folderPath = resolve(folder);
|
|
48
|
+
|
|
49
|
+
// Parse expiration if provided
|
|
50
|
+
let expiresAt = null;
|
|
51
|
+
if (options.expires) {
|
|
52
|
+
try {
|
|
53
|
+
expiresAt = calculateExpiresAt(options.expires);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
error(err.message);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate folder exists
|
|
61
|
+
if (!existsSync(folderPath)) {
|
|
62
|
+
error(`Folder not found: ${folderPath}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check folder is not empty
|
|
67
|
+
const files = await readdir(folderPath, { recursive: true, withFileTypes: true });
|
|
68
|
+
const fileCount = files.filter(f => f.isFile()).length;
|
|
69
|
+
|
|
70
|
+
if (fileCount === 0) {
|
|
71
|
+
error('Folder is empty. Nothing to deploy.');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Generate or use provided subdomain
|
|
76
|
+
const subdomain = options.name || generateSubdomain();
|
|
77
|
+
const url = `https://${subdomain}.launchpd.cloud`;
|
|
78
|
+
|
|
79
|
+
// Calculate estimated upload size
|
|
80
|
+
const estimatedBytes = await calculateFolderSize(folderPath);
|
|
81
|
+
|
|
82
|
+
// Check quota before deploying
|
|
83
|
+
info('Checking quota...');
|
|
84
|
+
const quotaCheck = await checkQuota(subdomain, estimatedBytes);
|
|
85
|
+
|
|
86
|
+
if (!quotaCheck.allowed) {
|
|
87
|
+
error('Deployment blocked due to quota limits');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Display any warnings
|
|
92
|
+
displayQuotaWarnings(quotaCheck.warnings);
|
|
93
|
+
|
|
94
|
+
// Show current user status
|
|
95
|
+
const creds = await getCredentials();
|
|
96
|
+
if (creds?.email) {
|
|
97
|
+
info(`Deploying as: ${creds.email}`);
|
|
98
|
+
} else {
|
|
99
|
+
info('Deploying as: anonymous (run "launchpd login" for more quota)');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
info(`Deploying ${fileCount} file(s) from ${folderPath}`);
|
|
103
|
+
info(`Target: ${url}`);
|
|
104
|
+
info(`Size: ${(estimatedBytes / 1024 / 1024).toFixed(2)}MB`);
|
|
105
|
+
|
|
106
|
+
if (options.dryRun) {
|
|
107
|
+
warning('Dry run mode - skipping upload');
|
|
108
|
+
|
|
109
|
+
// List files that would be uploaded
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
if (file.isFile()) {
|
|
112
|
+
const relativePath = file.parentPath
|
|
113
|
+
? `${file.parentPath.replace(folderPath, '')}/${file.name}`.replace(/^[\\/]/, '')
|
|
114
|
+
: file.name;
|
|
115
|
+
info(` Would upload: ${relativePath.replaceAll('\\', '/')}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
success(`Dry run complete. ${fileCount} file(s) would be deployed to:`);
|
|
120
|
+
console.log(`\n ${url}\n`);
|
|
121
|
+
|
|
122
|
+
// Show quota status after dry run
|
|
123
|
+
if (quotaCheck.quota) {
|
|
124
|
+
console.log('Quota after this deploy:');
|
|
125
|
+
const storageAfter = (quotaCheck.quota.usage?.storageUsed || 0) + estimatedBytes;
|
|
126
|
+
const sitesAfter = quotaCheck.isNewSite
|
|
127
|
+
? (quotaCheck.quota.usage?.siteCount || 0) + 1
|
|
128
|
+
: quotaCheck.quota.usage?.siteCount || 0;
|
|
129
|
+
console.log(` Sites: ${sitesAfter}/${quotaCheck.quota.limits.maxSites}`);
|
|
130
|
+
console.log(` Storage: ${(storageAfter / 1024 / 1024).toFixed(1)}MB/${quotaCheck.quota.limits.maxStorageMB}MB`);
|
|
131
|
+
console.log('');
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Perform actual upload
|
|
137
|
+
try {
|
|
138
|
+
// Get next version number for this subdomain (try API first, fallback to R2)
|
|
139
|
+
let version = await getNextVersionFromAPI(subdomain);
|
|
140
|
+
if (version === null) {
|
|
141
|
+
version = await getNextVersion(subdomain);
|
|
142
|
+
}
|
|
143
|
+
info(`Deploying as version ${version}...`);
|
|
144
|
+
|
|
145
|
+
const { totalBytes } = await uploadFolder(folderPath, subdomain, version);
|
|
146
|
+
|
|
147
|
+
// Set this version as active
|
|
148
|
+
await setActiveVersion(subdomain, version);
|
|
149
|
+
|
|
150
|
+
// Record deployment metadata
|
|
151
|
+
info('Recording deployment metadata...');
|
|
152
|
+
|
|
153
|
+
// Try API first for centralized storage
|
|
154
|
+
const folderName = basename(folderPath);
|
|
155
|
+
const apiResult = await recordToAPI({
|
|
156
|
+
subdomain,
|
|
157
|
+
folderName,
|
|
158
|
+
fileCount,
|
|
159
|
+
totalBytes,
|
|
160
|
+
version,
|
|
161
|
+
expiresAt: expiresAt?.toISOString() || null,
|
|
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
|
+
}
|
|
172
|
+
|
|
173
|
+
// Always save locally for quick access
|
|
174
|
+
await saveLocalDeployment({
|
|
175
|
+
subdomain,
|
|
176
|
+
folderName,
|
|
177
|
+
fileCount,
|
|
178
|
+
totalBytes,
|
|
179
|
+
version,
|
|
180
|
+
timestamp: new Date().toISOString(),
|
|
181
|
+
expiresAt: expiresAt?.toISOString() || null,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
success(`Deployed successfully! (v${version})`);
|
|
185
|
+
console.log(`\n š ${url}`);
|
|
186
|
+
if (expiresAt) {
|
|
187
|
+
warning(` ā° Expires: ${formatTimeRemaining(expiresAt)}`);
|
|
188
|
+
}
|
|
189
|
+
console.log('');
|
|
190
|
+
} catch (err) {
|
|
191
|
+
error(`Upload failed: ${err.message}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands index - exports all CLI commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { deploy } from './deploy.js';
|
|
6
|
+
export { list } from './list.js';
|
|
7
|
+
export { rollback } from './rollback.js';
|
|
8
|
+
export { versions } from './versions.js';
|
|
9
|
+
export { login, logout, register, whoami, quota } from './auth.js';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { getLocalDeployments } from '../utils/localConfig.js';
|
|
2
|
+
import { listDeployments as listFromAPI } from '../utils/api.js';
|
|
3
|
+
import { error, info, warning } from '../utils/logger.js';
|
|
4
|
+
import { formatTimeRemaining, isExpired } from '../utils/expiration.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* List all deployments (from API or local storage)
|
|
9
|
+
* @param {object} options - Command options
|
|
10
|
+
* @param {boolean} options.json - Output as JSON
|
|
11
|
+
* @param {boolean} options.local - Only show local deployments
|
|
12
|
+
*/
|
|
13
|
+
export async function list(options) {
|
|
14
|
+
try {
|
|
15
|
+
let deployments = [];
|
|
16
|
+
let source = 'local';
|
|
17
|
+
|
|
18
|
+
// Try API first unless --local flag is set
|
|
19
|
+
if (!options.local) {
|
|
20
|
+
const apiResult = await listFromAPI();
|
|
21
|
+
if (apiResult && apiResult.deployments) {
|
|
22
|
+
deployments = apiResult.deployments.map(d => ({
|
|
23
|
+
subdomain: d.subdomain,
|
|
24
|
+
folderName: d.folder_name,
|
|
25
|
+
fileCount: d.file_count,
|
|
26
|
+
totalBytes: d.total_bytes,
|
|
27
|
+
version: d.version,
|
|
28
|
+
timestamp: d.created_at,
|
|
29
|
+
expiresAt: d.expires_at,
|
|
30
|
+
isActive: d.active_version === d.version,
|
|
31
|
+
}));
|
|
32
|
+
source = 'api';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fallback to local storage if API unavailable
|
|
37
|
+
if (deployments.length === 0) {
|
|
38
|
+
deployments = await getLocalDeployments();
|
|
39
|
+
source = 'local';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (deployments.length === 0) {
|
|
43
|
+
warning('No deployments found.');
|
|
44
|
+
info('Deploy a folder with: launchpd deploy ./my-folder');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (options.json) {
|
|
49
|
+
console.log(JSON.stringify(deployments, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Display as table
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(chalk.bold('Your Deployments:'));
|
|
56
|
+
console.log(chalk.gray('ā'.repeat(95)));
|
|
57
|
+
|
|
58
|
+
// Header
|
|
59
|
+
console.log(
|
|
60
|
+
chalk.gray(
|
|
61
|
+
padRight('URL', 40) +
|
|
62
|
+
padRight('Folder', 15) +
|
|
63
|
+
padRight('Files', 7) +
|
|
64
|
+
padRight('Date', 12) +
|
|
65
|
+
'Status'
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
console.log(chalk.gray('ā'.repeat(95)));
|
|
69
|
+
|
|
70
|
+
// Rows (most recent first)
|
|
71
|
+
const sorted = [...deployments].reverse();
|
|
72
|
+
for (const dep of sorted) {
|
|
73
|
+
const url = `https://${dep.subdomain}.launchpd.cloud`;
|
|
74
|
+
const date = new Date(dep.timestamp).toLocaleDateString();
|
|
75
|
+
|
|
76
|
+
// Determine status
|
|
77
|
+
let status = chalk.green('active');
|
|
78
|
+
if (dep.expiresAt) {
|
|
79
|
+
if (isExpired(dep.expiresAt)) {
|
|
80
|
+
status = chalk.red('expired');
|
|
81
|
+
} else {
|
|
82
|
+
status = chalk.yellow(formatTimeRemaining(dep.expiresAt));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(
|
|
87
|
+
chalk.cyan(padRight(url, 40)) +
|
|
88
|
+
padRight(dep.folderName || '-', 15) +
|
|
89
|
+
padRight(String(dep.fileCount), 7) +
|
|
90
|
+
chalk.gray(padRight(date, 12)) +
|
|
91
|
+
status
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(chalk.gray('ā'.repeat(95)));
|
|
96
|
+
console.log(chalk.gray(`Total: ${deployments.length} deployment(s)`) + (source === 'api' ? chalk.green(' (synced)') : chalk.yellow(' (local only)')));
|
|
97
|
+
console.log('');
|
|
98
|
+
|
|
99
|
+
} catch (err) {
|
|
100
|
+
error(`Failed to list deployments: ${err.message}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Pad string to the right
|
|
107
|
+
*/
|
|
108
|
+
function padRight(str, len) {
|
|
109
|
+
if (str.length >= len) return str.substring(0, len - 1) + ' ';
|
|
110
|
+
return str + ' '.repeat(len - str.length);
|
|
111
|
+
}
|