@noobdemon/noob-cli 1.0.2 → 1.0.4
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/bin/noob.js +13 -0
- package/package.json +1 -1
- package/src/config.js +7 -0
- package/src/i18n.js +12 -1
- package/src/repl.js +63 -7
- package/src/update.js +77 -0
package/bin/noob.js
CHANGED
|
@@ -4,6 +4,7 @@ import { config } from "../src/config.js";
|
|
|
4
4
|
import { usage, ApiError } from "../src/api.js";
|
|
5
5
|
import { c } from "../src/ui.js";
|
|
6
6
|
import { t } from "../src/i18n.js";
|
|
7
|
+
import { checkLatest, runUpdate, CURRENT } from "../src/update.js";
|
|
7
8
|
|
|
8
9
|
const argv = process.argv.slice(2);
|
|
9
10
|
const opts = { yolo: false, model: undefined, prompt: undefined };
|
|
@@ -43,6 +44,17 @@ if (sub === "login") {
|
|
|
43
44
|
config.clearKey();
|
|
44
45
|
console.log(c.ok(t.loggedOut));
|
|
45
46
|
process.exit(0);
|
|
47
|
+
} else if (sub === "update") {
|
|
48
|
+
console.log(c.dim(t.updateChecking));
|
|
49
|
+
const v = await checkLatest({ throttle: false });
|
|
50
|
+
if (!v) {
|
|
51
|
+
console.log(c.ok(t.updateLatest(CURRENT)));
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
console.log(c.tool(t.updateFound(CURRENT, v)));
|
|
55
|
+
const ok = await runUpdate({ background: false });
|
|
56
|
+
console.log(ok ? c.ok(t.updateOk) : c.err(t.updateFail));
|
|
57
|
+
process.exit(ok ? 0 : 1);
|
|
46
58
|
} else if (sub === "usage") {
|
|
47
59
|
if (!config.apiKey) {
|
|
48
60
|
console.log(c.tool(t.notLoggedIn));
|
|
@@ -73,6 +85,7 @@ Cách dùng:
|
|
|
73
85
|
noob login <api-key> đăng nhập bằng API key
|
|
74
86
|
noob logout đăng xuất
|
|
75
87
|
noob usage xem hạn mức key
|
|
88
|
+
noob update cập nhật lên bản mới nhất
|
|
76
89
|
|
|
77
90
|
Tuỳ chọn:
|
|
78
91
|
-m, --model <id> chọn mô hình (vd: gateway-claude-opus-4-7)
|
package/package.json
CHANGED
package/src/config.js
CHANGED
package/src/i18n.js
CHANGED
|
@@ -55,7 +55,8 @@ export const t = {
|
|
|
55
55
|
cmdLogin: "/login <key> đăng nhập bằng API key",
|
|
56
56
|
cmdLogout: "/logout đăng xuất",
|
|
57
57
|
cmdUsage: "/usage xem hạn mức key còn lại",
|
|
58
|
-
cmdStatus: "/status xem mô hình
|
|
58
|
+
cmdStatus: "/status xem mô hình, version, trạng thái yolo, thư mục",
|
|
59
|
+
cmdVersion: "/version /v xem version hiện tại + trạng thái yolo",
|
|
59
60
|
cmdExit: "/exit /quit thoát",
|
|
60
61
|
tip1: "• Mô tả việc cần làm; noob sẽ đọc/sửa file & chạy lệnh giúp bạn.",
|
|
61
62
|
tip2: "• Thao tác nguy hiểm sẽ hỏi phép, trừ khi bật yolo (Shift+Tab).",
|
|
@@ -79,4 +80,14 @@ export const t = {
|
|
|
79
80
|
`lưu ý: mô hình ${p} trên gateway này hay từ chối giao thức tool; nên dùng Anthropic/DeepSeek cho tác vụ sửa code.`,
|
|
80
81
|
maxSteps: "_(đã dừng: chạm giới hạn số bước tool)_",
|
|
81
82
|
toolDenied: "Người dùng từ chối thao tác này. Hãy đổi cách làm hoặc hỏi lại.",
|
|
83
|
+
|
|
84
|
+
// update
|
|
85
|
+
cmdUpdate: "/update cập nhật noob lên bản mới nhất",
|
|
86
|
+
updateFound: (cur, lat) => `🆕 Có bản mới ${lat} (đang dùng ${cur}) — đang tự cập nhật nền…`,
|
|
87
|
+
updateBgDone: "Đang cập nhật nền. Mở lại noob để dùng bản mới.",
|
|
88
|
+
updateChecking: "Đang kiểm tra cập nhật…",
|
|
89
|
+
updateLatest: (cur) => `Đã ở bản mới nhất (${cur}).`,
|
|
90
|
+
updating: "Đang cập nhật…",
|
|
91
|
+
updateOk: "✓ Cập nhật xong. Mở lại noob để dùng bản mới.",
|
|
92
|
+
updateFail: "✗ Cập nhật thất bại. Thử thủ công: npm i -g @noobdemon/noob-cli@latest",
|
|
82
93
|
};
|
package/src/repl.js
CHANGED
|
@@ -9,6 +9,7 @@ import { MODELS, PROVIDERS, findModel, providerColor, DEFAULT_MODEL } from "./mo
|
|
|
9
9
|
import { c, banner, modelBadge, renderMarkdown, box } from "./ui.js";
|
|
10
10
|
import { config } from "./config.js";
|
|
11
11
|
import { t } from "./i18n.js";
|
|
12
|
+
import { checkLatest, runUpdate, CURRENT } from "./update.js";
|
|
12
13
|
|
|
13
14
|
export async function startRepl(opts = {}) {
|
|
14
15
|
const state = {
|
|
@@ -87,11 +88,37 @@ export async function startRepl(opts = {}) {
|
|
|
87
88
|
rl.prompt(true);
|
|
88
89
|
});
|
|
89
90
|
|
|
91
|
+
// Đừng để một lỗi bất ngờ làm "tự động tắt" CLI. Nguyên nhân hay gặp:
|
|
92
|
+
// tiến trình cập nhật nền (spawn npm) phát sự kiện 'error' không ai bắt,
|
|
93
|
+
// hoặc lỗi async trong một lượt → Node thoát ngay. Ở đây bắt lại, in ra,
|
|
94
|
+
// rồi vẽ lại prompt để phiên làm việc vẫn sống.
|
|
95
|
+
process.on("uncaughtException", (err) => {
|
|
96
|
+
if (abort) { abort.abort(); abort = null; }
|
|
97
|
+
console.log(c.err("\n ✗ lỗi: " + (err?.message || err)));
|
|
98
|
+
if (!closed) rl.prompt(true);
|
|
99
|
+
});
|
|
100
|
+
process.on("unhandledRejection", (err) => {
|
|
101
|
+
console.log(c.err("\n ✗ lỗi nền: " + (err?.message || err)));
|
|
102
|
+
if (!closed) rl.prompt(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
90
105
|
banner();
|
|
91
106
|
printStatus(state);
|
|
92
107
|
if (!config.apiKey) console.log("\n" + c.tool(" " + t.notLoggedIn) + "\n");
|
|
93
108
|
else console.log(c.dim(" " + t.ready + "\n"));
|
|
94
109
|
|
|
110
|
+
// Auto-update: non-blocking startup check; if newer, update in the background.
|
|
111
|
+
if (process.env.NOOB_NO_AUTOUPDATE !== "1") {
|
|
112
|
+
checkLatest()
|
|
113
|
+
.then((v) => {
|
|
114
|
+
if (!v) return;
|
|
115
|
+
console.log(c.tool(" " + t.updateFound(CURRENT, v)));
|
|
116
|
+
runUpdate({ background: true });
|
|
117
|
+
console.log(c.dim(" " + t.updateBgDone));
|
|
118
|
+
})
|
|
119
|
+
.catch(() => {});
|
|
120
|
+
}
|
|
121
|
+
|
|
95
122
|
if (opts.prompt) {
|
|
96
123
|
console.log(c.user(t.promptYou) + c.dim("› ") + opts.prompt);
|
|
97
124
|
await handle(opts.prompt);
|
|
@@ -99,18 +126,26 @@ export async function startRepl(opts = {}) {
|
|
|
99
126
|
|
|
100
127
|
// Main loop — runs until /exit, double Ctrl+C, or EOF. Never exits after a task.
|
|
101
128
|
while (true) {
|
|
102
|
-
const raw = await ask(c.user("\n" + t.promptYou) + c.dim("› "));
|
|
129
|
+
const raw = await ask(c.user("\n" + t.promptYou) + (state.yolo ? c.err("⚡ ") : "") + c.dim("› "));
|
|
103
130
|
if (raw == null) break; // stdin fully closed and drained
|
|
104
131
|
const input = raw.trim();
|
|
105
132
|
if (!input) continue;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
133
|
+
// Bọc cả lượt: một lỗi trong xử lý lệnh/agent không được phép thoát ra
|
|
134
|
+
// ngoài vòng lặp (sẽ rơi vào .catch ở bin/noob.js → process.exit(1) =
|
|
135
|
+
// "tự động tắt"). Bắt ở đây, in lỗi, rồi tiếp tục vòng lặp.
|
|
136
|
+
try {
|
|
137
|
+
if (input.startsWith("/")) {
|
|
138
|
+
const done = await command(input);
|
|
139
|
+
if (done) break;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
await handle(input);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
printError(err);
|
|
110
145
|
}
|
|
111
|
-
await handle(input);
|
|
112
146
|
}
|
|
113
147
|
rl.close();
|
|
148
|
+
process.exit(0);
|
|
114
149
|
|
|
115
150
|
// ── turn handler ─────────────────────────────────────────────────────────
|
|
116
151
|
async function handle(text) {
|
|
@@ -247,6 +282,9 @@ export async function startRepl(opts = {}) {
|
|
|
247
282
|
case "usage":
|
|
248
283
|
await showUsage();
|
|
249
284
|
break;
|
|
285
|
+
case "update":
|
|
286
|
+
await doUpdate();
|
|
287
|
+
break;
|
|
250
288
|
case "clear":
|
|
251
289
|
case "new":
|
|
252
290
|
state.history = [];
|
|
@@ -261,6 +299,10 @@ export async function startRepl(opts = {}) {
|
|
|
261
299
|
case "status":
|
|
262
300
|
printStatus(state);
|
|
263
301
|
break;
|
|
302
|
+
case "version":
|
|
303
|
+
case "v":
|
|
304
|
+
console.log(c.dim(" noob ") + c.accent("v" + CURRENT) + (state.yolo ? c.err(" ⚡ yolo: BẬT") : c.dim(" yolo: tắt")));
|
|
305
|
+
break;
|
|
264
306
|
case "exit":
|
|
265
307
|
case "quit":
|
|
266
308
|
case "q":
|
|
@@ -293,6 +335,15 @@ export async function startRepl(opts = {}) {
|
|
|
293
335
|
}
|
|
294
336
|
}
|
|
295
337
|
|
|
338
|
+
async function doUpdate() {
|
|
339
|
+
console.log(c.dim(" " + t.updateChecking));
|
|
340
|
+
const v = await checkLatest({ throttle: false });
|
|
341
|
+
if (!v) return console.log(c.ok(" " + t.updateLatest(CURRENT)));
|
|
342
|
+
console.log(c.tool(" " + t.updateFound(CURRENT, v)));
|
|
343
|
+
const ok = await runUpdate({ background: false });
|
|
344
|
+
console.log(ok ? c.ok(" " + t.updateOk) : c.err(" " + t.updateFail));
|
|
345
|
+
}
|
|
346
|
+
|
|
296
347
|
function selectModel(q) {
|
|
297
348
|
const s = q.toLowerCase();
|
|
298
349
|
const m =
|
|
@@ -312,7 +363,10 @@ export async function startRepl(opts = {}) {
|
|
|
312
363
|
const mode =
|
|
313
364
|
s.mode === "merge" ? c.tool("Merge AI") : s.mode === "search" ? c.accent("Tìm web") : modelBadge(s.model);
|
|
314
365
|
const key = config.apiKey ? c.ok(" 🔑") : c.err(" 🔒");
|
|
315
|
-
|
|
366
|
+
const yolo = s.yolo ? c.err(" ⚡ yolo: BẬT") : c.dim(" yolo: tắt");
|
|
367
|
+
console.log(
|
|
368
|
+
" " + mode + key + yolo + c.dim(" v" + CURRENT) + c.dim(" thư mục: " + shortCwd()),
|
|
369
|
+
);
|
|
316
370
|
}
|
|
317
371
|
}
|
|
318
372
|
|
|
@@ -371,8 +425,10 @@ function printHelp() {
|
|
|
371
425
|
" " + t.cmdLogin,
|
|
372
426
|
" " + t.cmdLogout,
|
|
373
427
|
" " + t.cmdUsage,
|
|
428
|
+
" " + t.cmdUpdate,
|
|
374
429
|
" " + t.cmdClear,
|
|
375
430
|
" " + t.cmdStatus,
|
|
431
|
+
" " + t.cmdVersion,
|
|
376
432
|
" " + t.cmdExit,
|
|
377
433
|
"",
|
|
378
434
|
chalk.bold(t.helpTips),
|
package/src/update.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { config } from "./config.js";
|
|
4
|
+
|
|
5
|
+
const NAME = "@noobdemon/noob-cli";
|
|
6
|
+
const CHECK_EVERY = 6 * 3600 * 1000; // 6h throttle
|
|
7
|
+
|
|
8
|
+
export const CURRENT = (() => {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
11
|
+
} catch {
|
|
12
|
+
return "0.0.0";
|
|
13
|
+
}
|
|
14
|
+
})();
|
|
15
|
+
|
|
16
|
+
function cmp(a, b) {
|
|
17
|
+
const pa = String(a).split(".").map(Number);
|
|
18
|
+
const pb = String(b).split(".").map(Number);
|
|
19
|
+
for (let i = 0; i < 3; i++) {
|
|
20
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
21
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
22
|
+
}
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function fetchLatest(timeout = 2500) {
|
|
27
|
+
const ctrl = new AbortController();
|
|
28
|
+
const t = setTimeout(() => ctrl.abort(), timeout);
|
|
29
|
+
try {
|
|
30
|
+
const r = await fetch(`https://registry.npmjs.org/${NAME}/latest`, { signal: ctrl.signal });
|
|
31
|
+
if (!r.ok) return null;
|
|
32
|
+
const j = await r.json();
|
|
33
|
+
return j.version || null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
} finally {
|
|
37
|
+
clearTimeout(t);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Returns the newer version string if an update exists (respecting the throttle), else null. */
|
|
42
|
+
export async function checkLatest({ throttle = true } = {}) {
|
|
43
|
+
if (throttle) {
|
|
44
|
+
const last = config.get("lastUpdateCheck") || 0;
|
|
45
|
+
if (Date.now() - last < CHECK_EVERY) return null;
|
|
46
|
+
config.set("lastUpdateCheck", Date.now());
|
|
47
|
+
}
|
|
48
|
+
const v = await fetchLatest();
|
|
49
|
+
return v && cmp(v, CURRENT) > 0 ? v : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Run `npm i -g @noobdemon/noob-cli@latest`. background=true detaches and returns immediately. */
|
|
53
|
+
export function runUpdate({ background = false } = {}) {
|
|
54
|
+
const isWin = process.platform === "win32";
|
|
55
|
+
const cmd = isWin ? "npm.cmd" : "npm";
|
|
56
|
+
const args = ["i", "-g", `${NAME}@latest`];
|
|
57
|
+
const env = { ...process.env };
|
|
58
|
+
if (process.env.NOOB_INSECURE_TLS === "1") {
|
|
59
|
+
env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
60
|
+
args.push("--strict-ssl=false");
|
|
61
|
+
}
|
|
62
|
+
if (background) {
|
|
63
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore", env, shell: isWin });
|
|
64
|
+
// QUAN TRỌNG: nếu spawn lỗi (vd npm không có trong PATH, hoặc shell trục
|
|
65
|
+
// trặc trên Windows) mà không có listener 'error', Node sẽ ném
|
|
66
|
+
// uncaughtException → tiến trình tự tắt ngay sau khi khởi động. Nuốt lỗi ở
|
|
67
|
+
// đây vì cập nhật nền chỉ là "best effort".
|
|
68
|
+
child.on("error", () => {});
|
|
69
|
+
child.unref();
|
|
70
|
+
return Promise.resolve(true);
|
|
71
|
+
}
|
|
72
|
+
return new Promise((res) => {
|
|
73
|
+
const child = spawn(cmd, args, { stdio: "inherit", env, shell: isWin });
|
|
74
|
+
child.on("close", (code) => res(code === 0));
|
|
75
|
+
child.on("error", () => res(false));
|
|
76
|
+
});
|
|
77
|
+
}
|