claude360 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude360",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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
@@ -1,5 +1,27 @@
1
+ // 品牌横幅:参照 zcf 的顶部边框设计 —— 圆角边框盒子内放渐变色 ASCII logo,
2
+ // TTY 下播放流光动效(渐变相位随帧移动),非 TTY / NO_COLOR 下降级为纯文本。
3
+ // 渲染函数默认无色(color: false),保证测试与管道输出稳定;
4
+ // 运行时由 index.js 通过 playBanner / 显式 color 参数开启彩色。
5
+
1
6
  export const BRAND_BASE_URL = "https://claude360.xyz";
2
7
 
8
+ const ESC = "\u001b[";
9
+ const RESET = `${ESC}0m`;
10
+ const BOLD = `${ESC}1m`;
11
+ const DIM = `${ESC}2m`;
12
+
13
+ const fg = (r, g, b) => `${ESC}38;2;${r};${g};${b}m`;
14
+
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
+ }
24
+
3
25
  const LOGO_LINES = [
4
26
  " ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗██████╗ ██████╗ ██████╗ ",
5
27
  "██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝╚════██╗██╔════╝ ██╔═████╗",
@@ -9,34 +31,245 @@ const LOGO_LINES = [
9
31
  " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚═════╝ ╚═════╝ ",
10
32
  ];
11
33
 
12
- export function renderBanner({ version = "", baseUrl = BRAND_BASE_URL } = {}) {
34
+ // 霓虹渐变色板:青 品红(与 zcf 的冷色霓虹风一致)
35
+ const GRADIENT = [
36
+ [0, 229, 255],
37
+ [59, 130, 246],
38
+ [99, 102, 241],
39
+ [168, 85, 247],
40
+ [236, 72, 153],
41
+ ];
42
+
43
+ // 在色板上往返插值(ping-pong),让流光动画首尾循环时颜色连续不跳变
44
+ function gradientAt(t) {
45
+ const segments = GRADIENT.length - 1;
46
+ let x = ((t % 1) + 1) % 1;
47
+ x = x < 0.5 ? x * 2 : (1 - x) * 2;
48
+ const pos = x * segments;
49
+ const i = Math.min(Math.floor(pos), segments - 1);
50
+ const f = pos - i;
51
+ const [r1, g1, b1] = GRADIENT[i];
52
+ const [r2, g2, b2] = GRADIENT[i + 1];
53
+ return [
54
+ Math.round(r1 + (r2 - r1) * f),
55
+ Math.round(g1 + (g2 - g1) * f),
56
+ Math.round(b1 + (b2 - b1) * f),
57
+ ];
58
+ }
59
+
60
+ function charDisplayWidth(codePoint) {
61
+ return (
62
+ (codePoint >= 0x1100 && codePoint <= 0x115f) ||
63
+ (codePoint >= 0x2e80 && codePoint <= 0x303e) ||
64
+ (codePoint >= 0x3041 && codePoint <= 0x33ff) ||
65
+ (codePoint >= 0x3400 && codePoint <= 0x4dbf) ||
66
+ (codePoint >= 0x4e00 && codePoint <= 0x9fff) ||
67
+ (codePoint >= 0xa000 && codePoint <= 0xa4cf) ||
68
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
69
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
70
+ (codePoint >= 0xfe30 && codePoint <= 0xfe4f) ||
71
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
72
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6)
73
+ ) ? 2 : 1;
74
+ }
75
+
76
+ export function displayWidth(text) {
77
+ let width = 0;
78
+ for (const ch of text) {
79
+ width += charDisplayWidth(ch.codePointAt(0));
80
+ }
81
+ return width;
82
+ }
83
+
84
+ function padCenter(text, width) {
85
+ const gap = Math.max(0, width - displayWidth(text));
86
+ const left = Math.floor(gap / 2);
87
+ return " ".repeat(left) + text + " ".repeat(gap - left);
88
+ }
89
+
90
+ const BORDER_CHARS = new Set(["─", "│", "╭", "╮", "╰", "╯"]);
91
+ const LOGO_EDGE_CHARS = new Set(["╔", "╗", "╚", "╝", "║", "═"]);
92
+
93
+ // style:
94
+ // gradient —— 整行按列渐变(logo 行、边框行)
95
+ // text:<sgr> —— 边框字符渐变,其余字符用固定 SGR 样式(标语行)
96
+ function paintLine(line, { phase, totalWidth, style }) {
97
+ let out = "";
98
+ let col = 0;
99
+ let lastColor = "";
100
+ for (const ch of line) {
101
+ const width = charDisplayWidth(ch.codePointAt(0));
102
+ if (ch === " ") {
103
+ if (lastColor !== "") {
104
+ out += RESET;
105
+ lastColor = "";
106
+ }
107
+ out += ch;
108
+ col += width;
109
+ continue;
110
+ }
111
+ let color;
112
+ if (style === "gradient" || BORDER_CHARS.has(ch)) {
113
+ const t = phase + col / Math.max(1, totalWidth);
114
+ let [r, g, b] = gradientAt(t);
115
+ if (LOGO_EDGE_CHARS.has(ch)) {
116
+ // logo 描边字符调暗,让实心块更立体
117
+ r = Math.round(r * 0.55);
118
+ g = Math.round(g * 0.55);
119
+ b = Math.round(b * 0.55);
120
+ }
121
+ color = fg(r, g, b);
122
+ } else {
123
+ color = style;
124
+ }
125
+ if (color !== lastColor) {
126
+ out += RESET + color;
127
+ lastColor = color;
128
+ }
129
+ out += ch;
130
+ col += width;
131
+ }
132
+ if (lastColor !== "") {
133
+ out += RESET;
134
+ }
135
+ return out;
136
+ }
137
+
138
+ const TAGLINE = "Claude360 · 模型站接入助手";
139
+ const SUBLINE = "一键配置 Claude Code / Codex / API Key / 充值";
140
+
141
+ function buildBannerLayout({ version, baseUrl }) {
142
+ const pad = 1;
143
+ const innerWidth = Math.max(
144
+ ...LOGO_LINES.map((line) => displayWidth(line)),
145
+ displayWidth(TAGLINE),
146
+ displayWidth(SUBLINE),
147
+ ) + pad * 2;
148
+ const top = `╭${"─".repeat(innerWidth)}╮`;
149
+ const bottom = `╰${"─".repeat(innerWidth)}╯`;
150
+ const boxed = (text) => `│${padCenter(text, innerWidth)}│`;
151
+ const footer = ` 版本 v${version || "-"} · 官网 ${baseUrl}`;
152
+ return {
153
+ totalWidth: innerWidth + 2,
154
+ rows: [
155
+ { text: top, style: "gradient" },
156
+ ...LOGO_LINES.map((line) => ({ text: boxed(line), style: "gradient" })),
157
+ { text: boxed(""), style: "gradient" },
158
+ { text: boxed(TAGLINE), style: "title" },
159
+ { text: boxed(SUBLINE), style: "sub" },
160
+ { text: bottom, style: "gradient" },
161
+ { text: footer, style: "footer" },
162
+ ],
163
+ };
164
+ }
165
+
166
+ // 终端太窄放不下大 logo 时的紧凑版
167
+ function renderCompactBanner({ version, baseUrl, color, phase }) {
13
168
  const lines = [
14
- "",
15
- ...LOGO_LINES,
16
- "",
17
- " Claude360 模型站接入助手",
18
- " 一键配置 Claude Code / Codex / API Key / 充值",
19
- "",
20
- `版本:${version || "-"} | 官网:${baseUrl}`,
21
- "",
169
+ "Claude360 CLI · 模型站接入助手",
170
+ SUBLINE,
171
+ `版本 v${version || "-"} · 官网 ${baseUrl}`,
22
172
  ];
23
- return lines.join("\n");
173
+ if (!color) {
174
+ return lines.join("\n");
175
+ }
176
+ return [
177
+ paintLine(lines[0], { phase, totalWidth: displayWidth(lines[0]), style: "gradient" }),
178
+ `${DIM}${lines[1]}${RESET}`,
179
+ `${DIM}${lines[2]}${RESET}`,
180
+ ].join("\n");
181
+ }
182
+
183
+ export function renderBanner({
184
+ version = "",
185
+ baseUrl = BRAND_BASE_URL,
186
+ color = false,
187
+ phase = 0,
188
+ columns = 0,
189
+ } = {}) {
190
+ const { totalWidth, rows } = buildBannerLayout({ version, baseUrl });
191
+ if (columns > 0 && columns < totalWidth) {
192
+ return renderCompactBanner({ version, baseUrl, color, phase });
193
+ }
194
+ if (!color) {
195
+ return rows.map((row) => row.text).join("\n");
196
+ }
197
+ const styles = {
198
+ title: `${BOLD}${fg(240, 246, 255)}`,
199
+ sub: `${fg(148, 163, 184)}`,
200
+ };
201
+ return rows.map((row) => {
202
+ if (row.style === "footer") {
203
+ return `${DIM}${row.text}${RESET}`;
204
+ }
205
+ const style = row.style === "gradient" ? "gradient" : styles[row.style];
206
+ return paintLine(row.text, { phase, totalWidth, style });
207
+ }).join("\n");
208
+ }
209
+
210
+ // 流光动效:固定布局重绘若干帧,仅移动渐变相位。
211
+ // 非 TTY / NO_COLOR / 终端过窄时直接输出静态版本。
212
+ export async function playBanner({
213
+ version = "",
214
+ baseUrl = BRAND_BASE_URL,
215
+ stream = process.stdout,
216
+ frames = 24,
217
+ intervalMs = 50,
218
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
219
+ } = {}) {
220
+ const columns = stream && stream.columns ? stream.columns : 0;
221
+ const animated = colorEnabled(stream)
222
+ && typeof stream.write === "function"
223
+ && (!columns || columns >= buildBannerLayout({ version, baseUrl }).totalWidth);
224
+ if (!animated) {
225
+ const text = renderBanner({ version, baseUrl, color: colorEnabled(stream), columns });
226
+ if (stream && typeof stream.write === "function") {
227
+ stream.write(text + "\n");
228
+ } else {
229
+ console.log(text);
230
+ }
231
+ return;
232
+ }
233
+ const lineCount = renderBanner({ version, baseUrl }).split("\n").length;
234
+ stream.write(`${ESC}?25l`);
235
+ try {
236
+ for (let i = 0; i <= frames; i += 1) {
237
+ const phase = i / frames;
238
+ stream.write(renderBanner({ version, baseUrl, color: true, phase }) + "\n");
239
+ if (i < frames) {
240
+ await sleep(intervalMs);
241
+ stream.write(`${ESC}${lineCount}A`);
242
+ }
243
+ }
244
+ } finally {
245
+ stream.write(`${ESC}?25h`);
246
+ }
24
247
  }
25
248
 
26
249
  export const CHECK_OK = "✓";
27
250
  export const CHECK_WARN = "!";
28
251
  export const CHECK_FAIL = "×";
29
252
 
30
- export function formatCheckLine(status, text) {
253
+ const CHECK_COLORS = {
254
+ ok: [74, 222, 128],
255
+ warn: [250, 204, 21],
256
+ fail: [248, 113, 113],
257
+ };
258
+
259
+ export function formatCheckLine(status, text, { color = false } = {}) {
31
260
  const symbols = { ok: CHECK_OK, warn: CHECK_WARN, fail: CHECK_FAIL };
32
261
  const symbol = symbols[status] || CHECK_WARN;
33
- return `${symbol} ${text}`;
262
+ if (!color) {
263
+ return `${symbol} ${text}`;
264
+ }
265
+ const [r, g, b] = CHECK_COLORS[status] || CHECK_COLORS.warn;
266
+ return `${fg(r, g, b)}${BOLD}${symbol}${RESET} ${text}`;
34
267
  }
35
268
 
36
- export function renderEnvironmentChecks(checks = []) {
269
+ export function renderEnvironmentChecks(checks = [], { color = false } = {}) {
37
270
  return [
38
271
  "正在检查运行环境...",
39
272
  "",
40
- ...checks.map((check) => formatCheckLine(check.status, check.text)),
273
+ ...checks.map((check) => formatCheckLine(check.status, check.text, { color })),
41
274
  ].join("\n");
42
275
  }
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ import { createRequire } from "node:module";
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 { renderBanner, formatCheckLine } from "./banner.js";
9
+ import { colorEnabled, 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 {
@@ -82,9 +82,15 @@ export async function runCli({
82
82
  let config = await configStore.load();
83
83
  const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
84
84
  let api = createApiClient({ baseUrl, cliToken: config.cliToken || "" });
85
+ // 仅在真实终端(writeLine 未被测试替换)启用彩色与动效
86
+ const fancyOutput = writeLine === console.log && colorEnabled();
85
87
 
86
88
  if (showBanner) {
87
- writeLine(renderBanner({ version, baseUrl }));
89
+ if (fancyOutput) {
90
+ await playBanner({ version, baseUrl });
91
+ } else {
92
+ writeLine(renderBanner({ version, baseUrl }));
93
+ }
88
94
  await showEnvironmentChecks();
89
95
  }
90
96
 
@@ -113,7 +119,7 @@ export async function runCli({
113
119
  checks.push(config.cliToken
114
120
  ? { status: "ok", text: "已登录 Claude360" }
115
121
  : { status: "warn", text: "尚未登录 Claude360" });
116
- writeLine(checks.map((check) => formatCheckLine(check.status, check.text)).join("\n"));
122
+ writeLine(checks.map((check) => formatCheckLine(check.status, check.text, { color: fancyOutput })).join("\n"));
117
123
  writeLine("");
118
124
  }
119
125
 
package/src/menu.js CHANGED
@@ -1,6 +1,21 @@
1
1
  // 菜单:区分首次使用菜单与日常使用菜单(PRD 第 10、11 章)。
2
2
  // 菜单自带分区标题与选择键(1-9 / A / 0 / Q),由 renderMenu 渲染、
3
3
  // resolveMenuSelection 解析,promptMenu 循环读取直到输入有效。
4
+ // 渲染默认无色(color: false)以保证测试与管道输出稳定,
5
+ // 真实终端下由 promptMenu 自动开启彩色分区样式。
6
+
7
+ import { colorEnabled, displayWidth } from "./banner.js";
8
+
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 MENU_RULE_WIDTH = 46;
4
19
 
5
20
  export function buildFirstRunMenu() {
6
21
  return {
@@ -32,14 +47,14 @@ export function buildDailyMenu() {
32
47
  title: "请选择功能:",
33
48
  sections: [
34
49
  {
35
- title: "--- 快速启动 ---",
50
+ title: "快速启动",
36
51
  items: [
37
52
  { key: "1", label: "启动 Claude Code", value: "launch_claude" },
38
53
  { key: "2", label: "启动 Codex", value: "launch_codex" },
39
54
  ],
40
55
  },
41
56
  {
42
- title: "--- Claude360 ---",
57
+ title: "Claude360",
43
58
  items: [
44
59
  { key: "3", label: "余额与充值", value: "balance_topup" },
45
60
  { key: "4", label: "创建新的 API Key", value: "create_key" },
@@ -47,7 +62,7 @@ export function buildDailyMenu() {
47
62
  ],
48
63
  },
49
64
  {
50
- title: "--- 工具配置 ---",
65
+ title: "工具配置",
51
66
  items: [
52
67
  { key: "6", label: "Claude Code 配置", value: "claude_code_menu" },
53
68
  { key: "7", label: "Codex 配置", value: "codex_menu" },
@@ -56,7 +71,7 @@ export function buildDailyMenu() {
56
71
  ],
57
72
  },
58
73
  {
59
- title: "--- 维护 ---",
74
+ title: "维护",
60
75
  items: [
61
76
  { key: "A", label: "诊断与修复", value: "diagnostics_menu" },
62
77
  { key: "B", label: "安装或更新工具", value: "install_tools" },
@@ -68,17 +83,37 @@ export function buildDailyMenu() {
68
83
  };
69
84
  }
70
85
 
71
- export function renderMenu(menu) {
86
+ // 分区标题渲染为 `─── 标题 ──────…` 形式的分隔线,让各组功能在视觉上彼此分开
87
+ function renderSectionRule(title, color) {
88
+ const lead = "───";
89
+ const label = ` ${title} `;
90
+ const tail = "─".repeat(Math.max(4, MENU_RULE_WIDTH - lead.length - displayWidth(label)));
91
+ if (!color) {
92
+ return `${lead}${label}${tail}`;
93
+ }
94
+ return `${RULE_COLOR}${lead}${RESET}${BOLD}${SECTION_COLOR}${label}${RESET}${RULE_COLOR}${tail}${RESET}`;
95
+ }
96
+
97
+ function renderItem(item, color) {
98
+ const plain = ` ${item.key}. ${item.label}`;
99
+ if (!color) {
100
+ return plain;
101
+ }
102
+ const keyColor = item.value === "exit" ? EXIT_COLOR : KEY_COLOR;
103
+ return ` ${BOLD}${keyColor}${item.key}.${RESET} ${item.label}`;
104
+ }
105
+
106
+ export function renderMenu(menu, { color = false } = {}) {
72
107
  const lines = [];
73
108
  if (menu.title) {
74
- lines.push(menu.title, "");
109
+ lines.push(color ? `${BOLD}${menu.title}${RESET}` : menu.title, "");
75
110
  }
76
111
  for (const section of menu.sections) {
77
112
  if (section.title) {
78
- lines.push(section.title);
113
+ lines.push(renderSectionRule(section.title, color));
79
114
  }
80
115
  for (const item of section.items) {
81
- lines.push(`${item.key}. ${item.label}`);
116
+ lines.push(renderItem(item, color));
82
117
  }
83
118
  lines.push("");
84
119
  }
@@ -107,8 +142,10 @@ export async function promptMenu({ menu, promptInput, writeLine = console.log }
107
142
  if (typeof promptInput !== "function") {
108
143
  throw new Error("缺少菜单输入");
109
144
  }
145
+ // 仅在输出未被测试替换且终端支持时启用彩色
146
+ const color = writeLine === console.log && colorEnabled();
110
147
  while (true) {
111
- writeLine(renderMenu(menu));
148
+ writeLine(renderMenu(menu, { color }));
112
149
  const answer = await promptInput("请输入选项");
113
150
  const value = resolveMenuSelection(menu, answer);
114
151
  if (value !== null) {