claude360 0.2.6 → 0.2.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude360",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Interactive Claude360 CLI for browser auth, API key setup, balance checks, top-up, Claude Code and Codex launch.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/banner.js CHANGED
@@ -3,24 +3,13 @@
3
3
  // 渲染函数默认无色(color: false),保证测试与管道输出稳定;
4
4
  // 运行时由 index.js 通过 playBanner / 显式 color 参数开启彩色。
5
5
 
6
- export const BRAND_BASE_URL = "https://claude360.xyz";
7
-
8
- const ESC = "\u001b[";
9
- const RESET = `${ESC}0m`;
10
- const BOLD = `${ESC}1m`;
11
- const DIM = `${ESC}2m`;
6
+ import { BOLD, DIM, ESC, RESET, colorEnabled, colorLevel, fg, toLevel } from "./colors.js";
12
7
 
13
- const fg = (r, g, b) => `${ESC}38;2;${r};${g};${b}m`;
8
+ export const BRAND_BASE_URL = "https://claude360.xyz";
14
9
 
15
- export function colorEnabled(stream = process.stdout) {
16
- if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "") {
17
- return false;
18
- }
19
- if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") {
20
- return true;
21
- }
22
- return Boolean(stream && stream.isTTY);
23
- }
10
+ // colorEnabled / colorLevel 由 colors.js 统一提供,这里 re-export,
11
+ // 以保持 menu.js / index.js banner.js 导入的既有路径不变。
12
+ export { colorEnabled, colorLevel };
24
13
 
25
14
  const LOGO_LINES = [
26
15
  " ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗██████╗ ██████╗ ██████╗ ",
@@ -93,7 +82,7 @@ const LOGO_EDGE_CHARS = new Set(["╔", "╗", "╚", "╝", "║", "═"]);
93
82
  // style:
94
83
  // gradient —— 整行按列渐变(logo 行、边框行)
95
84
  // text:<sgr> —— 边框字符渐变,其余字符用固定 SGR 样式(标语行)
96
- function paintLine(line, { phase, totalWidth, style }) {
85
+ function paintLine(line, { phase, totalWidth, style, level = 2 }) {
97
86
  let out = "";
98
87
  let col = 0;
99
88
  let lastColor = "";
@@ -118,7 +107,7 @@ function paintLine(line, { phase, totalWidth, style }) {
118
107
  g = Math.round(g * 0.55);
119
108
  b = Math.round(b * 0.55);
120
109
  }
121
- color = fg(r, g, b);
110
+ color = fg(r, g, b, level);
122
111
  } else {
123
112
  color = style;
124
113
  }
@@ -165,16 +154,17 @@ function buildBannerLayout({ version, baseUrl }) {
165
154
 
166
155
  // 终端太窄放不下大 logo 时的紧凑版
167
156
  function renderCompactBanner({ version, baseUrl, color, phase }) {
157
+ const level = toLevel(color);
168
158
  const lines = [
169
159
  "Claude360 CLI · 模型站接入助手",
170
160
  SUBLINE,
171
161
  `版本 v${version || "-"} · 官网 ${baseUrl}`,
172
162
  ];
173
- if (!color) {
163
+ if (!level) {
174
164
  return lines.join("\n");
175
165
  }
176
166
  return [
177
- paintLine(lines[0], { phase, totalWidth: displayWidth(lines[0]), style: "gradient" }),
167
+ paintLine(lines[0], { phase, totalWidth: displayWidth(lines[0]), style: "gradient", level }),
178
168
  `${DIM}${lines[1]}${RESET}`,
179
169
  `${DIM}${lines[2]}${RESET}`,
180
170
  ].join("\n");
@@ -191,19 +181,20 @@ export function renderBanner({
191
181
  if (columns > 0 && columns < totalWidth) {
192
182
  return renderCompactBanner({ version, baseUrl, color, phase });
193
183
  }
194
- if (!color) {
184
+ const level = toLevel(color);
185
+ if (!level) {
195
186
  return rows.map((row) => row.text).join("\n");
196
187
  }
197
188
  const styles = {
198
- title: `${BOLD}${fg(240, 246, 255)}`,
199
- sub: `${fg(148, 163, 184)}`,
189
+ title: `${BOLD}${fg(240, 246, 255, level)}`,
190
+ sub: `${fg(148, 163, 184, level)}`,
200
191
  };
201
192
  return rows.map((row) => {
202
193
  if (row.style === "footer") {
203
194
  return `${DIM}${row.text}${RESET}`;
204
195
  }
205
196
  const style = row.style === "gradient" ? "gradient" : styles[row.style];
206
- return paintLine(row.text, { phase, totalWidth, style });
197
+ return paintLine(row.text, { phase, totalWidth, style, level });
207
198
  }).join("\n");
208
199
  }
209
200
 
@@ -218,11 +209,12 @@ export async function playBanner({
218
209
  sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
219
210
  } = {}) {
220
211
  const columns = stream && stream.columns ? stream.columns : 0;
221
- const animated = colorEnabled(stream)
212
+ const level = colorLevel(stream);
213
+ const animated = level > 0
222
214
  && typeof stream.write === "function"
223
215
  && (!columns || columns >= buildBannerLayout({ version, baseUrl }).totalWidth);
224
216
  if (!animated) {
225
- const text = renderBanner({ version, baseUrl, color: colorEnabled(stream), columns });
217
+ const text = renderBanner({ version, baseUrl, color: level, columns });
226
218
  if (stream && typeof stream.write === "function") {
227
219
  stream.write(text + "\n");
228
220
  } else {
@@ -235,7 +227,7 @@ export async function playBanner({
235
227
  try {
236
228
  for (let i = 0; i <= frames; i += 1) {
237
229
  const phase = i / frames;
238
- stream.write(renderBanner({ version, baseUrl, color: true, phase }) + "\n");
230
+ stream.write(renderBanner({ version, baseUrl, color: level, phase }) + "\n");
239
231
  if (i < frames) {
240
232
  await sleep(intervalMs);
241
233
  stream.write(`${ESC}${lineCount}A`);
@@ -259,11 +251,12 @@ const CHECK_COLORS = {
259
251
  export function formatCheckLine(status, text, { color = false } = {}) {
260
252
  const symbols = { ok: CHECK_OK, warn: CHECK_WARN, fail: CHECK_FAIL };
261
253
  const symbol = symbols[status] || CHECK_WARN;
262
- if (!color) {
254
+ const level = toLevel(color);
255
+ if (!level) {
263
256
  return `${symbol} ${text}`;
264
257
  }
265
258
  const [r, g, b] = CHECK_COLORS[status] || CHECK_COLORS.warn;
266
- return `${fg(r, g, b)}${BOLD}${symbol}${RESET} ${text}`;
259
+ return `${fg(r, g, b, level)}${BOLD}${symbol}${RESET} ${text}`;
267
260
  }
268
261
 
269
262
  export function renderEnvironmentChecks(checks = [], { color = false } = {}) {
package/src/colors.js ADDED
@@ -0,0 +1,93 @@
1
+ // 终端色彩能力检测与着色:truecolor(2) / 256 色(1) / 无色(0) 三档自适应。
2
+ // 统一被 banner.js / menu.js / ui.js / prompts.js 复用,消除各文件重复的
3
+ // ESC / RESET / fg 定义。macOS Terminal.app 等仅支持 256 色的终端无法解析
4
+ // 24-bit 真彩色序列(38;2;r;g;b)会出现颜色错乱,这里在 256 色档把 RGB
5
+ // 量化到 xterm-256 调色板,既保留渐变观感又避免错乱。
6
+
7
+ export const ESC = "[";
8
+ export const RESET = `${ESC}0m`;
9
+ export const BOLD = `${ESC}1m`;
10
+ export const DIM = `${ESC}2m`;
11
+
12
+ // 终端色彩深度:0=无色,1=256 色,2=真彩色(24-bit)。
13
+ // 仅检测能力(是否上色 + 用多深的色),不负责方向键 UI 判定(见 prompts.isInteractive)。
14
+ export function colorLevel(stream = process.stdout) {
15
+ const { NO_COLOR, FORCE_COLOR, COLORTERM, TERM } = process.env;
16
+ if (NO_COLOR !== undefined && NO_COLOR !== "") {
17
+ return 0;
18
+ }
19
+ let forced = false;
20
+ if (FORCE_COLOR !== undefined && FORCE_COLOR !== "") {
21
+ if (FORCE_COLOR === "0" || FORCE_COLOR === "false") {
22
+ return 0;
23
+ }
24
+ if (FORCE_COLOR === "1") {
25
+ return 1;
26
+ }
27
+ if (FORCE_COLOR === "2" || FORCE_COLOR === "3") {
28
+ return 2;
29
+ }
30
+ forced = true; // 其它非空值:强制上色,色深仍按下方终端能力判定
31
+ }
32
+ if (!forced && !(stream && stream.isTTY)) {
33
+ return 0;
34
+ }
35
+ const colorterm = String(COLORTERM || "").toLowerCase();
36
+ if (colorterm.includes("truecolor") || colorterm.includes("24bit")) {
37
+ return 2;
38
+ }
39
+ const term = String(TERM || "").toLowerCase();
40
+ if (/-direct|kitty|iterm/.test(term)) {
41
+ return 2;
42
+ }
43
+ // 其余可上色场景统一按 256 色处理(含 *-256color、空 TERM、dumb 等),
44
+ // 保证「isTTY 即至少 256 色」,使 banner 动画在各类 CI 的 TERM 下稳定触发。
45
+ return 1;
46
+ }
47
+
48
+ export function colorEnabled(stream = process.stdout) {
49
+ return colorLevel(stream) > 0;
50
+ }
51
+
52
+ // 把 render / prompt 的 color 入参(boolean | 0 | 1 | 2)归一为 level。
53
+ // true → 2(真彩色),保证既有「color: true」调用与测试行为完全不变。
54
+ export function toLevel(color) {
55
+ if (color === true) {
56
+ return 2;
57
+ }
58
+ if (!color) {
59
+ return 0;
60
+ }
61
+ const n = Number(color);
62
+ if (!Number.isFinite(n) || n <= 0) {
63
+ return 0;
64
+ }
65
+ return n >= 2 ? 2 : 1;
66
+ }
67
+
68
+ // 24-bit RGB 量化到 xterm-256 调色板:232-255 灰阶 + 16-231 的 6×6×6 立方。
69
+ export function rgbToAnsi256(r, g, b) {
70
+ if (r === g && g === b) {
71
+ if (r < 8) {
72
+ return 16;
73
+ }
74
+ if (r > 248) {
75
+ return 231;
76
+ }
77
+ return Math.round(((r - 8) / 247) * 24) + 232;
78
+ }
79
+ const q = (v) => Math.round((v / 255) * 5);
80
+ return 16 + 36 * q(r) + 6 * q(g) + q(b);
81
+ }
82
+
83
+ // 前景色:按 level 输出真彩色 / 256 色 / 空串。level 默认 2,
84
+ // 使任何未显式降级的调用安全回退为真彩色(与改造前行为一致)。
85
+ export function fg(r, g, b, level = 2) {
86
+ if (level >= 2) {
87
+ return `${ESC}38;2;${r};${g};${b}m`;
88
+ }
89
+ if (level === 1) {
90
+ return `${ESC}38;5;${rgbToAnsi256(r, g, b)}m`;
91
+ }
92
+ return "";
93
+ }
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import { ApiClient } from "./api-client.js";
7
7
  import { authenticateWithBrowser } from "./auth.js";
8
8
  import { formatAccountStatus, loadAccountStatus } from "./account-status.js";
9
- import { colorEnabled, formatCheckLine, playBanner, renderBanner } from "./banner.js";
9
+ import { colorLevel, formatCheckLine, playBanner, renderBanner } from "./banner.js";
10
10
  import { runCcSwitchGenerator } from "./cc-switch.js";
11
11
  import { createConfigStore } from "./config-store.js";
12
12
  import { createPrompts, isInteractive } from "./prompts.js";
@@ -113,7 +113,7 @@ export async function runCli({
113
113
  const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
114
114
  let api = createApiClient({ baseUrl, cliToken: config.cliToken || "" });
115
115
  // 仅在真实终端(writeLine 未被测试替换)启用彩色与动效
116
- const fancyOutput = writeLine === console.log && colorEnabled();
116
+ const fancyOutput = writeLine === console.log ? colorLevel() : 0;
117
117
  // 方向键交互:真实 TTY 且交互未被测试替换时启用(优化需求第五节),
118
118
  // 非交互环境降级为编号输入,由 prompts.js 统一处理
119
119
  const interactiveUi = writeLine === console.log && isInteractive();
package/src/menu.js CHANGED
@@ -4,18 +4,19 @@
4
4
  // 渲染默认无色(color: false)以保证测试与管道输出稳定,
5
5
  // 真实终端下由 promptMenu 自动开启彩色分区样式。
6
6
 
7
- import { colorEnabled, displayWidth } from "./banner.js";
7
+ import { displayWidth } from "./banner.js";
8
+ import { BOLD, RESET, colorLevel, fg, toLevel } from "./colors.js";
8
9
 
9
- const ESC = "\u001b[";
10
- const RESET = `${ESC}0m`;
11
- const BOLD = `${ESC}1m`;
12
- const fg = (r, g, b) => `${ESC}38;2;${r};${g};${b}m`;
13
-
14
- const RULE_COLOR = fg(71, 85, 105); // 分隔线:青灰
15
- const SECTION_COLOR = fg(34, 211, 238); // 分区标题:亮青
16
- const KEY_COLOR = fg(125, 211, 252); // 选择键:天蓝
17
- const EXIT_COLOR = fg(248, 113, 113); // 退出键:红
18
- const DESC_COLOR = fg(100, 116, 139); // 功能说明:暗灰
10
+ // 分区/选择键配色:按色彩深度 level 现算(真彩色保留 RGB,256 色量化到调色板)。
11
+ function palette(level) {
12
+ 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), // 功能说明:暗灰
18
+ };
19
+ }
19
20
  const MENU_RULE_WIDTH = 46;
20
21
 
21
22
  export function buildFirstRunMenu() {
@@ -88,36 +89,41 @@ export function buildDailyMenu() {
88
89
 
89
90
  // 分区标题渲染为 `─── 标题 ──────…` 形式的分隔线,让各组功能在视觉上彼此分开
90
91
  function renderSectionRule(title, color) {
92
+ const level = toLevel(color);
91
93
  const lead = "───";
92
94
  const label = ` ${title} `;
93
95
  const tail = "─".repeat(Math.max(4, MENU_RULE_WIDTH - lead.length - displayWidth(label)));
94
- if (!color) {
96
+ if (!level) {
95
97
  return `${lead}${label}${tail}`;
96
98
  }
97
- return `${RULE_COLOR}${lead}${RESET}${BOLD}${SECTION_COLOR}${label}${RESET}${RULE_COLOR}${tail}${RESET}`;
99
+ const c = palette(level);
100
+ return `${c.rule}${lead}${RESET}${BOLD}${c.section}${label}${RESET}${c.rule}${tail}${RESET}`;
98
101
  }
99
102
 
100
103
  function renderItem(item, color) {
104
+ const level = toLevel(color);
101
105
  const desc = item.desc ? ` - ${item.desc}` : "";
102
- if (!color) {
106
+ if (!level) {
103
107
  return ` ${item.key}. ${item.label}${desc}`;
104
108
  }
105
- const keyColor = item.value === "exit" ? EXIT_COLOR : KEY_COLOR;
106
- const coloredDesc = desc ? `${DESC_COLOR}${desc}${RESET}` : "";
109
+ const c = palette(level);
110
+ const keyColor = item.value === "exit" ? c.exit : c.key;
111
+ const coloredDesc = desc ? `${c.desc}${desc}${RESET}` : "";
107
112
  return ` ${BOLD}${keyColor}${item.key}.${RESET} ${item.label}${coloredDesc}`;
108
113
  }
109
114
 
110
115
  export function renderMenu(menu, { color = false } = {}) {
116
+ const level = toLevel(color);
111
117
  const lines = [];
112
118
  if (menu.title) {
113
- lines.push(color ? `${BOLD}${menu.title}${RESET}` : menu.title, "");
119
+ lines.push(level ? `${BOLD}${menu.title}${RESET}` : menu.title, "");
114
120
  }
115
121
  for (const section of menu.sections) {
116
122
  if (section.title) {
117
- lines.push(renderSectionRule(section.title, color));
123
+ lines.push(renderSectionRule(section.title, level));
118
124
  }
119
125
  for (const item of section.items) {
120
- lines.push(renderItem(item, color));
126
+ lines.push(renderItem(item, level));
121
127
  }
122
128
  lines.push("");
123
129
  }
@@ -155,7 +161,7 @@ export async function promptMenu({ menu, promptInput, select, writeLine = consol
155
161
  throw new Error("缺少菜单输入");
156
162
  }
157
163
  // 仅在输出未被测试替换且终端支持时启用彩色
158
- const color = writeLine === console.log && colorEnabled();
164
+ const color = writeLine === console.log ? colorLevel() : 0;
159
165
  while (true) {
160
166
  writeLine(renderMenu(menu, { color }));
161
167
  const answer = await promptInput("请输入选项");
package/src/prompts.js CHANGED
@@ -20,16 +20,19 @@ import {
20
20
 
21
21
  import { promptMultiSelect } from "./menu.js";
22
22
 
23
- const ESC = "[";
24
- const RESET = `${ESC}0m`;
25
- const BOLD = `${ESC}1m`;
26
- const fg = (r, g, b) => `${ESC}38;2;${r};${g};${b}m`;
27
- const CYAN = fg(34, 211, 238); // 品牌高亮:选中项
28
- const RED = fg(248, 113, 113); // 危险操作:NO 高亮
29
- const GRAY = fg(100, 116, 139); // 次要说明 / 未选中项
30
- const GREEN = fg(74, 222, 128); // 完成态答案
23
+ import { BOLD, RESET, colorLevel, fg, toLevel } from "./colors.js";
31
24
 
32
- const paint = (color, sgr, text) => (color ? `${sgr}${text}${RESET}` : text);
25
+ // 交互配色:按色彩深度 level 现算(真彩色保留 RGB,256 色量化到调色板)。
26
+ function palette(level) {
27
+ 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), // 完成态答案
32
+ };
33
+ }
34
+
35
+ const paint = (level, sgr, text) => (level ? `${sgr}${text}${RESET}` : text);
33
36
 
34
37
  export function isInteractive(stream = process.stdout) {
35
38
  if (process.env.CI) {
@@ -51,12 +54,14 @@ export function resolveEscValue(choices = []) {
51
54
  // 确认行:选中项大写、加粗、高亮并带箭头;未选中项小写灰色(需求第四节)。
52
55
  // 终端无法局部放大字体,用大小写 + 颜色 + 箭头模拟视觉差异。
53
56
  export function renderConfirmLine(yesActive, { color = true, danger = false } = {}) {
57
+ const level = toLevel(color);
58
+ const c = palette(level);
54
59
  const yes = yesActive
55
- ? paint(color, `${BOLD}${CYAN}`, "▶ YES ◀")
56
- : paint(color, GRAY, "yes");
60
+ ? paint(level,`${BOLD}${c.cyan}`, "▶ YES ◀")
61
+ : paint(level,c.gray, "yes");
57
62
  const no = !yesActive
58
- ? paint(color, `${BOLD}${danger ? RED : CYAN}`, "▶ NO ◀")
59
- : paint(color, GRAY, "no");
63
+ ? paint(level,`${BOLD}${danger ? c.red : c.cyan}`, "▶ NO ◀")
64
+ : paint(level,c.gray, "no");
60
65
  return ` ${yes} ${no}`;
61
66
  }
62
67
 
@@ -85,6 +90,8 @@ function assertSelectableChoices(message, choices) {
85
90
  // 单选:方向键移动(跳过分隔行)、回车确认、Esc 返回 escValue
86
91
  export const interactiveSelect = createPrompt((config, done) => {
87
92
  const { message, choices = [], escValue, color = true } = config;
93
+ const level = toLevel(color);
94
+ const c = palette(level);
88
95
  const selectable = assertSelectableChoices(message, choices);
89
96
  const [status, setStatus] = useState("idle");
90
97
  const [active, setActive] = useState(selectable[0]);
@@ -111,7 +118,7 @@ export const interactiveSelect = createPrompt((config, done) => {
111
118
  });
112
119
 
113
120
  if (status === "done") {
114
- return `${paint(color, BOLD, message)} ${paint(color, GREEN, choices[active]?.separator ? "" : String(choices[active]?.label ?? ""))}`;
121
+ return `${paint(level,BOLD, message)} ${paint(level,c.green, choices[active]?.separator ? "" : String(choices[active]?.label ?? ""))}`;
115
122
  }
116
123
 
117
124
  const page = usePagination({
@@ -121,22 +128,24 @@ export const interactiveSelect = createPrompt((config, done) => {
121
128
  loop: false,
122
129
  renderItem({ item, isActive }) {
123
130
  if (item.separator) {
124
- return paint(color, GRAY, item.separator);
131
+ return paint(level,c.gray, item.separator);
125
132
  }
126
- const hint = item.hint ? ` ${paint(color, GRAY, item.hint)}` : "";
133
+ const hint = item.hint ? ` ${paint(level,c.gray, item.hint)}` : "";
127
134
  if (isActive) {
128
- return `${paint(color, `${BOLD}${CYAN}`, `❯ ${item.label}`)}${hint}`;
135
+ return `${paint(level,`${BOLD}${c.cyan}`, `❯ ${item.label}`)}${hint}`;
129
136
  }
130
137
  return ` ${item.label}${hint}`;
131
138
  },
132
139
  });
133
- const help = paint(color, GRAY, `↑↓ 移动 · Enter 确认${escValue !== undefined ? " · Esc 返回" : ""}`);
134
- return `${paint(color, BOLD, message)}\n${page}\n${help}`;
140
+ const help = paint(level,c.gray, `↑↓ 移动 · Enter 确认${escValue !== undefined ? " · Esc 返回" : ""}`);
141
+ return `${paint(level,BOLD, message)}\n${page}\n${help}`;
135
142
  });
136
143
 
137
144
  // 多选:空格选择/取消、a 全选、i 反选、回车确认、Esc 取消(需求第 4 节)
138
145
  export const interactiveMultiSelect = createPrompt((config, done) => {
139
146
  const { message, choices = [], preselected = [], color = true } = config;
147
+ const level = toLevel(color);
148
+ const c = palette(level);
140
149
  if (choices.length === 0) {
141
150
  throw new Error(`${message || "多选列表"}缺少可选择项`);
142
151
  }
@@ -189,7 +198,7 @@ export const interactiveMultiSelect = createPrompt((config, done) => {
189
198
  });
190
199
 
191
200
  if (status === "done") {
192
- return `${paint(color, BOLD, message)} ${paint(color, GREEN, `已选 ${selected.size} 项`)}`;
201
+ return `${paint(level,BOLD, message)} ${paint(level,c.green, `已选 ${selected.size} 项`)}`;
193
202
  }
194
203
 
195
204
  const page = usePagination({
@@ -198,20 +207,22 @@ export const interactiveMultiSelect = createPrompt((config, done) => {
198
207
  pageSize: PAGE_SIZE,
199
208
  loop: false,
200
209
  renderItem({ item, isActive }) {
201
- const mark = selected.has(item.value) ? paint(color, GREEN, "[x]") : "[ ]";
202
- const hint = item.hint ? ` ${paint(color, GRAY, item.hint)}` : "";
203
- const label = isActive ? paint(color, `${BOLD}${CYAN}`, `❯ ${mark} ${item.label}`) : ` ${mark} ${item.label}`;
210
+ const mark = selected.has(item.value) ? paint(level,c.green, "[x]") : "[ ]";
211
+ const hint = item.hint ? ` ${paint(level,c.gray, item.hint)}` : "";
212
+ const label = isActive ? paint(level,`${BOLD}${c.cyan}`, `❯ ${mark} ${item.label}`) : ` ${mark} ${item.label}`;
204
213
  return `${label}${hint}`;
205
214
  },
206
215
  });
207
- const help = paint(color, GRAY, "↑↓ 移动 · 空格 选择 · a 全选 · i 反选 · Enter 确认 · Esc 取消");
208
- return `${paint(color, BOLD, message)}\n${page}\n${help}`;
216
+ const help = paint(level,c.gray, "↑↓ 移动 · 空格 选择 · a 全选 · i 反选 · Enter 确认 · Esc 取消");
217
+ return `${paint(level,BOLD, message)}\n${page}\n${help}`;
209
218
  });
210
219
 
211
220
  // 确认:左右方向键 / Tab 切换 YES、NO,回车确认,y / n 快捷,Esc 取消。
212
221
  // 默认值由调用方按风险设置:普通继续默认 YES,覆盖 / 删除 / 重置默认 NO。
213
222
  export const interactiveConfirm = createPrompt((config, done) => {
214
223
  const { message, defaultYes = false, danger = false, color = true } = config;
224
+ const level = toLevel(color);
225
+ const c = palette(level);
215
226
  const [status, setStatus] = useState("idle");
216
227
  const [yes, setYes] = useState(Boolean(defaultYes));
217
228
 
@@ -243,10 +254,10 @@ export const interactiveConfirm = createPrompt((config, done) => {
243
254
  });
244
255
 
245
256
  if (status === "done") {
246
- return `${paint(color, BOLD, message)} ${paint(color, GREEN, yes ? "YES" : "NO")}`;
257
+ return `${paint(level,BOLD, message)} ${paint(level,c.green, yes ? "YES" : "NO")}`;
247
258
  }
248
- const help = paint(color, GRAY, "←→ 切换 · Enter 确认 · Esc 取消");
249
- return `${paint(color, BOLD, message)}\n\n${renderConfirmLine(yes, { color, danger })}\n\n${help}`;
259
+ const help = paint(level,c.gray, "←→ 切换 · Enter 确认 · Esc 取消");
260
+ return `${paint(level,BOLD, message)}\n\n${renderConfirmLine(yes, { color, danger })}\n\n${help}`;
250
261
  });
251
262
 
252
263
  // ──────────────────────────────────────────────
@@ -319,7 +330,7 @@ async function guardExit(run) {
319
330
 
320
331
  export function createPrompts({
321
332
  interactive = isInteractive(),
322
- color = true,
333
+ color = colorLevel(),
323
334
  promptInput,
324
335
  writeLine,
325
336
  } = {}) {
package/src/ui.js CHANGED
@@ -5,12 +5,15 @@
5
5
 
6
6
  import { displayWidth } from "./banner.js";
7
7
 
8
- const ESC = "\u001b[";
9
- const RESET = `${ESC}0m`;
10
- const BOLD = `${ESC}1m`;
11
- const fg = (r, g, b) => `${ESC}38;2;${r};${g};${b}m`;
12
- const BORDER_COLOR = fg(71, 85, 105);
13
- const HEAD_COLOR = fg(125, 211, 252);
8
+ import { BOLD, RESET, fg, toLevel } from "./colors.js";
9
+
10
+ // 表格/标题配色:按色彩深度 level 现算(真彩色保留 RGB,256 色量化到调色板)。
11
+ function palette(level) {
12
+ return {
13
+ border: fg(71, 85, 105, level), // 边框:青灰
14
+ head: fg(125, 211, 252, level), // 表头:天蓝
15
+ };
16
+ }
14
17
 
15
18
  // eslint-disable-next-line no-control-regex
16
19
  const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
@@ -47,17 +50,18 @@ function padCell(text, width) {
47
50
  return `${text}${" ".repeat(gap)}`;
48
51
  }
49
52
 
50
- function borderLine(widths, [left, mid, right], color) {
53
+ function borderLine(widths, [left, mid, right], level) {
51
54
  const line = `${left}${widths.map((w) => "─".repeat(w + 2)).join(mid)}${right}`;
52
- return color ? `${BORDER_COLOR}${line}${RESET}` : line;
55
+ return level ? `${palette(level).border}${line}${RESET}` : line;
53
56
  }
54
57
 
55
- function contentLine(cells, widths, { color, bold = false } = {}) {
56
- const bar = color ? `${BORDER_COLOR}│${RESET}` : "│";
58
+ function contentLine(cells, widths, { level = 0, bold = false } = {}) {
59
+ const c = level ? palette(level) : null;
60
+ const bar = level ? `${c.border}│${RESET}` : "│";
57
61
  const body = cells
58
62
  .map((cell, i) => {
59
63
  const padded = padCell(cell, widths[i]);
60
- return bold && color ? ` ${BOLD}${HEAD_COLOR}${padded}${RESET} ` : ` ${padded} `;
64
+ return bold && level ? ` ${BOLD}${c.head}${padded}${RESET} ` : ` ${padded} `;
61
65
  })
62
66
  .join(bar);
63
67
  return `${bar}${body}${bar}`;
@@ -93,17 +97,18 @@ function resolveWidths(head, rows, width) {
93
97
  }
94
98
 
95
99
  export function renderTable({ head = [], rows = [] } = {}, { color = false, width = 0 } = {}) {
100
+ const level = toLevel(color);
96
101
  const widths = resolveWidths(head, rows, width);
97
102
  const fit = (row) => widths.map((w, i) => truncateDisplay(row[i] ?? "", w));
98
- const lines = [borderLine(widths, ["┌", "┬", "┐"], color)];
103
+ const lines = [borderLine(widths, ["┌", "┬", "┐"], level)];
99
104
  if (head.length > 0) {
100
- lines.push(contentLine(fit(head), widths, { color, bold: true }));
101
- lines.push(borderLine(widths, ["├", "┼", "┤"], color));
105
+ lines.push(contentLine(fit(head), widths, { level, bold: true }));
106
+ lines.push(borderLine(widths, ["├", "┼", "┤"], level));
102
107
  }
103
108
  for (const row of rows) {
104
- lines.push(contentLine(fit(row), widths, { color }));
109
+ lines.push(contentLine(fit(row), widths, { level }));
105
110
  }
106
- lines.push(borderLine(widths, ["└", "┴", "┘"], color));
111
+ lines.push(borderLine(widths, ["└", "┴", "┘"], level));
107
112
  return lines.join("\n");
108
113
  }
109
114
 
@@ -127,13 +132,15 @@ export function renderStatusTable({ head = [], row = [] } = {}, { color = false,
127
132
 
128
133
  // 章节标题:与 menu.js 分区分隔线风格一致
129
134
  export function renderSectionTitle(title, { color = false, width = 46 } = {}) {
135
+ const level = toLevel(color);
130
136
  const lead = "───";
131
137
  const label = ` ${title} `;
132
138
  const tail = "─".repeat(Math.max(4, width - lead.length - displayWidth(label)));
133
- if (!color) {
139
+ if (!level) {
134
140
  return `${lead}${label}${tail}`;
135
141
  }
136
- return `${BORDER_COLOR}${lead}${RESET}${BOLD}${HEAD_COLOR}${label}${RESET}${BORDER_COLOR}${tail}${RESET}`;
142
+ const c = palette(level);
143
+ return `${c.border}${lead}${RESET}${BOLD}${c.head}${label}${RESET}${c.border}${tail}${RESET}`;
137
144
  }
138
145
 
139
146
  // ──────────────────────────────────────────────
@@ -142,18 +149,20 @@ export function renderSectionTitle(title, { color = false, width = 46 } = {}) {
142
149
 
143
150
  // 页面标题区:品牌名 + 当前页面名,统一边框样式
144
151
  export function renderHeader(title, { subtitle = "", color = false } = {}) {
152
+ const level = toLevel(color);
153
+ const c = level ? palette(level) : null;
145
154
  const label = `Claude360 CLI · ${title}`;
146
155
  const inner = Math.max(cellWidth(label), cellWidth(subtitle)) + 2;
147
156
  const line = (text) => {
148
157
  const body = ` ${padCell(text, inner - 2)} `;
149
- if (!color) {
158
+ if (!level) {
150
159
  return `│${body}│`;
151
160
  }
152
- return `${BORDER_COLOR}│${RESET}${BOLD}${HEAD_COLOR}${body}${RESET}${BORDER_COLOR}│${RESET}`;
161
+ return `${c.border}│${RESET}${BOLD}${c.head}${body}${RESET}${c.border}│${RESET}`;
153
162
  };
154
163
  const edge = (left, right) => {
155
164
  const text = `${left}${"─".repeat(inner)}${right}`;
156
- return color ? `${BORDER_COLOR}${text}${RESET}` : text;
165
+ return level ? `${c.border}${text}${RESET}` : text;
157
166
  };
158
167
  const lines = [edge("┌", "┐"), line(label)];
159
168
  if (subtitle) {
@@ -165,15 +174,20 @@ export function renderHeader(title, { subtitle = "", color = false } = {}) {
165
174
 
166
175
  // 信息盒子:info / warn / error / success 四种状态,
167
176
  // 颜色只增强层级,状态始终有文字前缀(需求二「不依赖颜色表达唯一信息」)
168
- const BOX_KINDS = {
169
- info: { mark: "i", color: fg(125, 211, 252) },
170
- warn: { mark: "!", color: fg(250, 204, 21) },
171
- error: { mark: "×", color: fg(248, 113, 113) },
172
- success: { mark: "✓", color: fg(74, 222, 128) },
177
+ // 四种状态:文字前缀(mark)始终存在,颜色仅增强层级(需求二「不依赖颜色表达唯一信息」)
178
+ const BOX_MARKS = { info: "i", warn: "!", error: "×", success: "✓" };
179
+ const BOX_RGB = {
180
+ info: [125, 211, 252],
181
+ warn: [250, 204, 21],
182
+ error: [248, 113, 113],
183
+ success: [74, 222, 128],
173
184
  };
174
185
 
175
186
  export function renderBox(message, { kind = "info", color = false, width = 0 } = {}) {
176
- const { mark, color: kindColor } = BOX_KINDS[kind] || BOX_KINDS.info;
187
+ const level = toLevel(color);
188
+ const mark = BOX_MARKS[kind] || BOX_MARKS.info;
189
+ const [r, g, b] = BOX_RGB[kind] || BOX_RGB.info;
190
+ const kindColor = fg(r, g, b, level);
177
191
  const rawLines = String(message ?? "").split("\n");
178
192
  const textLines = rawLines.map((line, index) => (index === 0 ? `${mark} ${line}` : ` ${line}`));
179
193
  const maxAvailable = width > 4 ? width - 4 : 0;
@@ -181,12 +195,12 @@ export function renderBox(message, { kind = "info", color = false, width = 0 } =
181
195
  const inner = Math.max(...fitted.map((line) => cellWidth(line))) + 2;
182
196
  const edge = (left, right) => {
183
197
  const text = `${left}${"─".repeat(inner)}${right}`;
184
- return color ? `${kindColor}${text}${RESET}` : text;
198
+ return level ? `${kindColor}${text}${RESET}` : text;
185
199
  };
186
200
  const lines = [edge("┌", "┐")];
187
201
  for (const line of fitted) {
188
202
  const body = ` ${padCell(line, inner - 2)} `;
189
- lines.push(color ? `${kindColor}│${RESET}${body}${kindColor}│${RESET}` : `│${body}│`);
203
+ lines.push(level ? `${kindColor}│${RESET}${body}${kindColor}│${RESET}` : `│${body}│`);
190
204
  }
191
205
  lines.push(edge("└", "┘"));
192
206
  return lines.join("\n");