@double-coding/flow2spec
Advanced tools
+347
| const fs = require("fs"); | ||
| const path = require("path"); | ||
| const { AGENTS } = require("./agents"); | ||
| const { | ||
| loadFlow2specConfig, | ||
| CONFIG_FILENAME, | ||
| } = require("./flow2specConfig"); | ||
| const { resolveDeveloperContext } = require("./developerId"); | ||
| const knowledgeEngine = require("./knowledgeEngine"); | ||
| const STATUS = { | ||
| pass: "pass", | ||
| warning: "warning", | ||
| error: "error", | ||
| }; | ||
| function numericVersion(version) { | ||
| return String(version || "") | ||
| .replace(/^v/, "") | ||
| .split(".") | ||
| .slice(0, 3) | ||
| .map((part) => Number.parseInt(part, 10) || 0); | ||
| } | ||
| function compareVersions(left, right) { | ||
| const a = numericVersion(left); | ||
| const b = numericVersion(right); | ||
| for (let index = 0; index < 3; index += 1) { | ||
| const difference = (a[index] || 0) - (b[index] || 0); | ||
| if (difference !== 0) return difference; | ||
| } | ||
| return 0; | ||
| } | ||
| function satisfiesNodeEngine(version, engine) { | ||
| const minimum = String(engine || "").match(/>=\s*v?(\d+(?:\.\d+){0,2})/); | ||
| if (!minimum) return true; | ||
| return compareVersions(version, minimum[1]) >= 0; | ||
| } | ||
| function makeCheck(id, label, status, message, repair = null, details) { | ||
| const check = { id, label, status, message, repair }; | ||
| if (details !== undefined) check.details = details; | ||
| return check; | ||
| } | ||
| function checkKnowledge(cwd) { | ||
| try { | ||
| const graph = knowledgeEngine.loadKnowledgeGraph(cwd); | ||
| const validation = knowledgeEngine.validateKnowledgeGraph(graph, { | ||
| strictRevision: true, | ||
| }); | ||
| const normalized = knowledgeEngine.normalizeRoutingWithGraph(graph); | ||
| const routingDrift = | ||
| normalized.changed || | ||
| knowledgeEngine.stableStringify(normalized.routing) !== | ||
| knowledgeEngine.stableStringify(graph.routing); | ||
| const details = { | ||
| topicCount: validation.topicCount, | ||
| issues: validation.issues, | ||
| warnings: validation.warnings, | ||
| routingDrift, | ||
| }; | ||
| if (!validation.ok || routingDrift) { | ||
| const reasons = [...validation.issues]; | ||
| if (routingDrift) reasons.push("routing metadata differs from topic frontmatter"); | ||
| return makeCheck( | ||
| "knowledge", | ||
| "知识库", | ||
| STATUS.error, | ||
| `知识图存在 ${reasons.length} 个问题。`, | ||
| "运行 flow2spec kb build --fix-topics,再运行 flow2spec kb check --strict。", | ||
| details, | ||
| ); | ||
| } | ||
| if (validation.warnings.length > 0) { | ||
| return makeCheck( | ||
| "knowledge", | ||
| "知识库", | ||
| STATUS.warning, | ||
| `知识图可用,但有 ${validation.warnings.length} 条警告。`, | ||
| "运行 flow2spec kb check --strict 查看详情。", | ||
| details, | ||
| ); | ||
| } | ||
| return makeCheck( | ||
| "knowledge", | ||
| "知识库", | ||
| STATUS.pass, | ||
| `${validation.topicCount} 个 topic 校验通过,routing 无漂移。`, | ||
| null, | ||
| details, | ||
| ); | ||
| } catch (error) { | ||
| return makeCheck( | ||
| "knowledge", | ||
| "知识库", | ||
| STATUS.error, | ||
| error.message || String(error), | ||
| "确认 .Knowledge/manifest-routing.json 与其引用的 topic、matcher 均存在且为有效格式。", | ||
| ); | ||
| } | ||
| } | ||
| function isIgnoredByRootGitignore(cwd, entry) { | ||
| const gitignore = path.join(cwd, ".gitignore"); | ||
| if (!fs.existsSync(gitignore)) return false; | ||
| const lines = fs | ||
| .readFileSync(gitignore, "utf8") | ||
| .split(/\r?\n/) | ||
| .map((line) => line.trim()) | ||
| .filter((line) => line && !line.startsWith("#")); | ||
| return lines.includes(entry) || lines.includes(entry.replace(/\/$/, "")); | ||
| } | ||
| function runDoctor(cwd = process.cwd(), options = {}) { | ||
| const pkg = options.package || require("../package.json"); | ||
| const nodeVersion = options.nodeVersion || process.version; | ||
| const knowledgeCheck = options.knowledgeCheck || checkKnowledge; | ||
| const checks = []; | ||
| const engine = pkg.engines?.node || ""; | ||
| const runtimeOk = satisfiesNodeEngine(nodeVersion, engine); | ||
| checks.push( | ||
| makeCheck( | ||
| "runtime", | ||
| "Node.js", | ||
| runtimeOk ? STATUS.pass : STATUS.error, | ||
| runtimeOk | ||
| ? `${nodeVersion} 满足 ${engine || "包要求"}。` | ||
| : `${nodeVersion} 不满足 ${engine}。`, | ||
| runtimeOk ? null : `升级 Node.js 到满足 ${engine} 的版本。`, | ||
| { version: nodeVersion, required: engine }, | ||
| ), | ||
| ); | ||
| const configPath = path.join(cwd, CONFIG_FILENAME); | ||
| let config = null; | ||
| if (!fs.existsSync(configPath)) { | ||
| checks.push( | ||
| makeCheck( | ||
| "config", | ||
| "项目配置", | ||
| STATUS.error, | ||
| `缺少 ${CONFIG_FILENAME}。`, | ||
| "在项目根运行 flow2spec init。", | ||
| ), | ||
| ); | ||
| } else { | ||
| try { | ||
| config = loadFlow2specConfig(cwd); | ||
| checks.push( | ||
| makeCheck( | ||
| "config", | ||
| "项目配置", | ||
| STATUS.pass, | ||
| `${CONFIG_FILENAME} 存在且可解析。`, | ||
| null, | ||
| { locale: config.locale }, | ||
| ), | ||
| ); | ||
| } catch (error) { | ||
| checks.push( | ||
| makeCheck( | ||
| "config", | ||
| "项目配置", | ||
| STATUS.error, | ||
| error.message || String(error), | ||
| `修正 ${CONFIG_FILENAME} 的 JSON 格式。`, | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
| const agentsPath = path.join(cwd, "AGENTS.md"); | ||
| checks.push( | ||
| fs.existsSync(agentsPath) | ||
| ? makeCheck("agents-entry", "项目入口", STATUS.pass, "根 AGENTS.md 已就绪。") | ||
| : makeCheck( | ||
| "agents-entry", | ||
| "项目入口", | ||
| STATUS.error, | ||
| "缺少根 AGENTS.md。", | ||
| "运行 flow2spec init codex,或重新初始化所需 Agent。", | ||
| ), | ||
| ); | ||
| const manifestPath = path.join(cwd, ".Knowledge", "manifest-routing.json"); | ||
| checks.push( | ||
| fs.existsSync(manifestPath) | ||
| ? makeCheck( | ||
| "knowledge-entry", | ||
| "知识库入口", | ||
| STATUS.pass, | ||
| ".Knowledge/manifest-routing.json 已就绪。", | ||
| ) | ||
| : makeCheck( | ||
| "knowledge-entry", | ||
| "知识库入口", | ||
| STATUS.error, | ||
| "缺少 .Knowledge/manifest-routing.json。", | ||
| "在项目根运行 flow2spec init。", | ||
| ), | ||
| ); | ||
| const requiredAgentFiles = { | ||
| codex: ["AGENTS.md", "hooks.json"], | ||
| claude: ["settings.json"], | ||
| cursor: ["hooks.json"], | ||
| }; | ||
| const initializedAgents = Object.entries(AGENTS).filter(([, agent]) => | ||
| fs.existsSync(path.join(cwd, agent.root)), | ||
| ); | ||
| if (initializedAgents.length === 0) { | ||
| checks.push( | ||
| makeCheck( | ||
| "agent-roots", | ||
| "Agent 配置", | ||
| STATUS.warning, | ||
| "未检测到 .codex、.claude 或 .cursor 配置根。", | ||
| "运行 flow2spec init <agent> 初始化实际使用的 Agent。", | ||
| ), | ||
| ); | ||
| } else { | ||
| for (const [id, agent] of initializedAgents) { | ||
| const missing = (requiredAgentFiles[id] || []).filter( | ||
| (file) => !fs.existsSync(path.join(cwd, agent.root, file)), | ||
| ); | ||
| checks.push( | ||
| missing.length === 0 | ||
| ? makeCheck( | ||
| `agent-${id}`, | ||
| `${agent.label} 配置`, | ||
| STATUS.pass, | ||
| `${agent.root} 初始化文件完整。`, | ||
| ) | ||
| : makeCheck( | ||
| `agent-${id}`, | ||
| `${agent.label} 配置`, | ||
| STATUS.error, | ||
| `${agent.root} 缺少 ${missing.join("、")}。`, | ||
| `运行 flow2spec init ${id} 补齐配置。`, | ||
| { missing }, | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
| if (config) { | ||
| try { | ||
| const context = resolveDeveloperContext(config, { | ||
| cwd, | ||
| gitIdentity: options.gitIdentity, | ||
| skipGit: Boolean(options.gitIdentity), | ||
| }); | ||
| const warnings = [...context.warnings]; | ||
| if (context.legacy && context.enabled) { | ||
| warnings.push("未找到 developerId,将使用 legacy .task/ 根。"); | ||
| } | ||
| checks.push( | ||
| makeCheck( | ||
| "collaboration", | ||
| "协作上下文", | ||
| warnings.length > 0 ? STATUS.warning : STATUS.pass, | ||
| context.legacy | ||
| ? `使用 ${context.taskRoot}(${context.enabled ? "legacy" : "协作隔离已关闭"})。` | ||
| : `developerId=${context.developerId},TASK_ROOT=${context.taskRoot}。`, | ||
| warnings.length > 0 | ||
| ? "在 flow2spec.config.json 配置 collaboration.developerId。" | ||
| : null, | ||
| { ...context, warnings }, | ||
| ), | ||
| ); | ||
| } catch (error) { | ||
| checks.push( | ||
| makeCheck( | ||
| "collaboration", | ||
| "协作上下文", | ||
| STATUS.error, | ||
| error.message || String(error), | ||
| "修正 flow2spec.config.json 的 collaboration 配置。", | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
| const taskIgnored = isIgnoredByRootGitignore(cwd, ".task/"); | ||
| checks.push( | ||
| taskIgnored | ||
| ? makeCheck("task-ignore", "任务目录", STATUS.pass, ".task/ 已在根 .gitignore 中忽略。") | ||
| : makeCheck( | ||
| "task-ignore", | ||
| "任务目录", | ||
| STATUS.warning, | ||
| ".task/ 未在根 .gitignore 中忽略。", | ||
| "在根 .gitignore 中加入 .task/,或重新运行 flow2spec init。", | ||
| ), | ||
| ); | ||
| checks.push(knowledgeCheck(cwd)); | ||
| const summary = checks.reduce( | ||
| (result, check) => { | ||
| if (check.status === STATUS.pass) result.passed += 1; | ||
| if (check.status === STATUS.warning) result.warnings += 1; | ||
| if (check.status === STATUS.error) result.errors += 1; | ||
| return result; | ||
| }, | ||
| { passed: 0, warnings: 0, errors: 0 }, | ||
| ); | ||
| return { | ||
| ok: summary.errors === 0, | ||
| package: { name: pkg.name, version: pkg.version }, | ||
| cwd: path.resolve(cwd), | ||
| summary, | ||
| checks, | ||
| }; | ||
| } | ||
| function formatDoctorReport(report) { | ||
| const marker = { pass: "[PASS]", warning: "[WARN]", error: "[FAIL]" }; | ||
| const lines = [ | ||
| `Flow2Spec Doctor v${report.package.version}`, | ||
| `项目: ${report.cwd}`, | ||
| "", | ||
| ]; | ||
| for (const check of report.checks) { | ||
| lines.push(`${marker[check.status]} ${check.label}: ${check.message}`); | ||
| if (check.repair) lines.push(` 建议: ${check.repair}`); | ||
| } | ||
| lines.push( | ||
| "", | ||
| `结果: ${report.summary.passed} 通过,${report.summary.warnings} 警告,${report.summary.errors} 错误。`, | ||
| ); | ||
| return lines.join("\n"); | ||
| } | ||
| module.exports = { | ||
| STATUS, | ||
| runDoctor, | ||
| formatDoctorReport, | ||
| satisfiesNodeEngine, | ||
| checkKnowledge, | ||
| }; |
+28
-0
@@ -18,2 +18,3 @@ #!/usr/bin/env node | ||
| const knowledgeEngine = require("./lib/knowledgeEngine"); | ||
| const { runDoctor, formatDoctorReport } = require("./lib/doctor"); | ||
@@ -238,2 +239,3 @@ const { execFileSync } = require("child_process"); | ||
| flow2spec config 打印项目根 ${CONFIG_FILENAME} 的解析结果(缺省值合并后) | ||
| flow2spec doctor [--json] 只读检查运行环境、项目初始化、协作上下文与知识库健康 | ||
| flow2spec kb 知识库协作引擎:status / check / plan / apply / build | ||
@@ -330,2 +332,28 @@ flow2spec version 显示当前 flow2spec 版本 | ||
| if (sub === "doctor") { | ||
| const doctorArgs = args.slice(1); | ||
| if (doctorArgs.includes("--help") || doctorArgs.includes("-h")) { | ||
| console.log(` | ||
| 用法: | ||
| flow2spec doctor [--json] | ||
| 只读检查 Node.js、项目配置、Agent 初始化、协作上下文、.task 忽略规则与知识库健康。 | ||
| 警告不阻塞(exit 0),错误会返回 exit 1;本命令不会修改文件或访问网络。 | ||
| `.trim()); | ||
| process.exit(0); | ||
| } | ||
| const unknown = doctorArgs.filter((arg) => arg !== "--json"); | ||
| if (unknown.length > 0) { | ||
| console.error(`doctor 不支持参数:${unknown.join(" ")}`); | ||
| process.exit(1); | ||
| } | ||
| const report = runDoctor(process.cwd()); | ||
| if (doctorArgs.includes("--json")) { | ||
| printJson(report); | ||
| } else { | ||
| console.log(formatDoctorReport(report)); | ||
| } | ||
| process.exit(report.ok ? 0 : 1); | ||
| } | ||
| if (sub === "kb") { | ||
@@ -332,0 +360,0 @@ const kbSub = args[1]; |
+2
-2
| { | ||
| "name": "@double-coding/flow2spec", | ||
| "version": "3.2.11", | ||
| "version": "3.2.12", | ||
| "description": "在业务仓库初始化「文档驱动、可写回知识库」的 AI 协作骨架:项目根 .Knowledge 承载 stock-docs/req-docs 与机读路由,.cursor/.claude/.codex 写入 f2s-* 规则与技能(含 Karpathy 式编码行为准则,init 同步 rules / Codex topics / skills);init 只落结构与模板,业务内容由各 f2s-* 技能在对话中维护。", | ||
@@ -29,3 +29,3 @@ "homepage": "https://github.com/double-coding-lab/Flow2Spec#readme", | ||
| "scripts": { | ||
| "test": "node cli.js --help && node cli.js kb check && node scripts/test-knowledge-engine.js && node scripts/test-developer-id.js && node scripts/test-template-knowledge.js && node scripts/test-init-gitignore.js", | ||
| "test": "node cli.js --help && node cli.js kb check && node scripts/test-knowledge-engine.js && node scripts/test-developer-id.js && node scripts/test-template-knowledge.js && node scripts/test-init-gitignore.js && node scripts/test-doctor.js", | ||
| "sync:agents": "node cli.js init cursor claude codex", | ||
@@ -32,0 +32,0 @@ "prepublishOnly": "node cli.js --help", |
+1
-8
@@ -21,3 +21,2 @@ # Flow2Spec | ||
| <img alt="npm latest" src="https://img.shields.io/npm/v/@double-coding/flow2spec?label=latest"> | ||
| <img alt="npm beta" src="https://img.shields.io/npm/v/@double-coding/flow2spec/beta?label=beta"> | ||
| <img alt="node version" src="https://img.shields.io/node/v/@double-coding/flow2spec"> | ||
@@ -33,8 +32,2 @@ <img alt="license" src="https://img.shields.io/npm/l/@double-coding/flow2spec"> | ||
| Try the current beta: | ||
| ```bash | ||
| npx @double-coding/flow2spec@beta init | ||
| ``` | ||
| ## Why it exists | ||
@@ -149,3 +142,3 @@ | ||
| - [Flow2Spec 基础介绍](./docs/Flow2Spec基础介绍.md) — Chinese long-form introduction. | ||
| - [Live demo](https://double-coding-lab.github.io/Flow2Spec) — 13-slide HTML presentation. | ||
| - [Product website](https://double-coding-lab.github.io/Flow2Spec/en/) — a website-style guide to Flow2Spec's core capabilities and workflow. | ||
@@ -152,0 +145,0 @@ ## License |
+1
-8
@@ -21,3 +21,2 @@ # Flow2Spec | ||
| <img alt="npm latest" src="https://img.shields.io/npm/v/@double-coding/flow2spec?label=latest"> | ||
| <img alt="npm beta" src="https://img.shields.io/npm/v/@double-coding/flow2spec/beta?label=beta"> | ||
| <img alt="node version" src="https://img.shields.io/node/v/@double-coding/flow2spec"> | ||
@@ -33,8 +32,2 @@ <img alt="license" src="https://img.shields.io/npm/l/@double-coding/flow2spec"> | ||
| Try the current beta: | ||
| ```bash | ||
| npx @double-coding/flow2spec@beta init | ||
| ``` | ||
| ## Why it exists | ||
@@ -158,3 +151,3 @@ | ||
| - [Flow2Spec 基础介绍](./docs/Flow2Spec基础介绍.md) — Chinese long-form introduction. | ||
| - [Live demo](https://double-coding-lab.github.io/Flow2Spec) — 13-slide HTML presentation. | ||
| - [Product website](https://double-coding-lab.github.io/Flow2Spec/en/) — a website-style guide to Flow2Spec's core capabilities and workflow. | ||
@@ -161,0 +154,0 @@ ## License |
+1
-8
@@ -21,3 +21,2 @@ # Flow2Spec | ||
| <img alt="npm latest" src="https://img.shields.io/npm/v/@double-coding/flow2spec?label=latest"> | ||
| <img alt="npm beta" src="https://img.shields.io/npm/v/@double-coding/flow2spec/beta?label=beta"> | ||
| <img alt="node version" src="https://img.shields.io/node/v/@double-coding/flow2spec"> | ||
@@ -33,8 +32,2 @@ <img alt="license" src="https://img.shields.io/npm/l/@double-coding/flow2spec"> | ||
| 尝试当前 beta: | ||
| ```bash | ||
| npx @double-coding/flow2spec@beta init | ||
| ``` | ||
| ## 为什么需要它 | ||
@@ -158,3 +151,3 @@ | ||
| - [Flow2Spec Introduction](./docs/en/Flow2Spec-Introduction.md) — 英文长文介绍。 | ||
| - [在线演示](https://double-coding-lab.github.io/Flow2Spec) — 13 页 HTML PPT。 | ||
| - [在线产品介绍](https://double-coding-lab.github.io/Flow2Spec) — 网站式产品导览,快速了解核心能力与使用路径。 | ||
@@ -161,0 +154,0 @@ ## 协议 |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
920195
1.19%121
0.83%5643
6.63%154
-4.35%19
5.56%