claude360 0.2.7 → 0.2.9

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.
@@ -9,8 +9,13 @@ import path from "node:path";
9
9
 
10
10
  import { createBackup } from "./backup.js";
11
11
  import { upsertMarkedBlock } from "./mcp-skill.js";
12
- import { upsertProfileKey, validateBasicToml } from "./tool-launcher.js";
12
+ import { upsertTopLevelKey, validateBasicToml } from "./tool-launcher.js";
13
13
  import { renderModelTable } from "./ui.js";
14
+ import { colorLevel } from "./colors.js";
15
+ import { createMessenger } from "./messages.js";
16
+
17
+ // 语义消息器工厂:真实终端着色,测试/管道(writeLine 被替换)下无色。
18
+ const mk = (writeLine) => createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
14
19
 
15
20
  const defaultFs = { copyFile, cp, mkdir, readFile, stat, writeFile };
16
21
 
@@ -36,10 +41,11 @@ async function readFileIfExists(fs, filePath) {
36
41
  // 写入 Markdown 标记区块:先备份目标文件到 baseDir/backup/backup_<时间戳>/,
37
42
  // 再以 claude360 标记区块幂等更新,不触碰用户已有内容。
38
43
  async function writeMarkedBlock({ baseDir, filePath, blockId, content, writeLine, fs, now }) {
44
+ const msg = mk(writeLine);
39
45
  const current = await readFileIfExists(fs, filePath);
40
46
  const { backupDir } = await createBackup({ baseDir, paths: [filePath], fs, now });
41
47
  if (backupDir) {
42
- writeLine(`✓ 已创建备份:${backupDir}`);
48
+ msg.success(`已创建备份:${backupDir}`);
43
49
  }
44
50
  await fs.mkdir(path.dirname(filePath), { recursive: true });
45
51
  await fs.writeFile(filePath, upsertMarkedBlock(current, blockId, content), "utf8");
@@ -85,12 +91,13 @@ export async function configureOutputLanguage({
85
91
  if (typeof promptSelect !== "function") {
86
92
  throw new Error("缺少选择输入");
87
93
  }
94
+ const msg = mk(writeLine);
88
95
  const selected = await promptSelect("请选择 AI 输出语言:", [
89
96
  ...OUTPUT_LANGUAGES.map((language) => ({ label: language.label, value: language.id })),
90
97
  { label: "跳过", value: "skip" },
91
98
  ]);
92
99
  if (selected === "skip") {
93
- writeLine("已跳过 AI 输出语言配置。");
100
+ msg.info("已跳过 AI 输出语言配置。");
94
101
  return { skipped: true };
95
102
  }
96
103
  const language = OUTPUT_LANGUAGES.find((item) => item.id === selected);
@@ -103,7 +110,7 @@ export async function configureOutputLanguage({
103
110
  fs,
104
111
  now,
105
112
  });
106
- writeLine(`✓ AI 输出语言已写入:${filePath}`);
113
+ msg.success(`AI 输出语言已写入:${filePath}`);
107
114
  return { skipped: false, language: language.id };
108
115
  }
109
116
 
@@ -164,8 +171,9 @@ export async function configurePromptStyle({
164
171
  if (typeof promptSelect !== "function") {
165
172
  throw new Error("缺少选择输入");
166
173
  }
174
+ const msg = mk(writeLine);
167
175
  for (const style of PROMPT_STYLES) {
168
- writeLine(`${style.label.trim()}:${style.desc}`);
176
+ msg.info(`${style.label.trim()}:${style.desc}`);
169
177
  }
170
178
  writeLine("");
171
179
  const selected = await promptSelect("请选择系统提示词风格:", [
@@ -173,7 +181,7 @@ export async function configurePromptStyle({
173
181
  { label: "跳过", value: "skip" },
174
182
  ]);
175
183
  if (selected === "skip") {
176
- writeLine("已跳过系统提示词风格配置。");
184
+ msg.info("已跳过系统提示词风格配置。");
177
185
  return { skipped: true };
178
186
  }
179
187
  const style = PROMPT_STYLES.find((item) => item.id === selected);
@@ -186,7 +194,7 @@ export async function configurePromptStyle({
186
194
  fs,
187
195
  now,
188
196
  });
189
- writeLine(`✓ 系统提示词风格已写入:${filePath}`);
197
+ msg.success(`系统提示词风格已写入:${filePath}`);
190
198
  return { skipped: false, style: style.id };
191
199
  }
192
200
 
@@ -252,9 +260,10 @@ async function readJsonIfExists(fs, filePath) {
252
260
  }
253
261
 
254
262
  async function writeJsonWithBackup({ baseDir, filePath, json, writeLine, fs, now }) {
263
+ const msg = mk(writeLine);
255
264
  const { backupDir } = await createBackup({ baseDir, paths: [filePath], fs, now });
256
265
  if (backupDir) {
257
- writeLine(`✓ 已创建备份:${backupDir}`);
266
+ msg.success(`已创建备份:${backupDir}`);
258
267
  }
259
268
  await fs.mkdir(path.dirname(filePath), { recursive: true });
260
269
  await fs.writeFile(filePath, `${JSON.stringify(json, null, 2)}\n`, "utf8");
@@ -271,22 +280,23 @@ export async function configureClaudeDefaultModel({
271
280
  if (typeof promptSelect !== "function") {
272
281
  throw new Error("缺少选择输入");
273
282
  }
283
+ const msg = mk(writeLine);
274
284
  const { models, source } = await loadAvailableModels(api, "claude_code");
275
285
  let list = models;
276
286
  if (source !== "backend") {
277
- writeLine("Claude360 后端暂未提供模型列表,以下为 Claude Code 官方模型别名:");
287
+ msg.info("Claude360 后端暂未提供模型列表,以下为 Claude Code 官方模型别名:");
278
288
  list = FALLBACK_CLAUDE_ALIASES;
279
289
  }
280
290
  const selected = await selectModelFromList({ models: list, promptSelect, writeLine });
281
291
  if (selected === "skip") {
282
- writeLine("已跳过默认模型配置。");
292
+ msg.info("已跳过默认模型配置。");
283
293
  return { skipped: true };
284
294
  }
285
295
  const settingsPath = path.join(claudeDir, "settings.json");
286
296
  const settings = await readJsonIfExists(fs, settingsPath);
287
297
  settings.model = selected;
288
298
  await writeJsonWithBackup({ baseDir: claudeDir, filePath: settingsPath, json: settings, writeLine, fs, now });
289
- writeLine(`✓ 默认模型已设置为 ${selected}(${settingsPath})`);
299
+ msg.success(`默认模型已设置为 ${selected}(${settingsPath})`);
290
300
  return { skipped: false, model: selected };
291
301
  }
292
302
 
@@ -328,8 +338,9 @@ export async function importClaudeEnvPermissions({
328
338
  if (typeof confirm !== "function") {
329
339
  throw new Error("缺少确认输入");
330
340
  }
341
+ const msg = mk(writeLine);
331
342
  const settingsPath = path.join(claudeDir, "settings.json");
332
- writeLine([
343
+ msg.info([
333
344
  "导入推荐环境变量和权限配置",
334
345
  "",
335
346
  "该操作将为 Claude Code 写入推荐配置:",
@@ -341,7 +352,7 @@ export async function importClaudeEnvPermissions({
341
352
  ].join("\n"));
342
353
  const approved = await confirm("是否继续?");
343
354
  if (!approved) {
344
- writeLine("已跳过推荐环境变量和权限配置。");
355
+ msg.info("已跳过推荐环境变量和权限配置。");
345
356
  return { skipped: true };
346
357
  }
347
358
  const settings = await readJsonIfExists(fs, settingsPath);
@@ -350,7 +361,7 @@ export async function importClaudeEnvPermissions({
350
361
  const allow = new Set([...(settings.permissions?.allow || []), ...recommended.permissions.allow]);
351
362
  settings.permissions = { ...(settings.permissions || {}), allow: [...allow] };
352
363
  await writeJsonWithBackup({ baseDir: claudeDir, filePath: settingsPath, json: settings, writeLine, fs, now });
353
- writeLine(`✓ 推荐环境变量与权限配置已写入:${settingsPath}`);
364
+ msg.success(`推荐环境变量与权限配置已写入:${settingsPath}`);
354
365
  return { skipped: false, path: settingsPath };
355
366
  }
356
367
 
@@ -371,31 +382,34 @@ export async function configureCodexDefaultModel({
371
382
  if (typeof promptSelect !== "function") {
372
383
  throw new Error("缺少选择输入");
373
384
  }
385
+ const msg = mk(writeLine);
374
386
  const { models, source } = await loadAvailableModels(api, "codex");
375
387
  if (source !== "backend") {
376
- writeLine("Claude360 后端暂未提供 Codex 模型列表,已跳过默认模型写入。");
377
- writeLine("可在启动 Codex 后使用 /model 命令选择模型。");
388
+ msg.info("Claude360 后端暂未提供 Codex 模型列表,已跳过默认模型写入。");
389
+ msg.hint("可在启动 Codex 后使用 /model 命令选择模型。");
378
390
  return { skipped: true };
379
391
  }
380
392
  const selected = await selectModelFromList({ models, promptSelect, writeLine });
381
393
  if (selected === "skip") {
382
- writeLine("已跳过默认模型配置。");
394
+ msg.info("已跳过默认模型配置。");
383
395
  return { skipped: true };
384
396
  }
385
- const configPath = path.join(codexDir, "config.toml");
386
- const current = await readFileIfExists(fs, configPath);
387
- const next = `${upsertProfileKey(current, "claude360", "model", selected).trimEnd()}\n`;
397
+ // Codex profile v2:模型写入 profile 覆盖层 claude360.config.toml 的顶层 model 键,
398
+ // 不再写主 config.toml [profiles.claude360] 表(新版 Codex 拒绝其与 --profile 共存)。
399
+ const profileConfigPath = path.join(codexDir, "claude360.config.toml");
400
+ const current = await readFileIfExists(fs, profileConfigPath);
401
+ const next = `${upsertTopLevelKey(current, "model", selected).trimEnd()}\n`;
388
402
  const tomlError = validateBasicToml(next);
389
403
  if (tomlError) {
390
- writeLine( 生成的 Codex 配置 TOML 校验失败:${tomlError}\n已放弃写入,原配置保持不变。`);
404
+ msg.error(`生成的 Codex 配置 TOML 校验失败:${tomlError}\n已放弃写入,原配置保持不变。`);
391
405
  return { skipped: true };
392
406
  }
393
- const { backupDir } = await createBackup({ baseDir: codexDir, paths: [configPath], fs, now });
407
+ const { backupDir } = await createBackup({ baseDir: codexDir, paths: [profileConfigPath], fs, now });
394
408
  if (backupDir) {
395
- writeLine(`✓ 已创建备份:${backupDir}`);
409
+ msg.success(`已创建备份:${backupDir}`);
396
410
  }
397
411
  await fs.mkdir(codexDir, { recursive: true });
398
- await fs.writeFile(configPath, next, "utf8");
399
- writeLine(`✓ Codex 默认模型已设置为 ${selected}(${configPath})`);
412
+ await fs.writeFile(profileConfigPath, next, "utf8");
413
+ msg.success(`Codex 默认模型已设置为 ${selected}(${profileConfigPath})`);
400
414
  return { skipped: false, model: selected };
401
415
  }
package/src/init-flow.js CHANGED
@@ -3,26 +3,33 @@
3
3
  // 不阻塞主流程(AC-05)。各步骤动作由 index.js 注入,本模块只负责编排与输出。
4
4
 
5
5
  import { safeErrorMessage } from "./sanitize.js";
6
+ import { colorLevel } from "./colors.js";
7
+ import { createMessenger } from "./messages.js";
8
+
9
+ // 语义消息器工厂:真实终端着色,测试/管道(writeLine 被替换)下无色。
10
+ const mk = (writeLine) => createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
6
11
 
7
12
  export async function runFullInit({ title, steps, writeLine = console.log } = {}) {
8
- writeLine(`${title}\n`);
13
+ const msg = mk(writeLine);
14
+ msg.title(title);
15
+ writeLine("");
9
16
  let index = 0;
10
17
  for (const step of steps) {
11
18
  index += 1;
12
- writeLine(`第 ${index} 步:${step.title}`);
19
+ msg.step(`第 ${index} 步:${step.title}`);
13
20
  try {
14
21
  const result = await step.run();
15
22
  if (step.required && result === false) {
16
- writeLine(`已在「${step.title}」中止初始化。`);
23
+ msg.warn(`已在「${step.title}」中止初始化。`);
17
24
  return { completed: false, abortedAt: step.title };
18
25
  }
19
26
  } catch (error) {
20
27
  if (step.required) {
21
- writeLine(`× ${step.title}失败:${safeErrorMessage(error)}`);
22
- writeLine("初始化已中止,可稍后在菜单中重试。");
28
+ msg.error(`${step.title}失败:${safeErrorMessage(error)}`);
29
+ msg.hint("初始化已中止,可稍后在菜单中重试。");
23
30
  return { completed: false, abortedAt: step.title };
24
31
  }
25
- writeLine(`! ${step.title}失败(已跳过,不影响后续步骤):${safeErrorMessage(error)}`);
32
+ msg.warn(`${step.title}失败(已跳过,不影响后续步骤):${safeErrorMessage(error)}`);
26
33
  }
27
34
  writeLine("");
28
35
  }
package/src/mcp-skill.js CHANGED
@@ -9,6 +9,11 @@ import path from "node:path";
9
9
 
10
10
  import { createBackup } from "./backup.js";
11
11
  import { upsertTomlTable, validateBasicToml } from "./tool-launcher.js";
12
+ import { colorLevel } from "./colors.js";
13
+ import { createMessenger } from "./messages.js";
14
+
15
+ // 语义消息器工厂:真实终端着色,测试/管道(writeLine 被替换)下无色。
16
+ const mk = (writeLine) => createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
12
17
 
13
18
  // ──────────────────────────────────────────────
14
19
  // 推荐 MCP(PRD 6.6):Claude Code 通过官方 `claude mcp add --scope user`
@@ -138,6 +143,7 @@ export async function readInstalledClaudeMcps({
138
143
  // 多选推荐 MCP;展示推荐标识 ★、说明与平台兼容性,
139
144
  // 已安装项展示为「已安装」且不可重复安装(PRD 6.6 / 优化需求第 4 节)
140
145
  async function selectMcps({ mcps, multiSelect, installedIds, writeLine }) {
146
+ const msg = mk(writeLine);
141
147
  const choices = mcps.map((mcp) => ({
142
148
  label: `${mcp.recommended ? "★ " : ""}${mcp.label}${installedIds.includes(mcp.id) ? "(已安装)" : ""}`,
143
149
  value: mcp.id,
@@ -154,7 +160,7 @@ async function selectMcps({ mcps, multiSelect, installedIds, writeLine }) {
154
160
  const fresh = [];
155
161
  for (const id of selected) {
156
162
  if (installedIds.includes(id)) {
157
- writeLine(`已安装,跳过:${id}`);
163
+ msg.info(`已安装,跳过:${id}`);
158
164
  continue;
159
165
  }
160
166
  fresh.push(mcps.find((mcp) => mcp.id === id));
@@ -164,6 +170,7 @@ async function selectMcps({ mcps, multiSelect, installedIds, writeLine }) {
164
170
 
165
171
  // 重依赖(Playwright / Serena 等)逐项单独确认
166
172
  async function confirmHeavyMcps(mcps, confirm, writeLine) {
173
+ const msg = mk(writeLine);
167
174
  const approved = [];
168
175
  for (const mcp of mcps) {
169
176
  if (!mcp.heavy) {
@@ -174,7 +181,7 @@ async function confirmHeavyMcps(mcps, confirm, writeLine) {
174
181
  if (ok) {
175
182
  approved.push(mcp);
176
183
  } else {
177
- writeLine(`已跳过 ${mcp.id}。`);
184
+ msg.info(`已跳过 ${mcp.id}。`);
178
185
  }
179
186
  }
180
187
  return approved;
@@ -193,37 +200,38 @@ export async function installRecommendedMcps({
193
200
  throw new Error("缺少交互输入");
194
201
  }
195
202
  const { mcps } = await loadRecommendedMcps(api);
203
+ const msg = mk(writeLine);
196
204
  const installedIds = await readInstalledClaudeMcps({ claudeJsonPath, fs });
197
205
  const fresh = await selectMcps({ mcps, multiSelect, installedIds, writeLine });
198
206
  if (fresh.length === 0) {
199
- writeLine("已跳过 MCP 安装。");
207
+ msg.info("已跳过 MCP 安装。");
200
208
  return [];
201
209
  }
202
210
  const approved = await confirmHeavyMcps(fresh, confirm, writeLine);
203
211
  if (approved.length === 0) {
204
- writeLine("已跳过 MCP 安装。");
212
+ msg.info("已跳过 MCP 安装。");
205
213
  return [];
206
214
  }
207
215
 
208
- writeLine([
216
+ msg.info([
209
217
  "即将通过 Claude Code 官方命令安装以下 MCP(修改 ~/.claude.json 用户级配置):",
210
218
  ...approved.map((mcp) => ` claude ${mcp.claudeArgs.join(" ")}`),
211
219
  ].join("\n"));
212
220
  if (!(await confirm("是否继续?"))) {
213
- writeLine("已取消 MCP 安装。");
221
+ msg.info("已取消 MCP 安装。");
214
222
  return [];
215
223
  }
216
224
 
217
225
  const installed = [];
218
226
  for (const mcp of approved) {
219
- writeLine(`正在安装 MCP:${mcp.label}...`);
227
+ msg.step(`正在安装 MCP:${mcp.label}...`);
220
228
  const result = await execCommand("claude", mcp.claudeArgs);
221
229
  if (result.ok) {
222
230
  installed.push(mcp.id);
223
- writeLine(`✓ 已安装 MCP:${mcp.id}`);
231
+ msg.success(`已安装 MCP:${mcp.id}`);
224
232
  } else {
225
- writeLine([
226
- 安装 ${mcp.id} 失败`,
233
+ msg.error([
234
+ `安装 ${mcp.id} 失败`,
227
235
  "",
228
236
  `原因:${sanitizeText(result.stderr || result.error || "未知错误")}`,
229
237
  "",
@@ -279,21 +287,22 @@ export async function installCodexMcps({
279
287
  if (typeof multiSelect !== "function" || typeof confirm !== "function") {
280
288
  throw new Error("缺少交互输入");
281
289
  }
290
+ const msg = mk(writeLine);
282
291
  const configPath = path.join(codexDir, "config.toml");
283
292
  const { mcps } = await loadRecommendedMcps(api);
284
293
  const installedIds = await readInstalledCodexMcps({ codexDir, fs });
285
294
  const fresh = await selectMcps({ mcps, multiSelect, installedIds, writeLine });
286
295
  if (fresh.length === 0) {
287
- writeLine("已跳过 MCP 安装。");
296
+ msg.info("已跳过 MCP 安装。");
288
297
  return [];
289
298
  }
290
299
  const approved = await confirmHeavyMcps(fresh, confirm, writeLine);
291
300
  if (approved.length === 0) {
292
- writeLine("已跳过 MCP 安装。");
301
+ msg.info("已跳过 MCP 安装。");
293
302
  return [];
294
303
  }
295
304
  if (!(await confirm(`即将在 ${configPath} 中写入 ${approved.map((mcp) => mcp.id).join("、")} 的 MCP 配置(写入前备份)。\n是否继续?`))) {
296
- writeLine("已取消 MCP 安装。");
305
+ msg.info("已取消 MCP 安装。");
297
306
  return [];
298
307
  }
299
308
 
@@ -304,17 +313,17 @@ export async function installCodexMcps({
304
313
  content = `${content.trimEnd()}\n`;
305
314
  const tomlError = validateBasicToml(content);
306
315
  if (tomlError) {
307
- writeLine( 生成的 Codex 配置 TOML 校验失败:${tomlError}\n已放弃写入,原配置保持不变。`);
316
+ msg.error(`生成的 Codex 配置 TOML 校验失败:${tomlError}\n已放弃写入,原配置保持不变。`);
308
317
  return [];
309
318
  }
310
319
  const { backupDir } = await createBackup({ baseDir: codexDir, paths: [configPath], fs, now });
311
320
  if (backupDir) {
312
- writeLine(`✓ 已创建备份:${backupDir}`);
321
+ msg.success(`已创建备份:${backupDir}`);
313
322
  }
314
323
  await fs.mkdir(codexDir, { recursive: true });
315
324
  await fs.writeFile(configPath, content, "utf8");
316
325
  for (const mcp of approved) {
317
- writeLine(`✓ 已安装 MCP:${mcp.id}`);
326
+ msg.success(`已安装 MCP:${mcp.id}`);
318
327
  }
319
328
  return approved.map((mcp) => mcp.id);
320
329
  }
@@ -407,6 +416,7 @@ export async function installRecommendedSkills({
407
416
  claudeDir = resolveClaudeUserDir(),
408
417
  fs = { readFile, writeFile, mkdir, copyFile },
409
418
  } = {}) {
419
+ const msg = mk(writeLine);
410
420
  if (typeof promptSelect !== "function" || typeof confirm !== "function") {
411
421
  throw new Error("缺少交互输入");
412
422
  }
@@ -434,7 +444,7 @@ export async function installRecommendedSkills({
434
444
  `即将更新 ${memoryPath}(以标记区块追加/替换“${skill.label}”,不影响文件其他内容,写入前备份)。\n是否继续?`,
435
445
  );
436
446
  if (!approved) {
437
- writeLine(`已跳过 ${skill.label}。`);
447
+ msg.info(`已跳过 ${skill.label}。`);
438
448
  continue;
439
449
  }
440
450
  const current = await readFileIfExists(fs.readFile, memoryPath);
@@ -444,7 +454,7 @@ export async function installRecommendedSkills({
444
454
  }
445
455
  await fs.writeFile(memoryPath, upsertMarkedBlock(current, skill.id, skill.content), "utf8");
446
456
  installed.push(skill.id);
447
- writeLine(`✓ 已安装:${skill.label}`);
457
+ msg.success(`已安装:${skill.label}`);
448
458
  continue;
449
459
  }
450
460
 
@@ -460,7 +470,7 @@ export async function installRecommendedSkills({
460
470
  await fs.mkdir(commandsDir, { recursive: true });
461
471
  await fs.writeFile(filePath, `${skill.content}\n`, "utf8");
462
472
  installed.push(skill.id);
463
- writeLine(`✓ 已安装:${skill.label}(使用 /claude360:${skill.file.replace(/\.md$/, "")} 调用)`);
473
+ msg.success(`已安装:${skill.label}(使用 /claude360:${skill.file.replace(/\.md$/, "")} 调用)`);
464
474
  }
465
475
  return installed;
466
476
  }
@@ -489,6 +499,7 @@ export async function installCodexAgents({
489
499
  codexDir = resolveCodexUserDir(),
490
500
  fs = { readFile, writeFile, mkdir, copyFile },
491
501
  } = {}) {
502
+ const msg = mk(writeLine);
492
503
  if (typeof confirm !== "function") {
493
504
  throw new Error("缺少确认输入");
494
505
  }
@@ -497,7 +508,7 @@ export async function installCodexAgents({
497
508
  `即将更新 ${agentsPath}(以标记区块追加/替换 Claude360 协作规范,不影响文件其他内容,写入前备份)。\n是否继续?`,
498
509
  );
499
510
  if (!approved) {
500
- writeLine("已跳过 Codex AGENTS 配置。");
511
+ msg.info("已跳过 Codex AGENTS 配置。");
501
512
  return { installed: false };
502
513
  }
503
514
  const current = await readFileIfExists(fs.readFile, agentsPath);
@@ -506,7 +517,7 @@ export async function installCodexAgents({
506
517
  await fs.copyFile(agentsPath, `${agentsPath}.claude360.bak`);
507
518
  }
508
519
  await fs.writeFile(agentsPath, upsertMarkedBlock(current, CODEX_AGENTS_BLOCK_ID, CODEX_AGENTS_CONTENT), "utf8");
509
- writeLine(`✓ 已更新:${agentsPath}`);
520
+ msg.success(`已更新:${agentsPath}`);
510
521
  return { installed: true, path: agentsPath };
511
522
  }
512
523
 
package/src/menu.js CHANGED
@@ -5,16 +5,17 @@
5
5
  // 真实终端下由 promptMenu 自动开启彩色分区样式。
6
6
 
7
7
  import { displayWidth } from "./banner.js";
8
- import { BOLD, RESET, colorLevel, fg, toLevel } from "./colors.js";
8
+ import { BOLD, RESET, colorLevel, theme, toLevel } from "./colors.js";
9
9
 
10
- // 分区/选择键配色:按色彩深度 level 现算(真彩色保留 RGB,256 色量化到调色板)。
10
+ // 分区/选择键配色:从统一语义主题取色(见 colors.js theme),不再各自硬编码 RGB
11
11
  function palette(level) {
12
+ const t = theme(level);
12
13
  return {
13
- rule: fg(71, 85, 105, level), // 分隔线:青灰
14
- section: fg(34, 211, 238, level), // 分区标题:亮青
15
- key: fg(125, 211, 252, level), // 选择键:天蓝
16
- exit: fg(248, 113, 113, level), // 退出键:红
17
- desc: fg(100, 116, 139, level), // 功能说明:暗灰
14
+ rule: t.border, // 分隔线:青灰
15
+ section: t.heading, // 分区标题:亮青
16
+ key: t.info, // 选择键:天蓝
17
+ exit: t.error, // 退出键:红
18
+ desc: t.hint, // 功能说明:暗灰
18
19
  };
19
20
  }
20
21
  const MENU_RULE_WIDTH = 46;
@@ -0,0 +1,78 @@
1
+ // 语义消息层:把行内交互文字(成功 / 失败 / 警告 / info / 步骤 / 键值回显 /
2
+ // 路径 / 次要说明 / 标题)统一为「符号前缀 + 专属颜色」,让终端输出有清晰层级,
3
+ // 不再千篇一律。颜色全部取自 colors.js 的统一语义主题 theme()。
4
+ //
5
+ // 设计要点(与既有约定对齐):
6
+ // - 符号沿用全 CLI 既有标准:成功 ✓ / 失败 × / 警告 !(与 banner.formatCheckLine、
7
+ // ui.renderBox、diagnostics 一致),不依赖颜色表达唯一信息(无色档仍有符号)。
8
+ // - info / path / hint 为中性或次要文本:不加符号,仅靠颜色区分;无色档与原始
9
+ // 未加前缀的行字节一致,保证管道 / 测试零回归。
10
+ // - formatMessage 为纯函数(镜像 banner.formatCheckLine 的 (text,{color}) 签名),
11
+ // 便于单测;createMessenger 在其上包一层 writeLine,供各页面直接调用。
12
+
13
+ import { BOLD, RESET, theme, toLevel } from "./colors.js";
14
+
15
+ // 角色定义:符号前缀 + 主题色名(见 colors.js PALETTE/theme)+ 是否加粗。
16
+ // mark 为空串表示该角色不加符号(中性 / 次要文本,仅靠颜色与文字表达)。
17
+ const ROLES = {
18
+ success: { mark: "✓", color: "success", bold: false }, // 操作成功
19
+ error: { mark: "×", color: "error", bold: false }, // 失败 / 异常
20
+ warn: { mark: "!", color: "warn", bold: false }, // 警告 / 降级
21
+ step: { mark: "→", color: "step", bold: false }, // 进行中 / 步骤
22
+ info: { mark: "", color: "info", bold: false }, // 中性信息 / 说明
23
+ hint: { mark: "", color: "hint", bold: false }, // 次要建议 / 帮助
24
+ path: { mark: "", color: "path", bold: false }, // 路径 / 命令 / URL 回显
25
+ title: { mark: "", color: "title", bold: true }, // 页面 / 品牌标题
26
+ heading: { mark: "", color: "heading", bold: true }, // 章节标题
27
+ prompt: { mark: "?", color: "info", bold: true }, // 提问标题
28
+ };
29
+
30
+ // 单行语义消息。未知 kind 回退为 info。无色档(level=0)仅返回「符号 + 文字」,
31
+ // 与改造前的纯文本前缀字节一致。
32
+ export function formatMessage(kind, text, { color = false } = {}) {
33
+ const role = ROLES[kind] || ROLES.info;
34
+ const body = String(text ?? "");
35
+ const prefixed = role.mark ? `${role.mark} ${body}` : body;
36
+ const level = toLevel(color);
37
+ if (!level) {
38
+ return prefixed;
39
+ }
40
+ const t = theme(level);
41
+ return `${role.bold ? BOLD : ""}${t[role.color]}${prefixed}${RESET}`;
42
+ }
43
+
44
+ // 键值回显:标签弱化(灰蓝)、值强调(白色加粗),形成「次要 → 主要」的视觉落差,
45
+ // 取代过去用 ✓ 表示中性回显的混用。无色档退化为 `标签:值`(与原始回显行一致)。
46
+ export function formatResult(label, value, { color = false } = {}) {
47
+ const lbl = String(label ?? "");
48
+ const val = String(value ?? "");
49
+ const level = toLevel(color);
50
+ if (!level) {
51
+ return `${lbl}:${val}`;
52
+ }
53
+ const t = theme(level);
54
+ return `${t.path}${lbl}:${RESET}${BOLD}${t.title}${val}${RESET}`;
55
+ }
56
+
57
+ // 工厂:绑定 writeLine 与色深,返回各语义方法。
58
+ // color 通常来自 index.js 的 fancyOutput(真实终端为 colorLevel(),测试/管道为 0),
59
+ // 因此测试替换 writeLine 后输出无 ANSI,断言可直接匹配纯文本前缀。
60
+ export function createMessenger({ writeLine = console.log, color = false } = {}) {
61
+ const emit = (kind) => (text) => writeLine(formatMessage(kind, text, { color }));
62
+ return {
63
+ success: emit("success"),
64
+ error: emit("error"),
65
+ warn: emit("warn"),
66
+ info: emit("info"),
67
+ step: emit("step"),
68
+ hint: emit("hint"),
69
+ path: emit("path"),
70
+ title: emit("title"),
71
+ heading: emit("heading"),
72
+ prompt: emit("prompt"),
73
+ // 键值回显:messenger.result("当前 Key", name)
74
+ result: (label, value) => writeLine(formatResult(label, value, { color })),
75
+ // 原样输出:空行、或已自带格式的多行块(renderTable / renderBox / renderHeader 结果)
76
+ raw: (text = "") => writeLine(text),
77
+ };
78
+ }
package/src/onboarding.js CHANGED
@@ -1,5 +1,10 @@
1
1
  import { commandVersion, defaultCheckPathWritable, defaultExecCommand } from "./diagnostics.js";
2
2
  import { installOrUpdateTools as defaultInstallOrUpdateTools } from "./tool-installer.js";
3
+ import { colorLevel } from "./colors.js";
4
+ import { createMessenger } from "./messages.js";
5
+
6
+ // 语义消息器工厂:真实终端着色,测试/管道(writeLine 被替换)下无色。
7
+ const mk = (writeLine) => createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
3
8
 
4
9
  const MIN_NODE_MAJOR = 18;
5
10
 
@@ -8,6 +13,7 @@ export async function ensureEnvironment({
8
13
  checkPathWritable = defaultCheckPathWritable,
9
14
  writeLine = console.log,
10
15
  } = {}) {
16
+ const msg = mk(writeLine);
11
17
  const node = await commandVersion(execCommand, "node", ["--version"]);
12
18
  if (!node.ok) {
13
19
  throw new Error("未检测到 Node.js。Claude360 CLI 需要 Node.js 18+,请先安装 Node.js LTS 后重新运行。");
@@ -26,10 +32,10 @@ export async function ensureEnvironment({
26
32
  const globalDir = prefix.ok ? prefix.stdout.trim() : "";
27
33
  const globalNpmWritable = globalDir ? await checkPathWritable(globalDir) : false;
28
34
  if (!globalNpmWritable) {
29
- writeLine("警告:npm 全局目录可能不可写,安装工具时可能需要修复目录权限或切换到用户级 npm prefix。");
35
+ msg.warn("npm 全局目录可能不可写,安装工具时可能需要修复目录权限或切换到用户级 npm prefix。");
30
36
  }
31
37
 
32
- writeLine(`环境检查通过:Node ${node.version},npm ${npm.version}。`);
38
+ msg.success(`环境检查通过:Node ${node.version},npm ${npm.version}。`);
33
39
  return { node: node.version, npm: npm.version, globalNpmWritable };
34
40
  }
35
41
 
@@ -44,6 +50,7 @@ export async function guideToolInstall({
44
50
  throw new Error("缺少工具选择输入");
45
51
  }
46
52
 
53
+ const msg = mk(writeLine);
47
54
  const claudeInstalled = (await commandVersion(execCommand, "claude", ["--version"])).ok;
48
55
  const codexInstalled = (await commandVersion(execCommand, "codex", ["--version"])).ok;
49
56
 
@@ -55,7 +62,7 @@ export async function guideToolInstall({
55
62
  ]);
56
63
 
57
64
  if (selected === "skip") {
58
- writeLine("已跳过工具安装,可稍后在主菜单选择“安装或更新工具”。");
65
+ msg.info("已跳过工具安装,可稍后在主菜单选择“安装或更新工具”。");
59
66
  return { skipped: true, targets: [] };
60
67
  }
61
68
 
@@ -63,11 +70,11 @@ export async function guideToolInstall({
63
70
  const results = await installOrUpdateTools({ targets, confirm });
64
71
  for (const result of results || []) {
65
72
  if (result?.skipped) {
66
- writeLine(`已跳过 ${result.target} 安装。`);
73
+ msg.info(`已跳过 ${result.target} 安装。`);
67
74
  continue;
68
75
  }
69
76
  if (result?.ok === false) {
70
- writeLine(`安装 ${result.target} 失败:${result.error || ""}${result.remediation ? `\n建议:${result.remediation}` : ""}`);
77
+ msg.error(`安装 ${result.target} 失败:${result.error || ""}${result.remediation ? `\n建议:${result.remediation}` : ""}`);
71
78
  }
72
79
  }
73
80
  return { skipped: false, targets, results };
package/src/prompts.js CHANGED
@@ -20,15 +20,16 @@ import {
20
20
 
21
21
  import { promptMultiSelect } from "./menu.js";
22
22
 
23
- import { BOLD, RESET, colorLevel, fg, toLevel } from "./colors.js";
23
+ import { BOLD, RESET, colorLevel, theme, toLevel } from "./colors.js";
24
24
 
25
- // 交互配色:按色彩深度 level 现算(真彩色保留 RGB,256 色量化到调色板)。
25
+ // 交互配色:从统一语义主题取色(见 colors.js theme),不再各自硬编码 RGB
26
26
  function palette(level) {
27
+ const t = theme(level);
27
28
  return {
28
- cyan: fg(34, 211, 238, level), // 品牌高亮:选中项
29
- red: fg(248, 113, 113, level), // 危险操作:NO 高亮
30
- gray: fg(100, 116, 139, level), // 次要说明 / 未选中项
31
- green: fg(74, 222, 128, level), // 完成态答案
29
+ cyan: t.heading, // 品牌高亮:选中项
30
+ red: t.error, // 危险操作:NO 高亮
31
+ gray: t.hint, // 次要说明 / 未选中项
32
+ green: t.success, // 完成态答案
32
33
  };
33
34
  }
34
35