@gravito/launchpad 0.1.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.
Files changed (38) hide show
  1. package/.turbo/turbo-test.log +82 -0
  2. package/.turbo/turbo-typecheck.log +1 -0
  3. package/Dockerfile +23 -0
  4. package/README.md +44 -0
  5. package/debug-launch.ts +66 -0
  6. package/package.json +26 -0
  7. package/server.ts +7 -0
  8. package/simulate-github.ts +55 -0
  9. package/src/Application/MissionControl.ts +59 -0
  10. package/src/Application/PayloadInjector.ts +60 -0
  11. package/src/Application/PoolManager.ts +81 -0
  12. package/src/Application/RefurbishUnit.ts +40 -0
  13. package/src/Domain/Events.ts +28 -0
  14. package/src/Domain/Interfaces.ts +62 -0
  15. package/src/Domain/Mission.ts +27 -0
  16. package/src/Domain/Rocket.ts +116 -0
  17. package/src/Domain/RocketStatus.ts +10 -0
  18. package/src/Infrastructure/Docker/DockerAdapter.ts +127 -0
  19. package/src/Infrastructure/Git/ShellGitAdapter.ts +22 -0
  20. package/src/Infrastructure/GitHub/OctokitGitHubAdapter.ts +42 -0
  21. package/src/Infrastructure/Persistence/CachedRocketRepository.ts +42 -0
  22. package/src/Infrastructure/Persistence/InMemoryRocketRepository.ts +32 -0
  23. package/src/Infrastructure/Router/BunProxyAdapter.ts +54 -0
  24. package/src/index.ts +223 -0
  25. package/tests/Deployment.test.ts +50 -0
  26. package/tests/Integration.test.ts +42 -0
  27. package/tests/PoolManager.test.ts +60 -0
  28. package/tests/Refurbishment.test.ts +43 -0
  29. package/tests/Rocket.test.ts +49 -0
  30. package/tests/Telemetry.test.ts +50 -0
  31. package/tests/docker-adapter.test.ts +110 -0
  32. package/tests/enterprise-coverage.test.ts +62 -0
  33. package/tests/mission-control.test.ts +79 -0
  34. package/tests/mission.test.ts +18 -0
  35. package/tests/payload-injector.test.ts +64 -0
  36. package/tests/pool-manager.test.ts +93 -0
  37. package/tests/repository.test.ts +31 -0
  38. package/tsconfig.json +8 -0
@@ -0,0 +1,82 @@
1
+ $ bun test
2
+ bun test v1.3.4 (5eb2145b)
3
+
4
+ tests/mission.test.ts:
5
+ (pass) Mission > exposes mission properties [0.13ms]
6
+
7
+ tests/Telemetry.test.ts:
8
+ [MissionControl] 準備發射任務: pr-1
9
+ [MissionControl] 任務 pr-1 映射端口: 3000
10
+ (pass) MissionControl Telemetry > 發射時應該能正確啟動日誌串流與效能監控 [0.60ms]
11
+
12
+ tests/mission-control.test.ts:
13
+ [MissionControl] 準備發射任務: mission-telemetry
14
+ [MissionControl] 任務 mission-telemetry 映射端口: 3000
15
+ [MissionControl] 任務 mission-telemetry TTL 已到期,執行自動回收...
16
+ (pass) MissionControl timers > emits stats and schedules recycle [0.80ms]
17
+
18
+ tests/pool-manager.test.ts:
19
+ [LaunchPad] 正在熱機,準備發射 2 架新火箭...
20
+ (pass) PoolManager > warms up the pool with base containers [0.93ms]
21
+ [LaunchPad] 正在熱機,準備發射 1 架新火箭...
22
+ [LaunchPad] 資源吃緊,正在緊急呼叫後援火箭...
23
+ (pass) PoolManager > assigns missions to idle rockets or creates new ones [0.70ms]
24
+ [LaunchPad] 正在熱機,準備發射 1 架新火箭...
25
+ (pass) PoolManager > recycles rockets through refurbish flow [0.68ms]
26
+
27
+ tests/enterprise-coverage.test.ts:
28
+ (pass) Launchpad enterprise coverage > covers core enterprise primitives [0.36ms]
29
+
30
+ tests/repository.test.ts:
31
+ (pass) InMemoryRocketRepository > stores and retrieves rockets [0.23ms]
32
+ (pass) InMemoryRocketRepository > finds idle rockets [0.02ms]
33
+
34
+ tests/Rocket.test.ts:
35
+ (pass) Rocket > transitions through mission lifecycle and emits events [0.26ms]
36
+ (pass) Rocket > guards invalid transitions [0.11ms]
37
+
38
+ tests/Refurbishment.test.ts:
39
+ [RefurbishUnit] 正在翻新火箭: r1 (容器: c1)
40
+ [RefurbishUnit] 火箭 r1 翻新完成,已進入 IDLE 狀態。
41
+ (pass) RefurbishUnit (Rocket Recovery) > 應該能執行深度清理並將火箭重置為 IDLE [0.45ms]
42
+ [RefurbishUnit] 正在翻新火箭: r2 (容器: c2)
43
+ [RefurbishUnit] 清理失敗: Disk Full
44
+ (pass) RefurbishUnit (Rocket Recovery) > 如果清理失敗應該將火箭除役 [0.17ms]
45
+
46
+ tests/payload-injector.test.ts:
47
+ (pass) PayloadInjector > throws when rocket has no mission [0.62ms]
48
+ [PayloadInjector] 正在拉取代碼: https://example.com/repo.git (main)
49
+ [PayloadInjector] 正在注入載荷至容器: container-10
50
+ [PayloadInjector] 正在安裝依賴...
51
+ [PayloadInjector] 點火!
52
+ (pass) PayloadInjector > deploys payload and ignites rocket [0.45ms]
53
+
54
+ tests/docker-adapter.test.ts:
55
+ (pass) DockerAdapter > creates base container when stdout has container id [1.38ms]
56
+ (pass) DockerAdapter > throws when container creation fails [0.09ms]
57
+ (pass) DockerAdapter > copyFiles throws on non-zero exit code [0.25ms]
58
+ (pass) DockerAdapter > executeCommand returns stdout and stderr [0.26ms]
59
+ (pass) DockerAdapter > getStats parses cpu and memory output [0.74ms]
60
+ (pass) DockerAdapter > streamLogs forwards stdout and stderr [2.73ms]
61
+
62
+ tests/Integration.test.ts:
63
+ [LaunchPad] 正在熱機,準備發射 1 架新火箭...
64
+ [Test] 容器內 Bun 版本: 1.0.36
65
+ (pass) LaunchPad 集成測試 (真實 Docker) > 應該能成功熱機並在容器內執行指令 [613.64ms]
66
+
67
+ tests/Deployment.test.ts:
68
+ [LaunchPad] 資源吃緊,正在緊急呼叫後援火箭...
69
+ [PayloadInjector] 正在拉取代碼: http://git (dev)
70
+ [PayloadInjector] 正在注入載荷至容器: cid-1
71
+ [PayloadInjector] 正在安裝依賴...
72
+ [PayloadInjector] 點火!
73
+ (pass) Payload Injection Flow > 應該能從指派任務到成功點火 [0.39ms]
74
+
75
+ tests/PoolManager.test.ts:
76
+ [LaunchPad] 正在熱機,準備發射 2 架新火箭...
77
+ (pass) PoolManager (Application Service) > 應該能正確熱機並指派任務 [0.53ms]
78
+
79
+ 24 pass
80
+ 0 fail
81
+ 81 expect() calls
82
+ Ran 24 tests across 13 files. [833.00ms]
@@ -0,0 +1 @@
1
+ $ tsc --noEmit
package/Dockerfile ADDED
@@ -0,0 +1,23 @@
1
+ FROM oven/bun:1.0 AS base
2
+ WORKDIR /usr/src/app
3
+
4
+ # 安裝 Docker CLI
5
+ RUN apt-get update && apt-get install -y docker.io
6
+
7
+ # 複製基礎設定
8
+ COPY package.json bun.lock turbo.json tsconfig.json ./
9
+ COPY packages ./packages
10
+
11
+ # 建立空的工作區目錄以滿足 Bun 檢查
12
+ RUN mkdir -p templates examples
13
+
14
+ # 安裝依賴
15
+ RUN bun install
16
+
17
+ # 設定工作目錄到 launchpad
18
+ WORKDIR /usr/src/app/packages/launchpad
19
+
20
+ EXPOSE 4000
21
+ EXPOSE 8080
22
+
23
+ ENTRYPOINT [ "bun", "run", "server.ts" ]
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @gravito/launchpad
2
+
3
+ > 🚀 Bun 火箭回收系統:專為 Bun 打造的秒級容器部署與生命週期管理系統。
4
+
5
+ ## 核心特性
6
+
7
+ - **Rocket Pool**: 預熱容器池,消除啟動冷啟動。
8
+ - **Payload Injection**: 跳過 Docker Build,透過 `docker cp` 秒級注入代碼。
9
+ - **DDD 架構**: 基於 `@gravito/enterprise` 實作,具備嚴謹的狀態機管理。
10
+ - **可回收性**: 任務結束後自動翻新容器,資源零浪費。
11
+
12
+ ## 架構概覽
13
+
14
+ 本套件遵循 **Clean Architecture** 與 **DDD**:
15
+
16
+ - **Domain**: 定義 `Rocket` 狀態機與 `Mission` 邏輯。
17
+ - **Application**: `PoolManager` (調度) 與 `PayloadInjector` (部署)。
18
+ - **Infrastructure**: 底層 Docker 與 Git 操作實作。
19
+
20
+ ## 快速開始
21
+
22
+ ```typescript
23
+ import { PoolManager, PayloadInjector } from '@gravito/launchpad'
24
+ import { DockerAdapter, ShellGitAdapter, InMemoryRocketRepository } from '@gravito/launchpad/infra'
25
+
26
+ const manager = new PoolManager(new DockerAdapter(), new InMemoryRocketRepository())
27
+ const injector = new PayloadInjector(new DockerAdapter(), new ShellGitAdapter())
28
+
29
+ // 1. 預熱池子
30
+ await manager.warmup(3)
31
+
32
+ // 2. 指派任務
33
+ const mission = Mission.create({ ... })
34
+ const rocket = await manager.assignMission(mission)
35
+
36
+ // 3. 秒級部署
37
+ await injector.deploy(rocket)
38
+ ```
39
+
40
+ ## 測試
41
+
42
+ ```bash
43
+ bun test
44
+ ```
@@ -0,0 +1,66 @@
1
+ import { DockerAdapter } from './src/Infrastructure/Docker/DockerAdapter'
2
+ import { bootstrapLaunchpad } from './src/index'
3
+
4
+ async function run() {
5
+ console.log('🤖 Starting Auto-Debug Sequence...')
6
+
7
+ // 1. Cleanup
8
+ const _docker = new DockerAdapter()
9
+ try {
10
+ const containers = await Bun.spawn([
11
+ 'docker',
12
+ 'ps',
13
+ '-aq',
14
+ '--filter',
15
+ 'label=gravito-origin=launchpad',
16
+ ]).text()
17
+ if (containers.trim()) {
18
+ console.log('🧹 Cleaning up old containers...')
19
+ await Bun.spawn(['docker', 'rm', '-f', ...containers.trim().split('\n')]).exited
20
+ }
21
+ } catch (_e) {}
22
+
23
+ // 2. Start Server
24
+ const config = await bootstrapLaunchpad()
25
+ const server = Bun.serve(config)
26
+ console.log(`🚀 Server started at http://localhost:${config.port}`)
27
+
28
+ // 3. Launch Mission
29
+ console.log('🔫 Firing Mission...')
30
+ try {
31
+ const res = await fetch(`http://localhost:${config.port}/launch`, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'X-GitHub-Event': 'pull_request', // 模擬 GitHub 事件
36
+ },
37
+ body: JSON.stringify({
38
+ action: 'opened',
39
+ pull_request: {
40
+ number: 77,
41
+ head: { ref: 'feat/launchpad-dashboard', sha: 'latest' },
42
+ base: {
43
+ repo: {
44
+ owner: { login: 'gravito' },
45
+ name: 'core',
46
+ clone_url: 'https://github.com/gravito-framework/gravito.git',
47
+ },
48
+ },
49
+ },
50
+ }),
51
+ })
52
+
53
+ const data = await res.json()
54
+ console.log('Response:', data)
55
+
56
+ console.log('✅ Mission active. Monitoring for 30s...')
57
+ await new Promise((r) => setTimeout(r, 30000))
58
+ } catch (e) {
59
+ console.error('❌ Error:', e)
60
+ } finally {
61
+ server.stop()
62
+ process.exit(0)
63
+ }
64
+ }
65
+
66
+ run()
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@gravito/launchpad",
3
+ "version": "0.1.0",
4
+ "description": "Container lifecycle management system for flash deployments",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsup src/index.ts --format cjs,esm --dts",
10
+ "test": "bun test",
11
+ "test:coverage": "bun test --coverage --coverage-threshold=80",
12
+ "test:ci": "bun test --coverage --coverage-threshold=80",
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "@gravito/enterprise": "workspace:*",
17
+ "@gravito/ripple": "workspace:*",
18
+ "@gravito/stasis": "workspace:*",
19
+ "@octokit/rest": "^22.0.1",
20
+ "gravito-core": "workspace:*"
21
+ },
22
+ "devDependencies": {
23
+ "tsup": "^8.0.0",
24
+ "typescript": "^5.0.0"
25
+ }
26
+ }
package/server.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { bootstrapLaunchpad } from './src/index'
2
+
3
+ const config = await bootstrapLaunchpad()
4
+ Bun.serve(config)
5
+
6
+ console.log(`🚀 Launchpad Command Center active at: http://localhost:${config.port}`)
7
+ console.log(`📡 Telemetry WebSocket channel: ws://localhost:${config.port}/ws`)
@@ -0,0 +1,55 @@
1
+ /**
2
+ * GitHub Webhook 模擬器
3
+ * 模擬 GitHub 發送 pull_request 事件給 Launchpad
4
+ */
5
+
6
+ const LAUNCHPAD_URL = 'http://localhost:4000/launch'
7
+
8
+ async function simulateWebhook(action: 'opened' | 'synchronize' | 'closed') {
9
+ console.log(`\n🚀 正在模擬 GitHub Action: ${action.toUpperCase()}...`)
10
+
11
+ const payload = {
12
+ action: action,
13
+ number: 19,
14
+ pull_request: {
15
+ number: 19,
16
+ state: action === 'closed' ? 'closed' : 'open',
17
+ head: {
18
+ ref: 'feat/launchpad-github-bot',
19
+ sha: '25837ad8225837ad8225837ad8225837ad825837',
20
+ },
21
+ base: {
22
+ repo: {
23
+ name: 'gravito',
24
+ owner: { login: 'gravito-framework' },
25
+ clone_url: 'https://github.com/gravito-framework/gravito.git',
26
+ },
27
+ },
28
+ },
29
+ }
30
+
31
+ try {
32
+ const response = await fetch(LAUNCHPAD_URL, {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ 'X-GitHub-Event': 'pull_request',
37
+ 'X-Hub-Signature-256': 'sha256=MOCK_SIGNATURE', // 模擬簽名
38
+ },
39
+ body: JSON.stringify(payload),
40
+ })
41
+
42
+ const result = await response.json()
43
+ console.log('✅ Launchpad 回應:', JSON.stringify(result, null, 2))
44
+ } catch (error) {
45
+ console.error('❌ 模擬失敗:', error)
46
+ }
47
+ }
48
+
49
+ // 執行模擬流程
50
+ async function runTest() {
51
+ // 1. 模擬開啟 PR (觸發部署)
52
+ await simulateWebhook('opened')
53
+ }
54
+
55
+ runTest()
@@ -0,0 +1,59 @@
1
+ import type { IDockerAdapter, IRouterAdapter } from '../Domain/Interfaces'
2
+ import type { Mission } from '../Domain/Mission'
3
+ import type { PayloadInjector } from './PayloadInjector'
4
+ import type { PoolManager } from './PoolManager'
5
+
6
+ export class MissionControl {
7
+ constructor(
8
+ private poolManager: PoolManager,
9
+ private injector: PayloadInjector,
10
+ private docker: IDockerAdapter,
11
+ private router?: IRouterAdapter
12
+ ) {}
13
+
14
+ async launch(mission: Mission, onTelemetry: (type: string, data: any) => void): Promise<string> {
15
+ console.log(`[MissionControl] 準備發射任務: ${mission.id}`)
16
+
17
+ // 1. 從池子抓火箭
18
+ const rocket = await this.poolManager.assignMission(mission)
19
+
20
+ // 2. 注入代碼並點火
21
+ await this.injector.deploy(rocket)
22
+
23
+ // 3. 獲取真實映射端口
24
+ const publicPort = await this.docker.getExposedPort(rocket.containerId, 3000)
25
+ console.log(`[MissionControl] 任務 ${mission.id} 映射端口: ${publicPort}`)
26
+
27
+ // 4. 分配域名
28
+ const domain = `${mission.id}.dev.local`.toLowerCase()
29
+ rocket.assignDomain(domain)
30
+
31
+ if (this.router) {
32
+ // 關鍵修復:Proxy 指向真實的 Host Port
33
+ this.router.register(domain, `http://localhost:${publicPort}`)
34
+ }
35
+
36
+ this.docker.streamLogs(rocket.containerId, (log) => {
37
+ onTelemetry('log', { rocketId: rocket.id, text: log })
38
+ })
39
+
40
+ const statsTimer = setInterval(async () => {
41
+ if (rocket.status === 'DECOMMISSIONED' || rocket.status === 'IDLE') {
42
+ clearInterval(statsTimer)
43
+ return
44
+ }
45
+ const stats = await this.docker.getStats(rocket.containerId)
46
+ onTelemetry('stats', { rocketId: rocket.id, ...stats })
47
+ }, 5000)
48
+
49
+ const ttl = 10 * 60 * 1000
50
+ setTimeout(async () => {
51
+ console.log(`[MissionControl] 任務 ${mission.id} TTL 已到期,執行自動回收...`)
52
+ clearInterval(statsTimer)
53
+ await this.poolManager.recycle(mission.id)
54
+ onTelemetry('log', { rocketId: rocket.id, text: '--- MISSION EXPIRED (TTL) ---' })
55
+ }, ttl)
56
+
57
+ return rocket.id
58
+ }
59
+ }
@@ -0,0 +1,60 @@
1
+ import type { IDockerAdapter, IGitAdapter } from '../Domain/Interfaces'
2
+ import type { Rocket } from '../Domain/Rocket'
3
+
4
+ export class PayloadInjector {
5
+ constructor(
6
+ private docker: IDockerAdapter,
7
+ private git: IGitAdapter
8
+ ) {}
9
+
10
+ async deploy(rocket: Rocket): Promise<void> {
11
+ if (!rocket.currentMission) {
12
+ throw new Error(`Rocket ${rocket.id} 沒有指派任務,無法部署`)
13
+ }
14
+
15
+ const { repoUrl, branch } = rocket.currentMission
16
+ const containerId = rocket.containerId
17
+
18
+ console.log(`[PayloadInjector] 正在拉取代碼: ${repoUrl} (${branch})`)
19
+ const codePath = await this.git.clone(repoUrl, branch)
20
+
21
+ console.log(`[PayloadInjector] 正在注入載荷至容器: ${containerId}`)
22
+ await this.docker.copyFiles(containerId, codePath, '/app')
23
+
24
+ console.log(`[PayloadInjector] 正在安裝依賴...`)
25
+
26
+ // 寫入一個強制覆蓋的 bunfig.toml
27
+ const bunfigContent = `[install]\nfrozenLockfile = false\n`
28
+ await this.docker.executeCommand(containerId, [
29
+ 'sh',
30
+ '-c',
31
+ `echo "${bunfigContent}" > /app/bunfig.toml`,
32
+ ])
33
+
34
+ // 刪除舊的 lockfile
35
+ await this.docker.executeCommand(containerId, ['rm', '-f', '/app/bun.lockb'])
36
+
37
+ // 安裝 (跳過腳本以避免編譯原生模組失敗)
38
+ const installRes = await this.docker.executeCommand(containerId, [
39
+ 'bun',
40
+ 'install',
41
+ '--cwd',
42
+ '/app',
43
+ '--no-save',
44
+ '--ignore-scripts',
45
+ ])
46
+
47
+ if (installRes.exitCode !== 0) {
48
+ throw new Error(`安裝依賴失敗: ${installRes.stderr}`)
49
+ }
50
+
51
+ console.log(`[PayloadInjector] 點火!`)
52
+
53
+ // 真正啟動應用程式
54
+ this.docker.executeCommand(containerId, ['bun', 'run', '/app/examples/demo.ts']).catch((e) => {
55
+ console.error(`[PayloadInjector] 運行異常:`, e)
56
+ })
57
+
58
+ rocket.ignite()
59
+ }
60
+ }
@@ -0,0 +1,81 @@
1
+ import type { IDockerAdapter, IRocketRepository, IRouterAdapter } from '../Domain/Interfaces'
2
+ import type { Mission } from '../Domain/Mission'
3
+ import { Rocket } from '../Domain/Rocket'
4
+ import type { RefurbishUnit } from './RefurbishUnit'
5
+
6
+ export class PoolManager {
7
+ constructor(
8
+ private dockerAdapter: IDockerAdapter,
9
+ private rocketRepository: IRocketRepository,
10
+ private refurbishUnit?: RefurbishUnit,
11
+ private router?: IRouterAdapter
12
+ ) {}
13
+
14
+ /**
15
+ * 初始化發射場:預先準備指定數量的火箭
16
+ */
17
+ async warmup(count: number): Promise<void> {
18
+ const currentRockets = await this.rocketRepository.findAll()
19
+ const needed = count - currentRockets.length
20
+
21
+ if (needed <= 0) {
22
+ return
23
+ }
24
+
25
+ console.log(`[LaunchPad] 正在熱機,準備發射 ${needed} 架新火箭...`)
26
+
27
+ for (let i = 0; i < needed; i++) {
28
+ const containerId = await this.dockerAdapter.createBaseContainer()
29
+ const rocketId = `rocket-${Math.random().toString(36).substring(2, 9)}`
30
+ const rocket = new Rocket(rocketId, containerId)
31
+ await this.rocketRepository.save(rocket)
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 獲取一架可用的火箭並分配任務
37
+ */
38
+ async assignMission(mission: Mission): Promise<Rocket> {
39
+ let rocket = await this.rocketRepository.findIdle()
40
+
41
+ // 如果池子空了,動態建立一架(實務上應該報警或等待,這裡先簡化為動態建立)
42
+ if (!rocket) {
43
+ console.log(`[LaunchPad] 資源吃緊,正在緊急呼叫後援火箭...`)
44
+ const containerId = await this.dockerAdapter.createBaseContainer()
45
+ rocket = new Rocket(`rocket-dynamic-${Date.now()}`, containerId)
46
+ }
47
+
48
+ rocket.assignMission(mission)
49
+ await this.rocketRepository.save(rocket)
50
+
51
+ return rocket
52
+ }
53
+
54
+ /**
55
+ * 回收指定任務的火箭
56
+ */
57
+ async recycle(missionId: string): Promise<void> {
58
+ const allRockets = await this.rocketRepository.findAll()
59
+ const rocket = allRockets.find((r) => r.currentMission?.id === missionId)
60
+
61
+ if (!rocket) {
62
+ console.warn(`[LaunchPad] 找不到屬於任務 ${missionId} 的火箭`)
63
+ return
64
+ }
65
+
66
+ // [New] 註銷域名映射
67
+ if (this.router && rocket.assignedDomain) {
68
+ this.router.unregister(rocket.assignedDomain)
69
+ }
70
+
71
+ if (this.refurbishUnit) {
72
+ await this.refurbishUnit.refurbish(rocket)
73
+ } else {
74
+ // 回退邏輯:如果沒有提供回收單元,則直接標記完成
75
+ rocket.splashDown()
76
+ rocket.finishRefurbishment()
77
+ }
78
+
79
+ await this.rocketRepository.save(rocket)
80
+ }
81
+ }
@@ -0,0 +1,40 @@
1
+ import type { IDockerAdapter } from '../Domain/Interfaces'
2
+ import type { Rocket } from '../Domain/Rocket'
3
+
4
+ export class RefurbishUnit {
5
+ constructor(private docker: IDockerAdapter) {}
6
+
7
+ /**
8
+ * 執行火箭翻新邏輯
9
+ */
10
+ async refurbish(rocket: Rocket): Promise<void> {
11
+ console.log(`[RefurbishUnit] 正在翻新火箭: ${rocket.id} (容器: ${rocket.containerId})`)
12
+
13
+ // 1. 進入狀態機的回收階段
14
+ rocket.splashDown()
15
+
16
+ try {
17
+ // 2. 執行深度清理指令
18
+ // - 刪除 /app 目錄 (注入的代碼)
19
+ // - 殺掉所有除了 tail 以外的 bun 進程
20
+ // - 清理暫存檔
21
+ const cleanupCommands = ['sh', '-c', 'rm -rf /app/* && pkill -f bun || true && rm -rf /tmp/*']
22
+
23
+ const result = await this.docker.executeCommand(rocket.containerId, cleanupCommands)
24
+
25
+ if (result.exitCode !== 0) {
26
+ console.error(`[RefurbishUnit] 清理失敗: ${result.stderr}`)
27
+ // 這裡可以選擇將火箭標記為 DECOMMISSIONED
28
+ rocket.decommission()
29
+ return
30
+ }
31
+
32
+ // 3. 翻新完成,回歸池中
33
+ rocket.finishRefurbishment()
34
+ console.log(`[RefurbishUnit] 火箭 ${rocket.id} 翻新完成,已進入 IDLE 狀態。`)
35
+ } catch (error) {
36
+ console.error(`[RefurbishUnit] 回收過程發生異常:`, error)
37
+ rocket.decommission()
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,28 @@
1
+ import { DomainEvent } from '@gravito/enterprise'
2
+
3
+ export class MissionAssigned extends DomainEvent {
4
+ constructor(
5
+ public readonly rocketId: string,
6
+ public readonly missionId: string
7
+ ) {
8
+ super()
9
+ }
10
+ }
11
+
12
+ export class RocketIgnited extends DomainEvent {
13
+ constructor(public readonly rocketId: string) {
14
+ super()
15
+ }
16
+ }
17
+
18
+ export class RocketSplashedDown extends DomainEvent {
19
+ constructor(public readonly rocketId: string) {
20
+ super()
21
+ }
22
+ }
23
+
24
+ export class RefurbishmentCompleted extends DomainEvent {
25
+ constructor(public readonly rocketId: string) {
26
+ super()
27
+ }
28
+ }
@@ -0,0 +1,62 @@
1
+ import type { Rocket } from './Rocket'
2
+
3
+ /**
4
+ * 負責與底層容器引擎(如 Docker)溝通的轉接器介面
5
+ */
6
+ export interface IDockerAdapter {
7
+ createBaseContainer(): Promise<string>
8
+ copyFiles(containerId: string, sourcePath: string, targetPath: string): Promise<void>
9
+ executeCommand(
10
+ containerId: string,
11
+ command: string[]
12
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }>
13
+
14
+ /**
15
+ * 獲取容器映射到宿主機的真實端口
16
+ */
17
+ getExposedPort(containerId: string, containerPort?: number): Promise<number>
18
+
19
+ removeContainer(containerId: string): Promise<void>
20
+ /**
21
+ * 根據 Label 批量移除容器
22
+ */
23
+ removeContainerByLabel(label: string): Promise<void>
24
+
25
+ streamLogs(containerId: string, onData: (data: string) => void): void
26
+ getStats(containerId: string): Promise<{ cpu: string; memory: string }>
27
+ }
28
+
29
+ /**
30
+ * 負責代碼獲取
31
+ */
32
+ export interface IGitAdapter {
33
+ clone(repoUrl: string, branch: string): Promise<string>
34
+ }
35
+
36
+ /**
37
+ * 負責動態反向代理與域名路由的轉接器
38
+ */
39
+ export interface IRouterAdapter {
40
+ register(domain: string, targetUrl: string): void
41
+ unregister(domain: string): void
42
+ start(port: number): void
43
+ }
44
+
45
+ /**
46
+ * 負責與 GitHub API 互動的轉接器
47
+ */
48
+ export interface IGitHubAdapter {
49
+ verifySignature(payload: string, signature: string, secret: string): boolean
50
+ postComment(repoOwner: string, repoName: string, prNumber: number, comment: string): Promise<void>
51
+ }
52
+
53
+ /**
54
+ * 負責持久化火箭狀態的儲存庫介面
55
+ */
56
+ export interface IRocketRepository {
57
+ save(rocket: Rocket): Promise<void>
58
+ findById(id: string): Promise<Rocket | null>
59
+ findIdle(): Promise<Rocket | null>
60
+ findAll(): Promise<Rocket[]>
61
+ delete(id: string): Promise<void>
62
+ }
@@ -0,0 +1,27 @@
1
+ import { ValueObject } from '@gravito/enterprise'
2
+
3
+ interface MissionProps {
4
+ id: string // PR ID
5
+ repoUrl: string // Git Repo URL
6
+ branch: string // Branch Name
7
+ commitSha: string // Commit Hash
8
+ }
9
+
10
+ export class Mission extends ValueObject<MissionProps> {
11
+ get id() {
12
+ return this.props.id
13
+ }
14
+ get repoUrl() {
15
+ return this.props.repoUrl
16
+ }
17
+ get branch() {
18
+ return this.props.branch
19
+ }
20
+ get commitSha() {
21
+ return this.props.commitSha
22
+ }
23
+
24
+ static create(props: MissionProps): Mission {
25
+ return new Mission(props)
26
+ }
27
+ }