@gravito/launchpad 1.2.2 → 1.3.2

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,218 @@ 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
+ default:
524
+ if (this.missionQueue.isFull) {
525
+ throw new PoolExhaustedException(`Rocket Pool \u8207\u968A\u5217\u5747\u5DF2\u6EFF\uFF0C\u7121\u6CD5\u8655\u7406\u4EFB\u52D9 ${mission.id}`);
526
+ }
527
+ console.log(`[PoolManager] \u4EFB\u52D9 ${mission.id} \u9032\u5165\u7B49\u5F85\u968A\u5217`);
528
+ return this.missionQueue.enqueue(mission);
283
529
  }
284
- rocket.assignMission(mission);
285
- await this.rocketRepository.save(rocket);
286
- return rocket;
287
530
  }
288
531
  /**
289
532
  * 回收指定任務的火箭
@@ -292,7 +535,7 @@ var PoolManager = class {
292
535
  const allRockets = await this.rocketRepository.findAll();
293
536
  const rocket = allRockets.find((r) => r.currentMission?.id === missionId);
294
537
  if (!rocket) {
295
- console.warn(`[LaunchPad] \u627E\u4E0D\u5230\u5C6C\u65BC\u4EFB\u52D9 ${missionId} \u7684\u706B\u7BAD`);
538
+ console.warn(`[PoolManager] \u627E\u4E0D\u5230\u5C6C\u65BC\u4EFB\u52D9 ${missionId} \u7684\u706B\u7BAD`);
296
539
  return;
297
540
  }
298
541
  if (this.router && rocket.assignedDomain) {
@@ -305,30 +548,67 @@ var PoolManager = class {
305
548
  rocket.finishRefurbishment();
306
549
  }
307
550
  await this.rocketRepository.save(rocket);
551
+ await this.processQueue();
552
+ }
553
+ /**
554
+ * 處理等待隊列中的任務
555
+ */
556
+ async processQueue() {
557
+ const queuedItem = this.missionQueue.dequeue();
558
+ if (!queuedItem) {
559
+ return;
560
+ }
561
+ try {
562
+ const rocket = await this.rocketRepository.findIdle();
563
+ if (rocket) {
564
+ rocket.assignMission(queuedItem.mission);
565
+ await this.rocketRepository.save(rocket);
566
+ queuedItem.resolve(rocket);
567
+ console.log(`[PoolManager] \u968A\u5217\u4EFB\u52D9 ${queuedItem.mission.id} \u5DF2\u5206\u914D\u706B\u7BAD ${rocket.id}`);
568
+ } else {
569
+ console.warn(`[PoolManager] \u8655\u7406\u968A\u5217\u6642\u4ECD\u7121\u53EF\u7528\u706B\u7BAD\uFF0C\u4EFB\u52D9\u91CD\u65B0\u5165\u968A`);
570
+ this.missionQueue.enqueue(queuedItem.mission).then(queuedItem.resolve).catch(queuedItem.reject);
571
+ }
572
+ } catch (error) {
573
+ queuedItem.reject(error);
574
+ }
575
+ }
576
+ /**
577
+ * 取得隊列統計資訊
578
+ */
579
+ getQueueStats() {
580
+ return this.missionQueue.getStats();
308
581
  }
309
582
  };
310
583
 
311
584
  // src/Application/RefurbishUnit.ts
312
585
  var RefurbishUnit = class {
313
- constructor(docker) {
586
+ constructor(docker, config) {
314
587
  this.docker = docker;
588
+ this.config = { ...DEFAULT_REFURBISH_CONFIG, ...config };
315
589
  }
590
+ config;
316
591
  /**
317
592
  * 執行火箭翻新邏輯
318
593
  */
319
594
  async refurbish(rocket) {
320
- console.log(`[RefurbishUnit] \u6B63\u5728\u7FFB\u65B0\u706B\u7BAD: ${rocket.id} (\u5BB9\u5668: ${rocket.containerId})`);
595
+ console.log(
596
+ `[RefurbishUnit] \u6B63\u5728\u7FFB\u65B0\u706B\u7BAD: ${rocket.id} (\u7B56\u7565: ${this.config.strategy}, \u5BB9\u5668: ${rocket.containerId})`
597
+ );
321
598
  rocket.splashDown();
322
599
  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);
600
+ const commands = this.config.cleanupCommands ?? [];
601
+ const fullCommand = commands.join(" && ");
602
+ const result = await this.docker.executeCommand(rocket.containerId, ["sh", "-c", fullCommand]);
325
603
  if (result.exitCode !== 0) {
326
604
  console.error(`[RefurbishUnit] \u6E05\u7406\u5931\u6557: ${result.stderr}`);
327
- rocket.decommission();
328
- return;
605
+ if (this.config.failureAction === "decommission") {
606
+ rocket.decommission();
607
+ return;
608
+ }
329
609
  }
330
610
  rocket.finishRefurbishment();
331
- console.log(`[RefurbishUnit] \u706B\u7BAD ${rocket.id} \u7FFB\u65B0\u5B8C\u6210\uFF0C\u5DF2\u9032\u5165 IDLE \u72C0\u614B\u3002`);
611
+ console.log(`[RefurbishUnit] \u706B\u7BAD ${rocket.id} \u7FFB\u65B0\u5B8C\u6210\uFF0C\u5DF2\u9032\u5165 IDLE \u72C0\u614B`);
332
612
  } catch (error) {
333
613
  console.error(`[RefurbishUnit] \u56DE\u6536\u904E\u7A0B\u767C\u751F\u7570\u5E38:`, error);
334
614
  rocket.decommission();
@@ -361,8 +641,15 @@ import { getRuntimeAdapter } from "@gravito/core";
361
641
  var DockerAdapter = class {
362
642
  baseImage = "oven/bun:1.0-slim";
363
643
  runtime = getRuntimeAdapter();
644
+ // 快取目錄配置
645
+ cacheConfig = {
646
+ hostCachePath: process.env.BUN_CACHE_PATH || `${process.env.HOME}/.bun/install/cache`,
647
+ containerCachePathRoot: "/root/.bun/install/cache",
648
+ containerCachePathBun: "/home/bun/.bun/install/cache"
649
+ };
364
650
  async createBaseContainer() {
365
651
  const rocketId = `rocket-${crypto.randomUUID()}`;
652
+ await this.ensureCacheDirectory();
366
653
  const proc = this.runtime.spawn([
367
654
  "docker",
368
655
  "run",
@@ -374,10 +661,39 @@ var DockerAdapter = class {
374
661
  "-p",
375
662
  "3000",
376
663
  // 讓 Docker 分配隨機宿主機埠
664
+ // === 快取掛載(關鍵) ===
377
665
  "-v",
378
- `${process.env.HOME}/.bun/install/cache:/root/.bun/install/cache`,
666
+ `${this.cacheConfig.hostCachePath}:${this.cacheConfig.containerCachePathRoot}:rw`,
379
667
  "-v",
380
- `${process.env.HOME}/.bun/install/cache:/home/bun/.bun/install/cache`,
668
+ `${this.cacheConfig.hostCachePath}:${this.cacheConfig.containerCachePathBun}:rw`,
669
+ // === 環境變數配置(確保 bun 使用快取) ===
670
+ "-e",
671
+ `BUN_INSTALL_CACHE_DIR=${this.cacheConfig.containerCachePathBun}`,
672
+ "-e",
673
+ "BUN_INSTALL_CACHE=shared",
674
+ "-e",
675
+ "BUN_INSTALL_PREFER_OFFLINE=true",
676
+ // === 效能相關環境變數 ===
677
+ "-e",
678
+ "NODE_ENV=development",
679
+ // === 資源限制(防止單一容器占用過多資源) ===
680
+ "--memory",
681
+ "1g",
682
+ "--memory-swap",
683
+ "1g",
684
+ "--cpus",
685
+ "1.0",
686
+ // === 安全設定 ===
687
+ "--security-opt",
688
+ "no-new-privileges:true",
689
+ "--cap-drop",
690
+ "ALL",
691
+ "--cap-add",
692
+ "CHOWN",
693
+ "--cap-add",
694
+ "SETUID",
695
+ "--cap-add",
696
+ "SETGID",
381
697
  this.baseImage,
382
698
  "tail",
383
699
  "-f",
@@ -387,6 +703,7 @@ var DockerAdapter = class {
387
703
  const containerId = stdout.trim();
388
704
  const exitCode = await proc.exited;
389
705
  if (containerId.length === 64 && /^[0-9a-f]+$/.test(containerId)) {
706
+ await this.verifyCacheMount(containerId);
390
707
  return containerId;
391
708
  }
392
709
  if (exitCode !== 0) {
@@ -395,6 +712,37 @@ var DockerAdapter = class {
395
712
  }
396
713
  return containerId;
397
714
  }
715
+ /**
716
+ * 確保快取目錄存在
717
+ *
718
+ * @private
719
+ * @since 1.3.0
720
+ */
721
+ async ensureCacheDirectory() {
722
+ const cachePath = this.cacheConfig.hostCachePath;
723
+ const proc = this.runtime.spawn(["mkdir", "-p", cachePath]);
724
+ await proc.exited;
725
+ const chmodProc = this.runtime.spawn(["chmod", "777", cachePath]);
726
+ await chmodProc.exited;
727
+ }
728
+ /**
729
+ * 驗證快取是否正確掛載
730
+ *
731
+ * @private
732
+ * @since 1.3.0
733
+ */
734
+ async verifyCacheMount(containerId) {
735
+ const result = await this.executeCommand(containerId, [
736
+ "sh",
737
+ "-c",
738
+ `ls -la ${this.cacheConfig.containerCachePathBun} 2>/dev/null || echo 'NOT_MOUNTED'`
739
+ ]);
740
+ if (result.stdout.includes("NOT_MOUNTED")) {
741
+ console.warn(`[DockerAdapter] \u8B66\u544A: \u5BB9\u5668 ${containerId} \u7684\u5FEB\u53D6\u76EE\u9304\u53EF\u80FD\u672A\u6B63\u78BA\u639B\u8F09`);
742
+ } else {
743
+ console.log(`[DockerAdapter] \u5FEB\u53D6\u76EE\u9304\u5DF2\u78BA\u8A8D\u639B\u8F09: ${containerId}`);
744
+ }
745
+ }
398
746
  async getExposedPort(containerId, containerPort = 3e3) {
399
747
  const proc = this.runtime.spawn(["docker", "port", containerId, containerPort.toString()]);
400
748
  const stdout = await new Response(proc.stdout ?? null).text();
@@ -774,13 +1122,25 @@ async function bootstrapLaunchpad() {
774
1122
  };
775
1123
  }
776
1124
  export {
1125
+ DEFAULT_POOL_CONFIG,
1126
+ DEFAULT_REFURBISH_CONFIG,
777
1127
  LaunchpadOrbit,
778
1128
  Mission,
1129
+ MissionAssigned,
779
1130
  MissionControl,
1131
+ MissionQueue,
1132
+ MissionQueueTimeout,
1133
+ MissionQueued,
780
1134
  PayloadInjector,
1135
+ PoolExhausted,
1136
+ PoolExhaustedException,
781
1137
  PoolManager,
1138
+ QueueTimeoutException,
782
1139
  RefurbishUnit,
1140
+ RefurbishmentCompleted,
783
1141
  Rocket,
1142
+ RocketIgnited,
1143
+ RocketSplashedDown,
784
1144
  RocketStatus,
785
1145
  bootstrapLaunchpad
786
1146
  };
package/package.json CHANGED
@@ -1,23 +1,25 @@
1
1
  {
2
2
  "name": "@gravito/launchpad",
3
- "version": "1.2.2",
3
+ "version": "1.3.2",
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
- "@gravito/enterprise": "workspace:*",
17
- "@gravito/ripple": "workspace:*",
18
- "@gravito/stasis": "workspace:*",
18
+ "@gravito/enterprise": "^1.0.4",
19
+ "@gravito/ripple": "^4.0.1",
20
+ "@gravito/stasis": "^3.1.1",
19
21
  "@octokit/rest": "^22.0.1",
20
- "@gravito/core": "workspace:*"
22
+ "@gravito/core": "^1.6.1"
21
23
  },
22
24
  "devDependencies": {
23
25
  "bun-types": "^1.3.5",
@@ -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}%.`)