fix(#1615): install Windsurf slash workflows
This commit is contained in:
5
.changeset/jolly-hawks-run.md
Normal file
5
.changeset/jolly-hawks-run.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
type: Fixed
|
||||||
|
pr: 0
|
||||||
|
---
|
||||||
|
**Windsurf installs expose /gsd-* commands in Cascade again** — Windsurf runtime installs now emit workflow files under .windsurf/workflows instead of dead skills-only artifacts. (#1615)
|
||||||
@@ -2457,20 +2457,18 @@ function convertClaudeToWindsurfMarkdown(content) {
|
|||||||
// Replace subagent_type from Claude to Windsurf format
|
// Replace subagent_type from Claude to Windsurf format
|
||||||
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
||||||
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
||||||
// Replace project-level Claude conventions with Windsurf/Devin equivalents
|
// Replace project-level Claude conventions with Windsurf equivalents.
|
||||||
// Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085).
|
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
|
||||||
// Legacy .windsurf/ is still recognized on read but new installs use .devin/.
|
converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
|
||||||
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.devin/rules`');
|
converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
|
||||||
converted = converted.replace(/\.\/CLAUDE\.md/g, '.devin/rules');
|
converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
|
||||||
converted = converted.replace(/`CLAUDE\.md`/g, '`.devin/rules`');
|
converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
|
||||||
converted = converted.replace(/\bCLAUDE\.md\b/g, '.devin/rules');
|
converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
|
||||||
converted = converted.replace(/\.claude\/skills\//g, '.devin/skills/');
|
converted = converted.replace(/\.claude\//g, '.windsurf/');
|
||||||
converted = converted.replace(/\.\/\.claude\//g, './.devin/');
|
|
||||||
converted = converted.replace(/\.claude\//g, '.devin/');
|
|
||||||
// Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
|
// Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
|
||||||
// Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
|
// Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
|
||||||
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.devin');
|
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
|
||||||
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.devin');
|
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
|
||||||
// Environment variable name rewrite
|
// Environment variable name rewrite
|
||||||
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
|
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
|
||||||
// Remove Claude Code-specific bug workarounds before brand replacement
|
// Remove Claude Code-specific bug workarounds before brand replacement
|
||||||
@@ -2524,6 +2522,19 @@ function convertClaudeCommandToWindsurfSkill(content, skillName) {
|
|||||||
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
|
||||||
|
const converted = convertClaudeToWindsurfMarkdown(content);
|
||||||
|
const { frontmatter } = extractFrontmatterAndBody(converted);
|
||||||
|
const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
|
||||||
|
const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName;
|
||||||
|
const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
|
||||||
|
const byteLength = Buffer.byteLength(workflow, 'utf8');
|
||||||
|
if (byteLength > 12000) {
|
||||||
|
throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`);
|
||||||
|
}
|
||||||
|
return workflow;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert Claude Code agent markdown to Windsurf agent format.
|
* Convert Claude Code agent markdown to Windsurf agent format.
|
||||||
* Strips frontmatter fields Windsurf doesn't support (color, skills),
|
* Strips frontmatter fields Windsurf doesn't support (color, skills),
|
||||||
@@ -9638,6 +9649,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|||||||
} else {
|
} else {
|
||||||
failures.push('agents/gsd.yaml');
|
failures.push('agents/gsd.yaml');
|
||||||
}
|
}
|
||||||
|
} else if (isWindsurf) {
|
||||||
|
if (isGlobal) {
|
||||||
|
console.log(` ${green}✓${reset} Windsurf global install skipped workflow artifacts (workspace-only)`);
|
||||||
|
} else {
|
||||||
|
const workflowsDir = path.join(targetDir, 'workflows');
|
||||||
|
if (fs.existsSync(workflowsDir)) {
|
||||||
|
const workflowCount = fs.readdirSync(workflowsDir)
|
||||||
|
.filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length;
|
||||||
|
if (workflowCount > 0) {
|
||||||
|
console.log(` ${green}✓${reset} Installed ${workflowCount} workflows to workflows/`);
|
||||||
|
} else {
|
||||||
|
failures.push('workflows/gsd-*');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failures.push('workflows/gsd-*');
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const skillsDir = path.join(targetDir, 'skills');
|
const skillsDir = path.join(targetDir, 'skills');
|
||||||
if (fs.existsSync(skillsDir)) {
|
if (fs.existsSync(skillsDir)) {
|
||||||
@@ -11980,6 +12008,7 @@ module.exports = {
|
|||||||
skillFrontmatterName,
|
skillFrontmatterName,
|
||||||
convertClaudeToWindsurfMarkdown,
|
convertClaudeToWindsurfMarkdown,
|
||||||
convertClaudeCommandToWindsurfSkill,
|
convertClaudeCommandToWindsurfSkill,
|
||||||
|
convertClaudeCommandToWindsurfWorkflow,
|
||||||
convertClaudeAgentToWindsurfAgent,
|
convertClaudeAgentToWindsurfAgent,
|
||||||
convertClaudeToAugmentMarkdown,
|
convertClaudeToAugmentMarkdown,
|
||||||
convertClaudeCommandToAugmentSkill,
|
convertClaudeCommandToAugmentSkill,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"role": "runtime",
|
"role": "runtime",
|
||||||
"version": "1.6.0-rc.2",
|
"version": "1.6.0-rc.2",
|
||||||
"title": "Windsurf",
|
"title": "Windsurf",
|
||||||
"description": "Windsurf (Codeium) — nested under ~/.codeium/windsurf; skills-only artifact layout; no hook surface; no hook events; tier-2 support.",
|
"description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.",
|
||||||
"tier": "core",
|
"tier": "core",
|
||||||
"requires": [],
|
"requires": [],
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -20,24 +20,15 @@
|
|||||||
},
|
},
|
||||||
"configFormat": "none",
|
"configFormat": "none",
|
||||||
"artifactLayout": {
|
"artifactLayout": {
|
||||||
"global": [
|
"global": [],
|
||||||
{
|
|
||||||
"kind": "skills",
|
|
||||||
"destSubpath": "skills",
|
|
||||||
"prefix": "gsd-",
|
|
||||||
"nesting": "flat",
|
|
||||||
"recursive": false,
|
|
||||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"local": [
|
"local": [
|
||||||
{
|
{
|
||||||
"kind": "skills",
|
"kind": "commands",
|
||||||
"destSubpath": "skills",
|
"destSubpath": "workflows",
|
||||||
"prefix": "gsd-",
|
"prefix": "gsd-",
|
||||||
"nesting": "flat",
|
"nesting": "flat",
|
||||||
"recursive": false,
|
"recursive": false,
|
||||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
"converter": "convertClaudeCommandToWindsurfWorkflow"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -598,7 +598,7 @@ Equivalent paths for other runtimes:
|
|||||||
- **Copilot:** `~/.copilot/` global or `./.github/` local
|
- **Copilot:** `~/.copilot/` global or `./.github/` local
|
||||||
- **Antigravity:** auto-detected global root (`~/.gemini/antigravity/`, `~/.gemini/antigravity-ide/`, or `~/.gemini/antigravity-cli/`) or `./.agent/` local
|
- **Antigravity:** auto-detected global root (`~/.gemini/antigravity/`, `~/.gemini/antigravity-ide/`, or `~/.gemini/antigravity-cli/`) or `./.agent/` local
|
||||||
- **Cursor:** `~/.cursor/` global or `./.cursor/` local
|
- **Cursor:** `~/.cursor/` global or `./.cursor/` local
|
||||||
- **Windsurf/Devin Desktop:** `~/.codeium/windsurf/` global or `./.devin/` local (canonical, #1085); `./.windsurf/` local is still recognized as legacy
|
- **Windsurf/Devin Desktop:** `~/.codeium/windsurf/` global config or `./.windsurf/` local workflows
|
||||||
- **Augment Code:** `~/.augment/` global or `./.augment/` local
|
- **Augment Code:** `~/.augment/` global or `./.augment/` local
|
||||||
- **Trae:** `~/.trae/` global or `./.trae/` local
|
- **Trae:** `~/.trae/` global or `./.trae/` local
|
||||||
- **Qwen Code:** `~/.qwen/` global or `./.qwen/` local
|
- **Qwen Code:** `~/.qwen/` global or `./.qwen/` local
|
||||||
@@ -842,7 +842,7 @@ The migration-specific ownership and source snapshots live in
|
|||||||
| GitHub Copilot | `~/.copilot` | `./.github` | `skills/gsd-*/SKILL.md` (flat), `copilot-instructions.md`, and `AGENTS.md` (repo root, local) | `.agent.md` files | Self-contained `sessionStart` hook (`hooks/gsd-session.json`, inline `command` type); no statusline |
|
| GitHub Copilot | `~/.copilot` | `./.github` | `skills/gsd-*/SKILL.md` (flat), `copilot-instructions.md`, and `AGENTS.md` (repo root, local) | `.agent.md` files | Self-contained `sessionStart` hook (`hooks/gsd-session.json`, inline `command` type); no statusline |
|
||||||
| Antigravity | auto-detected: `~/.gemini/antigravity`, `~/.gemini/antigravity-ide`, or `~/.gemini/antigravity-cli` | `./.agent` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | Gemini-style `settings.json` hook entries when installed by GSD |
|
| Antigravity | auto-detected: `~/.gemini/antigravity`, `~/.gemini/antigravity-ide`, or `~/.gemini/antigravity-cli` | `./.agent` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | Gemini-style `settings.json` hook entries when installed by GSD |
|
||||||
| Cursor | `~/.cursor` | `./.cursor` | `skills/gsd-*/SKILL.md` (flat) | `agents/gsd-*.md` | Rule references under `rules/`; `hooks.json` with sessionStart context injection and postToolUse STATE.md monitor (#777) |
|
| Cursor | `~/.cursor` | `./.cursor` | `skills/gsd-*/SKILL.md` (flat) | `agents/gsd-*.md` | Rule references under `rules/`; `hooks.json` with sessionStart context injection and postToolUse STATE.md monitor (#777) |
|
||||||
| Windsurf | `~/.codeium/windsurf` | `./.devin` (canonical, #1085); `./.windsurf` legacy recognized | `skills/gsd-*/SKILL.md` (flat) | `agents/gsd-*.md` | Rule references under `rules/`; no GSD hooks |
|
| Windsurf | `~/.codeium/windsurf` config | `./.windsurf` | `workflows/gsd-*.md` slash-command workflows | No custom-agent artifact surface | No GSD hooks |
|
||||||
| Augment Code | `~/.augment` | `./.augment` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | No GSD hooks or statusline |
|
| Augment Code | `~/.augment` | `./.augment` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | No GSD hooks or statusline |
|
||||||
| Trae | `~/.trae` | `./.trae` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | Rule references under `rules/`; no GSD hooks |
|
| Trae | `~/.trae` | `./.trae` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | Rule references under `rules/`; no GSD hooks |
|
||||||
| Qwen Code | `~/.qwen` | `./.qwen` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | Common GSD settings and hook entries where supported |
|
| Qwen Code | `~/.qwen` | `./.qwen` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | Common GSD settings and hook entries where supported |
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ npx @opengsd/gsd-core@latest --windsurf --global
|
|||||||
npx @opengsd/gsd-core@latest --devin-desktop --global
|
npx @opengsd/gsd-core@latest --devin-desktop --global
|
||||||
```
|
```
|
||||||
|
|
||||||
Global skills land in `~/.codeium/windsurf/` (unchanged). Local workspace installs write to `.devin/skills/` (Devin Desktop's preferred location, #1085); the legacy `.windsurf/skills/` layout is still recognized for backward-compat. GSD installs skills, agents, and workspace rules.
|
Use a workspace install for Windsurf slash commands. Workspace installs write `/gsd-*` commands as Windsurf workflow files under `.windsurf/workflows/`. Windsurf discovers those `.md` workflow files in Cascade and exposes them through the `/` menu. Global-scope Windsurf workflow installation is intentionally a no-op for now because global workflow locations are outside GSD's normal user-owned runtime config directory.
|
||||||
|
|
||||||
**Override the install directory:**
|
**Override the install directory:**
|
||||||
|
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ for the new shape before changing migration behavior.
|
|||||||
| GitHub Copilot | Skills in `skills/gsd-*/SKILL.md`; agents as `.agent.md`; repository instructions in `copilot-instructions.md` | Global `COPILOT_CONFIG_DIR`, `COPILOT_HOME`, or `~/.copilot`; local `./.github` | GSD owns generated skill/agent files and GSD-authored instruction files; no hook/statusline ownership | [Repository custom instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions), [Copilot CLI custom instructions](https://docs.github.com/en/copilot/how-tos/copilot-cli/add-custom-instructions); GitHub Docs product docs, checked 2026-05-11 |
|
| GitHub Copilot | Skills in `skills/gsd-*/SKILL.md`; agents as `.agent.md`; repository instructions in `copilot-instructions.md` | Global `COPILOT_CONFIG_DIR`, `COPILOT_HOME`, or `~/.copilot`; local `./.github` | GSD owns generated skill/agent files and GSD-authored instruction files; no hook/statusline ownership | [Repository custom instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions), [Copilot CLI custom instructions](https://docs.github.com/en/copilot/how-tos/copilot-cli/add-custom-instructions); GitHub Docs product docs, checked 2026-05-11 |
|
||||||
| Antigravity | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; Gemini-style `settings.json` hooks when installed by GSD | Global `ANTIGRAVITY_CONFIG_DIR` or `~/.gemini/antigravity`; local `./.agents` (canonical, #791) or `./.agent` (legacy, recognized for backward-compat) | GSD owns generated skills/agents/hooks and GSD settings entries only | Public Antigravity install/config docs for this file layout were not stable or complete as of 2026-05-11; installer compatibility therefore uses GSD's Gemini-compatible settings policy, documented shim baseline. Fresh installs write to `.agents/` (the Google-Codelabs-documented form); existing `.agent/` installs continue to be detected and served. |
|
| Antigravity | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; Gemini-style `settings.json` hooks when installed by GSD | Global `ANTIGRAVITY_CONFIG_DIR` or `~/.gemini/antigravity`; local `./.agents` (canonical, #791) or `./.agent` (legacy, recognized for backward-compat) | GSD owns generated skills/agents/hooks and GSD settings entries only | Public Antigravity install/config docs for this file layout were not stable or complete as of 2026-05-11; installer compatibility therefore uses GSD's Gemini-compatible settings policy, documented shim baseline. Fresh installs write to `.agents/` (the Google-Codelabs-documented form); existing `.agent/` installs continue to be detected and served. |
|
||||||
| Cursor | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/`; lifecycle hooks via `hooks.json` (sessionStart + postToolUse, #777) | Global `CURSOR_CONFIG_DIR` or `~/.cursor`; local `./.cursor` | GSD owns generated skills/agents, GSD rule files or references, and GSD-managed `hooks.json` entries (sentinel `gsd-managed:true`); no statusline ownership | [Cursor rules](https://docs.cursor.com/context/rules); [Cursor hooks](https://docs.cursor.com/context/hooks); docs not versioned, checked 2026-06-07 |
|
| Cursor | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/`; lifecycle hooks via `hooks.json` (sessionStart + postToolUse, #777) | Global `CURSOR_CONFIG_DIR` or `~/.cursor`; local `./.cursor` | GSD owns generated skills/agents, GSD rule files or references, and GSD-managed `hooks.json` entries (sentinel `gsd-managed:true`); no statusline ownership | [Cursor rules](https://docs.cursor.com/context/rules); [Cursor hooks](https://docs.cursor.com/context/hooks); docs not versioned, checked 2026-06-07 |
|
||||||
| Windsurf / Devin Desktop | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/` | Global `WINDSURF_CONFIG_DIR` or `~/.codeium/windsurf`; local `./.devin` (canonical, #1085) or `./.windsurf` (legacy, recognized for backward-compat) | GSD owns generated skills/agents and GSD rule files or references; no hook/statusline ownership | Windsurf has rebranded to Devin Desktop; workspace skills install to `.devin/` per Devin Desktop documented preferred location (#1085). Global `~/.codeium/windsurf/` is unchanged. Windsurf public rule docs were source-limited in search results as of 2026-05-11; installer targets the common workspace rules convention `./.devin/rules` and must be rechecked before migrations rewrite rules |
|
| Windsurf / Devin Desktop | Local slash-command workflows in `workflows/gsd-*.md`; no custom-agent artifact surface | Local workflow directory `./.windsurf/workflows`; global workflow install is intentionally a no-op | GSD owns generated local workflow files only; no hook/statusline ownership | Windsurf workflows are the documented `/` command surface. Workspace workflows live under `.windsurf/workflows/*.md`; global workflow locations are outside GSD's normal user-owned runtime config directory and are not written by the GSD installer. |
|
||||||
| Augment Code | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/` | Global `AUGMENT_CONFIG_DIR` or `~/.augment`; local `./.augment` | GSD owns generated skills/agents only; no hook/statusline ownership | [Augment Agent Skills](https://docs.augmentcode.com/cli/skills), [Augment IDE skills](https://docs.augmentcode.com/using-augment/skills); IDE skills public beta in VS Code 0.789.0+, checked 2026-05-11 |
|
| Augment Code | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/` | Global `AUGMENT_CONFIG_DIR` or `~/.augment`; local `./.augment` | GSD owns generated skills/agents only; no hook/statusline ownership | [Augment Agent Skills](https://docs.augmentcode.com/cli/skills), [Augment IDE skills](https://docs.augmentcode.com/using-augment/skills); IDE skills public beta in VS Code 0.789.0+, checked 2026-05-11 |
|
||||||
| Trae | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/` | Global `TRAE_CONFIG_DIR` or `~/.trae`; local `./.trae` | GSD owns generated skills/agents and GSD rule files or references; no hook/statusline ownership | Public Trae docs expose AI settings and `.rules` announcements, but no stable skills/config API was found as of 2026-05-11; migrations must treat this row as source-limited |
|
| Trae | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/` | Global `TRAE_CONFIG_DIR` or `~/.trae`; local `./.trae` | GSD owns generated skills/agents and GSD rule files or references; no hook/statusline ownership | Public Trae docs expose AI settings and `.rules` announcements, but no stable skills/config API was found as of 2026-05-11; migrations must treat this row as source-limited |
|
||||||
| Qwen Code | Claude-compatible skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; optional common hook/settings integration through GSD | Global `QWEN_CONFIG_DIR` or `~/.qwen`; local `./.qwen` | GSD owns generated skills/agents/hooks and GSD settings entries only | [Qwen commands and skills](https://qwenlm.github.io/qwen-code-docs/en/users/features/commands/); docs last updated 2026-05-06 |
|
| Qwen Code | Claude-compatible skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; optional common hook/settings integration through GSD | Global `QWEN_CONFIG_DIR` or `~/.qwen`; local `./.qwen` | GSD owns generated skills/agents/hooks and GSD settings entries only | [Qwen commands and skills](https://qwenlm.github.io/qwen-code-docs/en/users/features/commands/); docs last updated 2026-05-06 |
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ For the transform each converter applies, see [ADR-1593 §3 — converter transf
|
|||||||
| **cursor** | `skills/` | `gsd-` | flat | recursive | `convertClaudeCommandToCursorSkill` | Also ships flat `commands/` via `convertClaudeCommandToCursorCommand`. `configFormat: none`. |
|
| **cursor** | `skills/` | `gsd-` | flat | recursive | `convertClaudeCommandToCursorSkill` | Also ships flat `commands/` via `convertClaudeCommandToCursorCommand`. `configFormat: none`. |
|
||||||
| **copilot** | `skills/` | `gsd-` | flat | unconfirmed → conservative | `convertClaudeCommandToCopilotSkill` | Markdown config. Scope-aware converter (global-home vs workspace-relative). |
|
| **copilot** | `skills/` | `gsd-` | flat | unconfirmed → conservative | `convertClaudeCommandToCopilotSkill` | Markdown config. Scope-aware converter (global-home vs workspace-relative). |
|
||||||
| **antigravity** | `skills/` | `gsd-` | nested | non-recursive (one-level) | `convertClaudeCommandToAntigravitySkill` | `dot-home-nested` config home. Scope-aware converter. Nesting confirmed: *"will not recursive scan"*. |
|
| **antigravity** | `skills/` | `gsd-` | nested | non-recursive (one-level) | `convertClaudeCommandToAntigravitySkill` | `dot-home-nested` config home. Scope-aware converter. Nesting confirmed: *"will not recursive scan"*. |
|
||||||
| **windsurf** | `skills/` | `gsd-` | flat | unconfirmed → conservative | `convertClaudeCommandToWindsurfSkill` | `configFormat: none`. `installSurface: profile-marker-only`. |
|
| **windsurf** | — *(no skills kind)* | `gsd-` | — | workflows | `convertClaudeCommandToWindsurfWorkflow` | Emits `.windsurf/workflows/gsd-*.md` slash-command workflows. `configFormat: none`. `installSurface: profile-marker-only`. |
|
||||||
| **augment** | `skills/` | `gsd-` | nested | non-recursive (single-level) | `convertClaudeCommandToAugmentSkill` | Also ships flat `commands/`. Settings-json config. |
|
| **augment** | `skills/` | `gsd-` | nested | non-recursive (single-level) | `convertClaudeCommandToAugmentSkill` | Also ships flat `commands/`. Settings-json config. |
|
||||||
| **trae** | `skills/` | `gsd-` | nested | non-recursive (flat; nesting errors) | `convertClaudeCommandToTraeSkill` | `configFormat: none`. Trae IDE (trae.ai), not trae-agent. |
|
| **trae** | `skills/` | `gsd-` | nested | non-recursive (flat; nesting errors) | `convertClaudeCommandToTraeSkill` | `configFormat: none`. Trae IDE (trae.ai), not trae-agent. |
|
||||||
| **qwen** | `skills/` | `gsd-` | nested | non-recursive (flat readdir) | `convertClaudeCommandToClaudeSkill` | **Shares Claude's converter.** Emits numeric `priority:` (`QWEN_SKILL_PRIORITY`) for `/skills` ordering. Settings-json config. |
|
| **qwen** | `skills/` | `gsd-` | nested | non-recursive (flat readdir) | `convertClaudeCommandToClaudeSkill` | **Shares Claude's converter.** Emits numeric `priority:` (`QWEN_SKILL_PRIORITY`) for `/skills` ordering. Settings-json config. |
|
||||||
|
|||||||
@@ -1977,7 +1977,7 @@ const capabilities = {
|
|||||||
"role": "runtime",
|
"role": "runtime",
|
||||||
"version": "1.6.0-rc.2",
|
"version": "1.6.0-rc.2",
|
||||||
"title": "Windsurf",
|
"title": "Windsurf",
|
||||||
"description": "Windsurf (Codeium) — nested under ~/.codeium/windsurf; skills-only artifact layout; no hook surface; no hook events; tier-2 support.",
|
"description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.",
|
||||||
"tier": "core",
|
"tier": "core",
|
||||||
"requires": [],
|
"requires": [],
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1994,24 +1994,15 @@ const capabilities = {
|
|||||||
},
|
},
|
||||||
"configFormat": "none",
|
"configFormat": "none",
|
||||||
"artifactLayout": {
|
"artifactLayout": {
|
||||||
"global": [
|
"global": [],
|
||||||
{
|
|
||||||
"kind": "skills",
|
|
||||||
"destSubpath": "skills",
|
|
||||||
"prefix": "gsd-",
|
|
||||||
"nesting": "flat",
|
|
||||||
"recursive": false,
|
|
||||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"local": [
|
"local": [
|
||||||
{
|
{
|
||||||
"kind": "skills",
|
"kind": "commands",
|
||||||
"destSubpath": "skills",
|
"destSubpath": "workflows",
|
||||||
"prefix": "gsd-",
|
"prefix": "gsd-",
|
||||||
"nesting": "flat",
|
"nesting": "flat",
|
||||||
"recursive": false,
|
"recursive": false,
|
||||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
"converter": "convertClaudeCommandToWindsurfWorkflow"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -3656,7 +3647,7 @@ const runtimes = {
|
|||||||
"role": "runtime",
|
"role": "runtime",
|
||||||
"version": "1.6.0-rc.2",
|
"version": "1.6.0-rc.2",
|
||||||
"title": "Windsurf",
|
"title": "Windsurf",
|
||||||
"description": "Windsurf (Codeium) — nested under ~/.codeium/windsurf; skills-only artifact layout; no hook surface; no hook events; tier-2 support.",
|
"description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.",
|
||||||
"tier": "core",
|
"tier": "core",
|
||||||
"requires": [],
|
"requires": [],
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3673,24 +3664,15 @@ const runtimes = {
|
|||||||
},
|
},
|
||||||
"configFormat": "none",
|
"configFormat": "none",
|
||||||
"artifactLayout": {
|
"artifactLayout": {
|
||||||
"global": [
|
"global": [],
|
||||||
{
|
|
||||||
"kind": "skills",
|
|
||||||
"destSubpath": "skills",
|
|
||||||
"prefix": "gsd-",
|
|
||||||
"nesting": "flat",
|
|
||||||
"recursive": false,
|
|
||||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"local": [
|
"local": [
|
||||||
{
|
{
|
||||||
"kind": "skills",
|
"kind": "commands",
|
||||||
"destSubpath": "skills",
|
"destSubpath": "workflows",
|
||||||
"prefix": "gsd-",
|
"prefix": "gsd-",
|
||||||
"nesting": "flat",
|
"nesting": "flat",
|
||||||
"recursive": false,
|
"recursive": false,
|
||||||
"converter": "convertClaudeCommandToWindsurfSkill"
|
"converter": "convertClaudeCommandToWindsurfWorkflow"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -665,6 +665,7 @@ const VALID_CONVERTER_NAMES = new Set([
|
|||||||
'convertClaudeCommandToOpencodeSkill',
|
'convertClaudeCommandToOpencodeSkill',
|
||||||
'convertClaudeCommandToTraeSkill',
|
'convertClaudeCommandToTraeSkill',
|
||||||
'convertClaudeCommandToWindsurfSkill',
|
'convertClaudeCommandToWindsurfSkill',
|
||||||
|
'convertClaudeCommandToWindsurfWorkflow',
|
||||||
// agent converters (#1173 — descriptor-driven agent conversion wiring)
|
// agent converters (#1173 — descriptor-driven agent conversion wiring)
|
||||||
'convertClaudeAgentToCopilotAgent',
|
'convertClaudeAgentToCopilotAgent',
|
||||||
'convertClaudeAgentToAntigravityAgent',
|
'convertClaudeAgentToAntigravityAgent',
|
||||||
|
|||||||
@@ -941,20 +941,18 @@ function convertClaudeToWindsurfMarkdown(content) {
|
|||||||
// Replace subagent_type from Claude to Windsurf format
|
// Replace subagent_type from Claude to Windsurf format
|
||||||
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
||||||
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
||||||
// Replace project-level Claude conventions with Windsurf/Devin equivalents
|
// Replace project-level Claude conventions with Windsurf equivalents.
|
||||||
// Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085).
|
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
|
||||||
// Legacy .windsurf/ is still recognized on read but new installs use .devin/.
|
converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
|
||||||
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.devin/rules`');
|
converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
|
||||||
converted = converted.replace(/\.\/CLAUDE\.md/g, '.devin/rules');
|
converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
|
||||||
converted = converted.replace(/`CLAUDE\.md`/g, '`.devin/rules`');
|
converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
|
||||||
converted = converted.replace(/\bCLAUDE\.md\b/g, '.devin/rules');
|
converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
|
||||||
converted = converted.replace(/\.claude\/skills\//g, '.devin/skills/');
|
converted = converted.replace(/\.claude\//g, '.windsurf/');
|
||||||
converted = converted.replace(/\.\/\.claude\//g, './.devin/');
|
|
||||||
converted = converted.replace(/\.claude\//g, '.devin/');
|
|
||||||
// Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
|
// Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
|
||||||
// Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
|
// Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
|
||||||
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.devin');
|
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
|
||||||
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.devin');
|
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
|
||||||
// Environment variable name rewrite
|
// Environment variable name rewrite
|
||||||
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
|
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
|
||||||
// Remove Claude Code-specific bug workarounds before brand replacement
|
// Remove Claude Code-specific bug workarounds before brand replacement
|
||||||
@@ -1008,6 +1006,19 @@ function convertClaudeCommandToWindsurfSkill(content, skillName) {
|
|||||||
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
|
||||||
|
const converted = convertClaudeToWindsurfMarkdown(content);
|
||||||
|
const { frontmatter } = extractFrontmatterAndBody(converted);
|
||||||
|
const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
|
||||||
|
const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName;
|
||||||
|
const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
|
||||||
|
const byteLength = Buffer.byteLength(workflow, 'utf8');
|
||||||
|
if (byteLength > 12000) {
|
||||||
|
throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`);
|
||||||
|
}
|
||||||
|
return workflow;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Augment converters ---
|
// --- Augment converters ---
|
||||||
// Augment uses a tool set similar to Cursor/Windsurf.
|
// Augment uses a tool set similar to Cursor/Windsurf.
|
||||||
// Config lives in .augment/ (local) and ~/.augment/ (global).
|
// Config lives in .augment/ (local) and ~/.augment/ (global).
|
||||||
@@ -2568,6 +2579,7 @@ export = {
|
|||||||
convertClaudeCommandToCursorCommand,
|
convertClaudeCommandToCursorCommand,
|
||||||
convertClaudeToWindsurfMarkdown,
|
convertClaudeToWindsurfMarkdown,
|
||||||
convertClaudeCommandToWindsurfSkill,
|
convertClaudeCommandToWindsurfSkill,
|
||||||
|
convertClaudeCommandToWindsurfWorkflow,
|
||||||
convertClaudeToAugmentMarkdown,
|
convertClaudeToAugmentMarkdown,
|
||||||
convertClaudeCommandToAugmentSkill,
|
convertClaudeCommandToAugmentSkill,
|
||||||
convertClaudeToTraeMarkdown,
|
convertClaudeToTraeMarkdown,
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export function getProjectInstructionFile(runtime: unknown): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Map a canonical runtime id to its on-disk local config directory name
|
* Map a canonical runtime id to its on-disk local config directory name
|
||||||
* (e.g. `cursor` -> `.cursor`, `windsurf` -> `.devin`). Unknown/empty inputs
|
* (e.g. `cursor` -> `.cursor`, `windsurf` -> `.windsurf`). Unknown/empty inputs
|
||||||
* fall back to `.claude`.
|
* fall back to `.claude`.
|
||||||
*
|
*
|
||||||
* Pure runtime-identity projection. Relocated from `bin/install.js` per
|
* Pure runtime-identity projection. Relocated from `bin/install.js` per
|
||||||
@@ -149,7 +149,7 @@ export function getDirName(runtime: string): string {
|
|||||||
if (runtime === 'codex') return '.codex';
|
if (runtime === 'codex') return '.codex';
|
||||||
if (runtime === 'antigravity') return '.agents';
|
if (runtime === 'antigravity') return '.agents';
|
||||||
if (runtime === 'cursor') return '.cursor';
|
if (runtime === 'cursor') return '.cursor';
|
||||||
if (runtime === 'windsurf') return '.devin';
|
if (runtime === 'windsurf') return '.windsurf';
|
||||||
if (runtime === 'augment') return '.augment';
|
if (runtime === 'augment') return '.augment';
|
||||||
if (runtime === 'trae') return '.trae';
|
if (runtime === 'trae') return '.trae';
|
||||||
if (runtime === 'qwen') return '.qwen';
|
if (runtime === 'qwen') return '.qwen';
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ export const RUNTIME_DIRS: RuntimeDirEntry[] = [
|
|||||||
['antigravity', '.gemini/antigravity'],
|
['antigravity', '.gemini/antigravity'],
|
||||||
['antigravity', '.agents'], // local Antigravity install dir canonical (#791; bin/install.js getDirName('antigravity'))
|
['antigravity', '.agents'], // local Antigravity install dir canonical (#791; bin/install.js getDirName('antigravity'))
|
||||||
['antigravity', '.agent'], // local Antigravity install dir legacy (#503; backward-compat with pre-#791 installs)
|
['antigravity', '.agent'], // local Antigravity install dir legacy (#503; backward-compat with pre-#791 installs)
|
||||||
['windsurf', '.devin'], // local Windsurf/Devin Desktop install dir canonical (#1085; bin/install.js getDirName('windsurf'))
|
['windsurf', '.windsurf'], // local Windsurf workflow dir canonical (#1615; bin/install.js getDirName('windsurf'))
|
||||||
['windsurf', '.windsurf'], // local Windsurf install dir legacy (#1085; backward-compat with pre-#1085 installs)
|
['windsurf', '.devin'], // local Devin Desktop install dir legacy (#1085; backward-compat)
|
||||||
['gemini', '.gemini'],
|
['gemini', '.gemini'],
|
||||||
['kilo', '.config/kilo'],
|
['kilo', '.config/kilo'],
|
||||||
['kilo', '.kilo'],
|
['kilo', '.kilo'],
|
||||||
|
|||||||
@@ -56,13 +56,13 @@ const RUNTIME_META = {
|
|||||||
opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') },
|
opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') },
|
||||||
qwen: { localDir: '.qwen', globalSuffix: '.qwen' },
|
qwen: { localDir: '.qwen', globalSuffix: '.qwen' },
|
||||||
trae: { localDir: '.trae', globalSuffix: '.trae' },
|
trae: { localDir: '.trae', globalSuffix: '.trae' },
|
||||||
windsurf: { localDir: '.devin', globalSuffix: path.join('.codeium', 'windsurf') },
|
windsurf: { localDir: '.windsurf', globalSuffix: path.join('.codeium', 'windsurf') },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Runtimes that emit per-skill files under skills/ (not rules-based or commands-based)
|
// Runtimes that emit per-skill files under skills/ (not rules-based or commands-based)
|
||||||
const SKILL_RUNTIMES = [
|
const SKILL_RUNTIMES = [
|
||||||
'claude', 'opencode', 'gemini', 'kilo', 'codex', 'copilot', 'antigravity',
|
'claude', 'opencode', 'gemini', 'kilo', 'codex', 'copilot', 'antigravity',
|
||||||
'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'codebuddy',
|
'cursor', 'augment', 'trae', 'qwen', 'codebuddy',
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─── Helper functions ─────────────────────────────────────────────────────────
|
// ─── Helper functions ─────────────────────────────────────────────────────────
|
||||||
@@ -114,7 +114,7 @@ function runMinimalInstall({ runtime, scope, extraArgs = [] }) {
|
|||||||
const LOCAL_DIR_NAME = {
|
const LOCAL_DIR_NAME = {
|
||||||
claude: '.claude', opencode: '.opencode', gemini: '.gemini', kilo: '.kilo',
|
claude: '.claude', opencode: '.opencode', gemini: '.gemini', kilo: '.kilo',
|
||||||
codex: '.codex', copilot: '.github', antigravity: '.agents', cursor: '.cursor',
|
codex: '.codex', copilot: '.github', antigravity: '.agents', cursor: '.cursor',
|
||||||
windsurf: '.devin', augment: '.augment', trae: '.trae', qwen: '.qwen',
|
windsurf: '.windsurf', augment: '.augment', trae: '.trae', qwen: '.qwen',
|
||||||
codebuddy: '.codebuddy', cline: '.',
|
codebuddy: '.codebuddy', cline: '.',
|
||||||
};
|
};
|
||||||
let configDir;
|
let configDir;
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ describe('installRuntimeArtifacts — consumes Runtime Artifact Install Plan Mod
|
|||||||
|
|
||||||
const SKILLS_RUNTIMES_LAYOUT = [
|
const SKILLS_RUNTIMES_LAYOUT = [
|
||||||
'claude', 'cursor', 'codex', 'copilot', 'antigravity',
|
'claude', 'cursor', 'codex', 'copilot', 'antigravity',
|
||||||
'windsurf', 'augment', 'trae', 'qwen', 'kimi', 'codebuddy',
|
'augment', 'trae', 'qwen', 'kimi', 'codebuddy',
|
||||||
];
|
];
|
||||||
|
|
||||||
const ALL_RUNTIMES_LAYOUT = [
|
const ALL_RUNTIMES_LAYOUT = [
|
||||||
@@ -298,6 +298,47 @@ describe('installRuntimeArtifacts — cursor commands layout (#785)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('installRuntimeArtifacts — windsurf workflows layout (#1615)', () => {
|
||||||
|
test('windsurf: local install writes workflow slash-command files, not skills', (t) => {
|
||||||
|
const configDir = createTempDir('gsd-ial-windsurf-');
|
||||||
|
t.after(() => cleanup(configDir));
|
||||||
|
|
||||||
|
installRuntimeArtifacts('windsurf', configDir, 'local', RESOLVED_CORE);
|
||||||
|
|
||||||
|
const workflowsDir = path.join(configDir, 'workflows');
|
||||||
|
assert.ok(fs.existsSync(workflowsDir), 'workflows/ must exist for Windsurf local install');
|
||||||
|
assert.ok(fs.existsSync(path.join(workflowsDir, 'gsd-help.md')),
|
||||||
|
'workflows/gsd-help.md must exist for /gsd-help');
|
||||||
|
assert.ok(!fs.existsSync(path.join(configDir, 'skills')),
|
||||||
|
'Windsurf must not install dead skills/ artifacts for slash commands');
|
||||||
|
|
||||||
|
const helpContent = fs.readFileSync(path.join(workflowsDir, 'gsd-help.md'), 'utf8');
|
||||||
|
assert.ok(!helpContent.startsWith('---'), 'Windsurf workflows must be plain markdown, not SKILL.md frontmatter');
|
||||||
|
assert.match(helpContent, /# gsd-help/, 'workflow should identify the slash command it backs');
|
||||||
|
assert.ok(helpContent.includes(`${configDir}/gsd-core/commands/gsd/help.md`),
|
||||||
|
'workflow should reference the installed command body using the actual install target');
|
||||||
|
|
||||||
|
for (const fileName of fs.readdirSync(workflowsDir)) {
|
||||||
|
if (!fileName.endsWith('.md')) continue;
|
||||||
|
const workflowPath = path.join(workflowsDir, fileName);
|
||||||
|
const byteLength = Buffer.byteLength(fs.readFileSync(workflowPath, 'utf8'), 'utf8');
|
||||||
|
assert.ok(byteLength <= 12000, `${fileName} must respect Windsurf's 12,000-character workflow limit`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('windsurf: global install is explicit no-op for workflow artifacts', (t) => {
|
||||||
|
const configDir = createTempDir('gsd-ial-windsurf-global-');
|
||||||
|
t.after(() => cleanup(configDir));
|
||||||
|
|
||||||
|
installRuntimeArtifacts('windsurf', configDir, 'global', RESOLVED_CORE);
|
||||||
|
|
||||||
|
assert.ok(!fs.existsSync(path.join(configDir, 'workflows')),
|
||||||
|
'global Windsurf install must not write workflows under the config root');
|
||||||
|
assert.ok(!fs.existsSync(path.join(configDir, 'skills')),
|
||||||
|
'global Windsurf install must not write dead skills artifacts');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('installRuntimeArtifacts — cline skills (#782)', () => {
|
describe('installRuntimeArtifacts — cline skills (#782)', () => {
|
||||||
test('cline: global install writes gsd-prefixed skill dirs under skills/', (t) => {
|
test('cline: global install writes gsd-prefixed skill dirs under skills/', (t) => {
|
||||||
const configDir = createTempDir('gsd-ial-cline-');
|
const configDir = createTempDir('gsd-ial-cline-');
|
||||||
|
|||||||
@@ -1269,12 +1269,12 @@ describe('install — --devin-desktop CLI flag routes to windsurf runtime (#792)
|
|||||||
assert.deepStrictEqual(selectRuntimesFromArgs(['--devin-desktop']), ['windsurf']);
|
assert.deepStrictEqual(selectRuntimesFromArgs(['--devin-desktop']), ['windsurf']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// ─── Section N: Windsurf .devin canonical workspace dir (#1085) ─────────────
|
// ─── Section N: Windsurf workflow slash-command install (#1615) ─────────────
|
||||||
// allow-test-rule: runtime-contract-is-the-product
|
// allow-test-rule: runtime-contract-is-the-product
|
||||||
// Reads deployed skill .md files whose text IS the product surface the
|
// Reads deployed workflow .md files whose text IS the product surface the
|
||||||
// Windsurf/Devin Desktop runtime loads at startup (path references, command names).
|
// Windsurf runtime loads at startup (path references, command names).
|
||||||
|
|
||||||
describe('windsurf local install writes to .devin/ canonical dir (#1085)', () => {
|
describe('windsurf local install writes workflow slash commands (#1615)', () => {
|
||||||
let tmpDir;
|
let tmpDir;
|
||||||
let previousCwd;
|
let previousCwd;
|
||||||
|
|
||||||
@@ -1289,50 +1289,48 @@ describe('windsurf local install writes to .devin/ canonical dir (#1085)', () =>
|
|||||||
cleanup(tmpDir);
|
cleanup(tmpDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('install writes workspace skills under .devin/skills/', () => {
|
test('install writes workspace workflows under .windsurf/workflows/', () => {
|
||||||
const result = install(false, 'windsurf');
|
const result = install(false, 'windsurf');
|
||||||
const devinDir = path.join(tmpDir, '.devin');
|
const windsurfDir = path.join(tmpDir, '.windsurf');
|
||||||
assert.strictEqual(result.runtime, 'windsurf');
|
assert.strictEqual(result.runtime, 'windsurf');
|
||||||
assert.ok(fs.existsSync(devinDir), '.devin/ must be created for local windsurf install');
|
assert.ok(fs.existsSync(windsurfDir), '.windsurf/ must be created for local windsurf install');
|
||||||
const skillsDir = path.join(devinDir, 'skills');
|
const workflowsDir = path.join(windsurfDir, 'workflows');
|
||||||
assert.ok(fs.existsSync(skillsDir), '.devin/skills/ must exist after install');
|
assert.ok(fs.existsSync(workflowsDir), '.windsurf/workflows/ must exist after install');
|
||||||
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
const workflowEntries = fs.readdirSync(workflowsDir, { withFileTypes: true })
|
||||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
.filter(e => e.isFile() && e.name.startsWith('gsd-') && e.name.endsWith('.md'));
|
||||||
assert.ok(skillEntries.length > 0, 'at least one gsd-* skill must be installed under .devin/skills/');
|
assert.ok(workflowEntries.length > 0, 'at least one gsd-* workflow must be installed under .windsurf/workflows/');
|
||||||
const firstSkill = path.join(skillsDir, skillEntries[0].name, 'SKILL.md');
|
assert.ok(fs.existsSync(path.join(workflowsDir, 'gsd-help.md')), 'gsd-help.md workflow must exist');
|
||||||
assert.ok(fs.existsSync(firstSkill), `SKILL.md must exist at ${firstSkill}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('legacy .windsurf/ is NOT written on a fresh local install', () => {
|
test('legacy .devin/skills is NOT written on a fresh local install', () => {
|
||||||
install(false, 'windsurf');
|
install(false, 'windsurf');
|
||||||
const legacyDir = path.join(tmpDir, '.windsurf');
|
assert.ok(!fs.existsSync(path.join(tmpDir, '.devin', 'skills')),
|
||||||
assert.ok(!fs.existsSync(legacyDir),
|
'.devin/skills must not be created by a fresh install (new installs use .windsurf/workflows)');
|
||||||
'.windsurf/ must not be created by a fresh install (new installs use .devin/)');
|
assert.ok(!fs.existsSync(path.join(tmpDir, '.windsurf', 'skills')),
|
||||||
|
'.windsurf/skills must not be created for slash commands');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('installed skill content references .devin/ not bare .windsurf/ or ~/.claude/', () => {
|
test('installed workflow content references command body, not skill locations', () => {
|
||||||
install(false, 'windsurf');
|
install(false, 'windsurf');
|
||||||
const skillsDir = path.join(tmpDir, '.devin', 'skills');
|
const workflowsDir = path.join(tmpDir, '.windsurf', 'workflows');
|
||||||
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
const workflowEntries = fs.readdirSync(workflowsDir, { withFileTypes: true })
|
||||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
.filter(e => e.isFile() && e.name.startsWith('gsd-') && e.name.endsWith('.md'));
|
||||||
assert.ok(skillEntries.length > 0, 'pre-condition: at least one gsd-* skill must be installed');
|
assert.ok(workflowEntries.length > 0, 'pre-condition: at least one gsd-* workflow must be installed');
|
||||||
for (const skillEntry of skillEntries) {
|
for (const workflowEntry of workflowEntries) {
|
||||||
const skillFile = path.join(skillsDir, skillEntry.name, 'SKILL.md');
|
const workflowFile = path.join(workflowsDir, workflowEntry.name);
|
||||||
if (!fs.existsSync(skillFile)) continue;
|
const content = fs.readFileSync(workflowFile, 'utf8');
|
||||||
const content = fs.readFileSync(skillFile, 'utf8');
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes('~/.claude/') && !content.includes('$HOME/.claude/'),
|
content.includes(`${tmpDir}/.windsurf/gsd-core/commands/gsd/`),
|
||||||
`${skillEntry.name}/SKILL.md must not contain ~/.claude/ or $HOME/.claude/ in a local install`,
|
`${workflowEntry.name} must reference the installed canonical command body`,
|
||||||
);
|
);
|
||||||
// Local install must use workspace-relative .devin/ form, not the legacy .windsurf/ form
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes('~/.windsurf/') && !content.includes('.windsurf/skills/'),
|
!content.includes('/skills/') && !content.includes('SKILL.md'),
|
||||||
`${skillEntry.name}/SKILL.md must not contain bare .windsurf/ path in a local install (use .devin/ instead)`,
|
`${workflowEntry.name} must not reference Windsurf skill locations`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('global windsurf install still writes to ~/.codeium/windsurf/ (unchanged)', () => {
|
test('global windsurf install does not write unsupported workflows or skills', () => {
|
||||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-global-'));
|
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-global-'));
|
||||||
const savedHome = process.env.HOME;
|
const savedHome = process.env.HOME;
|
||||||
const savedUserProfile = process.env.USERPROFILE;
|
const savedUserProfile = process.env.USERPROFILE;
|
||||||
@@ -1348,12 +1346,12 @@ describe('windsurf local install writes to .devin/ canonical dir (#1085)', () =>
|
|||||||
`global windsurf install must go to codeium/windsurf path, got: ${result.configDir}`,
|
`global windsurf install must go to codeium/windsurf path, got: ${result.configDir}`,
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
fs.existsSync(path.join(result.configDir, 'skills')),
|
!fs.existsSync(path.join(result.configDir, 'workflows')),
|
||||||
'global windsurf install must create skills/ under ~/.codeium/windsurf',
|
'global windsurf install must not create unsupported workflows/ under ~/.codeium/windsurf',
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!fs.existsSync(path.join(homeDir, '.devin')),
|
!fs.existsSync(path.join(result.configDir, 'skills')),
|
||||||
'.devin/ must NOT be created by a global install (global path is ~/.codeium/windsurf)',
|
'global windsurf install must not create dead skills/ artifacts',
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (savedHome === undefined) delete process.env.HOME;
|
if (savedHome === undefined) delete process.env.HOME;
|
||||||
@@ -1366,7 +1364,7 @@ describe('windsurf local install writes to .devin/ canonical dir (#1085)', () =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('global windsurf install skill content references codeium path not .devin/', () => {
|
test('global windsurf install still installs shared workflow assets', () => {
|
||||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-global-c-'));
|
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-global-c-'));
|
||||||
const savedHome = process.env.HOME;
|
const savedHome = process.env.HOME;
|
||||||
const savedUserProfile = process.env.USERPROFILE;
|
const savedUserProfile = process.env.USERPROFILE;
|
||||||
@@ -1376,37 +1374,10 @@ describe('windsurf local install writes to .devin/ canonical dir (#1085)', () =>
|
|||||||
process.env.USERPROFILE = homeDir;
|
process.env.USERPROFILE = homeDir;
|
||||||
try {
|
try {
|
||||||
const result = install(true, 'windsurf');
|
const result = install(true, 'windsurf');
|
||||||
const skillsDir = path.join(result.configDir, 'skills');
|
assert.ok(fs.existsSync(path.join(result.configDir, 'gsd-core', 'workflows', 'update.md')),
|
||||||
if (!fs.existsSync(skillsDir)) return; // no skills emitted — skip
|
'global windsurf install should still copy shared gsd-core workflow assets');
|
||||||
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
assert.ok(!fs.existsSync(path.join(result.configDir, 'workflows')),
|
||||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
'global windsurf install must not write workflow files into an undocumented global path');
|
||||||
// At least one skill body must reference the codeium/windsurf global path (#1085):
|
|
||||||
// the isGlobal-threaded rewrite converts .devin/skills/ → $HOME/.codeium/windsurf/skills/
|
|
||||||
let foundGlobalRef = false;
|
|
||||||
for (const skillEntry of skillEntries) {
|
|
||||||
const skillFile = path.join(skillsDir, skillEntry.name, 'SKILL.md');
|
|
||||||
if (!fs.existsSync(skillFile)) continue;
|
|
||||||
const content = fs.readFileSync(skillFile, 'utf8');
|
|
||||||
// Global skill content must not reference local workspace-relative .devin/ paths
|
|
||||||
assert.ok(
|
|
||||||
!content.includes('.devin/skills/'),
|
|
||||||
`${skillEntry.name}/SKILL.md must not reference .devin/skills/ in global install (should use codeium path)`,
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
!content.includes('~/.claude/') && !content.includes('$HOME/.claude/'),
|
|
||||||
`${skillEntry.name}/SKILL.md must not contain ~/.claude/ or $HOME/.claude/ in global install`,
|
|
||||||
);
|
|
||||||
if (content.includes('codeium/windsurf/skills/') || content.includes('$HOME/.codeium/windsurf/skills/')) {
|
|
||||||
foundGlobalRef = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Verify the global-path rewrite actually fired on at least one skill (FIX 1 guard)
|
|
||||||
if (skillEntries.some(e => fs.existsSync(path.join(skillsDir, e.name, 'SKILL.md')))) {
|
|
||||||
assert.ok(
|
|
||||||
foundGlobalRef,
|
|
||||||
'at least one global windsurf SKILL.md must reference the codeium/windsurf/skills/ path (isGlobal rewrite must have fired)',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
if (savedHome === undefined) delete process.env.HOME;
|
if (savedHome === undefined) delete process.env.HOME;
|
||||||
else process.env.HOME = savedHome;
|
else process.env.HOME = savedHome;
|
||||||
|
|||||||
@@ -104,12 +104,9 @@ const GOLDEN = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
// ── windsurf ─────────────────────────────────────────────────────────────────
|
// ── windsurf ─────────────────────────────────────────────────────────────────
|
||||||
// Old switch: no scope branch → local == global. 5b backfill restores this.
|
'windsurf/global': [],
|
||||||
'windsurf/global': [
|
|
||||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
|
||||||
],
|
|
||||||
'windsurf/local': [
|
'windsurf/local': [
|
||||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
{ kind: 'commands', destSubpath: 'workflows', prefix: 'gsd-' },
|
||||||
],
|
],
|
||||||
|
|
||||||
// ── augment ──────────────────────────────────────────────────────────────────
|
// ── augment ──────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -134,16 +134,23 @@ describe('resolveRuntimeArtifactLayout — antigravity', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('resolveRuntimeArtifactLayout — windsurf', () => {
|
describe('resolveRuntimeArtifactLayout — windsurf', () => {
|
||||||
test('returns correct layout for windsurf', () => {
|
test('returns local workflow layout for windsurf', () => {
|
||||||
const layout = resolveRuntimeArtifactLayout('windsurf', FAKE_DIR);
|
const layout = resolveRuntimeArtifactLayout('windsurf', FAKE_DIR, 'local');
|
||||||
assert.strictEqual(layout.runtime, 'windsurf');
|
assert.strictEqual(layout.runtime, 'windsurf');
|
||||||
assert.strictEqual(layout.configDir, FAKE_DIR);
|
assert.strictEqual(layout.configDir, FAKE_DIR);
|
||||||
assert.strictEqual(layout.kinds.length, 1);
|
assert.strictEqual(layout.kinds.length, 1);
|
||||||
assert.strictEqual(layout.kinds[0].kind, 'skills');
|
assert.strictEqual(layout.kinds[0].kind, 'commands');
|
||||||
assert.strictEqual(layout.kinds[0].destSubpath, 'skills');
|
assert.strictEqual(layout.kinds[0].destSubpath, 'workflows');
|
||||||
assert.strictEqual(layout.kinds[0].prefix, 'gsd-');
|
assert.strictEqual(layout.kinds[0].prefix, 'gsd-');
|
||||||
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
|
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('returns empty global layout for windsurf', () => {
|
||||||
|
const layout = resolveRuntimeArtifactLayout('windsurf', FAKE_DIR, 'global');
|
||||||
|
assert.strictEqual(layout.runtime, 'windsurf');
|
||||||
|
assert.strictEqual(layout.configDir, FAKE_DIR);
|
||||||
|
assert.strictEqual(layout.kinds.length, 0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resolveRuntimeArtifactLayout — augment', () => {
|
describe('resolveRuntimeArtifactLayout — augment', () => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const assert = require('node:assert/strict');
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
convertClaudeCommandToWindsurfSkill,
|
convertClaudeCommandToWindsurfSkill,
|
||||||
|
convertClaudeCommandToWindsurfWorkflow,
|
||||||
convertClaudeAgentToWindsurfAgent,
|
convertClaudeAgentToWindsurfAgent,
|
||||||
convertClaudeToWindsurfMarkdown,
|
convertClaudeToWindsurfMarkdown,
|
||||||
} = require('../bin/install.js');
|
} = require('../bin/install.js');
|
||||||
@@ -73,6 +74,29 @@ Body content.
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('convertClaudeCommandToWindsurfWorkflow', () => {
|
||||||
|
test('writes a plain workflow wrapper for slash commands', () => {
|
||||||
|
const input = `---
|
||||||
|
name: quick
|
||||||
|
description: Execute a quick task
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Test body
|
||||||
|
</objective>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = convertClaudeCommandToWindsurfWorkflow(input, 'gsd-quick');
|
||||||
|
|
||||||
|
assert.ok(!result.startsWith('---'), 'workflow has no YAML frontmatter');
|
||||||
|
assert.match(result, /^# gsd-quick$/m, 'workflow title names the slash command');
|
||||||
|
assert.ok(result.includes('Execute a quick task'), 'description is preserved');
|
||||||
|
assert.ok(result.includes('@~/.claude/gsd-core/commands/gsd/quick.md'), 'workflow delegates to canonical command body');
|
||||||
|
assert.ok(result.includes('/gsd-quick'), 'workflow mentions the slash command invocation');
|
||||||
|
assert.ok(Buffer.byteLength(result, 'utf8') <= 12000, 'workflow respects Windsurf limit');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('convertClaudeAgentToWindsurfAgent', () => {
|
describe('convertClaudeAgentToWindsurfAgent', () => {
|
||||||
test('converts agent frontmatter with unquoted name', () => {
|
test('converts agent frontmatter with unquoted name', () => {
|
||||||
const input = `---
|
const input = `---
|
||||||
@@ -105,17 +129,17 @@ describe('convertClaudeToWindsurfMarkdown', () => {
|
|||||||
assert.ok(!result.includes('Claude Code'), 'original brand removed');
|
assert.ok(!result.includes('Claude Code'), 'original brand removed');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('replaces CLAUDE.md with .devin/rules (no trailing slash)', () => {
|
test('replaces CLAUDE.md with .windsurf/rules (no trailing slash)', () => {
|
||||||
const input = 'See `CLAUDE.md` for configuration. Also check ./CLAUDE.md file.';
|
const input = 'See `CLAUDE.md` for configuration. Also check ./CLAUDE.md file.';
|
||||||
const result = convertClaudeToWindsurfMarkdown(input);
|
const result = convertClaudeToWindsurfMarkdown(input);
|
||||||
assert.ok(result.includes('.devin/rules'), 'CLAUDE.md replaced with .devin/rules (#1085)');
|
assert.ok(result.includes('.windsurf/rules'), 'CLAUDE.md replaced with .windsurf/rules');
|
||||||
assert.ok(!result.includes('.devin/rules/'), 'no trailing slash (Node v25 compat)');
|
assert.ok(!result.includes('.windsurf/rules/'), 'no trailing slash (Node v25 compat)');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('replaces .claude/skills/ with .devin/skills/', () => {
|
test('replaces .claude/skills/ with .windsurf/skills/', () => {
|
||||||
const input = 'Skills are stored in .claude/skills/ directory.';
|
const input = 'Skills are stored in .claude/skills/ directory.';
|
||||||
const result = convertClaudeToWindsurfMarkdown(input);
|
const result = convertClaudeToWindsurfMarkdown(input);
|
||||||
assert.ok(result.includes('.devin/skills/'), 'skills path replaced with .devin/skills/ (#1085)');
|
assert.ok(result.includes('.windsurf/skills/'), 'skills path replaced with .windsurf/skills/');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('replaces Bash( with Shell( and Edit( with StrReplace(', () => {
|
test('replaces Bash( with Shell( and Edit( with StrReplace(', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user