@gravito/launchpad 1.2.1 → 1.3.1

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.js CHANGED
@@ -20,13 +20,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ DEFAULT_POOL_CONFIG: () => DEFAULT_POOL_CONFIG,
24
+ DEFAULT_REFURBISH_CONFIG: () => DEFAULT_REFURBISH_CONFIG,
23
25
  LaunchpadOrbit: () => LaunchpadOrbit,
24
26
  Mission: () => Mission,
27
+ MissionAssigned: () => MissionAssigned,
25
28
  MissionControl: () => MissionControl,
29
+ MissionQueue: () => MissionQueue,
30
+ MissionQueueTimeout: () => MissionQueueTimeout,
31
+ MissionQueued: () => MissionQueued,
26
32
  PayloadInjector: () => PayloadInjector,
33
+ PoolExhausted: () => PoolExhausted,
34
+ PoolExhaustedException: () => PoolExhaustedException,
27
35
  PoolManager: () => PoolManager,
36
+ QueueTimeoutException: () => QueueTimeoutException,
28
37
  RefurbishUnit: () => RefurbishUnit,
38
+ RefurbishmentCompleted: () => RefurbishmentCompleted,
29
39
  Rocket: () => Rocket,
40
+ RocketIgnited: () => RocketIgnited,
41
+ RocketSplashedDown: () => RocketSplashedDown,
30
42
  RocketStatus: () => RocketStatus,
31
43
  bootstrapLaunchpad: () => bootstrapLaunchpad
32
44
  });
@@ -143,6 +155,36 @@ registry = "${fallbackRegistry}"
143
155
  }
144
156
  };
145
157
 
158
+ // src/Domain/Interfaces.ts
159
+ var DEFAULT_POOL_CONFIG = {
160
+ maxRockets: 20,
161
+ warmupCount: 3,
162
+ maxQueueSize: 50,
163
+ queueTimeoutMs: 3e4,
164
+ exhaustionStrategy: "queue",
165
+ dynamicLimit: 5
166
+ };
167
+ var DEFAULT_REFURBISH_CONFIG = {
168
+ strategy: "deep-clean",
169
+ cleanupCommands: [
170
+ // 1. 殺掉所有使用者進程
171
+ "pkill -9 -u bun || true",
172
+ 'pkill -9 -u root -f "bun|node" || true',
173
+ // 2. 清理應用目錄
174
+ "rm -rf /app/* /app/.* 2>/dev/null || true",
175
+ // 3. 清理暫存檔案
176
+ "rm -rf /tmp/* /var/tmp/* 2>/dev/null || true",
177
+ // 4. 清理 bun 暫存
178
+ "rm -rf /root/.bun/install/cache/tmp/* 2>/dev/null || true",
179
+ "rm -rf /home/bun/.bun/install/cache/tmp/* 2>/dev/null || true",
180
+ // 5. 清理日誌
181
+ "rm -rf /var/log/*.log 2>/dev/null || true"
182
+ ],
183
+ cleanupTimeoutMs: 3e4,
184
+ failureAction: "decommission",
185
+ maxRetries: 2
186
+ };
187
+
146
188
  // src/Domain/Rocket.ts
147
189
  var import_enterprise2 = require("@gravito/enterprise");
148
190
 
@@ -173,6 +215,28 @@ var RefurbishmentCompleted = class extends import_enterprise.DomainEvent {
173
215
  this.rocketId = rocketId;
174
216
  }
175
217
  };
218
+ var PoolExhausted = class extends import_enterprise.DomainEvent {
219
+ constructor(totalRockets, maxRockets, queueLength) {
220
+ super();
221
+ this.totalRockets = totalRockets;
222
+ this.maxRockets = maxRockets;
223
+ this.queueLength = queueLength;
224
+ }
225
+ };
226
+ var MissionQueued = class extends import_enterprise.DomainEvent {
227
+ constructor(missionId, queuePosition) {
228
+ super();
229
+ this.missionId = missionId;
230
+ this.queuePosition = queuePosition;
231
+ }
232
+ };
233
+ var MissionQueueTimeout = class extends import_enterprise.DomainEvent {
234
+ constructor(missionId, waitTimeMs) {
235
+ super();
236
+ this.missionId = missionId;
237
+ this.waitTimeMs = waitTimeMs;
238
+ }
239
+ };
176
240
 
177
241
  // src/Domain/RocketStatus.ts
178
242
  var RocketStatus = /* @__PURE__ */ ((RocketStatus2) => {
@@ -260,6 +324,23 @@ var Rocket = class _Rocket extends import_enterprise2.AggregateRoot {
260
324
  decommission() {
261
325
  this._status = "DECOMMISSIONED" /* DECOMMISSIONED */;
262
326
  }
327
+ /**
328
+ * 替換底層容器(僅在 REFURBISHING 狀態時允許)
329
+ * 用於 destroy-recreate 策略
330
+ *
331
+ * @param newContainerId - 新容器 ID
332
+ * @throws {Error} 當火箭不在 REFURBISHING 狀態時
333
+ *
334
+ * @public
335
+ * @since 1.3.0
336
+ */
337
+ replaceContainer(newContainerId) {
338
+ if (this._status !== "REFURBISHING" /* REFURBISHING */) {
339
+ throw new Error(`\u7121\u6CD5\u66FF\u63DB\u5BB9\u5668\uFF1A\u706B\u7BAD ${this.id} \u4E0D\u5728 REFURBISHING \u72C0\u614B`);
340
+ }
341
+ this._containerId = newContainerId;
342
+ this._assignedDomain = null;
343
+ }
263
344
  toJSON() {
264
345
  return {
265
346
  id: this.id,
@@ -278,44 +359,219 @@ var Rocket = class _Rocket extends import_enterprise2.AggregateRoot {
278
359
  }
279
360
  };
280
361
 
362
+ // src/Application/MissionQueue.ts
363
+ var MissionQueue = class {
364
+ queue = [];
365
+ maxSize;
366
+ timeoutMs;
367
+ constructor(maxSize = 50, timeoutMs = 3e4) {
368
+ this.maxSize = maxSize;
369
+ this.timeoutMs = timeoutMs;
370
+ }
371
+ get length() {
372
+ return this.queue.length;
373
+ }
374
+ get isFull() {
375
+ return this.queue.length >= this.maxSize;
376
+ }
377
+ /**
378
+ * 將任務加入隊列,返回 Promise 等待分配
379
+ */
380
+ enqueue(mission) {
381
+ if (this.isFull) {
382
+ return Promise.reject(
383
+ new PoolExhaustedException(`\u4EFB\u52D9\u968A\u5217\u5DF2\u6EFF\uFF08${this.maxSize}\uFF09\uFF0C\u7121\u6CD5\u63A5\u53D7\u65B0\u4EFB\u52D9`)
384
+ );
385
+ }
386
+ return new Promise((resolve, reject) => {
387
+ const timeoutId = setTimeout(() => {
388
+ this.removeFromQueue(mission.id);
389
+ reject(new QueueTimeoutException(`\u4EFB\u52D9 ${mission.id} \u7B49\u5F85\u8D85\u6642`));
390
+ }, this.timeoutMs);
391
+ this.queue.push({
392
+ mission,
393
+ resolve,
394
+ reject,
395
+ enqueuedAt: Date.now(),
396
+ timeoutId
397
+ });
398
+ console.log(
399
+ `[MissionQueue] \u4EFB\u52D9 ${mission.id} \u5DF2\u52A0\u5165\u968A\u5217\uFF0C\u7576\u524D\u968A\u5217\u9577\u5EA6: ${this.queue.length}`
400
+ );
401
+ });
402
+ }
403
+ /**
404
+ * 取出下一個等待的任務
405
+ */
406
+ dequeue() {
407
+ const item = this.queue.shift();
408
+ if (item) {
409
+ clearTimeout(item.timeoutId);
410
+ }
411
+ return item ?? null;
412
+ }
413
+ /**
414
+ * 從隊列中移除指定任務
415
+ */
416
+ removeFromQueue(missionId) {
417
+ const index = this.queue.findIndex((q) => q.mission.id === missionId);
418
+ if (index >= 0) {
419
+ const [removed] = this.queue.splice(index, 1);
420
+ clearTimeout(removed.timeoutId);
421
+ }
422
+ }
423
+ /**
424
+ * 清空隊列並拒絕所有等待的任務
425
+ */
426
+ clear(reason) {
427
+ for (const item of this.queue) {
428
+ clearTimeout(item.timeoutId);
429
+ item.reject(new Error(reason));
430
+ }
431
+ this.queue = [];
432
+ }
433
+ /**
434
+ * 取得隊列統計資訊
435
+ */
436
+ getStats() {
437
+ const oldestWaitMs = this.queue.length > 0 ? Date.now() - this.queue[0].enqueuedAt : null;
438
+ return {
439
+ length: this.queue.length,
440
+ maxSize: this.maxSize,
441
+ oldestWaitMs
442
+ };
443
+ }
444
+ };
445
+ var PoolExhaustedException = class extends Error {
446
+ constructor(message) {
447
+ super(message);
448
+ this.name = "PoolExhaustedException";
449
+ }
450
+ };
451
+ var QueueTimeoutException = class extends Error {
452
+ constructor(message) {
453
+ super(message);
454
+ this.name = "QueueTimeoutException";
455
+ }
456
+ };
457
+
281
458
  // src/Application/PoolManager.ts
282
459
  var PoolManager = class {
283
- constructor(dockerAdapter, rocketRepository, refurbishUnit, router) {
460
+ constructor(dockerAdapter, rocketRepository, refurbishUnit, router, config) {
284
461
  this.dockerAdapter = dockerAdapter;
285
462
  this.rocketRepository = rocketRepository;
286
463
  this.refurbishUnit = refurbishUnit;
287
464
  this.router = router;
465
+ this.config = { ...DEFAULT_POOL_CONFIG, ...config };
466
+ this.missionQueue = new MissionQueue(this.config.maxQueueSize, this.config.queueTimeoutMs);
467
+ }
468
+ config;
469
+ missionQueue;
470
+ dynamicRocketCount = 0;
471
+ /**
472
+ * 取得當前 Pool 狀態
473
+ */
474
+ async getPoolStatus() {
475
+ const rockets = await this.rocketRepository.findAll();
476
+ const statusCounts = {
477
+ total: rockets.length,
478
+ idle: 0,
479
+ orbiting: 0,
480
+ refurbishing: 0,
481
+ decommissioned: 0
482
+ };
483
+ for (const rocket of rockets) {
484
+ switch (rocket.status) {
485
+ case "IDLE" /* IDLE */:
486
+ statusCounts.idle++;
487
+ break;
488
+ case "PREPARING" /* PREPARING */:
489
+ case "ORBITING" /* ORBITING */:
490
+ statusCounts.orbiting++;
491
+ break;
492
+ case "REFURBISHING" /* REFURBISHING */:
493
+ statusCounts.refurbishing++;
494
+ break;
495
+ case "DECOMMISSIONED" /* DECOMMISSIONED */:
496
+ statusCounts.decommissioned++;
497
+ break;
498
+ }
499
+ }
500
+ return {
501
+ ...statusCounts,
502
+ dynamicCount: this.dynamicRocketCount,
503
+ maxRockets: this.config.maxRockets,
504
+ queueLength: this.missionQueue.length
505
+ };
288
506
  }
289
507
  /**
290
508
  * 初始化發射場:預先準備指定數量的火箭
291
509
  */
292
510
  async warmup(count) {
511
+ const targetCount = count ?? this.config.warmupCount;
293
512
  const currentRockets = await this.rocketRepository.findAll();
294
- const needed = count - currentRockets.length;
513
+ const activeRockets = currentRockets.filter((r) => r.status !== "DECOMMISSIONED" /* DECOMMISSIONED */);
514
+ const needed = Math.min(
515
+ targetCount - activeRockets.length,
516
+ this.config.maxRockets - activeRockets.length
517
+ );
295
518
  if (needed <= 0) {
519
+ console.log(`[PoolManager] \u7121\u9700\u71B1\u6A5F\uFF0C\u7576\u524D\u5DF2\u6709 ${activeRockets.length} \u67B6\u706B\u7BAD`);
296
520
  return;
297
521
  }
298
- console.log(`[LaunchPad] \u6B63\u5728\u71B1\u6A5F\uFF0C\u6E96\u5099\u767C\u5C04 ${needed} \u67B6\u65B0\u706B\u7BAD...`);
522
+ console.log(`[PoolManager] \u6B63\u5728\u71B1\u6A5F\uFF0C\u6E96\u5099\u767C\u5C04 ${needed} \u67B6\u65B0\u706B\u7BAD...`);
299
523
  for (let i = 0; i < needed; i++) {
300
524
  const containerId = await this.dockerAdapter.createBaseContainer();
301
- const rocketId = `rocket-${Math.random().toString(36).substring(2, 9)}`;
525
+ const rocketId = `rocket-${crypto.randomUUID()}`;
302
526
  const rocket = new Rocket(rocketId, containerId);
303
527
  await this.rocketRepository.save(rocket);
304
528
  }
529
+ console.log(`[PoolManager] \u71B1\u6A5F\u5B8C\u6210\uFF0CPool \u7576\u524D\u5171 ${activeRockets.length + needed} \u67B6\u706B\u7BAD`);
305
530
  }
306
531
  /**
307
532
  * 獲取一架可用的火箭並分配任務
533
+ *
534
+ * @throws {PoolExhaustedException} 當 Pool 耗盡且無法處理請求時
308
535
  */
309
536
  async assignMission(mission) {
310
537
  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);
538
+ if (rocket) {
539
+ rocket.assignMission(mission);
540
+ await this.rocketRepository.save(rocket);
541
+ return rocket;
542
+ }
543
+ const poolStatus = await this.getPoolStatus();
544
+ const activeCount = poolStatus.total - poolStatus.decommissioned;
545
+ console.log(
546
+ `[PoolManager] \u7121\u53EF\u7528\u706B\u7BAD\uFF0C\u7576\u524D\u72C0\u614B: \u7E3D\u6578=${activeCount}/${this.config.maxRockets}, \u968A\u5217=${this.missionQueue.length}/${this.config.maxQueueSize}`
547
+ );
548
+ switch (this.config.exhaustionStrategy) {
549
+ case "reject":
550
+ throw new PoolExhaustedException(`Rocket Pool \u5DF2\u8017\u76E1\uFF0C\u7121\u6CD5\u8655\u7406\u4EFB\u52D9 ${mission.id}`);
551
+ case "dynamic": {
552
+ if (activeCount < this.config.maxRockets && this.dynamicRocketCount < (this.config.dynamicLimit ?? 5)) {
553
+ console.log(`[PoolManager] \u52D5\u614B\u5EFA\u7ACB\u5F8C\u63F4\u706B\u7BAD...`);
554
+ const containerId = await this.dockerAdapter.createBaseContainer();
555
+ rocket = new Rocket(`rocket-dynamic-${Date.now()}`, containerId);
556
+ this.dynamicRocketCount++;
557
+ rocket.assignMission(mission);
558
+ await this.rocketRepository.save(rocket);
559
+ return rocket;
560
+ }
561
+ if (this.missionQueue.isFull) {
562
+ throw new PoolExhaustedException(`Rocket Pool \u8207\u968A\u5217\u5747\u5DF2\u6EFF\uFF0C\u7121\u6CD5\u8655\u7406\u4EFB\u52D9 ${mission.id}`);
563
+ }
564
+ console.log(`[PoolManager] \u4EFB\u52D9 ${mission.id} \u9032\u5165\u7B49\u5F85\u968A\u5217`);
565
+ return this.missionQueue.enqueue(mission);
566
+ }
567
+ case "queue":
568
+ default:
569
+ if (this.missionQueue.isFull) {
570
+ throw new PoolExhaustedException(`Rocket Pool \u8207\u968A\u5217\u5747\u5DF2\u6EFF\uFF0C\u7121\u6CD5\u8655\u7406\u4EFB\u52D9 ${mission.id}`);
571
+ }
572
+ console.log(`[PoolManager] \u4EFB\u52D9 ${mission.id} \u9032\u5165\u7B49\u5F85\u968A\u5217`);
573
+ return this.missionQueue.enqueue(mission);
315
574
  }
316
- rocket.assignMission(mission);
317
- await this.rocketRepository.save(rocket);
318
- return rocket;
319
575
  }
320
576
  /**
321
577
  * 回收指定任務的火箭
@@ -324,7 +580,7 @@ var PoolManager = class {
324
580
  const allRockets = await this.rocketRepository.findAll();
325
581
  const rocket = allRockets.find((r) => r.currentMission?.id === missionId);
326
582
  if (!rocket) {
327
- console.warn(`[LaunchPad] \u627E\u4E0D\u5230\u5C6C\u65BC\u4EFB\u52D9 ${missionId} \u7684\u706B\u7BAD`);
583
+ console.warn(`[PoolManager] \u627E\u4E0D\u5230\u5C6C\u65BC\u4EFB\u52D9 ${missionId} \u7684\u706B\u7BAD`);
328
584
  return;
329
585
  }
330
586
  if (this.router && rocket.assignedDomain) {
@@ -337,30 +593,67 @@ var PoolManager = class {
337
593
  rocket.finishRefurbishment();
338
594
  }
339
595
  await this.rocketRepository.save(rocket);
596
+ await this.processQueue();
597
+ }
598
+ /**
599
+ * 處理等待隊列中的任務
600
+ */
601
+ async processQueue() {
602
+ const queuedItem = this.missionQueue.dequeue();
603
+ if (!queuedItem) {
604
+ return;
605
+ }
606
+ try {
607
+ const rocket = await this.rocketRepository.findIdle();
608
+ if (rocket) {
609
+ rocket.assignMission(queuedItem.mission);
610
+ await this.rocketRepository.save(rocket);
611
+ queuedItem.resolve(rocket);
612
+ console.log(`[PoolManager] \u968A\u5217\u4EFB\u52D9 ${queuedItem.mission.id} \u5DF2\u5206\u914D\u706B\u7BAD ${rocket.id}`);
613
+ } else {
614
+ console.warn(`[PoolManager] \u8655\u7406\u968A\u5217\u6642\u4ECD\u7121\u53EF\u7528\u706B\u7BAD\uFF0C\u4EFB\u52D9\u91CD\u65B0\u5165\u968A`);
615
+ this.missionQueue.enqueue(queuedItem.mission).then(queuedItem.resolve).catch(queuedItem.reject);
616
+ }
617
+ } catch (error) {
618
+ queuedItem.reject(error);
619
+ }
620
+ }
621
+ /**
622
+ * 取得隊列統計資訊
623
+ */
624
+ getQueueStats() {
625
+ return this.missionQueue.getStats();
340
626
  }
341
627
  };
342
628
 
343
629
  // src/Application/RefurbishUnit.ts
344
630
  var RefurbishUnit = class {
345
- constructor(docker) {
631
+ constructor(docker, config) {
346
632
  this.docker = docker;
633
+ this.config = { ...DEFAULT_REFURBISH_CONFIG, ...config };
347
634
  }
635
+ config;
348
636
  /**
349
637
  * 執行火箭翻新邏輯
350
638
  */
351
639
  async refurbish(rocket) {
352
- console.log(`[RefurbishUnit] \u6B63\u5728\u7FFB\u65B0\u706B\u7BAD: ${rocket.id} (\u5BB9\u5668: ${rocket.containerId})`);
640
+ console.log(
641
+ `[RefurbishUnit] \u6B63\u5728\u7FFB\u65B0\u706B\u7BAD: ${rocket.id} (\u7B56\u7565: ${this.config.strategy}, \u5BB9\u5668: ${rocket.containerId})`
642
+ );
353
643
  rocket.splashDown();
354
644
  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);
645
+ const commands = this.config.cleanupCommands ?? [];
646
+ const fullCommand = commands.join(" && ");
647
+ const result = await this.docker.executeCommand(rocket.containerId, ["sh", "-c", fullCommand]);
357
648
  if (result.exitCode !== 0) {
358
649
  console.error(`[RefurbishUnit] \u6E05\u7406\u5931\u6557: ${result.stderr}`);
359
- rocket.decommission();
360
- return;
650
+ if (this.config.failureAction === "decommission") {
651
+ rocket.decommission();
652
+ return;
653
+ }
361
654
  }
362
655
  rocket.finishRefurbishment();
363
- console.log(`[RefurbishUnit] \u706B\u7BAD ${rocket.id} \u7FFB\u65B0\u5B8C\u6210\uFF0C\u5DF2\u9032\u5165 IDLE \u72C0\u614B\u3002`);
656
+ console.log(`[RefurbishUnit] \u706B\u7BAD ${rocket.id} \u7FFB\u65B0\u5B8C\u6210\uFF0C\u5DF2\u9032\u5165 IDLE \u72C0\u614B`);
364
657
  } catch (error) {
365
658
  console.error(`[RefurbishUnit] \u56DE\u6536\u904E\u7A0B\u767C\u751F\u7570\u5E38:`, error);
366
659
  rocket.decommission();
@@ -393,8 +686,15 @@ var import_core = require("@gravito/core");
393
686
  var DockerAdapter = class {
394
687
  baseImage = "oven/bun:1.0-slim";
395
688
  runtime = (0, import_core.getRuntimeAdapter)();
689
+ // 快取目錄配置
690
+ cacheConfig = {
691
+ hostCachePath: process.env.BUN_CACHE_PATH || `${process.env.HOME}/.bun/install/cache`,
692
+ containerCachePathRoot: "/root/.bun/install/cache",
693
+ containerCachePathBun: "/home/bun/.bun/install/cache"
694
+ };
396
695
  async createBaseContainer() {
397
- const rocketId = `rocket-${Math.random().toString(36).substring(2, 9)}`;
696
+ const rocketId = `rocket-${crypto.randomUUID()}`;
697
+ await this.ensureCacheDirectory();
398
698
  const proc = this.runtime.spawn([
399
699
  "docker",
400
700
  "run",
@@ -406,10 +706,39 @@ var DockerAdapter = class {
406
706
  "-p",
407
707
  "3000",
408
708
  // 讓 Docker 分配隨機宿主機埠
709
+ // === 快取掛載(關鍵) ===
409
710
  "-v",
410
- `${process.env.HOME}/.bun/install/cache:/root/.bun/install/cache`,
711
+ `${this.cacheConfig.hostCachePath}:${this.cacheConfig.containerCachePathRoot}:rw`,
411
712
  "-v",
412
- `${process.env.HOME}/.bun/install/cache:/home/bun/.bun/install/cache`,
713
+ `${this.cacheConfig.hostCachePath}:${this.cacheConfig.containerCachePathBun}:rw`,
714
+ // === 環境變數配置(確保 bun 使用快取) ===
715
+ "-e",
716
+ `BUN_INSTALL_CACHE_DIR=${this.cacheConfig.containerCachePathBun}`,
717
+ "-e",
718
+ "BUN_INSTALL_CACHE=shared",
719
+ "-e",
720
+ "BUN_INSTALL_PREFER_OFFLINE=true",
721
+ // === 效能相關環境變數 ===
722
+ "-e",
723
+ "NODE_ENV=development",
724
+ // === 資源限制(防止單一容器占用過多資源) ===
725
+ "--memory",
726
+ "1g",
727
+ "--memory-swap",
728
+ "1g",
729
+ "--cpus",
730
+ "1.0",
731
+ // === 安全設定 ===
732
+ "--security-opt",
733
+ "no-new-privileges:true",
734
+ "--cap-drop",
735
+ "ALL",
736
+ "--cap-add",
737
+ "CHOWN",
738
+ "--cap-add",
739
+ "SETUID",
740
+ "--cap-add",
741
+ "SETGID",
413
742
  this.baseImage,
414
743
  "tail",
415
744
  "-f",
@@ -419,6 +748,7 @@ var DockerAdapter = class {
419
748
  const containerId = stdout.trim();
420
749
  const exitCode = await proc.exited;
421
750
  if (containerId.length === 64 && /^[0-9a-f]+$/.test(containerId)) {
751
+ await this.verifyCacheMount(containerId);
422
752
  return containerId;
423
753
  }
424
754
  if (exitCode !== 0) {
@@ -427,6 +757,37 @@ var DockerAdapter = class {
427
757
  }
428
758
  return containerId;
429
759
  }
760
+ /**
761
+ * 確保快取目錄存在
762
+ *
763
+ * @private
764
+ * @since 1.3.0
765
+ */
766
+ async ensureCacheDirectory() {
767
+ const cachePath = this.cacheConfig.hostCachePath;
768
+ const proc = this.runtime.spawn(["mkdir", "-p", cachePath]);
769
+ await proc.exited;
770
+ const chmodProc = this.runtime.spawn(["chmod", "777", cachePath]);
771
+ await chmodProc.exited;
772
+ }
773
+ /**
774
+ * 驗證快取是否正確掛載
775
+ *
776
+ * @private
777
+ * @since 1.3.0
778
+ */
779
+ async verifyCacheMount(containerId) {
780
+ const result = await this.executeCommand(containerId, [
781
+ "sh",
782
+ "-c",
783
+ `ls -la ${this.cacheConfig.containerCachePathBun} 2>/dev/null || echo 'NOT_MOUNTED'`
784
+ ]);
785
+ if (result.stdout.includes("NOT_MOUNTED")) {
786
+ console.warn(`[DockerAdapter] \u8B66\u544A: \u5BB9\u5668 ${containerId} \u7684\u5FEB\u53D6\u76EE\u9304\u53EF\u80FD\u672A\u6B63\u78BA\u639B\u8F09`);
787
+ } else {
788
+ console.log(`[DockerAdapter] \u5FEB\u53D6\u76EE\u9304\u5DF2\u78BA\u8A8D\u639B\u8F09: ${containerId}`);
789
+ }
790
+ }
430
791
  async getExposedPort(containerId, containerPort = 3e3) {
431
792
  const proc = this.runtime.spawn(["docker", "port", containerId, containerPort.toString()]);
432
793
  const stdout = await new Response(proc.stdout ?? null).text();
@@ -509,7 +870,7 @@ var import_core2 = require("@gravito/core");
509
870
  var ShellGitAdapter = class {
510
871
  baseDir = "/tmp/gravito-launchpad-git";
511
872
  async clone(repoUrl, branch) {
512
- const dirName = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
873
+ const dirName = `${Date.now()}-${crypto.randomUUID()}`;
513
874
  const targetDir = `${this.baseDir}/${dirName}`;
514
875
  await (0, import_promises.mkdir)(this.baseDir, { recursive: true });
515
876
  const runtime = (0, import_core2.getRuntimeAdapter)();
@@ -704,9 +1065,19 @@ var LaunchpadServiceProvider = class extends import_core4.ServiceProvider {
704
1065
  }
705
1066
  };
706
1067
  var LaunchpadOrbit = class {
1068
+ /**
1069
+ * Create a new LaunchpadOrbit instance.
1070
+ * @param ripple - Ripple instance for real-time telemetry communication.
1071
+ */
707
1072
  constructor(ripple) {
708
1073
  this.ripple = ripple;
709
1074
  }
1075
+ /**
1076
+ * Install the Launchpad orbit into PlanetCore.
1077
+ * Registers the service provider and sets up webhook routes for deployment.
1078
+ *
1079
+ * @param core - The PlanetCore instance.
1080
+ */
710
1081
  async install(core) {
711
1082
  core.register(new LaunchpadServiceProvider());
712
1083
  core.router.post("/launch", async (c) => {
@@ -780,12 +1151,7 @@ async function bootstrapLaunchpad() {
780
1151
  PORT: 4e3,
781
1152
  CACHE_DRIVER: "file"
782
1153
  },
783
- orbits: [
784
- new import_stasis.OrbitCache(),
785
- ripple,
786
- new LaunchpadOrbit(ripple)
787
- // 傳入實例
788
- ]
1154
+ orbits: [new import_stasis.OrbitCache(), ripple, new LaunchpadOrbit(ripple)]
789
1155
  });
790
1156
  await core.bootstrap();
791
1157
  const liftoffConfig = core.liftoff();
@@ -802,13 +1168,25 @@ async function bootstrapLaunchpad() {
802
1168
  }
803
1169
  // Annotate the CommonJS export names for ESM import in node:
804
1170
  0 && (module.exports = {
1171
+ DEFAULT_POOL_CONFIG,
1172
+ DEFAULT_REFURBISH_CONFIG,
805
1173
  LaunchpadOrbit,
806
1174
  Mission,
1175
+ MissionAssigned,
807
1176
  MissionControl,
1177
+ MissionQueue,
1178
+ MissionQueueTimeout,
1179
+ MissionQueued,
808
1180
  PayloadInjector,
1181
+ PoolExhausted,
1182
+ PoolExhaustedException,
809
1183
  PoolManager,
1184
+ QueueTimeoutException,
810
1185
  RefurbishUnit,
1186
+ RefurbishmentCompleted,
811
1187
  Rocket,
1188
+ RocketIgnited,
1189
+ RocketSplashedDown,
812
1190
  RocketStatus,
813
1191
  bootstrapLaunchpad
814
1192
  });