@lyra-ai/toolkit 0.2.1 → 0.2.3

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/src/status.mjs CHANGED
@@ -5,14 +5,15 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import os from 'node:os';
8
+ import { load as loadConfig, UPLOADED_PATH } from './shared/config.mjs';
8
9
 
9
10
  const HOME = os.homedir();
10
11
  const PLUGIN_DIR = path.join(HOME, '.claude', 'plugins', 'toolkit');
11
- const TOOLKIT_DIR = path.join(HOME, '.toolkit');
12
12
 
13
13
  export function status() {
14
14
  const pluginExists = fs.existsSync(path.join(PLUGIN_DIR, '.claude-plugin', 'plugin.json'));
15
- const configExists = fs.existsSync(path.join(TOOLKIT_DIR, 'config.json'));
15
+ const config = loadConfig();
16
+ const configExists = !!config.registry || !!config.uid;
16
17
 
17
18
  console.log('Toolkit Status');
18
19
  console.log('════════════════════════════════════════');
@@ -29,28 +30,26 @@ export function status() {
29
30
  console.log(` 版本: ${pj.version}`);
30
31
  }
31
32
 
32
- // 配置
33
+ // 配置(走 shared/config.mjs)
33
34
  if (configExists) {
34
- const config = JSON.parse(fs.readFileSync(path.join(TOOLKIT_DIR, 'config.json'), 'utf-8'));
35
35
  console.log(` 平台地址: ${config.registry}${config.contexts?.metrics || '/ai-metrics'}/`);
36
36
  console.log(` 身份 UID: ${config.uid?.slice(0, 12)}...`);
37
37
  }
38
38
 
39
39
  // 上传状态
40
- const uploadedPath = path.join(TOOLKIT_DIR, 'uploaded.json');
41
40
  try {
42
- const up = JSON.parse(fs.readFileSync(uploadedPath, 'utf-8'));
43
- console.log(` 已传会话: ${up.uploaded_sids?.length || 0} 个`);
44
- console.log(` 上次上传: ${up.last_flush || '从未'}`);
41
+ const up = JSON.parse(fs.readFileSync(UPLOADED_PATH, 'utf-8'));
42
+ console.log(` 已同步: ${up.uploaded_sids?.length || 0} 个会话`);
43
+ console.log(` 上次同步: ${up.last_flush || '从未'}`);
45
44
  } catch {
46
- console.log(' 上传状态: 未初始化');
45
+ console.log(' 同步状态: 未初始化');
47
46
  }
48
47
 
49
48
  // 待传会话计数
50
49
  const projectsDir = path.join(HOME, '.claude', 'projects');
51
50
  let pending = 0;
52
51
  if (fs.existsSync(projectsDir) && configExists) {
53
- const up = JSON.parse(fs.readFileSync(uploadedPath, 'utf-8'));
52
+ const up = JSON.parse(fs.readFileSync(UPLOADED_PATH, 'utf-8'));
54
53
  const uploadedSet = new Set(up.uploaded_sids || []);
55
54
  for (const projDir of fs.readdirSync(projectsDir)) {
56
55
  const pp = path.join(projectsDir, projDir);
@@ -62,6 +61,6 @@ export function status() {
62
61
  } catch {}
63
62
  }
64
63
  }
65
- console.log(` 待传会话: ${pending} 个`);
64
+ console.log(` 待同步: ${pending} 个会话`);
66
65
  console.log('════════════════════════════════════════');
67
66
  }
package/src/zentao.mjs CHANGED
@@ -62,7 +62,7 @@ const HELP = `zentao-cli —— 面向 AI agent 的禅道 CLI
62
62
  bug browse-types # 列出 sync --status 的合法 browseType
63
63
  config 列出所有配置(密码脱敏)
64
64
  config get <key> 取单个配置值(ZENTAO_PASSWORD 需 --raw 才显原文)
65
- config set <key> <v> 写入 ~/.zentao-cli/.env
65
+ config set <key> <v> 写入 ~/.toolkit/config.json(同时写 .env 兼容)
66
66
  config unset <key> 删除配置项
67
67
  config path 输出配置文件路径
68
68
  init 向导式初始化配置(交互提示填 URL/用户/密码等)
@@ -71,8 +71,8 @@ const HELP = `zentao-cli —— 面向 AI agent 的禅道 CLI
71
71
  ping 验证连通性 + 凭据
72
72
 
73
73
  通用: --pretty(人读) --raw(原始 JSON) 默认输出规整 JSON
74
- 配置: ZENTAO_URL / ZENTAO_USERNAME / ZENTAO_PASSWORD(必需),见 .env.example
75
- 支持: ZENTAO_DEFAULT_PROJECT / ZENTAO_TOKEN_TTL / ZENTAO_LOG_LEVEL / ZENTAO_CACHE_STALE_MS
74
+ 配置: 统一存 ~/.toolkit/config.json zentao 段(也支持 ~/.zentao-cli/.env 兼容)
75
+ toolkit config set zentao.url <url> / zentao.username <u> / zentao.password <p>
76
76
  `;
77
77
 
78
78
  function fileTokenStore(cachePath) {
@@ -91,6 +91,15 @@ function loadFilesText() {
91
91
  return files; // 项目级在前(优先级高)
92
92
  }
93
93
 
94
+ // 从 ~/.toolkit/config.json → zentao 段读配置(优先级高于 .env)
95
+ function loadToolkitZentaoConfig() {
96
+ try {
97
+ const configPath = path.join(os.homedir(), '.toolkit', 'config.json');
98
+ const raw = fs.readFileSync(configPath, 'utf8');
99
+ return JSON.parse(raw).zentao || {};
100
+ } catch { return {}; }
101
+ }
102
+
94
103
  function main() {
95
104
  const argv = process.argv.slice(2);
96
105
  const parsed = parseArgs(argv);
@@ -139,18 +148,28 @@ export function parseEnvFile(text) {
139
148
 
140
149
  const REQUIRED = ['ZENTAO_URL', 'ZENTAO_USERNAME', 'ZENTAO_PASSWORD'];
141
150
 
142
- export function loadConfig({ env, filesText }, { required } = {}) {
143
- // filesText: 数组,靠前优先级高(项目级 .env 在前,用户级在后)
151
+ export function loadConfig({ env, filesText, toolkitZentao }, { required } = {}) {
152
+ // 优先级(高→低): OS env > 项目 .env > 用户 .env > ~/.toolkit/config.json → zentao 段
144
153
  const merged = {};
154
+
155
+ // 1. toolkit config.json → zentao 段(最低优先级,作为底)
156
+ const tkZentao = toolkitZentao !== undefined ? toolkitZentao : loadToolkitZentaoConfig();
157
+ for (const [camel, upper] of Object.entries(ZENTAO_KEY_MAP)) {
158
+ if (tkZentao[camel] != null) merged[upper] = String(tkZentao[camel]);
159
+ }
160
+
161
+ // 2. .env 文件(项目级 > 用户级)
145
162
  for (let i = filesText.length - 1; i >= 0; i--) {
146
163
  Object.assign(merged, parseEnvFile(filesText[i]));
147
164
  }
148
- Object.assign(merged, env); // env 优先级最高
165
+
166
+ // 3. OS env(最高优先级)
167
+ Object.assign(merged, env);
149
168
 
150
169
  const requiredKeys = required !== false ? REQUIRED : [];
151
170
  const missing = requiredKeys.filter((k) => !merged[k]);
152
171
  if (missing.length) {
153
- throw new ZentaoError('CONFIG_ERROR', `缺少必需环境变量: ${missing.join(', ')}(见 .env.example)`);
172
+ throw new ZentaoError('CONFIG_ERROR', `缺少必需配置: ${missing.join(', ')}(toolkit config set zentao.url / zentao.username / zentao.password 或编辑 .env)`);
154
173
  }
155
174
  return {
156
175
  zentaoUrl: String(merged.ZENTAO_URL || '').replace(/\/+$/, ''),
@@ -165,12 +184,27 @@ export function loadConfig({ env, filesText }, { required } = {}) {
165
184
  // ---- config 读写(操作 ~/.zentao-cli/.env)----
166
185
 
167
186
  export function configFilePath() {
187
+ // .env 文件路径(兼容读写);toolkit config.json 通过 loadToolkitZentaoConfig 读
168
188
  return path.join(os.homedir(), '.zentao-cli', '.env');
169
189
  }
170
190
 
191
+ export function toolkitConfigPath() {
192
+ return path.join(os.homedir(), '.toolkit', 'config.json');
193
+ }
194
+
171
195
  // 已知配置键(用于 list 展示顺序与 set 校验提示)
172
196
  const KNOWN_KEYS = ['ZENTAO_URL', 'ZENTAO_USERNAME', 'ZENTAO_PASSWORD', 'ZENTAO_DEFAULT_PROJECT', 'ZENTAO_TOKEN_TTL', 'ZENTAO_LOG_LEVEL', 'ZENTAO_CACHE_STALE_MS'];
173
197
 
198
+ // toolkit config.json 键名映射(camelCase ↔ UPPER_CASE)
199
+ const ZENTAO_KEY_MAP = {
200
+ 'url': 'ZENTAO_URL',
201
+ 'username': 'ZENTAO_USERNAME',
202
+ 'password': 'ZENTAO_PASSWORD',
203
+ 'default-project': 'ZENTAO_DEFAULT_PROJECT',
204
+ 'token-ttl': 'ZENTAO_TOKEN_TTL',
205
+ 'cache-stale-ms': 'ZENTAO_CACHE_STALE_MS',
206
+ };
207
+
174
208
  // 敏感键:list/get 默认脱敏
175
209
  const SENSITIVE_KEYS = new Set(['ZENTAO_PASSWORD']);
176
210
 
@@ -499,9 +533,23 @@ function cmdConfigSet(parsed, filesystem) {
499
533
  const key = parsed.positional[0];
500
534
  const value = parsed.positional[1];
501
535
  if (!key || value === undefined) return { stderr: '用法:zentao config set <key> <value>', exitCode: 2 };
502
- const keyUpper = key.toUpperCase();
536
+ // 接受 ZENTAO_URL 或 url 或 URL 两种写法
537
+ const keyUpper = key.toUpperCase().replace(/^ZENTAO_/, 'ZENTAO_').startsWith('ZENTAO_') ? key.toUpperCase() : `ZENTAO_${key.toUpperCase()}`;
503
538
  if (!KNOWN_KEYS.includes(keyUpper)) {
504
- return { stderr: `未知配置项: ${key}(合法值: ${KNOWN_KEYS.join(', ')})。如需自定义变量请直接编辑 .env`, exitCode: 2 };
539
+ return { stderr: `未知配置项: ${key}(合法值: ${KNOWN_KEYS.join(', ')})。也可用 toolkit config set zentao.url <value>`, exitCode: 2 };
540
+ }
541
+ // 优先写 ~/.toolkit/config.json → zentao 段;同时也写 .env(兼容旧逻辑)
542
+ const camelKey = Object.entries(ZENTAO_KEY_MAP).find(([_, u]) => u === keyUpper)?.[0];
543
+ if (camelKey) {
544
+ try {
545
+ const configPath = path.join(os.homedir(), '.toolkit', 'config.json');
546
+ let tkConfig = {};
547
+ try { tkConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
548
+ if (!tkConfig.zentao) tkConfig.zentao = {};
549
+ tkConfig.zentao[camelKey] = value;
550
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
551
+ fs.writeFileSync(configPath, JSON.stringify(tkConfig, null, 2));
552
+ } catch { /* 写 toolkit config 失败不影响 .env */ }
505
553
  }
506
554
  setConfigKey(keyUpper, value, filesystem);
507
555
  return { stdout: JSON.stringify({ set: keyUpper, value: maskIfSensitive(keyUpper, value) }, null, 2) };
@@ -516,7 +564,7 @@ function cmdConfigUnset(parsed, filesystem) {
516
564
  }
517
565
 
518
566
  function cmdConfigPath() {
519
- return { stdout: configFilePath() };
567
+ return { stdout: toolkitConfigPath() + ' (zentao 段) / ' + configFilePath() + ' (.env 兼容)' };
520
568
  }
521
569
 
522
570
  // ---- init 向导 ----