skill-base 2.0.16 → 2.0.18

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.
Files changed (109) hide show
  1. package/README.md +8 -2
  2. package/bin/skill-base.js +73 -28
  3. package/package.json +2 -2
  4. package/src/cappy.js +162 -50
  5. package/src/database.js +17 -17
  6. package/src/index.js +75 -22
  7. package/src/middleware/admin.js +3 -3
  8. package/src/middleware/auth.js +22 -22
  9. package/src/middleware/error.js +4 -4
  10. package/src/models/skill.js +6 -6
  11. package/src/models/user.js +10 -10
  12. package/src/models/version.js +6 -6
  13. package/src/routes/auth.js +17 -17
  14. package/src/routes/collaborators.js +28 -28
  15. package/src/routes/init.js +7 -7
  16. package/src/routes/publish.js +15 -15
  17. package/src/routes/skills.js +13 -13
  18. package/src/routes/users.js +9 -9
  19. package/src/utils/crypto.js +6 -6
  20. package/src/utils/detect-language.js +56 -0
  21. package/src/utils/permission.js +7 -7
  22. package/src/utils/zip.js +6 -6
  23. package/static/assets/{index-BHB0vddE.js → index-BVgsNsqr.js} +43 -43
  24. package/static/assets/index-ByONPaqz.css +1 -0
  25. package/static/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  26. package/static/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  27. package/static/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  28. package/static/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  29. package/static/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  30. package/static/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  31. package/static/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
  32. package/static/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
  33. package/static/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  34. package/static/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  35. package/static/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  36. package/static/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  37. package/static/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  38. package/static/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  39. package/static/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
  40. package/static/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
  41. package/static/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  42. package/static/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  43. package/static/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  44. package/static/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  45. package/static/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  46. package/static/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  47. package/static/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
  48. package/static/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
  49. package/static/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  50. package/static/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  51. package/static/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  52. package/static/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  53. package/static/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  54. package/static/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  55. package/static/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
  56. package/static/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
  57. package/static/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  58. package/static/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  59. package/static/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  60. package/static/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  61. package/static/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  62. package/static/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  63. package/static/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  64. package/static/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  65. package/static/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  66. package/static/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  67. package/static/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  68. package/static/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  69. package/static/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  70. package/static/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  71. package/static/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
  72. package/static/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
  73. package/static/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  74. package/static/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  75. package/static/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  76. package/static/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  77. package/static/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  78. package/static/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  79. package/static/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
  80. package/static/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
  81. package/static/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  82. package/static/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  83. package/static/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  84. package/static/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  85. package/static/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  86. package/static/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  87. package/static/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  88. package/static/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  89. package/static/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  90. package/static/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  91. package/static/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  92. package/static/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  93. package/static/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  94. package/static/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  95. package/static/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  96. package/static/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  97. package/static/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  98. package/static/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  99. package/static/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  100. package/static/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  101. package/static/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  102. package/static/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  103. package/static/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  104. package/static/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  105. package/static/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  106. package/static/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  107. package/static/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  108. package/static/index.html +2 -5
  109. package/static/assets/index-EVWfLxoq.css +0 -1
package/README.md CHANGED
@@ -141,7 +141,7 @@ skb login
141
141
  skb search vue # 搜索
142
142
  skb install vue-best-practices # 安装最新版
143
143
  skb install vue-best-practices@v20260115 # 安装指定版本
144
- skb install vue-best-practices -d ./.cursor/rules # 安装到 Cursor 规则目录
144
+ skb install vue-best-practices -d ./.cursor/skills # 安装到 Cursor Skill 目录
145
145
 
146
146
  # 发布团队内部的新 Skill
147
147
  skb publish ./my-business-skill --changelog "新增了报表组件的使用规范"
@@ -155,12 +155,18 @@ skb publish ./my-business-skill --changelog "新增了报表组件的使用规
155
155
  skb install my-team-rules
156
156
 
157
157
  # 或者使用快捷参数,直接注入当前项目的 AI 上下文
158
- skb install team-vue-rules --ide cursor # 自动生成到 .cursor/rules/
158
+ skb install team-vue-rules --ide cursor # 自动生成到 .cursor/skills/
159
159
  skb install team-vue-rules --ide qoder # 自动生成到 .qoder/skills/
160
160
  skb install team-vue-rules --ide copilot # 自动生成到 .github/instructions/
161
161
 
162
162
  # 支持全局安装通用素养规则
163
163
  skb install git-commit-rules --ide cursor --global
164
+
165
+ # 查看本地通过 skb 安装过的所有 Skill,并继续更新/删除/清记录
166
+ skb list
167
+
168
+ # 更新时可先选版本,再批量勾选已安装目录
169
+ skb update team-vue-rules
164
170
  ```
165
171
 
166
172
  ### 🌐 Web 端操作
package/bin/skill-base.js CHANGED
@@ -7,6 +7,72 @@
7
7
 
8
8
  const path = require('path');
9
9
  const fs = require('fs');
10
+ const { detectSystemLanguage } = require('../src/utils/detect-language');
11
+
12
+ const appLanguage = detectSystemLanguage();
13
+
14
+ function pickMessage(message) {
15
+ if (typeof message === 'string') return message;
16
+ if (!message || typeof message !== 'object') return '';
17
+ return message[appLanguage] || message.en || message.zh || '';
18
+ }
19
+
20
+ const helpText = {
21
+ zh: `
22
+ Skill Base - 内网轻量版 Skill 管理平台
23
+
24
+ 用法:
25
+ npx skill-base [options]
26
+
27
+ 选项:
28
+ -p, --port <port> 指定端口号 (默认: 8000)
29
+ -h, --host <host> 指定监听地址 (默认: 0.0.0.0)
30
+ -d, --data-dir <path> 指定数据目录 (默认: 包内 data/)
31
+ --base-path <path> 指定部署前缀 (默认: /,例如: /skills/)
32
+ --cache-max-mb <mb> 指定进程内 LRU 缓存总容量,单位 MB (默认: 50)
33
+ --no-cappy 禁用 Cappy 水豚吉祥物
34
+ -v, --verbose 启用调试信息
35
+ --help 显示帮助信息
36
+ --version 显示版本号
37
+
38
+ 示例:
39
+ npx skill-base # 启动服务 (端口 8000)
40
+ npx skill-base -p 3000 # 使用端口 3000
41
+ npx skill-base --host 127.0.0.1 # 仅本地访问
42
+ npx skill-base -d ./data # 数据存储到当前目录的 data 文件夹
43
+ npx skill-base -d . -p 3000 # 数据存储到当前目录
44
+ npx skill-base --base-path /skills/ # 部署在子路径下
45
+ npx skill-base --cache-max-mb 100 # 将 LRU 缓存上限调整为 100MB
46
+ npx skill-base --no-cappy # 禁用吉祥物
47
+ `,
48
+ en: `
49
+ Skill Base - Lightweight skill management platform
50
+
51
+ Usage:
52
+ npx skill-base [options]
53
+
54
+ Options:
55
+ -p, --port <port> Set the port number (default: 8000)
56
+ -h, --host <host> Set the listen address (default: 0.0.0.0)
57
+ -d, --data-dir <path> Set the data directory (default: bundled data/)
58
+ --base-path <path> Set the deploy prefix (default: /, for example: /skills/)
59
+ --cache-max-mb <mb> Set total in-process LRU cache size in MB (default: 50)
60
+ --no-cappy Disable the Cappy mascot
61
+ -v, --verbose Enable debug logs
62
+ --help Show help
63
+ --version Show version
64
+
65
+ Examples:
66
+ npx skill-base # Start the server on port 8000
67
+ npx skill-base -p 3000 # Use port 3000
68
+ npx skill-base --host 127.0.0.1 # Local access only
69
+ npx skill-base -d ./data # Store data in ./data
70
+ npx skill-base -d . -p 3000 # Store data in the current directory
71
+ npx skill-base --base-path /skills/ # Deploy under a sub path
72
+ npx skill-base --cache-max-mb 100 # Raise LRU cache limit to 100MB
73
+ npx skill-base --no-cappy # Disable the mascot
74
+ `
75
+ };
10
76
 
11
77
  // 解析命令行参数
12
78
  const args = process.argv.slice(2);
@@ -39,33 +105,7 @@ for (let i = 0; i < args.length; i++) {
39
105
  } else if (args[i] === '-v' || args[i] === '--verbose') {
40
106
  debug = true;
41
107
  } else if (args[i] === '--help') {
42
- console.log(`
43
- Skill Base - 内网轻量版 Skill 管理平台
44
-
45
- Usage:
46
- npx skill-base [options]
47
-
48
- Options:
49
- -p, --port <port> 指定端口号 (默认: 8000)
50
- -h, --host <host> 指定监听地址 (默认: 0.0.0.0)
51
- -d, --data-dir <path> 指定数据目录 (默认: 包内 data/)
52
- --base-path <path> 指定部署前缀 (默认: /,例如: /skills/)
53
- --cache-max-mb <mb> 指定进程内 LRU 缓存总容量,单位 MB (默认: 50)
54
- --no-cappy 禁用 Cappy 水豚吉祥物
55
- -v, --verbose 启用调试信息
56
- --help 显示帮助信息
57
- --version 显示版本号
58
-
59
- Examples:
60
- npx skill-base # 启动服务 (端口 8000)
61
- npx skill-base -p 3000 # 使用端口 3000
62
- npx skill-base --host 127.0.0.1 # 仅本地访问
63
- npx skill-base -d ./data # 数据存储到当前目录的 data 文件夹
64
- npx skill-base -d . -p 3000 # 数据存储到当前目录
65
- npx skill-base --base-path /skills/ # 部署在子路径下
66
- npx skill-base --cache-max-mb 100 # 将 LRU 缓存上限调整为 100MB
67
- npx skill-base --no-cappy # 禁用吉祥物
68
- `);
108
+ console.log(pickMessage(helpText));
69
109
  process.exit(0);
70
110
  } else if (args[i] === '--version') {
71
111
  const pkg = require('../package.json');
@@ -90,7 +130,12 @@ if (dataDir) {
90
130
  }
91
131
  process.env.DATA_DIR = dataDir;
92
132
  process.env.DATABASE_PATH = path.join(dataDir, 'skills.db');
93
- console.log(`Data directory: ${dataDir}`);
133
+ console.log(
134
+ pickMessage({
135
+ zh: `数据目录: ${dataDir}`,
136
+ en: `Data directory: ${dataDir}`
137
+ })
138
+ );
94
139
  }
95
140
 
96
141
  // 启动服务
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-base",
3
- "version": "2.0.16",
3
+ "version": "2.0.18",
4
4
  "description": "Skill Base - 私有部署的轻量级 Skill 管理平台",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  "dev": "nodemon src/index.js",
18
18
  "build": "cd web && pnpm build",
19
19
  "web:dev": "cd web && pnpm dev",
20
- "test": "node --test tests/**/*.test.js"
20
+ "test": "node --test tests/**/*.test.js tests/cli/*.test.mjs"
21
21
  },
22
22
  "dependencies": {
23
23
  "@fastify/cookie": "^11.0.2",
package/src/cappy.js CHANGED
@@ -1,9 +1,15 @@
1
1
  const readline = require('readline');
2
+ const { detectSystemLanguage } = require('./utils/detect-language');
2
3
 
3
4
  class CappyMascot {
4
- constructor(port, basePath = '/') {
5
+ static detectSystemLanguage() {
6
+ return detectSystemLanguage();
7
+ }
8
+
9
+ constructor(port, basePath = '/', language = detectSystemLanguage()) {
5
10
  this.port = port;
6
11
  this.basePath = basePath;
12
+ this.lang = language === 'zh' ? 'zh' : 'en';
7
13
  this.frameTimer = null;
8
14
  this.sceneTimer = null;
9
15
  this.lastRenderHeight = 0;
@@ -21,14 +27,93 @@ class CappyMascot {
21
27
  reset: '\x1b[0m'
22
28
  };
23
29
 
30
+ this.text = {
31
+ intro: {
32
+ zh: `Skill Base 正在预热,端口 ${port} 已就绪。`,
33
+ en: `Skill Base is warming up; port ${port} is live.`
34
+ },
35
+ idle: [
36
+ {
37
+ zh: `Cappy 正盯着端口 ${port}。`,
38
+ en: `Cappy is staring at port ${port}.`
39
+ },
40
+ {
41
+ zh: '一切安静。没有过度设计,就没有运行时焦虑。',
42
+ en: 'All quiet. No over-engineering, no runtime anxiety.'
43
+ },
44
+ {
45
+ zh: '技能库很平静。代码直接,系统就稳。',
46
+ en: 'The skill library is calm. Straightforward code brings that peace.'
47
+ },
48
+ {
49
+ zh: '系统稳定。Cappy 讨厌没有意义的复杂度。',
50
+ en: 'System stable. Cappy despises pointless complexity.'
51
+ }
52
+ ],
53
+ blink: [
54
+ {
55
+ zh: '慢慢眨眼。不是摸鱼,是低成本巡逻。',
56
+ en: 'Slow blink. Not slacking—low-cost patrolling.'
57
+ },
58
+ {
59
+ zh: '写简单代码,比堆一堆监控脚本更靠谱。',
60
+ en: 'Better to write simple code than to pile on monitoring scripts.'
61
+ }
62
+ ],
63
+ think: [
64
+ {
65
+ zh: '简单架构才会赢。真正的稳定不靠花哨设计。',
66
+ en: 'Simple architecture wins. Real stability needs no fancy design.'
67
+ },
68
+ {
69
+ zh: '在思考。直接把代码写明白,胜过自作聪明的抽象层。',
70
+ en: 'Thinking. Writing code directly beats clever layers of abstraction.'
71
+ }
72
+ ],
73
+ soak: [
74
+ {
75
+ zh: '数据结构一旦对了,逻辑自然像水一样流动。',
76
+ en: 'Get the data structures right and the logic flows like water.'
77
+ },
78
+ {
79
+ zh: '先泡一会儿。别猜未来需求,YAGNI 就够了。',
80
+ en: 'Let it soak in. Do not guess future needs—YAGNI.'
81
+ }
82
+ ],
83
+ scout: [
84
+ {
85
+ zh: '出去转一圈,确认没人又开始过度设计。',
86
+ en: 'Short walk. Checking no one over-engineered anything.'
87
+ },
88
+ {
89
+ zh: '不要多余动作,只做有意义的移动。代码也该这样。',
90
+ en: 'No extra steps. Only moves that matter. Code should be the same.'
91
+ }
92
+ ],
93
+ work: {
94
+ zh: '收到任务。Cappy 会用最朴素的办法处理。',
95
+ en: 'Task received. Cappy is handling it the plain way.'
96
+ },
97
+ actionFallback: {
98
+ zh: '有新情况发生了,Cappy 继续稳住。',
99
+ en: 'Something new happened; Cappy stays steady.'
100
+ },
101
+ goodbye: {
102
+ zh: 'Cappy 下班了,明天见。',
103
+ en: 'Cappy is off duty. See you tomorrow.'
104
+ },
105
+ footer: {
106
+ zh: 'Cappy 值班中',
107
+ en: 'Cappy on duty'
108
+ }
109
+ };
110
+
24
111
  // Scenes: personality + frames + messages.
25
112
  this.scenes = {
26
113
  intro: {
27
114
  frameDelay: 240,
28
115
  loops: 1,
29
- messages: [
30
- `Skill Base is warming up; port ${port} is live.`
31
- ],
116
+ messages: [this.resolveMessage(this.text.intro)],
32
117
  frames: [
33
118
  { color: 'warm', sprite: this.createSprite('o o', '___', 'paw') },
34
119
  { color: 'orange', sprite: this.createSprite('- -', '___', 'paw') },
@@ -39,12 +124,7 @@ class CappyMascot {
39
124
  weight: 5,
40
125
  frameDelay: 320,
41
126
  loops: 4,
42
- messages: [
43
- `Cappy is staring at port ${port}.`,
44
- 'All quiet. No over-engineering, no runtime anxiety.',
45
- 'The skill library is calm. Straightforward code brings that peace.',
46
- 'System stable. Cappy despises pointless complexity.'
47
- ],
127
+ messages: this.resolveMessages(this.text.idle),
48
128
  frames: [
49
129
  { color: 'warm', sprite: this.createSprite('o o', '___', 'still') },
50
130
  { color: 'warm', sprite: this.createSprite('o o', '___', 'breath') },
@@ -55,10 +135,7 @@ class CappyMascot {
55
135
  weight: 2,
56
136
  frameDelay: 180,
57
137
  loops: 2,
58
- messages: [
59
- 'Slow blink. Not slacking—low-cost patrolling.',
60
- 'Better to write simple code than to pile on monitoring scripts.'
61
- ],
138
+ messages: this.resolveMessages(this.text.blink),
62
139
  frames: [
63
140
  { color: 'warm', sprite: this.createSprite('o o', '___', 'still') },
64
141
  { color: 'warm', sprite: this.createSprite('- -', '___', 'still') },
@@ -69,10 +146,7 @@ class CappyMascot {
69
146
  weight: 1,
70
147
  frameDelay: 280,
71
148
  loops: 3,
72
- messages: [
73
- 'Simple architecture wins. Real stability needs no fancy design.',
74
- 'Thinking. Writing code directly beats clever layers of abstraction.'
75
- ],
149
+ messages: this.resolveMessages(this.text.think),
76
150
  frames: [
77
151
  { color: 'cyan', sprite: this.createSprite('o o', '___', 'think-left') },
78
152
  { color: 'cyan', sprite: this.createSprite('^ ^', '___', 'think-mid') },
@@ -83,10 +157,7 @@ class CappyMascot {
83
157
  weight: 1,
84
158
  frameDelay: 340,
85
159
  loops: 3,
86
- messages: [
87
- 'Get the data structures right and the logic flows like water.',
88
- 'Let it soak in. Do not guess future needs—YAGNI.'
89
- ],
160
+ messages: this.resolveMessages(this.text.soak),
90
161
  frames: [
91
162
  { color: 'pink', sprite: this.createSprite('^ ^', '~~~', 'steam-left') },
92
163
  { color: 'pink', sprite: this.createSprite('- -', '~~~', 'steam-mid') },
@@ -97,10 +168,7 @@ class CappyMascot {
97
168
  weight: 1,
98
169
  frameDelay: 220,
99
170
  loops: 4,
100
- messages: [
101
- 'Short walk. Checking no one over-engineered anything.',
102
- 'No extra steps. Only moves that matter. Code should be the same.'
103
- ],
171
+ messages: this.resolveMessages(this.text.scout),
104
172
  frames: [
105
173
  { color: 'cyan', sprite: this.createSprite('o o', '___', 'step-left') },
106
174
  { color: 'cyan', sprite: this.createSprite('o o', '___', 'step-mid') },
@@ -110,9 +178,7 @@ class CappyMascot {
110
178
  work: {
111
179
  frameDelay: 180,
112
180
  loops: 6,
113
- messages: [
114
- 'Task received. Cappy is handling it the plain way.'
115
- ],
181
+ messages: [this.resolveMessage(this.text.work)],
116
182
  frames: [
117
183
  { color: 'cyan', sprite: this.createSprite('> <', '===', 'spark-left') },
118
184
  { color: 'orange', sprite: this.createSprite('> <', '===', 'spark-right') }
@@ -141,7 +207,7 @@ class CappyMascot {
141
207
  if (!this.isRunning || this.isStopped) return;
142
208
 
143
209
  this.playScene('work', {
144
- message: message || 'Something new happened; Cappy stays steady.',
210
+ message: this.resolveMessage(message || this.text.actionFallback),
145
211
  onDone: () => this.scheduleNextIdle(600)
146
212
  });
147
213
  }
@@ -155,14 +221,15 @@ class CappyMascot {
155
221
  clearTimeout(this.sceneTimer);
156
222
 
157
223
  this.clearRender();
158
- const goodbyeMsg = 'Cappy is off duty. See you tomorrow.';
159
- const gBorder = '─'.repeat(goodbyeMsg.length + 2);
160
- const gSeg = goodbyeMsg.length + 1;
224
+ const goodbyeMsg = this.resolveMessage(this.text.goodbye);
225
+ const goodbyeWidth = this.getDisplayWidth(goodbyeMsg);
226
+ const gBorder = '─'.repeat(goodbyeWidth + 2);
227
+ const gSeg = goodbyeWidth + 1;
161
228
  const gLeft = Math.floor(gSeg / 2);
162
229
  const gRight = gSeg - gLeft;
163
230
  const goodbye = [
164
231
  `${this.colors.soft} ╭${gBorder}╮${this.colors.reset}`,
165
- `${this.colors.soft} │ ${goodbyeMsg} │${this.colors.reset}`,
232
+ `${this.colors.soft} │ ${this.padDisplay(goodbyeMsg, goodbyeWidth)} │${this.colors.reset}`,
166
233
  `${this.colors.soft} ╰${'─'.repeat(gLeft)}┬${'─'.repeat(gRight)}╯${this.colors.reset}`,
167
234
  `${this.colors.warm} \\${this.colors.reset}`,
168
235
  `${this.colors.warm} __${this.colors.reset}`,
@@ -230,7 +297,7 @@ class CappyMascot {
230
297
  const lines = [
231
298
  ...this.buildBubble(message),
232
299
  ...frame.sprite.map((line) => ` ${this.colors[frame.color]}${line}${this.colors.reset}`),
233
- ` ${this.colors.soft}http://localhost:${this.port}${this.basePath} | Cappy on duty${this.colors.reset}`
300
+ ` ${this.colors.soft}http://localhost:${this.port}${this.basePath} | ${this.resolveMessage(this.text.footer)}${this.colors.reset}`
234
301
  ];
235
302
 
236
303
  this.clearRender();
@@ -258,11 +325,11 @@ class CappyMascot {
258
325
  buildBubble(message) {
259
326
  const maxLineWidth = 44;
260
327
  const lines = this.wrapText(String(message), maxLineWidth);
261
- const innerWidth = Math.max(1, ...lines.map((l) => l.length));
328
+ const innerWidth = Math.max(1, ...lines.map((line) => this.getDisplayWidth(line)));
262
329
  const border = '─'.repeat(innerWidth + 2);
263
330
  const body = lines.map(
264
331
  (line) =>
265
- `${this.colors.soft} │ ${line.padEnd(innerWidth)} │${this.colors.reset}`
332
+ `${this.colors.soft} │ ${this.padDisplay(line, innerWidth)} │${this.colors.reset}`
266
333
  );
267
334
 
268
335
  return [
@@ -299,23 +366,23 @@ class CappyMascot {
299
366
  for (const word of words) {
300
367
  if (!word.length) continue;
301
368
 
302
- if (word.length > maxWidth) {
303
- flush();
304
- let rest = word;
305
- while (rest.length > maxWidth) {
306
- out.push(rest.slice(0, maxWidth));
307
- rest = rest.slice(maxWidth);
308
- }
309
- line = rest;
310
- continue;
311
- }
312
-
313
369
  const next = line.length ? `${line} ${word}` : word;
314
- if (next.length <= maxWidth) {
370
+ if (this.getDisplayWidth(next) <= maxWidth) {
315
371
  line = next;
316
372
  } else {
317
373
  flush();
318
- line = word;
374
+ if (this.getDisplayWidth(word) <= maxWidth) {
375
+ line = word;
376
+ continue;
377
+ }
378
+
379
+ let rest = word;
380
+ while (this.getDisplayWidth(rest) > maxWidth) {
381
+ const chunk = this.takeByWidth(rest, maxWidth);
382
+ out.push(chunk);
383
+ rest = rest.slice(chunk.length);
384
+ }
385
+ line = rest;
319
386
  }
320
387
  }
321
388
  flush();
@@ -324,6 +391,51 @@ class CappyMascot {
324
391
  return out.length ? out : [''];
325
392
  }
326
393
 
394
+ resolveMessage(message) {
395
+ if (typeof message === 'string') return message;
396
+ if (!message || typeof message !== 'object') return '';
397
+ return message[this.lang] || message.en || message.zh || '';
398
+ }
399
+
400
+ resolveMessages(messages) {
401
+ return messages.map((message) => this.resolveMessage(message));
402
+ }
403
+
404
+ getDisplayWidth(text) {
405
+ let width = 0;
406
+
407
+ for (const char of text) {
408
+ width += this.getCharWidth(char);
409
+ }
410
+
411
+ return width;
412
+ }
413
+
414
+ getCharWidth(char) {
415
+ return /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff01-\uff60\uffe0-\uffe6]/u.test(char)
416
+ ? 2
417
+ : 1;
418
+ }
419
+
420
+ takeByWidth(text, maxWidth) {
421
+ let width = 0;
422
+ let index = 0;
423
+
424
+ for (const char of text) {
425
+ const charWidth = this.getCharWidth(char);
426
+ if (index > 0 && width + charWidth > maxWidth) break;
427
+ width += charWidth;
428
+ index += char.length;
429
+ }
430
+
431
+ return text.slice(0, index || 1);
432
+ }
433
+
434
+ padDisplay(text, targetWidth) {
435
+ const padding = Math.max(0, targetWidth - this.getDisplayWidth(text));
436
+ return text + ' '.repeat(padding);
437
+ }
438
+
327
439
  pickIdleScene() {
328
440
  const entries = Object.entries(this.scenes)
329
441
  .filter(([key, scene]) => scene.weight)
package/src/database.js CHANGED
@@ -2,21 +2,21 @@ const Database = require('better-sqlite3');
2
2
  const bcrypt = require('bcryptjs');
3
3
  const path = require('path');
4
4
 
5
- // 数据库文件路径,支持环境变量配置
5
+ // Database file path, supports environment variable configuration
6
6
  const dbPath = process.env.DATABASE_PATH || path.join(__dirname, '../data/skills.db');
7
7
 
8
- // 创建数据库连接
8
+ // Create database connection
9
9
  const db = new Database(dbPath);
10
10
 
11
- // 开启 WAL 模式提升并发性能
11
+ // Enable WAL mode for better concurrency performance
12
12
  db.pragma('journal_mode = WAL');
13
13
 
14
- // 开启外键约束
14
+ // Enable foreign key constraints
15
15
  db.pragma('foreign_keys = ON');
16
16
 
17
- // 建表 SQL
17
+ // Table creation SQL
18
18
  const createTablesSql = `
19
- -- 用户表
19
+ -- Users table
20
20
  CREATE TABLE IF NOT EXISTS users (
21
21
  id INTEGER PRIMARY KEY AUTOINCREMENT,
22
22
  username TEXT UNIQUE NOT NULL,
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS users (
26
26
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
27
27
  );
28
28
 
29
- -- CLI 临时验证码表
29
+ -- CLI temporary auth code table
30
30
  CREATE TABLE IF NOT EXISTS cli_auth_codes (
31
31
  code TEXT PRIMARY KEY,
32
32
  user_id INTEGER NOT NULL,
@@ -35,7 +35,7 @@ CREATE TABLE IF NOT EXISTS cli_auth_codes (
35
35
  FOREIGN KEY (user_id) REFERENCES users(id)
36
36
  );
37
37
 
38
- -- 长效访问令牌表 (PAT)
38
+ -- Personal Access Token table (PAT)
39
39
  CREATE TABLE IF NOT EXISTS personal_access_tokens (
40
40
  token TEXT PRIMARY KEY,
41
41
  user_id INTEGER NOT NULL,
@@ -45,7 +45,7 @@ CREATE TABLE IF NOT EXISTS personal_access_tokens (
45
45
  FOREIGN KEY (user_id) REFERENCES users(id)
46
46
  );
47
47
 
48
- -- Skill 主表
48
+ -- Skill main table
49
49
  CREATE TABLE IF NOT EXISTS skills (
50
50
  id TEXT PRIMARY KEY,
51
51
  name TEXT NOT NULL,
@@ -57,7 +57,7 @@ CREATE TABLE IF NOT EXISTS skills (
57
57
  FOREIGN KEY (owner_id) REFERENCES users(id)
58
58
  );
59
59
 
60
- -- 版本表
60
+ -- Version table
61
61
  CREATE TABLE IF NOT EXISTS skill_versions (
62
62
  id INTEGER PRIMARY KEY AUTOINCREMENT,
63
63
  skill_id TEXT NOT NULL,
@@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS skill_versions (
71
71
  UNIQUE(skill_id, version)
72
72
  );
73
73
 
74
- -- Skill 协作者表
74
+ -- Skill collaborators table
75
75
  CREATE TABLE IF NOT EXISTS skill_collaborators (
76
76
  id INTEGER PRIMARY KEY AUTOINCREMENT,
77
77
  skill_id TEXT NOT NULL,
@@ -85,7 +85,7 @@ CREATE TABLE IF NOT EXISTS skill_collaborators (
85
85
  UNIQUE(skill_id, user_id)
86
86
  );
87
87
 
88
- -- Session 表(可选,通过 SESSION_STORE=sqlite 启用)
88
+ -- Session table (optional, enabled via SESSION_STORE=sqlite)
89
89
  CREATE TABLE IF NOT EXISTS sessions (
90
90
  session_id TEXT PRIMARY KEY,
91
91
  user_id INTEGER NOT NULL,
@@ -94,7 +94,7 @@ CREATE TABLE IF NOT EXISTS sessions (
94
94
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
95
95
  );
96
96
 
97
- -- 索引
97
+ -- Indexes
98
98
  CREATE INDEX IF NOT EXISTS idx_versions_skill_id ON skill_versions(skill_id);
99
99
  CREATE INDEX IF NOT EXISTS idx_cli_codes_user ON cli_auth_codes(user_id);
100
100
  CREATE INDEX IF NOT EXISTS idx_pat_tokens_user ON personal_access_tokens(user_id);
@@ -104,12 +104,12 @@ CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
104
104
  CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
105
105
  `;
106
106
 
107
- // 执行建表语句
107
+ // Execute table creation statements
108
108
  db.exec(createTablesSql);
109
109
 
110
- // 安全地添加新字段(如果不存在)
110
+ // Safely add new columns (if not exists)
111
111
  try { db.exec("ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active'"); } catch(e) {}
112
- // SQLite 不支持带 CURRENT_TIMESTAMP 默认值的 ALTER TABLE,需要分两步
112
+ // SQLite does not support ALTER TABLE with CURRENT_TIMESTAMP default, requires two steps
113
113
  try {
114
114
  db.exec("ALTER TABLE users ADD COLUMN updated_at DATETIME");
115
115
  db.exec("UPDATE users SET updated_at = datetime('now') WHERE updated_at IS NULL");
@@ -118,7 +118,7 @@ try { db.exec("ALTER TABLE users ADD COLUMN created_by INTEGER REFERENCES users(
118
118
  try { db.exec("ALTER TABLE users ADD COLUMN name TEXT"); } catch(e) {}
119
119
  try { db.exec("ALTER TABLE skill_versions ADD COLUMN description TEXT"); } catch(e) {}
120
120
 
121
- // 数据迁移:为已有 Skills owner 插入 skill_collaborators 记录
121
+ // Data migration: insert skill_collaborators record for existing Skills owners
122
122
  const existingSkills = db.prepare('SELECT id, owner_id FROM skills').all();
123
123
  const insertCollaborator = db.prepare(`
124
124
  INSERT OR IGNORE INTO skill_collaborators (skill_id, user_id, role, created_by)