@nextxai/cli 0.0.0-stage → 0.1.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +53 -2
  4. package/dist/ai/client.js +221 -0
  5. package/dist/auth/ci.js +64 -0
  6. package/dist/auth/credentials.js +192 -0
  7. package/dist/auth/device.js +76 -0
  8. package/dist/auth/login.js +145 -0
  9. package/dist/auth/loopback.js +98 -0
  10. package/dist/auth/mcp-oauth.js +174 -0
  11. package/dist/auth/session.js +205 -0
  12. package/dist/bin/nextx.js +5 -0
  13. package/dist/commands/ai.js +514 -0
  14. package/dist/commands/api.js +87 -0
  15. package/dist/commands/auth.js +165 -0
  16. package/dist/commands/docs.js +55 -0
  17. package/dist/commands/help.js +48 -0
  18. package/dist/commands/keys.js +85 -0
  19. package/dist/commands/mcp.js +202 -0
  20. package/dist/commands/tools.js +186 -0
  21. package/dist/commands/util.js +224 -0
  22. package/dist/commands/webhooks.js +249 -0
  23. package/dist/commands/workspace.js +44 -0
  24. package/dist/core/brand.js +51 -0
  25. package/dist/core/config.js +52 -0
  26. package/dist/core/context.js +42 -0
  27. package/dist/core/errors.js +28 -0
  28. package/dist/core/http.js +170 -0
  29. package/dist/core/i18n.js +90 -0
  30. package/dist/core/lock.js +44 -0
  31. package/dist/core/output.js +74 -0
  32. package/dist/core/paths.js +29 -0
  33. package/dist/core/prompt.js +137 -0
  34. package/dist/core/version.js +13 -0
  35. package/dist/main.js +38 -0
  36. package/dist/mcp/client.js +114 -0
  37. package/dist/program.js +102 -0
  38. package/dist/repl/App.js +771 -0
  39. package/dist/repl/components.js +66 -0
  40. package/dist/repl/index.js +12 -0
  41. package/dist/repl/markdown.js +57 -0
  42. package/dist/repl/slash.js +53 -0
  43. package/package.json +62 -4
@@ -0,0 +1,186 @@
1
+ import { Option } from "commander";
2
+ import { readFileSync } from "node:fs";
3
+ import { brand, envVar } from "../core/brand.js";
4
+ import { CliError, Exit } from "../core/errors.js";
5
+ import { currentLang, tr } from "../core/i18n.js";
6
+ import { c, emit, info, json, ok, table } from "../core/output.js";
7
+ import { get, post } from "../core/http.js";
8
+ import { hasTty } from "../core/prompt.js";
9
+ import { McpClient, resultValue } from "../mcp/client.js";
10
+ import { authorizeMcp, readGrant, revokeGrant } from "../auth/mcp-oauth.js";
11
+ import { browserAvailable } from "./auth.js";
12
+ let publicCache;
13
+ export async function publicTools() {
14
+ publicCache ??= (await get("/ai/public/tools", { anonymous: true })).filter((t) => !brand().modules || brand().modules.includes(t.module));
15
+ return publicCache;
16
+ }
17
+ /** MCP names may differ from catalog names (module prefix, dots); match loosely. */
18
+ export function sameTool(a, b) {
19
+ const n = (s) => s.toLowerCase().replace(/[.\-]/g, "_");
20
+ return n(a) === n(b) || n(a).endsWith(`_${n(b)}`) || n(b).endsWith(`_${n(a)}`);
21
+ }
22
+ /** `raw` keeps every value a string (api -f); otherwise numbers/booleans/JSON are inferred, but never for values with a leading zero (phone numbers, codes). */
23
+ export function parseArgs(o) {
24
+ let base = {};
25
+ try {
26
+ if (o.argsFile)
27
+ base = JSON.parse(readFileSync(o.argsFile === "-" ? 0 : o.argsFile, "utf8"));
28
+ if (o.args)
29
+ base = { ...base, ...JSON.parse(o.args) };
30
+ }
31
+ catch (e) {
32
+ throw new CliError(tr(`Tham số không phải JSON hợp lệ: ${e.message}`, `Arguments are not valid JSON: ${e.message}`), Exit.usage);
33
+ }
34
+ for (const kv of o.raw ?? []) {
35
+ const i = kv.indexOf("=");
36
+ if (i < 1)
37
+ throw new CliError(tr(`-f cần dạng khoá=giá_trị: ${kv}`, `-f needs key=value: ${kv}`), Exit.usage);
38
+ base[kv.slice(0, i)] = kv.slice(i + 1);
39
+ }
40
+ for (const kv of o.arg ?? []) {
41
+ const i = kv.indexOf("=");
42
+ if (i < 1)
43
+ throw new CliError(tr(`-a cần dạng khoá=giá_trị: ${kv}`, `-a needs key=value: ${kv}`), Exit.usage);
44
+ const k = kv.slice(0, i);
45
+ const raw = kv.slice(i + 1);
46
+ let v = raw;
47
+ if (/^-?(0|[1-9]\d*)(\.\d+)?$/.test(raw) && raw.length < 16)
48
+ v = Number(raw);
49
+ else if (raw === "true" || raw === "false")
50
+ v = raw === "true";
51
+ else if (/^[[{]/.test(raw)) {
52
+ try {
53
+ v = JSON.parse(raw);
54
+ }
55
+ catch {
56
+ // keep as string
57
+ }
58
+ }
59
+ base[k] = v;
60
+ }
61
+ return base;
62
+ }
63
+ const summaryOf = (t) => (t ? (currentLang() === "en" ? t.summaryEn ?? t.summary : t.summary) ?? "" : "");
64
+ function mergeList(mcp, pub) {
65
+ return mcp.map((m) => {
66
+ const p = pub.find((x) => sameTool(x.name, m.name));
67
+ return {
68
+ name: m.name,
69
+ module: p?.module ?? m.name.split(/[_.]/)[0] ?? "",
70
+ title: p?.title ?? m.title ?? m.annotations?.title ?? m.name,
71
+ kind: p?.kind ?? (m.annotations?.readOnlyHint ? "read" : "proposal"),
72
+ summary: summaryOf(p) || (m.description ?? "").split("\n")[0],
73
+ };
74
+ });
75
+ }
76
+ export function registerTools(program) {
77
+ const tools = program.command("tools").description(tr("công cụ AI bạn được dùng (theo quyền thật)", "AI tools you can use (by your real permissions)"));
78
+ tools.command("login")
79
+ .description(tr("cấp quyền công cụ AI cho CLI trên máy này (một lần)", "grant AI tool access to the CLI on this machine (once)"))
80
+ .option("--no-browser", tr("không tự mở trình duyệt", "do not open the browser"))
81
+ .action(async (o) => {
82
+ const g = await authorizeMcp({ openBrowser: o.browser !== false && browserAvailable(), manual: o.browser === false || !browserAvailable() });
83
+ emit({ scope: g.scope.split(" ") }, () => ok(tr(`Đã cấp quyền: ${g.scope}`, `Granted: ${g.scope}`)));
84
+ });
85
+ tools.command("logout").description(tr("thu hồi quyền công cụ AI của CLI", "revoke the CLI's AI tool access"))
86
+ .action(async () => {
87
+ await revokeGrant();
88
+ ok(tr("Đã thu hồi.", "Revoked."));
89
+ });
90
+ tools.command("list", { isDefault: true })
91
+ .description(tr("danh sách công cụ", "list tools"))
92
+ .option("--module <m>", tr("lọc theo phân hệ (crm, sales, gym…)", "filter by module (crm, sales, gym…)"))
93
+ .addOption(new Option("--kind <k>", tr("lọc theo loại", "filter by kind")).choices(["read", "proposal", "export"]))
94
+ .option("--public", tr("danh mục công khai (không cần đăng nhập)", "public catalog (no login)"))
95
+ .action(async (o) => {
96
+ const pub = await publicTools();
97
+ let rows;
98
+ if (o.public || envVar("API_KEY")) {
99
+ rows = pub.map((p) => ({ name: p.name, module: p.module, title: p.title ?? p.name, kind: p.kind ?? "", summary: summaryOf(p) }));
100
+ if (envVar("API_KEY"))
101
+ rows = rows.filter((r) => r.kind === "read");
102
+ }
103
+ else {
104
+ const mcp = await new McpClient(hasTty()).listTools();
105
+ rows = mergeList(mcp, pub);
106
+ }
107
+ if (o.module)
108
+ rows = rows.filter((r) => r.module === o.module);
109
+ if (o.kind)
110
+ rows = rows.filter((r) => r.kind === o.kind);
111
+ emit(rows, () => {
112
+ info(table([tr("Công cụ", "Tool"), tr("Phân hệ", "Module"), tr("Loại", "Kind"), tr("Mô tả", "Summary")], rows.map((r) => [r.name, r.module, r.kind === "read" ? r.kind : c.yellow(r.kind), r.summary.slice(0, 70)])));
113
+ info(c.dim(` ${rows.length} ${tr("công cụ", "tools")} · ${brand().bin} tools describe <${tr("tên", "name")}>`));
114
+ });
115
+ });
116
+ tools.command("describe <name>")
117
+ .description(tr("tham số và ví dụ của một công cụ", "parameters and an example for a tool"))
118
+ .action(async (name) => {
119
+ const pub = (await publicTools()).find((p) => sameTool(p.name, name));
120
+ let schema = pub?.parameters;
121
+ if (!pub && !envVar("API_KEY") && readGrant()) {
122
+ const m = (await new McpClient(false).listTools()).find((t) => sameTool(t.name, name));
123
+ schema = m?.inputSchema;
124
+ if (!m)
125
+ throw new CliError(tr("Không thấy công cụ này.", "Tool not found."), Exit.notFound);
126
+ }
127
+ else if (!pub)
128
+ throw new CliError(tr("Không thấy công cụ này.", "Tool not found."), Exit.notFound);
129
+ emit({ ...pub, parameters: schema }, () => {
130
+ info(`${c.bold(pub?.title ?? name)} ${c.dim(`${pub?.module ?? ""} · ${pub?.kind ?? ""} · ${pub?.scope ?? ""}`)}`);
131
+ if (pub)
132
+ info(summaryOf(pub));
133
+ const props = (schema?.properties ?? {});
134
+ const req = new Set(schema?.required ?? []);
135
+ if (Object.keys(props).length) {
136
+ info();
137
+ info(table([tr("Tham số", "Param"), tr("Kiểu", "Type"), "", tr("Mô tả", "Description")], Object.entries(props).map(([k, v]) => [
138
+ k, v.enum ? v.enum.join("|") : (v.type ?? ""), req.has(k) ? tr("bắt buộc", "required") : "", (currentLang() === "en" ? v["x-description-en"] ?? v.description : v.description) ?? "",
139
+ ])));
140
+ }
141
+ const ex = pub?.exampleArgs ?? Object.fromEntries([...req].map((k) => [k, `<${k}>`]));
142
+ info();
143
+ info(c.dim(` ${brand().bin} tools call ${name} --args '${JSON.stringify(ex)}'`));
144
+ });
145
+ });
146
+ tools.command("call <name>")
147
+ .description(tr("gọi một công cụ. Công cụ ghi chỉ tạo đề xuất để xác nhận", "call a tool. Write tools only create a proposal to confirm"))
148
+ .option("--args <json>", tr("tham số JSON", "JSON arguments"))
149
+ .option("--args-file <file>", tr("tham số từ tệp JSON (- = stdin)", "arguments from a JSON file (- = stdin)"))
150
+ .option("-a, --arg <key=value>", tr("một tham số (lặp được)", "one argument (repeatable)"), (v, p = []) => [...p, v])
151
+ .action(async (name, o) => {
152
+ const args = parseArgs(o);
153
+ let value;
154
+ if (envVar("API_KEY")) {
155
+ const pub = (await publicTools()).find((p) => sameTool(p.name, name));
156
+ if (!pub)
157
+ throw new CliError(tr("Không thấy công cụ công khai này.", "Public tool not found."), Exit.notFound);
158
+ if (pub.kind !== "read")
159
+ throw new CliError(tr("Khoá API chỉ gọi được công cụ đọc.", "API keys can only call read tools."), Exit.forbidden);
160
+ value = await post(`/ai/tools/${pub.module}/${pub.name}/read`, args);
161
+ }
162
+ else {
163
+ const client = new McpClient(hasTty());
164
+ const tool = (await client.listTools()).find((t) => sameTool(t.name, name));
165
+ if (!tool)
166
+ throw new CliError(tr("Không thấy công cụ này trong quyền của bạn.", "This tool is not in your permissions."), Exit.notFound, `${brand().bin} tools list`);
167
+ const props = Object.keys(tool.inputSchema?.properties ?? {});
168
+ const wrapped = props.length === 1 && props[0] === "args" && !("args" in args) ? { args } : args;
169
+ const r = await client.callTool(tool.name, wrapped);
170
+ value = resultValue(r);
171
+ if (r.isError) {
172
+ emit({ error: value }, () => info(c.red(typeof value === "string" ? value : JSON.stringify(value, null, 2))));
173
+ process.exitCode = Exit.general;
174
+ return;
175
+ }
176
+ }
177
+ const text = typeof value === "string" ? value : "";
178
+ const proposal = /xác nhận|confirm/i.test(text) && /https?:\/\//.test(text);
179
+ if (proposal)
180
+ process.exitCode = Exit.notConfirmed;
181
+ if (typeof value === "string")
182
+ emit({ result: value }, () => info(value));
183
+ else
184
+ json(value);
185
+ });
186
+ }
@@ -0,0 +1,224 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { fetch } from "undici";
5
+ import { brand, envVar } from "../core/brand.js";
6
+ import { context } from "../core/context.js";
7
+ import { CliError, Exit } from "../core/errors.js";
8
+ import { tr } from "../core/i18n.js";
9
+ import { c, emit, info, isJson, ok } from "../core/output.js";
10
+ import { loadConfig, saveConfig, SETTABLE, getProfile } from "../core/config.js";
11
+ import { updateCheckFile } from "../core/paths.js";
12
+ import { userAgent, isOlder } from "../core/http.js";
13
+ import { VERSION } from "../core/version.js";
14
+ import { storageKind } from "../auth/credentials.js";
15
+ import { authSource, currentAuth } from "../auth/session.js";
16
+ import { readGrant } from "../auth/mcp-oauth.js";
17
+ import { notesFileName } from "./ai.js";
18
+ async function timed(url, init) {
19
+ const t = Date.now();
20
+ const res = await fetch(url, { ...init, headers: { "User-Agent": userAgent(), ...init?.headers } });
21
+ return { res, ms: Date.now() - t };
22
+ }
23
+ export async function doctor() {
24
+ const ep = context().endpoints;
25
+ const checks = [];
26
+ const add = (name, ok, detail) => checks.push({ name, ok, detail });
27
+ for (const [label, url] of [["API", `${ep.api}/api/v1/iam/auth/me`], [tr("Đăng nhập", "Auth"), `${ep.auth}/.well-known/oauth-authorization-server`], ["MCP", `${new URL(ep.mcp).origin}/.well-known/oauth-protected-resource`]]) {
28
+ try {
29
+ const { res, ms } = await timed(url);
30
+ await res.body?.cancel();
31
+ add(label, res.status < 500, `${new URL(url).host} · HTTP ${res.status} · ${ms}ms`);
32
+ if (label === "API") {
33
+ const server = Date.parse(res.headers.get("date") ?? "");
34
+ if (server) {
35
+ const skew = Math.round((Date.now() - server) / 1000);
36
+ add(tr("Đồng hồ", "Clock"), Math.abs(skew) > 120 ? false : Math.abs(skew) > 30 ? "warn" : true, tr(`lệch ${skew}s so với máy chủ`, `${skew}s off from the server`));
37
+ }
38
+ }
39
+ }
40
+ catch (e) {
41
+ const code = e.cause?.code ?? e.message;
42
+ add(label, false, `${new URL(url).host} · ${code}`);
43
+ }
44
+ }
45
+ const proxy = process.env.HTTPS_PROXY ?? process.env.https_proxy;
46
+ if (proxy)
47
+ add("Proxy", true, proxy.replace(/\/\/[^@]*@/, "//***@"));
48
+ if (process.env.NODE_EXTRA_CA_CERTS)
49
+ add("CA", true, process.env.NODE_EXTRA_CA_CERTS);
50
+ const src = authSource();
51
+ if (src === "none")
52
+ add(tr("Danh tính", "Identity"), false, tr(`chưa đăng nhập · ${brand().bin} login`, `not logged in · ${brand().bin} login`));
53
+ else {
54
+ try {
55
+ await currentAuth({ required: true });
56
+ const { get } = await import("../core/http.js");
57
+ const me = await get("/iam/auth/me");
58
+ add(tr("Danh tính", "Identity"), true, `${src} · ${me.email ?? ""} · ${me.workspace?.name ?? ""} · ${me.permissions?.length ?? 0} ${tr("quyền", "permissions")}`);
59
+ try {
60
+ const u = await get("/operator/widget/support/usage");
61
+ add(tr("Gói AI", "AI plan"), u.limit === 0 ? "warn" : true, `${u.planName ?? "-"}${u.limit ? ` · ${tr("còn", "left")} ${u.remaining}/${u.limit}` : ""}`);
62
+ }
63
+ catch {
64
+ // API keys cannot read usage
65
+ }
66
+ }
67
+ catch (e) {
68
+ add(tr("Danh tính", "Identity"), false, e.message);
69
+ }
70
+ }
71
+ add(tr("Lưu token", "Token storage"), storageKind() === "keychain" ? true : "warn", storageKind() === "keychain" ? "Keychain" : tr("tệp 0600 (không có Keychain)", "0600 file (no keychain)"));
72
+ add(tr("Công cụ AI", "AI tools"), readGrant() ? true : "warn", readGrant() ? tr("đã cấp quyền", "granted") : tr(`chưa cấp · ${brand().bin} tools login`, `not granted · ${brand().bin} tools login`));
73
+ const latest = await latestVersion().catch(() => undefined);
74
+ add(tr("Phiên bản", "Version"), latest && isOlder(VERSION, latest) ? "warn" : true, latest && isOlder(VERSION, latest) ? `${VERSION} → ${latest} · ${brand().bin} update` : `${VERSION}${latest ? ` (${tr("mới nhất", "latest")})` : ""}`);
75
+ add("Node", Number(process.versions.node.split(".")[0]) >= 20, process.versions.node);
76
+ return checks;
77
+ }
78
+ // ── updates ────────────────────────────────────────────────────────────────────────────────────────
79
+ async function latestVersion(channel = loadConfig().updateChannel ?? "latest") {
80
+ const res = await fetch(`https://registry.npmjs.org/${brand().pkg.replace("/", "%2f")}`, { headers: { Accept: "application/vnd.npm.install-v1+json" }, signal: AbortSignal.timeout(3000) });
81
+ if (!res.ok)
82
+ return undefined;
83
+ const body = (await res.json());
84
+ return body["dist-tags"]?.[channel === "next" ? "next" : "latest"];
85
+ }
86
+ export function installMethod() {
87
+ const script = process.argv[1] ?? "";
88
+ if (/Cellar|homebrew/i.test(script) || /Cellar|homebrew/i.test(process.execPath))
89
+ return "brew";
90
+ if (!/node(\.exe)?$/i.test(process.execPath) && !/bun/i.test(process.execPath))
91
+ return "standalone";
92
+ if (script.includes("node_modules"))
93
+ return "npm";
94
+ return "dev";
95
+ }
96
+ export async function maybeNotifyUpdate() {
97
+ if (isJson() || !process.stderr.isTTY || envVar("NO_UPDATE_CHECK") === "1" || loadConfig().updateCheck === false)
98
+ return;
99
+ const f = updateCheckFile();
100
+ let last = 0;
101
+ let latest;
102
+ try {
103
+ const s = JSON.parse(readFileSync(f, "utf8"));
104
+ last = s.at;
105
+ latest = s.latest;
106
+ }
107
+ catch {
108
+ // first run
109
+ }
110
+ if (Date.now() - last > 24 * 3600e3) {
111
+ latest = await latestVersion().catch(() => undefined);
112
+ writeFileSync(f, JSON.stringify({ at: Date.now(), latest }));
113
+ }
114
+ if (latest && isOlder(VERSION, latest))
115
+ process.stderr.write(c.dim(`\n${tr("Có bản mới", "Update available")} ${VERSION} → ${latest} · ${brand().bin} update\n`));
116
+ }
117
+ // ── completion ─────────────────────────────────────────────────────────────────────────────────────
118
+ /** Candidates for the word being completed, walking the command tree. */
119
+ export function completions(program, words) {
120
+ let cmd = program;
121
+ const done = words.slice(0, -1);
122
+ const cur = words[words.length - 1] ?? "";
123
+ for (const w of done) {
124
+ const sub = cmd.commands.find((s) => s.name() === w || s.aliases().includes(w));
125
+ if (sub)
126
+ cmd = sub;
127
+ }
128
+ const opts = [...cmd.options, ...(cmd === program ? [] : program.options)].flatMap((o) => [o.long, o.short]).filter(Boolean);
129
+ const subs = cmd.commands.filter((s) => !s.name().startsWith("__")).map((s) => s.name());
130
+ return (cur.startsWith("-") ? opts : subs).filter((x) => x.startsWith(cur));
131
+ }
132
+ function completionScript(shell) {
133
+ const b = brand().bin;
134
+ switch (shell) {
135
+ case "bash":
136
+ return `_${b}_complete() {\n local IFS=$'\\n'\n COMPREPLY=( $(${b} __complete "\${COMP_WORDS[@]:1:COMP_CWORD}") )\n}\ncomplete -o default -F _${b}_complete ${b}\n`;
137
+ case "zsh":
138
+ return `#compdef ${b}\n_${b}() {\n local -a items\n items=("\${(@f)$(${b} __complete "\${words[@]:1:CURRENT-1}")}")\n compadd -a items\n}\ncompdef _${b} ${b}\n`;
139
+ case "fish":
140
+ return `complete -c ${b} -f -a '(${b} __complete (commandline -opc)[2..-1] (commandline -ct))'\n`;
141
+ case "powershell":
142
+ return `Register-ArgumentCompleter -Native -CommandName ${b} -ScriptBlock {\n param($wordToComplete, $commandAst)\n $words = $commandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object { $_.ToString() }\n if ($wordToComplete -eq '') { $words += '' }\n & ${b} __complete @words | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }\n}\n`;
143
+ default:
144
+ throw new CliError(tr("Shell: bash | zsh | fish | powershell", "Shell: bash | zsh | fish | powershell"), Exit.usage);
145
+ }
146
+ }
147
+ // ── register ───────────────────────────────────────────────────────────────────────────────────────
148
+ export function registerUtil(program) {
149
+ const b = brand();
150
+ program.command("doctor").description(tr("kiểm kết nối, đồng hồ, token, gói, phiên bản", "check connectivity, clock, token, plan, version"))
151
+ .action(async () => {
152
+ const checks = await doctor();
153
+ emit(checks, () => {
154
+ for (const ch of checks)
155
+ info(`${ch.ok === true ? c.green("✓") : ch.ok === "warn" ? c.yellow("⚠") : c.red("✗")} ${ch.name.padEnd(14)} ${ch.detail}`);
156
+ });
157
+ if (checks.some((ch) => ch.ok === false))
158
+ process.exitCode = Exit.general;
159
+ });
160
+ const cfg = program.command("config").description(tr("cài đặt", "settings"));
161
+ cfg.command("list", { isDefault: true }).action(() => {
162
+ const c0 = loadConfig();
163
+ emit(c0, () => {
164
+ for (const k of Object.keys(SETTABLE))
165
+ info(`${k.padEnd(16)} ${String(c0[k] ?? c.dim("—"))}`);
166
+ info(c.dim(`${tr("hồ sơ", "profiles")}: ${Object.keys(c0.profiles).join(", ") || "—"}`));
167
+ });
168
+ });
169
+ cfg.command("get <key>").action((key) => info(String(loadConfig()[key] ?? "")));
170
+ cfg.command("set <key> <value>").description(Object.keys(SETTABLE).join(" · ")).action((key, value) => {
171
+ const parse = SETTABLE[key];
172
+ if (!parse)
173
+ throw new CliError(tr(`Không có cài đặt "${key}". Có: ${Object.keys(SETTABLE).join(", ")}`, `No setting "${key}". Available: ${Object.keys(SETTABLE).join(", ")}`), Exit.usage);
174
+ const c0 = loadConfig();
175
+ try {
176
+ c0[key] = parse(value);
177
+ }
178
+ catch (e) {
179
+ throw new CliError(e.message, Exit.usage);
180
+ }
181
+ saveConfig(c0);
182
+ ok(`${key} = ${String(c0[key])}`);
183
+ });
184
+ program.command("completion <shell>").description(tr("gợi ý Tab cho bash | zsh | fish | powershell", "Tab completion for bash | zsh | fish | powershell"))
185
+ .addHelpText("after", tr(`\nVí dụ: ${b.bin} completion zsh > ~/.zfunc/_${b.bin}`, `\nExample: ${b.bin} completion zsh > ~/.zfunc/_${b.bin}`))
186
+ .action((shell) => void process.stdout.write(completionScript(shell)));
187
+ program.command("__complete", { hidden: true }).argument("[words...]").allowUnknownOption(true).helpOption(false)
188
+ .action((words = []) => void process.stdout.write(completions(program, words.length ? words : [""]).join("\n")));
189
+ program.command("update").description(tr("nâng lên bản mới", "upgrade to the latest version"))
190
+ .option("--channel <c>", "latest | next")
191
+ .action(async (o) => {
192
+ const channel = o.channel ?? loadConfig().updateChannel ?? "latest";
193
+ const latest = await latestVersion(channel).catch(() => undefined);
194
+ if (latest && !isOlder(VERSION, latest))
195
+ return ok(tr(`Đang dùng bản mới nhất (${VERSION}).`, `Already up to date (${VERSION}).`));
196
+ const how = installMethod();
197
+ const cmds = {
198
+ npm: ["npm", ["install", "-g", `${b.pkg}@${channel}`]],
199
+ brew: ["brew", ["upgrade", b.bin]],
200
+ };
201
+ if (how === "standalone") {
202
+ info(tr(`Tải bản mới: curl -fsSL ${context().endpoints.docs}/install.sh | sh`, `Get the new build: curl -fsSL ${context().endpoints.docs}/install.sh | sh`));
203
+ return;
204
+ }
205
+ if (how === "dev")
206
+ return info(tr("Bản chạy từ mã nguồn, cập nhật bằng git pull.", "Running from source, update with git pull."));
207
+ const [cmd, args] = cmds[how];
208
+ info(c.dim(`$ ${cmd} ${args.join(" ")}`));
209
+ const r = spawnSync(cmd, args, { stdio: "inherit" });
210
+ if (r.status === 0)
211
+ ok(tr(`Đã nâng lên ${latest ?? channel}.`, `Updated to ${latest ?? channel}.`));
212
+ else
213
+ throw new CliError(tr("Nâng cấp không thành công.", "Update failed."), Exit.general);
214
+ });
215
+ program.command("init").description(tr(`tạo ${notesFileName()}: chỉ dẫn riêng cho thư mục này`, `create ${notesFileName()}: instructions for this folder`))
216
+ .action(() => {
217
+ const f = join(process.cwd(), notesFileName());
218
+ if (existsSync(f))
219
+ throw new CliError(tr(`${notesFileName()} đã có.`, `${notesFileName()} already exists.`), Exit.usage);
220
+ const p = getProfile(context().profile);
221
+ writeFileSync(f, tr(`# Chỉ dẫn cho ${b.aiName}\n\n${b.aiName} đọc tệp này kèm mỗi câu hỏi khi bạn chạy \`${b.bin}\` trong thư mục này (tối đa 4 KB).\n\n## Bối cảnh\n- Workspace: ${p?.workspaceName ?? ""}\n- Dự án: …\n\n## Quy ước\n- Tiền tệ: VND, làm tròn nghìn.\n- "Khách VIP" = …\n\n## Đừng\n- …\n`, `# Instructions for ${b.aiName}\n\n${b.aiName} reads this file with every question when you run \`${b.bin}\` in this folder (max 4 KB).\n\n## Context\n- Workspace: ${p?.workspaceName ?? ""}\n- Project: …\n\n## Conventions\n- Currency: VND.\n\n## Don't\n- …\n`));
222
+ ok(tr(`Đã tạo ${f}`, `Created ${f}`));
223
+ });
224
+ }