@luxkit/cli 1.1.43 → 1.1.45
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 +11 -0
- package/README.md +262 -19
- package/dist/index.js +146 -113
- package/dist/skills/lux/skill.md +13 -3
- package/package.json +2 -2
- package/README_ZH.md +0 -242
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.45
|
|
4
|
+
|
|
5
|
+
- Added global package manager override: `lux set lux_package_manager=pnpm` forces a specific package manager across all projects
|
|
6
|
+
- Supports `auto`, `bun`, `pnpm`, `yarn`, `npm` values — `auto` (default) uses lockfile detection as before
|
|
7
|
+
- Warns when global config conflicts with a project's existing lockfile
|
|
8
|
+
|
|
9
|
+
## 1.1.44
|
|
10
|
+
|
|
11
|
+
- Merged English and Chinese README into a single file with in-page anchor switching
|
|
12
|
+
- Removed standalone `README_ZH.md` — no longer need to maintain two separate documents
|
|
13
|
+
|
|
3
14
|
## 1.1.43
|
|
4
15
|
|
|
5
16
|
- Fixed language switcher links in README pointing to npm 404 — now uses GitHub absolute URLs
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
11
|
[](https://nodejs.org/api/esm.html)
|
|
12
12
|
|
|
13
|
-
**English** | [中文](
|
|
13
|
+
**English** | [中文](#中文)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
@@ -95,22 +95,22 @@ lux fmt list
|
|
|
95
95
|
|
|
96
96
|
### 📖CLI Commands
|
|
97
97
|
|
|
98
|
-
| Command | Description
|
|
99
|
-
| :-------------------------- |
|
|
100
|
-
| `lux fmt <preset>` | Generate lint configs
|
|
101
|
-
| `lux fmt list` | List available lint presets
|
|
102
|
-
| `lux vscode <preset>` | Generate VSCode config (per-project)
|
|
103
|
-
| `lux vscode list` | List available VSCode presets
|
|
104
|
-
| `lux init` | Initialize Skill files to AI Agent
|
|
105
|
-
| `lux init --preset` | Initialize all built-in presets to `~/.lux/preset/`
|
|
106
|
-
| `lux set <key=value> [...]` |
|
|
107
|
-
| `lux unset` | Clear all stored
|
|
108
|
-
| `lux show env` | Display stored
|
|
109
|
-
| `lux vpn cmd` | Copy CMD proxy commands to clipboard
|
|
110
|
-
| `lux vpn pw` | Copy PowerShell proxy commands to clipboard
|
|
111
|
-
| `lux vpn bash` | Copy Bash proxy commands to clipboard
|
|
112
|
-
| `lux update` | Update `@luxkit/cli` to the latest version
|
|
113
|
-
| `lux update --check` | Check for available updates without installing
|
|
98
|
+
| Command | Description |
|
|
99
|
+
| :-------------------------- | :--------------------------------------------------------------------------------- |
|
|
100
|
+
| `lux fmt <preset>` | Generate lint configs |
|
|
101
|
+
| `lux fmt list` | List available lint presets |
|
|
102
|
+
| `lux vscode <preset>` | Generate VSCode config (per-project) |
|
|
103
|
+
| `lux vscode list` | List available VSCode presets |
|
|
104
|
+
| `lux init` | Initialize Skill files to AI Agent |
|
|
105
|
+
| `lux init --preset` | Initialize all built-in presets to `~/.lux/preset/` |
|
|
106
|
+
| `lux set <key=value> [...]` | Set config values (proxy, global lux package management`lux_package_manager=pnpm`) |
|
|
107
|
+
| `lux unset` | Clear all stored configuration |
|
|
108
|
+
| `lux show env` | Display stored configuration |
|
|
109
|
+
| `lux vpn cmd` | Copy CMD proxy commands to clipboard |
|
|
110
|
+
| `lux vpn pw` | Copy PowerShell proxy commands to clipboard |
|
|
111
|
+
| `lux vpn bash` | Copy Bash proxy commands to clipboard |
|
|
112
|
+
| `lux update` | Update `@luxkit/cli` to the latest version |
|
|
113
|
+
| `lux update --check` | Check for available updates without installing |
|
|
114
114
|
|
|
115
115
|
<br />
|
|
116
116
|
|
|
@@ -216,7 +216,7 @@ npm uninstall -g @luxkit/cli
|
|
|
216
216
|
| :----------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
217
217
|
| `package.json` errors | Ensure a valid `package.json` exists in the project root |
|
|
218
218
|
| Preset not found | Run `lux fmt list` to see all available presets — lux auto-suggests via fuzzy matching |
|
|
219
|
-
| Wrong package manager detected | Ensure the lockfile exists (`bun.lock` / `package-lock.json` / `pnpm-lock.yaml`)
|
|
219
|
+
| Wrong package manager detected | Ensure the lockfile exists (`bun.lock` / `package-lock.json` / `pnpm-lock.yaml`), or set globally: `lux set lux_package_manager=pnpm` |
|
|
220
220
|
| Skip dependency install | Use `--no-install` to only write to `package.json`, install manually |
|
|
221
221
|
| Preview before applying | Use `--dry-run` to see all operations without writing |
|
|
222
222
|
| Flags have no effect | Custom presets must include the corresponding config files and deps for `--stylelint`/`--cspell`/`--editorconfig`/`--husky`/`--lint-staged` to work |
|
|
@@ -237,6 +237,249 @@ Bug reports, feature suggestions, and code contributions are welcome!
|
|
|
237
237
|
|
|
238
238
|
[ISC](https://opensource.org/licenses/ISC) — Free to use, modify, and distribute.
|
|
239
239
|
|
|
240
|
+
<p align="right"><a href="#中文">切换到中文 →</a></p>
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
<br />
|
|
245
|
+
|
|
246
|
+
## 中文
|
|
247
|
+
|
|
248
|
+
<div align="center">
|
|
249
|
+
|
|
250
|
+
**一键项目格式化 & VSCode 配置 CLI**
|
|
251
|
+
|
|
252
|
+
[](https://www.npmjs.com/package/@luxkit/cli)
|
|
253
|
+
[](https://nodejs.org/)
|
|
254
|
+
[](https://opensource.org/licenses/ISC)
|
|
255
|
+
[](https://www.typescriptlang.org/)
|
|
256
|
+
[](https://nodejs.org/api/esm.html)
|
|
257
|
+
|
|
258
|
+
[English](#lux) | **中文**
|
|
259
|
+
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### 📌 为什么选择 lux?
|
|
265
|
+
|
|
266
|
+
`lux` 是一款专为现代化开发与 **AI 时代** 打造的工程化配置 CLI 工具,一条命令完成项目代码规范配置。
|
|
267
|
+
|
|
268
|
+
- 🚀 **一键配置**:ESLint、Prettier、CSpell、Stylelint、EditorConfig 及 VSCode 工作区配置,一条命令搞定。
|
|
269
|
+
- 🤖 **AI Agent 拍档**:为 Claude、Opencode 等提供技能(Skill)支持,可直接用自然语言(如*"/lux 帮我配一套适合团队的 react 代码规范"*)让 AI 自动构建和调整预设。
|
|
270
|
+
- 📦 **框架预设开箱即用**:内置 `web-vue`、`web-react`、`node` 等场景预设。
|
|
271
|
+
- 🎨 **高度可定制**:支持提取和微调内置预设,也支持**完全自定义私有预设**,兼顾开箱即用与团队定制化需求。
|
|
272
|
+
- 🧠 **安全接入已有项目**:智能冲突解决与合并机制,自动检测 `bun`、`pnpm`、`npm`、`yarn` 依赖树。
|
|
273
|
+
|
|
274
|
+
<div align="center">
|
|
275
|
+
<img src="https://github.com/TTT1231/lux/blob/main/demo.gif?raw=true" alt="lux 演示" width="640" />
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
## ⚡快速开始
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# 全局安装(选择你的包管理器)
|
|
282
|
+
npm install -g @luxkit/cli
|
|
283
|
+
|
|
284
|
+
# 初始化 skill 和 preset
|
|
285
|
+
lux init && lux init --preset
|
|
286
|
+
|
|
287
|
+
# 初始化你的项目 — lux 需要项目中有 package.json 来注入依赖和脚本
|
|
288
|
+
# 可使用 pnpm create vite、claude 等方式创建项目
|
|
289
|
+
# lux 通过锁文件检测包管理器(bun.lock / package-lock.json / pnpm-lock.yaml)
|
|
290
|
+
# 也可使用 --no-install 跳过依赖安装
|
|
291
|
+
|
|
292
|
+
# Lint 配置
|
|
293
|
+
lux fmt web-vue # 配置 ESLint + Prettier
|
|
294
|
+
lux fmt web-vue --stylelint # + Stylelint
|
|
295
|
+
lux fmt web-vue --editorconfig # + EditorConfig
|
|
296
|
+
lux fmt web-vue --cspell # + CSpell
|
|
297
|
+
lux fmt web-vue --lint-staged # + lint-staged(自动启用 --husky)
|
|
298
|
+
lux fmt web-vue --husky # + 仅 husky(不包含 lint-staged)
|
|
299
|
+
|
|
300
|
+
# VSCode 配置(可选,全局已配置可跳过)
|
|
301
|
+
lux vscode web-vue # 生成 .vscode/settings.json + extensions.json
|
|
302
|
+
|
|
303
|
+
# 查看可用预设
|
|
304
|
+
lux fmt list
|
|
305
|
+
lux vscode list
|
|
306
|
+
|
|
307
|
+
# 下一步:
|
|
308
|
+
# 🎨 自定义内置fmt lint预设,例如web-vue(可选)
|
|
309
|
+
# 🧩 自定义你自己的预设,例如fmt <your-lint-preset-name>(可选)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
<br />
|
|
313
|
+
|
|
314
|
+
## 🎨 自定义内置预设
|
|
315
|
+
|
|
316
|
+
> 前置条件:已完成 `lux init && lux init --preset`(见快速开始)
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# 使用 AI Agent 自定义(推荐)
|
|
320
|
+
# 在 Claude 等 AI Agent 中直接执行:
|
|
321
|
+
/lux 配置内置预设 web-react 模板,符合我的开发项目风格
|
|
322
|
+
|
|
323
|
+
# 也可手动编辑 ~/.lux/preset/ 下的预设文件
|
|
324
|
+
# 例如给 web-react 添加 cspell 脚本:
|
|
325
|
+
# 在 ~/.lux/preset/fmt/web-react/package.json 的 scripts 中添加 "cspell":"cspell \"src/**/*\""
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## 🧩 完全自定义预设
|
|
329
|
+
|
|
330
|
+
> 前置条件:已完成 `lux init && lux init --preset`(见快速开始)
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
# 使用 AI Agent 创建自定义预设(推荐)
|
|
334
|
+
# 在 Claude 等 AI Agent 中直接执行:
|
|
335
|
+
/lux 配置我的格式化模板 <your-custom-fmt-preset-name>,符合我的开发项目风格
|
|
336
|
+
|
|
337
|
+
# 验证预设是否注册成功
|
|
338
|
+
lux fmt list
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## 📖命令参考
|
|
342
|
+
|
|
343
|
+
| 命令 | 说明 |
|
|
344
|
+
| :-------------------------- | :---------------------------------------------------------- |
|
|
345
|
+
| `lux fmt <preset>` | 生成 Lint 配置 |
|
|
346
|
+
| `lux fmt list` | 列出可用的 Lint 预设 |
|
|
347
|
+
| `lux vscode <preset>` | 生成 VSCode 配置(项目内) |
|
|
348
|
+
| `lux vscode list` | 列出可用的 VSCode 预设 |
|
|
349
|
+
| `lux init` | 初始化 Skill 文件到 AI Agent |
|
|
350
|
+
| `lux init --preset` | 初始化所有内置预设到 `~/.lux/preset/` |
|
|
351
|
+
| `lux set <key=value> [...]` | 设置配置值(代理、全局lux包管理`lux_package_manager=pnpm`) |
|
|
352
|
+
| `lux unset` | 清除所有已存储的配置 |
|
|
353
|
+
| `lux show env` | 显示已存储的配置 |
|
|
354
|
+
| `lux vpn cmd` | 复制 CMD 代理命令到剪贴板 |
|
|
355
|
+
| `lux vpn pw` | 复制 PowerShell 代理命令到剪贴板 |
|
|
356
|
+
| `lux vpn bash` | 复制 Bash 代理命令到剪贴板 |
|
|
357
|
+
| `lux update` | 更新 `@luxkit/cli` 到最新版本 |
|
|
358
|
+
| `lux update --check` | 检查可用更新,不执行安装 |
|
|
359
|
+
|
|
240
360
|
<br />
|
|
241
361
|
|
|
242
|
-
|
|
362
|
+
### ⚙️命令选项
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
lux fmt <preset> [options]
|
|
366
|
+
|
|
367
|
+
--force 覆盖已有文件和脚本
|
|
368
|
+
--no-install 写入依赖到 package.json 但跳过安装
|
|
369
|
+
--dry-run 预览模式,不写入任何文件
|
|
370
|
+
--stylelint 包含 Stylelint 配置(按需启用)
|
|
371
|
+
--editorconfig 包含 EditorConfig 配置(按需启用)
|
|
372
|
+
--husky 初始化 husky Git hooks(按需启用)
|
|
373
|
+
--lint-staged 配置 lint-staged(自动启用 --husky,按需启用)
|
|
374
|
+
--cspell 包含 CSpell 配置(按需启用)
|
|
375
|
+
--reset 重置本地预设,从内置默认值重新创建
|
|
376
|
+
|
|
377
|
+
lux vscode <preset> [options]
|
|
378
|
+
|
|
379
|
+
--force 覆盖已有的 VSCode 配置文件
|
|
380
|
+
--dry-run 预览模式,不写入任何文件
|
|
381
|
+
--stylelint 包含 Stylelint 设置和扩展(按需启用)
|
|
382
|
+
--reset 重置本地预设,从内置默认值重新创建
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
<br />
|
|
386
|
+
|
|
387
|
+
## 🔧工作原理
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
lux fmt <preset> [options]
|
|
391
|
+
│
|
|
392
|
+
▼
|
|
393
|
+
解析 CLI 参数,校验项目 package.json
|
|
394
|
+
│
|
|
395
|
+
▼
|
|
396
|
+
预设类型判断
|
|
397
|
+
│
|
|
398
|
+
├── 内置预设 ──► --reset? ──► 重置本地副本
|
|
399
|
+
│ │
|
|
400
|
+
│ ├── 本地副本存在 (~/.lux/preset/)? ──► 从本地副本应用
|
|
401
|
+
│ └── 不存在 ──► 从内置生成 ──► 保存到 ~/.lux/preset/ ──► 应用
|
|
402
|
+
│
|
|
403
|
+
├── 自定义预设 (~/.lux/preset/fmt/<name>/) ──► 直接从本地目录应用
|
|
404
|
+
│
|
|
405
|
+
└── 未找到 ──► 模糊匹配所有可用预设(内置 + 自定义)并报错
|
|
406
|
+
│
|
|
407
|
+
▼
|
|
408
|
+
--stylelint / --editorconfig / --cspell / --husky / --lint-staged 过滤(自定义预设无对应配置时 warning)
|
|
409
|
+
│
|
|
410
|
+
▼
|
|
411
|
+
遍历每个配置文件:
|
|
412
|
+
│
|
|
413
|
+
├── 文件不存在? ──► 创建
|
|
414
|
+
├── 已存在 + --force? ──► 覆盖
|
|
415
|
+
└── 已存在? ──► 跳过
|
|
416
|
+
│
|
|
417
|
+
▼
|
|
418
|
+
注入脚本到 package.json(自动检测 bun / pnpm / npm / yarn)
|
|
419
|
+
│
|
|
420
|
+
▼
|
|
421
|
+
安装 devDependencies(检测 lockfile 判断包管理器)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
<br />
|
|
425
|
+
|
|
426
|
+
## 🔄重置与卸载
|
|
427
|
+
|
|
428
|
+
### 重置预设
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
# 重置内置预设(删除本地副本并从内置模板重新生成)
|
|
432
|
+
lux fmt web-vue --reset
|
|
433
|
+
lux vscode web-vue --reset
|
|
434
|
+
|
|
435
|
+
# 重新初始化所有内置预设(覆盖已有本地副本)
|
|
436
|
+
lux init --preset
|
|
437
|
+
|
|
438
|
+
# 重置自定义预设:手动删除对应目录
|
|
439
|
+
# rm -rf ~/.lux/preset/fmt/<your-custom-preset-name>
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
> 自定义预设使用 `--reset` 会提示警告并中止,因为不存在内置源可供恢复。
|
|
443
|
+
|
|
444
|
+
### 卸载
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
# 卸载 CLI
|
|
448
|
+
npm uninstall -g @luxkit/cli
|
|
449
|
+
|
|
450
|
+
# 清理配置目录(可选)
|
|
451
|
+
# rm -rf ~/.lux
|
|
452
|
+
|
|
453
|
+
# 清理skill,claude/opencode
|
|
454
|
+
# rm -rf ~/.claude/skill/lux
|
|
455
|
+
# rm -rf ~/.opencode/skill/lux
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## 🔍故障排查
|
|
459
|
+
|
|
460
|
+
| 问题 | 解决方案 |
|
|
461
|
+
| :---------------------- | :------------------------------------------------------------------------------------------------------------------------ |
|
|
462
|
+
| `package.json` 相关错误 | 确保项目根目录存在合法的 `package.json` |
|
|
463
|
+
| 预设未找到 | 运行 `lux fmt list` 查看所有可用预设,lux 会自动模糊匹配建议 |
|
|
464
|
+
| 包管理器检测不正确 | 确保 lockfile 存在(`bun.lock` / `package-lock.json` / `pnpm-lock.yaml`),或全局指定:`lux set lux_package_manager=pnpm` |
|
|
465
|
+
| 跳过依赖安装 | 使用 `--no-install` 仅写入 `package.json`,手动安装 |
|
|
466
|
+
| 预览操作结果 | 使用 `--dry-run` 查看将执行的所有操作 |
|
|
467
|
+
| flag 无效果 | 自定义预设需包含对应的配置文件和依赖,`--stylelint`/`--cspell`/`--editorconfig`/`--husky`/`--lint-staged` 才能生效 |
|
|
468
|
+
|
|
469
|
+
<br />
|
|
470
|
+
|
|
471
|
+
## 🤝 参与贡献
|
|
472
|
+
|
|
473
|
+
欢迎提交 Bug、功能建议或代码贡献!
|
|
474
|
+
|
|
475
|
+
- 🐛 **提交 Issue**:[GitHub Issues](https://github.com/TTT1231/lux/issues)
|
|
476
|
+
- 🛠 **提交 PR**:[GitHub Pull Requests](https://github.com/TTT1231/lux/pulls)
|
|
477
|
+
- ⭐️ **Star 支持**:如果 lux 对你有帮助,欢迎 [Star](https://github.com/TTT1231/lux)!
|
|
478
|
+
|
|
479
|
+
<br />
|
|
480
|
+
|
|
481
|
+
## 📄 许可证
|
|
482
|
+
|
|
483
|
+
[ISC](https://opensource.org/licenses/ISC) — 可自由使用、修改和分发。
|
|
484
|
+
|
|
485
|
+
<p align="right"><a href="#lux">← Switch to English</a></p>
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { program } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/fmt.ts
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import fs4 from "fs";
|
|
8
|
+
import path6 from "path";
|
|
9
9
|
import chalk2 from "chalk";
|
|
10
10
|
|
|
11
11
|
// src/presets/fmt/electron-vue.ts
|
|
@@ -875,7 +875,7 @@ function generateAllFmt(preset, opts) {
|
|
|
875
875
|
}
|
|
876
876
|
|
|
877
877
|
// src/utils/deps.ts
|
|
878
|
-
import
|
|
878
|
+
import path4 from "path";
|
|
879
879
|
import { spawn } from "child_process";
|
|
880
880
|
|
|
881
881
|
// src/utils/execFileNoThrow.ts
|
|
@@ -899,13 +899,76 @@ async function execFileNoThrow(command, args, options) {
|
|
|
899
899
|
}
|
|
900
900
|
}
|
|
901
901
|
|
|
902
|
+
// src/utils/config.ts
|
|
903
|
+
import fs2 from "fs";
|
|
904
|
+
import os from "os";
|
|
905
|
+
import path3 from "path";
|
|
906
|
+
var CONFIG_DIR = ".lux";
|
|
907
|
+
var ENV_FILE = "env.txt";
|
|
908
|
+
function getEnvConfigPath() {
|
|
909
|
+
return path3.join(os.homedir(), CONFIG_DIR, ENV_FILE);
|
|
910
|
+
}
|
|
911
|
+
function getEnvConfig() {
|
|
912
|
+
let content;
|
|
913
|
+
try {
|
|
914
|
+
content = fs2.readFileSync(getEnvConfigPath(), "utf-8");
|
|
915
|
+
} catch {
|
|
916
|
+
return {};
|
|
917
|
+
}
|
|
918
|
+
const result = {};
|
|
919
|
+
for (const line of content.split("\n")) {
|
|
920
|
+
const trimmed = line.trim();
|
|
921
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
922
|
+
const eqIndex = trimmed.indexOf("=");
|
|
923
|
+
if (eqIndex === -1) continue;
|
|
924
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
925
|
+
const raw = trimmed.slice(eqIndex + 1).trim();
|
|
926
|
+
const value = raw.replace(/^["']|["']$/g, "");
|
|
927
|
+
if (key) result[key] = value;
|
|
928
|
+
}
|
|
929
|
+
return result;
|
|
930
|
+
}
|
|
931
|
+
function setEnvConfig(data) {
|
|
932
|
+
const lines = Object.entries(data).filter(([, v]) => v !== "").map(([k, v]) => `${k}="${v}"`);
|
|
933
|
+
writeFile(getEnvConfigPath(), lines.join("\n") + "\n");
|
|
934
|
+
}
|
|
935
|
+
function clearEnvConfig() {
|
|
936
|
+
try {
|
|
937
|
+
fs2.unlinkSync(getEnvConfigPath());
|
|
938
|
+
} catch {
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
902
942
|
// src/utils/deps.ts
|
|
903
|
-
|
|
943
|
+
var PM_LOCKFILE_MAP = {
|
|
944
|
+
bun: ["bun.lockb", "bun.lock"],
|
|
945
|
+
pnpm: ["pnpm-lock.yaml"],
|
|
946
|
+
yarn: ["yarn.lock"],
|
|
947
|
+
npm: ["package-lock.json"]
|
|
948
|
+
};
|
|
949
|
+
function detectFromLockfile(cwd) {
|
|
904
950
|
if (fileExists(`${cwd}/bun.lockb`) || fileExists(`${cwd}/bun.lock`)) return "bun";
|
|
905
951
|
if (fileExists(`${cwd}/pnpm-lock.yaml`)) return "pnpm";
|
|
906
952
|
if (fileExists(`${cwd}/yarn.lock`)) return "yarn";
|
|
907
953
|
return "npm";
|
|
908
954
|
}
|
|
955
|
+
function detectPackageManager(cwd) {
|
|
956
|
+
const env = getEnvConfig();
|
|
957
|
+
const configured = env.lux_package_manager;
|
|
958
|
+
if (configured && configured !== "auto") {
|
|
959
|
+
const pm = configured;
|
|
960
|
+
const lockfiles = PM_LOCKFILE_MAP[pm] ?? [];
|
|
961
|
+
const hasMatch = lockfiles.some((f) => fileExists(`${cwd}/${f}`));
|
|
962
|
+
if (!hasMatch) {
|
|
963
|
+
const detected = detectFromLockfile(cwd);
|
|
964
|
+
if (detected !== "npm") {
|
|
965
|
+
logger.warn(`Global config is ${pm} but detected ${detected} lockfile`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return pm;
|
|
969
|
+
}
|
|
970
|
+
return detectFromLockfile(cwd);
|
|
971
|
+
}
|
|
909
972
|
function getLockfileName(pm) {
|
|
910
973
|
switch (pm) {
|
|
911
974
|
case "bun":
|
|
@@ -939,7 +1002,7 @@ async function fetchPackageVersion(pkg) {
|
|
|
939
1002
|
return lines[lines.length - 1].trim();
|
|
940
1003
|
}
|
|
941
1004
|
async function addDepsToManifest(packages, cwd) {
|
|
942
|
-
const pkgPath =
|
|
1005
|
+
const pkgPath = path4.join(cwd, "package.json");
|
|
943
1006
|
const pkg = readJson(pkgPath);
|
|
944
1007
|
if (!pkg) {
|
|
945
1008
|
throw new Error("package.json not found");
|
|
@@ -963,7 +1026,7 @@ async function addDepsToManifest(packages, cwd) {
|
|
|
963
1026
|
}
|
|
964
1027
|
async function installDevDeps(packages, cwd, pm) {
|
|
965
1028
|
const manager = pm ?? detectPackageManager(cwd);
|
|
966
|
-
const pkg = readJson(
|
|
1029
|
+
const pkg = readJson(path4.join(cwd, "package.json"));
|
|
967
1030
|
if (!pkg) {
|
|
968
1031
|
throw new Error("package.json not found");
|
|
969
1032
|
}
|
|
@@ -999,9 +1062,9 @@ async function installDevDeps(packages, cwd, pm) {
|
|
|
999
1062
|
}
|
|
1000
1063
|
|
|
1001
1064
|
// src/core/local-preset.ts
|
|
1002
|
-
import
|
|
1003
|
-
import
|
|
1004
|
-
import
|
|
1065
|
+
import fs3 from "fs";
|
|
1066
|
+
import os2 from "os";
|
|
1067
|
+
import path5 from "path";
|
|
1005
1068
|
|
|
1006
1069
|
// src/core/merge-settings.ts
|
|
1007
1070
|
var USER_PRIORITY_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -1082,27 +1145,27 @@ var STYLELINT_EXTENSION = "stylelint.vscode-stylelint";
|
|
|
1082
1145
|
var HUSKY_DEPS = /* @__PURE__ */ new Set(["husky"]);
|
|
1083
1146
|
var LINTSTAGED_DEPS = /* @__PURE__ */ new Set(["lint-staged"]);
|
|
1084
1147
|
function getLuxDir() {
|
|
1085
|
-
return process.env.LUX_HOME ||
|
|
1148
|
+
return process.env.LUX_HOME || path5.join(os2.homedir(), ".lux");
|
|
1086
1149
|
}
|
|
1087
1150
|
function getLocalPresetDir(type, presetName) {
|
|
1088
1151
|
if (!isValidPresetName(presetName)) {
|
|
1089
1152
|
throw new Error(`Invalid preset name: "${presetName}"`);
|
|
1090
1153
|
}
|
|
1091
|
-
return
|
|
1154
|
+
return path5.join(getLuxDir(), "preset", type, presetName);
|
|
1092
1155
|
}
|
|
1093
1156
|
function isValidPresetName(name) {
|
|
1094
1157
|
return name.length > 0 && !name.includes("/") && !name.includes("\\") && !name.includes("..");
|
|
1095
1158
|
}
|
|
1096
1159
|
function listCustomPresets() {
|
|
1097
|
-
const fmtDir =
|
|
1098
|
-
if (!
|
|
1099
|
-
const entries =
|
|
1160
|
+
const fmtDir = path5.join(getLuxDir(), "preset", "fmt");
|
|
1161
|
+
if (!fs3.existsSync(fmtDir)) return [];
|
|
1162
|
+
const entries = fs3.readdirSync(fmtDir, { withFileTypes: true });
|
|
1100
1163
|
const result = [];
|
|
1101
1164
|
for (const entry of entries) {
|
|
1102
1165
|
if (!entry.isDirectory()) continue;
|
|
1103
1166
|
if (!isValidPresetName(entry.name)) continue;
|
|
1104
|
-
const pkgPath =
|
|
1105
|
-
if (
|
|
1167
|
+
const pkgPath = path5.join(fmtDir, entry.name, "package.json");
|
|
1168
|
+
if (fs3.existsSync(pkgPath)) {
|
|
1106
1169
|
result.push(entry.name);
|
|
1107
1170
|
}
|
|
1108
1171
|
}
|
|
@@ -1110,19 +1173,19 @@ function listCustomPresets() {
|
|
|
1110
1173
|
}
|
|
1111
1174
|
function isValidCustomPreset(name) {
|
|
1112
1175
|
if (!isValidPresetName(name)) return false;
|
|
1113
|
-
const presetDir =
|
|
1114
|
-
if (!
|
|
1115
|
-
const pkgPath =
|
|
1116
|
-
return
|
|
1176
|
+
const presetDir = path5.join(getLuxDir(), "preset", "fmt", name);
|
|
1177
|
+
if (!fs3.existsSync(presetDir)) return false;
|
|
1178
|
+
const pkgPath = path5.join(presetDir, "package.json");
|
|
1179
|
+
return fs3.existsSync(pkgPath);
|
|
1117
1180
|
}
|
|
1118
1181
|
function localPresetExists(type, presetName) {
|
|
1119
1182
|
const dir = getLocalPresetDir(type, presetName);
|
|
1120
|
-
return
|
|
1183
|
+
return fs3.existsSync(dir);
|
|
1121
1184
|
}
|
|
1122
1185
|
function resetLocalPreset(type, presetName) {
|
|
1123
1186
|
const dir = getLocalPresetDir(type, presetName);
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1187
|
+
if (fs3.existsSync(dir)) {
|
|
1188
|
+
fs3.rmSync(dir, { recursive: true, force: true });
|
|
1126
1189
|
logger.log(`Reset local preset: ${dir}`);
|
|
1127
1190
|
}
|
|
1128
1191
|
}
|
|
@@ -1137,24 +1200,24 @@ function materializeFmtPreset(presetName, preset, opts) {
|
|
|
1137
1200
|
const content = getContent(preset);
|
|
1138
1201
|
if (content === void 0) continue;
|
|
1139
1202
|
const resolved = opts.lockfile ? content.replace(/<lockfile>/g, opts.lockfile) : content.replace(/<lockfile>\n?/g, "");
|
|
1140
|
-
writeFile(
|
|
1203
|
+
writeFile(path5.join(presetDir, filename), resolved);
|
|
1141
1204
|
}
|
|
1142
1205
|
const templatePkg = buildTemplatePackageJson(preset);
|
|
1143
|
-
writeJson(
|
|
1206
|
+
writeJson(path5.join(presetDir, "package.json"), templatePkg);
|
|
1144
1207
|
logger.log(`Local preset created at ${presetDir}`);
|
|
1145
1208
|
}
|
|
1146
1209
|
function materializeVscodePreset(cwd, presetName) {
|
|
1147
1210
|
const presetDir = getLocalPresetDir("vscode", presetName);
|
|
1148
1211
|
ensureDir(presetDir);
|
|
1149
|
-
const settingsSrc =
|
|
1212
|
+
const settingsSrc = path5.join(cwd, ".vscode", "settings.json");
|
|
1150
1213
|
if (fileExists(settingsSrc)) {
|
|
1151
|
-
const content =
|
|
1152
|
-
writeFile(
|
|
1214
|
+
const content = fs3.readFileSync(settingsSrc, "utf-8");
|
|
1215
|
+
writeFile(path5.join(presetDir, "settings.json"), content);
|
|
1153
1216
|
}
|
|
1154
|
-
const extensionsSrc =
|
|
1217
|
+
const extensionsSrc = path5.join(cwd, ".vscode", "extensions.json");
|
|
1155
1218
|
if (fileExists(extensionsSrc)) {
|
|
1156
|
-
const content =
|
|
1157
|
-
writeFile(
|
|
1219
|
+
const content = fs3.readFileSync(extensionsSrc, "utf-8");
|
|
1220
|
+
writeFile(path5.join(presetDir, "extensions.json"), content);
|
|
1158
1221
|
}
|
|
1159
1222
|
logger.log(`Local preset created at ${presetDir}`);
|
|
1160
1223
|
}
|
|
@@ -1162,9 +1225,9 @@ function materializeVscodePresetFromBuiltin(presetName, preset) {
|
|
|
1162
1225
|
const presetDir = getLocalPresetDir("vscode", presetName);
|
|
1163
1226
|
ensureDir(presetDir);
|
|
1164
1227
|
const settings = preset.settings();
|
|
1165
|
-
writeJson(
|
|
1228
|
+
writeJson(path5.join(presetDir, "settings.json"), settings);
|
|
1166
1229
|
const extensions = preset.extensions();
|
|
1167
|
-
writeJson(
|
|
1230
|
+
writeJson(path5.join(presetDir, "extensions.json"), { recommendations: extensions });
|
|
1168
1231
|
logger.log(`Local preset created at ${presetDir}`);
|
|
1169
1232
|
}
|
|
1170
1233
|
var InvalidPackageJsonError = class extends Error {
|
|
@@ -1183,25 +1246,25 @@ function applyLocalFmtPreset(cwd, presetName, opts) {
|
|
|
1183
1246
|
scriptsSkipped: 0
|
|
1184
1247
|
};
|
|
1185
1248
|
const presetDir = getLocalPresetDir("fmt", presetName);
|
|
1186
|
-
if (!
|
|
1249
|
+
if (!fs3.existsSync(presetDir)) {
|
|
1187
1250
|
logger.warn(`Local preset not found at ${presetDir}`);
|
|
1188
1251
|
return result;
|
|
1189
1252
|
}
|
|
1190
|
-
const projectPkgPath =
|
|
1253
|
+
const projectPkgPath = path5.join(cwd, "package.json");
|
|
1191
1254
|
if (fileExists(projectPkgPath)) {
|
|
1192
1255
|
try {
|
|
1193
|
-
JSON.parse(
|
|
1256
|
+
JSON.parse(fs3.readFileSync(projectPkgPath, "utf-8"));
|
|
1194
1257
|
} catch {
|
|
1195
1258
|
throw new InvalidPackageJsonError(projectPkgPath);
|
|
1196
1259
|
}
|
|
1197
1260
|
}
|
|
1198
|
-
const entries =
|
|
1261
|
+
const entries = fs3.readdirSync(presetDir).filter((name) => name !== "package.json" && fs3.statSync(path5.join(presetDir, name)).isFile());
|
|
1199
1262
|
for (const filename of entries) {
|
|
1200
1263
|
if (opts.noStylelint && STYLELINT_FILES.has(filename)) continue;
|
|
1201
1264
|
if (opts.noEditorconfig && filename === EDITORCONFIG_FILE) continue;
|
|
1202
1265
|
if (opts.noCspell && filename === CSPELL_FILE) continue;
|
|
1203
1266
|
if (opts.noLintStaged && filename === LINTSTAGED_FILE) continue;
|
|
1204
|
-
const destPath =
|
|
1267
|
+
const destPath = path5.join(cwd, filename);
|
|
1205
1268
|
const exists = fileExists(destPath);
|
|
1206
1269
|
if (exists && !opts.force) {
|
|
1207
1270
|
result.skipped.push(filename);
|
|
@@ -1215,14 +1278,14 @@ function applyLocalFmtPreset(cwd, presetName, opts) {
|
|
|
1215
1278
|
logger.log(`[dry-run] Would copy ${filename} from local preset`);
|
|
1216
1279
|
continue;
|
|
1217
1280
|
}
|
|
1218
|
-
const content =
|
|
1281
|
+
const content = fs3.readFileSync(path5.join(presetDir, filename), "utf-8");
|
|
1219
1282
|
writeFile(destPath, content);
|
|
1220
1283
|
(exists ? result.overwritten : result.created).push(filename);
|
|
1221
1284
|
}
|
|
1222
|
-
const templatePkg = readJson(
|
|
1285
|
+
const templatePkg = readJson(path5.join(presetDir, "package.json"));
|
|
1223
1286
|
const projectPkg = readJson(projectPkgPath);
|
|
1224
1287
|
if (templatePkg && projectPkg) {
|
|
1225
|
-
const pm = fileExists(
|
|
1288
|
+
const pm = fileExists(path5.join(cwd, "package.json")) ? detectPackageManager(cwd) : void 0;
|
|
1226
1289
|
const merged = mergeTemplateIntoProject(templatePkg, projectPkg, pm, opts, result);
|
|
1227
1290
|
if (!opts.dryRun) {
|
|
1228
1291
|
writeJson(projectPkgPath, merged);
|
|
@@ -1239,16 +1302,16 @@ function applyLocalVscodePreset(cwd, presetName, opts) {
|
|
|
1239
1302
|
scriptsSkipped: 0
|
|
1240
1303
|
};
|
|
1241
1304
|
const presetDir = getLocalPresetDir("vscode", presetName);
|
|
1242
|
-
if (!
|
|
1305
|
+
if (!fs3.existsSync(presetDir)) {
|
|
1243
1306
|
logger.warn(`Local preset not found at ${presetDir}`);
|
|
1244
1307
|
return result;
|
|
1245
1308
|
}
|
|
1246
|
-
const settingsSrc =
|
|
1309
|
+
const settingsSrc = path5.join(presetDir, "settings.json");
|
|
1247
1310
|
if (fileExists(settingsSrc)) {
|
|
1248
1311
|
const presetSettings = readJson(settingsSrc);
|
|
1249
1312
|
const filteredSettings = opts.noStylelint ? filterStylelintSettings(presetSettings ?? {}) : presetSettings;
|
|
1250
1313
|
if (filteredSettings) {
|
|
1251
|
-
const settingsDest =
|
|
1314
|
+
const settingsDest = path5.join(cwd, ".vscode", "settings.json");
|
|
1252
1315
|
const existingSettings = readJson(settingsDest);
|
|
1253
1316
|
if (existingSettings) {
|
|
1254
1317
|
if (opts.dryRun) {
|
|
@@ -1270,7 +1333,7 @@ function applyLocalVscodePreset(cwd, presetName, opts) {
|
|
|
1270
1333
|
}
|
|
1271
1334
|
}
|
|
1272
1335
|
}
|
|
1273
|
-
const extensionsSrc =
|
|
1336
|
+
const extensionsSrc = path5.join(presetDir, "extensions.json");
|
|
1274
1337
|
if (fileExists(extensionsSrc)) {
|
|
1275
1338
|
const extensionsData = readJson(extensionsSrc);
|
|
1276
1339
|
if (extensionsData) {
|
|
@@ -1284,7 +1347,7 @@ function applyLocalVscodePreset(cwd, presetName, opts) {
|
|
|
1284
1347
|
result.created.push(".vscode/extensions.json");
|
|
1285
1348
|
logger.log("[dry-run] Would create .vscode/extensions.json from local preset");
|
|
1286
1349
|
} else {
|
|
1287
|
-
const extensionsDest =
|
|
1350
|
+
const extensionsDest = path5.join(cwd, ".vscode", "extensions.json");
|
|
1288
1351
|
const existingExtensions = readJson(extensionsDest);
|
|
1289
1352
|
const existingRecommendations = existingExtensions?.recommendations ?? [];
|
|
1290
1353
|
const merged = [.../* @__PURE__ */ new Set([...existingRecommendations, ...presetRecommendations])];
|
|
@@ -1396,11 +1459,11 @@ function filterScripts(scripts, noStylelint, noEditorconfig, noCspell, noLintSta
|
|
|
1396
1459
|
return filtered;
|
|
1397
1460
|
}
|
|
1398
1461
|
function detectPresetCapabilities(presetName) {
|
|
1399
|
-
const presetDir =
|
|
1400
|
-
const entries =
|
|
1462
|
+
const presetDir = path5.join(getLuxDir(), "preset", "fmt", presetName);
|
|
1463
|
+
const entries = fs3.readdirSync(presetDir);
|
|
1401
1464
|
const hasStylelintFile = entries.some((f) => STYLELINT_FILES.has(f));
|
|
1402
1465
|
const pkg = readJson(
|
|
1403
|
-
|
|
1466
|
+
path5.join(presetDir, "package.json")
|
|
1404
1467
|
);
|
|
1405
1468
|
const hasStylelintDep = pkg?.devDependencies ? Object.keys(pkg.devDependencies).some((d) => isNotStylelintDep(d) === false) : false;
|
|
1406
1469
|
const hasEditorconfigFile = entries.includes(EDITORCONFIG_FILE);
|
|
@@ -1468,10 +1531,10 @@ function registerFmtCommand(program2) {
|
|
|
1468
1531
|
return;
|
|
1469
1532
|
}
|
|
1470
1533
|
const cwd = process.cwd();
|
|
1471
|
-
const pkgPath =
|
|
1534
|
+
const pkgPath = path6.join(cwd, "package.json");
|
|
1472
1535
|
if (fileExists(pkgPath)) {
|
|
1473
1536
|
try {
|
|
1474
|
-
JSON.parse(
|
|
1537
|
+
JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
1475
1538
|
} catch {
|
|
1476
1539
|
logger.error(
|
|
1477
1540
|
"package.json exists but is not valid JSON. Fix it first, then re-run this command."
|
|
@@ -1568,9 +1631,9 @@ async function executeLocalPath(cwd, presetName, options) {
|
|
|
1568
1631
|
`Added ${result.scriptsAdded} script${result.scriptsAdded > 1 ? "s" : ""} to package.json${result.scriptsSkipped > 0 ? ` (${result.scriptsSkipped} skipped)` : ""}`
|
|
1569
1632
|
);
|
|
1570
1633
|
}
|
|
1571
|
-
const pm = fileExists(
|
|
1634
|
+
const pm = fileExists(path6.join(cwd, "package.json")) ? detectPackageManager(cwd) : void 0;
|
|
1572
1635
|
if (!pm) return;
|
|
1573
|
-
const templatePkgPath =
|
|
1636
|
+
const templatePkgPath = path6.join(getLocalPresetDir("fmt", presetName), "package.json");
|
|
1574
1637
|
const templatePkg = readJson(templatePkgPath);
|
|
1575
1638
|
if (!templatePkg?.devDependencies) return;
|
|
1576
1639
|
const depsToInstall = filterDeps(
|
|
@@ -1581,7 +1644,7 @@ async function executeLocalPath(cwd, presetName, options) {
|
|
|
1581
1644
|
opts.noHusky,
|
|
1582
1645
|
opts.noLintStaged
|
|
1583
1646
|
);
|
|
1584
|
-
const projectPkgPath =
|
|
1647
|
+
const projectPkgPath = path6.join(cwd, "package.json");
|
|
1585
1648
|
const projectPkg = readJson(projectPkgPath);
|
|
1586
1649
|
if (!projectPkg) return;
|
|
1587
1650
|
const existingDeps = projectPkg.devDependencies ?? {};
|
|
@@ -1633,7 +1696,7 @@ async function executeLocalPath(cwd, presetName, options) {
|
|
|
1633
1696
|
}
|
|
1634
1697
|
}
|
|
1635
1698
|
async function executeBuiltinPath(cwd, presetName, preset, options) {
|
|
1636
|
-
const pm = fileExists(
|
|
1699
|
+
const pm = fileExists(path6.join(cwd, "package.json")) ? detectPackageManager(cwd) : void 0;
|
|
1637
1700
|
const noHusky = options.husky !== true && options.lintStaged !== true;
|
|
1638
1701
|
const noLintStaged = options.lintStaged !== true;
|
|
1639
1702
|
const opts = {
|
|
@@ -1788,7 +1851,7 @@ function summarizeFiles(filenames) {
|
|
|
1788
1851
|
return [...categories].join(", ");
|
|
1789
1852
|
}
|
|
1790
1853
|
async function injectScripts(scripts, opts, pm) {
|
|
1791
|
-
const pkgPath =
|
|
1854
|
+
const pkgPath = path6.join(opts.cwd, "package.json");
|
|
1792
1855
|
const pkg = readJson(pkgPath);
|
|
1793
1856
|
if (!pkg) {
|
|
1794
1857
|
logger.warn("package.json not found, skipping script injection");
|
|
@@ -1823,7 +1886,7 @@ async function injectScripts(scripts, opts, pm) {
|
|
|
1823
1886
|
}
|
|
1824
1887
|
}
|
|
1825
1888
|
async function initHusky(cwd, pm, opts) {
|
|
1826
|
-
const pkgPath =
|
|
1889
|
+
const pkgPath = path6.join(cwd, "package.json");
|
|
1827
1890
|
const pkg = readJson(pkgPath);
|
|
1828
1891
|
if (!pkg) {
|
|
1829
1892
|
logger.warn("package.json not found, skipping husky setup");
|
|
@@ -1833,8 +1896,8 @@ async function initHusky(cwd, pm, opts) {
|
|
|
1833
1896
|
const isYarn = pm === "yarn";
|
|
1834
1897
|
const initScriptName = isYarn ? "postinstall" : "prepare";
|
|
1835
1898
|
const hookCommand = opts.noLintStaged ? `${prefix} lint` : `${prefix} lint-staged`;
|
|
1836
|
-
const huskyDir =
|
|
1837
|
-
const preCommitPath =
|
|
1899
|
+
const huskyDir = path6.join(cwd, ".husky");
|
|
1900
|
+
const preCommitPath = path6.join(huskyDir, "pre-commit");
|
|
1838
1901
|
if (opts.dryRun) {
|
|
1839
1902
|
logger.log(`[dry-run] Would create .husky/pre-commit with: ${hookCommand}`);
|
|
1840
1903
|
logger.log(`[dry-run] Would inject "${initScriptName}": "husky" script`);
|
|
@@ -1847,7 +1910,7 @@ async function initHusky(cwd, pm, opts) {
|
|
|
1847
1910
|
ensureDir(huskyDir);
|
|
1848
1911
|
writeFile(preCommitPath, `${hookCommand}
|
|
1849
1912
|
`);
|
|
1850
|
-
|
|
1913
|
+
fs4.chmodSync(preCommitPath, 493);
|
|
1851
1914
|
}
|
|
1852
1915
|
const scripts = pkg.scripts ?? {};
|
|
1853
1916
|
if (scripts[initScriptName] !== void 0 && !opts.force) {
|
|
@@ -2651,18 +2714,18 @@ var VSCODE_PRESETS = [
|
|
|
2651
2714
|
];
|
|
2652
2715
|
|
|
2653
2716
|
// src/generators/init.ts
|
|
2654
|
-
import
|
|
2655
|
-
import
|
|
2717
|
+
import fs5 from "fs";
|
|
2718
|
+
import path7 from "path";
|
|
2656
2719
|
function resolveSkillsDir() {
|
|
2657
|
-
const entryDir =
|
|
2658
|
-
return
|
|
2720
|
+
const entryDir = path7.dirname(process.argv[1] ?? "");
|
|
2721
|
+
return path7.resolve(entryDir, "skills");
|
|
2659
2722
|
}
|
|
2660
2723
|
function listFilesRecursive(dir, base) {
|
|
2661
|
-
const entries =
|
|
2724
|
+
const entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
2662
2725
|
const files = [];
|
|
2663
2726
|
for (const entry of entries) {
|
|
2664
2727
|
const childBase = `${base}/${entry.name}`;
|
|
2665
|
-
const fullPath =
|
|
2728
|
+
const fullPath = path7.join(dir, entry.name);
|
|
2666
2729
|
if (entry.isDirectory()) {
|
|
2667
2730
|
files.push(...listFilesRecursive(fullPath, childBase));
|
|
2668
2731
|
} else {
|
|
@@ -2673,20 +2736,20 @@ function listFilesRecursive(dir, base) {
|
|
|
2673
2736
|
}
|
|
2674
2737
|
function generateInitSkills(targetBaseDir, cwd) {
|
|
2675
2738
|
const skillsDir = resolveSkillsDir();
|
|
2676
|
-
if (!
|
|
2739
|
+
if (!fs5.existsSync(skillsDir)) {
|
|
2677
2740
|
logger.error(`Bundled skills directory not found: ${skillsDir}`);
|
|
2678
2741
|
logger.error('Please run "lux build" or reinstall lux.');
|
|
2679
2742
|
return { copiedFiles: [], targetDir: targetBaseDir };
|
|
2680
2743
|
}
|
|
2681
|
-
const targetPath =
|
|
2744
|
+
const targetPath = path7.resolve(cwd, targetBaseDir);
|
|
2682
2745
|
try {
|
|
2683
|
-
|
|
2746
|
+
fs5.cpSync(skillsDir, targetPath, { recursive: true, force: true });
|
|
2684
2747
|
} catch (error) {
|
|
2685
2748
|
const message = error instanceof Error ? error.message : String(error);
|
|
2686
2749
|
logger.error(`Failed to copy skills to ${targetPath}: ${message}`);
|
|
2687
2750
|
return { copiedFiles: [], targetDir: targetBaseDir };
|
|
2688
2751
|
}
|
|
2689
|
-
const copiedFiles =
|
|
2752
|
+
const copiedFiles = fs5.existsSync(targetPath) ? listFilesRecursive(targetPath, targetBaseDir) : [];
|
|
2690
2753
|
return { copiedFiles, targetDir: targetBaseDir };
|
|
2691
2754
|
}
|
|
2692
2755
|
|
|
@@ -2746,46 +2809,6 @@ function materializeAllPresets() {
|
|
|
2746
2809
|
logger.success("All presets materialized to ~/.lux/preset/");
|
|
2747
2810
|
}
|
|
2748
2811
|
|
|
2749
|
-
// src/utils/config.ts
|
|
2750
|
-
import fs5 from "fs";
|
|
2751
|
-
import os2 from "os";
|
|
2752
|
-
import path7 from "path";
|
|
2753
|
-
var CONFIG_DIR = ".lux";
|
|
2754
|
-
var ENV_FILE = "env.txt";
|
|
2755
|
-
function getEnvConfigPath() {
|
|
2756
|
-
return path7.join(os2.homedir(), CONFIG_DIR, ENV_FILE);
|
|
2757
|
-
}
|
|
2758
|
-
function getEnvConfig() {
|
|
2759
|
-
let content;
|
|
2760
|
-
try {
|
|
2761
|
-
content = fs5.readFileSync(getEnvConfigPath(), "utf-8");
|
|
2762
|
-
} catch {
|
|
2763
|
-
return {};
|
|
2764
|
-
}
|
|
2765
|
-
const result = {};
|
|
2766
|
-
for (const line of content.split("\n")) {
|
|
2767
|
-
const trimmed = line.trim();
|
|
2768
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2769
|
-
const eqIndex = trimmed.indexOf("=");
|
|
2770
|
-
if (eqIndex === -1) continue;
|
|
2771
|
-
const key = trimmed.slice(0, eqIndex).trim();
|
|
2772
|
-
const raw = trimmed.slice(eqIndex + 1).trim();
|
|
2773
|
-
const value = raw.replace(/^["']|["']$/g, "");
|
|
2774
|
-
if (key) result[key] = value;
|
|
2775
|
-
}
|
|
2776
|
-
return result;
|
|
2777
|
-
}
|
|
2778
|
-
function setEnvConfig(data) {
|
|
2779
|
-
const lines = Object.entries(data).filter(([, v]) => v !== "").map(([k, v]) => `${k}="${v}"`);
|
|
2780
|
-
writeFile(getEnvConfigPath(), lines.join("\n") + "\n");
|
|
2781
|
-
}
|
|
2782
|
-
function clearEnvConfig() {
|
|
2783
|
-
try {
|
|
2784
|
-
fs5.unlinkSync(getEnvConfigPath());
|
|
2785
|
-
} catch {
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
|
-
|
|
2789
2812
|
// src/commands/show.ts
|
|
2790
2813
|
function handleShowEnv() {
|
|
2791
2814
|
const config = getEnvConfig();
|
|
@@ -3041,7 +3064,8 @@ function executeVscodeBuiltinPath(cwd, presetName, preset, options) {
|
|
|
3041
3064
|
|
|
3042
3065
|
// src/commands/vpn.ts
|
|
3043
3066
|
import { spawnSync } from "child_process";
|
|
3044
|
-
var ALLOWED_KEYS = ["https_proxy", "http_proxy", "all_proxy"];
|
|
3067
|
+
var ALLOWED_KEYS = ["https_proxy", "http_proxy", "all_proxy", "lux_package_manager"];
|
|
3068
|
+
var VALID_PM_VALUES = ["auto", "bun", "pnpm", "yarn", "npm"];
|
|
3045
3069
|
function buildCommands(shell, env) {
|
|
3046
3070
|
const entries = Object.entries(env);
|
|
3047
3071
|
if (shell === "cmd") {
|
|
@@ -3099,6 +3123,12 @@ function handleSet(args) {
|
|
|
3099
3123
|
logger.error(`Invalid key: "${key}". Allowed keys: ${ALLOWED_KEYS.join(", ")}`);
|
|
3100
3124
|
return;
|
|
3101
3125
|
}
|
|
3126
|
+
if (key === "lux_package_manager" && !VALID_PM_VALUES.includes(value)) {
|
|
3127
|
+
logger.error(
|
|
3128
|
+
`Invalid package manager: "${value}". Allowed values: ${VALID_PM_VALUES.join(", ")}`
|
|
3129
|
+
);
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3102
3132
|
merged[key] = value;
|
|
3103
3133
|
}
|
|
3104
3134
|
setEnvConfig(merged);
|
|
@@ -3123,6 +3153,9 @@ registerVscodeCommand(program);
|
|
|
3123
3153
|
registerVpnCommand(program);
|
|
3124
3154
|
registerShowCommand(program);
|
|
3125
3155
|
registerUpdateCommand(program);
|
|
3126
|
-
program.command("set").description("Set
|
|
3127
|
-
|
|
3156
|
+
program.command("set").description("Set config values using key=value pairs (proxy, lux_package_manager)").argument(
|
|
3157
|
+
"[args...]",
|
|
3158
|
+
"key=value pairs (e.g. https_proxy=http://127.0.0.1:7890, lux_package_manager=pnpm)"
|
|
3159
|
+
).action((args) => handleSet(args));
|
|
3160
|
+
program.command("unset").description("Clear stored configuration").action(() => handleUnset());
|
|
3128
3161
|
program.parse();
|
package/dist/skills/lux/skill.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lux
|
|
3
|
-
description: Use when
|
|
3
|
+
description: Use it only when users want to configure lint, ESLint, Prettier,Husky etc, and VSCode with lux.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## fmt — generate lint/format configs
|
|
@@ -73,10 +73,20 @@ lux vpn bash # copy Bash proxy commands
|
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
75
|
lux set https_proxy=http://127.0.0.1:7890
|
|
76
|
-
lux unset # clear all
|
|
77
|
-
lux show env # show stored
|
|
76
|
+
lux unset # clear all config
|
|
77
|
+
lux show env # show stored config
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
## package manager — global override
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
lux set lux_package_manager=pnpm # force pnpm globally (auto, bun, pnpm, yarn, npm)
|
|
84
|
+
lux set lux_package_manager=auto # revert to lockfile detection
|
|
85
|
+
lux unset # clear all config (reverts to auto)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
By default lux detects the package manager from lockfile (`bun.lock`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`). Setting `lux_package_manager` overrides this globally — lux will use the configured PM for all projects, with a warning if the project's lockfile doesn't match.
|
|
89
|
+
|
|
80
90
|
## update — self-update
|
|
81
91
|
|
|
82
92
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luxkit/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.45",
|
|
4
4
|
"description": "One-click project formatting & VSCode config CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
-
"
|
|
11
|
+
"README.md",
|
|
12
12
|
"CHANGELOG.md"
|
|
13
13
|
],
|
|
14
14
|
"publishConfig": {
|
package/README_ZH.md
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
# lux
|
|
4
|
-
|
|
5
|
-
**一键项目格式化 & VSCode 配置 CLI**
|
|
6
|
-
|
|
7
|
-
[](https://www.npmjs.com/package/@luxkit/cli)
|
|
8
|
-
[](https://nodejs.org/)
|
|
9
|
-
[](https://opensource.org/licenses/ISC)
|
|
10
|
-
[](https://www.typescriptlang.org/)
|
|
11
|
-
[](https://nodejs.org/api/esm.html)
|
|
12
|
-
|
|
13
|
-
[English](https://github.com/TTT1231/lux/blob/main/README.md) | **中文**
|
|
14
|
-
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
### 📌 为什么选择 lux?
|
|
20
|
-
|
|
21
|
-
`lux` 是一款专为现代化开发与 **AI 时代** 打造的工程化配置 CLI 工具,一条命令完成项目代码规范配置。
|
|
22
|
-
|
|
23
|
-
- 🚀 **一键配置**:ESLint、Prettier、CSpell、Stylelint、EditorConfig 及 VSCode 工作区配置,一条命令搞定。
|
|
24
|
-
- 🤖 **AI Agent 拍档**:为 Claude、Opencode 等提供技能(Skill)支持,可直接用自然语言(如*"/lux 帮我配一套适合团队的 react 代码规范"*)让 AI 自动构建和调整预设。
|
|
25
|
-
- 📦 **框架预设开箱即用**:内置 `web-vue`、`web-react`、`node` 等场景预设。
|
|
26
|
-
- 🎨 **高度可定制**:支持提取和微调内置预设,也支持**完全自定义私有预设**,兼顾开箱即用与团队定制化需求。
|
|
27
|
-
- 🧠 **安全接入已有项目**:智能冲突解决与合并机制,自动检测 `bun`、`pnpm`、`npm`、`yarn` 依赖树。
|
|
28
|
-
|
|
29
|
-
<div align="center">
|
|
30
|
-
<img src="https://github.com/TTT1231/lux/blob/main/demo.gif?raw=true" alt="lux 演示" width="640" />
|
|
31
|
-
</div>
|
|
32
|
-
|
|
33
|
-
## ⚡快速开始
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
# 全局安装(选择你的包管理器)
|
|
37
|
-
npm install -g @luxkit/cli
|
|
38
|
-
|
|
39
|
-
# 初始化 skill 和 preset
|
|
40
|
-
lux init && lux init --preset
|
|
41
|
-
|
|
42
|
-
# 初始化你的项目 — lux 需要项目中有 package.json 来注入依赖和脚本
|
|
43
|
-
# 可使用 pnpm create vite、claude 等方式创建项目
|
|
44
|
-
# lux 通过锁文件检测包管理器(bun.lock / package-lock.json / pnpm-lock.yaml)
|
|
45
|
-
# 也可使用 --no-install 跳过依赖安装
|
|
46
|
-
|
|
47
|
-
# Lint 配置
|
|
48
|
-
lux fmt web-vue # 配置 ESLint + Prettier
|
|
49
|
-
lux fmt web-vue --stylelint # + Stylelint
|
|
50
|
-
lux fmt web-vue --editorconfig # + EditorConfig
|
|
51
|
-
lux fmt web-vue --cspell # + CSpell
|
|
52
|
-
lux fmt web-vue --lint-staged # + lint-staged(自动启用 --husky)
|
|
53
|
-
lux fmt web-vue --husky # + 仅 husky(不包含 lint-staged)
|
|
54
|
-
|
|
55
|
-
# VSCode 配置(可选,全局已配置可跳过)
|
|
56
|
-
lux vscode web-vue # 生成 .vscode/settings.json + extensions.json
|
|
57
|
-
|
|
58
|
-
# 查看可用预设
|
|
59
|
-
lux fmt list
|
|
60
|
-
lux vscode list
|
|
61
|
-
|
|
62
|
-
# 下一步:
|
|
63
|
-
# 🎨 自定义内置fmt lint预设,例如web-vue(可选)
|
|
64
|
-
# 🧩 自定义你自己的预设,例如fmt <your-lint-preset-name>(可选)
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
<br />
|
|
68
|
-
|
|
69
|
-
## 🎨 自定义内置预设
|
|
70
|
-
|
|
71
|
-
> 前置条件:已完成 `lux init && lux init --preset`(见快速开始)
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
# 使用 AI Agent 自定义(推荐)
|
|
75
|
-
# 在 Claude 等 AI Agent 中直接执行:
|
|
76
|
-
/lux 配置内置预设 web-react 模板,符合我的开发项目风格
|
|
77
|
-
|
|
78
|
-
# 也可手动编辑 ~/.lux/preset/ 下的预设文件
|
|
79
|
-
# 例如给 web-react 添加 cspell 脚本:
|
|
80
|
-
# 在 ~/.lux/preset/fmt/web-react/package.json 的 scripts 中添加 "cspell":"cspell \"src/**/*\""
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## 🧩 完全自定义预设
|
|
84
|
-
|
|
85
|
-
> 前置条件:已完成 `lux init && lux init --preset`(见快速开始)
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
# 使用 AI Agent 创建自定义预设(推荐)
|
|
89
|
-
# 在 Claude 等 AI Agent 中直接执行:
|
|
90
|
-
/lux 配置我的格式化模板 <your-custom-fmt-preset-name>,符合我的开发项目风格
|
|
91
|
-
|
|
92
|
-
# 验证预设是否注册成功
|
|
93
|
-
lux fmt list
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## 📖命令参考
|
|
97
|
-
|
|
98
|
-
| 命令 | 说明 |
|
|
99
|
-
| :-------------------------- | :----------------------------------------------------------- |
|
|
100
|
-
| `lux fmt <preset>` | 生成 Lint 配置 |
|
|
101
|
-
| `lux fmt list` | 列出可用的 Lint 预设 |
|
|
102
|
-
| `lux vscode <preset>` | 生成 VSCode 配置(项目内) |
|
|
103
|
-
| `lux vscode list` | 列出可用的 VSCode 预设 |
|
|
104
|
-
| `lux init` | 初始化 Skill 文件到 AI Agent |
|
|
105
|
-
| `lux init --preset` | 初始化所有内置预设到 `~/.lux/preset/` |
|
|
106
|
-
| `lux set <key=value> [...]` | 设置代理环境变量(如 `https_proxy="http://127.0.0.1:7890"`) |
|
|
107
|
-
| `lux unset` | 清除所有代理配置 |
|
|
108
|
-
| `lux show env` | 显示已配置的代理环境变量 |
|
|
109
|
-
| `lux vpn cmd` | 复制 CMD 代理命令到剪贴板 |
|
|
110
|
-
| `lux vpn pw` | 复制 PowerShell 代理命令到剪贴板 |
|
|
111
|
-
| `lux vpn bash` | 复制 Bash 代理命令到剪贴板 |
|
|
112
|
-
| `lux update` | 更新 `@luxkit/cli` 到最新版本 |
|
|
113
|
-
| `lux update --check` | 检查可用更新,不执行安装 |
|
|
114
|
-
|
|
115
|
-
<br />
|
|
116
|
-
|
|
117
|
-
### ⚙️命令选项
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
lux fmt <preset> [options]
|
|
121
|
-
|
|
122
|
-
--force 覆盖已有文件和脚本
|
|
123
|
-
--no-install 写入依赖到 package.json 但跳过安装
|
|
124
|
-
--dry-run 预览模式,不写入任何文件
|
|
125
|
-
--stylelint 包含 Stylelint 配置(按需启用)
|
|
126
|
-
--editorconfig 包含 EditorConfig 配置(按需启用)
|
|
127
|
-
--husky 初始化 husky Git hooks(按需启用)
|
|
128
|
-
--lint-staged 配置 lint-staged(自动启用 --husky,按需启用)
|
|
129
|
-
--cspell 包含 CSpell 配置(按需启用)
|
|
130
|
-
--reset 重置本地预设,从内置默认值重新创建
|
|
131
|
-
|
|
132
|
-
lux vscode <preset> [options]
|
|
133
|
-
|
|
134
|
-
--force 覆盖已有的 VSCode 配置文件
|
|
135
|
-
--dry-run 预览模式,不写入任何文件
|
|
136
|
-
--stylelint 包含 Stylelint 设置和扩展(按需启用)
|
|
137
|
-
--reset 重置本地预设,从内置默认值重新创建
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
<br />
|
|
141
|
-
|
|
142
|
-
## 🔧工作原理
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
lux fmt <preset> [options]
|
|
146
|
-
│
|
|
147
|
-
▼
|
|
148
|
-
解析 CLI 参数,校验项目 package.json
|
|
149
|
-
│
|
|
150
|
-
▼
|
|
151
|
-
预设类型判断
|
|
152
|
-
│
|
|
153
|
-
├── 内置预设 ──► --reset? ──► 重置本地副本
|
|
154
|
-
│ │
|
|
155
|
-
│ ├── 本地副本存在 (~/.lux/preset/)? ──► 从本地副本应用
|
|
156
|
-
│ └── 不存在 ──► 从内置生成 ──► 保存到 ~/.lux/preset/ ──► 应用
|
|
157
|
-
│
|
|
158
|
-
├── 自定义预设 (~/.lux/preset/fmt/<name>/) ──► 直接从本地目录应用
|
|
159
|
-
│
|
|
160
|
-
└── 未找到 ──► 模糊匹配所有可用预设(内置 + 自定义)并报错
|
|
161
|
-
│
|
|
162
|
-
▼
|
|
163
|
-
--stylelint / --editorconfig / --cspell / --husky / --lint-staged 过滤(自定义预设无对应配置时 warning)
|
|
164
|
-
│
|
|
165
|
-
▼
|
|
166
|
-
遍历每个配置文件:
|
|
167
|
-
│
|
|
168
|
-
├── 文件不存在? ──► 创建
|
|
169
|
-
├── 已存在 + --force? ──► 覆盖
|
|
170
|
-
└── 已存在? ──► 跳过
|
|
171
|
-
│
|
|
172
|
-
▼
|
|
173
|
-
注入脚本到 package.json(自动检测 bun / pnpm / npm / yarn)
|
|
174
|
-
│
|
|
175
|
-
▼
|
|
176
|
-
安装 devDependencies(检测 lockfile 判断包管理器)
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
<br />
|
|
180
|
-
|
|
181
|
-
## 🔄重置与卸载
|
|
182
|
-
|
|
183
|
-
### 重置预设
|
|
184
|
-
|
|
185
|
-
```bash
|
|
186
|
-
# 重置内置预设(删除本地副本并从内置模板重新生成)
|
|
187
|
-
lux fmt web-vue --reset
|
|
188
|
-
lux vscode web-vue --reset
|
|
189
|
-
|
|
190
|
-
# 重新初始化所有内置预设(覆盖已有本地副本)
|
|
191
|
-
lux init --preset
|
|
192
|
-
|
|
193
|
-
# 重置自定义预设:手动删除对应目录
|
|
194
|
-
# rm -rf ~/.lux/preset/fmt/<your-custom-preset-name>
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
> 自定义预设使用 `--reset` 会提示警告并中止,因为不存在内置源可供恢复。
|
|
198
|
-
|
|
199
|
-
### 卸载
|
|
200
|
-
|
|
201
|
-
```bash
|
|
202
|
-
# 卸载 CLI
|
|
203
|
-
npm uninstall -g @luxkit/cli
|
|
204
|
-
|
|
205
|
-
# 清理配置目录(可选)
|
|
206
|
-
# rm -rf ~/.lux
|
|
207
|
-
|
|
208
|
-
# 清理skill,claude/opencode
|
|
209
|
-
# rm rum ~/.claude/skill/lux
|
|
210
|
-
# rm rum ~/.opencode/skill/lux
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
## 🔍故障排查
|
|
214
|
-
|
|
215
|
-
| 问题 | 解决方案 |
|
|
216
|
-
| :---------------------- | :----------------------------------------------------------------------------------------------------------------- |
|
|
217
|
-
| `package.json` 相关错误 | 确保项目根目录存在合法的 `package.json` |
|
|
218
|
-
| 预设未找到 | 运行 `lux fmt list` 查看所有可用预设,lux 会自动模糊匹配建议 |
|
|
219
|
-
| 包管理器检测不正确 | 确保 lockfile 存在(`bun.lock` / `package-lock.json` / `pnpm-lock.yaml`) |
|
|
220
|
-
| 跳过依赖安装 | 使用 `--no-install` 仅写入 `package.json`,手动安装 |
|
|
221
|
-
| 预览操作结果 | 使用 `--dry-run` 查看将执行的所有操作 |
|
|
222
|
-
| flag 无效果 | 自定义预设需包含对应的配置文件和依赖,`--stylelint`/`--cspell`/`--editorconfig`/`--husky`/`--lint-staged` 才能生效 |
|
|
223
|
-
|
|
224
|
-
<br />
|
|
225
|
-
|
|
226
|
-
## 🤝 参与贡献
|
|
227
|
-
|
|
228
|
-
欢迎提交 Bug、功能建议或代码贡献!
|
|
229
|
-
|
|
230
|
-
- 🐛 **提交 Issue**:[GitHub Issues](https://github.com/TTT1231/lux/issues)
|
|
231
|
-
- 🛠 **提交 PR**:[GitHub Pull Requests](https://github.com/TTT1231/lux/pulls)
|
|
232
|
-
- ⭐️ **Star 支持**:如果 lux 对你有帮助,欢迎 [Star](https://github.com/TTT1231/lux)!
|
|
233
|
-
|
|
234
|
-
<br />
|
|
235
|
-
|
|
236
|
-
## 📄 许可证
|
|
237
|
-
|
|
238
|
-
[ISC](https://opensource.org/licenses/ISC) — 可自由使用、修改和分发。
|
|
239
|
-
|
|
240
|
-
<br />
|
|
241
|
-
|
|
242
|
-
<p align="right"><a href="https://github.com/TTT1231/lux/blob/main/README.md">← Switch to English</a></p>
|