@noobdemon/noob-cli 1.0.1 → 1.0.3
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 +10 -0
- package/src/repl.js +64 -34
- package/src/update.js +72 -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
|
@@ -79,4 +79,14 @@ export const t = {
|
|
|
79
79
|
`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
80
|
maxSteps: "_(đã dừng: chạm giới hạn số bước tool)_",
|
|
81
81
|
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.",
|
|
82
|
+
|
|
83
|
+
// update
|
|
84
|
+
cmdUpdate: "/update cập nhật noob lên bản mới nhất",
|
|
85
|
+
updateFound: (cur, lat) => `🆕 Có bản mới ${lat} (đang dùng ${cur}) — đang tự cập nhật nền…`,
|
|
86
|
+
updateBgDone: "Đang cập nhật nền. Mở lại noob để dùng bản mới.",
|
|
87
|
+
updateChecking: "Đang kiểm tra cập nhật…",
|
|
88
|
+
updateLatest: (cur) => `Đã ở bản mới nhất (${cur}).`,
|
|
89
|
+
updating: "Đang cập nhật…",
|
|
90
|
+
updateOk: "✓ Cập nhật xong. Mở lại noob để dùng bản mới.",
|
|
91
|
+
updateFail: "✗ Cập nhật thất bại. Thử thủ công: npm i -g @noobdemon/noob-cli@latest",
|
|
82
92
|
};
|
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 = {
|
|
@@ -19,31 +20,43 @@ export async function startRepl(opts = {}) {
|
|
|
19
20
|
yolo: !!opts.yolo,
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
23
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "" });
|
|
23
24
|
let closed = false;
|
|
24
|
-
|
|
25
|
-
let
|
|
26
|
-
|
|
25
|
+
const queue = []; // lines typed/piped, consumed in order
|
|
26
|
+
let waiter = null; // resolver awaiting the next line
|
|
27
|
+
|
|
28
|
+
rl.on("line", (line) => {
|
|
29
|
+
if (waiter) {
|
|
30
|
+
const w = waiter;
|
|
31
|
+
waiter = null;
|
|
32
|
+
w(line);
|
|
33
|
+
} else {
|
|
34
|
+
queue.push(line); // type-ahead / buffered input — never dropped
|
|
35
|
+
}
|
|
36
|
+
});
|
|
27
37
|
rl.on("close", () => {
|
|
28
38
|
closed = true;
|
|
29
|
-
if (
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
if (waiter) {
|
|
40
|
+
const w = waiter;
|
|
41
|
+
waiter = null;
|
|
42
|
+
w(null);
|
|
33
43
|
}
|
|
34
44
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
|
|
46
|
+
// Robust input: every line is captured by the 'line' event (nothing is lost
|
|
47
|
+
// while a turn is processing) and handed out one at a time. Works for an
|
|
48
|
+
// interactive TTY and for piped / non-TTY stdin (Git Bash, CI, etc.).
|
|
49
|
+
function nextLine() {
|
|
50
|
+
if (queue.length) return Promise.resolve(queue.shift());
|
|
51
|
+
if (closed) return Promise.resolve(null);
|
|
52
|
+
return new Promise((res) => (waiter = res));
|
|
53
|
+
}
|
|
54
|
+
function ask(promptStr) {
|
|
55
|
+
if (closed && !queue.length) return Promise.resolve(null);
|
|
56
|
+
rl.setPrompt(promptStr);
|
|
57
|
+
rl.prompt();
|
|
58
|
+
return nextLine();
|
|
59
|
+
}
|
|
47
60
|
|
|
48
61
|
// Shift+Tab — quick yolo toggle.
|
|
49
62
|
if (process.stdin.isTTY) {
|
|
@@ -51,16 +64,8 @@ export async function startRepl(opts = {}) {
|
|
|
51
64
|
process.stdin.on("keypress", (_str, key) => {
|
|
52
65
|
if (!key || key.name !== "tab" || !key.shift) return;
|
|
53
66
|
state.yolo = !state.yolo;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const buf = rl.line;
|
|
57
|
-
readline.cursorTo(process.stdout, 0);
|
|
58
|
-
readline.clearLine(process.stdout, 0);
|
|
59
|
-
console.log(msg);
|
|
60
|
-
process.stdout.write(lastPrompt + buf);
|
|
61
|
-
} else {
|
|
62
|
-
console.log("\n" + msg);
|
|
63
|
-
}
|
|
67
|
+
console.log(state.yolo ? c.err("\n " + t.yoloOn) : c.ok("\n " + t.yoloOff));
|
|
68
|
+
rl.prompt(true);
|
|
64
69
|
});
|
|
65
70
|
}
|
|
66
71
|
|
|
@@ -71,7 +76,7 @@ export async function startRepl(opts = {}) {
|
|
|
71
76
|
abort.abort();
|
|
72
77
|
abort = null;
|
|
73
78
|
console.log(c.err("\n ✗ " + t.interrupted));
|
|
74
|
-
return;
|
|
79
|
+
return; // the main loop will redraw the prompt
|
|
75
80
|
}
|
|
76
81
|
if (sigintArmed) {
|
|
77
82
|
console.log(c.dim("\n " + t.bye));
|
|
@@ -80,7 +85,7 @@ export async function startRepl(opts = {}) {
|
|
|
80
85
|
sigintArmed = true;
|
|
81
86
|
console.log(c.dim("\n " + t.pressAgainToExit));
|
|
82
87
|
setTimeout(() => (sigintArmed = false), 2000);
|
|
83
|
-
|
|
88
|
+
rl.prompt(true);
|
|
84
89
|
});
|
|
85
90
|
|
|
86
91
|
banner();
|
|
@@ -88,15 +93,27 @@ export async function startRepl(opts = {}) {
|
|
|
88
93
|
if (!config.apiKey) console.log("\n" + c.tool(" " + t.notLoggedIn) + "\n");
|
|
89
94
|
else console.log(c.dim(" " + t.ready + "\n"));
|
|
90
95
|
|
|
96
|
+
// Auto-update: non-blocking startup check; if newer, update in the background.
|
|
97
|
+
if (process.env.NOOB_NO_AUTOUPDATE !== "1") {
|
|
98
|
+
checkLatest()
|
|
99
|
+
.then((v) => {
|
|
100
|
+
if (!v) return;
|
|
101
|
+
console.log(c.tool(" " + t.updateFound(CURRENT, v)));
|
|
102
|
+
runUpdate({ background: true });
|
|
103
|
+
console.log(c.dim(" " + t.updateBgDone));
|
|
104
|
+
})
|
|
105
|
+
.catch(() => {});
|
|
106
|
+
}
|
|
107
|
+
|
|
91
108
|
if (opts.prompt) {
|
|
92
109
|
console.log(c.user(t.promptYou) + c.dim("› ") + opts.prompt);
|
|
93
110
|
await handle(opts.prompt);
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
// Main loop — runs until /exit, double Ctrl+C, or EOF. Never exits after a task.
|
|
97
|
-
while (
|
|
114
|
+
while (true) {
|
|
98
115
|
const raw = await ask(c.user("\n" + t.promptYou) + c.dim("› "));
|
|
99
|
-
if (raw == null) break;
|
|
116
|
+
if (raw == null) break; // stdin fully closed and drained
|
|
100
117
|
const input = raw.trim();
|
|
101
118
|
if (!input) continue;
|
|
102
119
|
if (input.startsWith("/")) {
|
|
@@ -243,6 +260,9 @@ export async function startRepl(opts = {}) {
|
|
|
243
260
|
case "usage":
|
|
244
261
|
await showUsage();
|
|
245
262
|
break;
|
|
263
|
+
case "update":
|
|
264
|
+
await doUpdate();
|
|
265
|
+
break;
|
|
246
266
|
case "clear":
|
|
247
267
|
case "new":
|
|
248
268
|
state.history = [];
|
|
@@ -289,6 +309,15 @@ export async function startRepl(opts = {}) {
|
|
|
289
309
|
}
|
|
290
310
|
}
|
|
291
311
|
|
|
312
|
+
async function doUpdate() {
|
|
313
|
+
console.log(c.dim(" " + t.updateChecking));
|
|
314
|
+
const v = await checkLatest({ throttle: false });
|
|
315
|
+
if (!v) return console.log(c.ok(" " + t.updateLatest(CURRENT)));
|
|
316
|
+
console.log(c.tool(" " + t.updateFound(CURRENT, v)));
|
|
317
|
+
const ok = await runUpdate({ background: false });
|
|
318
|
+
console.log(ok ? c.ok(" " + t.updateOk) : c.err(" " + t.updateFail));
|
|
319
|
+
}
|
|
320
|
+
|
|
292
321
|
function selectModel(q) {
|
|
293
322
|
const s = q.toLowerCase();
|
|
294
323
|
const m =
|
|
@@ -367,6 +396,7 @@ function printHelp() {
|
|
|
367
396
|
" " + t.cmdLogin,
|
|
368
397
|
" " + t.cmdLogout,
|
|
369
398
|
" " + t.cmdUsage,
|
|
399
|
+
" " + t.cmdUpdate,
|
|
370
400
|
" " + t.cmdClear,
|
|
371
401
|
" " + t.cmdStatus,
|
|
372
402
|
" " + t.cmdExit,
|
package/src/update.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
child.unref();
|
|
65
|
+
return Promise.resolve(true);
|
|
66
|
+
}
|
|
67
|
+
return new Promise((res) => {
|
|
68
|
+
const child = spawn(cmd, args, { stdio: "inherit", env, shell: isWin });
|
|
69
|
+
child.on("close", (code) => res(code === 0));
|
|
70
|
+
child.on("error", () => res(false));
|
|
71
|
+
});
|
|
72
|
+
}
|