launchpd 1.0.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.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/bin/cli.js +94 -0
- package/bin/setup.js +40 -0
- package/package.json +67 -0
- package/src/commands/auth.js +357 -0
- package/src/commands/deploy.js +242 -0
- package/src/commands/index.js +9 -0
- package/src/commands/list.js +133 -0
- package/src/commands/rollback.js +119 -0
- package/src/commands/versions.js +117 -0
- package/src/config.js +14 -0
- package/src/utils/api.js +182 -0
- package/src/utils/credentials.js +153 -0
- package/src/utils/expiration.js +89 -0
- package/src/utils/id.js +17 -0
- package/src/utils/index.js +14 -0
- package/src/utils/localConfig.js +85 -0
- package/src/utils/logger.js +152 -0
- package/src/utils/machineId.js +28 -0
- package/src/utils/metadata.js +201 -0
- package/src/utils/prompt.js +87 -0
- package/src/utils/quota.js +231 -0
- package/src/utils/upload.js +181 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, posix, sep } from 'node:path';
|
|
3
|
+
import mime from 'mime-types';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
import { getApiKey, getApiSecret } from './credentials.js';
|
|
6
|
+
import { createHmac } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
const API_BASE_URL = config.apiUrl;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert Windows path to POSIX for R2 keys
|
|
12
|
+
* @param {string} windowsPath
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function toPosixPath(windowsPath) {
|
|
16
|
+
return windowsPath.split(sep).join(posix.sep);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Upload a single file via API proxy
|
|
21
|
+
* @param {Buffer} content - File content
|
|
22
|
+
* @param {string} subdomain - Target subdomain
|
|
23
|
+
* @param {number} version - Version number
|
|
24
|
+
* @param {string} filePath - Relative file path
|
|
25
|
+
* @param {string} contentType - MIME type
|
|
26
|
+
*/
|
|
27
|
+
async function uploadFile(content, subdomain, version, filePath, contentType) {
|
|
28
|
+
const apiKey = await getApiKey();
|
|
29
|
+
const apiSecret = await getApiSecret();
|
|
30
|
+
const headers = {
|
|
31
|
+
'X-API-Key': apiKey,
|
|
32
|
+
'X-Subdomain': subdomain,
|
|
33
|
+
'X-Version': String(version),
|
|
34
|
+
'X-File-Path': filePath,
|
|
35
|
+
'X-Content-Type': contentType,
|
|
36
|
+
'Content-Type': 'application/octet-stream',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (apiSecret) {
|
|
40
|
+
const timestamp = Date.now().toString();
|
|
41
|
+
const endpoint = '/api/upload/file'; // Match the worker path
|
|
42
|
+
const hmac = createHmac('sha256', apiSecret);
|
|
43
|
+
hmac.update('POST');
|
|
44
|
+
hmac.update(endpoint);
|
|
45
|
+
hmac.update(timestamp);
|
|
46
|
+
hmac.update(content); // Buffer is fine for update()
|
|
47
|
+
|
|
48
|
+
headers['X-Timestamp'] = timestamp;
|
|
49
|
+
headers['X-Signature'] = hmac.digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const response = await fetch(`${API_BASE_URL}/api/upload/file`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers,
|
|
55
|
+
body: content,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const error = await response.json().catch(() => ({ error: 'Upload failed' }));
|
|
60
|
+
throw new Error(error.error || `Upload failed: ${response.status}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return response.json();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Mark upload complete and set active version
|
|
68
|
+
* @param {string} subdomain - Target subdomain
|
|
69
|
+
* @param {number} version - Version number
|
|
70
|
+
* @param {number} fileCount - Number of files uploaded
|
|
71
|
+
* @param {number} totalBytes - Total bytes uploaded
|
|
72
|
+
* @param {string} folderName - Original folder name
|
|
73
|
+
* @param {string|null} expiresAt - ISO expiration timestamp
|
|
74
|
+
*/
|
|
75
|
+
async function completeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt) {
|
|
76
|
+
const apiKey = await getApiKey();
|
|
77
|
+
const apiSecret = await getApiSecret();
|
|
78
|
+
const headers = {
|
|
79
|
+
'X-API-Key': apiKey,
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const body = JSON.stringify({
|
|
84
|
+
subdomain,
|
|
85
|
+
version,
|
|
86
|
+
fileCount,
|
|
87
|
+
totalBytes,
|
|
88
|
+
folderName,
|
|
89
|
+
expiresAt,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (apiSecret) {
|
|
93
|
+
const timestamp = Date.now().toString();
|
|
94
|
+
const endpoint = '/api/upload/complete';
|
|
95
|
+
const hmac = createHmac('sha256', apiSecret);
|
|
96
|
+
hmac.update('POST');
|
|
97
|
+
hmac.update(endpoint);
|
|
98
|
+
hmac.update(timestamp);
|
|
99
|
+
hmac.update(body);
|
|
100
|
+
|
|
101
|
+
headers['X-Timestamp'] = timestamp;
|
|
102
|
+
headers['X-Signature'] = hmac.digest('hex');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const response = await fetch(`${API_BASE_URL}/api/upload/complete`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers,
|
|
108
|
+
body,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
let errorMsg = 'Complete upload failed';
|
|
113
|
+
const text = await response.text();
|
|
114
|
+
try {
|
|
115
|
+
const data = JSON.parse(text);
|
|
116
|
+
errorMsg = data.error || `Complete upload failed: ${response.status} ${response.statusText}`;
|
|
117
|
+
} catch {
|
|
118
|
+
errorMsg = `Complete upload failed: ${response.status} ${response.statusText} - ${text.substring(0, 100)}`;
|
|
119
|
+
}
|
|
120
|
+
throw new Error(errorMsg);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return response.json();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Upload a folder to Launchpd via API proxy
|
|
128
|
+
* @param {string} localPath - Local folder path
|
|
129
|
+
* @param {string} subdomain - Subdomain to use as bucket prefix
|
|
130
|
+
* @param {number} version - Version number for this deployment
|
|
131
|
+
* @param {function} onProgress - Progress callback (uploaded, total, fileName)
|
|
132
|
+
*/
|
|
133
|
+
export async function uploadFolder(localPath, subdomain, version = 1, onProgress = null) {
|
|
134
|
+
const files = await readdir(localPath, { recursive: true, withFileTypes: true });
|
|
135
|
+
|
|
136
|
+
let uploaded = 0;
|
|
137
|
+
let totalBytes = 0;
|
|
138
|
+
const total = files.filter(f => f.isFile()).length;
|
|
139
|
+
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
if (!file.isFile()) continue;
|
|
142
|
+
|
|
143
|
+
// Build full local path
|
|
144
|
+
const fullPath = join(file.parentPath || file.path, file.name);
|
|
145
|
+
|
|
146
|
+
// Build relative path for R2 key
|
|
147
|
+
const relativePath = relative(localPath, fullPath);
|
|
148
|
+
const posixPath = toPosixPath(relativePath);
|
|
149
|
+
|
|
150
|
+
// Detect content type
|
|
151
|
+
const contentType = mime.lookup(file.name) || 'application/octet-stream';
|
|
152
|
+
|
|
153
|
+
// Read file and upload via API
|
|
154
|
+
const body = await readFile(fullPath);
|
|
155
|
+
totalBytes += body.length;
|
|
156
|
+
|
|
157
|
+
await uploadFile(body, subdomain, version, posixPath, contentType);
|
|
158
|
+
|
|
159
|
+
uploaded++;
|
|
160
|
+
|
|
161
|
+
// Call progress callback if provided
|
|
162
|
+
if (onProgress) {
|
|
163
|
+
onProgress(uploaded, total, posixPath);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { uploaded, subdomain, totalBytes };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Complete the upload and set active version
|
|
172
|
+
* @param {string} subdomain - Target subdomain
|
|
173
|
+
* @param {number} version - Version number
|
|
174
|
+
* @param {number} fileCount - Number of files
|
|
175
|
+
* @param {number} totalBytes - Total bytes
|
|
176
|
+
* @param {string} folderName - Folder name
|
|
177
|
+
* @param {string|null} expiresAt - Expiration ISO timestamp
|
|
178
|
+
*/
|
|
179
|
+
export async function finalizeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt = null) {
|
|
180
|
+
return completeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt);
|
|
181
|
+
}
|