claude360 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ui.js ADDED
@@ -0,0 +1,137 @@
1
+ // 终端表格渲染:账户状态 / Key 列表 / 诊断报告等结构化输出的统一视觉组件。
2
+ // 与 banner.js / menu.js 的手写渲染风格保持一致,不引入额外 UI 依赖。
3
+ // 默认无色(color: false),保证测试与管道输出稳定;宽度不足时按列截断,
4
+ // 单行宽表可降级为「字段/值」双列紧凑表,避免撑破终端。
5
+
6
+ import { displayWidth } from "./banner.js";
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);
14
+
15
+ // eslint-disable-next-line no-control-regex
16
+ const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
17
+
18
+ export function stripAnsi(text) {
19
+ return String(text ?? "").replace(ANSI_PATTERN, "");
20
+ }
21
+
22
+ export function cellWidth(text) {
23
+ return displayWidth(stripAnsi(text));
24
+ }
25
+
26
+ // 按显示宽度截断,超出部分以 … 结尾(… 计 1 列宽)
27
+ export function truncateDisplay(text, maxWidth) {
28
+ const plain = String(text ?? "");
29
+ if (maxWidth <= 0 || cellWidth(plain) <= maxWidth) {
30
+ return plain;
31
+ }
32
+ let out = "";
33
+ let width = 0;
34
+ for (const ch of stripAnsi(plain)) {
35
+ const w = displayWidth(ch);
36
+ if (width + w > maxWidth - 1) {
37
+ break;
38
+ }
39
+ out += ch;
40
+ width += w;
41
+ }
42
+ return `${out}…`;
43
+ }
44
+
45
+ function padCell(text, width) {
46
+ const gap = Math.max(0, width - cellWidth(text));
47
+ return `${text}${" ".repeat(gap)}`;
48
+ }
49
+
50
+ function borderLine(widths, [left, mid, right], color) {
51
+ const line = `${left}${widths.map((w) => "─".repeat(w + 2)).join(mid)}${right}`;
52
+ return color ? `${BORDER_COLOR}${line}${RESET}` : line;
53
+ }
54
+
55
+ function contentLine(cells, widths, { color, bold = false } = {}) {
56
+ const bar = color ? `${BORDER_COLOR}│${RESET}` : "│";
57
+ const body = cells
58
+ .map((cell, i) => {
59
+ const padded = padCell(cell, widths[i]);
60
+ return bold && color ? ` ${BOLD}${HEAD_COLOR}${padded}${RESET} ` : ` ${padded} `;
61
+ })
62
+ .join(bar);
63
+ return `${bar}${body}${bar}`;
64
+ }
65
+
66
+ // 计算各列宽:先取内容最大宽,总宽超过可用宽度时从最宽列开始压缩
67
+ function resolveWidths(head, rows, width) {
68
+ const columnCount = Math.max(head.length, ...rows.map((row) => row.length), 0);
69
+ const widths = Array.from({ length: columnCount }, (_, i) =>
70
+ Math.max(
71
+ cellWidth(head[i] ?? ""),
72
+ ...rows.map((row) => cellWidth(row[i] ?? "")),
73
+ 1,
74
+ ));
75
+ if (!width || width <= 0) {
76
+ return widths;
77
+ }
78
+ const overhead = columnCount * 3 + 1; // 边框与内边距
79
+ const available = width - overhead;
80
+ let total = widths.reduce((sum, w) => sum + w, 0);
81
+ // 反复压缩当前最宽列,直到放得下或所有列都到达下限
82
+ const MIN_COL = 4;
83
+ while (total > available) {
84
+ const max = Math.max(...widths);
85
+ if (max <= MIN_COL) {
86
+ break;
87
+ }
88
+ const index = widths.indexOf(max);
89
+ widths[index] -= 1;
90
+ total -= 1;
91
+ }
92
+ return widths;
93
+ }
94
+
95
+ export function renderTable({ head = [], rows = [] } = {}, { color = false, width = 0 } = {}) {
96
+ const widths = resolveWidths(head, rows, width);
97
+ const fit = (row) => widths.map((w, i) => truncateDisplay(row[i] ?? "", w));
98
+ const lines = [borderLine(widths, ["┌", "┬", "┐"], color)];
99
+ if (head.length > 0) {
100
+ lines.push(contentLine(fit(head), widths, { color, bold: true }));
101
+ lines.push(borderLine(widths, ["├", "┼", "┤"], color));
102
+ }
103
+ for (const row of rows) {
104
+ lines.push(contentLine(fit(row), widths, { color }));
105
+ }
106
+ lines.push(borderLine(widths, ["└", "┴", "┘"], color));
107
+ return lines.join("\n");
108
+ }
109
+
110
+ // 双列紧凑表:窄终端下展示「字段 / 值」对,仍保留边框与分组感
111
+ export function renderKeyValueTable(pairs = [], { color = false, width = 0 } = {}) {
112
+ return renderTable(
113
+ { head: [], rows: pairs.map(([key, value]) => [key, value]) },
114
+ { color, width },
115
+ );
116
+ }
117
+
118
+ // 单行横向表,放不下时自动降级为双列表(补充需求第 1 节)
119
+ export function renderStatusTable({ head = [], row = [] } = {}, { color = false, width = 0 } = {}) {
120
+ const wide = renderTable({ head, rows: [row] }, { color });
121
+ const wideWidth = cellWidth(wide.split("\n")[0]);
122
+ if (!width || wideWidth <= width) {
123
+ return wide;
124
+ }
125
+ return renderKeyValueTable(head.map((key, i) => [key, row[i] ?? ""]), { color, width });
126
+ }
127
+
128
+ // 章节标题:与 menu.js 分区分隔线风格一致
129
+ export function renderSectionTitle(title, { color = false, width = 46 } = {}) {
130
+ const lead = "───";
131
+ const label = ` ${title} `;
132
+ const tail = "─".repeat(Math.max(4, width - lead.length - displayWidth(label)));
133
+ if (!color) {
134
+ return `${lead}${label}${tail}`;
135
+ }
136
+ return `${BORDER_COLOR}${lead}${RESET}${BOLD}${HEAD_COLOR}${label}${RESET}${BORDER_COLOR}${tail}${RESET}`;
137
+ }
@@ -0,0 +1,331 @@
1
+ // 推荐工作流(PRD 6.5 / 8.4):流程设计参考 NPX ZCF,内容为 Claude360 自研改编,
2
+ // 文件头保留 attribution。Claude Code 写入 ~/.claude/commands/claude360/,
3
+ // Codex 写入 ~/.codex/prompts/。安装前展示目录、备份同名文件、冲突二次确认,
4
+ // 安装结果按 PRD 第 10 章样式展示。
5
+
6
+ import { copyFile, cp, mkdir, readFile, stat, writeFile } from "node:fs/promises";
7
+ import path from "node:path";
8
+
9
+ import { createBackup } from "./backup.js";
10
+ import { ZCF_ATTRIBUTION_COMMENT } from "./zcf-notice.js";
11
+
12
+ const defaultFs = { copyFile, cp, mkdir, readFile, stat, writeFile };
13
+
14
+ function commandDoc(description, body) {
15
+ return [
16
+ ZCF_ATTRIBUTION_COMMENT,
17
+ "---",
18
+ `description: ${description}`,
19
+ "---",
20
+ "",
21
+ body.trim(),
22
+ "",
23
+ ].join("\n");
24
+ }
25
+
26
+ const SIX_STEP_BODY = `
27
+ 请按六步工作流推进本次任务:$ARGUMENTS
28
+
29
+ 1. **研究**:阅读相关代码与文档,弄清现状与约束,列出已知/未知。
30
+ 2. **构思**:给出至少 2 种可行方案,说明取舍,推荐其一。
31
+ 3. **计划**:拆解为可验证的最小步骤,列出涉及文件与风险。
32
+ 4. **执行**:按计划实现,保持最小改动面,遵循项目现有风格。
33
+ 5. **优化**:自查代码质量、边界条件与错误处理,运行测试。
34
+ 6. **评审**:总结改动点、影响范围与后续建议,输出中文报告。
35
+
36
+ 每步完成后简要汇报再进入下一步;计划阶段需等待用户确认。
37
+ `;
38
+
39
+ const FEAT_BODY = `
40
+ 请基于以下需求完成功能规划与 UX 设计:$ARGUMENTS
41
+
42
+ 输出包含:
43
+ 1. 需求澄清:目标用户、核心场景、价值假设、范围边界(做什么 / 不做什么)。
44
+ 2. 功能拆解:模块划分、用户故事(As a... I want... So that...)、优先级(P0/P1/P2)。
45
+ 3. 交互与 UX:关键页面 / 流程草图(文字或 ASCII)、状态与异常分支、文案建议。
46
+ 4. 技术落地:与现有代码的衔接点、接口草案、开发任务清单与依赖顺序。
47
+ 5. 风险与待确认问题清单。
48
+
49
+ 全部使用中文,结论先行,可直接用于评审。
50
+ `;
51
+
52
+ const GIT_COMMIT_BODY = `
53
+ 请阅读 git 暂存区改动(git diff --cached;为空时改读 git diff),
54
+ 生成符合 Conventional Commits 的中文提交信息:type(scope): 描述。
55
+
56
+ 要求:
57
+ - type 从 feat/fix/docs/style/refactor/perf/test/chore 中选择。
58
+ - 标题不超过 50 字,body 补充动机与影响范围。
59
+ - 多个不相关改动时建议拆分提交并给出拆分方案。
60
+ - 只输出提交信息本身,不要执行 git commit,除非用户明确要求。
61
+ `;
62
+
63
+ const GIT_ROLLBACK_BODY = `
64
+ 请协助安全回滚 git 改动:$ARGUMENTS
65
+
66
+ 步骤:
67
+ 1. 先运行 git status 与 git log --oneline -10,确认当前状态与目标提交。
68
+ 2. 区分场景给出方案:未暂存改动(git restore)、已暂存(git restore --staged)、
69
+ 已提交未推送(git reset)、已推送(git revert,禁止 force push 共享分支)。
70
+ 3. 涉及丢弃用户数据的操作必须先列出将丢失的内容并等待用户确认。
71
+ `;
72
+
73
+ const GIT_CLEAN_BRANCHES_BODY = `
74
+ 请协助清理本地分支:
75
+
76
+ 1. 列出已合并到主分支的本地分支(git branch --merged)。
77
+ 2. 标出可安全删除的分支,排除当前分支、main/master/develop 等长期分支。
78
+ 3. 输出建议执行的删除命令清单,等待用户确认后再执行。
79
+ 4. 未合并分支默认不删除;如用户要求强删,逐个确认并提示风险。
80
+ `;
81
+
82
+ const GIT_WORKTREE_BODY = `
83
+ 请协助管理 git worktree:$ARGUMENTS
84
+
85
+ 支持的操作:
86
+ - 新建:git worktree add ../<repo>-<branch> -b <branch>,并说明目录位置。
87
+ - 列出:git worktree list,标注每个 worktree 的分支与状态。
88
+ - 清理:git worktree remove <path>,删除前确认无未提交改动。
89
+
90
+ 操作前先展示将执行的命令,破坏性操作需用户确认。
91
+ `;
92
+
93
+ const INIT_PROJECT_BODY = `
94
+ 请初始化或更新本项目的 AI 协作配置:
95
+
96
+ 1. 分析项目结构、技术栈、构建与测试命令。
97
+ 2. 生成或更新项目根目录 CLAUDE.md:项目概述、目录结构、常用命令、
98
+ 代码风格约定、注意事项。已有内容以标记区块方式增量更新,不覆盖用户自定义内容。
99
+ 3. 输出更新摘要。全部使用中文。
100
+ `;
101
+
102
+ const BMAD_BODY = `
103
+ 请协助安装 BMad-Method 敏捷开发扩展:
104
+
105
+ 1. 说明 BMad-Method 的用途(敏捷开发工作流:PO/架构师/开发/QA 等角色协作)。
106
+ 2. 检查 Node.js 环境后,提示用户确认执行:npx bmad-method install
107
+ 3. 安装过程出错时给出排查建议(网络、npm 镜像、Node 版本)。
108
+ 4. 安装完成后说明如何在对话中使用 BMad 工作流。
109
+
110
+ 注意:该命令会写入项目目录,执行前必须获得用户确认。
111
+ `;
112
+
113
+ // ──────────────────────────────────────────────
114
+ // Claude Code 推荐工作流(PRD 6.5)
115
+ // ──────────────────────────────────────────────
116
+
117
+ export const CLAUDE_WORKFLOWS = [
118
+ {
119
+ id: "common-tools",
120
+ label: "通用工具工作流",
121
+ hint: "初始化、通用 agents、基础开发规范",
122
+ files: [
123
+ { name: "init-project.md", content: commandDoc("初始化 / 更新项目 CLAUDE.md 与协作规范", INIT_PROJECT_BODY) },
124
+ ],
125
+ },
126
+ {
127
+ id: "six-steps",
128
+ label: "六步工作流",
129
+ hint: "需求到实现的标准开发流程",
130
+ files: [
131
+ { name: "workflow.md", content: commandDoc("六步工作流:研究→构思→计划→执行→优化→评审", SIX_STEP_BODY) },
132
+ ],
133
+ },
134
+ {
135
+ id: "feat-ux",
136
+ label: "功能规划和 UX 设计",
137
+ hint: "面向产品功能设计、交互设计、PRD 拆解",
138
+ files: [
139
+ { name: "feat.md", content: commandDoc("功能规划与 UX 设计", FEAT_BODY) },
140
+ ],
141
+ },
142
+ {
143
+ id: "git",
144
+ label: "Git 指令工作流",
145
+ hint: "commit、rollback、cleanBranches、worktree",
146
+ files: [
147
+ { name: "git-commit.md", content: commandDoc("生成规范的中文提交信息", GIT_COMMIT_BODY) },
148
+ { name: "git-rollback.md", content: commandDoc("安全回滚 git 改动", GIT_ROLLBACK_BODY) },
149
+ { name: "git-clean-branches.md", content: commandDoc("清理已合并的本地分支", GIT_CLEAN_BRANCHES_BODY) },
150
+ { name: "git-worktree.md", content: commandDoc("管理 git worktree", GIT_WORKTREE_BODY) },
151
+ ],
152
+ },
153
+ {
154
+ id: "bmad",
155
+ label: "BMad-Method 扩展安装器",
156
+ hint: "支持敏捷开发相关工作流",
157
+ files: [
158
+ { name: "bmad-init.md", content: commandDoc("安装 BMad-Method 敏捷开发扩展", BMAD_BODY) },
159
+ ],
160
+ },
161
+ ];
162
+
163
+ // ──────────────────────────────────────────────
164
+ // Codex 推荐工作流(PRD 8.4):写入 ~/.codex/prompts/
165
+ // ──────────────────────────────────────────────
166
+
167
+ export const CODEX_WORKFLOWS = [
168
+ {
169
+ id: "six-steps",
170
+ label: "六步工作流",
171
+ hint: "需求到实现的标准开发流程",
172
+ files: [
173
+ { name: "workflow.md", content: commandDoc("六步工作流:研究→构思→计划→执行→优化→评审", SIX_STEP_BODY) },
174
+ ],
175
+ },
176
+ {
177
+ id: "git",
178
+ label: "Git 指令工作流",
179
+ hint: "commit、rollback、cleanBranches、worktree",
180
+ files: [
181
+ { name: "git-commit.md", content: commandDoc("生成规范的中文提交信息", GIT_COMMIT_BODY) },
182
+ { name: "git-rollback.md", content: commandDoc("安全回滚 git 改动", GIT_ROLLBACK_BODY) },
183
+ { name: "git-clean-branches.md", content: commandDoc("清理已合并的本地分支", GIT_CLEAN_BRANCHES_BODY) },
184
+ { name: "git-worktree.md", content: commandDoc("管理 git worktree", GIT_WORKTREE_BODY) },
185
+ ],
186
+ },
187
+ {
188
+ id: "feat-ux",
189
+ label: "功能规划和 UX 设计",
190
+ hint: "面向产品功能设计、交互设计、PRD 拆解",
191
+ files: [
192
+ { name: "feat.md", content: commandDoc("功能规划与 UX 设计", FEAT_BODY) },
193
+ ],
194
+ },
195
+ {
196
+ id: "bmad",
197
+ label: "BMad-Method 扩展安装器",
198
+ hint: "支持敏捷开发相关工作流",
199
+ files: [
200
+ { name: "bmad-init.md", content: commandDoc("安装 BMad-Method 敏捷开发扩展", BMAD_BODY) },
201
+ ],
202
+ },
203
+ ];
204
+
205
+ async function fileExists(fs, filePath) {
206
+ try {
207
+ await fs.stat(filePath);
208
+ return true;
209
+ } catch (error) {
210
+ if (error?.code === "ENOENT") {
211
+ return false;
212
+ }
213
+ throw error;
214
+ }
215
+ }
216
+
217
+ // 通用安装流程:多选 → 展示写入目录 → 备份已有同名文件 → 冲突二次确认 → 写入 → 结果展示
218
+ async function installWorkflowSet({
219
+ workflows,
220
+ targetDir,
221
+ backupBaseDir,
222
+ multiSelect,
223
+ confirm,
224
+ writeLine,
225
+ fs,
226
+ now,
227
+ usageHint,
228
+ }) {
229
+ if (typeof multiSelect !== "function" || typeof confirm !== "function") {
230
+ throw new Error("缺少交互输入");
231
+ }
232
+ const selected = await multiSelect({
233
+ message: "请选择要安装的工作流:",
234
+ choices: workflows.map((workflow) => ({
235
+ label: workflow.label,
236
+ value: workflow.id,
237
+ hint: workflow.hint,
238
+ })),
239
+ });
240
+ if (selected.length === 0) {
241
+ writeLine("已跳过工作流安装。");
242
+ return { installed: [], skipped: true };
243
+ }
244
+
245
+ const picked = workflows.filter((workflow) => selected.includes(workflow.id));
246
+ writeLine(`安装将写入目录:${targetDir}`);
247
+
248
+ // 备份已存在的同名文件(一次性集中备份到时间戳目录)
249
+ const targets = picked.flatMap((workflow) => workflow.files.map((file) => path.join(targetDir, file.name)));
250
+ const { backupDir } = await createBackup({ baseDir: backupBaseDir, paths: targets, fs, now });
251
+ if (backupDir) {
252
+ writeLine(`✓ 已创建备份:${backupDir}`);
253
+ }
254
+
255
+ const installed = [];
256
+ for (const workflow of picked) {
257
+ writeLine(`正在安装工作流:${workflow.label}...`);
258
+ let wroteAny = false;
259
+ for (const file of workflow.files) {
260
+ const filePath = path.join(targetDir, file.name);
261
+ if (await fileExists(fs, filePath)) {
262
+ const existing = await fs.readFile(filePath, "utf8");
263
+ if (existing !== file.content) {
264
+ const approved = await confirm(`文件已存在:${filePath}\n是否覆盖?(原文件已备份)`);
265
+ if (!approved) {
266
+ writeLine(`已跳过:${file.name}`);
267
+ continue;
268
+ }
269
+ }
270
+ }
271
+ await fs.mkdir(targetDir, { recursive: true });
272
+ await fs.writeFile(filePath, file.content, "utf8");
273
+ writeLine(`✓ 已安装命令:${file.name}`);
274
+ wroteAny = true;
275
+ }
276
+ if (wroteAny) {
277
+ installed.push(workflow.id);
278
+ writeLine(`✓ ${workflow.label}安装成功`);
279
+ }
280
+ }
281
+ if (installed.length > 0 && usageHint) {
282
+ writeLine(usageHint);
283
+ }
284
+ return { installed, skipped: false };
285
+ }
286
+
287
+ export function resolveClaudeCommandsDir(claudeDir) {
288
+ return path.join(claudeDir, "commands", "claude360");
289
+ }
290
+
291
+ export async function installClaudeWorkflows({
292
+ claudeDir,
293
+ multiSelect,
294
+ confirm,
295
+ writeLine = console.log,
296
+ fs = defaultFs,
297
+ now,
298
+ } = {}) {
299
+ return installWorkflowSet({
300
+ workflows: CLAUDE_WORKFLOWS,
301
+ targetDir: resolveClaudeCommandsDir(claudeDir),
302
+ backupBaseDir: claudeDir,
303
+ multiSelect,
304
+ confirm,
305
+ writeLine,
306
+ fs,
307
+ now,
308
+ usageHint: "在 Claude Code 中使用 /claude360:<命令名> 调用,例如 /claude360:workflow。",
309
+ });
310
+ }
311
+
312
+ export async function installCodexWorkflows({
313
+ codexDir,
314
+ multiSelect,
315
+ confirm,
316
+ writeLine = console.log,
317
+ fs = defaultFs,
318
+ now,
319
+ } = {}) {
320
+ return installWorkflowSet({
321
+ workflows: CODEX_WORKFLOWS,
322
+ targetDir: path.join(codexDir, "prompts"),
323
+ backupBaseDir: codexDir,
324
+ multiSelect,
325
+ confirm,
326
+ writeLine,
327
+ fs,
328
+ now,
329
+ usageHint: "在 Codex 中使用 /prompts 或输入 /<命令名> 调用,例如 /workflow。",
330
+ });
331
+ }
@@ -0,0 +1,40 @@
1
+ // 开源参考声明(PRD 第 2 章):进入借鉴自 NPX ZCF 的初始化 / 工作流 /
2
+ // MCP / Skill / AGENTS 配置流程前展示,用户选择「继续」才进入流程。
3
+ // 普通余额查看、充值、打开控制台、创建 Key 不展示。
4
+
5
+ export const ZCF_PROJECT_URL = "https://github.com/UfoMiao/zcf";
6
+
7
+ export const OPEN_SOURCE_NOTICE = [
8
+ "开源参考声明",
9
+ "",
10
+ "本功能中的交互式初始化流程、工作流导入、MCP 推荐配置等设计,",
11
+ "借鉴并参考了开源项目 NPX ZCF 的部分功能体验。",
12
+ "",
13
+ "感谢 NPX ZCF 作者 UfoMiao 的开源贡献。",
14
+ `项目地址:${ZCF_PROJECT_URL}`,
15
+ "",
16
+ "Claude360 CLI 会在此基础上结合 Claude360 模型站能力,",
17
+ "提供授权登录、API Key、余额充值、Claude Code / Codex 配置注入等服务。",
18
+ ].join("\n");
19
+
20
+ // 工作流 / AGENTS 等改编内容的文件头 attribution(PRD 2.3 / 6.5)
21
+ export const ZCF_ATTRIBUTION_COMMENT = [
22
+ "<!--",
23
+ ` 本工作流参考并改编自开源项目 NPX ZCF(${ZCF_PROJECT_URL}),`,
24
+ " 感谢作者 UfoMiao 的开源贡献。由 Claude360 CLI 安装与维护。",
25
+ "-->",
26
+ ].join("\n");
27
+
28
+ // 展示声明并询问是否继续;返回 true 表示继续,false 表示返回上级菜单。
29
+ export async function showOpenSourceNotice({ promptSelect, writeLine = console.log } = {}) {
30
+ if (typeof promptSelect !== "function") {
31
+ throw new Error("缺少选择输入");
32
+ }
33
+ writeLine(OPEN_SOURCE_NOTICE);
34
+ writeLine("");
35
+ const action = await promptSelect("是否继续?", [
36
+ { label: "继续", value: "continue" },
37
+ { label: "返回", value: "back" },
38
+ ]);
39
+ return action === "continue";
40
+ }