launchpd 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,10 @@
1
1
  MIT License
2
2
 
3
+ <<<<<<< HEAD
3
4
  Copyright (c) 2026 Launchpd
5
+ =======
6
+ Copyright (c) 2026 Kent Edoloverio
7
+ >>>>>>> 12ada1bf2dcc63674c3b6955e525a51209308d45
4
8
 
5
9
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
10
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -107,4 +107,4 @@ launchpd logout
107
107
 
108
108
  ## License
109
109
 
110
- MIT
110
+ MIT
package/bin/setup.js CHANGED
@@ -1,62 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { config, validateConfig } from '../src/config.js';
4
- import { info, success, error } from '../src/utils/logger.js';
3
+ import { config } from '../src/config.js';
4
+ import { info, success } from '../src/utils/logger.js';
5
5
  import chalk from 'chalk';
6
6
 
7
7
  /**
8
- * Setup script to validate and display configuration status
8
+ * Setup script to display CLI information
9
9
  */
10
10
  async function setup() {
11
11
  console.log('\n' + chalk.bold.blue('═══════════════════════════════════════'));
12
- console.log(chalk.bold.blue(' Launchpd Configuration Check'));
12
+ console.log(chalk.bold.blue(' Launchpd CLI'));
13
13
  console.log(chalk.bold.blue('═══════════════════════════════════════\n'));
14
14
 
15
- // Check environment variables
16
- info('Checking configuration...\n');
15
+ info('Launchpd is ready to use!\n');
17
16
 
18
- const validation = validateConfig();
19
-
20
- if (!validation.valid) {
21
- error(`Missing required environment variables:`);
22
- for (const missing of validation.missing) {
23
- console.log(chalk.red(` ✗ ${missing}`));
24
- }
25
- console.log('\n' + chalk.yellow('Setup Instructions:'));
26
- console.log(' 1. Copy .env.example to .env:');
27
- console.log(chalk.gray(' cp .env.example .env'));
28
- console.log(' 2. Edit .env and add your Cloudflare R2 credentials');
29
- console.log(' 3. Get credentials from: https://dash.cloudflare.com → R2 → API Tokens\n');
30
- process.exit(1);
31
- }
32
-
33
- // Display current configuration
34
- console.log(chalk.green('✓ All required environment variables set\n'));
35
-
36
- console.log(chalk.bold('Current Configuration:'));
17
+ console.log(chalk.bold('Configuration:'));
37
18
  console.log(chalk.gray('─'.repeat(50)));
38
19
  console.log(chalk.cyan(' Domain: '), config.domain);
39
- console.log(chalk.cyan(' R2 Bucket: '), config.r2.bucketName);
40
- console.log(chalk.cyan(' Account ID: '), config.r2.accountId ? '✓ Set' : '✗ Missing');
41
- console.log(chalk.cyan(' Access Key: '), config.r2.accessKeyId ? '✓ Set' : '✗ Missing');
42
- console.log(chalk.cyan(' Secret Key: '), config.r2.secretAccessKey ? '✓ Set' : '✗ Missing');
20
+ console.log(chalk.cyan(' API: '), config.apiUrl);
21
+ console.log(chalk.cyan(' Version: '), config.version);
43
22
  console.log(chalk.gray('─'.repeat(50)) + '\n');
44
23
 
45
- // Next steps
46
- console.log(chalk.bold('Next Steps:'));
47
- console.log(chalk.gray(' 1. Test dry-run deployment:'));
48
- console.log(chalk.cyan(' npm run dev'));
49
- console.log(chalk.gray(' 2. Deploy Worker:'));
50
- console.log(chalk.cyan(' cd worker && wrangler deploy'));
51
- console.log(chalk.gray(' 3. Verify DNS settings in Cloudflare Dashboard:'));
52
- console.log(chalk.cyan(` DNS → Records → Add A record * → 192.0.2.1 (Proxied)`));
53
- console.log(chalk.gray(' 4. Deploy your first site:'));
24
+ console.log(chalk.bold('Quick Start:'));
25
+ console.log(chalk.gray(' Deploy your first site:'));
54
26
  console.log(chalk.cyan(' launchpd deploy ./your-folder\n'));
55
27
 
56
- success('Setup validation complete!');
28
+ console.log(chalk.gray(' Login for more quota:'));
29
+ console.log(chalk.cyan(' launchpd login\n'));
30
+
31
+ console.log(chalk.gray(' List your deployments:'));
32
+ console.log(chalk.cyan(' launchpd list\n'));
33
+
34
+ success('No configuration needed - just deploy!');
57
35
  }
58
36
 
59
37
  setup().catch(err => {
60
- error(`Setup failed: ${err.message}`);
38
+ console.error(`Setup failed: ${err.message}`);
61
39
  process.exit(1);
62
40
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launchpd",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Deploy static sites instantly to a live URL",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,6 @@
21
21
  "scripts": {
22
22
  "start": "node bin/cli.js",
23
23
  "dev": "node bin/cli.js deploy ../examples/test-site --dry-run",
24
- "setup": "node bin/setup.js",
25
24
  "test": "vitest run",
26
25
  "test:watch": "vitest",
27
26
  "test:coverage": "vitest run --coverage",
@@ -35,7 +34,6 @@
35
34
  "deploy",
36
35
  "cli",
37
36
  "cloudflare",
38
- "r2",
39
37
  "cdn",
40
38
  "website",
41
39
  "publish"
@@ -52,10 +50,8 @@
52
50
  },
53
51
  "homepage": "https://launchpd.cloud",
54
52
  "dependencies": {
55
- "@aws-sdk/client-s3": "^3.700.0",
56
53
  "chalk": "^5.4.0",
57
54
  "commander": "^14.0.0",
58
- "dotenv": "^16.4.0",
59
55
  "mime-types": "^2.1.35",
60
56
  "nanoid": "^5.1.0"
61
57
  },
@@ -2,10 +2,10 @@ 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 { recordDeployment as recordMetadata, getNextVersion, setActiveVersion } from '../utils/metadata.js';
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 { recordDeployment as recordToAPI, getNextVersionFromAPI } from '../utils/api.js';
8
+ import { getNextVersionFromAPI } from '../utils/api.js';
9
9
  import { success, error, info, warning } from '../utils/logger.js';
10
10
  import { calculateExpiresAt, formatTimeRemaining } from '../utils/expiration.js';
11
11
  import { checkQuota, displayQuotaWarnings } from '../utils/quota.js';
@@ -135,42 +135,29 @@ export async function deploy(folder, options) {
135
135
 
136
136
  // Perform actual upload
137
137
  try {
138
- // Get next version number for this subdomain (try API first, fallback to R2)
138
+ // Get next version number for this subdomain (try API first, fallback to local)
139
139
  let version = await getNextVersionFromAPI(subdomain);
140
140
  if (version === null) {
141
141
  version = await getNextVersion(subdomain);
142
142
  }
143
143
  info(`Deploying as version ${version}...`);
144
144
 
145
+ // Upload all files via API proxy
146
+ const folderName = basename(folderPath);
145
147
  const { totalBytes } = await uploadFolder(folderPath, subdomain, version);
146
148
 
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({
149
+ // Finalize upload: set active version and record metadata
150
+ info('Finalizing deployment...');
151
+ await finalizeUpload(
156
152
  subdomain,
157
- folderName,
153
+ version,
158
154
  fileCount,
159
155
  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
- }
156
+ folderName,
157
+ expiresAt?.toISOString() || null
158
+ );
172
159
 
173
- // Always save locally for quick access
160
+ // Save locally for quick access
174
161
  await saveLocalDeployment({
175
162
  subdomain,
176
163
  folderName,
package/src/config.js CHANGED
@@ -1,36 +1,14 @@
1
- import 'dotenv/config';
2
-
3
1
  /**
4
- * Application configuration loaded from environment variables
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: process.env.STATICLAUNCH_DOMAIN || 'launchpd.cloud',
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
- const missing = required.filter(key => !process.env[key]);
9
+ // API endpoint
10
+ apiUrl: 'https://api.launchpd.cloud',
30
11
 
31
- if (missing.length > 0) {
32
- return { valid: false, missing };
33
- }
34
-
35
- return { valid: true, missing: [] };
36
- }
12
+ // CLI version
13
+ version: '0.1.1',
14
+ };
@@ -1,114 +1,85 @@
1
- import { S3Client, GetObjectCommand, PutObjectCommand, ListObjectsV2Command, CopyObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
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 METADATA_KEY = '_meta/deployments.json';
8
+ const API_BASE_URL = config.apiUrl;
5
9
 
6
10
  /**
7
- * Create S3-compatible client for Cloudflare R2
11
+ * Get API key for requests
8
12
  */
9
- function createR2Client() {
10
- return new S3Client({
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
- * Get existing deployments metadata from R2
22
- * @returns {Promise<{version: number, deployments: Array}>}
18
+ * Make an authenticated API request
23
19
  */
24
- async function getDeploymentsData(client) {
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 client.send(new GetObjectCommand({
27
- Bucket: config.r2.bucketName,
28
- Key: METADATA_KEY,
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
- const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');
47
- const backupKey = `_meta/backups/deployments-${timestamp}.json`;
37
+ if (!response.ok) {
38
+ throw new Error(data.error || `API error: ${response.status}`);
39
+ }
48
40
 
49
- await client.send(new PutObjectCommand({
50
- Bucket: config.r2.bucketName,
51
- Key: backupKey,
52
- Body: JSON.stringify(data, null, 2),
53
- ContentType: 'application/json',
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 R2 metadata
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 {string|null} expiresAt - ISO timestamp for expiration, or null for no expiration
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
- // Append new deployment
79
- const deployment = {
80
- subdomain,
81
- timestamp: new Date().toISOString(),
82
- folderName,
83
- fileCount,
84
- totalBytes,
85
- cliVersion: '0.1.0',
86
- version,
87
- isActive: true,
88
- expiresAt,
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 from R2 metadata
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 client = createR2Client();
110
- const data = await getDeploymentsData(client);
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 client = createR2Client();
121
- const data = await getDeploymentsData(client);
91
+ const result = await apiRequest(`/api/versions/${subdomain}`);
122
92
 
123
- const existingDeployments = data.deployments.filter(d => d.subdomain === subdomain);
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(...existingDeployments.map(d => d.version || 1));
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 client = createR2Client();
139
- const data = await getDeploymentsData(client);
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
- * Copy files from one version to another (for rollback)
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
- const client = createR2Client();
194
-
195
- // Create/update an active pointer file
196
- const pointerKey = `${subdomain}/_active`;
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 client = createR2Client();
129
+ const result = await apiRequest(`/api/versions/${subdomain}`);
130
+ return result?.activeVersion || 1;
131
+ }
215
132
 
216
- try {
217
- const response = await client.send(new GetObjectCommand({
218
- Bucket: config.r2.bucketName,
219
- Key: `${subdomain}/_active`,
220
- }));
221
- const text = await response.Body.transformToString();
222
- const data = JSON.parse(text);
223
- return data.activeVersion || 1;
224
- } catch {
225
- // No active pointer, default to version 1
226
- return 1;
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 keys
150
+ * @returns {Promise<Array>} Array of file info
235
151
  */
236
152
  export async function listVersionFiles(subdomain, version) {
237
- const client = createR2Client();
153
+ const result = await apiRequest(`/api/deployments/${subdomain}`);
238
154
 
239
- const prefix = `${subdomain}/v${version}/`;
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
- return response.Contents.map(obj => ({
251
- key: obj.Key,
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
- * @param {string} subdomain - The subdomain to delete
260
- * @returns {Promise<{deletedCount: number}>}
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(subdomain) {
263
- const client = createR2Client();
264
-
265
- // List all files for this subdomain
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
- const client = createR2Client();
300
- const data = await getDeploymentsData(client);
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
- * @param {string} subdomain - The subdomain to remove
186
+ * Note: This should be an admin operation
187
+ * @param {string} _subdomain - The subdomain to remove
311
188
  */
312
- export async function removeDeploymentRecords(subdomain) {
313
- const client = createR2Client();
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
- const expired = await getExpiredDeployments();
337
- const cleaned = [];
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
  }
@@ -1,22 +1,16 @@
1
- import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
2
1
  import { readdir, readFile } from 'node:fs/promises';
3
2
  import { join, relative, posix, sep } from 'node:path';
4
3
  import mime from 'mime-types';
5
4
  import { config } from '../config.js';
6
5
  import { info } from './logger.js';
7
6
 
7
+ const API_BASE_URL = `https://api.${config.domain}`;
8
+
8
9
  /**
9
- * Create S3-compatible client for Cloudflare R2
10
+ * Get API key for requests
10
11
  */
11
- function createR2Client() {
12
- return new S3Client({
13
- region: 'auto',
14
- endpoint: `https://${config.r2.accountId}.r2.cloudflarestorage.com`,
15
- credentials: {
16
- accessKeyId: config.r2.accessKeyId,
17
- secretAccessKey: config.r2.secretAccessKey,
18
- },
19
- });
12
+ function getApiKey() {
13
+ return process.env.STATICLAUNCH_API_KEY || 'public-beta-key';
20
14
  }
21
15
 
22
16
  /**
@@ -29,13 +23,76 @@ function toPosixPath(windowsPath) {
29
23
  }
30
24
 
31
25
  /**
32
- * Upload a folder to R2 under a subdomain prefix with versioning
26
+ * Upload a single file via API proxy
27
+ * @param {Buffer} content - File content
28
+ * @param {string} subdomain - Target subdomain
29
+ * @param {number} version - Version number
30
+ * @param {string} filePath - Relative file path
31
+ * @param {string} contentType - MIME type
32
+ */
33
+ async function uploadFile(content, subdomain, version, filePath, contentType) {
34
+ const response = await fetch(`${API_BASE_URL}/api/upload/file`, {
35
+ method: 'POST',
36
+ headers: {
37
+ 'X-API-Key': getApiKey(),
38
+ 'X-Subdomain': subdomain,
39
+ 'X-Version': String(version),
40
+ 'X-File-Path': filePath,
41
+ 'X-Content-Type': contentType,
42
+ 'Content-Type': 'application/octet-stream',
43
+ },
44
+ body: content,
45
+ });
46
+
47
+ if (!response.ok) {
48
+ const error = await response.json().catch(() => ({ error: 'Upload failed' }));
49
+ throw new Error(error.error || `Upload failed: ${response.status}`);
50
+ }
51
+
52
+ return response.json();
53
+ }
54
+
55
+ /**
56
+ * Mark upload complete and set active version
57
+ * @param {string} subdomain - Target subdomain
58
+ * @param {number} version - Version number
59
+ * @param {number} fileCount - Number of files uploaded
60
+ * @param {number} totalBytes - Total bytes uploaded
61
+ * @param {string} folderName - Original folder name
62
+ * @param {string|null} expiresAt - ISO expiration timestamp
63
+ */
64
+ async function completeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt) {
65
+ const response = await fetch(`${API_BASE_URL}/api/upload/complete`, {
66
+ method: 'POST',
67
+ headers: {
68
+ 'X-API-Key': getApiKey(),
69
+ 'Content-Type': 'application/json',
70
+ },
71
+ body: JSON.stringify({
72
+ subdomain,
73
+ version,
74
+ fileCount,
75
+ totalBytes,
76
+ folderName,
77
+ expiresAt,
78
+ }),
79
+ });
80
+
81
+ if (!response.ok) {
82
+ const error = await response.json().catch(() => ({ error: 'Complete upload failed' }));
83
+ throw new Error(error.error || `Complete upload failed: ${response.status}`);
84
+ }
85
+
86
+ return response.json();
87
+ }
88
+
89
+ /**
90
+ * Upload a folder to Launchpd via API proxy
33
91
  * @param {string} localPath - Local folder path
34
92
  * @param {string} subdomain - Subdomain to use as bucket prefix
35
93
  * @param {number} version - Version number for this deployment
36
94
  */
37
95
  export async function uploadFolder(localPath, subdomain, version = 1) {
38
- const client = createR2Client();
39
96
  const files = await readdir(localPath, { recursive: true, withFileTypes: true });
40
97
 
41
98
  let uploaded = 0;
@@ -48,27 +105,35 @@ export async function uploadFolder(localPath, subdomain, version = 1) {
48
105
  // Build full local path
49
106
  const fullPath = join(file.parentPath || file.path, file.name);
50
107
 
51
- // Build R2 key: subdomain/v{version}/relative/path/to/file.ext
108
+ // Build relative path for R2 key
52
109
  const relativePath = relative(localPath, fullPath);
53
- const key = `${subdomain}/v${version}/${toPosixPath(relativePath)}`;
110
+ const posixPath = toPosixPath(relativePath);
54
111
 
55
112
  // Detect content type
56
113
  const contentType = mime.lookup(file.name) || 'application/octet-stream';
57
114
 
58
- // Read file and upload
115
+ // Read file and upload via API
59
116
  const body = await readFile(fullPath);
60
117
  totalBytes += body.length;
61
118
 
62
- await client.send(new PutObjectCommand({
63
- Bucket: config.r2.bucketName,
64
- Key: key,
65
- Body: body,
66
- ContentType: contentType,
67
- }));
119
+ await uploadFile(body, subdomain, version, posixPath, contentType);
68
120
 
69
121
  uploaded++;
70
- info(` Uploaded (${uploaded}/${total}): ${toPosixPath(relativePath)}`);
122
+ info(` Uploaded (${uploaded}/${total}): ${posixPath}`);
71
123
  }
72
124
 
73
125
  return { uploaded, subdomain, totalBytes };
74
126
  }
127
+
128
+ /**
129
+ * Complete the upload and set active version
130
+ * @param {string} subdomain - Target subdomain
131
+ * @param {number} version - Version number
132
+ * @param {number} fileCount - Number of files
133
+ * @param {number} totalBytes - Total bytes
134
+ * @param {string} folderName - Folder name
135
+ * @param {string|null} expiresAt - Expiration ISO timestamp
136
+ */
137
+ export async function finalizeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt = null) {
138
+ return completeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt);
139
+ }