u1s1-cli 0.13.4 → 0.13.6

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.js CHANGED
@@ -134,8 +134,10 @@ function ensureTmuxKeyboardProtocol() {
134
134
  }
135
135
  async function runAgent(cfg, args) {
136
136
  cleanupBrandThemes();
137
- ensureBrandPrompt(ensureUsableShell());
138
137
  ensureDefaultSettings();
138
+ // Windows shell 体检异步先行,与取模型列表/搜索工具下载并行;首启验活
139
+ // (Defender 首扫 bash.exe 可达秒级)藏进网络等待,进 pi 前再 await
140
+ const shellReady = ensureUsableShell();
139
141
  // 预 seed fd/rg(国内直连 GitHub 不通,pi 自己下不动);与取模型列表并行,
140
142
  // 但必须在进 pi 之前就位,否则 pi 会自己去 GitHub 下载
141
143
  const searchToolsReady = ensureSearchTools(cfg);
@@ -153,6 +155,7 @@ async function runAgent(cfg, args) {
153
155
  console.error(" 获取模型列表失败,使用内置列表:", e.message);
154
156
  }
155
157
  await endpointsReady;
158
+ ensureBrandPrompt(await shellReady);
156
159
  ensureProviderModels(cfg);
157
160
  // 联网工具经 agentDir/extensions 投影,TUI 和 u1s1 web 共用一份注册
158
161
  writeWebToolsExtension(cfg, webSearchEnabled);
package/dist/login.js CHANGED
@@ -126,7 +126,7 @@ export async function login(keyArg) {
126
126
  quotaNote =
127
127
  me.free_claim === "first"
128
128
  ? "免费用量包还没领,去 https://u1s1.io/dashboard 点「领取」(首月每天 1 亿 Token)"
129
- : "免费用量包到期了,去 https://u1s1.io/dashboard 续领(每天 5000 万 Token)";
129
+ : "免费用量包到期了,去 https://u1s1.io/dashboard 续领(每天 3000 万 Token)";
130
130
  }
131
131
  else if (tpu > 0) {
132
132
  const tok = (usd) => {
@@ -5,6 +5,10 @@
5
5
  * 常见),所有命令 0.1s 内失败,stderr 还是 UTF-16 乱码,模型只能干瞪眼。
6
6
  * 这里在进 pi 之前把一个「验过活」的 bash 写进 settings.json 的 shellPath;
7
7
  * 找不到就当场把修复指引打给用户,并让品牌 prompt 告知模型别撞墙。
8
+ *
9
+ * 整个探测是异步的:调用方先拿 Promise 与拉模型列表等网络请求并行,进 pi 前
10
+ * 再 await——首启的 where/验活成本(Defender 首扫 bash.exe 可达秒级)藏进
11
+ * 网络等待里。验活结果持久化到 shellPath,之后每次启动只花 existsSync。
8
12
  */
9
13
  export type ShellDoctorResult = {
10
14
  status: "posix";
@@ -19,4 +23,4 @@ export type ShellDoctorResult = {
19
23
  } | {
20
24
  status: "unknown";
21
25
  };
22
- export declare function ensureUsableShell(): ShellDoctorResult;
26
+ export declare function ensureUsableShell(): Promise<ShellDoctorResult>;
@@ -1,4 +1,4 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { agentDir, agentSettingsFile } from "./config.js";
@@ -7,70 +7,125 @@ function isWslRelayBash(p) {
7
7
  const normalized = p.replace(/\//g, "\\").toLowerCase();
8
8
  return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized);
9
9
  }
10
- /** 真跑一条 echo 验活。WSL 转发器不支持 -c 传参(pi 对它走 stdin),探活同款。 */
11
- function bashWorks(bashPath) {
12
- const viaStdin = isWslRelayBash(bashPath);
13
- try {
14
- const res = spawnSync(bashPath, viaStdin ? ["-s"] : ["-c", "echo u1s1-shell-ok"], {
15
- input: viaStdin ? "echo u1s1-shell-ok\n" : undefined,
16
- encoding: "utf-8",
17
- timeout: 5000,
18
- windowsHide: true,
10
+ /** 异步跑子进程收 stdout;超时杀掉。任何失败都归为 ok=false,不抛。 */
11
+ function runCapture(cmd, args, opts) {
12
+ return new Promise((resolve) => {
13
+ let done = false;
14
+ let timer;
15
+ const finish = (ok, stdout) => {
16
+ if (done)
17
+ return;
18
+ done = true;
19
+ if (timer)
20
+ clearTimeout(timer);
21
+ resolve({ ok, stdout });
22
+ };
23
+ let child;
24
+ try {
25
+ child = spawn(cmd, args, { windowsHide: true, stdio: ["pipe", "pipe", "ignore"] });
26
+ }
27
+ catch {
28
+ finish(false, "");
29
+ return;
30
+ }
31
+ timer = setTimeout(() => {
32
+ try {
33
+ child.kill();
34
+ }
35
+ catch {
36
+ // 已退出
37
+ }
38
+ finish(false, "");
39
+ }, opts.timeoutMs);
40
+ let out = "";
41
+ child.stdout?.on("data", (d) => {
42
+ out += d.toString("utf8");
43
+ if (out.length > 65536)
44
+ out = out.slice(-65536);
19
45
  });
20
- // WSL 转发器可能按 UTF-16LE 输出(ASCII 字符间夹 NUL),剔掉再找标记
21
- return res.status === 0 && (res.stdout ?? "").replace(/\u0000/g, "").includes("u1s1-shell-ok");
22
- }
23
- catch {
24
- return false;
25
- }
26
- }
27
- function runWhere(name) {
28
- try {
29
- const res = spawnSync("where", [name], { encoding: "utf-8", timeout: 5000, windowsHide: true });
30
- if (res.status !== 0 || !res.stdout)
31
- return [];
32
- return res.stdout
33
- .trim()
34
- .split(/\r?\n/)
35
- .map((l) => l.trim())
36
- .filter(Boolean);
37
- }
38
- catch {
39
- return [];
40
- }
46
+ child.on("error", () => finish(false, out));
47
+ child.on("close", (code) => finish(code === 0, out));
48
+ child.stdin?.on("error", () => { });
49
+ child.stdin?.end(opts.input ?? "");
50
+ });
41
51
  }
42
52
  /**
43
- * Git Bash 候选位置:pi 只认 Program Files 两处,漏掉 winget 默认的用户级
44
- * 安装(%LOCALAPPDATA%\Programs\Git)和 scoop;再从 git.exe 反推兜住
45
- * portable 安装。只返回磁盘上真实存在的路径。
53
+ * 真跑一条 echo 验活。WSL 转发器不支持 -c 传参(pi 对它走 stdin),探活同款;
54
+ * WSL 冷启动一个发行版可能要好几秒,超时放宽——真坏的 WSL(缺 /bin/bash)
55
+ * 是瞬间失败,不会吃满超时。
46
56
  */
47
- function candidateGitBashPaths() {
57
+ async function bashWorks(bashPath) {
58
+ const viaStdin = isWslRelayBash(bashPath);
59
+ const res = await runCapture(bashPath, viaStdin ? ["-s"] : ["-c", "echo u1s1-shell-ok"], {
60
+ input: viaStdin ? "echo u1s1-shell-ok\n" : undefined,
61
+ timeoutMs: viaStdin ? 10_000 : 5_000,
62
+ });
63
+ // WSL 转发器可能按 UTF-16LE 输出(ASCII 字符间夹 NUL),剔掉再找标记
64
+ return res.ok && res.stdout.replace(/\u0000/g, "").includes("u1s1-shell-ok");
65
+ }
66
+ async function runWhere(name) {
67
+ const res = await runCapture("where", [name], { timeoutMs: 5_000 });
68
+ if (!res.ok)
69
+ return [];
70
+ return res.stdout
71
+ .trim()
72
+ .split(/\r?\n/)
73
+ .map((l) => l.trim())
74
+ .filter(Boolean);
75
+ }
76
+ /** 快路径候选:环境变量推得出的固定安装位置,只花 existsSync,零进程开销。 */
77
+ function knownGitBashPaths() {
48
78
  const out = [];
49
- const add = (p) => {
50
- if (!out.includes(p))
51
- out.push(p);
52
- };
53
79
  for (const [envKey, ...rest] of [
54
80
  ["ProgramFiles", "Git", "bin", "bash.exe"],
55
81
  ["ProgramFiles(x86)", "Git", "bin", "bash.exe"],
56
- ["LOCALAPPDATA", "Programs", "Git", "bin", "bash.exe"],
82
+ ["LOCALAPPDATA", "Programs", "Git", "bin", "bash.exe"], // winget 用户级默认位置
57
83
  ["USERPROFILE", "scoop", "apps", "git", "current", "bin", "bash.exe"],
58
84
  ]) {
59
85
  const base = process.env[envKey];
60
- if (base)
61
- add(join(base, ...rest));
86
+ if (!base)
87
+ continue;
88
+ const p = join(base, ...rest);
89
+ if (!out.includes(p) && existsSync(p))
90
+ out.push(p);
62
91
  }
92
+ return out;
93
+ }
94
+ /** 慢路径候选:where git.exe 反推安装根,兜住 portable 等非常规安装。 */
95
+ async function discoveredGitBashPaths(skip) {
96
+ const out = [];
63
97
  // git.exe 在 <root>\cmd、<root>\bin 或 <root>\mingw64\bin 下,向上找 root
64
- for (const gitPath of runWhere("git.exe")) {
98
+ for (const gitPath of await runWhere("git.exe")) {
65
99
  const dir = dirname(gitPath);
66
100
  for (const root of [dirname(dir), dirname(dirname(dir))]) {
67
- add(join(root, "bin", "bash.exe"));
68
- add(join(root, "usr", "bin", "bash.exe"));
101
+ for (const p of [join(root, "bin", "bash.exe"), join(root, "usr", "bin", "bash.exe")]) {
102
+ if (!out.includes(p) && !skip.includes(p) && existsSync(p))
103
+ out.push(p);
104
+ }
69
105
  }
70
106
  }
71
- return out.filter((p) => existsSync(p));
107
+ return out;
72
108
  }
73
- export function ensureUsableShell() {
109
+ /** 持久化写 settings.json:写入时现读现改,避免覆盖并行流程刚写的其他键。 */
110
+ function persistShellPath(p) {
111
+ let settings = {};
112
+ if (existsSync(agentSettingsFile)) {
113
+ try {
114
+ settings = JSON.parse(readFileSync(agentSettingsFile, "utf8"));
115
+ }
116
+ catch {
117
+ return false; // 不动损坏的文件
118
+ }
119
+ }
120
+ settings["shellPath"] = p;
121
+ writeFileSync(agentSettingsFile, JSON.stringify(settings, null, 2) + "\n");
122
+ return true;
123
+ }
124
+ function adviseInstallGitBash() {
125
+ console.error(" ⚠ 未找到 Git Bash,将使用 WSL 的 bash:命令会在 WSL 的 Linux 里执行,不是 Windows 本机。");
126
+ console.error(" 建议安装 Git for Windows(PowerShell 运行: winget install --id Git.Git)后重启 u1s1。");
127
+ }
128
+ export async function ensureUsableShell() {
74
129
  if (process.platform !== "win32")
75
130
  return { status: "posix" };
76
131
  mkdirSync(agentDir, { recursive: true });
@@ -83,36 +138,50 @@ export function ensureUsableShell() {
83
138
  return { status: "unknown" }; // 不动损坏的文件,pi 启动时会自己报
84
139
  }
85
140
  }
86
- // 已配置且文件还在:信它,不每次启动都探活(坏了 pi 会在会话里报错)
141
+ // 已配置且文件还在:信它,不每次启动都探活(坏了 pi 会在会话里报错)
142
+ // 配置的是 WSL 转发器时,零成本看一眼 Git Bash 是否新装了,装了就换轨。
87
143
  const configured = settings["shellPath"];
88
144
  if (typeof configured === "string" && configured && existsSync(configured)) {
89
- return isWslRelayBash(configured)
90
- ? { status: "wsl", shellPath: configured }
91
- : { status: "bash", shellPath: configured };
92
- }
93
- // shellPath 缺失或指向已卸载的文件:探测 + 验活,找到就固定下来
94
- for (const candidate of candidateGitBashPaths()) {
95
- if (bashWorks(candidate)) {
96
- settings["shellPath"] = candidate;
97
- writeFileSync(agentSettingsFile, JSON.stringify(settings, null, 2) + "\n");
98
- if (typeof configured === "string") {
99
- console.error(` 原 shellPath 已失效,自动改用: ${candidate}`);
145
+ if (!isWslRelayBash(configured))
146
+ return { status: "bash", shellPath: configured };
147
+ for (const candidate of knownGitBashPaths()) {
148
+ if (await bashWorks(candidate)) {
149
+ persistShellPath(candidate);
150
+ console.error(` 检测到 Git Bash,命令改回 Windows 本机执行: ${candidate}`);
151
+ return { status: "bash", shellPath: candidate };
100
152
  }
101
- return { status: "bash", shellPath: candidate };
102
153
  }
154
+ adviseInstallGitBash();
155
+ return { status: "wsl", shellPath: configured };
156
+ }
157
+ // shellPath 缺失或指向已卸载的文件:探测 + 验活,找到就固定下来。
158
+ // 先试零开销的固定位置,全灭才花 where git.exe 找非常规安装。
159
+ const adopt = (candidate) => {
160
+ persistShellPath(candidate);
161
+ if (typeof configured === "string") {
162
+ console.error(` 原 shellPath 已失效,自动改用: ${candidate}`);
163
+ }
164
+ return { status: "bash", shellPath: candidate };
165
+ };
166
+ const known = knownGitBashPaths();
167
+ for (const candidate of known) {
168
+ if (await bashWorks(candidate))
169
+ return adopt(candidate);
170
+ }
171
+ for (const candidate of await discoveredGitBashPaths(known)) {
172
+ if (await bashWorks(candidate))
173
+ return adopt(candidate);
103
174
  }
104
175
  // 没有 Git Bash:看 PATH 上还有什么(与 pi 的兜底一致,但这里要验活)
105
- const fallback = runWhere("bash.exe").find((p) => existsSync(p));
106
- if (fallback && bashWorks(fallback)) {
176
+ const fallback = (await runWhere("bash.exe")).find((p) => existsSync(p));
177
+ if (fallback && (await bashWorks(fallback))) {
178
+ // WSL 转发器也持久化:冷启 WSL 验活要好几秒,不能每次启动都付。装了
179
+ // Git Bash 后的换轨由上面的 configured-WSL 分支负责(零成本 existsSync)。
180
+ persistShellPath(fallback);
107
181
  if (isWslRelayBash(fallback)) {
108
- // 能用但跑在 Linux 里。不写进 settings:一旦用户装了 Git Bash,
109
- // 下次启动就能自动切回 Windows 本机执行。
110
- console.error(" ⚠ 未找到 Git Bash,将使用 WSL 的 bash:命令会在 WSL 的 Linux 里执行,不是 Windows 本机。");
111
- console.error(" 建议安装 Git for Windows(PowerShell 运行: winget install --id Git.Git)后重启 u1s1。");
182
+ adviseInstallGitBash();
112
183
  return { status: "wsl", shellPath: fallback };
113
184
  }
114
- settings["shellPath"] = fallback;
115
- writeFileSync(agentSettingsFile, JSON.stringify(settings, null, 2) + "\n");
116
185
  return { status: "bash", shellPath: fallback };
117
186
  }
118
187
  if (fallback) {
package/dist/update.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execSync, spawn } from "node:child_process";
1
+ import { execSync, spawnSync } from "node:child_process";
2
2
  import { mkdtempSync, writeFileSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { tmpdir } from "node:os";
@@ -65,14 +65,25 @@ async function portableSelfUpdate(latest) {
65
65
  if (process.platform === "win32") {
66
66
  console.log(`正在打开更新窗口(升级到 v${latest})…`);
67
67
  console.log("更新在新窗口里进行,本窗口会退出;装完后重新打开终端运行 u1s1 即可。");
68
+ console.log("若新窗口没有出现,可手动在 PowerShell 运行: irm https://u1s1.io/releases/install.ps1 | iex");
68
69
  // 不走 `cmd /c start`:start 只把**带引号**的首参当窗口标题,而 node 对
69
- // 无空格参数不加引号,标题会被当成命令执行(0.13.1 翻车现场)。node 在
70
- // Windows 上 detached + stdio ignore 的子进程自带新控制台,直接 spawn
71
- // powershell 即可,参数按 Win32 规则原样传递,没有二次解析。
72
- const tmp = join(mkdtempSync(join(tmpdir(), "u1s1-update-")), "update.ps1");
73
- writeFileSync(tmp, "irm https://u1s1.io/releases/install.ps1 | iex\n");
74
- const child = spawn("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-NoExit", "-File", tmp], { detached: true, stdio: "ignore" });
75
- child.unref();
70
+ // 无空格参数不加引号,标题会被当成命令执行(0.13.1 翻车现场)。也不走
71
+ // spawn detached:Windows 上 detached DETACHED_PROCESS——子进程**没有**
72
+ // 控制台,窗口根本不出现,0.13.2-0.13.4 的更新其实在隐形 powershell
73
+ // 跑完的(当时的注释把这个标志理解反了)。可靠开窗走 ShellExecute 语义:
74
+ // 同步跑一个无窗 launcher,由它 Start-Process 弹真窗口(ShellExecute 出的
75
+ // 进程完全独立,不随本进程退出)。路径经 $PSScriptRoot 传递,不往命令行
76
+ // 拼字符串,没有引号二次解析问题。
77
+ const dir = mkdtempSync(join(tmpdir(), "u1s1-update-"));
78
+ writeFileSync(join(dir, "update.ps1"), "irm https://u1s1.io/releases/install.ps1 | iex\n");
79
+ writeFileSync(join(dir, "launch.ps1"), "$inner = '-NoProfile -ExecutionPolicy Bypass -NoExit -File \"' + $PSScriptRoot + '\\update.ps1\"'\n" +
80
+ "Start-Process powershell -ArgumentList $inner\n");
81
+ const res = spawnSync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(dir, "launch.ps1")], { stdio: "ignore", windowsHide: true, timeout: 30_000 });
82
+ if (res.error || res.status !== 0) {
83
+ console.error("无法拉起更新窗口。请手动在 PowerShell 里运行:");
84
+ console.error(" irm https://u1s1.io/releases/install.ps1 | iex");
85
+ process.exit(1);
86
+ }
76
87
  // 立刻退出释放 node.exe 的文件锁,让安装器能替换安装目录
77
88
  process.exit(0);
78
89
  }
package/dist/usage.js CHANGED
@@ -66,7 +66,7 @@ export async function usage() {
66
66
  console.log(" → 你有免费用量包没领:首月每天 1 亿 Token,去 https://u1s1.io/dashboard 点「领取」");
67
67
  }
68
68
  else if (me.free_claim === "renew") {
69
- console.log(" → 免费用量包到期了,去 https://u1s1.io/dashboard 续领:每天 5000 万 Token,一年有效");
69
+ console.log(" → 免费用量包到期了,去 https://u1s1.io/dashboard 续领:每天 3000 万 Token,一年有效");
70
70
  }
71
71
  else {
72
72
  console.log(" 免费包每天 0 点恢复;邀请朋友双方各得一次性 1 亿 Token 包 → https://u1s1.io/dashboard");
package/dist/web.js CHANGED
@@ -21,8 +21,9 @@ const require = createRequire(import.meta.url);
21
21
  */
22
22
  export async function prepareWebEnv(cfg) {
23
23
  cleanupBrandThemes();
24
- ensureBrandPrompt(ensureUsableShell());
25
24
  ensureDefaultSettings();
25
+ // Windows shell 体检异步先行,与取模型列表并行,写 brand prompt 前 await
26
+ const shellReady = ensureUsableShell();
26
27
  // 网页版/App 的 agent 会话同样依赖 fd/rg(同一个 agentDir);与取模型列表并行
27
28
  const searchToolsReady = ensureSearchTools(cfg);
28
29
  // Fetch model list from server; fall back to built-in MODELS on error.
@@ -39,6 +40,7 @@ export async function prepareWebEnv(cfg) {
39
40
  console.error(" 获取模型列表失败,使用内置列表:", e.message);
40
41
  }
41
42
  await endpointsReady;
43
+ ensureBrandPrompt(await shellReady);
42
44
  ensureProviderModels(cfg);
43
45
  // pi-web-ui 靠 auth.json 判断「已配置」,否则网页会弹 pi 安装引导
44
46
  ensureAuthCredential();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "0.13.4",
3
+ "version": "0.13.6",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {