@gravito/launchpad 1.0.0-beta.1 → 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/dist/index.mjs ADDED
@@ -0,0 +1,785 @@
1
+ // src/index.ts
2
+ import { PlanetCore, ServiceProvider } from "@gravito/core";
3
+ import { OrbitRipple } from "@gravito/ripple";
4
+ import { OrbitCache } from "@gravito/stasis";
5
+
6
+ // src/Application/MissionControl.ts
7
+ var MissionControl = class {
8
+ constructor(poolManager, injector, docker, router) {
9
+ this.poolManager = poolManager;
10
+ this.injector = injector;
11
+ this.docker = docker;
12
+ this.router = router;
13
+ }
14
+ async launch(mission, onTelemetry) {
15
+ console.log(`[MissionControl] \u6E96\u5099\u767C\u5C04\u4EFB\u52D9: ${mission.id}`);
16
+ const rocket = await this.poolManager.assignMission(mission);
17
+ await this.injector.deploy(rocket);
18
+ const publicPort = await this.docker.getExposedPort(rocket.containerId, 3e3);
19
+ console.log(`[MissionControl] \u4EFB\u52D9 ${mission.id} \u6620\u5C04\u7AEF\u53E3: ${publicPort}`);
20
+ const domain = `${mission.id}.dev.local`.toLowerCase();
21
+ rocket.assignDomain(domain);
22
+ if (this.router) {
23
+ this.router.register(domain, `http://localhost:${publicPort}`);
24
+ }
25
+ this.docker.streamLogs(rocket.containerId, (log) => {
26
+ onTelemetry("log", { rocketId: rocket.id, text: log });
27
+ });
28
+ const statsTimer = setInterval(async () => {
29
+ if (rocket.status === "DECOMMISSIONED" || rocket.status === "IDLE") {
30
+ clearInterval(statsTimer);
31
+ return;
32
+ }
33
+ const stats = await this.docker.getStats(rocket.containerId);
34
+ onTelemetry("stats", { rocketId: rocket.id, ...stats });
35
+ }, 5e3);
36
+ const ttl = 10 * 60 * 1e3;
37
+ setTimeout(async () => {
38
+ console.log(`[MissionControl] \u4EFB\u52D9 ${mission.id} TTL \u5DF2\u5230\u671F\uFF0C\u57F7\u884C\u81EA\u52D5\u56DE\u6536...`);
39
+ clearInterval(statsTimer);
40
+ await this.poolManager.recycle(mission.id);
41
+ onTelemetry("log", { rocketId: rocket.id, text: "--- MISSION EXPIRED (TTL) ---" });
42
+ }, ttl);
43
+ return rocket.id;
44
+ }
45
+ };
46
+
47
+ // src/Application/PayloadInjector.ts
48
+ var PayloadInjector = class {
49
+ constructor(docker, git) {
50
+ this.docker = docker;
51
+ this.git = git;
52
+ }
53
+ async deploy(rocket) {
54
+ if (!rocket.currentMission) {
55
+ throw new Error(`Rocket ${rocket.id} \u6C92\u6709\u6307\u6D3E\u4EFB\u52D9\uFF0C\u7121\u6CD5\u90E8\u7F72`);
56
+ }
57
+ const { repoUrl, branch } = rocket.currentMission;
58
+ const containerId = rocket.containerId;
59
+ console.log(`[PayloadInjector] \u6B63\u5728\u62C9\u53D6\u4EE3\u78BC: ${repoUrl} (${branch})`);
60
+ const codePath = await this.git.clone(repoUrl, branch);
61
+ console.log(`[PayloadInjector] \u6B63\u5728\u6CE8\u5165\u8F09\u8377\u81F3\u5BB9\u5668: ${containerId}`);
62
+ await this.docker.copyFiles(containerId, codePath, "/app");
63
+ console.log(`[PayloadInjector] \u6B63\u5728\u5B89\u88DD\u4F9D\u8CF4...`);
64
+ const registry = process.env.LAUNCHPAD_NPM_REGISTRY || process.env.NPM_CONFIG_REGISTRY || "";
65
+ const baseInstallConfig = ["[install]", "frozenLockfile = false"];
66
+ const bunfigContent = registry ? `${baseInstallConfig.join("\n")}
67
+ registry = "${registry}"
68
+ ` : `${baseInstallConfig.join("\n")}
69
+ `;
70
+ await this.docker.executeCommand(containerId, [
71
+ "sh",
72
+ "-c",
73
+ `echo "${bunfigContent}" > /app/bunfig.toml`
74
+ ]);
75
+ await this.docker.executeCommand(containerId, ["rm", "-f", "/app/bun.lockb"]);
76
+ let installRes = await this.docker.executeCommand(containerId, [
77
+ "bun",
78
+ "install",
79
+ "--cwd",
80
+ "/app",
81
+ "--no-save",
82
+ "--ignore-scripts"
83
+ ]);
84
+ if (installRes.exitCode !== 0 && !registry) {
85
+ const fallbackRegistry = "https://registry.npmmirror.com";
86
+ const fallbackBunfig = `${baseInstallConfig.join("\n")}
87
+ registry = "${fallbackRegistry}"
88
+ `;
89
+ await this.docker.executeCommand(containerId, [
90
+ "sh",
91
+ "-c",
92
+ `echo "${fallbackBunfig}" > /app/bunfig.toml`
93
+ ]);
94
+ installRes = await this.docker.executeCommand(containerId, [
95
+ "bun",
96
+ "install",
97
+ "--cwd",
98
+ "/app",
99
+ "--no-save",
100
+ "--ignore-scripts"
101
+ ]);
102
+ }
103
+ if (installRes.exitCode !== 0) {
104
+ throw new Error(`\u5B89\u88DD\u4F9D\u8CF4\u5931\u6557: ${installRes.stderr}`);
105
+ }
106
+ console.log(`[PayloadInjector] \u9EDE\u706B\uFF01`);
107
+ this.docker.executeCommand(containerId, ["bun", "run", "/app/examples/demo.ts"]).catch((e) => {
108
+ console.error(`[PayloadInjector] \u904B\u884C\u7570\u5E38:`, e);
109
+ });
110
+ rocket.ignite();
111
+ }
112
+ };
113
+
114
+ // src/Domain/Rocket.ts
115
+ import { AggregateRoot } from "@gravito/enterprise";
116
+
117
+ // src/Domain/Events.ts
118
+ import { DomainEvent } from "@gravito/enterprise";
119
+ var MissionAssigned = class extends DomainEvent {
120
+ constructor(rocketId, missionId) {
121
+ super();
122
+ this.rocketId = rocketId;
123
+ this.missionId = missionId;
124
+ }
125
+ };
126
+ var RocketIgnited = class extends DomainEvent {
127
+ constructor(rocketId) {
128
+ super();
129
+ this.rocketId = rocketId;
130
+ }
131
+ };
132
+ var RocketSplashedDown = class extends DomainEvent {
133
+ constructor(rocketId) {
134
+ super();
135
+ this.rocketId = rocketId;
136
+ }
137
+ };
138
+ var RefurbishmentCompleted = class extends DomainEvent {
139
+ constructor(rocketId) {
140
+ super();
141
+ this.rocketId = rocketId;
142
+ }
143
+ };
144
+
145
+ // src/Domain/RocketStatus.ts
146
+ var RocketStatus = /* @__PURE__ */ ((RocketStatus2) => {
147
+ RocketStatus2["IDLE"] = "IDLE";
148
+ RocketStatus2["PREPARING"] = "PREPARING";
149
+ RocketStatus2["ORBITING"] = "ORBITING";
150
+ RocketStatus2["REFURBISHING"] = "REFURBISHING";
151
+ RocketStatus2["DECOMMISSIONED"] = "DECOMMISSIONED";
152
+ return RocketStatus2;
153
+ })(RocketStatus || {});
154
+
155
+ // src/Domain/Rocket.ts
156
+ var Rocket = class _Rocket extends AggregateRoot {
157
+ _status = "IDLE" /* IDLE */;
158
+ _currentMission = null;
159
+ _containerId;
160
+ _assignedDomain = null;
161
+ constructor(id, containerId) {
162
+ super(id);
163
+ this._containerId = containerId;
164
+ }
165
+ get status() {
166
+ return this._status;
167
+ }
168
+ get currentMission() {
169
+ return this._currentMission;
170
+ }
171
+ get containerId() {
172
+ return this._containerId;
173
+ }
174
+ get assignedDomain() {
175
+ return this._assignedDomain;
176
+ }
177
+ /**
178
+ * 分配域名
179
+ */
180
+ assignDomain(domain) {
181
+ this._assignedDomain = domain;
182
+ }
183
+ /**
184
+ * 分配任務 (指派任務給 IDLE 的火箭)
185
+ */
186
+ assignMission(mission) {
187
+ if (this._status !== "IDLE" /* IDLE */) {
188
+ throw new Error(`\u7121\u6CD5\u6307\u6D3E\u4EFB\u52D9\uFF1A\u706B\u7BAD ${this.id} \u72C0\u614B\u70BA ${this._status}\uFF0C\u975E IDLE`);
189
+ }
190
+ this._status = "PREPARING" /* PREPARING */;
191
+ this._currentMission = mission;
192
+ this.addDomainEvent(new MissionAssigned(this.id, mission.id));
193
+ }
194
+ /**
195
+ * 點火啟動 (應用程式啟動成功)
196
+ */
197
+ ignite() {
198
+ if (this._status !== "PREPARING" /* PREPARING */) {
199
+ throw new Error(`\u7121\u6CD5\u9EDE\u706B\uFF1A\u706B\u7BAD ${this.id} \u5C1A\u672A\u9032\u5165 PREPARING \u72C0\u614B`);
200
+ }
201
+ this._status = "ORBITING" /* ORBITING */;
202
+ this.addDomainEvent(new RocketIgnited(this.id));
203
+ }
204
+ /**
205
+ * 任務降落 (PR 合併或關閉,暫停服務)
206
+ */
207
+ splashDown() {
208
+ if (this._status !== "ORBITING" /* ORBITING */) {
209
+ throw new Error(`\u7121\u6CD5\u964D\u843D\uFF1A\u706B\u7BAD ${this.id} \u4E0D\u5728\u904B\u884C\u8ECC\u9053\u4E0A`);
210
+ }
211
+ this._status = "REFURBISHING" /* REFURBISHING */;
212
+ this.addDomainEvent(new RocketSplashedDown(this.id));
213
+ }
214
+ /**
215
+ * 翻新完成 (清理完畢,回歸池中)
216
+ */
217
+ finishRefurbishment() {
218
+ if (this._status !== "REFURBISHING" /* REFURBISHING */) {
219
+ throw new Error(`\u7121\u6CD5\u5B8C\u6210\u7FFB\u65B0\uFF1A\u706B\u7BAD ${this.id} \u4E0D\u5728 REFURBISHING \u72C0\u614B`);
220
+ }
221
+ this._status = "IDLE" /* IDLE */;
222
+ this._currentMission = null;
223
+ this.addDomainEvent(new RefurbishmentCompleted(this.id));
224
+ }
225
+ /**
226
+ * 火箭除役 (移除容器)
227
+ */
228
+ decommission() {
229
+ this._status = "DECOMMISSIONED" /* DECOMMISSIONED */;
230
+ }
231
+ toJSON() {
232
+ return {
233
+ id: this.id,
234
+ containerId: this.containerId,
235
+ status: this._status,
236
+ currentMission: this._currentMission,
237
+ assignedDomain: this._assignedDomain
238
+ };
239
+ }
240
+ static fromJSON(data) {
241
+ const rocket = new _Rocket(data.id, data.containerId);
242
+ rocket._status = data.status;
243
+ rocket._currentMission = data.currentMission;
244
+ rocket._assignedDomain = data.assignedDomain;
245
+ return rocket;
246
+ }
247
+ };
248
+
249
+ // src/Application/PoolManager.ts
250
+ var PoolManager = class {
251
+ constructor(dockerAdapter, rocketRepository, refurbishUnit, router) {
252
+ this.dockerAdapter = dockerAdapter;
253
+ this.rocketRepository = rocketRepository;
254
+ this.refurbishUnit = refurbishUnit;
255
+ this.router = router;
256
+ }
257
+ /**
258
+ * 初始化發射場:預先準備指定數量的火箭
259
+ */
260
+ async warmup(count) {
261
+ const currentRockets = await this.rocketRepository.findAll();
262
+ const needed = count - currentRockets.length;
263
+ if (needed <= 0) {
264
+ return;
265
+ }
266
+ console.log(`[LaunchPad] \u6B63\u5728\u71B1\u6A5F\uFF0C\u6E96\u5099\u767C\u5C04 ${needed} \u67B6\u65B0\u706B\u7BAD...`);
267
+ for (let i = 0; i < needed; i++) {
268
+ const containerId = await this.dockerAdapter.createBaseContainer();
269
+ const rocketId = `rocket-${Math.random().toString(36).substring(2, 9)}`;
270
+ const rocket = new Rocket(rocketId, containerId);
271
+ await this.rocketRepository.save(rocket);
272
+ }
273
+ }
274
+ /**
275
+ * 獲取一架可用的火箭並分配任務
276
+ */
277
+ async assignMission(mission) {
278
+ let rocket = await this.rocketRepository.findIdle();
279
+ if (!rocket) {
280
+ console.log(`[LaunchPad] \u8CC7\u6E90\u5403\u7DCA\uFF0C\u6B63\u5728\u7DCA\u6025\u547C\u53EB\u5F8C\u63F4\u706B\u7BAD...`);
281
+ const containerId = await this.dockerAdapter.createBaseContainer();
282
+ rocket = new Rocket(`rocket-dynamic-${Date.now()}`, containerId);
283
+ }
284
+ rocket.assignMission(mission);
285
+ await this.rocketRepository.save(rocket);
286
+ return rocket;
287
+ }
288
+ /**
289
+ * 回收指定任務的火箭
290
+ */
291
+ async recycle(missionId) {
292
+ const allRockets = await this.rocketRepository.findAll();
293
+ const rocket = allRockets.find((r) => r.currentMission?.id === missionId);
294
+ if (!rocket) {
295
+ console.warn(`[LaunchPad] \u627E\u4E0D\u5230\u5C6C\u65BC\u4EFB\u52D9 ${missionId} \u7684\u706B\u7BAD`);
296
+ return;
297
+ }
298
+ if (this.router && rocket.assignedDomain) {
299
+ this.router.unregister(rocket.assignedDomain);
300
+ }
301
+ if (this.refurbishUnit) {
302
+ await this.refurbishUnit.refurbish(rocket);
303
+ } else {
304
+ rocket.splashDown();
305
+ rocket.finishRefurbishment();
306
+ }
307
+ await this.rocketRepository.save(rocket);
308
+ }
309
+ };
310
+
311
+ // src/Application/RefurbishUnit.ts
312
+ var RefurbishUnit = class {
313
+ constructor(docker) {
314
+ this.docker = docker;
315
+ }
316
+ /**
317
+ * 執行火箭翻新邏輯
318
+ */
319
+ async refurbish(rocket) {
320
+ console.log(`[RefurbishUnit] \u6B63\u5728\u7FFB\u65B0\u706B\u7BAD: ${rocket.id} (\u5BB9\u5668: ${rocket.containerId})`);
321
+ rocket.splashDown();
322
+ try {
323
+ const cleanupCommands = ["sh", "-c", "rm -rf /app/* && pkill -f bun || true && rm -rf /tmp/*"];
324
+ const result = await this.docker.executeCommand(rocket.containerId, cleanupCommands);
325
+ if (result.exitCode !== 0) {
326
+ console.error(`[RefurbishUnit] \u6E05\u7406\u5931\u6557: ${result.stderr}`);
327
+ rocket.decommission();
328
+ return;
329
+ }
330
+ rocket.finishRefurbishment();
331
+ console.log(`[RefurbishUnit] \u706B\u7BAD ${rocket.id} \u7FFB\u65B0\u5B8C\u6210\uFF0C\u5DF2\u9032\u5165 IDLE \u72C0\u614B\u3002`);
332
+ } catch (error) {
333
+ console.error(`[RefurbishUnit] \u56DE\u6536\u904E\u7A0B\u767C\u751F\u7570\u5E38:`, error);
334
+ rocket.decommission();
335
+ }
336
+ }
337
+ };
338
+
339
+ // src/Domain/Mission.ts
340
+ import { ValueObject } from "@gravito/enterprise";
341
+ var Mission = class _Mission extends ValueObject {
342
+ get id() {
343
+ return this.props.id;
344
+ }
345
+ get repoUrl() {
346
+ return this.props.repoUrl;
347
+ }
348
+ get branch() {
349
+ return this.props.branch;
350
+ }
351
+ get commitSha() {
352
+ return this.props.commitSha;
353
+ }
354
+ static create(props) {
355
+ return new _Mission(props);
356
+ }
357
+ };
358
+
359
+ // src/Infrastructure/Docker/DockerAdapter.ts
360
+ import { getRuntimeAdapter } from "@gravito/core";
361
+ var DockerAdapter = class {
362
+ baseImage = "oven/bun:1.0-slim";
363
+ runtime = getRuntimeAdapter();
364
+ async createBaseContainer() {
365
+ const rocketId = `rocket-${Math.random().toString(36).substring(2, 9)}`;
366
+ const proc = this.runtime.spawn([
367
+ "docker",
368
+ "run",
369
+ "-d",
370
+ "--name",
371
+ rocketId,
372
+ "--label",
373
+ "gravito-origin=launchpad",
374
+ "-p",
375
+ "3000",
376
+ // 讓 Docker 分配隨機宿主機埠
377
+ "-v",
378
+ `${process.env.HOME}/.bun/install/cache:/root/.bun/install/cache`,
379
+ "-v",
380
+ `${process.env.HOME}/.bun/install/cache:/home/bun/.bun/install/cache`,
381
+ this.baseImage,
382
+ "tail",
383
+ "-f",
384
+ "/dev/null"
385
+ ]);
386
+ const stdout = await new Response(proc.stdout ?? null).text();
387
+ const containerId = stdout.trim();
388
+ const exitCode = await proc.exited;
389
+ if (containerId.length === 64 && /^[0-9a-f]+$/.test(containerId)) {
390
+ return containerId;
391
+ }
392
+ if (exitCode !== 0) {
393
+ const stderr = await new Response(proc.stderr ?? null).text();
394
+ throw new Error(`Docker \u5BB9\u5668\u5EFA\u7ACB\u5931\u6557: ${stderr || stdout}`);
395
+ }
396
+ return containerId;
397
+ }
398
+ async getExposedPort(containerId, containerPort = 3e3) {
399
+ const proc = this.runtime.spawn(["docker", "port", containerId, containerPort.toString()]);
400
+ const stdout = await new Response(proc.stdout ?? null).text();
401
+ const firstLine = stdout.split("\n")[0] ?? "";
402
+ if (!firstLine) {
403
+ throw new Error(`\u7121\u6CD5\u7372\u53D6\u5BB9\u5668\u6620\u5C04\u7AEF\u53E3: ${stdout}`);
404
+ }
405
+ const match = firstLine.match(/:(\d+)$/);
406
+ if (!match?.[1]) {
407
+ throw new Error(`\u7121\u6CD5\u7372\u53D6\u5BB9\u5668\u6620\u5C04\u7AEF\u53E3: ${stdout}`);
408
+ }
409
+ return Number.parseInt(match[1], 10);
410
+ }
411
+ async copyFiles(containerId, sourcePath, targetPath) {
412
+ const proc = this.runtime.spawn(["docker", "cp", sourcePath, `${containerId}:${targetPath}`]);
413
+ const exitCode = await proc.exited;
414
+ if (exitCode && exitCode !== 0) {
415
+ const stderr = await new Response(proc.stderr ?? null).text();
416
+ throw new Error(stderr || "Docker copy failed");
417
+ }
418
+ }
419
+ async executeCommand(containerId, command) {
420
+ const proc = this.runtime.spawn(["docker", "exec", "-u", "0", containerId, ...command]);
421
+ const stdout = await new Response(proc.stdout ?? null).text();
422
+ const stderr = await new Response(proc.stderr ?? null).text();
423
+ const exitCode = await proc.exited;
424
+ return { stdout, stderr, exitCode };
425
+ }
426
+ async removeContainer(containerId) {
427
+ await this.runtime.spawn(["docker", "rm", "-f", containerId]).exited;
428
+ }
429
+ async removeContainerByLabel(label) {
430
+ const listProc = this.runtime.spawn(["docker", "ps", "-aq", "--filter", `label=${label}`]);
431
+ const ids = await new Response(listProc.stdout ?? null).text();
432
+ if (ids.trim()) {
433
+ const idList = ids.trim().split("\n");
434
+ await this.runtime.spawn(["docker", "rm", "-f", ...idList]).exited;
435
+ }
436
+ }
437
+ streamLogs(containerId, onData) {
438
+ const proc = this.runtime.spawn(["docker", "logs", "-f", containerId], {
439
+ stdout: "pipe",
440
+ stderr: "pipe"
441
+ });
442
+ const read = async (stream) => {
443
+ if (!stream) {
444
+ return;
445
+ }
446
+ const reader = stream.getReader();
447
+ const decoder = new TextDecoder();
448
+ while (true) {
449
+ const { done, value } = await reader.read();
450
+ if (done) {
451
+ break;
452
+ }
453
+ onData(decoder.decode(value));
454
+ }
455
+ };
456
+ read(proc.stdout);
457
+ read(proc.stderr);
458
+ }
459
+ async getStats(containerId) {
460
+ const proc = this.runtime.spawn([
461
+ "docker",
462
+ "stats",
463
+ containerId,
464
+ "--no-stream",
465
+ "--format",
466
+ "{{.CPUPerc}},{{.MemUsage}}"
467
+ ]);
468
+ const stdout = await new Response(proc.stdout ?? null).text();
469
+ const [cpu, memory] = stdout.trim().split(",");
470
+ return { cpu: cpu || "0%", memory: memory || "0B / 0B" };
471
+ }
472
+ };
473
+
474
+ // src/Infrastructure/Git/ShellGitAdapter.ts
475
+ import { mkdir } from "fs/promises";
476
+ import { getRuntimeAdapter as getRuntimeAdapter2 } from "@gravito/core";
477
+ var ShellGitAdapter = class {
478
+ baseDir = "/tmp/gravito-launchpad-git";
479
+ async clone(repoUrl, branch) {
480
+ const dirName = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
481
+ const targetDir = `${this.baseDir}/${dirName}`;
482
+ await mkdir(this.baseDir, { recursive: true });
483
+ const runtime = getRuntimeAdapter2();
484
+ const proc = runtime.spawn([
485
+ "git",
486
+ "clone",
487
+ "--depth",
488
+ "1",
489
+ "--branch",
490
+ branch,
491
+ repoUrl,
492
+ targetDir
493
+ ]);
494
+ const exitCode = await proc.exited;
495
+ if (exitCode !== 0) {
496
+ throw new Error("Git Clone Failed");
497
+ }
498
+ return targetDir;
499
+ }
500
+ };
501
+
502
+ // src/Infrastructure/GitHub/OctokitGitHubAdapter.ts
503
+ import { createHmac, timingSafeEqual } from "crypto";
504
+ import { Octokit } from "@octokit/rest";
505
+ var OctokitGitHubAdapter = class {
506
+ octokit;
507
+ constructor(token) {
508
+ this.octokit = new Octokit({ auth: token });
509
+ }
510
+ verifySignature(payload, signature, secret) {
511
+ if (!signature) {
512
+ return false;
513
+ }
514
+ const hmac = createHmac("sha256", secret);
515
+ const digest = Buffer.from(`sha256=${hmac.update(payload).digest("hex")}`, "utf8");
516
+ const checksum = Buffer.from(signature, "utf8");
517
+ return digest.length === checksum.length && timingSafeEqual(digest, checksum);
518
+ }
519
+ async postComment(repoOwner, repoName, prNumber, comment) {
520
+ try {
521
+ await this.octokit.issues.createComment({
522
+ owner: repoOwner,
523
+ repo: repoName,
524
+ issue_number: prNumber,
525
+ body: comment
526
+ });
527
+ console.log(`[GitHub] \u5DF2\u5728 PR #${prNumber} \u7559\u4E0B\u90E8\u7F72\u8CC7\u8A0A`);
528
+ } catch (error) {
529
+ console.error(`[GitHub] \u7559\u8A00\u5931\u6557: ${error.message}`);
530
+ }
531
+ }
532
+ };
533
+
534
+ // src/Infrastructure/Persistence/CachedRocketRepository.ts
535
+ var CachedRocketRepository = class {
536
+ constructor(cache) {
537
+ this.cache = cache;
538
+ }
539
+ CACHE_KEY = "launchpad:rockets";
540
+ async getMap() {
541
+ const data = await this.cache.get(this.CACHE_KEY);
542
+ return new Map(Object.entries(data || {}));
543
+ }
544
+ async save(rocket) {
545
+ const map = await this.getMap();
546
+ map.set(rocket.id, rocket.toJSON());
547
+ await this.cache.set(this.CACHE_KEY, Object.fromEntries(map));
548
+ }
549
+ async findById(id) {
550
+ const map = await this.getMap();
551
+ const data = map.get(id);
552
+ return data ? Rocket.fromJSON(data) : null;
553
+ }
554
+ async findIdle() {
555
+ const rockets = await this.findAll();
556
+ return rockets.find((r) => r.status === "IDLE") || null;
557
+ }
558
+ async findAll() {
559
+ const map = await this.getMap();
560
+ return Array.from(map.values()).map((data) => Rocket.fromJSON(data));
561
+ }
562
+ async delete(id) {
563
+ const map = await this.getMap();
564
+ map.delete(id);
565
+ await this.cache.set(this.CACHE_KEY, Object.fromEntries(map));
566
+ }
567
+ };
568
+
569
+ // src/Infrastructure/Router/BunProxyAdapter.ts
570
+ import { getRuntimeAdapter as getRuntimeAdapter3 } from "@gravito/core";
571
+ var BunProxyAdapter = class {
572
+ routes = /* @__PURE__ */ new Map();
573
+ // domain -> targetUrl
574
+ register(domain, targetUrl) {
575
+ console.log(`[Proxy] \u8A3B\u518A\u8DEF\u7531: ${domain} -> ${targetUrl}`);
576
+ this.routes.set(domain.toLowerCase(), targetUrl);
577
+ }
578
+ unregister(domain) {
579
+ console.log(`[Proxy] \u8A3B\u92B7\u8DEF\u7531: ${domain}`);
580
+ this.routes.delete(domain.toLowerCase());
581
+ }
582
+ start(port) {
583
+ const self = this;
584
+ const runtime = getRuntimeAdapter3();
585
+ runtime.serve({
586
+ port,
587
+ async fetch(request) {
588
+ const host = request.headers.get("host")?.split(":")[0]?.toLowerCase();
589
+ if (!host || !self.routes.has(host)) {
590
+ return new Response("Rocket Not Found or Mission Not Started", { status: 404 });
591
+ }
592
+ const targetBase = self.routes.get(host);
593
+ const url = new URL(request.url);
594
+ const targetUrl = `${targetBase}${url.pathname}${url.search}`;
595
+ console.log(`[Proxy] \u8F49\u767C: ${host}${url.pathname} -> ${targetUrl}`);
596
+ try {
597
+ const proxyReq = new Request(targetUrl, {
598
+ method: request.method,
599
+ headers: request.headers,
600
+ body: request.body,
601
+ // @ts-expect-error Bun specific
602
+ duplex: "half"
603
+ });
604
+ return await fetch(proxyReq);
605
+ } catch (error) {
606
+ console.error(`[Proxy] \u8F49\u767C\u5931\u6557: ${error.message}`);
607
+ return new Response("Proxy Error", { status: 502 });
608
+ }
609
+ }
610
+ });
611
+ console.log(`[Proxy] \u52D5\u614B\u8DEF\u7531\u4F3A\u670D\u5668\u5DF2\u555F\u52D5\u65BC Port: ${port}`);
612
+ }
613
+ };
614
+
615
+ // src/index.ts
616
+ var LaunchpadServiceProvider = class extends ServiceProvider {
617
+ register(container) {
618
+ if (!container.has("cache")) {
619
+ const cacheFromServices = this.core?.services?.get("cache");
620
+ if (cacheFromServices) {
621
+ container.instance("cache", cacheFromServices);
622
+ }
623
+ }
624
+ container.singleton("launchpad.docker", () => new DockerAdapter());
625
+ container.singleton("launchpad.git", () => new ShellGitAdapter());
626
+ container.singleton("launchpad.router", () => new BunProxyAdapter());
627
+ container.singleton(
628
+ "launchpad.github",
629
+ () => new OctokitGitHubAdapter(process.env.GITHUB_TOKEN)
630
+ );
631
+ container.singleton("launchpad.repo", () => {
632
+ const cache = container.make("cache");
633
+ return new CachedRocketRepository(cache);
634
+ });
635
+ container.singleton("launchpad.refurbish", () => {
636
+ return new RefurbishUnit(container.make("launchpad.docker"));
637
+ });
638
+ container.singleton("launchpad.pool", () => {
639
+ return new PoolManager(
640
+ container.make("launchpad.docker"),
641
+ container.make("launchpad.repo"),
642
+ container.make("launchpad.refurbish"),
643
+ container.make("launchpad.router")
644
+ );
645
+ });
646
+ container.singleton("launchpad.injector", () => {
647
+ return new PayloadInjector(
648
+ container.make("launchpad.docker"),
649
+ container.make("launchpad.git")
650
+ );
651
+ });
652
+ container.singleton("launchpad.ctrl", () => {
653
+ return new MissionControl(
654
+ container.make("launchpad.pool"),
655
+ container.make("launchpad.injector"),
656
+ container.make("launchpad.docker"),
657
+ container.make("launchpad.router")
658
+ );
659
+ });
660
+ }
661
+ boot() {
662
+ const core = this.core;
663
+ if (!core) {
664
+ return;
665
+ }
666
+ const logger = core.logger;
667
+ const router = core.container.make("launchpad.router");
668
+ const docker = core.container.make("launchpad.docker");
669
+ router.start(8080);
670
+ logger.info("[Launchpad] \u6B63\u5728\u6383\u63CF\u4E26\u6E05\u7406\u6B98\u7559\u8CC7\u6E90...");
671
+ docker.removeContainerByLabel("gravito-origin=launchpad").then(() => {
672
+ logger.info("[Launchpad] \u8CC7\u6E90\u6E05\u7406\u5B8C\u7562\uFF0C\u7CFB\u7D71\u5C31\u7DD2\u3002");
673
+ }).catch((err) => {
674
+ logger.warn("[Launchpad] \u81EA\u52D5\u6E05\u7406\u5931\u6557 (\u53EF\u80FD\u662F\u74B0\u5883\u7121\u5BB9\u5668):", err.message);
675
+ });
676
+ }
677
+ };
678
+ var LaunchpadOrbit = class {
679
+ constructor(ripple) {
680
+ this.ripple = ripple;
681
+ }
682
+ async install(core) {
683
+ core.register(new LaunchpadServiceProvider());
684
+ core.router.post("/launch", async (c) => {
685
+ const rawBody = await c.req.text();
686
+ const signature = c.req.header("x-hub-signature-256") || "";
687
+ const secret = process.env.GITHUB_WEBHOOK_SECRET;
688
+ const github = core.container.make("launchpad.github");
689
+ if (secret && !github.verifySignature(rawBody, signature, secret)) {
690
+ core.logger.error("[GitHub] Webhook signature verification failed");
691
+ return c.json({ error: "Invalid signature" }, 401);
692
+ }
693
+ const body = JSON.parse(rawBody);
694
+ const event = c.req.header("x-github-event");
695
+ if (event === "pull_request") {
696
+ const action = body.action;
697
+ const pr = body.pull_request;
698
+ const missionId = `pr-${pr.number}`;
699
+ if (["opened", "synchronize", "reopened"].includes(action)) {
700
+ const mission = Mission.create({
701
+ id: missionId,
702
+ repoUrl: pr.base.repo.clone_url,
703
+ branch: pr.head.ref,
704
+ commitSha: pr.head.sha
705
+ });
706
+ const ctrl = core.container.make("launchpad.ctrl");
707
+ const rocketId = await ctrl.launch(mission, (type, data) => {
708
+ this.ripple.getServer().broadcast("telemetry", "telemetry.data", { type, data });
709
+ });
710
+ if (action === "opened" || action === "reopened") {
711
+ const previewUrl = `http://${missionId}.dev.local:8080`;
712
+ const dashboardUrl = `http://${c.req.header("host")?.split(":")[0]}:5173`;
713
+ const comment = `\u{1F680} **Gravito Launchpad: Deployment Ready!**
714
+
715
+ - **Preview URL**: [${previewUrl}](${previewUrl})
716
+ - **Mission Dashboard**: [${dashboardUrl}](${dashboardUrl})
717
+
718
+ *Rocket ID: ${rocketId} | Commit: ${pr.head.sha.slice(0, 7)}*`;
719
+ await github.postComment(
720
+ pr.base.repo.owner.login,
721
+ pr.base.repo.name,
722
+ pr.number,
723
+ comment
724
+ );
725
+ }
726
+ return c.json({ success: true, missionId, rocketId });
727
+ }
728
+ if (action === "closed") {
729
+ const pool = core.container.make("launchpad.pool");
730
+ await pool.recycle(missionId);
731
+ return c.json({ success: true, action: "recycled" });
732
+ }
733
+ }
734
+ return c.json({ success: true, message: "Event ignored" });
735
+ });
736
+ core.router.post("/recycle", async (c) => {
737
+ const body = await c.req.json();
738
+ if (!body.missionId) {
739
+ return c.json({ error: "missionId required" }, 400);
740
+ }
741
+ const pool = core.container.make("launchpad.pool");
742
+ await pool.recycle(body.missionId);
743
+ return c.json({ success: true });
744
+ });
745
+ }
746
+ };
747
+ async function bootstrapLaunchpad() {
748
+ const ripple = new OrbitRipple({ path: "/ws" });
749
+ const core = await PlanetCore.boot({
750
+ config: {
751
+ APP_NAME: "Gravito Launchpad",
752
+ PORT: 4e3,
753
+ CACHE_DRIVER: "file"
754
+ },
755
+ orbits: [
756
+ new OrbitCache(),
757
+ ripple,
758
+ new LaunchpadOrbit(ripple)
759
+ // 傳入實例
760
+ ]
761
+ });
762
+ await core.bootstrap();
763
+ const liftoffConfig = core.liftoff();
764
+ return {
765
+ port: liftoffConfig.port,
766
+ websocket: ripple.getHandler(),
767
+ fetch: (req, server) => {
768
+ if (ripple.getServer().upgrade(req, server)) {
769
+ return;
770
+ }
771
+ return liftoffConfig.fetch(req, server);
772
+ }
773
+ };
774
+ }
775
+ export {
776
+ LaunchpadOrbit,
777
+ Mission,
778
+ MissionControl,
779
+ PayloadInjector,
780
+ PoolManager,
781
+ RefurbishUnit,
782
+ Rocket,
783
+ RocketStatus,
784
+ bootstrapLaunchpad
785
+ };