u1s1-cli 0.11.0 → 0.11.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.
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { writeFileSync } from "node:fs";
4
4
  import { cleanupBrandThemes, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
5
5
  import { printConsoleBanner } from "./brand.js";
6
6
  import { agentDir, apiModelToDef, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
7
+ import { ensureSearchTools } from "./search-tools.js";
7
8
  import { applyBrandUi, setUpdateNotice } from "./style.js";
8
9
  import { offerStarterTemplates } from "./templates.js";
9
10
  import { fetchModels } from "./api.js";
@@ -117,6 +118,9 @@ async function runAgent(cfg, args) {
117
118
  cleanupBrandThemes();
118
119
  ensureBrandPrompt();
119
120
  ensureDefaultSettings();
121
+ // 预 seed fd/rg(国内直连 GitHub 不通,pi 自己下不动);与取模型列表并行,
122
+ // 但必须在进 pi 之前就位,否则 pi 会自己去 GitHub 下载
123
+ const searchToolsReady = ensureSearchTools(cfg);
120
124
  // Fetch model list from server; fall back to built-in MODELS on error.
121
125
  // 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search,模型就不会白调
122
126
  let webSearchEnabled = true;
@@ -138,6 +142,7 @@ async function runAgent(cfg, args) {
138
142
  process.env["U1S1_TOOLS_VIA_EXTENSION"] = "1";
139
143
  // hide pi's own "Run pi update" banner; users should run `u1s1 update` instead
140
144
  process.env["PI_SKIP_VERSION_CHECK"] = "1";
145
+ await searchToolsReady;
141
146
  const { CustomEditor, main } = await import("@earendil-works/pi-coding-agent");
142
147
  // 网页终端(如 Taikula)把 Shift+Enter 发成 ESC+CR(\x1b\r)。pi 在没开 Kitty
143
148
  // 协议时把它当成 Alt+Enter:空闲就直接发送,忙碌才排队。先改成 CSI-u 的
@@ -0,0 +1,3 @@
1
+ import { type CliConfig } from "./config.js";
2
+ /** 缺哪个装哪个;都在(本地目录或 PATH)则秒退。失败只提示,不阻塞启动。 */
3
+ export declare function ensureSearchTools(cfg: Pick<CliConfig, "baseUrl">): Promise<void>;
@@ -0,0 +1,153 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
3
+ import { arch, platform } from "node:os";
4
+ import { join } from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { agentDir } from "./config.js";
8
+ /**
9
+ * 预 seed pi 的搜索组件(fd / ripgrep)到 ~/.u1s1/agent/bin。
10
+ *
11
+ * pi 首启发现缺 fd/rg 会从 GitHub 现下,国内直连基本不通:启动时连弹
12
+ * "Failed to download ... fetch failed",更糟的是 grep/find 工具运行时直接
13
+ * 报错,没有降级路径。这里在进 pi 之前从自家网关(/tools/<asset>,CF 边缘
14
+ * 代理 GitHub release,见 gateway/src/index.ts)把二进制放进 pi 的探测目录
15
+ * (探测顺序:agentDir/bin → PATH);探到本地文件后 pi 不再碰 GitHub。
16
+ * 任何失败都不阻塞启动——pi 自己的 GitHub 下载仍是兜底(有代理的用户能通)。
17
+ *
18
+ * 版本钉死而不是查 latest:免去对 GitHub API 的依赖,且 fd 10.4+ 不再发
19
+ * x86_64 macOS 产物,统一钉 10.3.0(与 pi 对 darwin-x64 的钉版一致)。
20
+ */
21
+ const TOOLS = [
22
+ { bin: "fd", version: "10.3.0", pathNames: ["fd", "fdfind"], asset: fdAsset },
23
+ { bin: "rg", version: "15.2.0", pathNames: ["rg"], asset: rgAsset },
24
+ ];
25
+ const binDir = join(agentDir, "bin");
26
+ const DOWNLOAD_TIMEOUT_MS = 60_000;
27
+ function archStr() {
28
+ const a = arch();
29
+ return a === "arm64" ? "aarch64" : a === "x64" ? "x86_64" : null;
30
+ }
31
+ function fdAsset(version) {
32
+ const a = archStr();
33
+ if (!a)
34
+ return null;
35
+ const p = platform();
36
+ if (p === "darwin")
37
+ return `fd-v${version}-${a}-apple-darwin.tar.gz`;
38
+ if (p === "linux")
39
+ return `fd-v${version}-${a}-unknown-linux-gnu.tar.gz`;
40
+ if (p === "win32")
41
+ return `fd-v${version}-${a}-pc-windows-msvc.zip`;
42
+ return null;
43
+ }
44
+ function rgAsset(version) {
45
+ const a = archStr();
46
+ if (!a)
47
+ return null;
48
+ const p = platform();
49
+ if (p === "darwin")
50
+ return `ripgrep-${version}-${a}-apple-darwin.tar.gz`;
51
+ // linux 上 arm64 用 gnu、x64 用 musl:与 pi 的资产选择保持一致
52
+ if (p === "linux") {
53
+ return a === "aarch64"
54
+ ? `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`
55
+ : `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;
56
+ }
57
+ if (p === "win32")
58
+ return `ripgrep-${version}-${a}-pc-windows-msvc.zip`;
59
+ return null;
60
+ }
61
+ /** 与 pi 的 commandExists 同语义:能 spawn 就算在 PATH 里(fd 也认 Debian 名 fdfind)。 */
62
+ function inPath(names) {
63
+ return names.some((name) => {
64
+ const r = spawnSync(name, ["--version"], { stdio: "pipe" });
65
+ return r.error === undefined || r.error === null;
66
+ });
67
+ }
68
+ function run(command, cmdArgs) {
69
+ const r = spawnSync(command, cmdArgs, { stdio: "pipe" });
70
+ return !r.error && r.status === 0;
71
+ }
72
+ function extract(archive, dir) {
73
+ if (archive.endsWith(".zip")) {
74
+ // 只有 win32 会拿到 zip。System32 的 tar 是 bsdtar,认 zip;Git Bash 的
75
+ // GNU tar 不认——优先绝对路径,失败再退 powershell Expand-Archive。
76
+ const sysRoot = process.env["SystemRoot"] ?? process.env["WINDIR"];
77
+ const sysTar = sysRoot ? join(sysRoot, "System32", "tar.exe") : "tar.exe";
78
+ if (run(existsSync(sysTar) ? sysTar : "tar.exe", ["xf", archive, "-C", dir]))
79
+ return true;
80
+ return run("powershell.exe", [
81
+ "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command",
82
+ "& { param($a,$d) $ErrorActionPreference='Stop'; Expand-Archive -LiteralPath $a -DestinationPath $d -Force }",
83
+ archive, dir,
84
+ ]);
85
+ }
86
+ return run("tar", ["xzf", archive, "-C", dir]);
87
+ }
88
+ /** 资产里的二进制可能嵌在版本号目录下(fd 是,rg 也是),递归找。 */
89
+ function findBinary(root, name) {
90
+ const stack = [root];
91
+ while (stack.length > 0) {
92
+ const dir = stack.pop();
93
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
94
+ const full = join(dir, entry.name);
95
+ if (entry.isFile() && entry.name === name)
96
+ return full;
97
+ if (entry.isDirectory())
98
+ stack.push(full);
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ async function install(tool, baseUrl, binName) {
104
+ const asset = tool.asset(tool.version);
105
+ if (!asset)
106
+ throw new Error(`不支持的平台 ${platform()}/${arch()}`);
107
+ mkdirSync(binDir, { recursive: true });
108
+ const res = await fetch(new URL(`/tools/${asset}`, baseUrl), {
109
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
110
+ });
111
+ if (!res.ok || !res.body)
112
+ throw new Error(`HTTP ${res.status}`);
113
+ const archive = join(binDir, asset);
114
+ // fd/rg 并行安装,解压目录按工具名+pid 隔离,避免互踩
115
+ const extractDir = join(binDir, `preseed_${tool.bin}_${process.pid}`);
116
+ try {
117
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(archive));
118
+ mkdirSync(extractDir, { recursive: true });
119
+ if (!extract(archive, extractDir))
120
+ throw new Error(`解压失败 ${asset}`);
121
+ const found = findBinary(extractDir, binName);
122
+ if (!found)
123
+ throw new Error(`包内没有 ${binName}`);
124
+ renameSync(found, join(binDir, binName));
125
+ if (platform() !== "win32")
126
+ chmodSync(join(binDir, binName), 0o755);
127
+ }
128
+ finally {
129
+ rmSync(archive, { force: true });
130
+ rmSync(extractDir, { recursive: true, force: true });
131
+ }
132
+ }
133
+ /** 缺哪个装哪个;都在(本地目录或 PATH)则秒退。失败只提示,不阻塞启动。 */
134
+ export async function ensureSearchTools(cfg) {
135
+ try {
136
+ const ext = platform() === "win32" ? ".exe" : "";
137
+ const missing = TOOLS.filter((t) => !existsSync(join(binDir, t.bin + ext)) && !inPath(t.pathNames));
138
+ if (missing.length === 0)
139
+ return;
140
+ console.log(` 正在安装搜索组件(${missing.map((t) => t.bin).join("/")})…`);
141
+ const results = await Promise.allSettled(missing.map((t) => install(t, cfg.baseUrl, t.bin + ext)));
142
+ for (let i = 0; i < results.length; i++) {
143
+ const r = results[i];
144
+ if (r.status === "rejected") {
145
+ const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
146
+ console.error(` ${missing[i].bin} 安装失败(${msg}),稍后将尝试 GitHub 直连`);
147
+ }
148
+ }
149
+ }
150
+ catch {
151
+ // 预 seed 只是加速,任何意外都不该挡住启动
152
+ }
153
+ }
package/dist/web.js CHANGED
@@ -4,6 +4,7 @@ import { createRequire } from "node:module";
4
4
  import { dirname, join } from "node:path";
5
5
  import { cleanupBrandThemes, ensureAuthCredential, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
6
6
  import { agentDir, apiModelToDef, resolvePreferredModel, setModelsFromApi, u1s1Dir, writeAgentDefaultModel, } from "./config.js";
7
+ import { ensureSearchTools } from "./search-tools.js";
7
8
  import { applyWebUiBranding, applyWebUiFrontend } from "./webui-brand.js";
8
9
  import { fetchModels } from "./api.js";
9
10
  const require = createRequire(import.meta.url);
@@ -21,6 +22,8 @@ export async function prepareWebEnv(cfg) {
21
22
  cleanupBrandThemes();
22
23
  ensureBrandPrompt();
23
24
  ensureDefaultSettings();
25
+ // 网页版/App 的 agent 会话同样依赖 fd/rg(同一个 agentDir);与取模型列表并行
26
+ const searchToolsReady = ensureSearchTools(cfg);
24
27
  // Fetch model list from server; fall back to built-in MODELS on error.
25
28
  // 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search
26
29
  let webSearchEnabled = true;
@@ -42,6 +45,7 @@ export async function prepareWebEnv(cfg) {
42
45
  writeAgentDefaultModel(resolvePreferredModel(cfg.model));
43
46
  const dataDir = join(u1s1Dir, "web");
44
47
  mkdirSync(dataDir, { recursive: true });
48
+ await searchToolsReady;
45
49
  return {
46
50
  PI_CODING_AGENT_DIR: agentDir,
47
51
  U1S1_API_KEY: cfg.apiKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {