@gravito/launchpad 1.2.2 → 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.mjs CHANGED
@@ -111,6 +111,36 @@ registry = "${fallbackRegistry}"
111
111
  }
112
112
  };
113
113
 
114
+ // src/Domain/Interfaces.ts
115
+ var DEFAULT_POOL_CONFIG = {
116
+ maxRockets: 20,
117
+ warmupCount: 3,
118
+ maxQueueSize: 50,
119
+ queueTimeoutMs: 3e4,
120
+ exhaustionStrategy: "queue",
121
+ dynamicLimit: 5
122
+ };
123
+ var DEFAULT_REFURBISH_CONFIG = {
124
+ strategy: "deep-clean",
125
+ cleanupCommands: [
126
+ // 1. 殺掉所有使用者進程
127
+ "pkill -9 -u bun || true",
128
+ 'pkill -9 -u root -f "bun|node" || true',
129
+ // 2. 清理應用目錄
130
+ "rm -rf /app/* /app/.* 2>/dev/null || true",
131
+ // 3. 清理暫存檔案
132
+ "rm -rf /tmp/* /var/tmp/* 2>/dev/null || true",
133
+ // 4. 清理 bun 暫存
134
+ "rm -rf /root/.bun/install/cache/tmp/* 2>/dev/null || true",
135
+ "rm -rf /home/bun/.bun/install/cache/tmp/* 2>/dev/null || true",
136
+ // 5. 清理日誌
137
+ "rm -rf /var/log/*.log 2>/dev/null || true"
138
+ ],
139
+ cleanupTimeoutMs: 3e4,
140
+ failureAction: "decommission",
141
+ maxRetries: 2
142
+ };
143
+
114
144
  // src/Domain/Rocket.ts
115
145
  import { AggregateRoot } from "@gravito/enterprise";
116
146
 
@@ -141,6 +171,28 @@ var RefurbishmentCompleted = class extends DomainEvent {
141
171
  this.rocketId = rocketId;
142
172
  }
143
173
  };
174
+ var PoolExhausted = class extends DomainEvent {
175
+ constructor(totalRockets, maxRockets, queueLength) {
176
+ super();
177
+ this.totalRockets = totalRockets;
178
+ this.maxRockets = maxRockets;
179
+ this.queueLength = queueLength;
180
+ }
181
+ };
182
+ var MissionQueued = class extends DomainEvent {
183
+ constructor(missionId, queuePosition) {
184
+ super();
185
+ this.missionId = missionId;
186
+ this.queuePosition = queuePosition;
187
+ }
188
+ };
189
+ var MissionQueueTimeout = class extends DomainEvent {
190
+ constructor(missionId, waitTimeMs) {
191
+ super();
192
+ this.missionId = missionId;
193
+ this.waitTimeMs = waitTimeMs;
194
+ }
195
+ };
144
196
 
145
197
  // src/Domain/RocketStatus.ts
146
198
  var RocketStatus = /* @__PURE__ */ ((RocketStatus2) => {
@@ -228,6 +280,23 @@ var Rocket = class _Rocket extends AggregateRoot {
228
280
  decommission() {
229
281
  this._status = "DECOMMISSIONED" /* DECOMMISSIONED */;
230
282
  }
283
+ /**
284
+ * 替換底層容器(僅在 REFURBISHING 狀態時允許)
285
+ * 用於 destroy-recreate 策略
286
+ *
287
+ * @param newContainerId - 新容器 ID
288
+ * @throws {Error} 當火箭不在 REFURBISHING 狀態時
289
+ *
290
+ * @public
291
+ * @since 1.3.0
292
+ */
293
+ replaceContainer(newContainerId) {
294
+ if (this._status !== "REFURBISHING" /* REFURBISHING */) {
295
+ throw new Error(`\u7121\u6CD5\u66FF\u63DB\u5BB9\u5668\uFF1A\u706B\u7BAD ${this.id} \u4E0D\u5728 REFURBISHING \u72C0\u614B`);
296
+ }
297
+ this._containerId = newContainerId;
298
+ this._assignedDomain = null;
299
+ }
231
300
  toJSON() {
232
301
  return {
233
302
  id: this.id,
@@ -246,44 +315,219 @@ var Rocket = class _Rocket extends AggregateRoot {
246
315
  }
247
316
  };
248
317
 
318
+ // src/Application/MissionQueue.ts
319
+ var MissionQueue = class {
320
+ queue = [];
321
+ maxSize;
322
+ timeoutMs;
323
+ constructor(maxSize = 50, timeoutMs = 3e4) {
324
+ this.maxSize = maxSize;
325
+ this.timeoutMs = timeoutMs;
326
+ }
327
+ get length() {
328
+ return this.queue.length;
329
+ }
330
+ get isFull() {
331
+ return this.queue.length >= this.maxSize;
332
+ }
333
+ /**
334
+ * 將任務加入隊列,返回 Promise 等待分配
335
+ */
336
+ enqueue(mission) {
337
+ if (this.isFull) {
338
+ return Promise.reject(
339
+ new PoolExhaustedException(`\u4EFB\u52D9\u968A\u5217\u5DF2\u6EFF\uFF08${this.maxSize}\uFF09\uFF0C\u7121\u6CD5\u63A5\u53D7\u65B0\u4EFB\u52D9`)
340
+ );
341
+ }
342
+ return new Promise((resolve, reject) => {
343
+ const timeoutId = setTimeout(() => {
344
+ this.removeFromQueue(mission.id);
345
+ reject(new QueueTimeoutException(`\u4EFB\u52D9 ${mission.id} \u7B49\u5F85\u8D85\u6642`));
346
+ }, this.timeoutMs);
347
+ this.queue.push({
348
+ mission,
349
+ resolve,
350
+ reject,
351
+ enqueuedAt: Date.now(),
352
+ timeoutId
353
+ });
354
+ console.log(
355
+ `[MissionQueue] \u4EFB\u52D9 ${mission.id} \u5DF2\u52A0\u5165\u968A\u5217\uFF0C\u7576\u524D\u968A\u5217\u9577\u5EA6: ${this.queue.length}`
356
+ );
357
+ });
358
+ }
359
+ /**
360
+ * 取出下一個等待的任務
361
+ */
362
+ dequeue() {
363
+ const item = this.queue.shift();
364
+ if (item) {
365
+ clearTimeout(item.timeoutId);
366
+ }
367
+ return item ?? null;
368
+ }
369
+ /**
370
+ * 從隊列中移除指定任務
371
+ */
372
+ removeFromQueue(missionId) {
373
+ const index = this.queue.findIndex((q) => q.mission.id === missionId);
374
+ if (index >= 0) {
375
+ const [removed] = this.queue.splice(index, 1);
376
+ clearTimeout(removed.timeoutId);
377
+ }
378
+ }
379
+ /**
380
+ * 清空隊列並拒絕所有等待的任務
381
+ */
382
+ clear(reason) {
383
+ for (const item of this.queue) {
384
+ clearTimeout(item.timeoutId);
385
+ item.reject(new Error(reason));
386
+ }
387
+ this.queue = [];
388
+ }
389
+ /**
390
+ * 取得隊列統計資訊
391
+ */
392
+ getStats() {
393
+ const oldestWaitMs = this.queue.length > 0 ? Date.now() - this.queue[0].enqueuedAt : null;
394
+ return {
395
+ length: this.queue.length,
396
+ maxSize: this.maxSize,
397
+ oldestWaitMs
398
+ };
399
+ }
400
+ };
401
+ var PoolExhaustedException = class extends Error {
402
+ constructor(message) {
403
+ super(message);
404
+ this.name = "PoolExhaustedException";
405
+ }
406
+ };
407
+ var QueueTimeoutException = class extends Error {
408
+ constructor(message) {
409
+ super(message);
410
+ this.name = "QueueTimeoutException";
411
+ }
412
+ };
413
+
249
414
  // src/Application/PoolManager.ts
250
415
  var PoolManager = class {
251
- constructor(dockerAdapter, rocketRepository, refurbishUnit, router) {
416
+ constructor(dockerAdapter, rocketRepository, refurbishUnit, router, config) {
252
417
  this.dockerAdapter = dockerAdapter;
253
418
  this.rocketRepository = rocketRepository;
254
419
  this.refurbishUnit = refurbishUnit;
255
420
  this.router = router;
421
+ this.config = { ...DEFAULT_POOL_CONFIG, ...config };
422
+ this.missionQueue = new MissionQueue(this.config.maxQueueSize, this.config.queueTimeoutMs);
423
+ }
424
+ config;
425
+ missionQueue;
426
+ dynamicRocketCount = 0;
427
+ /**
428
+ * 取得當前 Pool 狀態
429
+ */
430
+ async getPoolStatus() {
431
+ const rockets = await this.rocketRepository.findAll();
432
+ const statusCounts = {
433
+ total: rockets.length,
434
+ idle: 0,
435
+ orbiting: 0,
436
+ refurbishing: 0,
437
+ decommissioned: 0
438
+ };
439
+ for (const rocket of rockets) {
440
+ switch (rocket.status) {
441
+ case "IDLE" /* IDLE */:
442
+ statusCounts.idle++;
443
+ break;
444
+ case "PREPARING" /* PREPARING */:
445
+ case "ORBITING" /* ORBITING */:
446
+ statusCounts.orbiting++;
447
+ break;
448
+ case "REFURBISHING" /* REFURBISHING */:
449
+ statusCounts.refurbishing++;
450
+ break;
451
+ case "DECOMMISSIONED" /* DECOMMISSIONED */:
452
+ statusCounts.decommissioned++;
453
+ break;
454
+ }
455
+ }
456
+ return {
457
+ ...statusCounts,
458
+ dynamicCount: this.dynamicRocketCount,
459
+ maxRockets: this.config.maxRockets,
460
+ queueLength: this.missionQueue.length
461
+ };
256
462
  }
257
463
  /**
258
464
  * 初始化發射場:預先準備指定數量的火箭
259
465
  */
260
466
  async warmup(count) {
467
+ const targetCount = count ?? this.config.warmupCount;
261
468
  const currentRockets = await this.rocketRepository.findAll();
262
- const needed = count - currentRockets.length;
469
+ const activeRockets = currentRockets.filter((r) => r.status !== "DECOMMISSIONED" /* DECOMMISSIONED */);
470
+ const needed = Math.min(
471
+ targetCount - activeRockets.length,
472
+ this.config.maxRockets - activeRockets.length
473
+ );
263
474
  if (needed <= 0) {
475
+ console.log(`[PoolManager] \u7121\u9700\u71B1\u6A5F\uFF0C\u7576\u524D\u5DF2\u6709 ${activeRockets.length} \u67B6\u706B\u7BAD`);
264
476
  return;
265
477
  }
266
- console.log(`[LaunchPad] \u6B63\u5728\u71B1\u6A5F\uFF0C\u6E96\u5099\u767C\u5C04 ${needed} \u67B6\u65B0\u706B\u7BAD...`);
478
+ console.log(`[PoolManager] \u6B63\u5728\u71B1\u6A5F\uFF0C\u6E96\u5099\u767C\u5C04 ${needed} \u67B6\u65B0\u706B\u7BAD...`);
267
479
  for (let i = 0; i < needed; i++) {
268
480
  const containerId = await this.dockerAdapter.createBaseContainer();
269
481
  const rocketId = `rocket-${crypto.randomUUID()}`;
270
482
  const rocket = new Rocket(rocketId, containerId);
271
483
  await this.rocketRepository.save(rocket);
272
484
  }
485
+ console.log(`[PoolManager] \u71B1\u6A5F\u5B8C\u6210\uFF0CPool \u7576\u524D\u5171 ${activeRockets.length + needed} \u67B6\u706B\u7BAD`);
273
486
  }
274
487
  /**
275
488
  * 獲取一架可用的火箭並分配任務
489
+ *
490
+ * @throws {PoolExhaustedException} 當 Pool 耗盡且無法處理請求時
276
491
  */
277
492
  async assignMission(mission) {
278
493
  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);
494
+ if (rocket) {
495
+ rocket.assignMission(mission);
496
+ await this.rocketRepository.save(rocket);
497
+ return rocket;
498
+ }
499
+ const poolStatus = await this.getPoolStatus();
500
+ const activeCount = poolStatus.total - poolStatus.decommissioned;
501
+ console.log(
502
+ `[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}`
503
+ );
504
+ switch (this.config.exhaustionStrategy) {
505
+ case "reject":
506
+ throw new PoolExhaustedException(`Rocket Pool \u5DF2\u8017\u76E1\uFF0C\u7121\u6CD5\u8655\u7406\u4EFB\u52D9 ${mission.id}`);
507
+ case "dynamic": {
508
+ if (activeCount < this.config.maxRockets && this.dynamicRocketCount < (this.config.dynamicLimit ?? 5)) {
509
+ console.log(`[PoolManager] \u52D5\u614B\u5EFA\u7ACB\u5F8C\u63F4\u706B\u7BAD...`);
510
+ const containerId = await this.dockerAdapter.createBaseContainer();
511
+ rocket = new Rocket(`rocket-dynamic-${Date.now()}`, containerId);
512
+ this.dynamicRocketCount++;
513
+ rocket.assignMission(mission);
514
+ await this.rocketRepository.save(rocket);
515
+ return rocket;
516
+ }
517
+ if (this.missionQueue.isFull) {
518
+ throw new PoolExhaustedException(`Rocket Pool \u8207\u968A\u5217\u5747\u5DF2\u6EFF\uFF0C\u7121\u6CD5\u8655\u7406\u4EFB\u52D9 ${mission.id}`);
519
+ }
520
+ console.log(`[PoolManager] \u4EFB\u52D9 ${mission.id} \u9032\u5165\u7B49\u5F85\u968A\u5217`);
521
+ return this.missionQueue.enqueue(mission);
522
+ }
523
+ case "queue":
524
+ default:
525
+ if (this.missionQueue.isFull) {
526
+ throw new PoolExhaustedException(`Rocket Pool \u8207\u968A\u5217\u5747\u5DF2\u6EFF\uFF0C\u7121\u6CD5\u8655\u7406\u4EFB\u52D9 ${mission.id}`);
527
+ }
528
+ console.log(`[PoolManager] \u4EFB\u52D9 ${mission.id} \u9032\u5165\u7B49\u5F85\u968A\u5217`);
529
+ return this.missionQueue.enqueue(mission);
283
530
  }
284
- rocket.assignMission(mission);
285
- await this.rocketRepository.save(rocket);
286
- return rocket;
287
531
  }
288
532
  /**
289
533
  * 回收指定任務的火箭
@@ -292,7 +536,7 @@ var PoolManager = class {
292
536
  const allRockets = await this.rocketRepository.findAll();
293
537
  const rocket = allRockets.find((r) => r.currentMission?.id === missionId);
294
538
  if (!rocket) {
295
- console.warn(`[LaunchPad] \u627E\u4E0D\u5230\u5C6C\u65BC\u4EFB\u52D9 ${missionId} \u7684\u706B\u7BAD`);
539
+ console.warn(`[PoolManager] \u627E\u4E0D\u5230\u5C6C\u65BC\u4EFB\u52D9 ${missionId} \u7684\u706B\u7BAD`);
296
540
  return;
297
541
  }
298
542
  if (this.router && rocket.assignedDomain) {
@@ -305,30 +549,67 @@ var PoolManager = class {
305
549
  rocket.finishRefurbishment();
306
550
  }
307
551
  await this.rocketRepository.save(rocket);
552
+ await this.processQueue();
553
+ }
554
+ /**
555
+ * 處理等待隊列中的任務
556
+ */
557
+ async processQueue() {
558
+ const queuedItem = this.missionQueue.dequeue();
559
+ if (!queuedItem) {
560
+ return;
561
+ }
562
+ try {
563
+ const rocket = await this.rocketRepository.findIdle();
564
+ if (rocket) {
565
+ rocket.assignMission(queuedItem.mission);
566
+ await this.rocketRepository.save(rocket);
567
+ queuedItem.resolve(rocket);
568
+ console.log(`[PoolManager] \u968A\u5217\u4EFB\u52D9 ${queuedItem.mission.id} \u5DF2\u5206\u914D\u706B\u7BAD ${rocket.id}`);
569
+ } else {
570
+ console.warn(`[PoolManager] \u8655\u7406\u968A\u5217\u6642\u4ECD\u7121\u53EF\u7528\u706B\u7BAD\uFF0C\u4EFB\u52D9\u91CD\u65B0\u5165\u968A`);
571
+ this.missionQueue.enqueue(queuedItem.mission).then(queuedItem.resolve).catch(queuedItem.reject);
572
+ }
573
+ } catch (error) {
574
+ queuedItem.reject(error);
575
+ }
576
+ }
577
+ /**
578
+ * 取得隊列統計資訊
579
+ */
580
+ getQueueStats() {
581
+ return this.missionQueue.getStats();
308
582
  }
309
583
  };
310
584
 
311
585
  // src/Application/RefurbishUnit.ts
312
586
  var RefurbishUnit = class {
313
- constructor(docker) {
587
+ constructor(docker, config) {
314
588
  this.docker = docker;
589
+ this.config = { ...DEFAULT_REFURBISH_CONFIG, ...config };
315
590
  }
591
+ config;
316
592
  /**
317
593
  * 執行火箭翻新邏輯
318
594
  */
319
595
  async refurbish(rocket) {
320
- console.log(`[RefurbishUnit] \u6B63\u5728\u7FFB\u65B0\u706B\u7BAD: ${rocket.id} (\u5BB9\u5668: ${rocket.containerId})`);
596
+ console.log(
597
+ `[RefurbishUnit] \u6B63\u5728\u7FFB\u65B0\u706B\u7BAD: ${rocket.id} (\u7B56\u7565: ${this.config.strategy}, \u5BB9\u5668: ${rocket.containerId})`
598
+ );
321
599
  rocket.splashDown();
322
600
  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);
601
+ const commands = this.config.cleanupCommands ?? [];
602
+ const fullCommand = commands.join(" && ");
603
+ const result = await this.docker.executeCommand(rocket.containerId, ["sh", "-c", fullCommand]);
325
604
  if (result.exitCode !== 0) {
326
605
  console.error(`[RefurbishUnit] \u6E05\u7406\u5931\u6557: ${result.stderr}`);
327
- rocket.decommission();
328
- return;
606
+ if (this.config.failureAction === "decommission") {
607
+ rocket.decommission();
608
+ return;
609
+ }
329
610
  }
330
611
  rocket.finishRefurbishment();
331
- console.log(`[RefurbishUnit] \u706B\u7BAD ${rocket.id} \u7FFB\u65B0\u5B8C\u6210\uFF0C\u5DF2\u9032\u5165 IDLE \u72C0\u614B\u3002`);
612
+ console.log(`[RefurbishUnit] \u706B\u7BAD ${rocket.id} \u7FFB\u65B0\u5B8C\u6210\uFF0C\u5DF2\u9032\u5165 IDLE \u72C0\u614B`);
332
613
  } catch (error) {
333
614
  console.error(`[RefurbishUnit] \u56DE\u6536\u904E\u7A0B\u767C\u751F\u7570\u5E38:`, error);
334
615
  rocket.decommission();
@@ -361,8 +642,15 @@ import { getRuntimeAdapter } from "@gravito/core";
361
642
  var DockerAdapter = class {
362
643
  baseImage = "oven/bun:1.0-slim";
363
644
  runtime = getRuntimeAdapter();
645
+ // 快取目錄配置
646
+ cacheConfig = {
647
+ hostCachePath: process.env.BUN_CACHE_PATH || `${process.env.HOME}/.bun/install/cache`,
648
+ containerCachePathRoot: "/root/.bun/install/cache",
649
+ containerCachePathBun: "/home/bun/.bun/install/cache"
650
+ };
364
651
  async createBaseContainer() {
365
652
  const rocketId = `rocket-${crypto.randomUUID()}`;
653
+ await this.ensureCacheDirectory();
366
654
  const proc = this.runtime.spawn([
367
655
  "docker",
368
656
  "run",
@@ -374,10 +662,39 @@ var DockerAdapter = class {
374
662
  "-p",
375
663
  "3000",
376
664
  // 讓 Docker 分配隨機宿主機埠
665
+ // === 快取掛載(關鍵) ===
377
666
  "-v",
378
- `${process.env.HOME}/.bun/install/cache:/root/.bun/install/cache`,
667
+ `${this.cacheConfig.hostCachePath}:${this.cacheConfig.containerCachePathRoot}:rw`,
379
668
  "-v",
380
- `${process.env.HOME}/.bun/install/cache:/home/bun/.bun/install/cache`,
669
+ `${this.cacheConfig.hostCachePath}:${this.cacheConfig.containerCachePathBun}:rw`,
670
+ // === 環境變數配置(確保 bun 使用快取) ===
671
+ "-e",
672
+ `BUN_INSTALL_CACHE_DIR=${this.cacheConfig.containerCachePathBun}`,
673
+ "-e",
674
+ "BUN_INSTALL_CACHE=shared",
675
+ "-e",
676
+ "BUN_INSTALL_PREFER_OFFLINE=true",
677
+ // === 效能相關環境變數 ===
678
+ "-e",
679
+ "NODE_ENV=development",
680
+ // === 資源限制(防止單一容器占用過多資源) ===
681
+ "--memory",
682
+ "1g",
683
+ "--memory-swap",
684
+ "1g",
685
+ "--cpus",
686
+ "1.0",
687
+ // === 安全設定 ===
688
+ "--security-opt",
689
+ "no-new-privileges:true",
690
+ "--cap-drop",
691
+ "ALL",
692
+ "--cap-add",
693
+ "CHOWN",
694
+ "--cap-add",
695
+ "SETUID",
696
+ "--cap-add",
697
+ "SETGID",
381
698
  this.baseImage,
382
699
  "tail",
383
700
  "-f",
@@ -387,6 +704,7 @@ var DockerAdapter = class {
387
704
  const containerId = stdout.trim();
388
705
  const exitCode = await proc.exited;
389
706
  if (containerId.length === 64 && /^[0-9a-f]+$/.test(containerId)) {
707
+ await this.verifyCacheMount(containerId);
390
708
  return containerId;
391
709
  }
392
710
  if (exitCode !== 0) {
@@ -395,6 +713,37 @@ var DockerAdapter = class {
395
713
  }
396
714
  return containerId;
397
715
  }
716
+ /**
717
+ * 確保快取目錄存在
718
+ *
719
+ * @private
720
+ * @since 1.3.0
721
+ */
722
+ async ensureCacheDirectory() {
723
+ const cachePath = this.cacheConfig.hostCachePath;
724
+ const proc = this.runtime.spawn(["mkdir", "-p", cachePath]);
725
+ await proc.exited;
726
+ const chmodProc = this.runtime.spawn(["chmod", "777", cachePath]);
727
+ await chmodProc.exited;
728
+ }
729
+ /**
730
+ * 驗證快取是否正確掛載
731
+ *
732
+ * @private
733
+ * @since 1.3.0
734
+ */
735
+ async verifyCacheMount(containerId) {
736
+ const result = await this.executeCommand(containerId, [
737
+ "sh",
738
+ "-c",
739
+ `ls -la ${this.cacheConfig.containerCachePathBun} 2>/dev/null || echo 'NOT_MOUNTED'`
740
+ ]);
741
+ if (result.stdout.includes("NOT_MOUNTED")) {
742
+ console.warn(`[DockerAdapter] \u8B66\u544A: \u5BB9\u5668 ${containerId} \u7684\u5FEB\u53D6\u76EE\u9304\u53EF\u80FD\u672A\u6B63\u78BA\u639B\u8F09`);
743
+ } else {
744
+ console.log(`[DockerAdapter] \u5FEB\u53D6\u76EE\u9304\u5DF2\u78BA\u8A8D\u639B\u8F09: ${containerId}`);
745
+ }
746
+ }
398
747
  async getExposedPort(containerId, containerPort = 3e3) {
399
748
  const proc = this.runtime.spawn(["docker", "port", containerId, containerPort.toString()]);
400
749
  const stdout = await new Response(proc.stdout ?? null).text();
@@ -774,13 +1123,25 @@ async function bootstrapLaunchpad() {
774
1123
  };
775
1124
  }
776
1125
  export {
1126
+ DEFAULT_POOL_CONFIG,
1127
+ DEFAULT_REFURBISH_CONFIG,
777
1128
  LaunchpadOrbit,
778
1129
  Mission,
1130
+ MissionAssigned,
779
1131
  MissionControl,
1132
+ MissionQueue,
1133
+ MissionQueueTimeout,
1134
+ MissionQueued,
780
1135
  PayloadInjector,
1136
+ PoolExhausted,
1137
+ PoolExhaustedException,
781
1138
  PoolManager,
1139
+ QueueTimeoutException,
782
1140
  RefurbishUnit,
1141
+ RefurbishmentCompleted,
783
1142
  Rocket,
1143
+ RocketIgnited,
1144
+ RocketSplashedDown,
784
1145
  RocketStatus,
785
1146
  bootstrapLaunchpad
786
1147
  };
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@gravito/launchpad",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "Container lifecycle management system for flash deployments",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
8
8
  "scripts": {
9
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": "bun tsc -p tsconfig.json --noEmit --skipLibCheck"
10
+ "test": "bun test --timeout=10000",
11
+ "test:coverage": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
12
+ "test:ci": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
13
+ "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
14
+ "test:unit": "bun test tests/ --timeout=10000",
15
+ "test:integration": "test $(find tests -name '*.integration.test.ts' 2>/dev/null | wc -l) -gt 0 && find tests -name '*.integration.test.ts' -print0 | xargs -0 bun test --timeout=10000 || echo 'No integration tests found'"
14
16
  },
15
17
  "dependencies": {
16
18
  "@gravito/enterprise": "workspace:*",
@@ -0,0 +1,64 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ const lcovPath = process.argv[2] ?? 'coverage/lcov.info'
5
+ const threshold = Number.parseFloat(process.env.COVERAGE_THRESHOLD ?? '80')
6
+
7
+ const root = resolve(process.cwd())
8
+ const srcRoot = `${resolve(root, 'src')}/`
9
+
10
+ // 檢查 lcov.info 是否存在
11
+ if (!existsSync(lcovPath)) {
12
+ console.error(`Coverage file not found: ${lcovPath}`)
13
+ process.exit(1)
14
+ }
15
+
16
+ let content: string
17
+ try {
18
+ content = readFileSync(lcovPath, 'utf-8')
19
+ } catch (error) {
20
+ console.error(`Failed to read coverage file: ${error}`)
21
+ process.exit(1)
22
+ }
23
+
24
+ const lines = content.split('\n')
25
+
26
+ let currentFile: string | null = null
27
+ let total = 0
28
+ let hit = 0
29
+
30
+ for (const line of lines) {
31
+ if (line.startsWith('SF:')) {
32
+ const filePath = line.slice(3).trim()
33
+ const abs = resolve(root, filePath)
34
+ currentFile = abs.startsWith(srcRoot) ? abs : null
35
+ continue
36
+ }
37
+
38
+ if (!currentFile) {
39
+ continue
40
+ }
41
+
42
+ if (line.startsWith('DA:')) {
43
+ const parts = line.slice(3).split(',')
44
+ if (parts.length >= 2) {
45
+ total += 1
46
+ const count = Number.parseInt(parts[1] ?? '0', 10)
47
+ if (count > 0) {
48
+ hit += 1
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ const percent = total === 0 ? 0 : (hit / total) * 100
55
+ const rounded = Math.round(percent * 100) / 100
56
+
57
+ if (rounded < threshold) {
58
+ console.error(
59
+ `launchpad coverage ${rounded}% is below threshold ${threshold}%. Covered lines: ${hit}/${total}.`
60
+ )
61
+ process.exit(1)
62
+ }
63
+
64
+ console.log(`launchpad coverage ${rounded}% (${hit}/${total}) meets threshold ${threshold}%.`)