enhancement(#782): emit gsd skills to ~/.cline/skills for Cline >= v3.48 (#809)

Cline added a global skills system (~/.cline/skills/<name>/SKILL.md) in
v3.48.0, but gsd treated Cline as rules-only and emitted zero skills
(getGlobalSkillsBase('cline')=null, empty artifact kinds). This makes gsd
emit skills for Cline at global scope, alongside the existing .clinerules.

- runtime-homes: getGlobalSkillsBase('cline') -> ~/.cline/skills (was null)
- runtime-artifact-layout: cline emits a skills kind for GLOBAL scope only
  (local stays .clinerules-only), mirroring claude's scope dispatch
- install.js: convertClaudeCommandToClineSkill emits name+description-only
  SKILL.md frontmatter (Cline/agentskills.io spec; no Claude-specific
  allowed-tools/argument-hint/agent), hyphen-normalized + .cline/-rewritten
  body; global cline routed through the skills path while .clinerules is
  still written; _applyRuntimeRewrites cline case handles custom
  CLINE_CONFIG_DIR; convertClaudeToCliineMarkdown also rewrites bare
  ~/.claude and CLAUDE_CONFIG_DIR
- docs: install-on-your-runtime.md documents Cline global skills vs local rules
- tests: converter (name+description-only), global emission, skills+.clinerules
  coexistence, scope-aware layout, custom-dir paths, idempotency

Closes #782

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-07 15:48:40 -04:00
committed by GitHub
parent 5618505f9a
commit 5e4e7de1ff
10 changed files with 787 additions and 36 deletions

View File

@@ -210,8 +210,19 @@ describe('resolveRuntimeArtifactLayout — codebuddy', () => {
});
describe('resolveRuntimeArtifactLayout — cline', () => {
test('returns correct layout for cline', () => {
const layout = resolveRuntimeArtifactLayout('cline', FAKE_DIR);
test('returns correct layout for cline global (skills-capable since v3.48.0 — #782)', () => {
const layout = resolveRuntimeArtifactLayout('cline', FAKE_DIR, 'global');
assert.strictEqual(layout.runtime, 'cline');
assert.strictEqual(layout.configDir, FAKE_DIR);
assert.strictEqual(layout.kinds.length, 1);
assert.strictEqual(layout.kinds[0].kind, 'skills');
assert.strictEqual(layout.kinds[0].destSubpath, 'skills');
assert.strictEqual(layout.kinds[0].prefix, 'gsd-');
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
});
test('cline local: no skills kinds (global-only, #782)', () => {
const layout = resolveRuntimeArtifactLayout('cline', FAKE_DIR, 'local');
assert.strictEqual(layout.runtime, 'cline');
assert.strictEqual(layout.configDir, FAKE_DIR);
assert.strictEqual(layout.kinds.length, 0);
@@ -253,9 +264,10 @@ describe('resolveRuntimeArtifactLayout edge-cases', () => {
assert.strictEqual(layout.kinds[0].prefix, '');
});
test('cline has no kinds', () => {
test('cline has one skills kind (#782)', () => {
const layout = resolveRuntimeArtifactLayout('cline', '/tmp/x');
assert.strictEqual(layout.kinds.length, 0);
assert.strictEqual(layout.kinds.length, 1);
assert.strictEqual(layout.kinds[0].kind, 'skills');
});
test('gemini has one commands kind', () => {