@double-coding/flow2spec-core
Advanced tools
+327
| export type Flow2SpecLocale = "zh-CN" | "en-US"; | ||
| export type Flow2SpecHost = "dsh" | "cursor" | "claude" | "codex"; | ||
| export type JsonPrimitive = string | number | boolean | null; | ||
| export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }; | ||
| export interface Flow2SpecProjectConfig { | ||
| subAgent?: boolean; | ||
| switchAgentVerification?: boolean; | ||
| intentRecognition?: boolean; | ||
| locale?: Flow2SpecLocale; | ||
| changeTracking?: { | ||
| feat?: boolean; | ||
| fix?: boolean; | ||
| implement?: boolean; | ||
| [key: string]: boolean | undefined; | ||
| }; | ||
| updateCheck?: { | ||
| enabled?: boolean; | ||
| [key: string]: JsonValue | undefined; | ||
| }; | ||
| collaboration?: { | ||
| enabled?: boolean; | ||
| developerId?: string; | ||
| [key: string]: JsonValue | undefined; | ||
| }; | ||
| [key: string]: unknown; | ||
| } | ||
| export interface CreateFlow2SpecOptions { | ||
| cwd?: string; | ||
| signal?: AbortSignal; | ||
| onProgress?: (event: unknown) => void; | ||
| } | ||
| export interface ProjectInitOptions { | ||
| mode?: "native-host" | "project-adapter" | string; | ||
| integrations?: Flow2SpecHost[] | string[]; | ||
| locale?: Flow2SpecLocale; | ||
| overwriteKnowledge?: boolean; | ||
| configValues?: Partial<Flow2SpecProjectConfig>; | ||
| [key: string]: unknown; | ||
| } | ||
| export interface ProjectInitResult { | ||
| ids: string[]; | ||
| mode: string; | ||
| overwriteKnowledge: boolean; | ||
| locale: Flow2SpecLocale; | ||
| projectConfig: Flow2SpecProjectConfig; | ||
| [key: string]: unknown; | ||
| } | ||
| export interface ProjectInspection { | ||
| cwd: string; | ||
| config: Flow2SpecProjectConfig; | ||
| } | ||
| export interface RoutingRule { | ||
| task?: string; | ||
| matcherId?: string; | ||
| matcherPath?: string; | ||
| topics?: string[]; | ||
| [key: string]: JsonValue | undefined; | ||
| } | ||
| export interface RoutingCandidate { | ||
| rule: RoutingRule | null; | ||
| score: number; | ||
| order?: number; | ||
| confidence: "high" | "medium" | "low"; | ||
| matchedPhrases: string[]; | ||
| topics: string[]; | ||
| fallback?: boolean; | ||
| } | ||
| export interface RoutingMatchInput { | ||
| request?: string; | ||
| query?: string; | ||
| task?: string; | ||
| } | ||
| export interface RoutingMatchResult { | ||
| request: string; | ||
| task: string | null; | ||
| primary: RoutingCandidate; | ||
| alternatives: RoutingCandidate[]; | ||
| candidates: RoutingCandidate[]; | ||
| manifestVersion: string; | ||
| topics?: string[]; | ||
| } | ||
| export interface RoutingMissingContext { | ||
| kind: "topic" | "context"; | ||
| id?: string; | ||
| path: string | null; | ||
| } | ||
| export interface RoutingVerification { | ||
| ok: boolean; | ||
| missing: RoutingMissingContext[]; | ||
| confidence: "high" | "medium" | "low"; | ||
| fallback: boolean; | ||
| } | ||
| export interface RoutingContextFile { | ||
| topic: string; | ||
| path: string; | ||
| content: string; | ||
| truncated: boolean; | ||
| } | ||
| export interface RoutingContext { | ||
| files: RoutingContextFile[]; | ||
| lineCount: number; | ||
| truncated: boolean; | ||
| } | ||
| export interface KnowledgeValidation { | ||
| ok: boolean; | ||
| issues: string[]; | ||
| warnings: string[]; | ||
| topicCount: number; | ||
| } | ||
| export type KnowledgeDeltaChangeType = | ||
| | "createTopic" | ||
| | "appendBody" | ||
| | "replaceBody" | ||
| | "updateFrontmatter"; | ||
| export interface KnowledgeDeltaChange { | ||
| type: KnowledgeDeltaChangeType | string; | ||
| targetTopic: string; | ||
| content?: string; | ||
| frontmatter?: Record<string, JsonValue>; | ||
| [key: string]: unknown; | ||
| } | ||
| export interface KnowledgeDelta { | ||
| taskId: string; | ||
| developerId: string; | ||
| baseRevisions: Record<string, number>; | ||
| changes: KnowledgeDeltaChange[]; | ||
| notes?: string; | ||
| } | ||
| export interface KnowledgePlanResult { | ||
| delta: KnowledgeDelta; | ||
| plan: unknown[]; | ||
| conflicts: unknown[]; | ||
| mergeable: boolean; | ||
| [key: string]: unknown; | ||
| } | ||
| export interface KnowledgeApplyResult { | ||
| dryRun: boolean; | ||
| changedFiles: string[]; | ||
| plan: unknown[]; | ||
| [key: string]: unknown; | ||
| } | ||
| export interface DeveloperContext { | ||
| developerId: string | null; | ||
| source: "config" | "git-email" | "git-email-hash" | "git-name" | "git-name-hash" | "legacy"; | ||
| legacy: boolean; | ||
| taskRoot: string; | ||
| enabled: boolean; | ||
| warnings: string[]; | ||
| } | ||
| export type DoctorCheckStatus = "pass" | "warning" | "error"; | ||
| export interface DoctorCheck { | ||
| id: string; | ||
| label: string; | ||
| status: DoctorCheckStatus; | ||
| message: string; | ||
| repair: string | null; | ||
| details?: unknown; | ||
| } | ||
| export interface DoctorReport { | ||
| ok: boolean; | ||
| package: { name: string; version: string }; | ||
| cwd: string; | ||
| summary: { passed: number; warnings: number; errors: number }; | ||
| checks: DoctorCheck[]; | ||
| } | ||
| export interface CapabilityDefinition { | ||
| id: string; | ||
| api: string; | ||
| since: string; | ||
| } | ||
| export interface CapabilityManifest { | ||
| schema: "flow2spec.capabilities.v1" | string; | ||
| protocolVersion: number; | ||
| package: string; | ||
| capabilities: CapabilityDefinition[]; | ||
| } | ||
| export interface HostResourceOptions { | ||
| host: Flow2SpecHost; | ||
| locale?: Flow2SpecLocale; | ||
| projectConfig?: Flow2SpecProjectConfig; | ||
| } | ||
| export interface Flow2SpecTextResource { | ||
| relativePath: string; | ||
| content: string; | ||
| mediaType: "text/markdown"; | ||
| } | ||
| export interface Flow2SpecSkillResource { | ||
| name: string; | ||
| description: string; | ||
| content: string; | ||
| relativePath: string; | ||
| resources: readonly Flow2SpecTextResource[]; | ||
| } | ||
| export type UpdateCheckStatus = | ||
| | "disabled" | ||
| | "skipped" | ||
| | "current" | ||
| | "upgrade-available" | ||
| | "unavailable"; | ||
| export interface UpdateCheckOptions { | ||
| packageName?: string; | ||
| force?: boolean; | ||
| signal?: AbortSignal; | ||
| timeout?: number; | ||
| } | ||
| export interface UpdateCheckResult { | ||
| status: UpdateCheckStatus; | ||
| checked: boolean; | ||
| fromCache: boolean; | ||
| packageName: string; | ||
| manifestVersion: string | null; | ||
| latestVersion: string | null; | ||
| needsUpgrade: boolean; | ||
| notice: string; | ||
| checkedAt: number | null; | ||
| reason: string | null; | ||
| } | ||
| export interface Flow2SpecApi { | ||
| context: { | ||
| cwd: string; | ||
| signal?: AbortSignal; | ||
| onProgress: (event: unknown) => void; | ||
| }; | ||
| project: { | ||
| init(options?: ProjectInitOptions): Promise<ProjectInitResult>; | ||
| inspect(): ProjectInspection; | ||
| }; | ||
| config: { | ||
| load(): Flow2SpecProjectConfig; | ||
| missingFields(): unknown[]; | ||
| }; | ||
| routing: { | ||
| graph(): unknown; | ||
| state(): { graph: unknown; validation: KnowledgeValidation }; | ||
| match(input?: RoutingMatchInput): RoutingMatchResult; | ||
| expand(result: RoutingMatchResult): RoutingMatchResult & { topics: string[] }; | ||
| verify( | ||
| result: RoutingMatchResult, | ||
| options?: { requiredContext?: string[] }, | ||
| ): RoutingVerification; | ||
| loadContext( | ||
| result: RoutingMatchResult, | ||
| options?: { maxFiles?: number; maxLines?: number }, | ||
| ): RoutingContext; | ||
| }; | ||
| knowledge: { | ||
| status(options?: Record<string, unknown>): unknown; | ||
| check(options?: { strict?: boolean; strictRevision?: boolean }): KnowledgeValidation; | ||
| plan(options?: { delta?: KnowledgeDelta; deltaFile?: string }): KnowledgePlanResult; | ||
| apply(options?: { | ||
| delta?: KnowledgeDelta; | ||
| deltaFile?: string; | ||
| dryRun?: boolean; | ||
| planHash?: string; | ||
| [key: string]: unknown; | ||
| }): KnowledgeApplyResult; | ||
| build(options?: Record<string, unknown>): unknown; | ||
| }; | ||
| collaboration: { | ||
| resolveDeveloper(options?: Record<string, unknown>): DeveloperContext; | ||
| }; | ||
| doctor: { | ||
| run(options?: Record<string, unknown>): DoctorReport; | ||
| }; | ||
| resources: { | ||
| root: string; | ||
| capabilities(): CapabilityManifest; | ||
| listSkills(locale?: Flow2SpecLocale): string[]; | ||
| listRules(locale?: Flow2SpecLocale): string[]; | ||
| listHooks(locale?: Flow2SpecLocale): string[]; | ||
| read(relativePath: string, locale?: Flow2SpecLocale): string; | ||
| skillCatalog(options: HostResourceOptions): Flow2SpecSkillResource[]; | ||
| unifiedEntry(options: HostResourceOptions): string; | ||
| }; | ||
| update: { | ||
| check(options?: UpdateCheckOptions): Promise<UpdateCheckResult>; | ||
| }; | ||
| } | ||
| export class Flow2SpecError extends Error { | ||
| constructor( | ||
| code: string, | ||
| message: string, | ||
| details?: Record<string, unknown>, | ||
| options?: { recoverable?: boolean }, | ||
| ); | ||
| code: string; | ||
| details: Record<string, unknown>; | ||
| recoverable: boolean; | ||
| } | ||
| export function createFlow2Spec(options?: CreateFlow2SpecOptions): Flow2SpecApi; | ||
| export function getCapabilities(): CapabilityManifest; | ||
| export const resourcesRoot: string; | ||
| export const legacy: Record<string, unknown>; |
+242
| "use strict"; | ||
| const fs = require("fs"); | ||
| const path = require("path"); | ||
| const { normalizeLocale, DEFAULT_LOCALE } = require("./flow2specConfig"); | ||
| const SUPPORTED_HOSTS = new Set(["dsh", "cursor", "claude", "codex"]); | ||
| const HOST_PATHS = { | ||
| dsh: { rules: "rules", skills: "skills", ruleExtension: ".md" }, | ||
| cursor: { rules: ".cursor/rules", skills: ".cursor/skills", ruleExtension: ".mdc" }, | ||
| claude: { rules: ".claude/rules", skills: ".claude/skills", ruleExtension: ".md" }, | ||
| codex: { rules: ".codex/topics", skills: ".codex/skills", ruleExtension: ".md" }, | ||
| }; | ||
| function resourceError(code, message, details = {}) { | ||
| const error = new Error(message); | ||
| error.code = code; | ||
| error.details = details; | ||
| return error; | ||
| } | ||
| function normalizeOptions(options = {}) { | ||
| const host = String(options.host || "").trim().toLowerCase(); | ||
| if (!SUPPORTED_HOSTS.has(host)) { | ||
| throw resourceError( | ||
| "F2S_INVALID_ARGUMENT", | ||
| `unsupported resource host: ${options.host || "<empty>"}`, | ||
| { field: "host", supported: Array.from(SUPPORTED_HOSTS) }, | ||
| ); | ||
| } | ||
| return { | ||
| host, | ||
| locale: normalizeLocale(options.locale, DEFAULT_LOCALE), | ||
| projectConfig: options.projectConfig, | ||
| }; | ||
| } | ||
| function templatesDir(templatesRoot, locale) { | ||
| return path.join(templatesRoot, locale); | ||
| } | ||
| function parseSkillDocument(raw, relativePath) { | ||
| const match = String(raw).match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); | ||
| if (!match) { | ||
| throw resourceError("F2S_RESOURCE_INVALID", `skill frontmatter missing: ${relativePath}`, { | ||
| relativePath, | ||
| }); | ||
| } | ||
| const frontmatter = {}; | ||
| for (const line of match[1].split(/\r?\n/)) { | ||
| const separator = line.indexOf(":"); | ||
| if (separator < 0) continue; | ||
| const key = line.slice(0, separator).trim(); | ||
| let value = line.slice(separator + 1).trim(); | ||
| if ( | ||
| value.length >= 2 && | ||
| ((value.startsWith('"') && value.endsWith('"')) || | ||
| (value.startsWith("'") && value.endsWith("'"))) | ||
| ) { | ||
| value = value.slice(1, -1); | ||
| } | ||
| frontmatter[key] = value; | ||
| } | ||
| if (!frontmatter.name || !frontmatter.description) { | ||
| throw resourceError( | ||
| "F2S_RESOURCE_INVALID", | ||
| `skill name or description missing: ${relativePath}`, | ||
| { relativePath }, | ||
| ); | ||
| } | ||
| return { | ||
| name: frontmatter.name, | ||
| description: frontmatter.description, | ||
| body: raw.slice(match[0].length), | ||
| }; | ||
| } | ||
| function ruleReferenceIds(content) { | ||
| const ids = new Set(["f2s-flow2spec-unified-entry", "f2s-config-check"]); | ||
| const pattern = /(?:(?:\.codex|\.dsh)\/topics\/|(?:\.cursor|\.claude)\/rules\/|rules\/)(f2s-[a-zA-Z0-9-]+)/g; | ||
| let match; | ||
| while ((match = pattern.exec(content))) ids.add(match[1]); | ||
| return Array.from(ids).sort(); | ||
| } | ||
| function hostRulePath(profile, ruleId) { | ||
| return `${profile.rules}/${ruleId}${profile.ruleExtension}`; | ||
| } | ||
| function hostSkillPath(profile, skillName) { | ||
| return `${profile.skills}/${skillName}/SKILL.md`; | ||
| } | ||
| function nativeSkillName(name, host) { | ||
| if (host !== "dsh") return name; | ||
| return String(name) | ||
| .replace(/([a-z0-9])([A-Z])/g, "$1-$2") | ||
| .toLowerCase(); | ||
| } | ||
| function adaptNativeSkillNames(content, host) { | ||
| if (host !== "dsh") return String(content); | ||
| return String(content).replace(/\bf2s-[a-zA-Z0-9-]*[A-Z][a-zA-Z0-9-]*\b/g, (name) => | ||
| nativeSkillName(name, host), | ||
| ); | ||
| } | ||
| function adaptHostPaths(content, host) { | ||
| const profile = HOST_PATHS[host]; | ||
| const adapted = String(content) | ||
| .replace( | ||
| /(?:(?:\.codex|\.dsh)\/topics\/|(?:\.cursor|\.claude)\/rules\/|rules\/)(f2s-[a-zA-Z0-9-]+)(?:\.(?:mdc?|\*))?/g, | ||
| (_, ruleId) => hostRulePath(profile, ruleId), | ||
| ) | ||
| .replace( | ||
| /(?:(?:\.codex|\.dsh|\.cursor|\.claude)\/skills\/|skills\/)(f2s-[a-zA-Z0-9-]+)\/SKILL\.md/g, | ||
| (_, skillName) => hostSkillPath(profile, skillName), | ||
| ); | ||
| if (host !== "dsh") return adapted; | ||
| // Native DSH resources are supplied by the provider, not copied into another | ||
| // client's project directory. Keep any remaining ancillary paths provider-relative. | ||
| return adaptNativeSkillNames( | ||
| adapted.replace(/\.(?:codex|cursor|claude|dsh)\//g, ""), | ||
| host, | ||
| ); | ||
| } | ||
| function replaceSection(content, startHeading, endHeading, replacement) { | ||
| const start = content.indexOf(startHeading); | ||
| if (start < 0) return content; | ||
| const end = content.indexOf(endHeading, start + startHeading.length); | ||
| if (end < 0) return content; | ||
| return `${content.slice(0, start)}${replacement.trim()}\n\n${content.slice(end)}`; | ||
| } | ||
| function adaptNativeDshEntry(content, locale) { | ||
| if (locale === "en-US") { | ||
| return replaceSection( | ||
| content, | ||
| "## Knowledge Base Version Check", | ||
| "## Topic Authoring Pointer", | ||
| `## Knowledge Base Version Check | ||
| The native host calls Core \`update.check()\` on session start. The API respects the project \`updateCheck.enabled\` switch and the daily \`.Knowledge/update-check.json\` cache. A notice is displayed when the published Core is newer than the project knowledge version. This check only detects and reports updates; it does not replace the configuration-read or knowledge-routing gates.`, | ||
| ); | ||
| } | ||
| return replaceSection( | ||
| content, | ||
| "## 知识库版本自检", | ||
| "## 主题创作", | ||
| `## 知识库版本自检 | ||
| 原生宿主在会话启动时调用 Core \`update.check()\`。该 API 服从项目的 \`updateCheck.enabled\` 开关并复用每日 \`.Knowledge/update-check.json\` 缓存;Core 发布版本高于项目知识版本时,由宿主展示升级提示。版本检查只负责检测与提醒,不替代配置前置读取和知识路由门禁。`, | ||
| ); | ||
| } | ||
| function configSummary(projectConfig, locale) { | ||
| if (!projectConfig || typeof projectConfig !== "object") return ""; | ||
| const known = { | ||
| subAgent: projectConfig.subAgent, | ||
| switchAgentVerification: projectConfig.switchAgentVerification, | ||
| intentRecognition: projectConfig.intentRecognition, | ||
| locale: projectConfig.locale, | ||
| changeTracking: projectConfig.changeTracking, | ||
| updateCheck: projectConfig.updateCheck, | ||
| collaboration: projectConfig.collaboration, | ||
| }; | ||
| for (const key of Object.keys(known)) { | ||
| if (known[key] === undefined) delete known[key]; | ||
| } | ||
| if (Object.keys(known).length === 0) return ""; | ||
| const heading = locale === "en-US" ? "## Current Project Configuration" : "## 当前项目配置"; | ||
| const note = | ||
| locale === "en-US" | ||
| ? "This snapshot is contextual only. Read `flow2spec.config.json` again before running any `f2s-*` skill." | ||
| : "此摘要只提供当前上下文。执行任何 `f2s-*` 技能前仍须重新读取 `flow2spec.config.json`。"; | ||
| return `${heading}\n\n${note}\n\n\`\`\`json\n${JSON.stringify(known, null, 2)}\n\`\`\`\n\n`; | ||
| } | ||
| function readRule(templatesRoot, locale, ruleId) { | ||
| const relativePath = path.posix.join("rules", `${ruleId}.md`); | ||
| const absolutePath = path.join(templatesDir(templatesRoot, locale), ...relativePath.split("/")); | ||
| if (!fs.existsSync(absolutePath)) return null; | ||
| return { relativePath, content: fs.readFileSync(absolutePath, "utf8") }; | ||
| } | ||
| function skillCatalog(templatesRoot, options = {}) { | ||
| const { host, locale } = normalizeOptions(options); | ||
| const profile = HOST_PATHS[host]; | ||
| const skillsRoot = path.join(templatesDir(templatesRoot, locale), "skills"); | ||
| if (!fs.existsSync(skillsRoot)) return []; | ||
| return fs | ||
| .readdirSync(skillsRoot, { withFileTypes: true }) | ||
| .filter((entry) => entry.isDirectory()) | ||
| .sort((left, right) => left.name.localeCompare(right.name)) | ||
| .map((entry) => { | ||
| const sourceRelativePath = path.posix.join("skills", entry.name, "SKILL.md"); | ||
| const sourcePath = path.join(skillsRoot, entry.name, "SKILL.md"); | ||
| if (!fs.existsSync(sourcePath)) { | ||
| throw resourceError("F2S_RESOURCE_MISSING", `skill resource missing: ${sourceRelativePath}`, { | ||
| relativePath: sourceRelativePath, | ||
| locale, | ||
| }); | ||
| } | ||
| const raw = fs.readFileSync(sourcePath, "utf8"); | ||
| const parsed = parseSkillDocument(raw, sourceRelativePath); | ||
| const skillName = nativeSkillName(parsed.name, host); | ||
| const resources = ruleReferenceIds(raw) | ||
| .map((ruleId) => readRule(templatesRoot, locale, ruleId)) | ||
| .filter(Boolean) | ||
| .map((resource) => ({ | ||
| relativePath: hostRulePath(profile, path.basename(resource.relativePath, ".md")), | ||
| content: adaptHostPaths(resource.content, host), | ||
| mediaType: "text/markdown", | ||
| })); | ||
| return { | ||
| name: skillName, | ||
| description: adaptNativeSkillNames(parsed.description, host), | ||
| content: adaptHostPaths(parsed.body, host), | ||
| relativePath: hostSkillPath(profile, skillName), | ||
| resources, | ||
| }; | ||
| }); | ||
| } | ||
| function unifiedEntry(templatesRoot, options = {}) { | ||
| const { host, locale, projectConfig } = normalizeOptions(options); | ||
| const entry = readRule(templatesRoot, locale, "f2s-flow2spec-unified-entry"); | ||
| if (!entry) { | ||
| throw resourceError("F2S_RESOURCE_MISSING", "unified entry resource is missing", { locale }); | ||
| } | ||
| let content = adaptHostPaths(entry.content, host); | ||
| if (host === "dsh") content = adaptNativeDshEntry(content, locale); | ||
| return `${configSummary(projectConfig, locale)}${content}`; | ||
| } | ||
| module.exports = { | ||
| SUPPORTED_HOSTS, | ||
| adaptHostPaths, | ||
| skillCatalog, | ||
| unifiedEntry, | ||
| }; |
| "use strict"; | ||
| const fs = require("fs"); | ||
| const path = require("path"); | ||
| const { execFile } = require("child_process"); | ||
| const DEFAULT_PACKAGE_NAME = "@double-coding/flow2spec-core"; | ||
| const KNOWLEDGE_ROOT = ".Knowledge"; | ||
| const CACHE_FILENAME = "update-check.json"; | ||
| function parseVersion(version) { | ||
| return String(version || "") | ||
| .replace(/^v/, "") | ||
| .split(/[.-]/) | ||
| .slice(0, 3) | ||
| .map((part) => { | ||
| const number = Number.parseInt(part, 10); | ||
| return Number.isFinite(number) ? number : 0; | ||
| }); | ||
| } | ||
| function compareVersions(left, right) { | ||
| const a = parseVersion(left); | ||
| const b = parseVersion(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 abortError() { | ||
| const error = new Error("update check aborted"); | ||
| error.code = "F2S_ABORTED"; | ||
| error.details = { operation: "update.check" }; | ||
| return error; | ||
| } | ||
| function assertNotAborted(signal) { | ||
| if (signal?.aborted) throw abortError(); | ||
| } | ||
| function readJson(filePath) { | ||
| try { | ||
| return JSON.parse(fs.readFileSync(filePath, "utf8")); | ||
| } catch (_) { | ||
| return null; | ||
| } | ||
| } | ||
| function sameLocalDay(timestamp, now) { | ||
| const checkedAt = Number(timestamp || 0); | ||
| return checkedAt > 0 && new Date(checkedAt).toDateString() === new Date(now).toDateString(); | ||
| } | ||
| function projectName(cwd) { | ||
| const pkg = readJson(path.join(cwd, "package.json")); | ||
| return pkg?.name ? String(pkg.name) : path.basename(cwd); | ||
| } | ||
| function buildNotice({ cwd, locale, manifestVersion, latestVersion }) { | ||
| if (locale === "en-US") { | ||
| return `[flow2spec] The project "${projectName(cwd)}" knowledge version is v${manifestVersion}; Core v${latestVersion} is available. Run the f2s-kb-upgrade skill to align templates and routing.`; | ||
| } | ||
| return `[flow2spec] 当前项目「${projectName(cwd)}」知识版本为 v${manifestVersion},Core 最新版本为 v${latestVersion}。可执行 f2s-kb-upgrade skill 对齐模板与路由。`; | ||
| } | ||
| function queryLatestVersion(packageName, options = {}) { | ||
| const npmExecutable = process.platform === "win32" ? "npm.cmd" : "npm"; | ||
| return new Promise((resolve, reject) => { | ||
| execFile( | ||
| npmExecutable, | ||
| ["view", packageName, "version", "--registry=https://registry.npmjs.org"], | ||
| { | ||
| encoding: "utf8", | ||
| timeout: options.timeout || 5000, | ||
| maxBuffer: 1024 * 1024, | ||
| signal: options.signal, | ||
| windowsHide: true, | ||
| }, | ||
| (error, stdout) => { | ||
| if (options.signal?.aborted || error?.name === "AbortError" || error?.code === "ABORT_ERR") { | ||
| reject(abortError()); | ||
| return; | ||
| } | ||
| if (error) { | ||
| reject(error); | ||
| return; | ||
| } | ||
| resolve(String(stdout || "").trim()); | ||
| }, | ||
| ); | ||
| }); | ||
| } | ||
| function result(status, values = {}) { | ||
| return { | ||
| status, | ||
| checked: false, | ||
| fromCache: false, | ||
| packageName: values.packageName || DEFAULT_PACKAGE_NAME, | ||
| manifestVersion: values.manifestVersion || null, | ||
| latestVersion: values.latestVersion || null, | ||
| needsUpgrade: status === "upgrade-available", | ||
| notice: values.notice || "", | ||
| checkedAt: values.checkedAt || null, | ||
| reason: values.reason || null, | ||
| ...values, | ||
| }; | ||
| } | ||
| async function checkUpdate(cwd, config, options = {}) { | ||
| assertNotAborted(options.signal); | ||
| const packageName = options.packageName || DEFAULT_PACKAGE_NAME; | ||
| const locale = config?.locale === "en-US" ? "en-US" : "zh-CN"; | ||
| if (config?.updateCheck?.enabled === false) { | ||
| return result("disabled", { packageName, reason: "config-disabled" }); | ||
| } | ||
| if (!options.force && (process.env.CI || process.env.CONTINUOUS_INTEGRATION)) { | ||
| return result("skipped", { packageName, reason: "continuous-integration" }); | ||
| } | ||
| const knowledgeDir = path.join(cwd, KNOWLEDGE_ROOT); | ||
| const manifestPath = path.join(knowledgeDir, "manifest-routing.json"); | ||
| const cachePath = path.join(knowledgeDir, CACHE_FILENAME); | ||
| const manifestVersion = readJson(manifestPath)?.version || null; | ||
| if (!manifestVersion) { | ||
| return result("skipped", { packageName, reason: "manifest-missing" }); | ||
| } | ||
| const now = Date.now(); | ||
| const cache = readJson(cachePath); | ||
| const cachePackageMatches = cache?.packageName | ||
| ? cache.packageName === packageName | ||
| : packageName === DEFAULT_PACKAGE_NAME; | ||
| if (!options.force && cache && cachePackageMatches && sameLocalDay(cache.checkedAt, now)) { | ||
| const latestVersion = cache.latestVersion || cache.latestNpm || null; | ||
| if (latestVersion && compareVersions(manifestVersion, latestVersion) >= 0) { | ||
| try { | ||
| fs.rmSync(cachePath, { force: true }); | ||
| } catch (_) {} | ||
| return result("current", { | ||
| checked: true, | ||
| fromCache: true, | ||
| packageName, | ||
| manifestVersion, | ||
| latestVersion, | ||
| checkedAt: Number(cache.checkedAt), | ||
| }); | ||
| } | ||
| const needsUpgrade = | ||
| Boolean(latestVersion) && | ||
| (cache.needsUpgrade === true || compareVersions(manifestVersion, latestVersion) < 0); | ||
| return result(needsUpgrade ? "upgrade-available" : "current", { | ||
| checked: true, | ||
| fromCache: true, | ||
| packageName, | ||
| manifestVersion, | ||
| latestVersion, | ||
| needsUpgrade, | ||
| notice: needsUpgrade | ||
| ? buildNotice({ cwd, locale, manifestVersion, latestVersion }) | ||
| : "", | ||
| checkedAt: Number(cache.checkedAt), | ||
| }); | ||
| } | ||
| let latestVersion; | ||
| try { | ||
| latestVersion = await queryLatestVersion(packageName, { | ||
| signal: options.signal, | ||
| timeout: options.timeout, | ||
| }); | ||
| } catch (error) { | ||
| if (error?.code === "F2S_ABORTED") throw error; | ||
| return result("unavailable", { | ||
| packageName, | ||
| manifestVersion, | ||
| reason: "registry-unavailable", | ||
| }); | ||
| } | ||
| assertNotAborted(options.signal); | ||
| if (!latestVersion) { | ||
| return result("unavailable", { | ||
| packageName, | ||
| manifestVersion, | ||
| reason: "empty-registry-version", | ||
| }); | ||
| } | ||
| const needsUpgrade = compareVersions(manifestVersion, latestVersion) < 0; | ||
| const notice = needsUpgrade | ||
| ? buildNotice({ cwd, locale, manifestVersion, latestVersion }) | ||
| : ""; | ||
| const checkedAt = Date.now(); | ||
| try { | ||
| fs.mkdirSync(knowledgeDir, { recursive: true }); | ||
| fs.writeFileSync( | ||
| cachePath, | ||
| `${JSON.stringify( | ||
| { | ||
| packageName, | ||
| latestVersion, | ||
| latestNpm: latestVersion, | ||
| manifestVersion, | ||
| needsUpgrade, | ||
| notice, | ||
| checkedAt, | ||
| }, | ||
| null, | ||
| 2, | ||
| )}\n`, | ||
| "utf8", | ||
| ); | ||
| } catch (_) {} | ||
| return result(needsUpgrade ? "upgrade-available" : "current", { | ||
| checked: true, | ||
| packageName, | ||
| manifestVersion, | ||
| latestVersion, | ||
| needsUpgrade, | ||
| notice, | ||
| checkedAt, | ||
| }); | ||
| } | ||
| module.exports = { | ||
| DEFAULT_PACKAGE_NAME, | ||
| compareVersions, | ||
| checkUpdate, | ||
| }; |
| { | ||
| "schema": "flow2spec.capabilities.v1", | ||
| "protocolVersion": 1, | ||
| "protocolVersion": 2, | ||
| "package": "@double-coding/flow2spec-core", | ||
@@ -25,4 +25,7 @@ "capabilities": [ | ||
| { "id": "resources.rules", "api": "resources.listRules", "since": "3.3.0" }, | ||
| { "id": "resources.read", "api": "resources.read", "since": "3.3.0" } | ||
| { "id": "resources.read", "api": "resources.read", "since": "3.3.0" }, | ||
| { "id": "resources.skill-catalog", "api": "resources.skillCatalog", "since": "3.4.0" }, | ||
| { "id": "resources.unified-entry", "api": "resources.unifiedEntry", "since": "3.4.0" }, | ||
| { "id": "update.check", "api": "update.check", "since": "3.4.0" } | ||
| ] | ||
| } |
+38
-2
@@ -16,2 +16,4 @@ "use strict"; | ||
| const routing = require("./lib/routing"); | ||
| const hostResources = require("./lib/resources"); | ||
| const updateCheck = require("./lib/updateCheck"); | ||
| const capabilities = require("./capabilities.json"); | ||
@@ -78,2 +80,26 @@ | ||
| function toFlow2SpecError(error) { | ||
| if (error instanceof Flow2SpecError) return error; | ||
| if (error && typeof error.code === "string" && error.code.startsWith("F2S_")) { | ||
| return new Flow2SpecError(error.code, error.message || String(error), error.details || {}); | ||
| } | ||
| return error; | ||
| } | ||
| function callCore(operation) { | ||
| try { | ||
| return operation(); | ||
| } catch (error) { | ||
| throw toFlow2SpecError(error); | ||
| } | ||
| } | ||
| async function callCoreAsync(operation) { | ||
| try { | ||
| return await operation(); | ||
| } catch (error) { | ||
| throw toFlow2SpecError(error); | ||
| } | ||
| } | ||
| function createFlow2Spec(options = {}) { | ||
@@ -117,4 +143,4 @@ const cwd = assertCwd(options.cwd || process.cwd()); | ||
| }, | ||
| apply: ({ deltaFile, ...applyOptions } = {}) => | ||
| knowledgeEngine.applyKnowledgeDelta(cwd, deltaFile, applyOptions), | ||
| apply: ({ delta, deltaFile, ...applyOptions } = {}) => | ||
| knowledgeEngine.applyKnowledgeDelta(cwd, delta || deltaFile, applyOptions), | ||
| build: (buildOptions = {}) => knowledgeEngine.buildKnowledgeGraph(cwd, buildOptions), | ||
@@ -139,3 +165,13 @@ }, | ||
| read: (relativePath, locale = "zh-CN") => readResource(relativePath, locale), | ||
| skillCatalog: (resourceOptions = {}) => | ||
| callCore(() => hostResources.skillCatalog(path.join(__dirname, "templates"), resourceOptions)), | ||
| unifiedEntry: (resourceOptions = {}) => | ||
| callCore(() => hostResources.unifiedEntry(path.join(__dirname, "templates"), resourceOptions)), | ||
| }, | ||
| update: { | ||
| check: (checkOptions = {}) => | ||
| callCoreAsync(() => | ||
| updateCheck.checkUpdate(cwd, config.loadFlow2specConfig(cwd), checkOptions), | ||
| ), | ||
| }, | ||
| }; | ||
@@ -142,0 +178,0 @@ } |
+3
-1
| { | ||
| "name": "@double-coding/flow2spec-core", | ||
| "version": "3.3.1", | ||
| "version": "3.4.0", | ||
| "description": "Flow2Spec Core APIs, knowledge engine, project initialization and shared resources", | ||
@@ -12,4 +12,6 @@ "homepage": "https://github.com/double-coding-lab/Flow2Spec#readme", | ||
| "main": "./index.js", | ||
| "types": "./index.d.ts", | ||
| "files": [ | ||
| "index.js", | ||
| "index.d.ts", | ||
| "lib", | ||
@@ -16,0 +18,0 @@ "templates", |
+22
-0
@@ -6,1 +6,23 @@ # @double-coding/flow2spec-core | ||
| 普通用户通常不需要单独安装此包;安装 `@double-coding/flow2spec` 或 Flow2Spec 原生插件时会自动带上对应版本的 Core。 | ||
| ## 原生插件 API | ||
| ```js | ||
| const { createFlow2Spec } = require("@double-coding/flow2spec-core"); | ||
| const flow2spec = createFlow2Spec({ cwd: process.cwd() }); | ||
| const skills = flow2spec.resources.skillCatalog({ host: "dsh", locale: "zh-CN" }); | ||
| const entry = flow2spec.resources.unifiedEntry({ | ||
| host: "dsh", | ||
| locale: "zh-CN", | ||
| projectConfig: flow2spec.config.load(), | ||
| }); | ||
| const update = await flow2spec.update.check(); | ||
| ``` | ||
| - `resources.skillCatalog()` 返回带正文和关联规则资源的结构化 Skill 清单。Core 根据宿主适配规则与 Skill 路径,插件不需要重写其他客户端目录。 | ||
| - `resources.unifiedEntry()` 返回宿主适配后的统一入口,可附带当前项目配置摘要。 | ||
| - `update.check()` 复用 `.Knowledge/update-check.json` 的每日缓存与版本比较语义;网络不可用时返回 `unavailable`,不会阻断宿主。 | ||
| - `capabilities.json` 的 `protocolVersion` 用于插件启动时执行能力兼容校验。 | ||
| 包通过 `index.d.ts` 导出完整公共契约类型。普通 CLI 用户继续使用 `npx @double-coding/flow2spec init`,无需直接调用这些 API。 |
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.
899655
3.05%126
2.44%6099
14.34%28
366.67%22
22.22%4
33.33%