@kitecd/cli 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/bin/kite.js +2 -0
- package/dist/home.js +114 -0
- package/dist/index.js +603 -0
- package/dist/local-server.js +434 -0
- package/dist/local-store.js +142 -0
- package/dist/pack.js +137 -0
- package/dist/serve.js +208 -0
- package/dist/server/index.js +30043 -0
- package/dist/upload.js +84 -0
- package/dist/web/assets/Dashboard-pjIWWLub.js +1 -0
- package/dist/web/assets/DefaultLayout-Bj8fPWym.css +1 -0
- package/dist/web/assets/DefaultLayout-DelfwTTT.js +1 -0
- package/dist/web/assets/FileExplorer-xY5ejhhN.js +1 -0
- package/dist/web/assets/LogBoard-DzW-cEqH.css +1 -0
- package/dist/web/assets/LogBoard-tT61QjOx.js +6 -0
- package/dist/web/assets/Login-B4C149oC.js +1 -0
- package/dist/web/assets/ProjectDetail-Z8cZoqr5.js +1 -0
- package/dist/web/assets/ProjectList-9rbMuJeY.js +1 -0
- package/dist/web/assets/Settings-CtCNDUXY.js +1 -0
- package/dist/web/assets/activity-DItEGOtI.js +1 -0
- package/dist/web/assets/circle-alert-Bfrn_ovD.js +1 -0
- package/dist/web/assets/clock-BPXGSCIV.js +1 -0
- package/dist/web/assets/constants-C4Zrkm2g.js +1 -0
- package/dist/web/assets/createLucideIcon-Cgv1AIRL.js +1 -0
- package/dist/web/assets/folder-open-jX-_Q7bA.js +1 -0
- package/dist/web/assets/index-C615tnMi.js +2 -0
- package/dist/web/assets/index-C9LiRc31.css +1 -0
- package/dist/web/assets/project-BFuaDcvV.js +1 -0
- package/dist/web/assets/refresh-cw-DWmqwQRn.js +1 -0
- package/dist/web/assets/save-BkiMrL9q.js +1 -0
- package/dist/web/assets/server-C33taHNn.js +1 -0
- package/dist/web/assets/settings-CrCWmNyB.js +1 -0
- package/dist/web/assets/square-terminal-C8toRwjx.js +1 -0
- package/dist/web/favicon.svg +5 -0
- package/dist/web/icons.svg +24 -0
- package/dist/web/index.html +15 -0
- package/package.json +40 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated This module is superseded by the Elysia-based server in apps/server/.
|
|
3
|
+
* It is retained for backward compatibility and will be removed in a future version.
|
|
4
|
+
* Use `kite serve` which now starts the Elysia server via bun.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import fsp from 'fs/promises';
|
|
8
|
+
import http from 'http';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import * as readline from 'readline';
|
|
13
|
+
import AdmZip from 'adm-zip';
|
|
14
|
+
import Busboy from 'busboy';
|
|
15
|
+
import { LocalStore } from './local-store.js';
|
|
16
|
+
import { ensureKiteHome, randomToken, setGlobalConfig } from './home.js';
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const webRoot = path.join(__dirname, 'web');
|
|
19
|
+
const json = (status, data) => ({
|
|
20
|
+
status,
|
|
21
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
22
|
+
body: JSON.stringify(data)
|
|
23
|
+
});
|
|
24
|
+
const text = (status, body, contentType = 'text/plain; charset=utf-8') => ({
|
|
25
|
+
status,
|
|
26
|
+
headers: { 'Content-Type': contentType },
|
|
27
|
+
body
|
|
28
|
+
});
|
|
29
|
+
const readBody = async (request) => {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
for await (const chunk of request) {
|
|
32
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
33
|
+
}
|
|
34
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
35
|
+
};
|
|
36
|
+
const getAuthToken = (request) => {
|
|
37
|
+
const auth = request.headers.authorization;
|
|
38
|
+
if (!auth?.startsWith('Bearer '))
|
|
39
|
+
return '';
|
|
40
|
+
return auth.slice('Bearer '.length);
|
|
41
|
+
};
|
|
42
|
+
const parseMultipart = (request) => {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const storeHome = ensureKiteHome();
|
|
45
|
+
const tempPath = path.join(storeHome, 'tmp', `${Date.now()}-${Math.random().toString(16).slice(2)}.zip`);
|
|
46
|
+
const fields = {};
|
|
47
|
+
let filePath;
|
|
48
|
+
const busboy = Busboy({
|
|
49
|
+
headers: request.headers,
|
|
50
|
+
limits: { fileSize: 50 * 1024 * 1024, files: 1 }
|
|
51
|
+
});
|
|
52
|
+
busboy.on('field', (name, value) => {
|
|
53
|
+
fields[name] = value;
|
|
54
|
+
});
|
|
55
|
+
busboy.on('file', (_name, file) => {
|
|
56
|
+
filePath = tempPath;
|
|
57
|
+
file.pipe(fs.createWriteStream(tempPath));
|
|
58
|
+
});
|
|
59
|
+
busboy.on('error', reject);
|
|
60
|
+
busboy.on('finish', () => resolve({ fields, filePath }));
|
|
61
|
+
request.pipe(busboy);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
// SSE broadcast: deployId -> Set of response objects
|
|
65
|
+
const deploySubscribers = new Map();
|
|
66
|
+
function broadcastToSubscribers(deployId, event, data) {
|
|
67
|
+
const subs = deploySubscribers.get(deployId);
|
|
68
|
+
if (!subs || subs.size === 0)
|
|
69
|
+
return;
|
|
70
|
+
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
71
|
+
for (const res of subs) {
|
|
72
|
+
try {
|
|
73
|
+
res.write(message);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
subs.delete(res);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Streaming shell command: yields lines with raw ANSI codes
|
|
81
|
+
async function* runShellCommand(command, cwd) {
|
|
82
|
+
const proc = spawn('/bin/sh', ['-c', command], { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
83
|
+
const rlStdout = readline.createInterface({ input: proc.stdout });
|
|
84
|
+
const rlStderr = readline.createInterface({ input: proc.stderr });
|
|
85
|
+
// Yield lines from both stdout and stderr as they arrive
|
|
86
|
+
const lineQueue = [];
|
|
87
|
+
let resolve = null;
|
|
88
|
+
let done = false;
|
|
89
|
+
const pushLine = (line) => {
|
|
90
|
+
lineQueue.push(line);
|
|
91
|
+
if (resolve) {
|
|
92
|
+
resolve();
|
|
93
|
+
resolve = null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
rlStdout.on('line', pushLine);
|
|
97
|
+
rlStderr.on('line', pushLine);
|
|
98
|
+
const onClose = () => {
|
|
99
|
+
done = true;
|
|
100
|
+
if (resolve) {
|
|
101
|
+
resolve();
|
|
102
|
+
resolve = null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
proc.on('close', onClose);
|
|
106
|
+
while (!done || lineQueue.length > 0) {
|
|
107
|
+
if (lineQueue.length === 0) {
|
|
108
|
+
await new Promise(r => { resolve = r; });
|
|
109
|
+
}
|
|
110
|
+
while (lineQueue.length > 0) {
|
|
111
|
+
yield lineQueue.shift();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const exitCode = await new Promise(r => proc.on('close', (code) => r(code ?? 0)));
|
|
115
|
+
yield `\x00EXIT:${exitCode}`;
|
|
116
|
+
}
|
|
117
|
+
// Handle the streaming upload endpoint directly on the http response
|
|
118
|
+
async function handleStreamingUpload(request, response, store) {
|
|
119
|
+
const start = performance.now();
|
|
120
|
+
try {
|
|
121
|
+
const token = getAuthToken(request);
|
|
122
|
+
let project = store.findProjectByToken(token);
|
|
123
|
+
const form = await parseMultipart(request);
|
|
124
|
+
if (!project) {
|
|
125
|
+
const globalToken = store.getGlobalDeployToken();
|
|
126
|
+
if (!globalToken || token !== globalToken) {
|
|
127
|
+
if (form.filePath)
|
|
128
|
+
await fsp.rm(form.filePath, { force: true });
|
|
129
|
+
response.writeHead(403, { 'Content-Type': 'application/json' });
|
|
130
|
+
response.end(JSON.stringify({ error: 'Invalid Token' }));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
project = store.findProjectById(form.fields.projectId);
|
|
134
|
+
if (!project) {
|
|
135
|
+
if (form.filePath)
|
|
136
|
+
await fsp.rm(form.filePath, { force: true });
|
|
137
|
+
response.writeHead(404, { 'Content-Type': 'application/json' });
|
|
138
|
+
response.end(JSON.stringify({ error: 'Project not found' }));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (form.fields.projectId !== project.id) {
|
|
143
|
+
if (form.filePath)
|
|
144
|
+
await fsp.rm(form.filePath, { force: true });
|
|
145
|
+
response.writeHead(403, { 'Content-Type': 'application/json' });
|
|
146
|
+
response.end(JSON.stringify({ error: 'Project ID mismatch' }));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!form.filePath) {
|
|
150
|
+
response.writeHead(400, { 'Content-Type': 'application/json' });
|
|
151
|
+
response.end(JSON.stringify({ error: 'Missing upload file' }));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const preDeployCmd = form.fields.preDeploy || project.preDeployScript;
|
|
155
|
+
const postDeployCmd = form.fields.postDeploy || project.postDeployScript;
|
|
156
|
+
const deployLog = store.createDeployment({
|
|
157
|
+
projectId: project.id,
|
|
158
|
+
projectName: project.name,
|
|
159
|
+
status: 'running',
|
|
160
|
+
triggerSource: 'cli',
|
|
161
|
+
startTime: new Date().toISOString(),
|
|
162
|
+
output: ''
|
|
163
|
+
});
|
|
164
|
+
let output = '';
|
|
165
|
+
const appendLog = (line) => {
|
|
166
|
+
output += `${line}\n`;
|
|
167
|
+
store.updateDeployment(deployLog.id, { output });
|
|
168
|
+
broadcastToSubscribers(deployLog.id, 'log', JSON.stringify(line));
|
|
169
|
+
};
|
|
170
|
+
// Start streaming response
|
|
171
|
+
response.writeHead(200, { 'Content-Type': 'application/x-ndjson', 'Transfer-Encoding': 'chunked' });
|
|
172
|
+
const sendEvent = (event, data) => {
|
|
173
|
+
response.write(JSON.stringify({ event, ...data }) + '\n');
|
|
174
|
+
};
|
|
175
|
+
const startedAt = Date.now();
|
|
176
|
+
store.updateProject(project.id, { status: 'running' });
|
|
177
|
+
try {
|
|
178
|
+
await fsp.mkdir(project.deployPath, { recursive: true });
|
|
179
|
+
sendEvent('log', { data: `[Kite Deploy] Starting deployment for ${project.name}...` });
|
|
180
|
+
appendLog(`[Kite Deploy] Starting deployment for ${project.name}...`);
|
|
181
|
+
sendEvent('log', { data: `[Kite Deploy] Target deploy path: ${project.deployPath}` });
|
|
182
|
+
appendLog(`[Kite Deploy] Target deploy path: ${project.deployPath}`);
|
|
183
|
+
if (preDeployCmd) {
|
|
184
|
+
sendEvent('log', { data: `[Kite Deploy] Running Pre-deploy: ${preDeployCmd}` });
|
|
185
|
+
appendLog(`[Kite Deploy] Running Pre-deploy: ${preDeployCmd}`);
|
|
186
|
+
let failed = false;
|
|
187
|
+
for await (const line of runShellCommand(preDeployCmd, project.deployPath)) {
|
|
188
|
+
if (line.startsWith('\x00EXIT:')) {
|
|
189
|
+
if (parseInt(line.slice(6)) !== 0)
|
|
190
|
+
failed = true;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
sendEvent('log', { data: line });
|
|
194
|
+
appendLog(line);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (failed)
|
|
198
|
+
throw new Error('Pre-deploy failed');
|
|
199
|
+
}
|
|
200
|
+
sendEvent('log', { data: '[Kite Deploy] Extracting files...' });
|
|
201
|
+
appendLog('[Kite Deploy] Extracting files...');
|
|
202
|
+
new AdmZip(form.filePath).extractAllTo(project.deployPath, true);
|
|
203
|
+
if (postDeployCmd) {
|
|
204
|
+
sendEvent('log', { data: `[Kite Deploy] Running Post-deploy: ${postDeployCmd}` });
|
|
205
|
+
appendLog(`[Kite Deploy] Running Post-deploy: ${postDeployCmd}`);
|
|
206
|
+
let failed = false;
|
|
207
|
+
for await (const line of runShellCommand(postDeployCmd, project.deployPath)) {
|
|
208
|
+
if (line.startsWith('\x00EXIT:')) {
|
|
209
|
+
if (parseInt(line.slice(6)) !== 0)
|
|
210
|
+
failed = true;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
sendEvent('log', { data: line });
|
|
214
|
+
appendLog(line);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (failed)
|
|
218
|
+
throw new Error('Post-deploy failed');
|
|
219
|
+
}
|
|
220
|
+
const duration = `${((Date.now() - startedAt) / 1000).toFixed(1)}s`;
|
|
221
|
+
const successMsg = `[Kite Deploy] Deployment completed successfully in ${duration}.`;
|
|
222
|
+
sendEvent('log', { data: successMsg });
|
|
223
|
+
appendLog(successMsg);
|
|
224
|
+
store.updateDeployment(deployLog.id, { status: 'success', duration, endTime: new Date().toISOString() });
|
|
225
|
+
store.updateProject(project.id, { status: 'success' });
|
|
226
|
+
sendEvent('status', { status: 'success', duration, deployId: deployLog.id });
|
|
227
|
+
broadcastToSubscribers(deployLog.id, 'status', JSON.stringify({ status: 'success', duration }));
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const duration = `${((Date.now() - startedAt) / 1000).toFixed(1)}s`;
|
|
231
|
+
const failMsg = `[Kite Deploy] Deployment failed: ${error.message}`;
|
|
232
|
+
sendEvent('log', { data: failMsg });
|
|
233
|
+
appendLog(failMsg);
|
|
234
|
+
store.updateDeployment(deployLog.id, { status: 'failed', duration, endTime: new Date().toISOString() });
|
|
235
|
+
store.updateProject(project.id, { status: 'failed' });
|
|
236
|
+
sendEvent('status', { status: 'failed', duration, deployId: deployLog.id });
|
|
237
|
+
broadcastToSubscribers(deployLog.id, 'status', JSON.stringify({ status: 'failed', duration }));
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
if (form.filePath)
|
|
241
|
+
await fsp.rm(form.filePath, { force: true });
|
|
242
|
+
response.end();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
console.error('[Deploy] Error:', error);
|
|
247
|
+
if (!response.headersSent) {
|
|
248
|
+
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
249
|
+
}
|
|
250
|
+
response.end(JSON.stringify({ error: error.message }));
|
|
251
|
+
}
|
|
252
|
+
const ms = (performance.now() - start).toFixed(0);
|
|
253
|
+
console.log(`POST /api/deploy/upload 200 ${ms}ms`);
|
|
254
|
+
}
|
|
255
|
+
const serveStatic = async (url) => {
|
|
256
|
+
const requested = url.pathname === '/' ? '/index.html' : url.pathname;
|
|
257
|
+
const decoded = decodeURIComponent(requested);
|
|
258
|
+
const filePath = path.resolve(webRoot, decoded.replace(/^\/+/, ''));
|
|
259
|
+
if (!filePath.startsWith(webRoot)) {
|
|
260
|
+
return text(403, 'Forbidden');
|
|
261
|
+
}
|
|
262
|
+
const fallback = path.join(webRoot, 'index.html');
|
|
263
|
+
const target = fs.existsSync(filePath) && fs.statSync(filePath).isFile() ? filePath : fallback;
|
|
264
|
+
if (!fs.existsSync(target)) {
|
|
265
|
+
return text(404, 'Kite Web assets not found. Run `bun run build` before packaging the CLI.');
|
|
266
|
+
}
|
|
267
|
+
const ext = path.extname(target);
|
|
268
|
+
const contentTypes = {
|
|
269
|
+
'.html': 'text/html; charset=utf-8',
|
|
270
|
+
'.css': 'text/css; charset=utf-8',
|
|
271
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
272
|
+
'.svg': 'image/svg+xml',
|
|
273
|
+
'.json': 'application/json; charset=utf-8'
|
|
274
|
+
};
|
|
275
|
+
return {
|
|
276
|
+
status: 200,
|
|
277
|
+
headers: { 'Content-Type': contentTypes[ext] || 'application/octet-stream' },
|
|
278
|
+
body: await fsp.readFile(target)
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
export async function startLocalServer(options = {}) {
|
|
282
|
+
const runtime = options.runtime || 'auto';
|
|
283
|
+
const host = options.host || '127.0.0.1';
|
|
284
|
+
const port = Number(options.port || process.env.PORT || 3000);
|
|
285
|
+
const store = new LocalStore();
|
|
286
|
+
const serverUrl = `http://${host}:${port}`;
|
|
287
|
+
if (runtime !== 'auto' && runtime !== 'node' && runtime !== 'bun') {
|
|
288
|
+
throw new Error(`Unsupported runtime: ${runtime}`);
|
|
289
|
+
}
|
|
290
|
+
const handleApi = async (request, url) => {
|
|
291
|
+
if (request.method === 'POST' && url.pathname === '/api/auth/login') {
|
|
292
|
+
const body = JSON.parse(await readBody(request) || '{}');
|
|
293
|
+
if (body.token === store.getAdminToken()) {
|
|
294
|
+
return json(200, { success: true, message: 'Login successful' });
|
|
295
|
+
}
|
|
296
|
+
return json(401, { success: false, message: 'Invalid token' });
|
|
297
|
+
}
|
|
298
|
+
const adminToken = getAuthToken(request);
|
|
299
|
+
if (adminToken !== store.getAdminToken()) {
|
|
300
|
+
return json(401, { error: 'Unauthorized' });
|
|
301
|
+
}
|
|
302
|
+
if (url.pathname === '/api/projects' && request.method === 'GET') {
|
|
303
|
+
return json(200, store.findProjects());
|
|
304
|
+
}
|
|
305
|
+
if (url.pathname === '/api/projects' && request.method === 'POST') {
|
|
306
|
+
const body = JSON.parse(await readBody(request) || '{}');
|
|
307
|
+
return json(200, { success: true, project: store.createProject(body) });
|
|
308
|
+
}
|
|
309
|
+
const projectMatch = url.pathname.match(/^\/api\/projects\/([^/]+)$/);
|
|
310
|
+
if (projectMatch?.[1] && request.method === 'GET') {
|
|
311
|
+
const project = store.findProjectById(projectMatch[1]);
|
|
312
|
+
return project ? json(200, project) : json(404, { error: 'Project not found' });
|
|
313
|
+
}
|
|
314
|
+
if (projectMatch?.[1] && request.method === 'PUT') {
|
|
315
|
+
const body = JSON.parse(await readBody(request) || '{}');
|
|
316
|
+
const project = store.updateProject(projectMatch[1], body);
|
|
317
|
+
return project ? json(200, { success: true, project }) : json(404, { error: 'Project not found' });
|
|
318
|
+
}
|
|
319
|
+
if (projectMatch?.[1] && request.method === 'DELETE') {
|
|
320
|
+
const success = store.removeProject(projectMatch[1]);
|
|
321
|
+
return success ? json(200, { success: true }) : json(404, { error: 'Project not found' });
|
|
322
|
+
}
|
|
323
|
+
const tokenMatch = url.pathname.match(/^\/api\/projects\/([^/]+)\/token$/);
|
|
324
|
+
if (tokenMatch?.[1] && request.method === 'POST') {
|
|
325
|
+
const project = store.updateProject(tokenMatch[1], { token: randomToken('kt') });
|
|
326
|
+
return project ? json(200, { success: true, token: project.token }) : json(404, { error: 'Project not found' });
|
|
327
|
+
}
|
|
328
|
+
if (url.pathname === '/api/logs' && request.method === 'GET') {
|
|
329
|
+
return json(200, store.findDeployments());
|
|
330
|
+
}
|
|
331
|
+
const logMatch = url.pathname.match(/^\/api\/logs\/([^/]+)$/);
|
|
332
|
+
if (logMatch?.[1] && request.method === 'GET') {
|
|
333
|
+
const log = store.findDeploymentById(logMatch[1]);
|
|
334
|
+
return log ? json(200, log) : json(404, { error: 'Deployment log not found' });
|
|
335
|
+
}
|
|
336
|
+
// Settings routes
|
|
337
|
+
if (url.pathname === '/api/settings' && request.method === 'GET') {
|
|
338
|
+
return json(200, {
|
|
339
|
+
global_deploy_token: store.getGlobalDeployToken(),
|
|
340
|
+
default_deploy_path: '.deployments',
|
|
341
|
+
max_upload_size: '50',
|
|
342
|
+
webhook_url: '',
|
|
343
|
+
webhook_events: 'deploy_success,deploy_failure'
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (url.pathname === '/api/settings' && request.method === 'PUT') {
|
|
347
|
+
const body = JSON.parse(await readBody(request) || '{}');
|
|
348
|
+
if (body.global_deploy_token !== undefined) {
|
|
349
|
+
store.updateGlobalDeployToken(String(body.global_deploy_token));
|
|
350
|
+
}
|
|
351
|
+
return json(200, { success: true, message: 'Settings updated' });
|
|
352
|
+
}
|
|
353
|
+
if (url.pathname === '/api/settings/status' && request.method === 'GET') {
|
|
354
|
+
const projects = store.findProjects();
|
|
355
|
+
const deployments = store.findDeployments();
|
|
356
|
+
const successCount = deployments.filter(d => d.status === 'success').length;
|
|
357
|
+
const failedCount = deployments.filter(d => d.status === 'failed').length;
|
|
358
|
+
return json(200, {
|
|
359
|
+
version: '1.0.0',
|
|
360
|
+
uptime: '-',
|
|
361
|
+
projectCount: projects.length,
|
|
362
|
+
deploymentCount: deployments.length,
|
|
363
|
+
successCount,
|
|
364
|
+
failedCount,
|
|
365
|
+
successRate: deployments.length > 0 ? Math.round((successCount / deployments.length) * 100) : 0,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return json(404, { error: 'Not found' });
|
|
369
|
+
};
|
|
370
|
+
const server = http.createServer(async (request, response) => {
|
|
371
|
+
const start = performance.now();
|
|
372
|
+
try {
|
|
373
|
+
const url = new URL(request.url || '/', serverUrl);
|
|
374
|
+
// Handle SSE stream endpoint
|
|
375
|
+
const sseMatch = url.pathname.match(/^\/api\/logs\/([^/]+)\/stream$/);
|
|
376
|
+
if (sseMatch?.[1] && request.method === 'GET') {
|
|
377
|
+
const adminToken = getAuthToken(request);
|
|
378
|
+
if (adminToken !== store.getAdminToken()) {
|
|
379
|
+
response.writeHead(401, { 'Content-Type': 'application/json' });
|
|
380
|
+
response.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const deployId = sseMatch[1];
|
|
384
|
+
const log = store.findDeploymentById(deployId);
|
|
385
|
+
if (!log) {
|
|
386
|
+
response.writeHead(404, { 'Content-Type': 'application/json' });
|
|
387
|
+
response.end(JSON.stringify({ error: 'Deployment log not found' }));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
response.writeHead(200, {
|
|
391
|
+
'Content-Type': 'text/event-stream',
|
|
392
|
+
'Cache-Control': 'no-cache',
|
|
393
|
+
'Connection': 'keep-alive',
|
|
394
|
+
});
|
|
395
|
+
if (log.output) {
|
|
396
|
+
response.write(`event: log\ndata: ${JSON.stringify(log.output)}\n\n`);
|
|
397
|
+
}
|
|
398
|
+
if (log.status !== 'running') {
|
|
399
|
+
response.write(`event: status\ndata: ${JSON.stringify({ status: log.status, duration: log.duration })}\n\n`);
|
|
400
|
+
response.end();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (!deploySubscribers.has(deployId))
|
|
404
|
+
deploySubscribers.set(deployId, new Set());
|
|
405
|
+
deploySubscribers.get(deployId).add(response);
|
|
406
|
+
request.on('close', () => { deploySubscribers.get(deployId)?.delete(response); });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
// Handle streaming upload endpoint
|
|
410
|
+
if (url.pathname === '/api/deploy/upload' && request.method === 'POST') {
|
|
411
|
+
await handleStreamingUpload(request, response, store);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const result = await handleApi(request, url);
|
|
415
|
+
response.writeHead(result.status, result.headers);
|
|
416
|
+
response.end(result.body);
|
|
417
|
+
const ms = (performance.now() - start).toFixed(0);
|
|
418
|
+
console.log(`${request.method} ${url.pathname} ${result.status} ${ms}ms`);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
response.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
422
|
+
response.end(JSON.stringify({ error: error.message }));
|
|
423
|
+
const ms = (performance.now() - start).toFixed(0);
|
|
424
|
+
console.log(`${request.method} ${request.url} 500 ${ms}ms`);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
await new Promise((resolve) => server.listen(port, host, resolve));
|
|
428
|
+
setGlobalConfig('serverUrl', serverUrl);
|
|
429
|
+
console.log(`Kite is running at ${serverUrl}`);
|
|
430
|
+
console.log(`Web console: ${serverUrl}`);
|
|
431
|
+
console.log(`Admin token: ${store.getAdminToken()}`);
|
|
432
|
+
console.log(`Data home: ${store.home}`);
|
|
433
|
+
console.log(`Runtime: ${runtime === 'auto' ? `auto (${process.versions.bun ? 'bun' : 'node'})` : runtime}`);
|
|
434
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { ensureKiteHome, randomToken } from './home.js';
|
|
5
|
+
const createDefaultDb = () => {
|
|
6
|
+
const now = new Date().toISOString();
|
|
7
|
+
const home = ensureKiteHome();
|
|
8
|
+
return {
|
|
9
|
+
adminToken: randomToken('admin'),
|
|
10
|
+
projects: [
|
|
11
|
+
{
|
|
12
|
+
id: 'proj_abc123',
|
|
13
|
+
name: 'Kite Demo Project',
|
|
14
|
+
description: 'CLI 内置服务的演示项目,可直接配合 test-token 测试上传部署。',
|
|
15
|
+
deployPath: path.join(home, 'deployments', 'proj_abc123'),
|
|
16
|
+
token: 'test-token',
|
|
17
|
+
preDeployScript: '',
|
|
18
|
+
postDeployScript: 'echo "demo deployment finished"',
|
|
19
|
+
status: 'idle',
|
|
20
|
+
createdAt: now,
|
|
21
|
+
updatedAt: now
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
deployments: []
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export class LocalStore {
|
|
28
|
+
dbPath;
|
|
29
|
+
constructor() {
|
|
30
|
+
this.dbPath = path.join(ensureKiteHome(), 'kite.db.json');
|
|
31
|
+
if (!fs.existsSync(this.dbPath)) {
|
|
32
|
+
this.write(createDefaultDb());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
get path() {
|
|
36
|
+
return this.dbPath;
|
|
37
|
+
}
|
|
38
|
+
get home() {
|
|
39
|
+
return ensureKiteHome();
|
|
40
|
+
}
|
|
41
|
+
read() {
|
|
42
|
+
return JSON.parse(fs.readFileSync(this.dbPath, 'utf-8'));
|
|
43
|
+
}
|
|
44
|
+
write(db) {
|
|
45
|
+
fs.writeFileSync(this.dbPath, `${JSON.stringify(db, null, 2)}\n`);
|
|
46
|
+
}
|
|
47
|
+
getAdminToken() {
|
|
48
|
+
return this.read().adminToken;
|
|
49
|
+
}
|
|
50
|
+
updateAdminToken(adminToken) {
|
|
51
|
+
const db = this.read();
|
|
52
|
+
db.adminToken = adminToken;
|
|
53
|
+
this.write(db);
|
|
54
|
+
return adminToken;
|
|
55
|
+
}
|
|
56
|
+
getGlobalDeployToken() {
|
|
57
|
+
return this.read().globalDeployToken || '';
|
|
58
|
+
}
|
|
59
|
+
updateGlobalDeployToken(token) {
|
|
60
|
+
const db = this.read();
|
|
61
|
+
db.globalDeployToken = token;
|
|
62
|
+
this.write(db);
|
|
63
|
+
return token;
|
|
64
|
+
}
|
|
65
|
+
findProjects() {
|
|
66
|
+
return this.read().projects;
|
|
67
|
+
}
|
|
68
|
+
findProjectById(id) {
|
|
69
|
+
return this.read().projects.find(project => project.id === id) || null;
|
|
70
|
+
}
|
|
71
|
+
findProjectByToken(token) {
|
|
72
|
+
return this.read().projects.find(project => project.token === token) || null;
|
|
73
|
+
}
|
|
74
|
+
createProject(data) {
|
|
75
|
+
const db = this.read();
|
|
76
|
+
const now = new Date().toISOString();
|
|
77
|
+
const project = {
|
|
78
|
+
id: `proj_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
79
|
+
name: data.name,
|
|
80
|
+
description: data.description,
|
|
81
|
+
deployPath: data.deployPath,
|
|
82
|
+
token: randomToken('kt'),
|
|
83
|
+
preDeployScript: data.preDeployScript || '',
|
|
84
|
+
postDeployScript: data.postDeployScript || '',
|
|
85
|
+
status: 'idle',
|
|
86
|
+
createdAt: now,
|
|
87
|
+
updatedAt: now
|
|
88
|
+
};
|
|
89
|
+
db.projects.push(project);
|
|
90
|
+
this.write(db);
|
|
91
|
+
return project;
|
|
92
|
+
}
|
|
93
|
+
updateProject(id, data) {
|
|
94
|
+
const db = this.read();
|
|
95
|
+
const index = db.projects.findIndex(project => project.id === id);
|
|
96
|
+
if (index < 0)
|
|
97
|
+
return null;
|
|
98
|
+
db.projects[index] = {
|
|
99
|
+
...db.projects[index],
|
|
100
|
+
...data,
|
|
101
|
+
updatedAt: new Date().toISOString()
|
|
102
|
+
};
|
|
103
|
+
this.write(db);
|
|
104
|
+
return db.projects[index];
|
|
105
|
+
}
|
|
106
|
+
removeProject(id) {
|
|
107
|
+
const db = this.read();
|
|
108
|
+
const before = db.projects.length;
|
|
109
|
+
db.projects = db.projects.filter(project => project.id !== id);
|
|
110
|
+
db.deployments = db.deployments.filter(log => log.projectId !== id);
|
|
111
|
+
this.write(db);
|
|
112
|
+
return db.projects.length !== before;
|
|
113
|
+
}
|
|
114
|
+
findDeployments() {
|
|
115
|
+
return this.read().deployments.sort((a, b) => b.startTime.localeCompare(a.startTime));
|
|
116
|
+
}
|
|
117
|
+
findDeploymentById(id) {
|
|
118
|
+
return this.read().deployments.find(log => log.id === id) || null;
|
|
119
|
+
}
|
|
120
|
+
createDeployment(data) {
|
|
121
|
+
const db = this.read();
|
|
122
|
+
const log = {
|
|
123
|
+
...data,
|
|
124
|
+
id: crypto.randomUUID()
|
|
125
|
+
};
|
|
126
|
+
db.deployments.push(log);
|
|
127
|
+
this.write(db);
|
|
128
|
+
return log;
|
|
129
|
+
}
|
|
130
|
+
updateDeployment(id, data) {
|
|
131
|
+
const db = this.read();
|
|
132
|
+
const index = db.deployments.findIndex(log => log.id === id);
|
|
133
|
+
if (index < 0)
|
|
134
|
+
return null;
|
|
135
|
+
db.deployments[index] = {
|
|
136
|
+
...db.deployments[index],
|
|
137
|
+
...data
|
|
138
|
+
};
|
|
139
|
+
this.write(db);
|
|
140
|
+
return db.deployments[index];
|
|
141
|
+
}
|
|
142
|
+
}
|