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