@mimocode/cli 3.0.2

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/README.md ADDED
@@ -0,0 +1,373 @@
1
+ # @jwcode/cli — JWCode TypeScript CLI
2
+
3
+ > **JWCode** 是一款 AI 编程助手 CLI 工具,基于 TypeScript + React/Ink 构建,提供交互式终端界面(TUI),与 Java 后端通过 WebSocket 通信,实现 AI 驱动的代码辅助能力。
4
+
5
+ ---
6
+
7
+ ## 📦 安装
8
+
9
+ ### 前置要求
10
+
11
+ | 依赖 | 最低版本 | 说明 |
12
+ |------|---------|------|
13
+ | Node.js | 18+ | 运行时环境 (推荐 20 LTS) |
14
+ | npm | 9+ | 包管理器 |
15
+ | Java | 17+ | 后端服务 (仅 `start` 命令需要) |
16
+
17
+ ### 全局安装
18
+
19
+ ```bash
20
+ npm install -g @jwcode/cli
21
+ ```
22
+
23
+ ### 镜像安装(任选其一)
24
+
25
+ 本包在以下 15 个 npm 组织同步发布,版本号完全一致,可根据偏好任选一个安装:
26
+
27
+ ```bash
28
+ npm install -g @jwcode/cli # jwcode
29
+ npm install -g @zhipucode/cli # zhipucode
30
+ npm install -g @aliclaw/cli # aliclaw
31
+ npm install -g @zhupuclaw/cli # zhupuclaw
32
+ npm install -g @kimicode/cli # kimicode
33
+ npm install -g @minimaxcode/cli # minimaxcode
34
+ npm install -g @alicode/cli # alicode
35
+ npm install -g @huaweiyun/cli # huaweiyun
36
+ npm install -g @tencentclaw/cli # tencentclaw
37
+ npm install -g @deepseekclaw/cli # deepseekclaw
38
+ npm install -g @tencentcode/cli # tencentcode
39
+ npm install -g @deepseekcode/cli # deepseekcode
40
+ npm install -g @deepclaw/cli # deepclaw
41
+ npm install -g @minimaxclaw/cli # minimaxclaw
42
+ npm install -g @hyclaw/cli # hyclaw
43
+ ```
44
+
45
+ `@jwcode/cli` 是规范版本(canonical),所有 15 个包共享同一份代码和 GitHub Release 资源,行为完全相同。
46
+ 安装后可通过对应命令名启动(如 `aliclaw start`、`zhipucode run`)。
47
+
48
+ ### 本地开发安装
49
+
50
+ ```bash
51
+ # 克隆项目
52
+ git clone <repo-url>
53
+ cd ts-cli
54
+
55
+ # 安装依赖
56
+ npm install
57
+
58
+ # 构建
59
+ npm run build
60
+
61
+ # 链接到全局 (可选)
62
+ npm link
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 🚀 快速开始
68
+
69
+ ### 启动完整服务(后端 + TUI)
70
+
71
+ ```bash
72
+ jwcode start
73
+ ```
74
+
75
+ 启动后端 Java 服务并打开交互式终端界面。
76
+
77
+ ### 指定端口和工作目录
78
+
79
+ ```bash
80
+ jwcode start -p 8080 -w /path/to/workspace
81
+ ```
82
+
83
+ ### 仅启动 TUI 客户端(连接到已有后端)
84
+
85
+ ```bash
86
+ jwcode run -b http://localhost:8080
87
+ ```
88
+
89
+ ### 查看版本
90
+
91
+ ```bash
92
+ jwcode version
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 📖 命令参考
98
+
99
+ ### `jwcode start`
100
+
101
+ 启动 Java 后端服务并打开交互式 TUI 界面。
102
+
103
+ | 参数 | 别名 | 类型 | 默认值 | 说明 |
104
+ |------|------|------|--------|------|
105
+ | `--port` | `-p` | number | `17340` | 后端服务端口 |
106
+ | `--backend` | `-B` | boolean | `false` | 仅启动后端,不启动 TUI |
107
+ | `--workspace` | `-w` | string | `cwd` | 工作目录路径 |
108
+
109
+ ### `jwcode run`
110
+
111
+ 仅启动 TUI 客户端,连接到已有的后端服务。
112
+
113
+ | 参数 | 别名 | 类型 | 默认值 | 说明 |
114
+ |------|------|------|--------|------|
115
+ | `--backend-url` | `-b` | string | `http://localhost:17340` | 后端 WebSocket URL |
116
+ | `--ws-url` | `--ws` | string | 派生自 `-b` | 直接指定 WebSocket URL |
117
+
118
+ ### `jwcode version`
119
+
120
+ 显示 CLI 版本号和构建信息。
121
+
122
+ ---
123
+
124
+ ## ⚙️ 配置
125
+
126
+ ### 环境变量
127
+
128
+ | 变量名 | 类型 | 默认值 | 说明 |
129
+ |--------|------|--------|------|
130
+ | `JWCODE_THEME` | `dark` / `light` | `dark` | 界面主题 |
131
+ | `JWCODE_THEME_COLORS` | JSON string | — | 自定义主题颜色(覆盖默认色) |
132
+ | `JWCODE_PORT` | number | `17340` | 默认后端端口 |
133
+ | `DEBUG` | `jwcode:*` | — | 启用调试日志 |
134
+
135
+ ### 配置文件
136
+
137
+ 配置文件位于 `~/.jwcode/config.json`,支持以下配置项:
138
+
139
+ ```json
140
+ {
141
+ "theme": "dark",
142
+ "port": 17340,
143
+ "workspace": "/path/to/default/workspace"
144
+ }
145
+ ```
146
+
147
+ ### 主题自定义
148
+
149
+ 通过 `JWCODE_THEME_COLORS` 环境变量自定义颜色:
150
+
151
+ ```bash
152
+ # 覆盖主题色
153
+ export JWCODE_THEME_COLORS='{"primary":"#00ff00","bg":"#1a1a2e"}'
154
+ ```
155
+
156
+ ---
157
+
158
+ ## 🏗️ 项目架构
159
+
160
+ ```
161
+ ts-cli/
162
+ ├── src/
163
+ │ ├── main.ts # 入口文件
164
+ │ ├── App.tsx # React 根组件 (Ink)
165
+ │ ├── client.ts # WebSocket 客户端
166
+ │ ├── config.ts # 配置管理
167
+ │ ├── launcher.ts # 后端进程启动器
168
+ │ ├── pasteBuffer.ts # 粘贴缓冲区
169
+ │ ├── protocol.ts # WebSocket 消息协议
170
+ │ ├── store.ts # 全局状态管理
171
+ │ ├── theme.ts # 主题系统
172
+ │ ├── commands/ # CLI 命令实现
173
+ │ ├── components/ # React/Ink UI 组件
174
+ │ │ ├── SetupWizard.tsx # 安装向导
175
+ │ │ ├── CommandPalette.tsx # 命令面板
176
+ │ │ ├── StatusLine.tsx # 状态栏
177
+ │ │ ├── TextInput.tsx # 文本输入框
178
+ │ │ ├── ApprovalModal.tsx # 审批弹窗
179
+ │ │ ├── FilePalette.tsx # 文件选择面板
180
+ │ │ └── PlanTaskBoard.tsx # 计划任务面板
181
+ │ ├── hooks/ # React Hooks
182
+ │ │ ├── useMouseWheel.ts
183
+ │ │ ├── useStreamHandlers.ts
184
+ │ │ └── useWebSocket.ts
185
+ │ └── __tests__/ # 单元测试
186
+ ├── backend/ # Java 后端(独立项目)
187
+ ├── .github/workflows/ci.yml # CI 工作流配置
188
+ ├── build.mjs # esbuild 构建脚本
189
+ ├── package.json
190
+ ├── tsconfig.json
191
+ └── proguard.conf # Java 后端混淆配置
192
+ ```
193
+
194
+ ### 架构流程图
195
+
196
+ ```
197
+ ┌─────────────────────────────────────────────┐
198
+ │ 终端用户 (Terminal) │
199
+ └──────────────────┬──────────────────────────┘
200
+ │ stdin/stdout
201
+ ┌──────────────────▼──────────────────────────┐
202
+ │ @jwcode/cli (TypeScript TUI) │
203
+ │ ┌──────────┐ ┌──────────────────────┐ │
204
+ │ │ Commands │ │ React/Ink UI │ │
205
+ │ │ (CLI) │ │ (组件树) │ │
206
+ │ └─────┬────┘ └──────────┬───────────┘ │
207
+ │ │ │ │
208
+ │ ┌─────▼──────────────────▼───────────┐ │
209
+ │ │ JwCodeClient │ │
210
+ │ │ (WebSocket 客户端) │ │
211
+ │ └─────────────────┬──────────────────┘ │
212
+ └─────────────────────┼───────────────────────┘
213
+ │ WebSocket (ws://)
214
+ ┌─────────────────────▼───────────────────────┐
215
+ │ Java 后端服务 │
216
+ │ (WebSocket Server + AI 引擎) │
217
+ └─────────────────────────────────────────────┘
218
+ ```
219
+
220
+ ---
221
+
222
+ ## 🔧 开发指南
223
+
224
+ ### 开发环境搭建
225
+
226
+ ```bash
227
+ # 安装依赖
228
+ npm install
229
+
230
+ # 启动开发模式(自动构建 + 运行)
231
+ npm run go
232
+
233
+ # 或者分步执行
234
+ npm run build # 构建 CLI
235
+ node dist/cli.js run # 运行 CLI
236
+ ```
237
+
238
+ ### 测试
239
+
240
+ ```bash
241
+ # 运行所有测试
242
+ npm test
243
+
244
+ # 监听模式(开发时使用)
245
+ npm run test:watch
246
+
247
+ # 运行指定测试文件
248
+ npx vitest run src/__tests__/store.test.ts
249
+ ```
250
+
251
+ ### 构建
252
+
253
+ ```bash
254
+ # 生产构建
255
+ npm run build
256
+
257
+ # 验证构建产物
258
+ node dist/cli.js version
259
+ ```
260
+
261
+ 构建产物输出到 `dist/cli.js`,使用 **esbuild** 打包为单个 ESM 文件,外部依赖不打包。
262
+
263
+ ### 代码风格
264
+
265
+ - TypeScript: 严格模式 (`strict: true`)
266
+ - JSX: `react-jsx` 运行时
267
+ - 模块: ESM (`"type": "module"`)
268
+ - 缩进: 2 空格
269
+ - 命名规范:
270
+ - 变量/函数: `camelCase`
271
+ - 类/组件: `PascalCase`
272
+ - 文件: `camelCase.ts` / `PascalCase.tsx`
273
+ - 常量: `UPPER_SNAKE_CASE`
274
+
275
+ ---
276
+
277
+ ## 🧪 测试
278
+
279
+ 项目使用 [Vitest](https://vitest.dev/) 作为测试框架。
280
+
281
+ ### 现有测试
282
+
283
+ | 测试文件 | 覆盖模块 | 说明 |
284
+ |---------|---------|------|
285
+ | `store.test.ts` | `store.ts` | 全局状态管理单元测试 |
286
+ | `theme.test.ts` | `theme.ts` | 主题系统单元测试 |
287
+ | `pasteBuffer.test.ts` | `pasteBuffer.ts` | 粘贴缓冲区测试 |
288
+ | `tokenEstimate.test.ts` | 工具函数 | Token 估算测试 |
289
+
290
+ ### 编写新测试
291
+
292
+ ```typescript
293
+ // src/__tests__/example.test.ts
294
+ import { describe, it, expect } from 'vitest';
295
+ import { yourFunction } from '../yourModule.js';
296
+
297
+ describe('yourModule', () => {
298
+ it('should do something', () => {
299
+ expect(yourFunction()).toBe(expected);
300
+ });
301
+ });
302
+ ```
303
+
304
+ ---
305
+
306
+ ## 🚢 CI/CD
307
+
308
+ 项目使用 **GitHub Actions** 进行持续集成。
309
+
310
+ ### CI 工作流 (`.github/workflows/ci.yml`)
311
+
312
+ | 阶段 | 操作 | 说明 |
313
+ |------|------|------|
314
+ | Checkout | `actions/checkout@v4` | 检出代码 |
315
+ | Setup Node | `actions/setup-node@v4` | 配置 Node.js (18/20/22) |
316
+ | Install | `npm ci` | 安装依赖(锁定版本) |
317
+ | Type Check | `npx tsc --noEmit` | TypeScript 类型检查 |
318
+ | Test | `npm test` | 运行单元测试 |
319
+ | Build | `npm run build` | 构建打包 |
320
+ | Verify | 检查 dist/cli.js | 验证构建产物 |
321
+
322
+ ### 触发条件
323
+
324
+ - `push` 到 `main` / `master` 分支
325
+ - `pull_request` 到 `main` / `master` 分支
326
+
327
+ ---
328
+
329
+ ## ❓ 常见问题
330
+
331
+ ### 1. 启动时端口被占用
332
+
333
+ ```bash
334
+ # 指定其他端口
335
+ jwcode start -p 8080
336
+ ```
337
+
338
+ ### 2. WebSocket 连接失败
339
+
340
+ 确保后端服务已启动,检查 URL 是否正确:
341
+
342
+ ```bash
343
+ # 指定后端地址
344
+ jwcode run -b http://localhost:17340
345
+ ```
346
+
347
+ ### 3. 构建后运行报错
348
+
349
+ ```bash
350
+ # 清理后重新构建
351
+ rm -rf dist
352
+ npm run build
353
+ ```
354
+
355
+ ### 4. `EPIPE` / `ECONNRESET` 错误
356
+
357
+ 这是终端断开时的正常行为,不影响程序运行。项目已自动处理这些信号。
358
+
359
+ ---
360
+
361
+ ## 📄 许可
362
+
363
+ 本项目为 **内部项目**,未经授权不得分发或修改。
364
+
365
+ ---
366
+
367
+ ## 🏷️ 版本历史
368
+
369
+ | 版本 | 日期 | 说明 |
370
+ |------|------|------|
371
+ | 3.0.0 | — | 当前版本,TypeScript 重写,React/Ink TUI |
372
+ | 2.x | — | Python 版本 CLI |
373
+ | 1.x | — | 初版 CLI |
File without changes
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createStore } from '../store.js';
3
+ describe('store', () => {
4
+ it('creates store with initial state', () => {
5
+ const store = createStore({ count: 0, name: 'test' });
6
+ expect(store.getState().count).toBe(0);
7
+ expect(store.getState().name).toBe('test');
8
+ });
9
+ it('setState updates state immutably', () => {
10
+ const store = createStore({ count: 0, name: 'test' });
11
+ const prev = store.getState();
12
+ store.setState(s => ({ ...s, count: 5 }));
13
+ expect(store.getState().count).toBe(5);
14
+ expect(prev.count).toBe(0); // original unchanged
15
+ });
16
+ it('subscribe receives updates', () => {
17
+ const store = createStore({ count: 0, name: 'test' });
18
+ let called = false;
19
+ store.subscribe(() => { called = true; });
20
+ store.setState(s => ({ ...s, count: 10 }));
21
+ expect(called).toBe(true);
22
+ expect(store.getState().count).toBe(10);
23
+ });
24
+ it('subscribe returns unsubscribe function', () => {
25
+ const store = createStore({ count: 0, name: 'test' });
26
+ let callCount = 0;
27
+ const unsub = store.subscribe(() => { callCount++; });
28
+ store.setState(s => ({ ...s, count: 1 }));
29
+ expect(callCount).toBe(1);
30
+ unsub();
31
+ store.setState(s => ({ ...s, count: 2 }));
32
+ expect(callCount).toBe(1); // not called again
33
+ });
34
+ it('multiple subscribers all notified', () => {
35
+ const store = createStore({ count: 0, name: 'test' });
36
+ let calls = 0;
37
+ store.subscribe(() => { calls++; });
38
+ store.subscribe(() => { calls++; });
39
+ store.setState(s => ({ ...s, count: 42 }));
40
+ expect(calls).toBe(2);
41
+ expect(store.getState().count).toBe(42);
42
+ });
43
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { getTheme, setTheme, t } from '../theme.js';
3
+ describe('theme', () => {
4
+ afterEach(() => {
5
+ setTheme('dark');
6
+ });
7
+ it('default theme is dark', () => {
8
+ const theme = getTheme();
9
+ expect(theme.primary).toBe('cyan');
10
+ expect(theme.success).toBe('green');
11
+ expect(theme.error).toBe('red');
12
+ });
13
+ it('setTheme changes current theme', () => {
14
+ setTheme('light');
15
+ const theme = getTheme();
16
+ expect(theme.muted).toBe('blackBright');
17
+ });
18
+ it('all required color keys exist', () => {
19
+ const required = [
20
+ 'bg', 'text', 'muted', 'border',
21
+ 'primary', 'success', 'warning', 'error', 'info',
22
+ 'user', 'assistant', 'system', 'tool', 'thinking',
23
+ 'plan', 'auto', 'connected', 'disconnected',
24
+ ];
25
+ const theme = getTheme();
26
+ for (const key of required) {
27
+ expect(theme).toHaveProperty(key);
28
+ }
29
+ });
30
+ it('module-level t exports dark theme initially', () => {
31
+ expect(t.primary).toBe('cyan');
32
+ });
33
+ it('theme switches preserve all keys', () => {
34
+ setTheme('light');
35
+ const light = getTheme();
36
+ setTheme('dark');
37
+ const dark = getTheme();
38
+ expect(Object.keys(light)).toEqual(Object.keys(dark));
39
+ });
40
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ // Replicate the token estimation logic from TextInput.tsx
3
+ function estimateTokens(text) {
4
+ let cjk = 0;
5
+ let other = 0;
6
+ for (const ch of text) {
7
+ if (/[一-鿿㐀-䶿豈-﫿 -〿＀-￯]/.test(ch)) {
8
+ cjk++;
9
+ }
10
+ else {
11
+ other++;
12
+ }
13
+ }
14
+ return Math.ceil(cjk / 1.5 + other / 4);
15
+ }
16
+ describe('tokenEstimate', () => {
17
+ it('empty string is 0', () => {
18
+ expect(estimateTokens('')).toBe(0);
19
+ });
20
+ it('English text: ~4 chars per token', () => {
21
+ // 40 English chars → ~10 tokens
22
+ const tokens = estimateTokens('Hello world this is a test message here');
23
+ expect(tokens).toBeGreaterThan(6);
24
+ expect(tokens).toBeLessThan(20);
25
+ });
26
+ it('Chinese text: ~1.5 chars per token', () => {
27
+ // 15 Chinese chars → ~10 tokens
28
+ const tokens = estimateTokens('这是一段中文测试文字用于验证分词估算');
29
+ expect(tokens).toBeGreaterThan(8);
30
+ expect(tokens).toBeLessThan(15);
31
+ });
32
+ it('mixed text combines both ratios', () => {
33
+ const tokens = estimateTokens('Hello 世界 this 测试 works');
34
+ expect(tokens).toBeGreaterThan(0);
35
+ });
36
+ it('code block is mostly other chars', () => {
37
+ const code = 'function hello() { return 42; }';
38
+ const tokens = estimateTokens(code);
39
+ expect(tokens).toBeGreaterThan(2);
40
+ expect(tokens).toBeLessThan(15);
41
+ });
42
+ it('100K token threshold is detectable', () => {
43
+ // Very rough: 400K chars ≈ 100K tokens
44
+ const long = 'x'.repeat(400_000);
45
+ const tokens = estimateTokens(long);
46
+ expect(tokens).toBeGreaterThanOrEqual(100_000);
47
+ });
48
+ });