glmcode 0.1.10 → 0.1.12

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.1.12 (2026-08-28)
4
+
5
+ - **新增 `COMPATIBILITY.md` 跨平台兼容性说明**:覆盖 Windows / macOS / Linux / Android(Termux) / FreeBSD 等全部支持系统,含功能支持矩阵、各平台安装方式、跨平台自动适配机制、已知限制与自测清单
6
+ - **新增 `glmcode --selftest` 跨平台自检**:通过改写 `process.platform` 虚拟模拟 linux / darwin / win32 / freebsd / openbsd / android / sunos 七种平台,逐一验证 shellInfo、buildShellArgs、openUrlCandidates、包管理器探测与安装命令;并在真实平台验证 run_shell、配置存取、文件读写、SKILL.md 技能加载、zip 技能解压、TUI 命令过滤与状态栏。当前 35 项全部 PASS
7
+ - 修复 `run_shell` 严重 bug:`buildShellArgs` 返回 `args` 属性但调用处误解构为 `a`,导致所有平台 `/run` 实际以无参数方式调用 shell(静默无输出)。已修正解构,该 bug 由本次 `--selftest` 抓出
8
+ - 修复 `run_shell` 对 fish / csh / tcsh 等 shell 的兼容:这类 shell 不支持 `-lc` 参数,现自动改用 `-c`
9
+ - 修复 `PACKAGE_ROOT` 路径解析:改用 `fileURLToPath`,避免 Windows 下 `import.meta.url` 路径带前导斜杠导致技能目录定位异常
10
+ - `/install` 更稳健:Windows 检测 `winget`、macOS 检测 `brew` 是否真实存在,不存在时不再误报可用
11
+ - 版本升级至 0.1.12,`COMPATIBILITY.md` 纳入 npm 发布包
12
+
13
+ ## 0.1.11 (2026-08-28)
14
+
15
+ - **真 TUI 交互层**:引入 `terminal-kit`,彻底替换 readline
16
+ - 输入 `/` 立即弹出**悬浮命令菜单**(不回车也显示),输入字母**实时过滤**命令
17
+ - **↑/↓ 方向键**直接选择菜单中的命令,Enter 执行,Tab 补全
18
+ - 底部**常驻状态栏**(反色条):思考开关 / 模型 / 技能数 / 当前目录
19
+ - 支持命令历史(↑/↓ 无菜单时浏览)、Home/End/DEL、Ctrl+L 清屏
20
+ - 修复 `term.height` 未就绪时菜单无法绘制的问题
21
+
3
22
  ## 0.1.10 (2026-08-28)
4
23
 
5
24
  - 移除 `postinstall` 脚本,消除 npm `install-scripts / allowScripts` 警告
@@ -0,0 +1,153 @@
1
+ ---
2
+ AIGC:
3
+ Label: "1"
4
+ ContentProducer: 001191440300708461136T1XGW3
5
+ ProduceID: 71f570a12158158e965e493bb4836f3f_e104f8d2a29e11f192a2525400287e28
6
+ ReservedCode1: AgUVRG+diiMzAe/cS7waYAmhNStXQ2B/ks40W+DvMkeOdMLZ/iLUbP0FZZ28KCIW/jyxAA9UrTMBKscXFN2z2sq7NCobd3t4Dx9i49BsjmFok10emFN4Aw8REwEWYrFrxB0yEWvUDxY/7+hq5CQE0w4bkZQbSOseEEeoOB8XyQvo/8hG0ZnIjUi2Sms=
7
+ ContentPropagator: 001191440300708461136T1XGW3
8
+ PropagateID: 71f570a12158158e965e493bb4836f3f_e104f8d2a29e11f192a2525400287e28
9
+ ReservedCode2: AgUVRG+diiMzAe/cS7waYAmhNStXQ2B/ks40W+DvMkeOdMLZ/iLUbP0FZZ28KCIW/jyxAA9UrTMBKscXFN2z2sq7NCobd3t4Dx9i49BsjmFok10emFN4Aw8REwEWYrFrxB0yEWvUDxY/7+hq5CQE0w4bkZQbSOseEEeoOB8XyQvo/8hG0ZnIjUi2Sms=
10
+ ---
11
+
12
+ # GLMCode 跨平台兼容性说明
13
+
14
+ 本文档说明 GLMCode 在各操作系统下的支持情况。**设计目标:所有平台上的所有功能均可用**,差异仅在底层系统提供的工具(包管理器 / Shell / 打开浏览器命令)不同,GLMCode 已做自动适配。
15
+
16
+ ## 一、支持的系统
17
+
18
+ GLMCode 基于 Node.js 18+ 运行,凡能安装 Node.js 18+ 并支持 TTY 终端的系统均可运行。已显式适配与验证的平台:
19
+
20
+ | 平台 | process.platform | 安装方式 | 推荐包管理器 | 默认 Shell |
21
+ |------|------------------|----------|--------------|------------|
22
+ | Linux(主流发行版) | `linux` | npm | apt / apt-get / dnf / pacman | bash / zsh / sh |
23
+ | Alpine Linux 等 | `linux` | npm | apk | ash / sh |
24
+ | FreeBSD / OpenBSD | `freebsd` / `openbsd` | npm | pkg | sh / csh / tcsh |
25
+ | Android(Termux) | `linux` | npm | pkg | bash |
26
+ | macOS | `darwin` | npm | brew | zsh / bash |
27
+ | Windows | `win32` | npm | winget | PowerShell / cmd |
28
+
29
+ > 其他类 Unix 系统(如 Solaris 等):只要 Node.js 可运行,功能同样可用;GLMCode 按通用 POSIX 分支处理,不依赖特定发行版。
30
+
31
+ ## 二、各平台安装步骤
32
+
33
+ 统一通过 npm 全局安装,全部平台共用一条命令:
34
+
35
+ ```bash
36
+ npm install -g glmcode
37
+ ```
38
+
39
+ - **Android(Termux)**:先 `pkg install nodejs npm` 装好 Node,再执行上面的安装命令。
40
+ - **Windows**:先安装 [Node.js](https://nodejs.org/)(自带 npm),再执行安装命令;`glmcode` / `glm` / `gc` 三个命令自动加入 PATH。
41
+ - **macOS**:先 `brew install node` 或从官网安装 Node,再执行安装命令。
42
+ - **Linux**:用发行版包管理器安装 Node.js 后执行安装命令。
43
+
44
+ 安装后运行:
45
+
46
+ ```bash
47
+ glmcode auth set <你的智谱API_KEY> # 首次设置 API Key
48
+ glmcode # 启动对话
49
+ ```
50
+
51
+ API Key 也可以不写盘,直接用环境变量注入:
52
+
53
+ ```bash
54
+ export ZHIPU_API_KEY=sk-xxx # 或 GLM_API_KEY
55
+ ```
56
+
57
+ ## 三、功能支持矩阵
58
+
59
+ 以下所有功能在**全部受支持平台**上均可使用。✅ 表示原生支持,注释列说明平台差异点。
60
+
61
+ | 功能 | Windows | macOS | Linux | Android(Termux) | 平台差异说明 |
62
+ |------|:---:|:---:|:---:|:---:|------|
63
+ | 流式对话 + 思考模式 | ✅ | ✅ | ✅ | ✅ | 纯网络请求,无差异 |
64
+ | `/run` 执行 Shell 命令 | ✅ | ✅ | ✅ | ✅ | 自动识别 cmd/PowerShell/POSIX shell |
65
+ | `write_file` / `read_file` 读写文件 | ✅ | ✅ | ✅ | ✅ | 使用系统原生路径 |
66
+ | 技能系统(SKILL.md 目录 / zip 自动解压) | ✅ | ✅ | ✅ | ✅ | zip 解压依赖 `adm-zip`(自动安装) |
67
+ | 真 TUI 悬浮菜单 / 状态栏 / 方向键 | ✅ | ✅ | ✅ | ✅ | 需 TTY 终端;Windows 建议用 Windows Terminal |
68
+ | 命令历史 / Tab 补全 / Ctrl+L | ✅ | ✅ | ✅ | ✅ | 需 TTY 终端 |
69
+ | `/install` 自动安装程序 | ✅ | ✅ | ✅ | ✅ | 自动选用 winget / brew / pkg 等 |
70
+ | `auth login` 打开浏览器 | ✅ | ✅ | ✅ | ✅ | Windows: start;macOS: open;Linux: xdg-open/gio;Android: termux-open-url |
71
+ | `auth set/status/logout` | ✅ | ✅ | ✅ | ✅ | 凭据存于 `~/.glmcode/auth.json`(权限 600) |
72
+ | 配置持久化 | ✅ | ✅ | ✅ | ✅ | 存于 `~/.glmcode/config.json` |
73
+ | 环境变量注入 API Key / 技能目录 | ✅ | ✅ | ✅ | ✅ | `ZHIPU_API_KEY` / `GLM_API_KEY` / `GLM_SKILLS_DIR` |
74
+ | 依赖缺失自动安装(ensureDeps) | ✅ | ✅ | ✅ | ✅ | 需要本机已有 npm |
75
+ | `glmcode shell` 系统信息 | ✅ | ✅ | ✅ | ✅ | 输出 OS / 架构 / Shell |
76
+ | `glmcode --version` / `--help` | ✅ | ✅ | ✅ | ✅ | 无差异 |
77
+
78
+ ## 四、跨平台自动适配机制
79
+
80
+ GLMCode 内部按平台自动选择底层命令,无需用户手动配置:
81
+
82
+ 1. **Shell 识别(shellInfo)**
83
+ - Windows:检测 `PWSH` / `POWERSHELL` 环境变量,存在则用 PowerShell,否则用 `ComSpec`(cmd.exe)。
84
+ - 其他系统:读取 `$SHELL` 环境变量,缺省回退 `/bin/sh`。
85
+
86
+ 2. **Shell 命令拼装(run_shell)**
87
+ - Windows + PowerShell:`powershell -NoProfile -Command <命令>`
88
+ - Windows + cmd:`cmd /d /s /c <命令>`
89
+ - POSIX shell(sh/bash/zsh/dash/ash):`<shell> -lc <命令>`(登录 shell 执行)
90
+ - fish / csh / tcsh:`<shell> -c <命令>`(这三种 shell 不支持 `-lc` 组合参数)
91
+
92
+ 3. **打开浏览器(openUrl)**,按优先级自动挑选本机可用的命令:
93
+ - Windows:`cmd /c start "" <url>`
94
+ - macOS:`open <url>`
95
+ - Linux / 其他:`xdg-open` → `termux-open-url`(Android)→ `gio open`
96
+ - 逐个探测,找到可用的就执行;全部不可用时提示"当前环境无法自动打开浏览器"。
97
+
98
+ 4. **包管理器探测(detectInstaller)**,供 `/install` 使用:
99
+ - Windows:`winget`(未安装则提示无可用包管理器)
100
+ - macOS:`brew`(Homebrew)
101
+ - Linux / BSD / Android:按 `apk` → `apt-get` → `apt` → `dnf` → `pacman` → `pkg` 顺序探测
102
+ - 每个平台只会调用真实存在的包管理器,避免"找不到命令"报错。
103
+
104
+ 5. **命令存在性探测(commandExists)**
105
+ - Windows:`where <命令>`
106
+ - 其他系统:`which <命令>`
107
+
108
+ 6. **安装命令构建(/install)**
109
+
110
+ | 平台 | 实际执行 |
111
+ |------|----------|
112
+ | Windows | `winget install <包>` |
113
+ | macOS | `brew install <包>` |
114
+ | Alpine | `apk add <包>` |
115
+ | Debian/Ubuntu | `apt-get install -y <包>` 或 `apt install -y <包>` |
116
+ | Fedora/RHEL | `dnf install -y <包>` |
117
+ | Arch | `pacman -S --noconfirm <包>` |
118
+ | FreeBSD / Termux | `pkg install -y <包>` |
119
+
120
+ ## 五、路径约定
121
+
122
+ - 配置与凭据目录:`~/.glmcode/`(即各系统用户主目录下的 `.glmcode`,Windows 为 `%USERPROFILE%\.glmcode`)。
123
+ - `auth.json` 与 `config.json` 创建时权限为 `0600`(仅当前用户可读写),目录权限 `0700`。
124
+ - 技能目录:默认取包安装目录下的 `skills/`,可用环境变量 `GLM_SKILLS_DIR` 指向自定义目录(支持相对/绝对路径)。
125
+ - 所有路径拼接使用 Node.js `path` 模块,自动适配 Windows(`\`)与类 Unix(`/`)分隔符。
126
+
127
+ ## 六、已知限制与注意事项
128
+
129
+ 1. **真 TUI 需要真实 TTY**:TUI 悬浮菜单、状态栏、方向键交互依赖终端。通过管道/重定向或 CI 无 TTY 环境运行时,交互层不可用(`terminal-kit` 会退化),但 `auth`、`shell`、`--version` 等子命令不受影响。
130
+ 2. **Windows 建议使用 Windows Terminal / ConEmu**:GLMCode 使用 ANSI 转义序列渲染颜色与状态栏,旧版 `cmd.exe` 控制台可能显示异常;Windows Terminal 与 PowerShell 7 无此问题。
131
+ 3. **`/install` 需要对应权限**:Linux 上 `apt/dnf/pacman` 需要 root(一般以 `sudo` 启动 GLMCode 或自行以 sudo 安装);Termux 的 `pkg`、Windows 的 `winget`、macOS 的 `brew` 不需要 root。
132
+ 4. **openUrl 依赖桌面环境**:无图形界面的 Linux 服务器若未安装 `xdg-utils` 等工具,`auth login` 无法打开浏览器,但可通过 `auth set` 手动配置。
133
+ 5. **Android 存储目录**:Termux 直接读写 `/storage/emulated/0/...` 需要 `termux-setup-storage` 授权一次存储权限;读写自身私有目录(`~/...`)无需授权。
134
+ 6. **`/run` 命令 60 秒超时**:所有平台统一限制,超长命令会被终止并返回部分输出。
135
+ 7. **依赖自动安装需要 npm**:`ensureDeps` 在缺失 `adm-zip` / `terminal-kit` 时调用 `npm install` 兜底,因此本机需存在 npm(安装 GLMCode 时已隐含该前提)。
136
+
137
+ ## 七、已验证 / 建议自测清单
138
+
139
+ 每个平台可运行以下命令快速验证:
140
+
141
+ ```bash
142
+ glmcode --selftest # 一键跨平台自检:虚拟模拟 7 种平台 + 真实平台功能验证
143
+ glmcode --version # 版本输出
144
+ glmcode shell # 显示 OS / 架构 / Shell
145
+ glmcode shell list # 列出可用 Shell
146
+ glmcode --help # 帮助(含 TUI 用法说明)
147
+ glmcode auth status # 认证状态
148
+ ```
149
+
150
+ `glmcode --selftest` 会虚拟模拟 **linux / darwin / win32 / freebsd / openbsd / android / sunos** 七种平台,逐一验证 Shell 识别、Shell 命令拼装、打开浏览器候选命令、包管理器探测与安装命令;并在当前真实平台验证 `run_shell` 实际执行、配置存取、文件读写、SKILL.md 技能加载、zip 技能自动解压、TUI 命令过滤与状态栏渲染。全部通过会显示 `PASS 35 / FAIL 0`。
151
+
152
+ 启动对话后依次测试:`/` 弹出菜单 → ↑↓ 选择 → Enter 执行 → `/run echo ok` → `/ls` → `/install`(视平台)。
153
+ *(内容由AI生成,仅供参考)*
package/README.md CHANGED
@@ -11,9 +11,9 @@ npm install -g glmcode
11
11
  - 流式对话(支持思考模式)
12
12
  - 四个内置工具:`run_shell` / `write_file` / `read_file` / `use_skill`
13
13
  - 技能系统:把 `SKILL.md` 目录或 `.zip` 技能包放进 `skills/` 自动加载
14
- - 斜杠命令菜单:输入 `/` 或前缀即弹出菜单,Tab 补全
15
- - 底部状态栏:实时显示思考开关 / 模型 / 技能数 / 当前目录
16
- - 自动安装依赖:缺失 `adm-zip` 等依赖时启动自动 `npm install`
14
+ - **真 TUI 交互**:输入 `/` 立即弹出悬浮命令菜单(不回车也显示),按字母实时过滤,↑/↓ 方向键选择,Tab 补全,Enter 执行
15
+ - **底部常驻状态栏**(反色条):实时显示思考开关 / 模型 / 技能数 / 当前目录
16
+ - 自动安装依赖:缺失 `adm-zip`、`terminal-kit` 等依赖时启动自动 `npm install`
17
17
  - `/install` 命令:调用系统包管理器(apt/apk/pkg/brew/winget 等)自动安装程序
18
18
  - 认证:`glmcode auth set <API_KEY>`
19
19
 
@@ -21,6 +21,7 @@ npm install -g glmcode
21
21
 
22
22
  ```bash
23
23
  glmcode # 启动对话
24
+ glmcode --selftest # 一键跨平台自检(虚拟模拟 7 种平台 + 真实平台功能验证)
24
25
  glmcode --help # 帮助
25
26
  glmcode auth set sk-xxx # 设置 API Key
26
27
  glmcode shell # 查看系统信息
@@ -54,3 +55,9 @@ mkdir -p skills/my-skill
54
55
  ```
55
56
 
56
57
  启动后输入 `/技能名` 即可注入技能指令。
58
+
59
+ ## 跨平台兼容性
60
+
61
+ GLMCode 支持 **Windows / macOS / Linux / Android(Termux) / FreeBSD 等所有可运行 Node.js 18+ 的系统**,所有功能在全部平台上均可用。程序自动适配各平台的 Shell(cmd / PowerShell / bash / zsh / fish 等)、包管理器(winget / brew / apt / dnf / pacman / apk / pkg)、打开浏览器命令与路径分隔符。
62
+
63
+ 详见 [COMPATIBILITY.md](COMPATIBILITY.md)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glmcode",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "GLM coding agent CLI with shell tools, skills (SKILL.md/zip auto-extract), streaming chat, slash-command menu and bottom status bar. Auto-installs its dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,11 +15,13 @@
15
15
  "src/",
16
16
  "skills/",
17
17
  "README.md",
18
+ "COMPATIBILITY.md",
18
19
  "CHANGELOG.md",
19
20
  "LICENSE"
20
21
  ],
21
22
  "dependencies": {
22
- "adm-zip": "^0.5.16"
23
+ "adm-zip": "^0.5.16",
24
+ "terminal-kit": "^3.1.4"
23
25
  },
24
26
  "engines": {
25
27
  "node": ">=18"
package/src/cli.js CHANGED
@@ -2,11 +2,12 @@
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import readline from "node:readline";
6
5
  import {spawnSync, spawn} from "node:child_process";
7
6
  import {fileURLToPath} from "node:url";
7
+ import termkit from "terminal-kit";
8
+ const term = termkit.terminal;
8
9
 
9
- const VERSION="0.1.10";
10
+ const VERSION="0.1.12";
10
11
  const BASE_URL="https://open.bigmodel.cn/api/paas/v4";
11
12
  const API_KEYS_URL="https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys";
12
13
  const HOME=os.homedir();
@@ -28,7 +29,6 @@ const DEFAULT_CONFIG={
28
29
 
29
30
  let config={...DEFAULT_CONFIG};
30
31
  let apiKey=null, messages=[], cwd=process.cwd(), thinkingEnabled=false, skills=new Map();
31
- let stopping=false;
32
32
 
33
33
  function readJson(file,fallback={}){try{return JSON.parse(fs.readFileSync(file,"utf8"));}catch{return fallback;}}
34
34
  function saveJson(file,v){fs.mkdirSync(DATA_DIR,{recursive:true,mode:0o700});fs.writeFileSync(file,JSON.stringify(v,null,2)+"\n",{mode:0o600});try{fs.chmodSync(file,0o600);}catch{}}
@@ -49,12 +49,19 @@ function shellInfo(){
49
49
  }
50
50
  const exe=process.env.SHELL||"/bin/sh"; return {name:path.basename(exe),exe};
51
51
  }
52
+ function buildShellArgs(s,cmd){
53
+ if(process.platform==="win32")return {file:s.exe,args:s.name==="powershell"?["-NoProfile","-Command",cmd]:["/d","/s","/c",cmd]};
54
+ const n=s.name.toLowerCase();
55
+ return {file:s.exe,args:(n==="fish"||n==="csh"||n==="tcsh")?["-c",cmd]:["-lc",cmd]};
56
+ }
52
57
  function commandExists(c){try{spawnSync(process.platform==="win32"?"where":"which",[c],{stdio:"ignore"});return true;}catch{return false;}}
58
+ function openUrlCandidates(url){
59
+ if(process.platform==="win32")return [["cmd",["/c","start","",url]]];
60
+ if(process.platform==="darwin")return [["open",[url]]];
61
+ return [["xdg-open",[url]],["termux-open-url",[url]],["gio",["open",url]]];
62
+ }
53
63
  function openUrl(url){
54
- const c=process.platform==="win32"?[["cmd",["/c","start","",url]]]:
55
- process.platform==="darwin"?[["open",[url]]]:
56
- [["xdg-open",[url]],["termux-open-url",[url]],["gio",["open",url]]];
57
- for(const [cmd,args] of c){if(!commandExists(cmd))continue;try{const p=spawn(cmd,args,{detached:true,stdio:"ignore"});p.unref();return true;}catch{}}
64
+ for(const [cmd,args] of openUrlCandidates(url)){if(!commandExists(cmd))continue;try{const p=spawn(cmd,args,{detached:true,stdio:"ignore"});p.unref();return true;}catch{}}
58
65
  return false;
59
66
  }
60
67
 
@@ -62,6 +69,7 @@ function openUrl(url){
62
69
  async function ensureDeps(){
63
70
  const missing=[];
64
71
  try{await import("adm-zip");}catch{missing.push("adm-zip");}
72
+ try{await import("terminal-kit");}catch{missing.push("terminal-kit");}
65
73
  if(!missing.length)return true;
66
74
  console.log(color(`[glmcode] 缺少依赖: ${missing.join(", ")},正在自动安装...`,C.yellow));
67
75
  let ok=true;
@@ -84,7 +92,6 @@ async function extractZipSkills(){
84
92
  if(fs.existsSync(target))continue;
85
93
  try{
86
94
  new AdmZip(zp).extractAllTo(target,true);
87
- // 扁平化:zip 内带同名根目录时(如 my-skill/SKILL.md)解压会出现 target/my-skill/... 嵌套,上移一层
88
95
  const sub=fs.readdirSync(target,{withFileTypes:true}).find(d=>d.isDirectory());
89
96
  if(sub && !fs.existsSync(path.join(target,"SKILL.md")) && fs.existsSync(path.join(target,sub.name,"SKILL.md"))){
90
97
  for(const e of fs.readdirSync(path.join(target,sub.name))){
@@ -136,9 +143,8 @@ const TOOLS=[
136
143
 
137
144
  function execTool(name,args){
138
145
  if(name==="run_shell"){
139
- const s=shellInfo(), isWin=process.platform==="win32";
140
- const file=s.exe, a=isWin?(s.name==="powershell"?["-NoProfile","-Command",args.command]:["/d","/s","/c",args.command]):["-lc",args.command];
141
- try{const r=spawnSync(file,a,{cwd,encoding:"utf8",timeout:60000});return ((r.stdout||"")+(r.stderr?"\n[stderr]\n"+r.stderr:"")).slice(0,8000)||"(无输出)";}
146
+ const {file,args:shArgs}=buildShellArgs(shellInfo(),args.command);
147
+ try{const r=spawnSync(file,shArgs,{cwd,encoding:"utf8",timeout:60000});return ((r.stdout||"")+(r.stderr?"\n[stderr]\n"+r.stderr:"")).slice(0,8000)||"(无输出)";}
142
148
  catch(e){return `执行失败: ${e.message}`;}
143
149
  }
144
150
  if(name==="write_file"){try{const p=path.resolve(cwd,args.path);fs.mkdirSync(path.dirname(p),{recursive:true});fs.writeFileSync(p,args.content);return `已写入 ${p}`;}catch(e){return `写入失败: ${e.message}`;}}
@@ -194,7 +200,7 @@ async function chat(text){
194
200
  }
195
201
  }
196
202
  }
197
- /* ---- 斜杠命令菜单:输入 / 即弹出 ---- */
203
+ /* ---- 斜杠命令处理(/help 文本模式) ---- */
198
204
  function showMenu(prefix){
199
205
  const p=(prefix||"/").trim();
200
206
  const list=Object.entries(commands()).filter(([c])=>c.startsWith(p));
@@ -216,68 +222,232 @@ function handleCommand(line){
216
222
  else if(cmd==="/cat"){try{console.log(fs.readFileSync(path.resolve(cwd,arg),"utf8").slice(0,8000));}catch(e){console.log(color(`失败: ${e.message}`,C.red));}}
217
223
  else if(cmd==="/run"){if(!arg)console.log("用法: /run <命令>");else console.log(execTool("run_shell",{command:arg}));}
218
224
  else if(cmd==="/install"){installProgram(arg)}
219
- else if(cmd==="/exit"){console.log(color("再见",C.cyan));process.exit(0);}
225
+ else if(cmd==="/exit"){term.grabInput(false);term("\n");console.log(color("再见",C.cyan));process.exit(0);}
220
226
  else console.log(color(`未知命令: ${cmd}(输入 / 查看全部)`,C.red));
221
227
  }
222
228
 
223
229
  function logo(){
224
230
  const rows=[" ██████╗ ██╗ ███╗ ███╗","██╔════╝ ██║ ████╗ ████║","██║ ███╗ ██║ ██╔████╔██║","██║ ██║ ██║ ██║╚██╔╝██║","╚██████╔╝ ███████╗ ██║ ╚═╝ ██║"," ╚═════╝ ╚══════╝ ╚═╝ ╚═╝"];
225
- const W=44;console.log(color("".padStart(W),C.cyan));console.log(color("✧ GLM ✧".padStart((W+"✧ GLM ✧".length)/2),C.cyanB));for(const r of rows)console.log(color(r.padStart((W+r.length)/2),C.cyanB));console.log(color("GLM Code · Coding Agent",C.cyanB));console.log(color(`v${VERSION}`,C.gray));console.log(color("/help 查看命令 · /thinking 思考开关",C.gray));console.log("");
231
+ const W=44;console.log(color("".padStart(W),C.cyan));console.log(color("✧ GLM ✧".padStart((W+"✧ GLM ✧".length)/2),C.cyanB));for(const r of rows)console.log(color(r.padStart((W+r.length)/2),C.cyanB));console.log(color("GLM Code · Coding Agent",C.cyanB));console.log(color(`v${VERSION}`,C.gray));console.log(color("输入 / 弹菜单 · ↑↓ 选择 · 回车执行 · Ctrl+C 退出",C.gray));console.log("");
226
232
  }
227
- /* ---- 底部状态栏 ---- */
228
- function toolbar(){return `┌ 思考:${thinkingEnabled?"开":"关"} 模型:${config.model} 技能:${skills.size} 目录:${cwd} ┐`;}
229
- function completer(line){
230
- if(!line.startsWith("/"))return [[],[]];
231
- const list=Object.entries(commands()).filter(([c])=>c.startsWith(line)).map(([c,d])=>`${c} ${d}`);
232
- return [list.length?list:[line], list.map(v=>v.split(" — ")[0])];
233
+
234
+ /* =============== TUI:悬浮菜单 + 上下键 + 实时过滤 + 底部状态栏 =============== */
235
+ const PROMPT="┃ 输入( / 菜单 · ↑↓选择 · Enter执行) ┃ ";
236
+ const TUI={input:"",cursor:0,items:[],sel:0,scroll:0,history:[],histIdx:-1};
237
+ function statusText(){return ` 思考:${thinkingEnabled?"开":"关"} 模型:${config.model} 技能:${skills.size} 目录:${cwd} `;}
238
+ function filterItems(input){
239
+ const s=(input||"").trim();
240
+ if(!s.startsWith("/"))return [];
241
+ const name=s.slice(1).toLowerCase();
242
+ const list=Object.entries(commands());
243
+ if(!name)return list.map(([c,d])=>`${c} ${d}`);
244
+ return list.filter(([c,d])=>{
245
+ const n=c.slice(1).toLowerCase();
246
+ return n.startsWith(name)||n.includes(name)||d.toLowerCase().includes(name);
247
+ }).map(([c,d])=>`${c} ${d}`);
233
248
  }
234
- function promptLoop(){
235
- const rl=readline.createInterface({input:process.stdin,output:process.stdout,completer});
236
- rl.setPrompt(color("┃ 输入消息( / 弹菜单, Tab 补全) ┃ ",C.cyanB));
237
- function next(){console.log(color(toolbar(),C.gray));rl.prompt();}
238
- next();
239
- rl.on("line",async line=>{
240
- const text=line.trim();if(!text){next();return;}
241
- if(text.startsWith("/")){
242
- const cmds=commands();
243
- if(text!=="/"&&cmds[text])handleCommand(text);
244
- else showMenu(text==="/"?"/":text);
245
- }else{rl.pause();await chat(text);rl.resume();}
246
- next();
247
- });
248
- rl.on("SIGINT",()=>{if(stopping){rl.close();process.exit(0);}stopping=true;console.log(color("\n[再次 Ctrl+C 退出]",C.yellow));setTimeout(()=>stopping=false,1200);rl.prompt();});
249
- rl.on("close",()=>process.exit(0));
249
+ function menuMax(){const h=term.height>0?term.height:24;return Math.max(1,Math.min(10,h-3));}
250
+ function keepSelVisible(){
251
+ const MAX=menuMax();
252
+ if(TUI.sel<TUI.scroll)TUI.scroll=TUI.sel;
253
+ else if(TUI.sel>=TUI.scroll+MAX)TUI.scroll=TUI.sel-MAX+1;
254
+ }
255
+ function render(){
256
+ const h=term.height>0?term.height:24, w=term.width>0?term.width:80;
257
+ const inputRow=h-1, MAX=menuMax();
258
+ term.saveCursor();
259
+ // 底部状态栏(常驻)
260
+ term.moveTo(1,h);term.eraseLine();
261
+ const st=statusText().padEnd(Math.max(1,w-1));
262
+ term.inverse(st.slice(0,w));term.styleReset;
263
+ // 擦除菜单区
264
+ for(let j=0;j<MAX;j++){term.moveTo(1,inputRow-1-j);term.eraseLine();}
265
+ // 悬浮菜单
266
+ if(TUI.items.length){
267
+ const count=Math.min(TUI.items.length,MAX);
268
+ for(let j=0;j<count;j++){
269
+ const idx=TUI.scroll+j;
270
+ const row=inputRow-1-j;
271
+ term.moveTo(1,row);
272
+ const line=" "+TUI.items[idx]+" ";
273
+ if(idx===TUI.sel)term.bgBrightBlue.black(line.slice(0,w));
274
+ else term.gray(line.slice(0,w));
275
+ term.styleReset;
276
+ }
277
+ }
278
+ // 输入行
279
+ term.moveTo(1,inputRow);term.eraseLine();
280
+ term.cyan.bold(PROMPT);
281
+ const before=TUI.input.slice(0,TUI.cursor), after=TUI.input.slice(TUI.cursor);
282
+ term(before);term.cyan(after);term.styleReset;
283
+ // 光标
284
+ term.restoreCursor();
285
+ term.moveTo(Math.min(1+PROMPT.length+TUI.cursor,w),inputRow);
286
+ term.hideCursor(false);
287
+ }
288
+ function refreshFilter(){
289
+ TUI.items=filterItems(TUI.input);
290
+ if(TUI.sel>=TUI.items.length)TUI.sel=TUI.items.length?TUI.items.length-1:0;
291
+ keepSelVisible();
292
+ }
293
+ function tuiInput(){
294
+ return new Promise(resolve=>{
295
+ TUI.input="";TUI.cursor=0;TUI.items=[];TUI.sel=0;TUI.scroll=0;TUI.histIdx=-1;
296
+ term.grabInput(true);
297
+ render();
298
+ const onKey=(name,matches,data)=>{
299
+ if(data&&data.isCharacter&&name.length===1&&name.charCodeAt(0)>=32){
300
+ TUI.input=TUI.input.slice(0,TUI.cursor)+name+TUI.input.slice(TUI.cursor);
301
+ TUI.cursor++;refreshFilter();render();
302
+ }
303
+ else if(name==="UP"){
304
+ if(TUI.items.length){TUI.sel=TUI.sel>0?TUI.sel-1:TUI.items.length-1;keepSelVisible();}
305
+ else if(TUI.histIdx<TUI.history.length-1){TUI.histIdx++;TUI.input=TUI.history[TUI.history.length-1-TUI.histIdx];TUI.cursor=TUI.input.length;refreshFilter();}
306
+ render();
307
+ }
308
+ else if(name==="DOWN"){
309
+ if(TUI.items.length){TUI.sel=(TUI.sel+1)%TUI.items.length;keepSelVisible();}
310
+ else if(TUI.histIdx>0){TUI.histIdx--;TUI.input=TUI.history[TUI.history.length-1-TUI.histIdx];TUI.cursor=TUI.input.length;refreshFilter();}
311
+ else if(TUI.histIdx===0){TUI.histIdx=-1;TUI.input="";TUI.cursor=0;refreshFilter();}
312
+ render();
313
+ }
314
+ else if(name==="TAB"){
315
+ if(TUI.items.length){const chosen=TUI.items[TUI.sel].split(" ")[0];TUI.input=chosen;TUI.cursor=chosen.length;TUI.items=[];}
316
+ render();
317
+ }
318
+ else if(name==="ENTER"){
319
+ const v=TUI.input;
320
+ term.removeListener("key",onKey);
321
+ term.grabInput(false);
322
+ term.hideCursor();
323
+ term("\n");
324
+ if(v.trim())TUI.history.push(v.trim());
325
+ resolve(v);
326
+ }
327
+ else if(name==="BACKSPACE"){
328
+ if(TUI.cursor>0){TUI.input=TUI.input.slice(0,TUI.cursor-1)+TUI.input.slice(TUI.cursor);TUI.cursor--;refreshFilter();}
329
+ render();
330
+ }
331
+ else if(name==="DELETE"){
332
+ if(TUI.cursor<TUI.input.length){TUI.input=TUI.input.slice(0,TUI.cursor)+TUI.input.slice(TUI.cursor+1);refreshFilter();}
333
+ render();
334
+ }
335
+ else if(name==="LEFT"){if(TUI.cursor>0){TUI.cursor--;}render();}
336
+ else if(name==="RIGHT"){if(TUI.cursor<TUI.input.length){TUI.cursor++;}render();}
337
+ else if(name==="HOME"){TUI.cursor=0;render();}
338
+ else if(name==="END"){TUI.cursor=TUI.input.length;render();}
339
+ else if(name==="CTRL_C"){term.removeListener("key",onKey);term.grabInput(false);term.hideCursor(true);term("\n");resolve(null);}
340
+ else if(name==="CTRL_L"){term.clear();term.moveTo(1,1);render();}
341
+ };
342
+ term.on("key",onKey);
343
+ });
344
+ }
345
+ async function runTui(){
346
+ term.hideCursor();
347
+ while(true){
348
+ const input=await tuiInput();
349
+ if(input===null)break;
350
+ const text=input.trim();
351
+ if(!text)continue;
352
+ if(text.startsWith("/"))handleCommand(text);
353
+ else await chat(text);
354
+ }
355
+ term.grabInput(false);
356
+ term("\n"+color("再见",C.cyan)+"\n");
357
+ process.exit(0);
250
358
  }
251
-
252
359
 
253
360
  function detectInstaller(){
254
- if(process.platform==="win32")return "winget";
255
- if(process.platform==="darwin")return "brew";
256
- for(const pm of ["apk","apt-get","apt","dnf","pacman","pkg","termux-pkg"]){
361
+ if(process.platform==="win32")return commandExists("winget")?"winget":null;
362
+ if(process.platform==="darwin")return commandExists("brew")?"brew":null;
363
+ for(const pm of ["apk","apt-get","apt","dnf","pacman","pkg"]){
257
364
  if(spawnSync("sh",["-lc",`command -v ${pm}`],{encoding:"utf8"}).status===0)return pm;
258
365
  }
259
366
  return null;
260
367
  }
368
+ function installCmd(pm,name){
369
+ return {apk:`apk add ${name}`,"apt-get":`apt-get install -y ${name}`,apt:`apt install -y ${name}`,dnf:`dnf install -y ${name}`,pacman:`pacman -S --noconfirm ${name}`,pkg:`pkg install -y ${name}`,brew:`brew install ${name}`,winget:`winget install ${name}`}[pm];
370
+ }
261
371
  function installProgram(name){
262
372
  if(!name){console.log(color("用法: /install <程序>",C.yellow));return;}
263
373
  const pm=detectInstaller();
264
374
  if(!pm){console.log(color("未检测到可用的软件包管理器",C.red));return;}
265
- const q={apk:`apk add ${name}`,"apt-get":`apt-get install -y ${name}`,apt:`apt install -y ${name}`,dnf:`dnf install -y ${name}`,pacman:`pacman -S --noconfirm ${name}`,pkg:`pkg install -y ${name}`,brew:`brew install ${name}`,winget:`winget install ${name}`}[pm];
375
+ const q=installCmd(pm,name);
266
376
  console.log(color(`[安装] ${pm}: ${q}`,C.gray));
267
377
  const r=spawnSync(q,{shell:true,stdio:"inherit",cwd:process.cwd()});
268
378
  console.log(r.status===0?color("安装完成",C.green):color(`安装失败: ${r.status}`,C.red));
269
379
  }
380
+ /* =============== 跨平台自检:glmcode --selftest =============== */
381
+ async function selftest(){
382
+ const t0=Date.now();let pass=0,fail=0;
383
+ const realPlat=process.platform;
384
+ const push=(name,ok,detail)=>{
385
+ if(ok)pass++;else fail++;
386
+ console.log(` ${ok?color("PASS",C.green):color("FAIL",C.red)} ${name} ${color(detail||"",C.gray)}`);
387
+ };
388
+ const setPlat=p=>Object.defineProperty(process,"platform",{value:p,configurable:true});
389
+ const platforms=["linux","darwin","win32","freebsd","openbsd","android","sunos"];
390
+ console.log(color(`\n====== GLMCode 自检 (v${VERSION}) 真实平台=${realPlat} ======`,C.cyanB));
391
+ for(const p of platforms){
392
+ console.log(color(`\n[平台模拟] ${p}`,C.cyanB));
393
+ try{setPlat(p);}catch(e){push(`${p}/设置platform`,false,e.message);continue;}
394
+ try{const s=shellInfo();push(`${p}/shellInfo`,typeof s.exe==="string"&&s.exe.length>0,`exe=${s.exe} name=${s.name}`);}catch(e){push(`${p}/shellInfo`,false,e.message);}
395
+ try{
396
+ const cases=p==="win32"
397
+ ?[{n:"cmd",want:["/d","/s","/c","echo hi"]},{n:"powershell",want:["-NoProfile","-Command","echo hi"]}]
398
+ :[{n:"bash",want:["-lc","echo hi"]},{n:"zsh",want:["-lc","echo hi"]},{n:"fish",want:["-c","echo hi"]},{n:"csh",want:["-c","echo hi"]}];
399
+ let ok=true,det="";
400
+ for(const c of cases){const r=buildShellArgs({name:c.n,exe:c.n},"echo hi");if(JSON.stringify(r.args)!==JSON.stringify(c.want)){ok=false;det+=`${c.n}->${JSON.stringify(r.args)} `;}}
401
+ push(`${p}/buildShellArgs`,ok,ok?`${cases.length} 分支正确`:`MISMATCH ${det}`);
402
+ }catch(e){push(`${p}/buildShellArgs`,false,e.message);}
403
+ try{const c=openUrlCandidates("https://example.com");push(`${p}/openUrlCandidates`,Array.isArray(c)&&c.length>0,`${c.length} 候选: ${c.map(x=>x[0]).join(" → ")}`);}catch(e){push(`${p}/openUrlCandidates`,false,e.message);}
404
+ try{const pm=detectInstaller();const q=pm?installCmd(pm,"vim"):null;push(`${p}/installer探测+命令`,true,`pm=${pm||"(无,符合预期)"} cmd=${q||"-"}`);}catch(e){push(`${p}/installer探测+命令`,false,e.message);}
405
+ try{setPlat(realPlat);}catch{}
406
+ }
407
+ console.log(color("\n[真实平台功能]",C.cyanB));
408
+ try{
409
+ const {file,args:shA}=buildShellArgs(shellInfo(),"echo glm_selftest_ok");
410
+ const r=spawnSync(file,shA,{cwd,encoding:"utf8",timeout:60000});
411
+ const detail=`file=${file} args=${JSON.stringify(shA)} status=${r.status} err=${r.error&&r.error.message} out=${JSON.stringify(String(r.stdout||"")).slice(0,50)}`;
412
+ push("run_shell 真实执行",String(r.stdout||"").includes("glm_selftest_ok"),detail);
413
+ }catch(e){push("run_shell 真实执行",false,e.message);}
414
+ try{const tmp=fs.mkdtempSync(path.join(os.tmpdir(),"glmselftest-"));saveJson(path.join(tmp,"c.json"),{a:1});const v=readJson(path.join(tmp,"c.json"),{});push("配置 save/load",v.a===1,"ok");}catch(e){push("配置 save/load",false,e.message);}
415
+ try{const r1=execTool("write_file",{path:"_glmselftest.tmp",content:"hello"});const r2=execTool("read_file",{path:"_glmselftest.tmp"});try{fs.unlinkSync(path.join(cwd,"_glmselftest.tmp"));}catch{}push("write_file/read_file",r2==="hello",`read->${JSON.stringify(r2)}`);}catch(e){push("write_file/read_file",false,e.message);}
416
+ try{
417
+ const d=path.join(SKILLS_DIR,"demoskill");fs.mkdirSync(d,{recursive:true});
418
+ fs.writeFileSync(path.join(d,"SKILL.md"),"---\nname: demoskill\ndescription: demo\n---\n# demo\nsay hi\n");
419
+ await loadSkills();
420
+ const ok=skills.has("demoskill");
421
+ try{fs.rmSync(d,{recursive:true,force:true});}catch{}
422
+ push("SKILL.md 技能加载",ok,`skills=${skills.size}`);
423
+ }catch(e){push("SKILL.md 技能加载",false,e.message);}
424
+ try{
425
+ const AdmZip=(await import("adm-zip")).default;
426
+ const az=new AdmZip();az.addFile("demo/SKILL.md",Buffer.from("---\nname: demozip\n---\n# z\nhi\n"));const zf=path.join(SKILLS_DIR,"demo.zip");az.writeZip(zf);
427
+ await extractZipSkills();
428
+ const ok=fs.existsSync(path.join(SKILLS_DIR,"demo","SKILL.md"));
429
+ try{fs.rmSync(path.join(SKILLS_DIR,"demo"),{recursive:true,force:true});fs.unlinkSync(zf);}catch{}
430
+ push("zip 技能自动解压+扁平化",ok,"SKILL.md 已到位");
431
+ }catch(e){push("zip 技能自动解压+扁平化",false,e.message);}
432
+ try{const items=filterItems("/mod");const found=items.some(x=>x.startsWith("/model"));push("TUI 命令实时过滤",found,`/mod -> ${items.length} 项`);}catch(e){push("TUI 命令实时过滤",false,e.message);}
433
+ try{const st=statusText();const mx=menuMax();push("TUI 状态栏/菜单高度",st.includes("思考:")&&mx>=1,`len=${st.length} max=${mx}`);}catch(e){push("TUI 状态栏/菜单高度",false,e.message);}
434
+ console.log(color(`\n========== 自检完成: PASS ${pass} / FAIL ${fail} (${Date.now()-t0}ms) ==========`,fail>0?C.red:C.green));
435
+ process.exit(fail>0?1:0);
436
+ }
437
+
270
438
  async function main(){
271
439
  loadConfig();const a=loadAuth();if(!a){console.log(color("未检测到 GLM API Key。",C.red));console.log(`API Key 页面: ${API_KEYS_URL}`);console.log("获得 Key 后运行: glmcode auth set <API_KEY>");process.exit(1);}
272
440
  apiKey=a.key;await ensureDeps();await loadSkills();logo();console.log(color(`API: ${mask(apiKey)} (${a.source})`,C.green));console.log(color(`模型: ${config.model} 技能: ${skills.size}`,C.gray));console.log("");
273
- promptLoop();
441
+ runTui();
274
442
  }
275
443
  const argv=process.argv.slice(2);
444
+ if(argv[0]==="--selftest"||argv[0]==="selftest"){await selftest();}
276
445
  if(argv[0]==="--version"||argv[0]==="-v"){console.log(VERSION);process.exit(0);}
277
446
  if(argv[0]==="--help"||argv[0]==="-h"){console.log(`GLMCode ${VERSION}
278
447
  启动: glmcode
448
+ TUI: 输入 / 或字母实时弹出悬浮命令菜单,↑↓ 键选择,Tab 补全,Enter 执行
449
+ 底部常驻状态栏:思考开关 / 模型 / 技能数 / 当前目录
279
450
  斜杠命令: /help /clear /thinking /model /pwd /cd /ls /cat /run /install /exit
280
- 输入 / 或前缀自动弹出命令菜单,Tab 补全,底部常驻状态栏。
281
451
  技能: 将 SKILL.md 目录或 .zip 包放入 skills/ 自动加载。
282
452
  认证: glmcode auth set <API_KEY> | auth status | auth logout | auth login
283
453
  Shell: glmcode shell | shell list`);process.exit(0);}