@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.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
- const rocketId = `rocket-${Math.random().toString(36).substring(2, 9)}`;
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
- const rocketId = `rocket-${Math.random().toString(36).substring(2, 9)}`;
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();
@@ -477,7 +826,7 @@ import { getRuntimeAdapter as getRuntimeAdapter2 } from "@gravito/core";
477
826
  var ShellGitAdapter = class {
478
827
  baseDir = "/tmp/gravito-launchpad-git";
479
828
  async clone(repoUrl, branch) {
480
- const dirName = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
829
+ const dirName = `${Date.now()}-${crypto.randomUUID()}`;
481
830
  const targetDir = `${this.baseDir}/${dirName}`;
482
831
  await mkdir(this.baseDir, { recursive: true });
483
832
  const runtime = getRuntimeAdapter2();
@@ -672,9 +1021,19 @@ var LaunchpadServiceProvider = class extends ServiceProvider {
672
1021
  }
673
1022
  };
674
1023
  var LaunchpadOrbit = class {
1024
+ /**
1025
+ * Create a new LaunchpadOrbit instance.
1026
+ * @param ripple - Ripple instance for real-time telemetry communication.
1027
+ */
675
1028
  constructor(ripple) {
676
1029
  this.ripple = ripple;
677
1030
  }
1031
+ /**
1032
+ * Install the Launchpad orbit into PlanetCore.
1033
+ * Registers the service provider and sets up webhook routes for deployment.
1034
+ *
1035
+ * @param core - The PlanetCore instance.
1036
+ */
678
1037
  async install(core) {
679
1038
  core.register(new LaunchpadServiceProvider());
680
1039
  core.router.post("/launch", async (c) => {
@@ -748,12 +1107,7 @@ async function bootstrapLaunchpad() {
748
1107
  PORT: 4e3,
749
1108
  CACHE_DRIVER: "file"
750
1109
  },
751
- orbits: [
752
- new OrbitCache(),
753
- ripple,
754
- new LaunchpadOrbit(ripple)
755
- // 傳入實例
756
- ]
1110
+ orbits: [new OrbitCache(), ripple, new LaunchpadOrbit(ripple)]
757
1111
  });
758
1112
  await core.bootstrap();
759
1113
  const liftoffConfig = core.liftoff();
@@ -769,13 +1123,25 @@ async function bootstrapLaunchpad() {
769
1123
  };
770
1124
  }
771
1125
  export {
1126
+ DEFAULT_POOL_CONFIG,
1127
+ DEFAULT_REFURBISH_CONFIG,
772
1128
  LaunchpadOrbit,
773
1129
  Mission,
1130
+ MissionAssigned,
774
1131
  MissionControl,
1132
+ MissionQueue,
1133
+ MissionQueueTimeout,
1134
+ MissionQueued,
775
1135
  PayloadInjector,
1136
+ PoolExhausted,
1137
+ PoolExhaustedException,
776
1138
  PoolManager,
1139
+ QueueTimeoutException,
777
1140
  RefurbishUnit,
1141
+ RefurbishmentCompleted,
778
1142
  Rocket,
1143
+ RocketIgnited,
1144
+ RocketSplashedDown,
779
1145
  RocketStatus,
780
1146
  bootstrapLaunchpad
781
1147
  };
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@gravito/launchpad",
3
- "version": "1.2.1",
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}%.`)