ai-engineering-init 1.16.0 → 1.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,163 @@ 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
- const targets = detectConfigTargets('mysql-config.json');
1247
+ const configPath = isGlobal
1248
+ ? path.join(HOME_DIR, '.claude', 'mysql-config.json')
1249
+ : path.join(targetDir, '.claude', 'mysql-config.json');
1202
1250
 
1203
- if (targets.length === 0) {
1204
- console.log(fmt('yellow', '⚠ 未检测到 .claude/ 或 .cursor/ 目录。请先运行 init 安装框架。'));
1205
- return;
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
- const existingTarget = targets.find(t => fs.existsSync(t.configPath));
1210
- if (existingTarget) {
1211
- console.log(fmt('yellow', `⚠ 配置文件已存在:${existingTarget.configPath}`));
1212
- const overwrite = await ask(fmt('bold', '是否重新配置?[y/N]: '));
1213
- if (overwrite.toLowerCase() !== 'y') {
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', '请输入选项(如 1,2 或 1-3): '));
1285
+ const envAnswer = await ask(fmt('bold', '环境名称: '));
1286
+ const envNames = envAnswer.split(',').map(s => s.trim()).filter(Boolean);
1229
1287
 
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);
1239
-
1240
- if (selected.length === 0) {
1241
- console.error(fmt('red', '未选择任何环境,跳过 MySQL 配置。'));
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 environments = {};
1251
- for (const env of selected) {
1252
- const defaults = ENV_DEFAULTS[env];
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 [${defaults.host || '无默认'}]: `) || defaults.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 [${defaults.user || '无默认'}]: `) || defaults.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(` 描述 [${defaults.desc}]: `) || defaults.desc;
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
- if (!host) {
1263
- console.error(fmt('red', `错误:${env} 环境的 host 不能为空,跳过此环境。`));
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(environments).length === 0) {
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 configuredEnvs = Object.keys(environments);
1287
- let defaultEnv = configuredEnvs[0];
1288
- if (configuredEnvs.length > 1) {
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
- configuredEnvs.forEach((env, i) => {
1291
- console.log(` ${fmt('bold', String(i + 1))}) ${env}`);
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-${configuredEnvs.length}]: `));
1294
- const idx = parseInt(defaultAnswer, 10) - 1;
1295
- if (idx >= 0 && idx < configuredEnvs.length) {
1296
- defaultEnv = configuredEnvs[idx];
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: 'database 从日志自动提取(租户ID=数据库名),也可手动指定。使用时说\'连 dev 环境\'即可切换',
1350
+ _comment: '环境支持 range 字段(如 "1~15"),用户说"dev10"时自动匹配。查找顺序:本地 .claude/ > 全局 ~/.claude/',
1306
1351
  };
1307
1352
 
1308
- for (const t of targets) {
1309
- const dir = path.dirname(t.configPath);
1310
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1311
- fs.writeFileSync(t.configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
1312
- console.log(` ${fmt('green', '')} 已写入:${t.configPath}`);
1353
+ const configJson = JSON.stringify(config, null, 2) + '\n';
1354
+ // 写入主路径
1355
+ const dir = path.dirname(configPath);
1356
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1357
+ fs.writeFileSync(configPath, configJson, 'utf-8');
1358
+ console.log(` ${fmt('green', '✔')} 已写入:${configPath}`);
1359
+
1360
+ // 全局模式:同时写入 ~/.cursor/(如果目录存在)
1361
+ if (isGlobal) {
1362
+ const cursorConfigPath = path.join(HOME_DIR, '.cursor', 'mysql-config.json');
1363
+ if (fs.existsSync(path.join(HOME_DIR, '.cursor'))) {
1364
+ fs.writeFileSync(cursorConfigPath, configJson, 'utf-8');
1365
+ console.log(` ${fmt('green', '✔')} 已同步:${cursorConfigPath}`);
1366
+ }
1367
+ } else {
1368
+ ensureGitignore(['mysql-config.json']);
1313
1369
  }
1314
1370
 
1315
- // 确保 .gitignore
1316
- ensureGitignore(['mysql-config.json']);
1317
-
1318
1371
  console.log('');
1319
1372
  console.log(fmt('green', 'MySQL 数据库配置完成!'));
1320
- console.log(`使用 ${fmt('bold', 'mysql-debug')} 技能时将自动读取此配置。`);
1373
+ for (const [key, env] of Object.entries(newEnvironments)) {
1374
+ if (env.range) {
1375
+ console.log(fmt('cyan', ` ${key} 覆盖 ${key}${env.range.replace('~', '→')},说"${key}10"将自动匹配`));
1376
+ }
1377
+ }
1321
1378
  }
1322
1379
 
1323
1380
  // ── Loki 日志查询配置 ──────────────────────────────────────────────────────
1324
1381
 
1325
- async function runLokiConfig(ask) {
1382
+ async function runLokiConfig(ask, isGlobal) {
1326
1383
  console.log(fmt('blue', fmt('bold', '┌─ Loki 日志查询配置 ─┐')));
1327
1384
  console.log('');
1328
1385
 
1329
- // Loki 配置存放在 skills/loki-log-query/environments.json
1330
- const targets = detectLokiConfigTargets();
1386
+ const configPath = isGlobal
1387
+ ? path.join(HOME_DIR, '.claude', 'loki-config.json')
1388
+ : getLokiConfigPath();
1331
1389
 
1332
- if (targets.length === 0) {
1333
- console.log(fmt('yellow', '⚠ 未检测到 loki-log-query 技能目录。请先运行 init 安装框架。'));
1390
+ if (!configPath) {
1391
+ console.log(fmt('yellow', '⚠ 未检测到配置目录。请先运行 init 安装框架。'));
1334
1392
  return;
1335
1393
  }
1336
1394
 
1337
- // 读取已有配置作为模板
1338
1395
  let existingConfig = null;
1339
- for (const t of targets) {
1340
- if (fs.existsSync(t.configPath)) {
1341
- try {
1342
- existingConfig = JSON.parse(fs.readFileSync(t.configPath, 'utf-8'));
1343
- break;
1344
- } catch { /* ignore */ }
1345
- }
1396
+ if (fs.existsSync(configPath)) {
1397
+ try { existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch { /* ignore */ }
1346
1398
  }
1347
1399
 
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
1400
  if (existingConfig && existingConfig.environments) {
1372
1401
  const envs = existingConfig.environments;
1373
1402
  const envList = Object.keys(envs);
@@ -1376,182 +1405,201 @@ async function runLokiConfig(ask) {
1376
1405
  for (const key of envList) {
1377
1406
  const env = envs[key];
1378
1407
  const hasToken = env.token && env.token.length > 0;
1379
- const status = hasToken ? fmt('green', '✔ 已配置 Token') : fmt('red', '✗ 缺少 Token');
1380
- console.log(` ${fmt('bold', key)} ${env.name || key} ${status}`);
1381
- if (env.url) console.log(` URL: ${env.url}`);
1408
+ const status = hasToken ? fmt('green', '✔ Token') : fmt('red', '✗ Token');
1409
+ const rangeStr = env.range ? fmt('magenta', ` (range: ${env.range})`) : '';
1410
+ console.log(` ${fmt('bold', key)} — ${env.name || key} ${status}${rangeStr}`);
1382
1411
  }
1383
1412
  console.log('');
1384
1413
 
1385
- // 检查是否所有环境都已有 Token
1386
- const missingTokenEnvs = envList.filter(k => !envs[k].token);
1387
- if (missingTokenEnvs.length === 0) {
1388
- const reconfig = await ask(fmt('bold', '所有环境已配置 Token,是否重新配置?[y/N]: '));
1389
- if (reconfig.toLowerCase() !== 'y') {
1414
+ if (configAdd) {
1415
+ console.log(fmt('green', '将追加新环境到已有配置。'));
1416
+ } else {
1417
+ const missingTokenEnvs = envList.filter(k => !envs[k].token);
1418
+ const action = await ask(fmt('bold', '输入 token 补充Token,add 追加环境,N 跳过 [token/add/N]: ')) || 'token';
1419
+ if (action.toLowerCase() === 'token') {
1420
+ await updateLokiTokens(ask, existingConfig, configPath, isGlobal);
1421
+ return;
1422
+ } else if (action.toLowerCase() !== 'add') {
1390
1423
  console.log('已跳过 Loki 配置。');
1391
1424
  return;
1392
1425
  }
1393
- } else {
1394
- console.log(fmt('yellow', `有 ${missingTokenEnvs.length} 个环境缺少 Token,将引导配置。`));
1395
1426
  }
1396
1427
  console.log('');
1397
1428
 
1398
- // 为已有环境补充 Token
1399
- console.log(fmt('cyan', fmt('bold', 'Grafana Token 获取步骤:')));
1400
- console.log(` 1. 打开 Grafana 管理后台`);
1401
- console.log(` 2. 进入 Administration → Service accounts`);
1402
- console.log(` 3. 创建 Service Account(角色选 ${fmt('bold', 'Viewer')})`);
1403
- console.log(` 4. 点击 Add token → 复制 Token`);
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
- }
1429
+ // 追加环境
1430
+ await addLokiEnvironments(ask, existingConfig, configPath, isGlobal);
1431
+ } else {
1432
+ await createLokiConfig(ask, configPath, isGlobal);
1433
+ }
1434
+ }
1424
1435
 
1425
- // 选择默认环境
1426
- console.log(fmt('cyan', '请选择默认活跃环境:'));
1427
- envList.forEach((env, i) => {
1428
- console.log(` ${fmt('bold', String(i + 1))}) ${env} ${envs[env].name || env}`);
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('');
1436
+ async function updateLokiTokens(ask, config, configPath, isGlobal) {
1437
+ console.log('');
1438
+ console.log(fmt('cyan', fmt('bold', 'Grafana Token 获取:')));
1439
+ console.log(` Grafana Administration Service accounts Add(Viewer)→ Add token`);
1440
+ console.log('');
1434
1441
 
1435
- // 写入配置
1436
- existingConfig.active = activeEnv;
1437
- for (const t of targets) {
1438
- const dir = path.dirname(t.configPath);
1439
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1440
- fs.writeFileSync(t.configPath, JSON.stringify(existingConfig, null, 2) + '\n', 'utf-8');
1441
- console.log(` ${fmt('green', '')} 已写入:${t.configPath}`);
1442
+ const envs = config.environments;
1443
+ for (const key of Object.keys(envs)) {
1444
+ const env = envs[key];
1445
+ const hasToken = env.token && env.token.length > 0;
1446
+ if (hasToken) {
1447
+ const update = await ask(` ${fmt('bold', key)} 已有 Token,更新?[y/N]: `);
1448
+ if (update.toLowerCase() !== 'y') continue;
1449
+ }
1450
+ if (env.url) console.log(` URL: ${fmt('bold', env.url)}`);
1451
+ const token = await ask(` 输入 ${key} 的 Token: `);
1452
+ if (token) {
1453
+ envs[key].token = token;
1454
+ console.log(` ${fmt('green', '✔')} 已设置`);
1442
1455
  }
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
1456
  console.log('');
1457
+ }
1451
1458
 
1452
- console.log(fmt('cyan', fmt('bold', 'Grafana Token 获取步骤:')));
1453
- console.log(` 1. 打开 Grafana 管理后台`);
1454
- console.log(` 2. 进入 Administration → Service accounts`);
1455
- console.log(` 3. 创建 Service Account(角色选 ${fmt('bold', 'Viewer')})`);
1456
- console.log(` 4. 点击 Add token → 复制 Token`);
1459
+ writeLokiConfig(config, configPath, isGlobal);
1460
+ }
1461
+
1462
+ async function addLokiEnvironments(ask, config, configPath, isGlobal) {
1463
+ printLokiTokenGuide();
1464
+ const countAnswer = await ask(fmt('bold', '要追加几个环境?[1]: ')) || '1';
1465
+ const count = Math.max(1, Math.min(10, parseInt(countAnswer, 10) || 1));
1466
+ console.log('');
1467
+
1468
+ for (let i = 0; i < count; i++) {
1469
+ const envData = await collectLokiEnvInput(ask, i + 1, count);
1470
+ if (!envData) continue;
1471
+ config.environments[envData.key] = envData.value;
1472
+ }
1473
+
1474
+ writeLokiConfig(config, configPath, isGlobal);
1475
+ }
1476
+
1477
+ async function createLokiConfig(ask, configPath, isGlobal) {
1478
+ console.log(fmt('cyan', '将创建新的 Loki 日志查询配置。'));
1479
+ console.log('');
1480
+ printLokiTokenGuide();
1481
+ const countAnswer = await ask(fmt('bold', '要配置几个 Grafana 环境?[1]: ')) || '1';
1482
+ const count = Math.max(1, Math.min(10, parseInt(countAnswer, 10) || 1));
1483
+ console.log('');
1484
+
1485
+ const environments = {};
1486
+ let activeEnv = '';
1487
+
1488
+ for (let i = 0; i < count; i++) {
1489
+ const envData = await collectLokiEnvInput(ask, i + 1, count);
1490
+ if (!envData) continue;
1491
+ environments[envData.key] = envData.value;
1492
+ if (!activeEnv) activeEnv = envData.key;
1493
+ }
1494
+
1495
+ if (Object.keys(environments).length === 0) {
1496
+ console.error(fmt('red', '未配置任何环境。'));
1497
+ return;
1498
+ }
1499
+
1500
+ const envKeys = Object.keys(environments);
1501
+ if (envKeys.length > 1) {
1502
+ console.log(fmt('cyan', '请选择默认活跃环境:'));
1503
+ envKeys.forEach((env, i) => console.log(` ${fmt('bold', String(i + 1))}) ${env}`));
1504
+ const activeAnswer = await ask(fmt('bold', `请输入选项 [1-${envKeys.length}]: `));
1505
+ const idx = parseInt(activeAnswer, 10) - 1;
1506
+ if (idx >= 0 && idx < envKeys.length) activeEnv = envKeys[idx];
1457
1507
  console.log('');
1508
+ }
1458
1509
 
1459
- const environments = {};
1460
- let activeEnv = '';
1510
+ const config = {
1511
+ _comment: 'Loki 多环境配置。环境支持 range 字段。查找顺序:本地 > 全局 ~/.claude/',
1512
+ _setup: 'Token:Grafana → Administration → Service accounts → Add(Viewer)→ Add token',
1513
+ active: activeEnv,
1514
+ environments,
1515
+ };
1461
1516
 
1462
- for (let i = 0; i < count; i++) {
1463
- console.log(fmt('cyan', `── 环境 ${i + 1}/${count} ──`));
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('');
1517
+ writeLokiConfig(config, configPath, isGlobal);
1518
+ }
1475
1519
 
1476
- environments[envKey] = { name, url, token: token || '', aliases, projects: [] };
1477
- if (!activeEnv) activeEnv = envKey;
1478
- }
1520
+ function printLokiTokenGuide() {
1521
+ console.log(fmt('cyan', fmt('bold', 'Grafana Token 获取:')));
1522
+ console.log(` Grafana → Administration → Service accounts → Add(Viewer)→ Add token`);
1523
+ console.log('');
1524
+ }
1479
1525
 
1480
- if (Object.keys(environments).length === 0) {
1481
- console.error(fmt('red', '未配置任何环境。'));
1482
- return;
1483
- }
1526
+ async function collectLokiEnvInput(ask, index, total) {
1527
+ console.log(fmt('cyan', `── 环境 ${index}/${total} ──`));
1528
+ const envKey = await ask(` 环境标识(如 monitor-dev、test13): `);
1529
+ if (!envKey) { console.log(fmt('yellow', ' 跳过')); return null; }
1530
+ const name = await ask(` 环境名称(如 "开发环境"): `) || envKey;
1531
+ const url = await ask(` Grafana URL: `);
1532
+ const token = await ask(` Token(可留空稍后配): `);
1533
+ const aliasStr = await ask(` 别名(逗号分隔)[${envKey}]: `) || envKey;
1534
+ const aliases = aliasStr.split(',').map(s => s.trim()).filter(Boolean);
1535
+ const rangeInput = await ask(` 项目覆盖范围(如 ${fmt('bold', 'dev1~15')} 表示 dev01→dev15,留空=无范围): `);
1536
+ console.log('');
1484
1537
 
1485
- // 选择默认
1486
- const envKeys = Object.keys(environments);
1487
- if (envKeys.length > 1) {
1488
- console.log(fmt('cyan', '请选择默认活跃环境:'));
1489
- envKeys.forEach((env, i) => {
1490
- console.log(` ${fmt('bold', String(i + 1))}) ${env}`);
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];
1538
+ const value = { name, url, token: token || '', aliases };
1539
+ if (rangeInput) {
1540
+ value.range = rangeInput;
1541
+ value.projects = expandRange(rangeInput);
1542
+ if (value.projects.length > 0) {
1543
+ console.log(fmt('cyan', ` 已展开 ${value.projects.length} 个项目:${value.projects.slice(0, 5).join(', ')}${value.projects.length > 5 ? '...' : ''}`));
1495
1544
  console.log('');
1496
1545
  }
1497
-
1498
- const config = {
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
- }
1546
+ } else {
1547
+ value.projects = [];
1512
1548
  }
1513
1549
 
1514
- // 确保 .gitignore
1515
- ensureGitignore(['loki-log-query/environments.json']);
1550
+ return { key: envKey, value };
1551
+ }
1516
1552
 
1553
+ function writeLokiConfig(config, configPath, isGlobal) {
1554
+ const configJson = JSON.stringify(config, null, 2) + '\n';
1555
+ const dir = path.dirname(configPath);
1556
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1557
+ fs.writeFileSync(configPath, configJson, 'utf-8');
1558
+ console.log(` ${fmt('green', '✔')} 已写入:${configPath}`);
1559
+
1560
+ // 全局模式:同时写入 ~/.cursor/(如果目录存在)
1561
+ if (isGlobal) {
1562
+ const cursorConfigPath = path.join(HOME_DIR, '.cursor', 'loki-config.json');
1563
+ if (fs.existsSync(path.join(HOME_DIR, '.cursor'))) {
1564
+ fs.writeFileSync(cursorConfigPath, configJson, 'utf-8');
1565
+ console.log(` ${fmt('green', '✔')} 已同步:${cursorConfigPath}`);
1566
+ }
1567
+ } else {
1568
+ ensureGitignore(['loki-config.json', 'environments.json']);
1569
+ }
1517
1570
  console.log('');
1518
1571
  console.log(fmt('green', 'Loki 日志查询配置完成!'));
1519
- console.log(`使用 ${fmt('bold', 'loki-log-query')} 技能时将自动读取此配置。`);
1520
1572
  }
1521
1573
 
1522
1574
  // ── Config 工具函数 ─────────────────────────────────────────────────────────
1523
1575
 
1524
- /** 检测已安装工具目录,返回 MySQL 配置文件路径列表 */
1525
- function detectConfigTargets(filename) {
1526
- const targets = [];
1527
- // MySQL 配置统一放在 .claude/ 下,Cursor 技能也引用 .claude/mysql-config.json
1528
- const claudePath = path.join(targetDir, '.claude', filename);
1529
-
1530
- if (fs.existsSync(path.join(targetDir, '.claude'))) {
1531
- targets.push({ tool: 'claude', configPath: claudePath });
1532
- } else if (fs.existsSync(path.join(targetDir, '.cursor'))) {
1533
- // 如果只有 Cursor 没有 Claude,也写到 .claude/ 下(技能引用此路径)
1534
- targets.push({ tool: 'cursor', configPath: claudePath });
1535
- }
1536
- return targets;
1576
+ function getLokiConfigPath() {
1577
+ const lokiJsonClaude = path.join(targetDir, '.claude', 'loki-config.json');
1578
+ if (fs.existsSync(lokiJsonClaude)) return lokiJsonClaude;
1579
+ const envJsonClaude = path.join(targetDir, '.claude', 'skills', 'loki-log-query', 'environments.json');
1580
+ if (fs.existsSync(envJsonClaude)) return envJsonClaude;
1581
+ const envJsonCursor = path.join(targetDir, '.cursor', 'skills', 'loki-log-query', 'environments.json');
1582
+ if (fs.existsSync(envJsonCursor)) return envJsonCursor;
1583
+ if (fs.existsSync(path.join(targetDir, '.claude'))) return lokiJsonClaude;
1584
+ if (fs.existsSync(path.join(targetDir, '.cursor'))) return path.join(targetDir, '.cursor', 'loki-config.json');
1585
+ return null;
1537
1586
  }
1538
1587
 
1539
- /** 检测 Loki 配置文件路径列表 */
1540
- function detectLokiConfigTargets() {
1541
- const targets = [];
1542
- const claudePath = path.join(targetDir, '.claude', 'skills', 'loki-log-query', 'environments.json');
1543
- const cursorPath = path.join(targetDir, '.cursor', 'skills', 'loki-log-query', 'environments.json');
1544
-
1545
- if (fs.existsSync(path.join(targetDir, '.claude', 'skills', 'loki-log-query'))) {
1546
- targets.push({ tool: 'claude', configPath: claudePath });
1547
- }
1548
- if (fs.existsSync(path.join(targetDir, '.cursor', 'skills', 'loki-log-query'))) {
1549
- targets.push({ tool: 'cursor', configPath: cursorPath });
1550
- }
1551
- return targets;
1588
+ /** 展开范围字符串为项目名列表,如 "dev1~15" ["dev01","dev02",...,"dev15"] */
1589
+ function expandRange(rangeStr) {
1590
+ const match = rangeStr.match(/^([a-zA-Z-]*)(\d+)\s*[~~]\s*(\d+)$/);
1591
+ if (!match) return [];
1592
+ const prefix = match[1];
1593
+ const start = parseInt(match[2], 10);
1594
+ const end = parseInt(match[3], 10);
1595
+ const maxDigits = Math.max(String(start).length, String(end).length, 2);
1596
+ const result = [];
1597
+ for (let i = start; i <= end; i++) {
1598
+ result.push(prefix + String(i).padStart(maxDigits, '0'));
1599
+ }
1600
+ return result;
1552
1601
  }
1553
1602
 
1554
- /** 解析选择字符串(如 "1,2" 或 "1-3") */
1555
1603
  function parseSelection(answer, names) {
1556
1604
  const selected = new Set();
1557
1605
  for (const part of answer.split(',')) {
@@ -1571,7 +1619,6 @@ function parseSelection(answer, names) {
1571
1619
  return [...selected];
1572
1620
  }
1573
1621
 
1574
- /** 确保敏感配置文件在 .gitignore 中 */
1575
1622
  function ensureGitignore(patterns) {
1576
1623
  const gitignorePath = path.join(targetDir, '.gitignore');
1577
1624
  let content = '';
@@ -1580,9 +1627,7 @@ function ensureGitignore(patterns) {
1580
1627
  }
1581
1628
  const lines = content.split('\n').map(l => l.trim());
1582
1629
  const toAdd = [];
1583
-
1584
1630
  for (const pattern of patterns) {
1585
- // 检查是否已有任意形式的忽略规则(精确路径、通配符等)
1586
1631
  const alreadyIgnored = lines.some(line =>
1587
1632
  line.endsWith(pattern) || line.endsWith(`/${pattern}`) || line === `**/${pattern}`
1588
1633
  );
@@ -1591,11 +1636,10 @@ function ensureGitignore(patterns) {
1591
1636
  toAdd.push(`**/${pattern}`);
1592
1637
  }
1593
1638
  }
1594
-
1595
1639
  if (toAdd.length > 0) {
1596
1640
  const separator = content.endsWith('\n') || content === '' ? '' : '\n';
1597
1641
  fs.appendFileSync(gitignorePath, `${separator}${toAdd.join('\n')}\n`, 'utf-8');
1598
- console.log(` ${fmt('green', '✔')} 已更新 .gitignore(防止提交敏感配置)`);
1642
+ console.log(` ${fmt('green', '✔')} 已更新 .gitignore`);
1599
1643
  }
1600
1644
  }
1601
1645