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.
- package/README.md +107 -126
- package/package.json +1 -1
- package/src/account-status.js +8 -5
- package/src/auth.js +9 -2
- package/src/banner.js +6 -6
- package/src/cc-switch.js +11 -5
- package/src/colors.js +31 -0
- package/src/diagnostics.js +36 -17
- package/src/index.js +105 -81
- package/src/init-config.js +39 -25
- package/src/init-flow.js +13 -6
- package/src/mcp-skill.js +32 -21
- package/src/menu.js +8 -7
- package/src/messages.js +78 -0
- package/src/onboarding.js +12 -5
- package/src/prompts.js +7 -6
- package/src/tool-launcher.js +109 -46
- package/src/topup.js +6 -2
- package/src/ui.js +9 -8
- package/src/workflows.js +10 -7
package/src/init-config.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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
|
-
|
|
394
|
+
msg.info("已跳过默认模型配置。");
|
|
383
395
|
return { skipped: true };
|
|
384
396
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
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
|
-
|
|
404
|
+
msg.error(`生成的 Codex 配置 TOML 校验失败:${tomlError}\n已放弃写入,原配置保持不变。`);
|
|
391
405
|
return { skipped: true };
|
|
392
406
|
}
|
|
393
|
-
const { backupDir } = await createBackup({ baseDir: codexDir, paths: [
|
|
407
|
+
const { backupDir } = await createBackup({ baseDir: codexDir, paths: [profileConfigPath], fs, now });
|
|
394
408
|
if (backupDir) {
|
|
395
|
-
|
|
409
|
+
msg.success(`已创建备份:${backupDir}`);
|
|
396
410
|
}
|
|
397
411
|
await fs.mkdir(codexDir, { recursive: true });
|
|
398
|
-
await fs.writeFile(
|
|
399
|
-
|
|
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
|
|
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
|
-
|
|
19
|
+
msg.step(`第 ${index} 步:${step.title}`);
|
|
13
20
|
try {
|
|
14
21
|
const result = await step.run();
|
|
15
22
|
if (step.required && result === false) {
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
28
|
+
msg.error(`${step.title}失败:${safeErrorMessage(error)}`);
|
|
29
|
+
msg.hint("初始化已中止,可稍后在菜单中重试。");
|
|
23
30
|
return { completed: false, abortedAt: step.title };
|
|
24
31
|
}
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
+
msg.info("已跳过 MCP 安装。");
|
|
205
213
|
return [];
|
|
206
214
|
}
|
|
207
215
|
|
|
208
|
-
|
|
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
|
-
|
|
221
|
+
msg.info("已取消 MCP 安装。");
|
|
214
222
|
return [];
|
|
215
223
|
}
|
|
216
224
|
|
|
217
225
|
const installed = [];
|
|
218
226
|
for (const mcp of approved) {
|
|
219
|
-
|
|
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
|
-
|
|
231
|
+
msg.success(`已安装 MCP:${mcp.id}`);
|
|
224
232
|
} else {
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
+
msg.info("已跳过 MCP 安装。");
|
|
293
302
|
return [];
|
|
294
303
|
}
|
|
295
304
|
if (!(await confirm(`即将在 ${configPath} 中写入 ${approved.map((mcp) => mcp.id).join("、")} 的 MCP 配置(写入前备份)。\n是否继续?`))) {
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
8
|
+
import { BOLD, RESET, colorLevel, theme, toLevel } from "./colors.js";
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// 分区/选择键配色:从统一语义主题取色(见 colors.js theme),不再各自硬编码 RGB。
|
|
11
11
|
function palette(level) {
|
|
12
|
+
const t = theme(level);
|
|
12
13
|
return {
|
|
13
|
-
rule:
|
|
14
|
-
section:
|
|
15
|
-
key:
|
|
16
|
-
exit:
|
|
17
|
-
desc:
|
|
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;
|
package/src/messages.js
ADDED
|
@@ -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
|
-
|
|
35
|
+
msg.warn("npm 全局目录可能不可写,安装工具时可能需要修复目录权限或切换到用户级 npm prefix。");
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
msg.info(`已跳过 ${result.target} 安装。`);
|
|
67
74
|
continue;
|
|
68
75
|
}
|
|
69
76
|
if (result?.ok === false) {
|
|
70
|
-
|
|
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,
|
|
23
|
+
import { BOLD, RESET, colorLevel, theme, toLevel } from "./colors.js";
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// 交互配色:从统一语义主题取色(见 colors.js theme),不再各自硬编码 RGB。
|
|
26
26
|
function palette(level) {
|
|
27
|
+
const t = theme(level);
|
|
27
28
|
return {
|
|
28
|
-
cyan:
|
|
29
|
-
red:
|
|
30
|
-
gray:
|
|
31
|
-
green:
|
|
29
|
+
cyan: t.heading, // 品牌高亮:选中项
|
|
30
|
+
red: t.error, // 危险操作:NO 高亮
|
|
31
|
+
gray: t.hint, // 次要说明 / 未选中项
|
|
32
|
+
green: t.success, // 完成态答案
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|