From 5fd78bd9a193afd7f0ae086c96d695a99c90591a Mon Sep 17 00:00:00 2001 From: Soju06 Date: Sat, 28 Feb 2026 00:35:20 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20add=20update=20UX,=20file-level=20versio?= =?UTF-8?q?n=20patching,=20and=20v0.3=E2=86=92v0.4=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add version tags to all skill/correction files with release-please markers - Add file-level version patching on install (skip unchanged files) - Detect v0.3 legacy installs (no version tag) and auto-migrate - Migrate stale hooks (ephemeral→skills-scope path) on install - Add non-blocking update notification after install - Detect bunx/npx ephemeral execution and return correct update command - Show version check in non-interactive mode (one-line notification) - Update release-please config with extra-files for skill version bumping --- .github/release-please-config.json | 40 +++++ skills/knowpatch/SKILL.md | 1 + skills/knowpatch/corrections/cli-tools.md | 1 + skills/knowpatch/corrections/frameworks.md | 1 + .../knowpatch/corrections/frontier-models.md | 1 + skills/knowpatch/corrections/javascript.md | 1 + skills/knowpatch/corrections/macos.md | 1 + .../corrections/open-source-models.md | 1 + skills/knowpatch/corrections/platforms.md | 1 + skills/knowpatch/corrections/python.md | 1 + skills/knowpatch/corrections/runtimes.md | 1 + src/commands/install.ts | 168 +++++++++++++++++- src/commands/update.ts | 25 +-- src/core/package-manager.ts | 15 ++ src/core/parser.ts | 3 + tests/package-manager.test.ts | 8 + tests/parser.test.ts | 28 +++ 17 files changed, 279 insertions(+), 18 deletions(-) diff --git a/.github/release-please-config.json b/.github/release-please-config.json index 9be8469..40c1648 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -10,6 +10,46 @@ "type": "json", "path": ".claude-plugin/plugin.json", "jsonpath": "$.version" + }, + { + "type": "generic", + "path": "skills/knowpatch/SKILL.md" + }, + { + "type": "generic", + "path": "skills/knowpatch/corrections/cli-tools.md" + }, + { + "type": "generic", + "path": "skills/knowpatch/corrections/frameworks.md" + }, + { + "type": "generic", + "path": "skills/knowpatch/corrections/frontier-models.md" + }, + { + "type": "generic", + "path": "skills/knowpatch/corrections/javascript.md" + }, + { + "type": "generic", + "path": "skills/knowpatch/corrections/macos.md" + }, + { + "type": "generic", + "path": "skills/knowpatch/corrections/open-source-models.md" + }, + { + "type": "generic", + "path": "skills/knowpatch/corrections/platforms.md" + }, + { + "type": "generic", + "path": "skills/knowpatch/corrections/python.md" + }, + { + "type": "generic", + "path": "skills/knowpatch/corrections/runtimes.md" } ] } diff --git a/skills/knowpatch/SKILL.md b/skills/knowpatch/SKILL.md index 449fd0f..36c9789 100644 --- a/skills/knowpatch/SKILL.md +++ b/skills/knowpatch/SKILL.md @@ -1,5 +1,6 @@ --- name: knowpatch +version: "0.4.0" # x-release-please-version description: > LLM knowledge cutoff compensator — knowledge corrections for breaking changes and API drift. Covers: renamed packages (shadcn-ui→shadcn), changed APIs (z.string().email()→z.email()), diff --git a/skills/knowpatch/corrections/cli-tools.md b/skills/knowpatch/corrections/cli-tools.md index 5a5bc32..4a96994 100644 --- a/skills/knowpatch/corrections/cli-tools.md +++ b/skills/knowpatch/corrections/cli-tools.md @@ -2,6 +2,7 @@ ecosystem: cli-tools description: CLI tool renames, command changes, major versions tags: [shadcn, tailwind, eslint, create-react-app, vite, webpack, prettier, cli] +version: "0.4.0" # x-release-please-version last_updated: "2026-02-24" --- diff --git a/skills/knowpatch/corrections/frameworks.md b/skills/knowpatch/corrections/frameworks.md index b4b722d..d92ae45 100644 --- a/skills/knowpatch/corrections/frameworks.md +++ b/skills/knowpatch/corrections/frameworks.md @@ -2,6 +2,7 @@ ecosystem: frameworks description: Framework major version breaking changes tags: [next, nextjs, svelte, vue, nuxt, astro, remix, angular, framework] +version: "0.4.0" # x-release-please-version last_updated: "2026-02-24" --- diff --git a/skills/knowpatch/corrections/frontier-models.md b/skills/knowpatch/corrections/frontier-models.md index a7dee24..cf37bd6 100644 --- a/skills/knowpatch/corrections/frontier-models.md +++ b/skills/knowpatch/corrections/frontier-models.md @@ -2,6 +2,7 @@ ecosystem: frontier-models description: Proprietary frontier AI model names, IDs, SDK versions, multimodal support tags: [claude, gpt, gemini, openai, anthropic, model, llm, sdk, ai] +version: "0.4.0" # x-release-please-version last_updated: "2026-02-27" --- diff --git a/skills/knowpatch/corrections/javascript.md b/skills/knowpatch/corrections/javascript.md index 60e63ba..82f60a0 100644 --- a/skills/knowpatch/corrections/javascript.md +++ b/skills/knowpatch/corrections/javascript.md @@ -2,6 +2,7 @@ ecosystem: javascript description: JS/TS library API changes tags: [zod, react, typescript, npm, bun, deno, pnpm, esm, types, javascript, js, ts] +version: "0.4.0" # x-release-please-version last_updated: "2026-02-24" --- diff --git a/skills/knowpatch/corrections/macos.md b/skills/knowpatch/corrections/macos.md index 5e64455..1d1795f 100644 --- a/skills/knowpatch/corrections/macos.md +++ b/skills/knowpatch/corrections/macos.md @@ -2,6 +2,7 @@ ecosystem: macos description: macOS 26 version naming, Liquid Glass, Swift 6.2, system toolchain, Apple framework changes tags: [macos, tahoe, xcode, swift, swiftui, liquid-glass, metal, rosetta, intel, apple-silicon, foundation-models] +version: "0.4.0" # x-release-please-version last_updated: "2026-02-27" --- diff --git a/skills/knowpatch/corrections/open-source-models.md b/skills/knowpatch/corrections/open-source-models.md index 13a79a3..1fbdfef 100644 --- a/skills/knowpatch/corrections/open-source-models.md +++ b/skills/knowpatch/corrections/open-source-models.md @@ -2,6 +2,7 @@ ecosystem: open-source-models description: Open-source frontier LLMs for coding, agentic tasks, and self-hosting (2026) tags: [open-source, self-hosted, coding-model, llama, deepseek, mistral, kimi, minimax, glm, qwen, vllm, sglang, swe-bench, moe, model, llm, ai] +version: "0.4.0" # x-release-please-version last_updated: "2026-02-27" --- diff --git a/skills/knowpatch/corrections/platforms.md b/skills/knowpatch/corrections/platforms.md index 1e3305e..bef9463 100644 --- a/skills/knowpatch/corrections/platforms.md +++ b/skills/knowpatch/corrections/platforms.md @@ -2,6 +2,7 @@ ecosystem: platforms description: BaaS/platform API key changes, auth patterns tags: [supabase, anon, service_role, publishable, secret, jwks, baas, firebase, platform] +version: "0.4.0" # x-release-please-version last_updated: "2026-02-25" --- diff --git a/skills/knowpatch/corrections/python.md b/skills/knowpatch/corrections/python.md index 67d63de..a361832 100644 --- a/skills/knowpatch/corrections/python.md +++ b/skills/knowpatch/corrections/python.md @@ -2,6 +2,7 @@ ecosystem: python description: Python ecosystem tool/library changes tags: [pip, uv, poetry, pydantic, fastapi, django, ruff, flask, sqlalchemy, python] +version: "0.4.0" # x-release-please-version last_updated: "2026-02-24" --- diff --git a/skills/knowpatch/corrections/runtimes.md b/skills/knowpatch/corrections/runtimes.md index f50da7b..5748ae1 100644 --- a/skills/knowpatch/corrections/runtimes.md +++ b/skills/knowpatch/corrections/runtimes.md @@ -2,6 +2,7 @@ ecosystem: runtimes description: Runtime version tracks, LTS status tags: [node, python, bun, deno, go, java, runtime] +version: "0.4.0" # x-release-please-version last_updated: "2026-02-24" --- diff --git a/src/commands/install.ts b/src/commands/install.ts index 3f87f2c..32fe05f 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -1,8 +1,15 @@ -import { cp, mkdir, rm, symlink } from "node:fs/promises"; +import { cp, mkdir, readdir, readFile, rm, symlink } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, resolve } from "node:path"; import { checkbox } from "@inquirer/prompts"; -import { installPlatformHook, isPlatformHookInstalled } from "../core/hooks.js"; +import { + installPlatformHook, + isPlatformHookInstalled, + isPlatformHookUpToDate, + uninstallPlatformHook, +} from "../core/hooks.js"; +import { getUpdateCommand } from "../core/package-manager.js"; +import { parseFrontmatter } from "../core/parser.js"; import { getAgentsSkillPath, getPlatformSkillPath, @@ -16,6 +23,7 @@ import { safeLstat, } from "../core/paths.js"; import { PLATFORMS, type PlatformConfig } from "../core/platforms.js"; +import { checkForUpdate } from "../core/version.js"; import { isInteractive } from "../ui/interactive.js"; import { COLORS, ICONS } from "../ui/palette.js"; @@ -79,9 +87,8 @@ export async function installCommand(options: InstallOptions): Promise { // 1. Canonical: .agents/skills/knowpatch ← cp -r from source const canonicalPath = getAgentsSkillPath(scope); if (await isCanonicalInstalled(canonicalPath)) { - console.log( - ` ${ICONS.ok} ${COLORS.success("Canonical:")} ${canonicalPath}`, - ); + const syncResult = await syncCanonical(source, canonicalPath); + console.log(` ${ICONS.ok} ${COLORS.success("Canonical:")} ${syncResult}`); } else { const existing = await safeLstat(canonicalPath); if (existing !== null) { @@ -137,9 +144,17 @@ export async function installCommand(options: InstallOptions): Promise { // 3. Hook registration if (platform.supportsHooks) { if (await isPlatformHookInstalled(platform, scope)) { - console.log( - ` ${ICONS.ok} ${COLORS.success(`${platform.displayName}:`)} hook already registered`, - ); + if (!(await isPlatformHookUpToDate(platform, scope))) { + await uninstallPlatformHook(platform, scope); + await installPlatformHook(platform, scope); + console.log( + ` ${ICONS.ok} ${COLORS.success(`${platform.displayName}:`)} hook updated`, + ); + } else { + console.log( + ` ${ICONS.ok} ${COLORS.success(`${platform.displayName}:`)} hook already registered`, + ); + } } else { await installPlatformHook(platform, scope); console.log( @@ -155,4 +170,141 @@ export async function installCommand(options: InstallOptions): Promise { console.log( ` Done! Installed for ${count} platform${count !== 1 ? "s" : ""}.`, ); + + // Non-blocking update notification + showUpdateNotification().catch(() => {}); +} + +/** Read the version field from a SKILL.md or correction file's YAML frontmatter */ +async function readFileVersion(filePath: string): Promise { + try { + const content = await readFile(filePath, "utf-8"); + const { frontmatter } = parseFrontmatter(content); + return (frontmatter as Record | null)?.version as + | string + | undefined; + } catch { + return undefined; + } +} + +/** + * Sync canonical installation with source, using file-level version patching. + * Returns a human-readable status string. + */ +async function syncCanonical( + source: string, + canonicalPath: string, +): Promise { + const installedVersion = await readFileVersion( + resolve(canonicalPath, "SKILL.md"), + ); + const sourceVersion = await readFileVersion(resolve(source, "SKILL.md")); + + // No version tag → v0.3 legacy → full re-sync + if (!installedVersion) { + await rm(canonicalPath, { recursive: true, force: true }); + await mkdir(dirname(canonicalPath), { recursive: true }); + await cp(source, canonicalPath, { recursive: true }); + return `migrated from legacy → ${sourceVersion ?? "latest"}`; + } + + // Same version → up to date + if (installedVersion === sourceVersion) { + return `up to date (${installedVersion})`; + } + + // Different version → file-level patch + const patched = await patchFiles(source, canonicalPath); + return `updated ${installedVersion} → ${sourceVersion ?? "latest"} (${patched} file${patched !== 1 ? "s" : ""})`; +} + +/** + * Patch individual files by comparing source and installed versions. + * Returns the number of files patched. + */ +async function patchFiles(source: string, installed: string): Promise { + let patched = 0; + + // Patch SKILL.md + const srcSkillVer = await readFileVersion(resolve(source, "SKILL.md")); + const instSkillVer = await readFileVersion(resolve(installed, "SKILL.md")); + if (srcSkillVer !== instSkillVer) { + await cp(resolve(source, "SKILL.md"), resolve(installed, "SKILL.md")); + patched++; + } + + // Patch corrections/ + const srcCorr = resolve(source, "corrections"); + const instCorr = resolve(installed, "corrections"); + await mkdir(instCorr, { recursive: true }); + + const srcFiles = (await pathExists(srcCorr)) + ? (await readdir(srcCorr)).filter((f) => f.endsWith(".md")) + : []; + const instFiles = (await pathExists(instCorr)) + ? (await readdir(instCorr)).filter((f) => f.endsWith(".md")) + : []; + + const srcSet = new Set(srcFiles); + const instSet = new Set(instFiles); + + // Copy new or updated files + for (const file of srcFiles) { + const srcVer = await readFileVersion(resolve(srcCorr, file)); + const instVer = instSet.has(file) + ? await readFileVersion(resolve(instCorr, file)) + : undefined; + + if (srcVer !== instVer || !instSet.has(file)) { + await cp(resolve(srcCorr, file), resolve(instCorr, file)); + patched++; + } + } + + // Remove files that no longer exist in source + for (const file of instFiles) { + if (!srcSet.has(file)) { + await rm(resolve(instCorr, file), { force: true }); + patched++; + } + } + + // Patch bin/detect.js if source has it + const srcDetect = resolve(source, "bin/detect.js"); + const instDetect = resolve(installed, "bin/detect.js"); + if (await pathExists(srcDetect)) { + await mkdir(resolve(installed, "bin"), { recursive: true }); + await cp(srcDetect, instDetect); + patched++; + } + + return patched; +} + +/** Show a non-blocking update notification after install */ +async function showUpdateNotification(): Promise { + const pkgPath = resolve( + dirname(new URL(import.meta.url).pathname), + "../../package.json", + ); + let currentVersion: string; + try { + const pkg = JSON.parse(await readFile(pkgPath, "utf-8")) as { + version: string; + }; + currentVersion = pkg.version; + } catch { + return; + } + + const newer = await checkForUpdate(currentVersion); + if (!newer) return; + + const updateCmd = getUpdateCommand(); + console.log(); + console.log( + ` ${ICONS.drift} ${COLORS.warn(`Update available: ${currentVersion} → ${newer}`)}`, + ); + console.log(` Run ${COLORS.info(updateCmd)} to update`); } diff --git a/src/commands/update.ts b/src/commands/update.ts index b2e6500..5ab9da7 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -259,16 +259,19 @@ export async function updateCommand( const scope: Scope = (options.scope as Scope | undefined) ?? "user"; // CLI version check - if (isInteractive()) { - const spinner = startSpinner("Checking for updates..."); - const newer = await checkForUpdate(currentVersion); - spinner.stop(); + const spinner = isInteractive() + ? startSpinner("Checking for updates...") + : null; + const newer = await checkForUpdate(currentVersion); + spinner?.stop(); - if (newer) { - console.log( - ` ${ICONS.drift} ${COLORS.warn(`New version available: ${currentVersion} → ${newer}`)}`, - ); - const updateCmd = getUpdateCommand(); + if (newer) { + const updateCmd = getUpdateCommand(); + console.log( + ` ${ICONS.drift} ${COLORS.warn(`New version available: ${currentVersion} → ${newer}`)}`, + ); + + if (isInteractive()) { const doUpdate = await confirm({ message: `Update knowpatch CLI? (${updateCmd})`, default: true, @@ -284,8 +287,10 @@ export async function updateCommand( ); } } - console.log(); + } else { + console.log(` Run ${COLORS.info(updateCmd)} to update`); } + console.log(); } console.log(" Checking installation..."); diff --git a/src/core/package-manager.ts b/src/core/package-manager.ts index dc3fdc9..981c4ac 100644 --- a/src/core/package-manager.ts +++ b/src/core/package-manager.ts @@ -19,8 +19,23 @@ export function detectPackageManager(): PackageManager { return "npm"; } +/** Detect if running from an ephemeral bunx/npx cache directory */ +export function isEphemeral(): boolean { + const root = getPackageRoot(); + // bun ephemeral: /tmp/…/bunx-… or macOS /var/folders/…/T/bunx-… + if (/\/tmp\//.test(root) || /\/T\/bunx-/.test(root)) return true; + // npm ephemeral: /_npx/ or /npx/ + if (/\/_npx\//.test(root) || /\/npx\//.test(root)) return true; + return false; +} + export function getUpdateCommand(): string { const pm = detectPackageManager(); + + if (isEphemeral()) { + return pm === "bun" ? "bunx knowpatch@latest" : "npx knowpatch@latest"; + } + const cmds: Record = { bun: "bun update -g knowpatch", npm: "npm update -g knowpatch", diff --git a/src/core/parser.ts b/src/core/parser.ts index 227e2e9..198b7f4 100644 --- a/src/core/parser.ts +++ b/src/core/parser.ts @@ -7,6 +7,7 @@ export interface CorrectionFile { ecosystem: string; description: string; tags: string[]; + version?: string; last_updated: string; /** Raw markdown body below frontmatter */ body: string; @@ -18,6 +19,7 @@ interface Frontmatter { ecosystem: string; description: string; tags: string[]; + version?: string; last_updated: string; } @@ -58,6 +60,7 @@ export function parseCorrectionFile( ecosystem: frontmatter.ecosystem, description: frontmatter.description, tags: frontmatter.tags ?? [], + version: frontmatter.version, last_updated: frontmatter.last_updated, body, file: filename, diff --git a/tests/package-manager.test.ts b/tests/package-manager.test.ts index 3d5c0e8..0b150fd 100644 --- a/tests/package-manager.test.ts +++ b/tests/package-manager.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { detectPackageManager, getUpdateCommand, + isEphemeral, } from "../src/core/package-manager.js"; describe("detectPackageManager", () => { @@ -74,3 +75,10 @@ describe("getUpdateCommand", () => { expect(getUpdateCommand()).toBe("yarn global upgrade knowpatch"); }); }); + +describe("isEphemeral", () => { + test("returns false for non-ephemeral paths", () => { + // Test environment runs from the project directory, not a temp cache + expect(isEphemeral()).toBe(false); + }); +}); diff --git a/tests/parser.test.ts b/tests/parser.test.ts index c31ddb6..ce3f334 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -5,6 +5,7 @@ const sampleContent = `--- ecosystem: test-ecosystem description: Test corrections file tags: [react, typescript, test] +version: "0.4.0" last_updated: "2026-02-24" --- @@ -17,6 +18,16 @@ Some markdown body content here. - **Correct (current)**: React 19 `; +const sampleContentNoVersion = `--- +ecosystem: legacy-ecosystem +description: Legacy file without version +tags: [legacy] +last_updated: "2025-01-01" +--- + +# Legacy +`; + const noFrontmatterContent = `# Plain Markdown No YAML frontmatter here. @@ -30,10 +41,19 @@ describe("parseFrontmatter", () => { expect(frontmatter?.ecosystem).toBe("test-ecosystem"); expect(frontmatter?.description).toBe("Test corrections file"); expect(frontmatter?.tags).toEqual(["react", "typescript", "test"]); + expect(frontmatter?.version).toBe("0.4.0"); expect(frontmatter?.last_updated).toBe("2026-02-24"); expect(body).toContain("# Test Corrections"); }); + test("returns undefined version for frontmatter without version", () => { + const { frontmatter } = parseFrontmatter(sampleContentNoVersion); + + expect(frontmatter).not.toBeNull(); + expect(frontmatter?.version).toBeUndefined(); + expect(frontmatter?.ecosystem).toBe("legacy-ecosystem"); + }); + test("returns null frontmatter for content without frontmatter", () => { const { frontmatter, body } = parseFrontmatter(noFrontmatterContent); @@ -49,9 +69,17 @@ describe("parseCorrectionFile", () => { expect(result.ecosystem).toBe("test-ecosystem"); expect(result.file).toBe("test.md"); expect(result.tags).toEqual(["react", "typescript", "test"]); + expect(result.version).toBe("0.4.0"); expect(result.body).toContain("# Test Corrections"); }); + test("parses a file without version field", () => { + const result = parseCorrectionFile("legacy.md", sampleContentNoVersion); + + expect(result.ecosystem).toBe("legacy-ecosystem"); + expect(result.version).toBeUndefined(); + }); + test("handles file without frontmatter gracefully", () => { const result = parseCorrectionFile("plain.md", noFrontmatterContent);