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/utils/logger.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
|
|
4
|
+
// Store active spinner reference
|
|
5
|
+
let activeSpinner = null;
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Log a success message
|
|
@@ -11,9 +15,16 @@ export function success(message) {
|
|
|
11
15
|
/**
|
|
12
16
|
* Log an error message
|
|
13
17
|
* @param {string} message
|
|
18
|
+
* @param {object} options - Optional error details
|
|
19
|
+
* @param {boolean} options.verbose - Show verbose error details
|
|
20
|
+
* @param {Error} options.cause - Original error for verbose mode
|
|
14
21
|
*/
|
|
15
|
-
export function error(message) {
|
|
22
|
+
export function error(message, options = {}) {
|
|
16
23
|
console.error(chalk.red.bold('✗'), chalk.red(message));
|
|
24
|
+
if (options.verbose && options.cause) {
|
|
25
|
+
console.error(chalk.gray(' Stack trace:'));
|
|
26
|
+
console.error(chalk.gray(` ${options.cause.stack || options.cause.message}`));
|
|
27
|
+
}
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
/**
|
|
@@ -31,3 +42,115 @@ export function info(message) {
|
|
|
31
42
|
export function warning(message) {
|
|
32
43
|
console.log(chalk.yellow.bold('⚠'), chalk.yellow(message));
|
|
33
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create and start a spinner
|
|
48
|
+
* @param {string} text - Initial spinner text
|
|
49
|
+
* @returns {object} - Spinner instance with helper methods
|
|
50
|
+
*/
|
|
51
|
+
export function spinner(text) {
|
|
52
|
+
activeSpinner = ora({
|
|
53
|
+
text,
|
|
54
|
+
color: 'cyan',
|
|
55
|
+
spinner: 'dots',
|
|
56
|
+
}).start();
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
/**
|
|
60
|
+
* Update spinner text
|
|
61
|
+
* @param {string} newText
|
|
62
|
+
*/
|
|
63
|
+
update(newText) {
|
|
64
|
+
if (activeSpinner) {
|
|
65
|
+
activeSpinner.text = newText;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Mark spinner as successful and stop
|
|
71
|
+
* @param {string} text - Success message
|
|
72
|
+
*/
|
|
73
|
+
succeed(text) {
|
|
74
|
+
if (activeSpinner) {
|
|
75
|
+
activeSpinner.succeed(chalk.green(text));
|
|
76
|
+
activeSpinner = null;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Mark spinner as failed and stop
|
|
82
|
+
* @param {string} text - Failure message
|
|
83
|
+
*/
|
|
84
|
+
fail(text) {
|
|
85
|
+
if (activeSpinner) {
|
|
86
|
+
activeSpinner.fail(chalk.red(text));
|
|
87
|
+
activeSpinner = null;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Stop spinner with info message
|
|
93
|
+
* @param {string} text - Info message
|
|
94
|
+
*/
|
|
95
|
+
info(text) {
|
|
96
|
+
if (activeSpinner) {
|
|
97
|
+
activeSpinner.info(chalk.blue(text));
|
|
98
|
+
activeSpinner = null;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Stop spinner with warning
|
|
104
|
+
* @param {string} text - Warning message
|
|
105
|
+
*/
|
|
106
|
+
warn(text) {
|
|
107
|
+
if (activeSpinner) {
|
|
108
|
+
activeSpinner.warn(chalk.yellow(text));
|
|
109
|
+
activeSpinner = null;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Stop spinner without any symbol
|
|
115
|
+
*/
|
|
116
|
+
stop() {
|
|
117
|
+
if (activeSpinner) {
|
|
118
|
+
activeSpinner.stop();
|
|
119
|
+
activeSpinner = null;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Format bytes to human readable size (KB/MB/GB)
|
|
127
|
+
* @param {number} bytes - Size in bytes
|
|
128
|
+
* @param {number} decimals - Number of decimal places
|
|
129
|
+
* @returns {string} - Formatted size string
|
|
130
|
+
*/
|
|
131
|
+
export function formatSize(bytes, decimals = 2) {
|
|
132
|
+
if (bytes === 0) return '0 Bytes';
|
|
133
|
+
|
|
134
|
+
const k = 1024;
|
|
135
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
136
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
137
|
+
|
|
138
|
+
return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Log helpful error with suggestions
|
|
143
|
+
* @param {string} message - Error message
|
|
144
|
+
* @param {string[]} suggestions - Array of suggested actions
|
|
145
|
+
* @param {object} options - Error options
|
|
146
|
+
*/
|
|
147
|
+
export function errorWithSuggestions(message, suggestions = [], options = {}) {
|
|
148
|
+
error(message, options);
|
|
149
|
+
if (suggestions.length > 0) {
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(chalk.yellow('💡 Suggestions:'));
|
|
152
|
+
suggestions.forEach(suggestion => {
|
|
153
|
+
console.log(chalk.gray(` • ${suggestion}`));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/utils/metadata.js
CHANGED
|
@@ -1,114 +1,85 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Metadata utilities for Launchpd CLI
|
|
3
|
+
* All operations now go through the API proxy
|
|
4
|
+
*/
|
|
5
|
+
|
|
2
6
|
import { config } from '../config.js';
|
|
3
7
|
|
|
4
|
-
const
|
|
8
|
+
const API_BASE_URL = config.apiUrl;
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
|
-
*
|
|
11
|
+
* Get API key for requests
|
|
8
12
|
*/
|
|
9
|
-
function
|
|
10
|
-
return
|
|
11
|
-
region: 'auto',
|
|
12
|
-
endpoint: `https://${config.r2.accountId}.r2.cloudflarestorage.com`,
|
|
13
|
-
credentials: {
|
|
14
|
-
accessKeyId: config.r2.accessKeyId,
|
|
15
|
-
secretAccessKey: config.r2.secretAccessKey,
|
|
16
|
-
},
|
|
17
|
-
});
|
|
13
|
+
function getApiKey() {
|
|
14
|
+
return process.env.STATICLAUNCH_API_KEY || 'public-beta-key';
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
/**
|
|
21
|
-
*
|
|
22
|
-
* @returns {Promise<{version: number, deployments: Array}>}
|
|
18
|
+
* Make an authenticated API request
|
|
23
19
|
*/
|
|
24
|
-
async function
|
|
20
|
+
async function apiRequest(endpoint, options = {}) {
|
|
21
|
+
const url = `${API_BASE_URL}${endpoint}`;
|
|
22
|
+
|
|
23
|
+
const headers = {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
'X-API-Key': getApiKey(),
|
|
26
|
+
...options.headers,
|
|
27
|
+
};
|
|
28
|
+
|
|
25
29
|
try {
|
|
26
|
-
const response = await
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
})
|
|
30
|
-
const text = await response.Body.transformToString();
|
|
31
|
-
return JSON.parse(text);
|
|
32
|
-
} catch {
|
|
33
|
-
// File doesn't exist yet, return empty structure
|
|
34
|
-
return { version: 1, deployments: [] };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
...options,
|
|
32
|
+
headers,
|
|
33
|
+
});
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
* Create a timestamped backup of the metadata before overwriting
|
|
40
|
-
* @param {S3Client} client
|
|
41
|
-
* @param {object} data - Current metadata to backup
|
|
42
|
-
*/
|
|
43
|
-
async function backupMetadata(client, data) {
|
|
44
|
-
if (data.deployments.length === 0) return; // Nothing to backup
|
|
35
|
+
const data = await response.json();
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(data.error || `API error: ${response.status}`);
|
|
39
|
+
}
|
|
48
40
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
return data;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.message.includes('fetch failed') || err.message.includes('ENOTFOUND')) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
55
48
|
}
|
|
56
49
|
|
|
57
50
|
/**
|
|
58
|
-
* Record a deployment to
|
|
51
|
+
* Record a deployment to the API
|
|
59
52
|
* @param {string} subdomain - Deployed subdomain
|
|
60
53
|
* @param {string} folderPath - Original folder path
|
|
61
54
|
* @param {number} fileCount - Number of files deployed
|
|
62
55
|
* @param {number} totalBytes - Total bytes uploaded
|
|
63
56
|
* @param {number} version - Version number for this deployment
|
|
64
|
-
* @param {
|
|
57
|
+
* @param {Date|null} expiresAt - Expiration date, or null for no expiration
|
|
65
58
|
*/
|
|
66
59
|
export async function recordDeployment(subdomain, folderPath, fileCount, totalBytes = 0, version = 1, expiresAt = null) {
|
|
67
|
-
const client = createR2Client();
|
|
68
|
-
|
|
69
|
-
// Get existing data
|
|
70
|
-
const data = await getDeploymentsData(client);
|
|
71
|
-
|
|
72
|
-
// Backup before modifying (prevents accidental data loss)
|
|
73
|
-
await backupMetadata(client, data);
|
|
74
|
-
|
|
75
|
-
// Extract folder name from path
|
|
76
60
|
const folderName = folderPath.split(/[\\/]/).pop() || 'unknown';
|
|
77
61
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
data.deployments.push(deployment);
|
|
92
|
-
|
|
93
|
-
// Write back to R2
|
|
94
|
-
await client.send(new PutObjectCommand({
|
|
95
|
-
Bucket: config.r2.bucketName,
|
|
96
|
-
Key: METADATA_KEY,
|
|
97
|
-
Body: JSON.stringify(data, null, 2),
|
|
98
|
-
ContentType: 'application/json',
|
|
99
|
-
}));
|
|
100
|
-
|
|
101
|
-
return deployment;
|
|
62
|
+
return apiRequest('/api/deployments', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
subdomain,
|
|
66
|
+
folderName,
|
|
67
|
+
fileCount,
|
|
68
|
+
totalBytes,
|
|
69
|
+
version,
|
|
70
|
+
cliVersion: config.version,
|
|
71
|
+
expiresAt: expiresAt?.toISOString() || null,
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
102
74
|
}
|
|
103
75
|
|
|
104
76
|
/**
|
|
105
|
-
* List all deployments
|
|
77
|
+
* List all deployments for the current user
|
|
106
78
|
* @returns {Promise<Array>} Array of deployment records
|
|
107
79
|
*/
|
|
108
80
|
export async function listDeploymentsFromR2() {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
return data.deployments;
|
|
81
|
+
const result = await apiRequest('/api/deployments');
|
|
82
|
+
return result?.deployments || [];
|
|
112
83
|
}
|
|
113
84
|
|
|
114
85
|
/**
|
|
@@ -117,15 +88,13 @@ export async function listDeploymentsFromR2() {
|
|
|
117
88
|
* @returns {Promise<number>} Next version number
|
|
118
89
|
*/
|
|
119
90
|
export async function getNextVersion(subdomain) {
|
|
120
|
-
const
|
|
121
|
-
const data = await getDeploymentsData(client);
|
|
91
|
+
const result = await apiRequest(`/api/versions/${subdomain}`);
|
|
122
92
|
|
|
123
|
-
|
|
124
|
-
if (existingDeployments.length === 0) {
|
|
93
|
+
if (!result || !result.versions || result.versions.length === 0) {
|
|
125
94
|
return 1;
|
|
126
95
|
}
|
|
127
96
|
|
|
128
|
-
const maxVersion = Math.max(...
|
|
97
|
+
const maxVersion = Math.max(...result.versions.map(v => v.version));
|
|
129
98
|
return maxVersion + 1;
|
|
130
99
|
}
|
|
131
100
|
|
|
@@ -135,74 +104,20 @@ export async function getNextVersion(subdomain) {
|
|
|
135
104
|
* @returns {Promise<Array>} Array of deployment versions
|
|
136
105
|
*/
|
|
137
106
|
export async function getVersionsForSubdomain(subdomain) {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return data.deployments
|
|
142
|
-
.filter(d => d.subdomain === subdomain)
|
|
143
|
-
.sort((a, b) => (b.version || 1) - (a.version || 1));
|
|
107
|
+
const result = await apiRequest(`/api/versions/${subdomain}`);
|
|
108
|
+
return result?.versions || [];
|
|
144
109
|
}
|
|
145
110
|
|
|
146
111
|
/**
|
|
147
|
-
*
|
|
148
|
-
* @param {string} subdomain - The subdomain
|
|
149
|
-
* @param {number} fromVersion - Source version
|
|
150
|
-
* @param {number} toVersion - Target version (new active version)
|
|
151
|
-
*/
|
|
152
|
-
export async function copyVersionFiles(subdomain, fromVersion, toVersion) {
|
|
153
|
-
const client = createR2Client();
|
|
154
|
-
|
|
155
|
-
// List all files in the source version
|
|
156
|
-
const sourcePrefix = `${subdomain}/v${fromVersion}/`;
|
|
157
|
-
const targetPrefix = `${subdomain}/v${toVersion}/`;
|
|
158
|
-
|
|
159
|
-
const listResponse = await client.send(new ListObjectsV2Command({
|
|
160
|
-
Bucket: config.r2.bucketName,
|
|
161
|
-
Prefix: sourcePrefix,
|
|
162
|
-
}));
|
|
163
|
-
|
|
164
|
-
if (!listResponse.Contents || listResponse.Contents.length === 0) {
|
|
165
|
-
throw new Error(`No files found for version ${fromVersion}`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
let copiedCount = 0;
|
|
169
|
-
|
|
170
|
-
for (const object of listResponse.Contents) {
|
|
171
|
-
const sourceKey = object.Key;
|
|
172
|
-
const targetKey = sourceKey.replace(sourcePrefix, targetPrefix);
|
|
173
|
-
|
|
174
|
-
// Copy file to new version
|
|
175
|
-
await client.send(new CopyObjectCommand({
|
|
176
|
-
Bucket: config.r2.bucketName,
|
|
177
|
-
CopySource: `${config.r2.bucketName}/${sourceKey}`,
|
|
178
|
-
Key: targetKey,
|
|
179
|
-
}));
|
|
180
|
-
|
|
181
|
-
copiedCount++;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return { copiedCount, fromVersion, toVersion };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Update the active pointer for a subdomain (for rollback)
|
|
112
|
+
* Set the active version for a subdomain (rollback)
|
|
189
113
|
* @param {string} subdomain - The subdomain
|
|
190
114
|
* @param {number} version - Version to make active
|
|
191
115
|
*/
|
|
192
116
|
export async function setActiveVersion(subdomain, version) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
await client.send(new PutObjectCommand({
|
|
199
|
-
Bucket: config.r2.bucketName,
|
|
200
|
-
Key: pointerKey,
|
|
201
|
-
Body: JSON.stringify({ activeVersion: version, updatedAt: new Date().toISOString() }),
|
|
202
|
-
ContentType: 'application/json',
|
|
203
|
-
}));
|
|
204
|
-
|
|
205
|
-
return { subdomain, activeVersion: version };
|
|
117
|
+
return apiRequest(`/api/versions/${subdomain}/rollback`, {
|
|
118
|
+
method: 'PUT',
|
|
119
|
+
body: JSON.stringify({ version }),
|
|
120
|
+
});
|
|
206
121
|
}
|
|
207
122
|
|
|
208
123
|
/**
|
|
@@ -211,144 +126,76 @@ export async function setActiveVersion(subdomain, version) {
|
|
|
211
126
|
* @returns {Promise<number>} Active version number
|
|
212
127
|
*/
|
|
213
128
|
export async function getActiveVersion(subdomain) {
|
|
214
|
-
const
|
|
129
|
+
const result = await apiRequest(`/api/versions/${subdomain}`);
|
|
130
|
+
return result?.activeVersion || 1;
|
|
131
|
+
}
|
|
215
132
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
133
|
+
/**
|
|
134
|
+
* Copy files from one version to another (for rollback)
|
|
135
|
+
* Note: This is now handled server-side by the API
|
|
136
|
+
* @param {string} subdomain - The subdomain
|
|
137
|
+
* @param {number} fromVersion - Source version
|
|
138
|
+
* @param {number} toVersion - Target version
|
|
139
|
+
*/
|
|
140
|
+
export async function copyVersionFiles(subdomain, fromVersion, toVersion) {
|
|
141
|
+
// Rollback is now handled by setActiveVersion - no need to copy files
|
|
142
|
+
// The worker serves files from the specified version directly
|
|
143
|
+
return { fromVersion, toVersion, note: 'Handled by API' };
|
|
228
144
|
}
|
|
229
145
|
|
|
230
146
|
/**
|
|
231
147
|
* List all files for a specific version
|
|
232
148
|
* @param {string} subdomain - The subdomain
|
|
233
149
|
* @param {number} version - Version number
|
|
234
|
-
* @returns {Promise<Array>} Array of file
|
|
150
|
+
* @returns {Promise<Array>} Array of file info
|
|
235
151
|
*/
|
|
236
152
|
export async function listVersionFiles(subdomain, version) {
|
|
237
|
-
const
|
|
153
|
+
const result = await apiRequest(`/api/deployments/${subdomain}`);
|
|
238
154
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const response = await client.send(new ListObjectsV2Command({
|
|
242
|
-
Bucket: config.r2.bucketName,
|
|
243
|
-
Prefix: prefix,
|
|
244
|
-
}));
|
|
245
|
-
|
|
246
|
-
if (!response.Contents) {
|
|
155
|
+
if (!result || !result.versions) {
|
|
247
156
|
return [];
|
|
248
157
|
}
|
|
249
158
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
size: obj.Size,
|
|
253
|
-
lastModified: obj.LastModified,
|
|
254
|
-
}));
|
|
159
|
+
const versionInfo = result.versions.find(v => v.version === version);
|
|
160
|
+
return versionInfo ? [{ version, fileCount: versionInfo.file_count, totalBytes: versionInfo.total_bytes }] : [];
|
|
255
161
|
}
|
|
256
162
|
|
|
257
163
|
/**
|
|
258
164
|
* Delete all files for a subdomain (all versions)
|
|
259
|
-
*
|
|
260
|
-
* @
|
|
165
|
+
* Note: This should be an admin operation, not available to CLI users
|
|
166
|
+
* @param {string} _subdomain - The subdomain to delete
|
|
261
167
|
*/
|
|
262
|
-
export async function deleteSubdomain(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const prefix = `${subdomain}/`;
|
|
267
|
-
|
|
268
|
-
let deletedCount = 0;
|
|
269
|
-
let continuationToken;
|
|
270
|
-
|
|
271
|
-
do {
|
|
272
|
-
const response = await client.send(new ListObjectsV2Command({
|
|
273
|
-
Bucket: config.r2.bucketName,
|
|
274
|
-
Prefix: prefix,
|
|
275
|
-
ContinuationToken: continuationToken,
|
|
276
|
-
}));
|
|
277
|
-
|
|
278
|
-
if (response.Contents && response.Contents.length > 0) {
|
|
279
|
-
for (const object of response.Contents) {
|
|
280
|
-
await client.send(new DeleteObjectCommand({
|
|
281
|
-
Bucket: config.r2.bucketName,
|
|
282
|
-
Key: object.Key,
|
|
283
|
-
}));
|
|
284
|
-
deletedCount++;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
continuationToken = response.NextContinuationToken;
|
|
289
|
-
} while (continuationToken);
|
|
290
|
-
|
|
291
|
-
return { deletedCount };
|
|
168
|
+
export async function deleteSubdomain(_subdomain) {
|
|
169
|
+
// This operation is not available in the consumer CLI
|
|
170
|
+
// It should be handled through the admin dashboard or worker
|
|
171
|
+
throw new Error('Subdomain deletion is not available in the CLI. Contact support.');
|
|
292
172
|
}
|
|
293
173
|
|
|
294
174
|
/**
|
|
295
175
|
* Get all expired deployments
|
|
176
|
+
* Note: Cleanup is handled server-side automatically
|
|
296
177
|
* @returns {Promise<Array>} Array of expired deployment records
|
|
297
178
|
*/
|
|
298
179
|
export async function getExpiredDeployments() {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const now = Date.now();
|
|
302
|
-
|
|
303
|
-
return data.deployments.filter(d =>
|
|
304
|
-
d.expiresAt && new Date(d.expiresAt).getTime() < now
|
|
305
|
-
);
|
|
180
|
+
// Expiration cleanup is handled server-side
|
|
181
|
+
return [];
|
|
306
182
|
}
|
|
307
183
|
|
|
308
184
|
/**
|
|
309
185
|
* Remove deployment records for a subdomain from metadata
|
|
310
|
-
*
|
|
186
|
+
* Note: This should be an admin operation
|
|
187
|
+
* @param {string} _subdomain - The subdomain to remove
|
|
311
188
|
*/
|
|
312
|
-
export async function removeDeploymentRecords(
|
|
313
|
-
|
|
314
|
-
const data = await getDeploymentsData(client);
|
|
315
|
-
|
|
316
|
-
// Backup before modifying
|
|
317
|
-
await backupMetadata(client, data);
|
|
318
|
-
|
|
319
|
-
// Filter out the subdomain's deployments
|
|
320
|
-
data.deployments = data.deployments.filter(d => d.subdomain !== subdomain);
|
|
321
|
-
|
|
322
|
-
// Write back
|
|
323
|
-
await client.send(new PutObjectCommand({
|
|
324
|
-
Bucket: config.r2.bucketName,
|
|
325
|
-
Key: METADATA_KEY,
|
|
326
|
-
Body: JSON.stringify(data, null, 2),
|
|
327
|
-
ContentType: 'application/json',
|
|
328
|
-
}));
|
|
189
|
+
export async function removeDeploymentRecords(_subdomain) {
|
|
190
|
+
throw new Error('Deployment record removal is not available in the CLI. Contact support.');
|
|
329
191
|
}
|
|
330
192
|
|
|
331
193
|
/**
|
|
332
194
|
* Clean up all expired deployments
|
|
195
|
+
* Note: This is now handled automatically by the worker
|
|
333
196
|
* @returns {Promise<{cleaned: string[], errors: string[]}>}
|
|
334
197
|
*/
|
|
335
198
|
export async function cleanupExpiredDeployments() {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const errors = [];
|
|
339
|
-
|
|
340
|
-
// Get unique subdomains
|
|
341
|
-
const subdomains = [...new Set(expired.map(d => d.subdomain))];
|
|
342
|
-
|
|
343
|
-
for (const subdomain of subdomains) {
|
|
344
|
-
try {
|
|
345
|
-
await deleteSubdomain(subdomain);
|
|
346
|
-
await removeDeploymentRecords(subdomain);
|
|
347
|
-
cleaned.push(subdomain);
|
|
348
|
-
} catch (err) {
|
|
349
|
-
errors.push(`${subdomain}: ${err.message}`);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return { cleaned, errors };
|
|
199
|
+
// Cleanup is handled server-side automatically
|
|
200
|
+
return { cleaned: [], errors: [], note: 'Handled automatically by server' };
|
|
354
201
|
}
|