diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1a9330e..faaf4e7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,14 +6,14 @@ }, "metadata": { "description": "Syncable CLI skills for AI coding agents — project analysis, security, vulnerabilities, dependencies, IaC validation, and cloud deployment.", - "version": "0.1.8" + "version": "0.1.11" }, "plugins": [ { "name": "syncable-cli-skills", "source": "./installer/plugins/syncable-cli-skills", "description": "Syncable CLI skills for project analysis, security scanning, vulnerability detection, dependency auditing, IaC validation, Kubernetes optimization, and cloud deployment.", - "version": "0.1.8", + "version": "0.1.11", "author": { "name": "Syncable", "email": "support@syncable.dev" @@ -21,7 +21,15 @@ "homepage": "https://syncable.dev", "repository": "https://github.com/syncable-dev/syncable-cli", "license": "MIT", - "keywords": ["syncable", "devops", "security", "deployment", "kubernetes", "docker", "iac"] + "keywords": [ + "syncable", + "devops", + "security", + "deployment", + "kubernetes", + "docker", + "iac" + ] } ] } diff --git a/installer/package.json b/installer/package.json index fb46d29..ce24a2b 100644 --- a/installer/package.json +++ b/installer/package.json @@ -1,6 +1,6 @@ { "name": "syncable-cli-skills", - "version": "0.1.8", + "version": "0.1.11", "type": "module", "description": "Install Syncable CLI skills for AI coding agents (Claude Code, Cursor, Windsurf, Codex, Gemini CLI)", "license": "GPL-3.0", diff --git a/installer/plugins/syncable-cli-skills/.claude-plugin/plugin.json b/installer/plugins/syncable-cli-skills/.claude-plugin/plugin.json index fd74b05..f6e4a48 100644 --- a/installer/plugins/syncable-cli-skills/.claude-plugin/plugin.json +++ b/installer/plugins/syncable-cli-skills/.claude-plugin/plugin.json @@ -1,12 +1,20 @@ { "name": "syncable-cli-skills", "description": "Syncable CLI skills for project analysis, security scanning, vulnerability detection, dependency auditing, IaC validation, Kubernetes optimization, and cloud deployment.", - "version": "0.1.8", + "version": "0.1.11", "author": { "name": "Syncable", "email": "support@syncable.dev" }, "homepage": "https://syncable.dev", "license": "MIT", - "keywords": ["syncable", "devops", "security", "deployment", "kubernetes", "docker", "iac"] + "keywords": [ + "syncable", + "devops", + "security", + "deployment", + "kubernetes", + "docker", + "iac" + ] } diff --git a/installer/scripts/copy-skills.js b/installer/scripts/copy-skills.js index 06e7ed9..0605586 100644 --- a/installer/scripts/copy-skills.js +++ b/installer/scripts/copy-skills.js @@ -7,6 +7,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const source = resolve(__dirname, '..', '..', 'skills'); const dest = resolve(__dirname, '..', 'skills'); +// Read the canonical version from package.json +const packageJson = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8')); +const version = packageJson.version; + if (!existsSync(source)) { console.error('Error: skills/ directory not found at', source); process.exit(1); @@ -17,21 +21,58 @@ removeSync(dest); copySync(source, dest); console.log(`Copied skills from ${source} to ${dest}`); -// Also regenerate installer/plugins/syncable-cli-skills/skills/ -// so the Claude Code marketplace plugin stays in sync with the source skills. +// ── Sync version across all files that reference it ───────────────── + +// 1. installer/plugins/syncable-cli-skills/.claude-plugin/plugin.json +const pluginJsonPath = resolve(__dirname, '..', 'plugins', 'syncable-cli-skills', '.claude-plugin', 'plugin.json'); +if (existsSync(pluginJsonPath)) { + const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf-8')); + pluginJson.version = version; + writeFileSync(pluginJsonPath, JSON.stringify(pluginJson, null, 2) + '\n'); + console.log(`Synced plugin.json version to ${version}`); +} + +// 2. .claude-plugin/marketplace.json (repo root) +const marketplacePath = resolve(__dirname, '..', '..', '.claude-plugin', 'marketplace.json'); +if (existsSync(marketplacePath)) { + const marketplace = JSON.parse(readFileSync(marketplacePath, 'utf-8')); + if (marketplace.metadata) marketplace.metadata.version = version; + if (marketplace.plugins) { + for (const plugin of marketplace.plugins) { + if (plugin.name === 'syncable-cli-skills') { + plugin.version = version; + } + } + } + writeFileSync(marketplacePath, JSON.stringify(marketplace, null, 2) + '\n'); + console.log(`Synced marketplace.json version to ${version}`); +} + +// 3. PLUGIN_VERSION constant in src/transformers/claude.ts +const claudeTsPath = resolve(__dirname, '..', 'src', 'transformers', 'claude.ts'); +if (existsSync(claudeTsPath)) { + let claudeTs = readFileSync(claudeTsPath, 'utf-8'); + claudeTs = claudeTs.replace( + /const PLUGIN_VERSION = '[^']+';/, + `const PLUGIN_VERSION = '${version}';` + ); + writeFileSync(claudeTsPath, claudeTs); + console.log(`Synced PLUGIN_VERSION to ${version}`); +} + +// ── Regenerate plugin skills ──────────────────────────────────────── + const pluginSkillsDir = resolve(__dirname, '..', 'plugins', 'syncable-cli-skills', 'skills'); removeSync(pluginSkillsDir); function transformSkillFile(filePath) { const raw = readFileSync(filePath, 'utf-8'); - // Parse YAML frontmatter (---\n...\n---\n) const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!match) return null; const frontmatterRaw = match[1]; const body = match[2]; - // Extract description value (handles multi-line descriptions with quotes) const descMatch = frontmatterRaw.match(/^description:\s*(.+)$/m); if (!descMatch) return null; diff --git a/installer/src/transformers/claude.ts b/installer/src/transformers/claude.ts index 2593a17..300c4e8 100644 --- a/installer/src/transformers/claude.ts +++ b/installer/src/transformers/claude.ts @@ -6,7 +6,7 @@ import { TransformResult } from './types.js'; import { execCommand, commandExists } from '../utils.js'; const PLUGIN_NAME = 'syncable-cli-skills'; -const PLUGIN_VERSION = '0.1.8'; +const PLUGIN_VERSION = '0.1.11'; const MARKETPLACE_NAME = 'syncable'; const MARKETPLACE_REPO = 'syncable-dev/syncable-cli'; @@ -173,32 +173,23 @@ function enablePluginInSettings(): void { * 2. Fall back to manual: write cache files + update settings.json */ export async function installClaudePlugin(skills: Skill[]): Promise<{ cacheDir: string; skillCount: number }> { - // Try the official CLI first — this handles enabledPlugins registration. - // We don't return early on success because the CLI may have cached an old - // version of the plugin that is missing the skills directory (e.g. from a - // previous install before skills were added, or from a stale npx cache). - // We always write skills directly to the cache so they're guaranteed to exist. + // Try the official CLI first — this registers the marketplace and plugin + // in Claude Code's settings. We still do a manual write afterwards because + // the CLI-cached version may be stale or missing skills. await tryClaudeCliInstall(); const cacheDir = getClaudePluginCacheDir(); + const pluginRootDir = path.dirname(cacheDir); // .../syncable-cli-skills/ - // Remove stale older-version cache entries so Claude Code doesn't load an - // empty/outdated version instead of the current one. - const pluginRootDir = path.dirname(cacheDir); + // Nuke the ENTIRE plugin cache (all versions) and recreate fresh. + // This prevents version mismatches, stale caches, and — critically — + // removes any .orphaned_at marker that Claude Code writes when a cached + // version doesn't match the marketplace catalog. if (fs.existsSync(pluginRootDir)) { - for (const entry of fs.readdirSync(pluginRootDir)) { - if (entry !== PLUGIN_VERSION) { - fs.rmSync(path.join(pluginRootDir, entry), { recursive: true, force: true }); - } - } - } - - // Clear old skills and rewrite them so the cache is always up to date. - const skillsDir = path.join(cacheDir, 'skills'); - if (fs.existsSync(skillsDir)) { - fs.rmSync(skillsDir, { recursive: true }); + fs.rmSync(pluginRootDir, { recursive: true, force: true }); } + // Write every skill into a clean cache directory. for (const skill of skills) { const results = transformForClaude(skill); for (const { relativePath, content } of results) { @@ -211,9 +202,32 @@ export async function installClaudePlugin(skills: Skill[]): Promise<{ cacheDir: writePluginManifest(cacheDir); enablePluginInSettings(); + // Also write skills to ~/.claude/skills/ for SDK-based integrations + // (e.g. Zed's ACP adapter) that don't read from the plugin cache. + // The SDK loads user-level skills from this directory when configured + // with settingSources: ["user"]. + writeUserLevelSkills(skills); + return { cacheDir, skillCount: skills.length }; } +/** + * Write skills to ~/.claude/skills/ so they're available to SDK-based + * integrations (Zed, etc.) that don't read the plugin cache. + */ +function writeUserLevelSkills(skills: Skill[]): void { + const userSkillsDir = path.join(os.homedir(), '.claude', 'skills'); + + for (const skill of skills) { + const results = transformForClaude(skill); + for (const { relativePath, content } of results) { + const fullPath = path.join(userSkillsDir, relativePath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, content); + } + } +} + /** * Remove the Claude Code plugin. */ @@ -263,19 +277,18 @@ export async function uninstallClaudePlugin(): Promise { } } - // Clean up old flat-file skills - const oldDirs = [ - path.join(os.homedir(), '.claude', 'skills', 'syncable'), - ]; - for (const dir of oldDirs) { - if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true }); - } - - const flatSkillsDir = path.join(os.homedir(), '.claude', 'skills'); - if (fs.existsSync(flatSkillsDir)) { - for (const file of fs.readdirSync(flatSkillsDir)) { - if (file.startsWith('syncable-') && file.endsWith('.md')) { - fs.unlinkSync(path.join(flatSkillsDir, file)); + // Clean up user-level skills (both old flat files and new directory format) + const userSkillsDir = path.join(os.homedir(), '.claude', 'skills'); + if (fs.existsSync(userSkillsDir)) { + for (const entry of fs.readdirSync(userSkillsDir)) { + if (entry.startsWith('syncable-')) { + const entryPath = path.join(userSkillsDir, entry); + const stat = fs.statSync(entryPath); + if (stat.isDirectory()) { + fs.rmSync(entryPath, { recursive: true }); + } else if (entry.endsWith('.md')) { + fs.unlinkSync(entryPath); + } } } }