ai-engineering-init 1.16.0 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/loki-log-query/SKILL.md +76 -43
- package/.claude/skills/mysql-debug/SKILL.md +45 -23
- package/bin/index.js +287 -265
- package/package.json +1 -1
|
@@ -25,81 +25,114 @@ description: |
|
|
|
25
25
|
|
|
26
26
|
## 多环境配置
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
### 配置文件查找顺序
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
```
|
|
31
|
+
1. 本地项目:.claude/loki-config.json(新格式)
|
|
32
|
+
2. 本地项目:.claude/skills/loki-log-query/environments.json(旧格式,兼容)
|
|
33
|
+
3. 全局配置:~/.claude/loki-config.json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
本地配置优先。全局配置推荐用 `npx ai-engineering-init config --type loki --scope global` 创建。
|
|
37
|
+
|
|
38
|
+
### 配置结构(支持 range 范围匹配)
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"active": "monitor-dev",
|
|
43
|
+
"environments": {
|
|
44
|
+
"monitor-dev": {
|
|
45
|
+
"name": "Monitor 开发环境",
|
|
46
|
+
"url": "https://monitor-dev.xnzn.net/grafana",
|
|
47
|
+
"token": "glsa_xxx",
|
|
48
|
+
"aliases": ["mdev", "dev"],
|
|
49
|
+
"range": "dev1~15",
|
|
50
|
+
"projects": ["dev01","dev02","dev03","dev04","dev05","dev06","dev07","dev08","dev09","dev10","dev11","dev12","dev13","dev14","dev15"]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 环境匹配规则(含 range 范围匹配)
|
|
31
57
|
|
|
32
|
-
|
|
33
|
-
|----------|------|-----|--------|
|
|
34
|
-
| `test13` | 测试13(主测试环境) | `https://test13.xnzn.net/grafana` | test13, 13 |
|
|
35
|
-
| `monitor-test` | Monitor 测试环境 | `https://monitor-test.xnzn.net/grafana` | mtest |
|
|
36
|
-
| `monitor-dev` | Monitor 开发环境 | `https://monitor-dev.xnzn.net/grafana` | mdev, dev |
|
|
37
|
-
| `monitor02-dev` | Monitor02 开发环境 | `https://monitor02-dev.xnzn.net/grafana` | m02, monitor02 |
|
|
38
|
-
| `monitor-tyy-dev` | Monitor 体验园开发环境 | `https://monitor-tyy-dev.xnzn.net/grafana` | tyy, 体验园 |
|
|
58
|
+
用户说的话 → 匹配逻辑:
|
|
39
59
|
|
|
40
|
-
|
|
60
|
+
| 用户说法 | 匹配方式 | 结果 |
|
|
61
|
+
|---------|---------|------|
|
|
62
|
+
| "查 test13 的日志" | 精确匹配 key 或 aliases | `test13` 环境 |
|
|
63
|
+
| "去 dev10 查" | **range 匹配**:dev10 在 monitor-dev 的 projects 中 | `monitor-dev` 环境,`project="dev10"` |
|
|
64
|
+
| "去 monitor-dev 查" | 精确匹配 key | `monitor-dev` 环境 |
|
|
65
|
+
| "切到体验园" | aliases 匹配 | `monitor-tyy-dev` 环境 |
|
|
66
|
+
| 未指定环境 | 使用 `active` 字段 | 默认活跃环境 |
|
|
41
67
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
**range 匹配算法**:
|
|
69
|
+
```
|
|
70
|
+
1. 用户输入 "dev10"
|
|
71
|
+
2. 先精确匹配 key / aliases → 未命中
|
|
72
|
+
3. 遍历所有环境的 projects 列表,检查是否包含 "dev10"
|
|
73
|
+
4. 命中 → 使用该环境,并自动添加 project="dev10" 标签过滤
|
|
74
|
+
```
|
|
48
75
|
|
|
49
|
-
###
|
|
76
|
+
### 读取配置(含全局降级 + range 匹配)
|
|
50
77
|
|
|
51
78
|
```bash
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
echo "
|
|
79
|
+
# 按优先级查找配置文件
|
|
80
|
+
find_config() {
|
|
81
|
+
local PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
82
|
+
for f in \
|
|
83
|
+
"${PROJECT_DIR}/.claude/loki-config.json" \
|
|
84
|
+
"${PROJECT_DIR}/.claude/skills/loki-log-query/environments.json" \
|
|
85
|
+
"${HOME}/.claude/loki-config.json"; do
|
|
86
|
+
[ -f "$f" ] && echo "$f" && return
|
|
87
|
+
done
|
|
88
|
+
echo ""
|
|
62
89
|
}
|
|
63
90
|
|
|
64
|
-
|
|
91
|
+
ENV_FILE=$(find_config)
|
|
92
|
+
|
|
93
|
+
# 通过别名或 range 查找环境 key + project
|
|
65
94
|
find_env() {
|
|
66
95
|
python3 -c "
|
|
67
|
-
import json
|
|
96
|
+
import json, re
|
|
68
97
|
data = json.load(open('${ENV_FILE}'))
|
|
69
98
|
alias = '${1}'.lower()
|
|
99
|
+
# 1. 精确匹配 key 或 aliases
|
|
70
100
|
for key, env in data['environments'].items():
|
|
71
101
|
if alias == key or alias in env.get('aliases', []):
|
|
72
|
-
print(key)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
102
|
+
print(f'{key}|') # key|project(project 为空)
|
|
103
|
+
exit()
|
|
104
|
+
# 2. range 匹配:在 projects 列表中查找
|
|
105
|
+
for key, env in data['environments'].items():
|
|
106
|
+
if alias in env.get('projects', []):
|
|
107
|
+
print(f'{key}|{alias}')
|
|
108
|
+
exit()
|
|
109
|
+
# 3. 未命中,返回默认
|
|
110
|
+
print(f'{data[\"active\"]}|')
|
|
76
111
|
"
|
|
77
112
|
}
|
|
113
|
+
|
|
114
|
+
# 使用示例:
|
|
115
|
+
# result=$(find_env "dev10")
|
|
116
|
+
# ENV_KEY=$(echo "$result" | cut -d'|' -f1) # monitor-dev
|
|
117
|
+
# PROJECT=$(echo "$result" | cut -d'|' -f2) # dev10
|
|
118
|
+
# 查询时:{app="yunshitang",project="${PROJECT}"}
|
|
78
119
|
```
|
|
79
120
|
|
|
80
|
-
### 切换活跃环境
|
|
121
|
+
### 切换活跃环境 / 更新 Token
|
|
81
122
|
|
|
82
123
|
```bash
|
|
83
|
-
# 切换默认环境为 monitor-dev
|
|
84
124
|
python3 -c "
|
|
85
125
|
import json
|
|
86
126
|
data = json.load(open('${ENV_FILE}'))
|
|
87
127
|
data['active'] = 'monitor-dev'
|
|
88
128
|
json.dump(data, open('${ENV_FILE}', 'w'), indent=2, ensure_ascii=False)
|
|
89
|
-
print('Switched to:', data['active'])
|
|
90
129
|
"
|
|
91
|
-
```
|
|
92
130
|
|
|
93
|
-
### 更新 Token
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
# 为某个环境设置 Token
|
|
97
131
|
python3 -c "
|
|
98
132
|
import json
|
|
99
133
|
data = json.load(open('${ENV_FILE}'))
|
|
100
|
-
data['environments']['monitor-dev']['token'] = 'glsa_
|
|
134
|
+
data['environments']['monitor-dev']['token'] = 'glsa_新token'
|
|
101
135
|
json.dump(data, open('${ENV_FILE}', 'w'), indent=2, ensure_ascii=False)
|
|
102
|
-
print('Token updated for monitor-dev')
|
|
103
136
|
"
|
|
104
137
|
```
|
|
105
138
|
|
|
@@ -27,22 +27,27 @@ description: |
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## 连接信息获取(四级降级,自动查找)
|
|
31
31
|
|
|
32
32
|
当本技能被激活时,**按以下优先级获取数据库连接信息**:
|
|
33
33
|
|
|
34
34
|
### 优先级 1:用户对话中指定(最高优先级)
|
|
35
35
|
|
|
36
36
|
用户直接给出连接信息,或指定环境名:
|
|
37
|
-
- "连 dev 环境查一下" →
|
|
37
|
+
- "连 dev 环境查一下" → 使用配置中 dev 环境
|
|
38
|
+
- "去 dev10 查" → **范围匹配** dev 环境(range: "1~15")
|
|
38
39
|
- 直接给出 host/port/user/password → 直接使用
|
|
39
40
|
|
|
40
|
-
### 优先级 2
|
|
41
|
+
### 优先级 2:本地项目 `.claude/mysql-config.json`
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
**此文件为可选**,主要用于连接非本地环境(dev/prod 远程数据库)。
|
|
43
|
+
项目目录下的配置文件,优先于全局配置。
|
|
44
44
|
|
|
45
|
-
### 优先级 3
|
|
45
|
+
### 优先级 3:全局 `~/.claude/mysql-config.json`
|
|
46
|
+
|
|
47
|
+
全局配置文件(通过 `npx ai-engineering-init config --scope global` 创建),所有项目共享。
|
|
48
|
+
**推荐**:公司统一的数据库连接信息放全局,项目特定的覆盖放本地。
|
|
49
|
+
|
|
50
|
+
### 优先级 4:工程配置文件(零配置,本地开发默认)
|
|
46
51
|
|
|
47
52
|
从项目的 `bootstrap-dev.yml` 中自动提取连接信息:
|
|
48
53
|
|
|
@@ -107,43 +112,60 @@ brew install mysql-client
|
|
|
107
112
|
|
|
108
113
|
## 多环境支持
|
|
109
114
|
|
|
110
|
-
###
|
|
115
|
+
### 配置文件查找顺序
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
1. 本地项目:.claude/mysql-config.json
|
|
119
|
+
2. 全局配置:~/.claude/mysql-config.json
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
本地配置优先。如果本地无配置,使用全局。两者都存在时,本地覆盖全局(同名环境取本地值)。
|
|
111
123
|
|
|
112
|
-
|
|
124
|
+
### 配置文件结构(支持 range 范围匹配)
|
|
113
125
|
|
|
114
126
|
```json
|
|
115
127
|
{
|
|
116
128
|
"environments": {
|
|
117
|
-
"local": { "host": "127.0.0.1", "port": 3306, "user": "root", "password": "xxx" },
|
|
118
|
-
"dev": { "host": "dev-db.example.com", "port": 3306, "user": "dev_user", "password": "xxx" },
|
|
129
|
+
"local": { "host": "127.0.0.1", "port": 3306, "user": "root", "password": "xxx", "_desc": "本地环境" },
|
|
130
|
+
"dev": { "host": "dev-db.example.com", "port": 3306, "user": "dev_user", "password": "xxx", "range": "1~15", "_desc": "覆盖 dev1→dev15" },
|
|
131
|
+
"test": { "host": "test-db.example.com", "port": 3306, "user": "test_user", "password": "xxx", "range": "1~30", "_desc": "覆盖 test1→test30" },
|
|
119
132
|
"prod": { "host": "prod-db.example.com", "port": 3306, "user": "readonly", "password": "xxx" }
|
|
120
133
|
},
|
|
121
134
|
"default": "local"
|
|
122
135
|
}
|
|
123
136
|
```
|
|
124
137
|
|
|
138
|
+
### 环境范围匹配(range)
|
|
139
|
+
|
|
140
|
+
`range` 字段支持 `起始~结束` 格式,用于一个配置覆盖多个编号环境:
|
|
141
|
+
|
|
142
|
+
| 用户说法 | 匹配逻辑 | 结果 |
|
|
143
|
+
|---------|---------|------|
|
|
144
|
+
| "去 dev10 查" | 提取前缀 `dev`、编号 `10`,检查 dev 环境 range "1~15",10 在范围内 | 使用 dev 配置 |
|
|
145
|
+
| "连 test25" | 提取前缀 `test`、编号 `25`,检查 test 环境 range "1~30",25 在范围内 | 使用 test 配置 |
|
|
146
|
+
| "连 prod" | 精确匹配 prod 环境 | 使用 prod 配置 |
|
|
147
|
+
|
|
148
|
+
**匹配算法**:
|
|
149
|
+
```
|
|
150
|
+
1. 用户输入的环境名(如 "dev10")
|
|
151
|
+
2. 先精确匹配环境 key(environments["dev10"])
|
|
152
|
+
3. 未命中 → 拆分为前缀+编号("dev" + 10)
|
|
153
|
+
4. 查找有 range 字段的环境,前缀匹配 + 编号在范围内
|
|
154
|
+
5. 命中 → 使用该环境的 host/port/user/password
|
|
155
|
+
```
|
|
156
|
+
|
|
125
157
|
### 环境选择规则
|
|
126
158
|
|
|
127
159
|
| 优先级 | 来源 | 示例 |
|
|
128
160
|
|--------|------|------|
|
|
129
|
-
| 1(最高) | 用户对话中指定 | "连
|
|
161
|
+
| 1(最高) | 用户对话中指定 | "连 dev10 环境查一下" |
|
|
130
162
|
| 2 | 配置文件 `default` 字段 | `"default": "local"` |
|
|
131
163
|
|
|
132
|
-
### 环境关键词映射
|
|
133
|
-
|
|
134
|
-
用户说的话 → 对应环境名:
|
|
135
|
-
|
|
136
|
-
| 用户说法 | 环境 |
|
|
137
|
-
|---------|------|
|
|
138
|
-
| "本地"、"local"、"本地环境" | `local` |
|
|
139
|
-
| "开发"、"dev"、"测试环境"、"开发环境" | `dev` |
|
|
140
|
-
| "生产"、"prod"、"线上"、"正式环境" | `prod` |
|
|
141
|
-
|
|
142
164
|
### 连接示例
|
|
143
165
|
|
|
144
166
|
```bash
|
|
145
|
-
# 用户说"连
|
|
146
|
-
# →
|
|
167
|
+
# 用户说"连 dev10 环境查一下 order_info"
|
|
168
|
+
# → range 匹配到 dev 环境的连接信息 + 日志提取的数据库名
|
|
147
169
|
mysql -h dev-db.example.com -P 3306 -u dev_user -p'xxx' 546198574447230976 -e "SELECT ..."
|
|
148
170
|
```
|
|
149
171
|
|
package/bin/index.js
CHANGED
|
@@ -53,6 +53,8 @@ let force = false;
|
|
|
53
53
|
let skillFilter = ''; // sync-back --skill <名称>
|
|
54
54
|
let submitIssue = false; // sync-back --submit
|
|
55
55
|
let configType = ''; // config --type <mysql|loki|all>
|
|
56
|
+
let configScope = ''; // config --scope <local|global>
|
|
57
|
+
let configAdd = false; // config --add
|
|
56
58
|
|
|
57
59
|
for (let i = 0; i < args.length; i++) {
|
|
58
60
|
const arg = args[i];
|
|
@@ -109,6 +111,16 @@ for (let i = 0; i < args.length; i++) {
|
|
|
109
111
|
}
|
|
110
112
|
configType = args[++i];
|
|
111
113
|
break;
|
|
114
|
+
case '--scope':
|
|
115
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
116
|
+
console.error(fmt('red', `错误:${arg} 需要一个值(local | global)`));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
configScope = args[++i];
|
|
120
|
+
break;
|
|
121
|
+
case '--add':
|
|
122
|
+
configAdd = true;
|
|
123
|
+
break;
|
|
112
124
|
case '--help': case '-h':
|
|
113
125
|
printHelp();
|
|
114
126
|
process.exit(0);
|
|
@@ -140,6 +152,8 @@ function printHelp() {
|
|
|
140
152
|
console.log(' --skill, -s <技能> sync-back 时只对比指定技能');
|
|
141
153
|
console.log(' --submit sync-back 时自动创建 GitHub Issue(需要 gh CLI)');
|
|
142
154
|
console.log(' --type <类型> config 时指定配置类型: mysql | loki | all');
|
|
155
|
+
console.log(' --scope <范围> config 时指定范围: local(当前项目) | global(全局 ~/)');
|
|
156
|
+
console.log(' --add config 时追加环境(不覆盖已有配置)');
|
|
143
157
|
console.log(' --help, -h 显示此帮助\n');
|
|
144
158
|
console.log('示例:');
|
|
145
159
|
console.log(' npx ai-engineering-init --tool claude');
|
|
@@ -157,6 +171,8 @@ function printHelp() {
|
|
|
157
171
|
console.log(' npx ai-engineering-init config --type mysql # 只配置数据库连接');
|
|
158
172
|
console.log(' npx ai-engineering-init config --type loki # 只配置 Loki 日志');
|
|
159
173
|
console.log(' npx ai-engineering-init config --type all # 配置全部');
|
|
174
|
+
console.log(' npx ai-engineering-init config --type mysql --scope global # 全局配置(所有项目共享)');
|
|
175
|
+
console.log(' npx ai-engineering-init config --type mysql --add # 追加环境到已有配置');
|
|
160
176
|
}
|
|
161
177
|
|
|
162
178
|
// ── 工具定义(init 用)────────────────────────────────────────────────────
|
|
@@ -1132,7 +1148,8 @@ function runSyncBack(selectedTool, selectedSkill, doSubmit) {
|
|
|
1132
1148
|
console.log('');
|
|
1133
1149
|
}
|
|
1134
1150
|
|
|
1135
|
-
// ──
|
|
1151
|
+
// ── 环境配置初始化(MySQL / Loki)─────────────────────────────────────────
|
|
1152
|
+
|
|
1136
1153
|
function runConfig() {
|
|
1137
1154
|
if (!process.stdin.isTTY) {
|
|
1138
1155
|
console.error(fmt('red', '错误:config 命令需要交互式终端'));
|
|
@@ -1147,6 +1164,7 @@ function runConfig() {
|
|
|
1147
1164
|
(async () => {
|
|
1148
1165
|
try {
|
|
1149
1166
|
let type = configType;
|
|
1167
|
+
let scope = configScope;
|
|
1150
1168
|
|
|
1151
1169
|
// 未指定 --type 时显示交互式菜单
|
|
1152
1170
|
if (!type) {
|
|
@@ -1175,16 +1193,45 @@ function runConfig() {
|
|
|
1175
1193
|
process.exit(1);
|
|
1176
1194
|
}
|
|
1177
1195
|
|
|
1196
|
+
// 未指定 --scope 时询问
|
|
1197
|
+
if (!scope) {
|
|
1198
|
+
console.log(fmt('cyan', '请选择配置范围:'));
|
|
1199
|
+
console.log('');
|
|
1200
|
+
console.log(` ${fmt('bold', '1')}) ${fmt('green', 'global(全局)')} — 写入 ~/.claude/,所有项目共享`);
|
|
1201
|
+
console.log(` ${fmt('bold', '2')}) ${fmt('blue', 'local(本地)')} — 写入当前项目目录`);
|
|
1202
|
+
console.log('');
|
|
1203
|
+
const scopeAnswer = await ask(fmt('bold', '请输入选项 [1-2,默认 1]: ')) || '1';
|
|
1204
|
+
scope = scopeAnswer === '2' ? 'local' : 'global';
|
|
1205
|
+
console.log('');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (!['local', 'global'].includes(scope)) {
|
|
1209
|
+
console.error(fmt('red', `错误:不支持的范围 "${scope}",可选:local | global`));
|
|
1210
|
+
rl.close();
|
|
1211
|
+
process.exit(1);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const isGlobal = scope === 'global';
|
|
1215
|
+
if (isGlobal) {
|
|
1216
|
+
console.log(fmt('magenta', `配置范围:全局(~/.claude/),所有项目共享`));
|
|
1217
|
+
} else {
|
|
1218
|
+
console.log(fmt('magenta', `配置范围:本地(${targetDir})`));
|
|
1219
|
+
}
|
|
1220
|
+
console.log('');
|
|
1221
|
+
|
|
1178
1222
|
if (type === 'mysql' || type === 'all') {
|
|
1179
|
-
await runMysqlConfig(ask);
|
|
1223
|
+
await runMysqlConfig(ask, isGlobal);
|
|
1180
1224
|
}
|
|
1181
1225
|
if (type === 'loki' || type === 'all') {
|
|
1182
|
-
if (type === 'all') console.log('');
|
|
1183
|
-
await runLokiConfig(ask);
|
|
1226
|
+
if (type === 'all') console.log('');
|
|
1227
|
+
await runLokiConfig(ask, isGlobal);
|
|
1184
1228
|
}
|
|
1185
1229
|
|
|
1186
1230
|
console.log('');
|
|
1187
1231
|
console.log(fmt('green', fmt('bold', '配置初始化完成!')));
|
|
1232
|
+
if (isGlobal) {
|
|
1233
|
+
console.log(fmt('cyan', '技能会按 全局(~/.claude/) → 本地(.claude/) 顺序查找配置,本地优先。'));
|
|
1234
|
+
}
|
|
1188
1235
|
} finally {
|
|
1189
1236
|
rl.close();
|
|
1190
1237
|
}
|
|
@@ -1193,181 +1240,152 @@ function runConfig() {
|
|
|
1193
1240
|
|
|
1194
1241
|
// ── MySQL 数据库配置 ────────────────────────────────────────────────────────
|
|
1195
1242
|
|
|
1196
|
-
async function runMysqlConfig(ask) {
|
|
1243
|
+
async function runMysqlConfig(ask, isGlobal) {
|
|
1197
1244
|
console.log(fmt('blue', fmt('bold', '┌─ MySQL 数据库连接配置 ─┐')));
|
|
1198
1245
|
console.log('');
|
|
1199
1246
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1247
|
+
const configPath = isGlobal
|
|
1248
|
+
? path.join(HOME_DIR, '.claude', 'mysql-config.json')
|
|
1249
|
+
: path.join(targetDir, '.claude', 'mysql-config.json');
|
|
1202
1250
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1251
|
+
// 读取已有配置
|
|
1252
|
+
let existingConfig = null;
|
|
1253
|
+
if (fs.existsSync(configPath)) {
|
|
1254
|
+
try { existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch { /* ignore */ }
|
|
1206
1255
|
}
|
|
1207
1256
|
|
|
1208
|
-
//
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1257
|
+
// --add 模式
|
|
1258
|
+
if (configAdd && existingConfig && existingConfig.environments) {
|
|
1259
|
+
console.log(fmt('cyan', '当前已配置的环境:'));
|
|
1260
|
+
for (const [key, env] of Object.entries(existingConfig.environments)) {
|
|
1261
|
+
const rangeStr = env.range ? fmt('magenta', ` (range: ${env.range})`) : '';
|
|
1262
|
+
console.log(` ${fmt('bold', key)} — ${env._desc || key}${rangeStr} host=${env.host}`);
|
|
1263
|
+
}
|
|
1264
|
+
console.log('');
|
|
1265
|
+
console.log(fmt('green', '将追加新环境到已有配置。'));
|
|
1266
|
+
console.log('');
|
|
1267
|
+
} else if (existingConfig && !configAdd) {
|
|
1268
|
+
console.log(fmt('yellow', `⚠ 配置文件已存在:${configPath}`));
|
|
1269
|
+
const overwrite = await ask(fmt('bold', '输入 add 追加环境,y 重建,N 跳过 [add/y/N]: '));
|
|
1270
|
+
if (overwrite.toLowerCase() === 'add') {
|
|
1271
|
+
// 进入追加模式
|
|
1272
|
+
} else if (overwrite.toLowerCase() !== 'y') {
|
|
1214
1273
|
console.log('已跳过 MySQL 配置。');
|
|
1215
1274
|
return;
|
|
1275
|
+
} else {
|
|
1276
|
+
existingConfig = null;
|
|
1216
1277
|
}
|
|
1217
1278
|
console.log('');
|
|
1218
1279
|
}
|
|
1219
1280
|
|
|
1220
|
-
//
|
|
1221
|
-
console.log(fmt('cyan', '
|
|
1222
|
-
console.log('');
|
|
1223
|
-
console.log(` ${fmt('bold', '1')}) local — 本地开发环境`);
|
|
1224
|
-
console.log(` ${fmt('bold', '2')}) dev — 开发测试环境`);
|
|
1225
|
-
console.log(` ${fmt('bold', '3')}) test — 测试环境`);
|
|
1226
|
-
console.log(` ${fmt('bold', '4')}) prod — 生产环境`);
|
|
1281
|
+
// 自定义环境名输入
|
|
1282
|
+
console.log(fmt('cyan', '请输入要配置的环境(自定义名称,多个用逗号分隔):'));
|
|
1283
|
+
console.log(fmt('yellow', ' 示例:local, dev, test, prod 或自定义名称'));
|
|
1227
1284
|
console.log('');
|
|
1228
|
-
const envAnswer = await ask(fmt('bold', '
|
|
1229
|
-
|
|
1230
|
-
const ENV_DEFAULTS = {
|
|
1231
|
-
local: { host: '127.0.0.1', user: 'root', desc: '本地开发环境' },
|
|
1232
|
-
dev: { host: '', user: '', desc: '开发测试环境' },
|
|
1233
|
-
test: { host: '', user: '', desc: '测试环境' },
|
|
1234
|
-
prod: { host: '', user: '', desc: '生产环境' },
|
|
1235
|
-
};
|
|
1236
|
-
|
|
1237
|
-
const envNames = ['local', 'dev', 'test', 'prod'];
|
|
1238
|
-
const selected = parseSelection(envAnswer, envNames);
|
|
1285
|
+
const envAnswer = await ask(fmt('bold', '环境名称: '));
|
|
1286
|
+
const envNames = envAnswer.split(',').map(s => s.trim()).filter(Boolean);
|
|
1239
1287
|
|
|
1240
|
-
if (
|
|
1241
|
-
console.error(fmt('red', '
|
|
1288
|
+
if (envNames.length === 0) {
|
|
1289
|
+
console.error(fmt('red', '未输入任何环境名,跳过 MySQL 配置。'));
|
|
1242
1290
|
return;
|
|
1243
1291
|
}
|
|
1244
|
-
|
|
1245
|
-
console.log('');
|
|
1246
|
-
console.log(fmt('green', `已选择环境:${selected.join(', ')}`));
|
|
1247
1292
|
console.log('');
|
|
1248
1293
|
|
|
1249
1294
|
// 收集每个环境的配置
|
|
1250
|
-
const
|
|
1251
|
-
for (const env of
|
|
1252
|
-
|
|
1295
|
+
const newEnvironments = {};
|
|
1296
|
+
for (const env of envNames) {
|
|
1297
|
+
if (existingConfig && existingConfig.environments && existingConfig.environments[env]) {
|
|
1298
|
+
console.log(fmt('yellow', ` ${env} 已存在,跳过。使用 y 模式可重建。`));
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const isLocal = env === 'local';
|
|
1253
1303
|
console.log(fmt('cyan', `── ${env} 环境配置 ──`));
|
|
1254
1304
|
|
|
1255
|
-
const host = await ask(` host [${
|
|
1305
|
+
const host = await ask(` host [${isLocal ? '127.0.0.1' : '无默认'}]: `) || (isLocal ? '127.0.0.1' : '');
|
|
1306
|
+
if (!host) { console.error(fmt('red', ` host 不能为空,跳过。`)); continue; }
|
|
1256
1307
|
const port = await ask(' port [3306]: ') || '3306';
|
|
1257
|
-
const user = await ask(` user [${
|
|
1308
|
+
const user = await ask(` user [${isLocal ? 'root' : '无默认'}]: `) || (isLocal ? 'root' : '');
|
|
1309
|
+
if (!user) { console.error(fmt('red', ` user 不能为空,跳过。`)); continue; }
|
|
1258
1310
|
const password = await ask(' password: ');
|
|
1259
|
-
const desc = await ask(` 描述 [${
|
|
1311
|
+
const desc = await ask(` 描述 [${env}环境]: `) || `${env}环境`;
|
|
1312
|
+
const rangeInput = await ask(` 覆盖范围(如 ${fmt('bold', '1~15')} 表示 ${env}1→${env}15,留空=无范围): `);
|
|
1260
1313
|
console.log('');
|
|
1261
1314
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
continue;
|
|
1265
|
-
}
|
|
1266
|
-
if (!user) {
|
|
1267
|
-
console.error(fmt('red', `错误:${env} 环境的 user 不能为空,跳过此环境。`));
|
|
1268
|
-
continue;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
environments[env] = {
|
|
1272
|
-
host,
|
|
1273
|
-
port: parseInt(port, 10),
|
|
1274
|
-
user,
|
|
1275
|
-
password,
|
|
1276
|
-
_desc: desc,
|
|
1277
|
-
};
|
|
1315
|
+
newEnvironments[env] = { host, port: parseInt(port, 10), user, password, _desc: desc };
|
|
1316
|
+
if (rangeInput) newEnvironments[env].range = rangeInput;
|
|
1278
1317
|
}
|
|
1279
1318
|
|
|
1280
|
-
if (Object.keys(
|
|
1319
|
+
if (Object.keys(newEnvironments).length === 0 && !existingConfig) {
|
|
1281
1320
|
console.error(fmt('red', '未成功配置任何环境。'));
|
|
1282
1321
|
return;
|
|
1283
1322
|
}
|
|
1284
1323
|
|
|
1324
|
+
// 合并配置
|
|
1325
|
+
const allEnvironments = {
|
|
1326
|
+
...(existingConfig && existingConfig.environments ? existingConfig.environments : {}),
|
|
1327
|
+
...newEnvironments,
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1285
1330
|
// 选择默认环境
|
|
1286
|
-
const
|
|
1287
|
-
let defaultEnv =
|
|
1288
|
-
if (
|
|
1331
|
+
const allEnvKeys = Object.keys(allEnvironments);
|
|
1332
|
+
let defaultEnv = (existingConfig && existingConfig.default) || allEnvKeys[0];
|
|
1333
|
+
if (Object.keys(newEnvironments).length > 0 && allEnvKeys.length > 1) {
|
|
1289
1334
|
console.log(fmt('cyan', '请选择默认环境:'));
|
|
1290
|
-
|
|
1291
|
-
|
|
1335
|
+
allEnvKeys.forEach((env, i) => {
|
|
1336
|
+
const marker = env === defaultEnv ? fmt('green', ' (当前)') : '';
|
|
1337
|
+
console.log(` ${fmt('bold', String(i + 1))}) ${env}${marker}`);
|
|
1292
1338
|
});
|
|
1293
|
-
const defaultAnswer = await ask(fmt('bold', `请输入选项 [1-${
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
defaultEnv =
|
|
1339
|
+
const defaultAnswer = await ask(fmt('bold', `请输入选项 [1-${allEnvKeys.length},回车保持当前]: `));
|
|
1340
|
+
if (defaultAnswer) {
|
|
1341
|
+
const idx = parseInt(defaultAnswer, 10) - 1;
|
|
1342
|
+
if (idx >= 0 && idx < allEnvKeys.length) defaultEnv = allEnvKeys[idx];
|
|
1297
1343
|
}
|
|
1298
1344
|
console.log('');
|
|
1299
1345
|
}
|
|
1300
1346
|
|
|
1301
|
-
// 写入配置文件(多目标)
|
|
1302
1347
|
const config = {
|
|
1303
|
-
environments,
|
|
1348
|
+
environments: allEnvironments,
|
|
1304
1349
|
default: defaultEnv,
|
|
1305
|
-
_comment: '
|
|
1350
|
+
_comment: '环境支持 range 字段(如 "1~15"),用户说"dev10"时自动匹配。查找顺序:本地 .claude/ > 全局 ~/.claude/',
|
|
1306
1351
|
};
|
|
1307
1352
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
console.log(` ${fmt('green', '✔')} 已写入:${t.configPath}`);
|
|
1313
|
-
}
|
|
1353
|
+
const dir = path.dirname(configPath);
|
|
1354
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1355
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
1356
|
+
console.log(` ${fmt('green', '✔')} 已写入:${configPath}`);
|
|
1314
1357
|
|
|
1315
|
-
|
|
1316
|
-
ensureGitignore(['mysql-config.json']);
|
|
1358
|
+
if (!isGlobal) ensureGitignore(['mysql-config.json']);
|
|
1317
1359
|
|
|
1318
1360
|
console.log('');
|
|
1319
1361
|
console.log(fmt('green', 'MySQL 数据库配置完成!'));
|
|
1320
|
-
|
|
1362
|
+
for (const [key, env] of Object.entries(newEnvironments)) {
|
|
1363
|
+
if (env.range) {
|
|
1364
|
+
console.log(fmt('cyan', ` ${key} 覆盖 ${key}${env.range.replace('~', '→')},说"${key}10"将自动匹配`));
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1321
1367
|
}
|
|
1322
1368
|
|
|
1323
1369
|
// ── Loki 日志查询配置 ──────────────────────────────────────────────────────
|
|
1324
1370
|
|
|
1325
|
-
async function runLokiConfig(ask) {
|
|
1371
|
+
async function runLokiConfig(ask, isGlobal) {
|
|
1326
1372
|
console.log(fmt('blue', fmt('bold', '┌─ Loki 日志查询配置 ─┐')));
|
|
1327
1373
|
console.log('');
|
|
1328
1374
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1375
|
+
const configPath = isGlobal
|
|
1376
|
+
? path.join(HOME_DIR, '.claude', 'loki-config.json')
|
|
1377
|
+
: getLokiConfigPath();
|
|
1331
1378
|
|
|
1332
|
-
if (
|
|
1333
|
-
console.log(fmt('yellow', '⚠
|
|
1379
|
+
if (!configPath) {
|
|
1380
|
+
console.log(fmt('yellow', '⚠ 未检测到配置目录。请先运行 init 安装框架。'));
|
|
1334
1381
|
return;
|
|
1335
1382
|
}
|
|
1336
1383
|
|
|
1337
|
-
// 读取已有配置作为模板
|
|
1338
1384
|
let existingConfig = null;
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
try {
|
|
1342
|
-
existingConfig = JSON.parse(fs.readFileSync(t.configPath, 'utf-8'));
|
|
1343
|
-
break;
|
|
1344
|
-
} catch { /* ignore */ }
|
|
1345
|
-
}
|
|
1385
|
+
if (fs.existsSync(configPath)) {
|
|
1386
|
+
try { existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch { /* ignore */ }
|
|
1346
1387
|
}
|
|
1347
1388
|
|
|
1348
|
-
// 默认环境模板
|
|
1349
|
-
const DEFAULT_ENVS = {
|
|
1350
|
-
test: {
|
|
1351
|
-
name: '测试环境',
|
|
1352
|
-
url: '',
|
|
1353
|
-
aliases: ['test'],
|
|
1354
|
-
projects: [],
|
|
1355
|
-
},
|
|
1356
|
-
dev: {
|
|
1357
|
-
name: '开发环境',
|
|
1358
|
-
url: '',
|
|
1359
|
-
aliases: ['dev'],
|
|
1360
|
-
projects: [],
|
|
1361
|
-
},
|
|
1362
|
-
prod: {
|
|
1363
|
-
name: '生产环境',
|
|
1364
|
-
url: '',
|
|
1365
|
-
aliases: ['prod'],
|
|
1366
|
-
projects: [],
|
|
1367
|
-
},
|
|
1368
|
-
};
|
|
1369
|
-
|
|
1370
|
-
// 如果已有配置,展示当前状态
|
|
1371
1389
|
if (existingConfig && existingConfig.environments) {
|
|
1372
1390
|
const envs = existingConfig.environments;
|
|
1373
1391
|
const envList = Object.keys(envs);
|
|
@@ -1376,182 +1394,190 @@ async function runLokiConfig(ask) {
|
|
|
1376
1394
|
for (const key of envList) {
|
|
1377
1395
|
const env = envs[key];
|
|
1378
1396
|
const hasToken = env.token && env.token.length > 0;
|
|
1379
|
-
const status = hasToken ? fmt('green', '✔
|
|
1380
|
-
|
|
1381
|
-
|
|
1397
|
+
const status = hasToken ? fmt('green', '✔ Token') : fmt('red', '✗ 缺Token');
|
|
1398
|
+
const rangeStr = env.range ? fmt('magenta', ` (range: ${env.range})`) : '';
|
|
1399
|
+
console.log(` ${fmt('bold', key)} — ${env.name || key} ${status}${rangeStr}`);
|
|
1382
1400
|
}
|
|
1383
1401
|
console.log('');
|
|
1384
1402
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1403
|
+
if (configAdd) {
|
|
1404
|
+
console.log(fmt('green', '将追加新环境到已有配置。'));
|
|
1405
|
+
} else {
|
|
1406
|
+
const missingTokenEnvs = envList.filter(k => !envs[k].token);
|
|
1407
|
+
const action = await ask(fmt('bold', '输入 token 补充Token,add 追加环境,N 跳过 [token/add/N]: ')) || 'token';
|
|
1408
|
+
if (action.toLowerCase() === 'token') {
|
|
1409
|
+
await updateLokiTokens(ask, existingConfig, configPath, isGlobal);
|
|
1410
|
+
return;
|
|
1411
|
+
} else if (action.toLowerCase() !== 'add') {
|
|
1390
1412
|
console.log('已跳过 Loki 配置。');
|
|
1391
1413
|
return;
|
|
1392
1414
|
}
|
|
1393
|
-
} else {
|
|
1394
|
-
console.log(fmt('yellow', `有 ${missingTokenEnvs.length} 个环境缺少 Token,将引导配置。`));
|
|
1395
1415
|
}
|
|
1396
1416
|
console.log('');
|
|
1397
1417
|
|
|
1398
|
-
//
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
console.log('');
|
|
1405
|
-
|
|
1406
|
-
for (const key of envList) {
|
|
1407
|
-
const env = envs[key];
|
|
1408
|
-
const hasToken = env.token && env.token.length > 0;
|
|
1409
|
-
if (hasToken) {
|
|
1410
|
-
const update = await ask(` ${fmt('bold', key)} 已有 Token,是否更新?[y/N]: `);
|
|
1411
|
-
if (update.toLowerCase() !== 'y') continue;
|
|
1412
|
-
}
|
|
1413
|
-
console.log(` ${fmt('cyan', `── ${key}: ${env.name || key} ──`)}`);
|
|
1414
|
-
if (env.url) console.log(` Grafana URL: ${fmt('bold', env.url)}`);
|
|
1415
|
-
const token = await ask(` 输入 Grafana Service Account Token: `);
|
|
1416
|
-
if (token) {
|
|
1417
|
-
envs[key].token = token;
|
|
1418
|
-
console.log(` ${fmt('green', '✔')} Token 已设置`);
|
|
1419
|
-
} else {
|
|
1420
|
-
console.log(` ${fmt('yellow', '⚠')} 跳过(Token 为空)`);
|
|
1421
|
-
}
|
|
1422
|
-
console.log('');
|
|
1423
|
-
}
|
|
1418
|
+
// 追加环境
|
|
1419
|
+
await addLokiEnvironments(ask, existingConfig, configPath, isGlobal);
|
|
1420
|
+
} else {
|
|
1421
|
+
await createLokiConfig(ask, configPath, isGlobal);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
1424
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
const activeAnswer = await ask(fmt('bold', `请输入选项 [1-${envList.length}]: `));
|
|
1431
|
-
const activeIdx = parseInt(activeAnswer, 10) - 1;
|
|
1432
|
-
const activeEnv = (activeIdx >= 0 && activeIdx < envList.length) ? envList[activeIdx] : existingConfig.active;
|
|
1433
|
-
console.log('');
|
|
1425
|
+
async function updateLokiTokens(ask, config, configPath, isGlobal) {
|
|
1426
|
+
console.log('');
|
|
1427
|
+
console.log(fmt('cyan', fmt('bold', 'Grafana Token 获取:')));
|
|
1428
|
+
console.log(` Grafana → Administration → Service accounts → Add(Viewer)→ Add token`);
|
|
1429
|
+
console.log('');
|
|
1434
1430
|
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1431
|
+
const envs = config.environments;
|
|
1432
|
+
for (const key of Object.keys(envs)) {
|
|
1433
|
+
const env = envs[key];
|
|
1434
|
+
const hasToken = env.token && env.token.length > 0;
|
|
1435
|
+
if (hasToken) {
|
|
1436
|
+
const update = await ask(` ${fmt('bold', key)} 已有 Token,更新?[y/N]: `);
|
|
1437
|
+
if (update.toLowerCase() !== 'y') continue;
|
|
1438
|
+
}
|
|
1439
|
+
if (env.url) console.log(` URL: ${fmt('bold', env.url)}`);
|
|
1440
|
+
const token = await ask(` 输入 ${key} 的 Token: `);
|
|
1441
|
+
if (token) {
|
|
1442
|
+
envs[key].token = token;
|
|
1443
|
+
console.log(` ${fmt('green', '✔')} 已设置`);
|
|
1442
1444
|
}
|
|
1443
|
-
} else {
|
|
1444
|
-
// 无已有配置,从零创建
|
|
1445
|
-
console.log(fmt('cyan', '将创建新的 Loki 日志查询配置。'));
|
|
1446
|
-
console.log('');
|
|
1447
|
-
console.log(fmt('cyan', '请输入要配置的 Grafana 环境数量:'));
|
|
1448
|
-
const countAnswer = await ask(fmt('bold', '环境数量 [1]: ')) || '1';
|
|
1449
|
-
const count = Math.max(1, Math.min(10, parseInt(countAnswer, 10) || 1));
|
|
1450
1445
|
console.log('');
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
writeLokiConfig(config, configPath, isGlobal);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async function addLokiEnvironments(ask, config, configPath, isGlobal) {
|
|
1452
|
+
printLokiTokenGuide();
|
|
1453
|
+
const countAnswer = await ask(fmt('bold', '要追加几个环境?[1]: ')) || '1';
|
|
1454
|
+
const count = Math.max(1, Math.min(10, parseInt(countAnswer, 10) || 1));
|
|
1455
|
+
console.log('');
|
|
1456
|
+
|
|
1457
|
+
for (let i = 0; i < count; i++) {
|
|
1458
|
+
const envData = await collectLokiEnvInput(ask, i + 1, count);
|
|
1459
|
+
if (!envData) continue;
|
|
1460
|
+
config.environments[envData.key] = envData.value;
|
|
1461
|
+
}
|
|
1451
1462
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1463
|
+
writeLokiConfig(config, configPath, isGlobal);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
async function createLokiConfig(ask, configPath, isGlobal) {
|
|
1467
|
+
console.log(fmt('cyan', '将创建新的 Loki 日志查询配置。'));
|
|
1468
|
+
console.log('');
|
|
1469
|
+
printLokiTokenGuide();
|
|
1470
|
+
const countAnswer = await ask(fmt('bold', '要配置几个 Grafana 环境?[1]: ')) || '1';
|
|
1471
|
+
const count = Math.max(1, Math.min(10, parseInt(countAnswer, 10) || 1));
|
|
1472
|
+
console.log('');
|
|
1473
|
+
|
|
1474
|
+
const environments = {};
|
|
1475
|
+
let activeEnv = '';
|
|
1476
|
+
|
|
1477
|
+
for (let i = 0; i < count; i++) {
|
|
1478
|
+
const envData = await collectLokiEnvInput(ask, i + 1, count);
|
|
1479
|
+
if (!envData) continue;
|
|
1480
|
+
environments[envData.key] = envData.value;
|
|
1481
|
+
if (!activeEnv) activeEnv = envData.key;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
if (Object.keys(environments).length === 0) {
|
|
1485
|
+
console.error(fmt('red', '未配置任何环境。'));
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const envKeys = Object.keys(environments);
|
|
1490
|
+
if (envKeys.length > 1) {
|
|
1491
|
+
console.log(fmt('cyan', '请选择默认活跃环境:'));
|
|
1492
|
+
envKeys.forEach((env, i) => console.log(` ${fmt('bold', String(i + 1))}) ${env}`));
|
|
1493
|
+
const activeAnswer = await ask(fmt('bold', `请输入选项 [1-${envKeys.length}]: `));
|
|
1494
|
+
const idx = parseInt(activeAnswer, 10) - 1;
|
|
1495
|
+
if (idx >= 0 && idx < envKeys.length) activeEnv = envKeys[idx];
|
|
1457
1496
|
console.log('');
|
|
1497
|
+
}
|
|
1458
1498
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1499
|
+
const config = {
|
|
1500
|
+
_comment: 'Loki 多环境配置。环境支持 range 字段。查找顺序:本地 > 全局 ~/.claude/',
|
|
1501
|
+
_setup: 'Token:Grafana → Administration → Service accounts → Add(Viewer)→ Add token',
|
|
1502
|
+
active: activeEnv,
|
|
1503
|
+
environments,
|
|
1504
|
+
};
|
|
1461
1505
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
const envKey = await ask(` 环境标识(如 test、dev、prod): `);
|
|
1465
|
-
if (!envKey) {
|
|
1466
|
-
console.log(fmt('yellow', ' 跳过(标识为空)'));
|
|
1467
|
-
continue;
|
|
1468
|
-
}
|
|
1469
|
-
const name = await ask(` 环境名称(如 "测试环境"): `) || envKey;
|
|
1470
|
-
const url = await ask(` Grafana URL(如 https://grafana.example.com): `);
|
|
1471
|
-
const token = await ask(` Grafana Service Account Token: `);
|
|
1472
|
-
const aliasStr = await ask(` 别名(逗号分隔,如 test,t)[${envKey}]: `) || envKey;
|
|
1473
|
-
const aliases = aliasStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
1474
|
-
console.log('');
|
|
1506
|
+
writeLokiConfig(config, configPath, isGlobal);
|
|
1507
|
+
}
|
|
1475
1508
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1509
|
+
function printLokiTokenGuide() {
|
|
1510
|
+
console.log(fmt('cyan', fmt('bold', 'Grafana Token 获取:')));
|
|
1511
|
+
console.log(` Grafana → Administration → Service accounts → Add(Viewer)→ Add token`);
|
|
1512
|
+
console.log('');
|
|
1513
|
+
}
|
|
1479
1514
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1515
|
+
async function collectLokiEnvInput(ask, index, total) {
|
|
1516
|
+
console.log(fmt('cyan', `── 环境 ${index}/${total} ──`));
|
|
1517
|
+
const envKey = await ask(` 环境标识(如 monitor-dev、test13): `);
|
|
1518
|
+
if (!envKey) { console.log(fmt('yellow', ' 跳过')); return null; }
|
|
1519
|
+
const name = await ask(` 环境名称(如 "开发环境"): `) || envKey;
|
|
1520
|
+
const url = await ask(` Grafana URL: `);
|
|
1521
|
+
const token = await ask(` Token(可留空稍后配): `);
|
|
1522
|
+
const aliasStr = await ask(` 别名(逗号分隔)[${envKey}]: `) || envKey;
|
|
1523
|
+
const aliases = aliasStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
1524
|
+
const rangeInput = await ask(` 项目覆盖范围(如 ${fmt('bold', 'dev1~15')} 表示 dev01→dev15,留空=无范围): `);
|
|
1525
|
+
console.log('');
|
|
1484
1526
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
});
|
|
1492
|
-
const activeAnswer = await ask(fmt('bold', `请输入选项 [1-${envKeys.length}]: `));
|
|
1493
|
-
const idx = parseInt(activeAnswer, 10) - 1;
|
|
1494
|
-
if (idx >= 0 && idx < envKeys.length) activeEnv = envKeys[idx];
|
|
1527
|
+
const value = { name, url, token: token || '', aliases };
|
|
1528
|
+
if (rangeInput) {
|
|
1529
|
+
value.range = rangeInput;
|
|
1530
|
+
value.projects = expandRange(rangeInput);
|
|
1531
|
+
if (value.projects.length > 0) {
|
|
1532
|
+
console.log(fmt('cyan', ` 已展开 ${value.projects.length} 个项目:${value.projects.slice(0, 5).join(', ')}${value.projects.length > 5 ? '...' : ''}`));
|
|
1495
1533
|
console.log('');
|
|
1496
1534
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
_comment: 'Loki 多环境配置。每个环境需要独立的 Grafana Service Account Token。',
|
|
1500
|
-
_usage: "用户说'查 test 的日志'或'去 dev 查'时,匹配对应环境。",
|
|
1501
|
-
_setup: 'Token 创建:Grafana → Administration → Service accounts → Add(Viewer 角色)→ Add token → 复制到对应环境的 token 字段。',
|
|
1502
|
-
active: activeEnv,
|
|
1503
|
-
environments,
|
|
1504
|
-
};
|
|
1505
|
-
|
|
1506
|
-
for (const t of targets) {
|
|
1507
|
-
const dir = path.dirname(t.configPath);
|
|
1508
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1509
|
-
fs.writeFileSync(t.configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
1510
|
-
console.log(` ${fmt('green', '✔')} 已写入:${t.configPath}`);
|
|
1511
|
-
}
|
|
1535
|
+
} else {
|
|
1536
|
+
value.projects = [];
|
|
1512
1537
|
}
|
|
1513
1538
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1539
|
+
return { key: envKey, value };
|
|
1540
|
+
}
|
|
1516
1541
|
|
|
1542
|
+
function writeLokiConfig(config, configPath, isGlobal) {
|
|
1543
|
+
const dir = path.dirname(configPath);
|
|
1544
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1545
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
1546
|
+
console.log(` ${fmt('green', '✔')} 已写入:${configPath}`);
|
|
1547
|
+
if (!isGlobal) ensureGitignore(['loki-config.json', 'environments.json']);
|
|
1517
1548
|
console.log('');
|
|
1518
1549
|
console.log(fmt('green', 'Loki 日志查询配置完成!'));
|
|
1519
|
-
console.log(`使用 ${fmt('bold', 'loki-log-query')} 技能时将自动读取此配置。`);
|
|
1520
1550
|
}
|
|
1521
1551
|
|
|
1522
1552
|
// ── Config 工具函数 ─────────────────────────────────────────────────────────
|
|
1523
1553
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
if (fs.existsSync(
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
targets.push({ tool: 'cursor', configPath: claudePath });
|
|
1535
|
-
}
|
|
1536
|
-
return targets;
|
|
1554
|
+
function getLokiConfigPath() {
|
|
1555
|
+
const lokiJsonClaude = path.join(targetDir, '.claude', 'loki-config.json');
|
|
1556
|
+
if (fs.existsSync(lokiJsonClaude)) return lokiJsonClaude;
|
|
1557
|
+
const envJsonClaude = path.join(targetDir, '.claude', 'skills', 'loki-log-query', 'environments.json');
|
|
1558
|
+
if (fs.existsSync(envJsonClaude)) return envJsonClaude;
|
|
1559
|
+
const envJsonCursor = path.join(targetDir, '.cursor', 'skills', 'loki-log-query', 'environments.json');
|
|
1560
|
+
if (fs.existsSync(envJsonCursor)) return envJsonCursor;
|
|
1561
|
+
if (fs.existsSync(path.join(targetDir, '.claude'))) return lokiJsonClaude;
|
|
1562
|
+
if (fs.existsSync(path.join(targetDir, '.cursor'))) return path.join(targetDir, '.cursor', 'loki-config.json');
|
|
1563
|
+
return null;
|
|
1537
1564
|
}
|
|
1538
1565
|
|
|
1539
|
-
/**
|
|
1540
|
-
function
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
}
|
|
1551
|
-
return
|
|
1566
|
+
/** 展开范围字符串为项目名列表,如 "dev1~15" → ["dev01","dev02",...,"dev15"] */
|
|
1567
|
+
function expandRange(rangeStr) {
|
|
1568
|
+
const match = rangeStr.match(/^([a-zA-Z-]*)(\d+)\s*[~~]\s*(\d+)$/);
|
|
1569
|
+
if (!match) return [];
|
|
1570
|
+
const prefix = match[1];
|
|
1571
|
+
const start = parseInt(match[2], 10);
|
|
1572
|
+
const end = parseInt(match[3], 10);
|
|
1573
|
+
const maxDigits = Math.max(String(start).length, String(end).length, 2);
|
|
1574
|
+
const result = [];
|
|
1575
|
+
for (let i = start; i <= end; i++) {
|
|
1576
|
+
result.push(prefix + String(i).padStart(maxDigits, '0'));
|
|
1577
|
+
}
|
|
1578
|
+
return result;
|
|
1552
1579
|
}
|
|
1553
1580
|
|
|
1554
|
-
/** 解析选择字符串(如 "1,2" 或 "1-3") */
|
|
1555
1581
|
function parseSelection(answer, names) {
|
|
1556
1582
|
const selected = new Set();
|
|
1557
1583
|
for (const part of answer.split(',')) {
|
|
@@ -1571,7 +1597,6 @@ function parseSelection(answer, names) {
|
|
|
1571
1597
|
return [...selected];
|
|
1572
1598
|
}
|
|
1573
1599
|
|
|
1574
|
-
/** 确保敏感配置文件在 .gitignore 中 */
|
|
1575
1600
|
function ensureGitignore(patterns) {
|
|
1576
1601
|
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
1577
1602
|
let content = '';
|
|
@@ -1580,9 +1605,7 @@ function ensureGitignore(patterns) {
|
|
|
1580
1605
|
}
|
|
1581
1606
|
const lines = content.split('\n').map(l => l.trim());
|
|
1582
1607
|
const toAdd = [];
|
|
1583
|
-
|
|
1584
1608
|
for (const pattern of patterns) {
|
|
1585
|
-
// 检查是否已有任意形式的忽略规则(精确路径、通配符等)
|
|
1586
1609
|
const alreadyIgnored = lines.some(line =>
|
|
1587
1610
|
line.endsWith(pattern) || line.endsWith(`/${pattern}`) || line === `**/${pattern}`
|
|
1588
1611
|
);
|
|
@@ -1591,11 +1614,10 @@ function ensureGitignore(patterns) {
|
|
|
1591
1614
|
toAdd.push(`**/${pattern}`);
|
|
1592
1615
|
}
|
|
1593
1616
|
}
|
|
1594
|
-
|
|
1595
1617
|
if (toAdd.length > 0) {
|
|
1596
1618
|
const separator = content.endsWith('\n') || content === '' ? '' : '\n';
|
|
1597
1619
|
fs.appendFileSync(gitignorePath, `${separator}${toAdd.join('\n')}\n`, 'utf-8');
|
|
1598
|
-
console.log(` ${fmt('green', '✔')} 已更新 .gitignore
|
|
1620
|
+
console.log(` ${fmt('green', '✔')} 已更新 .gitignore`);
|
|
1599
1621
|
}
|
|
1600
1622
|
}
|
|
1601
1623
|
|