Skip to content

Commit 15e50a6

Browse files
authored
Merge pull request #320 from syncable-dev/develop
Develop
2 parents cc958d5 + 29b4058 commit 15e50a6

9 files changed

Lines changed: 91 additions & 35 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
},
77
"metadata": {
88
"description": "Syncable CLI skills for AI coding agents — project analysis, security, vulnerabilities, dependencies, IaC validation, and cloud deployment.",
9-
"version": "0.1.0"
9+
"version": "0.1.8"
1010
},
1111
"plugins": [
1212
{
1313
"name": "syncable-cli-skills",
1414
"source": "./installer/plugins/syncable-cli-skills",
1515
"description": "Syncable CLI skills for project analysis, security scanning, vulnerability detection, dependency auditing, IaC validation, Kubernetes optimization, and cloud deployment.",
16-
"version": "0.1.0",
16+
"version": "0.1.8",
1717
"author": {
1818
"name": "Syncable",
1919
"email": "support@syncable.dev"

installer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "syncable-cli-skills",
3-
"version": "0.1.5",
3+
"version": "0.1.8",
44
"type": "module",
55
"description": "Install Syncable CLI skills for AI coding agents (Claude Code, Cursor, Windsurf, Codex, Gemini CLI)",
66
"license": "GPL-3.0",

installer/plugins/syncable-cli-skills/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "syncable-cli-skills",
33
"description": "Syncable CLI skills for project analysis, security scanning, vulnerability detection, dependency auditing, IaC validation, Kubernetes optimization, and cloud deployment.",
4-
"version": "0.1.0",
4+
"version": "0.1.8",
55
"author": {
66
"name": "Syncable",
77
"email": "support@syncable.dev"

installer/scripts/copy-skills.js

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pkg from 'fs-extra';
2-
const { copySync, removeSync, existsSync } = pkg;
3-
import { resolve, dirname } from 'path';
2+
const { copySync, removeSync, existsSync, mkdirpSync, writeFileSync, readdirSync, readFileSync } = pkg;
3+
import { resolve, dirname, basename } from 'path';
44
import { fileURLToPath } from 'url';
55

66
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -12,7 +12,53 @@ if (!existsSync(source)) {
1212
process.exit(1);
1313
}
1414

15+
// Copy raw skills to installer/skills/ (used by the npm package at runtime)
1516
removeSync(dest);
1617
copySync(source, dest);
17-
1818
console.log(`Copied skills from ${source} to ${dest}`);
19+
20+
// Also regenerate installer/plugins/syncable-cli-skills/skills/
21+
// so the Claude Code marketplace plugin stays in sync with the source skills.
22+
const pluginSkillsDir = resolve(__dirname, '..', 'plugins', 'syncable-cli-skills', 'skills');
23+
removeSync(pluginSkillsDir);
24+
25+
function transformSkillFile(filePath) {
26+
const raw = readFileSync(filePath, 'utf-8');
27+
// Parse YAML frontmatter (---\n...\n---\n)
28+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
29+
if (!match) return null;
30+
31+
const frontmatterRaw = match[1];
32+
const body = match[2];
33+
34+
// Extract description value (handles multi-line descriptions with quotes)
35+
const descMatch = frontmatterRaw.match(/^description:\s*(.+)$/m);
36+
if (!descMatch) return null;
37+
38+
const desc = descMatch[1].trim().replace(/^["']|["']$/g, '');
39+
const safeDesc = desc.replace(/"/g, '\\"');
40+
41+
return `---\ndescription: "${safeDesc}"\n---\n${body}`;
42+
}
43+
44+
let skillCount = 0;
45+
for (const category of ['commands', 'workflows']) {
46+
const categoryDir = resolve(source, category);
47+
if (!existsSync(categoryDir)) continue;
48+
49+
for (const file of readdirSync(categoryDir)) {
50+
if (!file.endsWith('.md')) continue;
51+
const skillName = basename(file, '.md');
52+
const content = transformSkillFile(resolve(categoryDir, file));
53+
if (!content) {
54+
console.warn(`Warning: could not parse frontmatter for ${file}, skipping`);
55+
continue;
56+
}
57+
const outDir = resolve(pluginSkillsDir, skillName);
58+
mkdirpSync(outDir);
59+
writeFileSync(resolve(outDir, 'SKILL.md'), content);
60+
skillCount++;
61+
}
62+
}
63+
64+
console.log(`Generated ${skillCount} plugin skills at ${pluginSkillsDir}`);

installer/src/transformers/claude.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { TransformResult } from './types.js';
66
import { execCommand, commandExists } from '../utils.js';
77

88
const PLUGIN_NAME = 'syncable-cli-skills';
9-
const PLUGIN_VERSION = '0.1.0';
9+
const PLUGIN_VERSION = '0.1.8';
1010
const MARKETPLACE_NAME = 'syncable';
1111
const MARKETPLACE_REPO = 'syncable-dev/syncable-cli';
1212

@@ -19,8 +19,6 @@ export function transformForClaude(skill: Skill): TransformResult[] {
1919

2020
const safeDesc = skill.frontmatter.description
2121
.replace(/"/g, '\\"')
22-
.replace(/: /g, ' - ')
23-
.replace(/Trigger on:.*$/, '')
2422
.trim();
2523

2624
const content = `---\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
@@ -154,14 +152,15 @@ function enablePluginInSettings(): void {
154152
settings.extraKnownMarketplaces = {};
155153
}
156154
const marketplaces = settings.extraKnownMarketplaces as Record<string, unknown>;
157-
if (!marketplaces[MARKETPLACE_NAME]) {
158-
marketplaces[MARKETPLACE_NAME] = {
159-
source: {
160-
source: 'github',
161-
repo: MARKETPLACE_REPO,
162-
},
163-
};
164-
}
155+
// Always overwrite the marketplace entry to ensure it is canonical and free
156+
// of non-standard fields (e.g. a stale "path" override added by Claude Code
157+
// dev-mode that causes the plugin to be loaded from the local filesystem).
158+
marketplaces[MARKETPLACE_NAME] = {
159+
source: {
160+
source: 'github',
161+
repo: MARKETPLACE_REPO,
162+
},
163+
};
165164

166165
fs.mkdirSync(path.dirname(settingsFile), { recursive: true });
167166
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
@@ -174,22 +173,32 @@ function enablePluginInSettings(): void {
174173
* 2. Fall back to manual: write cache files + update settings.json
175174
*/
176175
export async function installClaudePlugin(skills: Skill[]): Promise<{ cacheDir: string; skillCount: number }> {
177-
// ── Attempt 1: Official CLI ────────────────────────────────────────
178-
const cliSuccess = await tryClaudeCliInstall();
179-
if (cliSuccess) {
180-
return { cacheDir: getClaudePluginCacheDir(), skillCount: skills.length };
181-
}
176+
// Try the official CLI first — this handles enabledPlugins registration.
177+
// We don't return early on success because the CLI may have cached an old
178+
// version of the plugin that is missing the skills directory (e.g. from a
179+
// previous install before skills were added, or from a stale npx cache).
180+
// We always write skills directly to the cache so they're guaranteed to exist.
181+
await tryClaudeCliInstall();
182182

183-
// ── Attempt 2: Manual write + settings.json ────────────────────────
184183
const cacheDir = getClaudePluginCacheDir();
185184

186-
// Clear old skills
185+
// Remove stale older-version cache entries so Claude Code doesn't load an
186+
// empty/outdated version instead of the current one.
187+
const pluginRootDir = path.dirname(cacheDir);
188+
if (fs.existsSync(pluginRootDir)) {
189+
for (const entry of fs.readdirSync(pluginRootDir)) {
190+
if (entry !== PLUGIN_VERSION) {
191+
fs.rmSync(path.join(pluginRootDir, entry), { recursive: true, force: true });
192+
}
193+
}
194+
}
195+
196+
// Clear old skills and rewrite them so the cache is always up to date.
187197
const skillsDir = path.join(cacheDir, 'skills');
188198
if (fs.existsSync(skillsDir)) {
189199
fs.rmSync(skillsDir, { recursive: true });
190200
}
191201

192-
// Write each skill
193202
for (const skill of skills) {
194203
const results = transformForClaude(skill);
195204
for (const { relativePath, content } of results) {
@@ -199,10 +208,7 @@ export async function installClaudePlugin(skills: Skill[]): Promise<{ cacheDir:
199208
}
200209
}
201210

202-
// Write plugin manifest
203211
writePluginManifest(cacheDir);
204-
205-
// Enable in settings.json (THE KEY FIX)
206212
enablePluginInSettings();
207213

208214
return { cacheDir, skillCount: skills.length };

installer/src/transformers/codex.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Skill } from '../skills.js';
22
import { TransformResult } from './types.js';
33

44
export function transformForCodex(skill: Skill): TransformResult[] {
5-
const content = `---\nname: ${skill.frontmatter.name}\ndescription: ${skill.frontmatter.description}\n---\n\n${skill.body}`;
5+
const safeName = skill.frontmatter.name.replace(/"/g, '\\"');
6+
const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"');
7+
const content = `---\nname: "${safeName}"\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
68
return [{ relativePath: `${skill.frontmatter.name}/SKILL.md`, content }];
79
}

installer/src/transformers/cursor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { TransformResult } from './types.js';
33

44
export function transformForCursor(skill: Skill): TransformResult[] {
55
const filename = skill.frontmatter.name + '.mdc';
6-
const content = `---\ndescription: "Syncable CLI: ${skill.frontmatter.description}"\nglobs:\nalwaysApply: true\n---\n\n${skill.body}`;
6+
const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"');
7+
const content = `---\ndescription: "Syncable CLI: ${safeDesc}"\nglobs:\nalwaysApply: true\n---\n\n${skill.body}`;
78
return [{ relativePath: filename, content }];
89
}

installer/src/transformers/windsurf.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { TransformResult } from './types.js';
33

44
export function transformForWindsurf(skill: Skill): TransformResult[] {
55
const filename = skill.frontmatter.name + '.md';
6-
const content = `---\ntrigger: always\ndescription: "Syncable CLI: ${skill.frontmatter.description}"\n---\n\n${skill.body}`;
6+
const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"');
7+
const content = `---\ntrigger: always\ndescription: "Syncable CLI: ${safeDesc}"\n---\n\n${skill.body}`;
78
return [{ relativePath: filename, content }];
89
}

installer/tests/transformers/codex.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ describe('transformForCodex', () => {
1515
expect(result[0].relativePath).toBe('syncable-analyze/SKILL.md');
1616
});
1717

18-
it('preserves frontmatter in SKILL.md', () => {
18+
it('preserves frontmatter in SKILL.md with quoted values', () => {
1919
const result = transformForCodex(sampleSkill);
20-
expect(result[0].content).toContain('name: syncable-analyze');
21-
expect(result[0].content).toContain('description: Use when analyzing a project');
20+
expect(result[0].content).toContain('name: "syncable-analyze"');
21+
expect(result[0].content).toContain('description: "Use when analyzing a project"');
2222
});
2323

2424
it('preserves body content', () => {

0 commit comments

Comments
 (0)