feat(#1049): phase 5d — drive artifactLayout from the runtime descriptor (retire the 128-LOC switch) (#1053)
resolveRuntimeArtifactLayout now builds Layout from
registry.runtimes[id].runtime.artifactLayout[scope] — a loop dispatching each
ArtifactKind through the SAME 5 builders (commandsKind/agentsKind/skillsKind/
convertedCommandsKind/kimiAgentsKind, unchanged) by (kind, converter, nesting) —
replacing the hardcoded switch(runtime). Equivalence-preserving for all 16 runtimes
× {global, local} (Codex-verified, no divergence). -43 LOC; bin/install.js + the
converters + the install loop untouched. getInstallExports()[converterName]
resolution, configDir threading, scope default, unknown-runtime guard all preserved.
Driving the local scope surfaced a 5a gap: the old switch had no scope branch for 13
runtimes (cursor/gemini/codex/copilot/antigravity/windsurf/augment/trae/qwen/hermes/
codebuddy/opencode/kilo) → local == global for them, but 5a authored local:[].
Backfilled local=global for those 13 (descriptor-faithful; a fall-through shim would
wrongly give cline/kimi local=global). claude/cline/kimi scope-gating untouched.
validateArtifactKindEntry tightened: destSubpath/prefix/nesting/converter required
(ConverterName enum still open — 5e). New 39-case deep-equal golden equivalence test.
Closes #1049
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,16 @@
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
|
||||
@@ -31,7 +31,24 @@
|
||||
"converter": "convertClaudeCommandToAugmentSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAugmentSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
|
||||
@@ -31,7 +31,24 @@
|
||||
"converter": "convertClaudeCommandToCodebuddySkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCodebuddyCommand"
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCodebuddySkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
|
||||
@@ -23,7 +23,16 @@
|
||||
"converter": "convertClaudeCommandToCodexSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCodexSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "shell-var",
|
||||
"hooksSurface": "codex-hooks-json",
|
||||
|
||||
@@ -23,7 +23,16 @@
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "copilot-inline",
|
||||
|
||||
@@ -31,7 +31,24 @@
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToCursorSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "cursor-hooks-json",
|
||||
|
||||
@@ -23,7 +23,16 @@
|
||||
"converter": null
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands/gsd",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
|
||||
@@ -23,7 +23,16 @@
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills/gsd",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
|
||||
@@ -36,7 +36,24 @@
|
||||
"converter": "convertClaudeCommandToKiloSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "command",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToKiloSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
|
||||
@@ -31,7 +31,24 @@
|
||||
"converter": "convertClaudeCommandToOpencodeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "command",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToOpencodeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
|
||||
@@ -23,7 +23,16 @@
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
|
||||
@@ -23,7 +23,16 @@
|
||||
"converter": "convertClaudeCommandToTraeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToTraeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
|
||||
@@ -24,7 +24,16 @@
|
||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
|
||||
@@ -40,7 +40,16 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -111,7 +120,24 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToAugmentSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAugmentSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -243,7 +269,24 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToCodebuddySkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCodebuddyCommand"
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCodebuddySkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -279,7 +322,16 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToCodexSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCodexSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "shell-var",
|
||||
"hooksSurface": "codex-hooks-json",
|
||||
@@ -316,7 +368,16 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "copilot-inline",
|
||||
@@ -359,7 +420,24 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToCursorSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "cursor-hooks-json",
|
||||
@@ -395,7 +473,16 @@ const capabilities = {
|
||||
"converter": null
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands/gsd",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -461,7 +548,16 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills/gsd",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -540,7 +636,24 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToKiloSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "command",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToKiloSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
@@ -633,7 +746,24 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToOpencodeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "command",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToOpencodeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
@@ -668,7 +798,16 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -704,7 +843,16 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToTraeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToTraeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
@@ -825,7 +973,16 @@ const capabilities = {
|
||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
@@ -1038,7 +1195,16 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -1082,7 +1248,24 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToAugmentSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAugmentSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -1214,7 +1397,24 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToCodebuddySkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCodebuddyCommand"
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCodebuddySkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -1250,7 +1450,16 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToCodexSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCodexSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "shell-var",
|
||||
"hooksSurface": "codex-hooks-json",
|
||||
@@ -1287,7 +1496,16 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "copilot-inline",
|
||||
@@ -1330,7 +1548,24 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToCursorSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "cursor-hooks-json",
|
||||
@@ -1366,7 +1601,16 @@ const runtimes = {
|
||||
"converter": null
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands/gsd",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -1402,7 +1646,16 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills/gsd",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -1453,7 +1706,24 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToKiloSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "command",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToKiloSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
@@ -1546,7 +1816,24 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToOpencodeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "command",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToOpencodeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
@@ -1581,7 +1868,16 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "settings-json",
|
||||
@@ -1617,7 +1913,16 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToTraeSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToTraeSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
@@ -1653,7 +1958,16 @@ const runtimes = {
|
||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
||||
}
|
||||
],
|
||||
"local": []
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
|
||||
@@ -581,21 +581,21 @@ function validateArtifactKindEntry(capId, entry, prefix) {
|
||||
errors.push(ctx + '.destSubpath must be a non-empty string');
|
||||
}
|
||||
|
||||
// nesting — optional; if present must be in closed vocab
|
||||
if (entry.nesting !== undefined) {
|
||||
if (!VALID_ARTIFACT_NESTINGS.has(entry.nesting)) {
|
||||
errors.push(
|
||||
ctx + '.nesting must be one of: ' + [...VALID_ARTIFACT_NESTINGS].join(', ') +
|
||||
' (got: ' + JSON.stringify(entry.nesting) + ')',
|
||||
);
|
||||
}
|
||||
// nesting — required; must be in closed vocab (ADR-857 §5d: now drives install)
|
||||
if (entry.nesting === undefined || entry.nesting === null) {
|
||||
errors.push(ctx + '.nesting is required and must be one of: ' + [...VALID_ARTIFACT_NESTINGS].join(', '));
|
||||
} else if (!VALID_ARTIFACT_NESTINGS.has(entry.nesting)) {
|
||||
errors.push(
|
||||
ctx + '.nesting must be one of: ' + [...VALID_ARTIFACT_NESTINGS].join(', ') +
|
||||
' (got: ' + JSON.stringify(entry.nesting) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// prefix — optional; if present must be a string
|
||||
if (entry.prefix !== undefined) {
|
||||
if (typeof entry.prefix !== 'string') {
|
||||
errors.push(ctx + '.prefix must be a string if present (got: ' + typeof entry.prefix + ')');
|
||||
}
|
||||
// prefix — required; must be a string (may be empty string '')
|
||||
if (entry.prefix === undefined || entry.prefix === null) {
|
||||
errors.push(ctx + '.prefix is required (must be a string, may be empty)');
|
||||
} else if (typeof entry.prefix !== 'string') {
|
||||
errors.push(ctx + '.prefix must be a string (got: ' + typeof entry.prefix + ')');
|
||||
}
|
||||
|
||||
// recursive — optional; if present must be a boolean
|
||||
@@ -605,11 +605,11 @@ function validateArtifactKindEntry(capId, entry, prefix) {
|
||||
}
|
||||
}
|
||||
|
||||
// converter — optional; if present must be a string or null (closed ConverterName enum in phase 5e)
|
||||
if (entry.converter !== undefined) {
|
||||
if (entry.converter !== null && typeof entry.converter !== 'string') {
|
||||
errors.push(ctx + '.converter must be a string or null if present (got: ' + typeof entry.converter + ')');
|
||||
}
|
||||
// converter — required; must be a string or null (closed ConverterName enum in phase 5e)
|
||||
if (!Object.prototype.hasOwnProperty.call(entry, 'converter')) {
|
||||
errors.push(ctx + '.converter is required (must be a string or null)');
|
||||
} else if (entry.converter !== null && typeof entry.converter !== 'string') {
|
||||
errors.push(ctx + '.converter must be a string or null (got: ' + typeof entry.converter + ')');
|
||||
}
|
||||
|
||||
return errors;
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
},
|
||||
"runtime-artifact-layout": {
|
||||
"files": [
|
||||
"runtime-artifact-layout-descriptor-drive.test.cjs",
|
||||
"runtime-artifact-layout-install-profiles.test.cjs",
|
||||
"runtime-artifact-layout-surface.test.cjs",
|
||||
"runtime-artifact-layout.test.cjs"
|
||||
|
||||
@@ -352,8 +352,72 @@ function convertedCommandsKind(
|
||||
// windsurf — docs.devin.ai/desktop/cascade/skills
|
||||
// codebuddy — codebuddy.ai/docs/cli/skills
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Descriptor-driven dispatch helpers (ADR-857 phase 5d)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ArtifactKindDescriptor {
|
||||
kind: string;
|
||||
destSubpath: string;
|
||||
prefix: string;
|
||||
nesting: 'flat' | 'nested';
|
||||
recursive: boolean;
|
||||
converter: string | null;
|
||||
}
|
||||
|
||||
interface ArtifactLayoutDescriptor {
|
||||
global: ArtifactKindDescriptor[];
|
||||
local: ArtifactKindDescriptor[];
|
||||
}
|
||||
|
||||
/** Lazy registry accessor — mirrors pattern from 5b/5c (runtime-homes.cts). */
|
||||
function getRegistry(): { runtimes: Record<string, { runtime?: { artifactLayout?: ArtifactLayoutDescriptor } }> } {
|
||||
return _require('./capability-registry.cjs') as {
|
||||
runtimes: Record<string, { runtime?: { artifactLayout?: ArtifactLayoutDescriptor } }>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a single ArtifactKindDescriptor entry to an ArtifactKind using the
|
||||
* matching builder function. Mirrors the hand-built calls in the old switch.
|
||||
*/
|
||||
function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, configDir: string): ArtifactKind {
|
||||
const { kind, destSubpath, prefix, nesting, converter } = entry;
|
||||
const nested = nesting === 'nested';
|
||||
|
||||
switch (kind) {
|
||||
case 'commands':
|
||||
if (converter == null) {
|
||||
return commandsKind(destSubpath, prefix, configDir);
|
||||
}
|
||||
return convertedCommandsKind(destSubpath, prefix, converter, configDir);
|
||||
|
||||
case 'agents':
|
||||
return agentsKind(destSubpath, prefix, configDir);
|
||||
|
||||
case 'skills':
|
||||
if (converter == null) {
|
||||
throw new TypeError(
|
||||
`resolveRuntimeArtifactLayout: skills entry for '${runtime}' has converter=null (converter is required for skills)`,
|
||||
);
|
||||
}
|
||||
return skillsKind(destSubpath, prefix, converter, runtime, configDir, nested);
|
||||
|
||||
case 'kimi-agents':
|
||||
return kimiAgentsKind(destSubpath, prefix, configDir);
|
||||
|
||||
default:
|
||||
throw new TypeError(
|
||||
`resolveRuntimeArtifactLayout: unknown kind '${kind}' in descriptor for runtime '${runtime}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the artifact layout for a given runtime and config directory.
|
||||
*
|
||||
* ADR-857 phase 5d: driven by the capability-registry artifactLayout descriptor
|
||||
* instead of a hardcoded switch statement.
|
||||
*/
|
||||
function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope: 'local' | 'global' = 'global'): Layout {
|
||||
if (typeof configDir !== 'string' || configDir === '') {
|
||||
@@ -366,122 +430,15 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
|
||||
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
|
||||
}
|
||||
|
||||
let kinds: ArtifactKind[];
|
||||
switch (runtime) {
|
||||
case 'claude':
|
||||
if (scope === 'local') {
|
||||
kinds = [
|
||||
commandsKind('commands/gsd', 'gsd-', configDir),
|
||||
agentsKind('agents', 'gsd-', configDir),
|
||||
];
|
||||
} else {
|
||||
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'claude', configDir)];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cursor':
|
||||
// Cursor 1.6+ supports two artifact surfaces:
|
||||
// 1. skills/gsd-<name>/SKILL.md — rich skills with frontmatter + adapter header
|
||||
// 2. commands/gsd-<name>.md — plain markdown slash commands (no frontmatter)
|
||||
// accessed via '/' in the Agent input (#785)
|
||||
kinds = [
|
||||
skillsKind('skills', 'gsd-', 'convertClaudeCommandToCursorSkill', 'cursor', configDir),
|
||||
convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCursorCommand', configDir),
|
||||
];
|
||||
break;
|
||||
|
||||
case 'gemini':
|
||||
kinds = [commandsKind('commands/gsd', 'gsd-', configDir)];
|
||||
break;
|
||||
|
||||
case 'codex':
|
||||
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToCodexSkill', 'codex', configDir)];
|
||||
break;
|
||||
|
||||
case 'copilot':
|
||||
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToCopilotSkill', 'copilot', configDir)];
|
||||
break;
|
||||
|
||||
case 'antigravity':
|
||||
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToAntigravitySkill', 'antigravity', configDir, true /* #69 nested */)];
|
||||
break;
|
||||
|
||||
case 'windsurf':
|
||||
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToWindsurfSkill', 'windsurf', configDir)];
|
||||
break;
|
||||
|
||||
case 'augment':
|
||||
kinds = [
|
||||
commandsKind('commands', 'gsd-', configDir),
|
||||
skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir, true /* #69 nested */),
|
||||
];
|
||||
break;
|
||||
|
||||
case 'trae':
|
||||
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToTraeSkill', 'trae', configDir, true /* #69 nested */)];
|
||||
break;
|
||||
|
||||
case 'qwen':
|
||||
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'qwen', configDir, true /* #69 nested */)];
|
||||
break;
|
||||
|
||||
case 'hermes':
|
||||
// #947: restore canonical gsd- prefix — skills land at skills/gsd/gsd-<stem>/SKILL.md
|
||||
// and dispatch as /gsd-<stem>, consistent with every other runtime.
|
||||
// The skills/gsd/ category bucket (introduced by #2841) is retained.
|
||||
// Prior bare-stem layout (prefix='') used by #3664 is reversed here.
|
||||
kinds = [skillsKind('skills/gsd', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'hermes', configDir, true /* #69 nested */)];
|
||||
break;
|
||||
|
||||
case 'codebuddy':
|
||||
// CodeBuddy (Tencent) reads two user-level surfaces (codebuddy.ai/docs/cli):
|
||||
// 1. commands/gsd-<name>.md — slash commands shown in the '/' menu (#789)
|
||||
// 2. skills/gsd-<name>/SKILL.md — model-invocable skills, emitted with
|
||||
// user-invocable:false so they stay OUT of '/' (the commands surface is
|
||||
// the sole '/' entry point) — avoids a duplicated /gsd-* per workflow.
|
||||
// Subagents (~/.codebuddy/agents/) are already emitted by the generic agents
|
||||
// block in bin/install.js; MCP is excluded (gsd ships no MCP server).
|
||||
kinds = [
|
||||
convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCodebuddyCommand', configDir),
|
||||
skillsKind('skills', 'gsd-', 'convertClaudeCommandToCodebuddySkill', 'codebuddy', configDir),
|
||||
];
|
||||
break;
|
||||
|
||||
case 'cline':
|
||||
kinds = scope === 'global' ? [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClineSkill', 'cline', configDir, true /* #69 nested */)] : [];
|
||||
break;
|
||||
|
||||
case 'kimi':
|
||||
kinds = scope === 'global'
|
||||
? [
|
||||
skillsKind('skills', 'gsd-', 'convertClaudeCommandToKimiSkill', 'kimi', configDir),
|
||||
kimiAgentsKind('agents', 'gsd', configDir),
|
||||
]
|
||||
: [];
|
||||
break;
|
||||
|
||||
case 'opencode':
|
||||
// OpenCode reads flat slash commands from command/ and on-demand skills
|
||||
// from skills/<name>/SKILL.md (https://opencode.ai/docs/skills). Emit both.
|
||||
kinds = [
|
||||
commandsKind('command', 'gsd-', configDir),
|
||||
skillsKind('skills', 'gsd-', 'convertClaudeCommandToOpencodeSkill', 'opencode', configDir),
|
||||
];
|
||||
break;
|
||||
|
||||
case 'kilo':
|
||||
// Kilo derives from OpenCode and shares the skills/<name>/SKILL.md layout
|
||||
// (https://kilo.ai/docs/customize/skills). Emit flat commands + skills.
|
||||
kinds = [
|
||||
commandsKind('command', 'gsd-', configDir),
|
||||
skillsKind('skills', 'gsd-', 'convertClaudeCommandToKiloSkill', 'kilo', configDir),
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
|
||||
const desc = getRegistry().runtimes[runtime]?.runtime?.artifactLayout;
|
||||
if (!desc) {
|
||||
// Runtime is in ALLOWED_RUNTIMES but has no descriptor — reproduce old default: throw.
|
||||
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
|
||||
}
|
||||
|
||||
const entries: ArtifactKindDescriptor[] = desc[scope] ?? [];
|
||||
const kinds: ArtifactKind[] = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir));
|
||||
|
||||
return { runtime, configDir, scope, kinds };
|
||||
}
|
||||
|
||||
|
||||
346
tests/runtime-artifact-layout-descriptor-drive.test.cjs
Normal file
346
tests/runtime-artifact-layout-descriptor-drive.test.cjs
Normal file
@@ -0,0 +1,346 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Equivalence proof for ADR-857 phase 5d: descriptor-driven resolveRuntimeArtifactLayout.
|
||||
*
|
||||
* For every runtime in the 16-entry capability registry × {global, local} scopes,
|
||||
* this test asserts that:
|
||||
* 1. kind.kind, kind.destSubpath, kind.prefix are byte-identical to the STEP-0
|
||||
* golden captured from the old switch() before any edits.
|
||||
* 2. typeof kind.stage === 'function' for every kind.
|
||||
* 3. layout.runtime === runtime, layout.configDir === configDir, layout.scope === scope.
|
||||
*
|
||||
* SCOPE-FALL-THROUGH NOTE:
|
||||
* The old switch() had no scope branches for 13 runtimes (cursor, gemini, codex,
|
||||
* copilot, antigravity, windsurf, augment, trae, qwen, hermes, codebuddy, opencode,
|
||||
* kilo), meaning scope='local' returned the same kinds as scope='global'. The 5a
|
||||
* descriptors incorrectly set local:[] for those runtimes, causing 31 local-install
|
||||
* test regressions. The 5b backfill sets local == global for these 13, restoring
|
||||
* the old switch's scope-agnostic behaviour.
|
||||
*
|
||||
* For runtimes that had explicit scope branches in the old switch
|
||||
* (claude: distinct local=commands+agents; cline: local=[]; kimi: local=[]),
|
||||
* the STEP-0 golden matches the descriptor exactly and is left unchanged.
|
||||
*
|
||||
* Unknown runtime case:
|
||||
* ALLOWED_RUNTIMES guard throws TypeError BEFORE the descriptor lookup,
|
||||
* so unknown runtimes still throw:
|
||||
* TypeError: Unknown runtime: 'grok' — add to runtime-artifact-layout.cjs table
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const { resolveRuntimeArtifactLayout } = require(
|
||||
path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs'),
|
||||
);
|
||||
|
||||
const FAKE_DIR = '/tmp/fake-config-dir-dd';
|
||||
|
||||
// ── STEP-0 golden (captured from switch BEFORE edits) ────────────────────────
|
||||
// Format: { kind, destSubpath, prefix } for each entry in kinds[].
|
||||
// 'function' means we assert typeof kind.stage === 'function'.
|
||||
|
||||
const GOLDEN = {
|
||||
// ── claude ──────────────────────────────────────────────────────────────────
|
||||
'claude/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'claude/local': [
|
||||
{ kind: 'commands', destSubpath: 'commands/gsd', prefix: 'gsd-' },
|
||||
{ kind: 'agents', destSubpath: 'agents', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── cursor ───────────────────────────────────────────────────────────────────
|
||||
// Old switch: BOTH scopes returned [skills, commands] (no scope branch).
|
||||
// 5b backfill: local == global.
|
||||
'cursor/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
|
||||
],
|
||||
'cursor/local': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── gemini ───────────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'gemini/global': [
|
||||
{ kind: 'commands', destSubpath: 'commands/gsd', prefix: 'gsd-' },
|
||||
],
|
||||
'gemini/local': [
|
||||
{ kind: 'commands', destSubpath: 'commands/gsd', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── codex ────────────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'codex/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'codex/local': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── copilot ──────────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'copilot/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'copilot/local': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── antigravity ──────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'antigravity/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'antigravity/local': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── windsurf ─────────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'windsurf/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'windsurf/local': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── augment ──────────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'augment/global': [
|
||||
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'augment/local': [
|
||||
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── trae ─────────────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'trae/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'trae/local': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── qwen ─────────────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'qwen/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'qwen/local': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── hermes ───────────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'hermes/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills/gsd', prefix: 'gsd-' },
|
||||
],
|
||||
'hermes/local': [
|
||||
{ kind: 'skills', destSubpath: 'skills/gsd', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── codebuddy ────────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'codebuddy/global': [
|
||||
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'codebuddy/local': [
|
||||
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── cline ────────────────────────────────────────────────────────────────────
|
||||
// Old switch: scope='global' → [skills]; scope='local' → []. Matches descriptor.
|
||||
'cline/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'cline/local': [],
|
||||
|
||||
// ── kimi ─────────────────────────────────────────────────────────────────────
|
||||
// Old switch: scope='global' → [skills, kimi-agents]; scope='local' → []. Matches descriptor.
|
||||
'kimi/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
{ kind: 'kimi-agents', destSubpath: 'agents', prefix: 'gsd' },
|
||||
],
|
||||
'kimi/local': [],
|
||||
|
||||
// ── opencode ─────────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'opencode/global': [
|
||||
{ kind: 'commands', destSubpath: 'command', prefix: 'gsd-' },
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'opencode/local': [
|
||||
{ kind: 'commands', destSubpath: 'command', prefix: 'gsd-' },
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
// ── kilo ─────────────────────────────────────────────────────────────────────
|
||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
||||
'kilo/global': [
|
||||
{ kind: 'commands', destSubpath: 'command', prefix: 'gsd-' },
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
'kilo/local': [
|
||||
{ kind: 'commands', destSubpath: 'command', prefix: 'gsd-' },
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
],
|
||||
};
|
||||
|
||||
// ── Parametrized tests ────────────────────────────────────────────────────────
|
||||
|
||||
const RUNTIMES = [
|
||||
'claude', 'cursor', 'gemini', 'codex', 'copilot',
|
||||
'antigravity', 'windsurf', 'augment', 'trae', 'qwen',
|
||||
'hermes', 'codebuddy', 'cline', 'kimi', 'opencode', 'kilo',
|
||||
];
|
||||
|
||||
for (const runtime of RUNTIMES) {
|
||||
for (const scope of ['global', 'local']) {
|
||||
const key = `${runtime}/${scope}`;
|
||||
const expected = GOLDEN[key];
|
||||
assert.ok(
|
||||
expected !== undefined,
|
||||
`GOLDEN missing entry for ${key} — update the golden table`,
|
||||
);
|
||||
|
||||
describe(`resolveRuntimeArtifactLayout — ${runtime} ${scope} (descriptor-driven)`, () => {
|
||||
test(`kinds array matches STEP-0 golden for ${runtime}/${scope}`, () => {
|
||||
const layout = resolveRuntimeArtifactLayout(runtime, FAKE_DIR, /** @type {'local'|'global'} */ (scope));
|
||||
|
||||
// Structural fields
|
||||
assert.strictEqual(layout.runtime, runtime, 'layout.runtime');
|
||||
assert.strictEqual(layout.configDir, FAKE_DIR, 'layout.configDir');
|
||||
assert.strictEqual(layout.scope, scope, 'layout.scope');
|
||||
|
||||
// kinds length matches golden
|
||||
assert.strictEqual(
|
||||
layout.kinds.length,
|
||||
expected.length,
|
||||
`kinds.length for ${key}: expected ${expected.length}, got ${layout.kinds.length}`,
|
||||
);
|
||||
|
||||
// Per-kind field checks
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
const actual = layout.kinds[i];
|
||||
const exp = expected[i];
|
||||
|
||||
assert.strictEqual(
|
||||
actual.kind,
|
||||
exp.kind,
|
||||
`kinds[${i}].kind for ${key}`,
|
||||
);
|
||||
assert.strictEqual(
|
||||
actual.destSubpath,
|
||||
exp.destSubpath,
|
||||
`kinds[${i}].destSubpath for ${key}`,
|
||||
);
|
||||
assert.strictEqual(
|
||||
actual.prefix,
|
||||
exp.prefix,
|
||||
`kinds[${i}].prefix for ${key}`,
|
||||
);
|
||||
assert.strictEqual(
|
||||
typeof actual.stage,
|
||||
'function',
|
||||
`kinds[${i}].stage must be a function for ${key}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Unknown runtime ───────────────────────────────────────────────────────────
|
||||
// ALLOWED_RUNTIMES guard fires BEFORE descriptor lookup — reproduces old behaviour.
|
||||
|
||||
describe('resolveRuntimeArtifactLayout — unknown runtime (descriptor-driven)', () => {
|
||||
test('throws TypeError for grok (not in ALLOWED_RUNTIMES)', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('grok', FAKE_DIR, 'global'),
|
||||
(err) => {
|
||||
assert.ok(err instanceof TypeError, 'must be TypeError');
|
||||
assert.ok(
|
||||
err.message.includes("Unknown runtime: 'grok'"),
|
||||
`message must include "Unknown runtime: 'grok'" — got: ${err.message}`,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('throws TypeError for an arbitrary unknown string', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('notaruntime', FAKE_DIR, 'global'),
|
||||
(err) => {
|
||||
assert.ok(err instanceof TypeError);
|
||||
assert.ok(err.message.includes("Unknown runtime: 'notaruntime'"));
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Scope default ─────────────────────────────────────────────────────────────
|
||||
// resolveRuntimeArtifactLayout(runtime, configDir) with no scope arg → 'global'.
|
||||
|
||||
describe('resolveRuntimeArtifactLayout — scope defaults to global (descriptor-driven)', () => {
|
||||
test('omitting scope yields global layout for claude', () => {
|
||||
const withDefault = resolveRuntimeArtifactLayout('claude', FAKE_DIR);
|
||||
const withExplicit = resolveRuntimeArtifactLayout('claude', FAKE_DIR, 'global');
|
||||
assert.strictEqual(withDefault.scope, 'global', 'default scope must be "global"');
|
||||
assert.strictEqual(withDefault.kinds.length, withExplicit.kinds.length);
|
||||
for (let i = 0; i < withDefault.kinds.length; i++) {
|
||||
assert.strictEqual(withDefault.kinds[i].kind, withExplicit.kinds[i].kind);
|
||||
assert.strictEqual(withDefault.kinds[i].destSubpath, withExplicit.kinds[i].destSubpath);
|
||||
assert.strictEqual(withDefault.kinds[i].prefix, withExplicit.kinds[i].prefix);
|
||||
}
|
||||
});
|
||||
|
||||
test('omitting scope yields global layout for kimi (2 kinds)', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR);
|
||||
assert.strictEqual(layout.scope, 'global');
|
||||
assert.strictEqual(layout.kinds.length, 2);
|
||||
assert.strictEqual(layout.kinds[0].kind, 'skills');
|
||||
assert.strictEqual(layout.kinds[1].kind, 'kimi-agents');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Non-vacuous check: verify at least one multi-kind runtime ─────────────────
|
||||
|
||||
describe('resolveRuntimeArtifactLayout — multi-kind runtimes non-vacuous (descriptor-driven)', () => {
|
||||
test('augment global returns 2 kinds (commands + skills)', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('augment', FAKE_DIR, 'global');
|
||||
assert.strictEqual(layout.kinds.length, 2);
|
||||
assert.strictEqual(layout.kinds[0].kind, 'commands');
|
||||
assert.strictEqual(layout.kinds[1].kind, 'skills');
|
||||
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
|
||||
assert.strictEqual(typeof layout.kinds[1].stage, 'function');
|
||||
});
|
||||
|
||||
test('kimi global returns skills then kimi-agents', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR, 'global');
|
||||
assert.strictEqual(layout.kinds.length, 2);
|
||||
assert.strictEqual(layout.kinds[0].kind, 'skills');
|
||||
assert.strictEqual(layout.kinds[1].kind, 'kimi-agents');
|
||||
assert.strictEqual(layout.kinds[1].destSubpath, 'agents');
|
||||
assert.strictEqual(layout.kinds[1].prefix, 'gsd');
|
||||
});
|
||||
|
||||
test('codebuddy global returns commands then skills', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('codebuddy', FAKE_DIR, 'global');
|
||||
assert.strictEqual(layout.kinds.length, 2);
|
||||
assert.strictEqual(layout.kinds[0].kind, 'commands');
|
||||
assert.strictEqual(layout.kinds[1].kind, 'skills');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user