nekostream 2026.4.1 → 2026.4.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/README.md +169 -90
- package/dist/cli/application.js +24 -0
- package/dist/cli/domain-startup.js +85 -0
- package/dist/cli/feedback.js +87 -0
- package/dist/cli/flows/anime-flow.js +225 -0
- package/dist/cli/flows/auth-flow.js +39 -0
- package/dist/cli/flows/provider-lists.js +72 -0
- package/dist/cli/menus/history-menu.js +65 -0
- package/dist/cli/menus/home-menu.js +198 -0
- package/dist/cli/menus/settings-menu.js +215 -0
- package/dist/cli/update-check.js +82 -0
- package/dist/index.js +9 -817
- package/dist/player-policy.js +96 -0
- package/dist/player.js +7 -1
- package/dist/prompts-wrapper.js +9 -6
- package/dist/provider-types.js +28 -0
- package/dist/providers.js +17 -17
- package/dist/scrapers/adblock-policy.js +148 -0
- package/dist/scrapers/anilist-api.js +1 -25
- package/dist/scrapers/anilist.js +11 -66
- package/dist/scrapers/auth/anime47/client.js +224 -0
- package/dist/scrapers/auth/anime47/lists.js +185 -0
- package/dist/scrapers/auth/anime47/login.js +230 -0
- package/dist/scrapers/auth/anime47/stream-interceptor.js +169 -0
- package/dist/scrapers/auth/animevietsub/lists.js +209 -0
- package/dist/scrapers/auth/animevietsub/login.js +89 -0
- package/dist/scrapers/auth/animevietsub/parsers.js +147 -0
- package/dist/scrapers/auth/auth-session-service.js +27 -0
- package/dist/scrapers/auth/auth-types.js +2 -0
- package/dist/scrapers/auth/browser-login.js +173 -0
- package/dist/scrapers/auth-service.js +28 -1208
- package/dist/scrapers/base.js +1 -1
- package/dist/scrapers/crawler.js +2 -17
- package/dist/scrapers/domain-registry.js +225 -0
- package/dist/scrapers/domain-resolver.js +308 -0
- package/dist/scrapers/errors.js +119 -0
- package/dist/scrapers/fingerprint.js +4 -14
- package/dist/scrapers/html-utils.js +59 -0
- package/dist/scrapers/http-client.js +58 -0
- package/dist/scrapers/interceptor.js +3 -3
- package/dist/scrapers/providers/anime47/parsers/cards.js +243 -0
- package/dist/scrapers/providers/anime47/parsers/detail.js +174 -0
- package/dist/scrapers/providers/anime47/parsers/episodes.js +160 -0
- package/dist/scrapers/providers/anime47/streams/extractors.js +109 -0
- package/dist/scrapers/providers/anime47.js +58 -603
- package/dist/scrapers/providers/animevietsub/parsers/cards.js +222 -0
- package/dist/scrapers/providers/animevietsub/parsers/detail.js +373 -0
- package/dist/scrapers/providers/animevietsub/parsers/episodes.js +76 -0
- package/dist/scrapers/providers/animevietsub/parsers/schedule.js +95 -0
- package/dist/scrapers/providers/animevietsub/streams/extractors.js +350 -0
- package/dist/scrapers/providers/animevietsub/streams/server-parser.js +222 -0
- package/dist/scrapers/providers/animevietsub.js +107 -1208
- package/dist/storage.js +136 -49
- package/package.json +26 -8
- package/player-main.js +434 -211
- package/player.html +123 -123
- package/webview-preload.js +465 -0
- package/dist/server/app.js +0 -53
- package/dist/server/config.js +0 -52
- package/dist/server/http.js +0 -78
- package/dist/server/index.js +0 -22
- package/dist/server/routes/admin.js +0 -39
- package/dist/server/routes/health.js +0 -14
- package/dist/server/routes/me.js +0 -50
- package/dist/server/routes/providers.js +0 -53
- package/dist/server/services/cache.js +0 -36
- package/dist/server/services/provider-service.js +0 -90
- package/dist/server/services/server-token.js +0 -51
- package/dist/server/services/session-store.js +0 -52
- package/dist/server/services/token-store.js +0 -58
package/README.md
CHANGED
|
@@ -1,90 +1,169 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
NekoStream CLI
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🐱 NekoStream CLI
|
|
4
|
+
|
|
5
|
+
**Tìm, duyệt và xem anime Việt — ngay trong terminal.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/nekostream)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
[](tsconfig.json)
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
╭──────────────────────────────────────────╮
|
|
13
|
+
│ ❯ nekostream │
|
|
14
|
+
│ │
|
|
15
|
+
│ ▸ Tìm anime │
|
|
16
|
+
│ Thịnh hành Mới cập nhật │
|
|
17
|
+
│ Lịch sử xem Tài khoản │
|
|
18
|
+
│ Cài đặt │
|
|
19
|
+
╰──────────────────────────────────────────╯
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Dự án này là gì
|
|
27
|
+
|
|
28
|
+
một CLI dùng được hằng ngày để xem anime từ các provider Việt Nam: không cần mở trình duyệt, không quảng cáo, không server trung gian.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Cài đặt
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g nekostream
|
|
36
|
+
nekostream
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Yêu cầu: **Node.js ≥ 22.12**, terminal hỗ trợ màu ANSI (Windows / macOS / Linux).
|
|
40
|
+
|
|
41
|
+
<details>
|
|
42
|
+
<summary><b>Cập nhật & kiểm tra phiên bản</b></summary>
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm view nekostream version # phiên bản mới nhất trên npm
|
|
46
|
+
npm install -g nekostream@latest # cập nhật
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
</details>
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Tính năng
|
|
54
|
+
|
|
55
|
+
| | |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| 🔍 **Tìm & duyệt** | Tìm theo tên, xem danh sách thịnh hành và mới cập nhật |
|
|
58
|
+
| 🎬 **Trình phát tích hợp** | Electron + ArtPlayer, chặn quảng cáo và popup của provider |
|
|
59
|
+
| 🌐 **Đa provider** | AnimeVietsub, Anime47 — kèm AnimeHay ở chế độ *duy trì, không đầu tư thêm* |
|
|
60
|
+
| 📚 **Lịch sử xem** | Lưu local, tiếp tục xem nhanh, giữ 100 mục gần nhất |
|
|
61
|
+
| 👤 **Đăng nhập provider** | Truy cập danh sách cá nhân; session mã hoá AES-256-GCM tại chỗ |
|
|
62
|
+
| 🔗 **Metadata AniList** | Bổ sung mô tả, thể loại, điểm — không bắt buộc, lỗi không làm hỏng luồng |
|
|
63
|
+
| 🎮 **Discord Rich Presence** | Bật/tắt trong Cài đặt |
|
|
64
|
+
| ⚙️ **Domain tuỳ chỉnh** | Đổi domain provider khi domain mặc định bị chặn |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Kiến trúc
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
nekostream
|
|
72
|
+
├── cli/ Tầng giao diện — menu, flow, theme, feedback
|
|
73
|
+
│ ├── menus/ Module độc lập cho từng menu (home, settings, history)
|
|
74
|
+
│ └── flows/ Luồng nghiệp vụ: anime-flow, auth-flow, provider-lists
|
|
75
|
+
├── scrapers/ Tầng dữ liệu & trích xuất
|
|
76
|
+
│ ├── providers/ animevietsub/ · anime47/ · animehay.ts
|
|
77
|
+
│ │ ├── <provider>/parsers/ Cheerio DOM parsers thuần túy (testable offline)
|
|
78
|
+
│ │ └── <provider>/streams/ Trích xuất luồng video & server parsers
|
|
79
|
+
│ ├── auth/ Tầng xác thực & quản lý phiên
|
|
80
|
+
│ │ ├── animevietsub/ Đăng nhập & danh sách theo dõi
|
|
81
|
+
│ │ ├── anime47/ Đăng nhập JWT & client REST API
|
|
82
|
+
│ │ └── auth-session-service.ts Quản lý trạng thái phiên
|
|
83
|
+
│ ├── auth-service.ts Facade tương thích ngược cho Auth
|
|
84
|
+
│ ├── http-client.ts Throttle theo domain, timeout controller, backoff
|
|
85
|
+
│ ├── adblock-policy.ts Bộ quy tắc chặn quảng cáo, popup và ad network
|
|
86
|
+
│ ├── html-utils.ts Giải mã HTML, chuẩn hóa URL, phân loại timeout
|
|
87
|
+
│ └── errors.ts Bảng mã lỗi chuẩn hóa (ProviderUnavailableError, ...)
|
|
88
|
+
├── player-policy.ts Cô lập host trình phát, kiểm tra payload & ad rules
|
|
89
|
+
├── player-main.js Electron main process phát video
|
|
90
|
+
├── webview-preload.js Preload cho <webview> phát video (sandboxing)
|
|
91
|
+
├── storage.ts Lưu trữ mã hóa AES-256-GCM (settings, history, session)
|
|
92
|
+
├── provider-types.ts Discriminated union — nguồn sự thật về provider
|
|
93
|
+
└── checks/ 6 bộ test suites chạy hoàn toàn offline
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Chi tiết xem tại [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Phát triển
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm install
|
|
104
|
+
npm start # chạy bản dev
|
|
105
|
+
npm run check # lint → typecheck → test (6 suites) → build → pack:check
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`npm run check` là cổng kiểm thử duy nhất. Mọi thay đổi phải vượt qua:
|
|
109
|
+
1. **ESLint**: 0 cảnh báo (`--max-warnings 0`).
|
|
110
|
+
2. **TypeScript**: `tsc --noEmit` chế độ strict.
|
|
111
|
+
3. **6 Offline Test Suites**: `search-check`, `parser-check` (HTML fixtures), `domain-check`, `storage-check`, `scraping-check`, `player-check`.
|
|
112
|
+
4. **Clean Build**: Xóa và biên dịch sạch vào `dist/`.
|
|
113
|
+
5. **Package Check**: Đảm bảo đủ 70 runtime assets trong npm tarball.
|
|
114
|
+
|
|
115
|
+
<details>
|
|
116
|
+
<summary><b>Gỡ lỗi</b></summary>
|
|
117
|
+
|
|
118
|
+
CLI ẩn log scraper để terminal sạch. Bật khi cần:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
NEKOSTREAM_DEBUG=1 nekostream # bash / zsh
|
|
122
|
+
$env:NEKOSTREAM_DEBUG="1"; nekostream # PowerShell
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Debug của **trình phát** (DOM dump, Eruda console) bật riêng qua `debugMode` trong menu Cài đặt — không đọc từ biến môi trường của shell, để một biến sót lại không thể tự mở công cụ debug bên trong trang phát.
|
|
126
|
+
|
|
127
|
+
</details>
|
|
128
|
+
|
|
129
|
+
<details>
|
|
130
|
+
<summary><b>Cập nhật fixture test</b></summary>
|
|
131
|
+
|
|
132
|
+
Fixture là snapshot HTML thật của provider. Khi provider đổi markup:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx tsx checks/capture-fixtures.mts
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Tool này **không** nằm trong `npm test`: nó cần mạng, còn test thì phải offline và deterministic.
|
|
139
|
+
|
|
140
|
+
</details>
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Đóng góp
|
|
145
|
+
|
|
146
|
+
Repo này hoan nghênh cả hai loại đóng góp:
|
|
147
|
+
|
|
148
|
+
- **Code** — đọc [CONTRIBUTING.md](CONTRIBUTING.md) và [CLEANUP_PLAN.md](CLEANUP_PLAN.md) trước; PR nhỏ, một scope, giữ `npm run check` xanh.
|
|
149
|
+
- **Phản biện đánh giá** — nếu bạn cho rằng [CODE_ASSESSMENT.md](CODE_ASSESSMENT.md) chấm sai hoặc bỏ sót một vấn đề, hãy mở issue. Một case study bị phản biện có giá trị hơn một case study được đồng thuận.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Miễn trừ trách nhiệm
|
|
154
|
+
|
|
155
|
+
- Dự án **không** lưu trữ, phát tán hoặc sở hữu bất kỳ nội dung video nào.
|
|
156
|
+
- NekoStream CLI là client kết nối trực tiếp từ máy người dùng đến provider — **không có server trung gian**.
|
|
157
|
+
- Dự án không thu thập dữ liệu cá nhân. Toàn bộ lịch sử và session nằm trong `~/.nekostream-cli/` trên máy bạn.
|
|
158
|
+
- Vui lòng sử dụng có trách nhiệm và tuân thủ điều khoản của từng provider.
|
|
159
|
+
|
|
160
|
+
> [!CAUTION]
|
|
161
|
+
> ### TUYÊN BỐ MIỄN TRỪ TRÁCH NHIỆM PHÁP LÝ (DISCLAIMER)
|
|
162
|
+
>
|
|
163
|
+
> 1. **Sử dụng với rủi ro tự chịu (As-Is):** Mã nguồn này được cung cấp theo nguyên trạng "như hiện có" mà không có bất kỳ sự đảm bảo nào, dù là rõ ràng hay ngầm định, bao gồm nhưng không giới hạn ở các bảo đảm về khả năng thương mại hoặc tính phù hợp cho một mục đích cụ thể.
|
|
164
|
+
> 2. **Giới hạn trách nhiệm:** Trong mọi trường hợp, **NekoTech** cũng như các thành viên phát triển không chịu trách nhiệm pháp lý đối với bất kỳ khiếu nại, thiệt hại hoặc tổn thất nào (bao gồm tổn thất dữ liệu, lỗ hổng bảo mật, hoặc thiệt hại về tài chính) phát sinh từ hoặc có liên quan đến việc sử dụng, sửa đổi, hoặc phân phối mã nguồn này.
|
|
165
|
+
> 3. **Không hỗ trợ pháp lý:** Việc bạn sử dụng lại mã nguồn để vận hành các dịch vụ công cộng hoặc thương mại tự chịu hoàn toàn trách nhiệm trước pháp luật hiện hành. NekoTech không có nghĩa vụ bảo vệ, can thiệp hoặc chịu trách nhiệm thay cho bất kỳ hành vi vi phạm bản quyền hay pháp lý nào từ phía bên thứ ba.
|
|
166
|
+
|
|
167
|
+
<div align="center">
|
|
168
|
+
|
|
169
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runApplication = runApplication;
|
|
4
|
+
const discord_1 = require("../discord");
|
|
5
|
+
const logger_1 = require("../logger");
|
|
6
|
+
const storage_1 = require("../storage");
|
|
7
|
+
const domain_startup_1 = require("./domain-startup");
|
|
8
|
+
const home_menu_1 = require("./menus/home-menu");
|
|
9
|
+
const update_check_1 = require("./update-check");
|
|
10
|
+
/**
|
|
11
|
+
* Application entry point: bring up the runtime services, then hand control to
|
|
12
|
+
* the home menu loop, which owns the interactive session until exit.
|
|
13
|
+
*
|
|
14
|
+
* Domain resolution runs after the update check — that one already exits the
|
|
15
|
+
* process when a newer version exists, so there is no point probing first.
|
|
16
|
+
*/
|
|
17
|
+
async function runApplication() {
|
|
18
|
+
(0, storage_1.ensureDataDir)();
|
|
19
|
+
await (0, update_check_1.checkUpdate)();
|
|
20
|
+
await (0, domain_startup_1.resolveProviderDomains)();
|
|
21
|
+
await (0, discord_1.initDiscord)();
|
|
22
|
+
(0, logger_1.debugLog)('NekoStream CLI started');
|
|
23
|
+
await (0, home_menu_1.showHomeMenu)();
|
|
24
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveProviderDomains = resolveProviderDomains;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const logger_1 = require("../logger");
|
|
10
|
+
const domain_resolver_1 = require("../scrapers/domain-resolver");
|
|
11
|
+
const feedback_1 = require("./feedback");
|
|
12
|
+
/**
|
|
13
|
+
* The fast path — probing the domain already in use — normally answers well
|
|
14
|
+
* inside this window, so no spinner is drawn at all and startup looks unchanged.
|
|
15
|
+
* The spinner only appears once resolution is slow enough to mean the current
|
|
16
|
+
* domain failed and the TLD fan-out is running.
|
|
17
|
+
*/
|
|
18
|
+
const SPINNER_DELAY_MS = 900;
|
|
19
|
+
function summarize(outcomes) {
|
|
20
|
+
return {
|
|
21
|
+
changed: outcomes.filter((o) => o.status === 'ok' && o.changed),
|
|
22
|
+
failed: outcomes.filter((o) => o.status === 'failed')
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A spinner that only materialises if the work outlives `SPINNER_DELAY_MS`.
|
|
27
|
+
*
|
|
28
|
+
* The state is kept behind functions rather than a local variable because the
|
|
29
|
+
* assignment happens inside a timer callback: TypeScript's control-flow analysis
|
|
30
|
+
* cannot see across that boundary and narrows a plain `let` to `null` forever.
|
|
31
|
+
*/
|
|
32
|
+
function delayedSpinner(text) {
|
|
33
|
+
let spinner;
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
spinner = (0, ora_1.default)(text).start();
|
|
36
|
+
}, SPINNER_DELAY_MS);
|
|
37
|
+
return {
|
|
38
|
+
stop: () => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
spinner?.stop();
|
|
41
|
+
},
|
|
42
|
+
wasShown: () => spinner !== undefined
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Pick each provider's live domain before the menu opens.
|
|
47
|
+
*
|
|
48
|
+
* Startup resolution is what turns domain discovery from reactive into
|
|
49
|
+
* proactive: previously a moved provider surfaced as three failed retries and a
|
|
50
|
+
* Playwright escalation in the middle of a search, and only then fell back.
|
|
51
|
+
*
|
|
52
|
+
* This never rejects and never blocks entry to the app. Offline or slow hosts
|
|
53
|
+
* simply mean the cached or seeded domain is used as-is.
|
|
54
|
+
*/
|
|
55
|
+
async function resolveProviderDomains() {
|
|
56
|
+
const spinner = delayedSpinner('Đang dò domain provider...');
|
|
57
|
+
let outcomes;
|
|
58
|
+
try {
|
|
59
|
+
outcomes = await (0, domain_resolver_1.resolveAllDomains)();
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
spinner.stop();
|
|
63
|
+
}
|
|
64
|
+
for (const outcome of outcomes) {
|
|
65
|
+
(0, logger_1.debugLog)(`[Domain] ${outcome.provider}: ${outcome.status} -> ${outcome.baseUrl}`);
|
|
66
|
+
}
|
|
67
|
+
const { changed, failed } = summarize(outcomes);
|
|
68
|
+
// Only speak up when something actually moved. A silent startup is the
|
|
69
|
+
// expected case and the whole point of the fast path.
|
|
70
|
+
if (changed.length > 0) {
|
|
71
|
+
for (const outcome of changed) {
|
|
72
|
+
console.log(chalk_1.default.cyan(` Domain ${outcome.provider} đã đổi -> ${outcome.baseUrl}`));
|
|
73
|
+
}
|
|
74
|
+
await (0, feedback_1.sleep)(feedback_1.CONFIRM_DELAY_MS);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Only mention a failure if the spinner had appeared. Without that guard an
|
|
78
|
+
// offline user gets a warning about a domain probe they never asked for and
|
|
79
|
+
// cannot act on; with it, the line only follows visible work.
|
|
80
|
+
if (failed.length > 0 && spinner.wasShown()) {
|
|
81
|
+
const names = failed.map((o) => o.provider).join(', ');
|
|
82
|
+
console.log(chalk_1.default.yellow(` Không dò được domain sống cho: ${names}. Dùng domain đã lưu.`));
|
|
83
|
+
await (0, feedback_1.sleep)(feedback_1.CONFIRM_DELAY_MS);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CONFIRM_DELAY_MS = exports.NOTICE_DELAY_MS = void 0;
|
|
7
|
+
exports.sleep = sleep;
|
|
8
|
+
exports.formatToggle = formatToggle;
|
|
9
|
+
exports.flushStdin = flushStdin;
|
|
10
|
+
exports.formatError = formatError;
|
|
11
|
+
exports.reportFailure = reportFailure;
|
|
12
|
+
exports.reportEmpty = reportEmpty;
|
|
13
|
+
exports.withSpinner = withSpinner;
|
|
14
|
+
exports.withSilentSpinner = withSilentSpinner;
|
|
15
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
16
|
+
const ora_1 = __importDefault(require("ora"));
|
|
17
|
+
const ui_1 = require("../ui");
|
|
18
|
+
/** How long a transient message stays on screen before the menu repaints. */
|
|
19
|
+
exports.NOTICE_DELAY_MS = 2000;
|
|
20
|
+
exports.CONFIRM_DELAY_MS = 1000;
|
|
21
|
+
async function sleep(ms) {
|
|
22
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
|
+
}
|
|
24
|
+
function formatToggle(value) {
|
|
25
|
+
return value ? chalk_1.default.green('Bật') : chalk_1.default.red('Tắt');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Drain buffered keypresses so a prompt does not immediately consume input
|
|
29
|
+
* typed while the previous screen was still rendering.
|
|
30
|
+
*/
|
|
31
|
+
function flushStdin() {
|
|
32
|
+
if (!process.stdin.isTTY)
|
|
33
|
+
return;
|
|
34
|
+
process.stdin.resume();
|
|
35
|
+
while (process.stdin.read() !== null) {
|
|
36
|
+
// Discard whatever was buffered.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function formatError(error) {
|
|
40
|
+
if (error instanceof Error)
|
|
41
|
+
return error.message;
|
|
42
|
+
return String(error);
|
|
43
|
+
}
|
|
44
|
+
/** Print a failure and hold it on screen long enough to be read. */
|
|
45
|
+
async function reportFailure(label, error) {
|
|
46
|
+
const detail = error === undefined ? '' : `: ${formatError(error)}`;
|
|
47
|
+
(0, ui_1.printError)(`\n${label}${detail}`);
|
|
48
|
+
await sleep(exports.NOTICE_DELAY_MS);
|
|
49
|
+
}
|
|
50
|
+
/** Print an empty-result notice and hold it on screen. */
|
|
51
|
+
async function reportEmpty(message) {
|
|
52
|
+
(0, ui_1.printError)(`\n${message}`);
|
|
53
|
+
await sleep(exports.NOTICE_DELAY_MS);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Run an async task behind a spinner. Returns `undefined` when the task throws,
|
|
57
|
+
* after showing `failure` to the user — callers treat that as "abort this step".
|
|
58
|
+
*/
|
|
59
|
+
async function withSpinner(message, failure, task) {
|
|
60
|
+
const spinner = (0, ora_1.default)(message).start();
|
|
61
|
+
try {
|
|
62
|
+
const value = await task();
|
|
63
|
+
spinner.stop();
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
spinner.stop();
|
|
68
|
+
await reportFailure(failure, error);
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Like {@link withSpinner} but keeps the spinner running on success so the
|
|
74
|
+
* caller can decide the final message.
|
|
75
|
+
*/
|
|
76
|
+
async function withSilentSpinner(message, task) {
|
|
77
|
+
const spinner = (0, ora_1.default)(message).start();
|
|
78
|
+
try {
|
|
79
|
+
const value = await task();
|
|
80
|
+
spinner.stop();
|
|
81
|
+
return { ok: true, value };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
spinner.stop();
|
|
85
|
+
return { ok: false, error };
|
|
86
|
+
}
|
|
87
|
+
}
|