Fresh windsurf/devin-desktop workspace installs write skills under .devin/ (legacy .windsurf/ recognized); global ~/.codeium/windsurf/ unchanged. Also threads real isGlobal through _applyRuntimeRewrites so global skill content references the codeium path. Closes #1085.
This commit is contained in:
5
.changeset/1085-devin-skills-workspace.md
Normal file
5
.changeset/1085-devin-skills-workspace.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 1093
|
||||
---
|
||||
**Windsurf/Devin workspace skills now install to the canonical `.devin/skills/` directory** — fresh workspace installs write skills under `.devin/skills/` (Devin Desktop's documented preferred location) instead of `.windsurf/skills/`; the legacy `.windsurf/skills/` layout is still recognized. The global `~/.codeium/windsurf/skills/` path is unchanged. (#1093)
|
||||
@@ -467,7 +467,7 @@ function getDirName(runtime) {
|
||||
if (runtime === 'codex') return '.codex';
|
||||
if (runtime === 'antigravity') return '.agents';
|
||||
if (runtime === 'cursor') return '.cursor';
|
||||
if (runtime === 'windsurf') return '.windsurf';
|
||||
if (runtime === 'windsurf') return '.devin';
|
||||
if (runtime === 'augment') return '.augment';
|
||||
if (runtime === 'trae') return '.trae';
|
||||
if (runtime === 'qwen') return '.qwen';
|
||||
@@ -2504,18 +2504,20 @@ function convertClaudeToWindsurfMarkdown(content) {
|
||||
// Replace subagent_type from Claude to Windsurf format
|
||||
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
||||
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
||||
// Replace project-level Claude conventions with Windsurf equivalents
|
||||
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
|
||||
converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
|
||||
converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
|
||||
converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
|
||||
converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
|
||||
converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
|
||||
converted = converted.replace(/\.claude\//g, '.windsurf/');
|
||||
// Replace project-level Claude conventions with Windsurf/Devin equivalents
|
||||
// Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085).
|
||||
// Legacy .windsurf/ is still recognized on read but new installs use .devin/.
|
||||
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.devin/rules`');
|
||||
converted = converted.replace(/\.\/CLAUDE\.md/g, '.devin/rules');
|
||||
converted = converted.replace(/`CLAUDE\.md`/g, '`.devin/rules`');
|
||||
converted = converted.replace(/\bCLAUDE\.md\b/g, '.devin/rules');
|
||||
converted = converted.replace(/\.claude\/skills\//g, '.devin/skills/');
|
||||
converted = converted.replace(/\.\/\.claude\//g, './.devin/');
|
||||
converted = converted.replace(/\.claude\//g, '.devin/');
|
||||
// Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
|
||||
// Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
|
||||
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
|
||||
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
|
||||
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.devin');
|
||||
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.devin');
|
||||
// Environment variable name rewrite
|
||||
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
|
||||
// Remove Claude Code-specific bug workarounds before brand replacement
|
||||
@@ -6782,8 +6784,9 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = '
|
||||
* @param {string} stagedDir
|
||||
* @param {string} runtime
|
||||
* @param {string} pathPrefix e.g. "~/.codex/" — trailing-slash string
|
||||
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
||||
*/
|
||||
function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix) {
|
||||
function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGlobal = false) {
|
||||
if (!fs.existsSync(stagedDir)) return;
|
||||
|
||||
// Walk all SKILL.md files under stagedDir
|
||||
@@ -6794,7 +6797,7 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix) {
|
||||
walkAndRewrite(fullPath);
|
||||
} else if (entry.name.endsWith('.md')) {
|
||||
let content = fs.readFileSync(fullPath, 'utf8');
|
||||
content = _applyRuntimeRewrites(content, runtime, pathPrefix);
|
||||
content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
|
||||
fs.writeFileSync(fullPath, content);
|
||||
}
|
||||
}
|
||||
@@ -6815,9 +6818,10 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix) {
|
||||
* @param {string} stagedDir directory of staged flat .md command files (may be source dir)
|
||||
* @param {string} runtime
|
||||
* @param {string} pathPrefix
|
||||
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
||||
* @returns {string} path to a temp dir with rewritten files (caller is responsible for cleanup)
|
||||
*/
|
||||
function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix) {
|
||||
function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix, isGlobal = false) {
|
||||
if (!fs.existsSync(stagedDir)) return stagedDir;
|
||||
// Always copy to a temp dir — stageSkillsForProfile() returns the original source
|
||||
// dir on full/default profile (skills === '*'), so writing in-place would corrupt the
|
||||
@@ -6827,7 +6831,7 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
|
||||
for (const entry of fs.readdirSync(stagedDir, { withFileTypes: true })) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
||||
let content = fs.readFileSync(path.join(stagedDir, entry.name), 'utf8');
|
||||
content = _applyRuntimeRewrites(content, runtime, pathPrefix);
|
||||
content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
|
||||
// For augment commands, apply the markdown conversion so tool references
|
||||
// and skill paths use Augment equivalents.
|
||||
if (runtime === 'augment') {
|
||||
@@ -6849,9 +6853,10 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
|
||||
* @param {string} content
|
||||
* @param {string} runtime
|
||||
* @param {string} pathPrefix trailing-slash string
|
||||
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
||||
* @returns {string}
|
||||
*/
|
||||
function _applyRuntimeRewrites(content, runtime, pathPrefix) {
|
||||
function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
|
||||
const dirName = getDirName(runtime);
|
||||
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
|
||||
|
||||
@@ -6888,18 +6893,30 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix) {
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
break;
|
||||
|
||||
case 'windsurf':
|
||||
case 'windsurf': {
|
||||
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
||||
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
||||
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
||||
// Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that
|
||||
// .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary
|
||||
// fires between 'e' and '-', which rewrites .claude-plugin → .windsurf-plugin).
|
||||
// fires between 'e' and '-', which rewrites .claude-plugin → .devin-plugin).
|
||||
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
||||
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
||||
content = content.replace(/~\/\.codeium\/windsurf\//g, pathPrefix);
|
||||
// Stage-1 converter rewrites .claude/skills/ → .devin/skills/ (workspace-relative
|
||||
// form). For global installs the real path is pathPrefix + skills/, so fix that up
|
||||
// here using the real isGlobal flag (threaded from installRuntimeArtifacts scope,
|
||||
// not derived from pathPrefix substring which misclassifies custom config dirs).
|
||||
// For local installs, the relative .devin/ form is correct — leave it. (#1085)
|
||||
if (isGlobal) {
|
||||
content = content.replace(/\.devin\/skills\//g, `${pathPrefix}skills/`);
|
||||
content = content.replace(/\.\/\.devin\//g, pathPrefix);
|
||||
content = content.replace(/~\/\.devin(?![\w-])/g, normalizedPathPrefix);
|
||||
content = content.replace(/\$HOME\/\.devin(?![\w-])/g, normalizedPathPrefix);
|
||||
}
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'augment':
|
||||
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
||||
@@ -7331,11 +7348,12 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
||||
// stagedForCopy: the directory to copy from (may differ from staged if rewrites
|
||||
// produce a temp copy — see applyRuntimeContentRewritesForCommandsInPlace).
|
||||
let stagedForCopy = staged;
|
||||
const isGlobal = scope === 'global';
|
||||
if (kind.kind === 'skills' || kind.kind === 'kimi-agents') {
|
||||
applyRuntimeContentRewritesInPlace(staged, runtime, pathPrefix);
|
||||
applyRuntimeContentRewritesInPlace(staged, runtime, pathPrefix, isGlobal);
|
||||
} else if (kind.kind === 'commands') {
|
||||
// Returns a temp dir with rewritten content so source files are never mutated.
|
||||
stagedForCopy = applyRuntimeContentRewritesForCommandsInPlace(staged, runtime, pathPrefix);
|
||||
stagedForCopy = applyRuntimeContentRewritesForCommandsInPlace(staged, runtime, pathPrefix, isGlobal);
|
||||
}
|
||||
// applyRuntimeContentRewritesForCommandsInPlace() returns a fresh mkdtemp dir under
|
||||
// os.tmpdir() (gsd-cmd-rewrites-*); remove it once copied so it does not accumulate (#856).
|
||||
@@ -7670,11 +7688,12 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
||||
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cursor');
|
||||
fs.writeFileSync(destPath, jsContent);
|
||||
} else if (isWindsurf && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
||||
// For Windsurf, also convert Claude references in JS/CJS utility scripts
|
||||
// For Windsurf/Devin, also convert Claude references in JS/CJS utility scripts.
|
||||
// Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085).
|
||||
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
||||
jsContent = jsContent.replace(/gsd:/gi, 'gsd-');
|
||||
jsContent = jsContent.replace(/\.claude\/skills\//g, '.windsurf/skills/');
|
||||
jsContent = jsContent.replace(/CLAUDE\.md/g, '.windsurf/rules');
|
||||
jsContent = jsContent.replace(/\.claude\/skills\//g, '.devin/skills/');
|
||||
jsContent = jsContent.replace(/CLAUDE\.md/g, '.devin/rules');
|
||||
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Windsurf');
|
||||
fs.writeFileSync(destPath, jsContent);
|
||||
} else if (isTrae && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
||||
@@ -11907,7 +11926,8 @@ const _LEGACY_SCAN_SUBDIR_NAMES = [
|
||||
'.agents', // antigravity local form (canonical, #791)
|
||||
'.agent', // antigravity local form (legacy, backward-compat)
|
||||
'.cursor',
|
||||
'.windsurf',
|
||||
'.devin', // windsurf local form (canonical, #1085; Devin Desktop preferred dir)
|
||||
'.windsurf', // windsurf local form (legacy, backward-compat with pre-#1085 installs)
|
||||
'.codeium/windsurf',
|
||||
'.augment',
|
||||
'.trae',
|
||||
|
||||
@@ -586,7 +586,7 @@ Equivalent paths for other runtimes:
|
||||
- **Copilot:** `~/.copilot/` global or `./.github/` local
|
||||
- **Antigravity:** auto-detected global root (`~/.gemini/antigravity/`, `~/.gemini/antigravity-ide/`, or `~/.gemini/antigravity-cli/`) or `./.agent/` local
|
||||
- **Cursor:** `~/.cursor/` global or `./.cursor/` local
|
||||
- **Windsurf:** `~/.codeium/windsurf/` global or `./.windsurf/` local
|
||||
- **Windsurf/Devin Desktop:** `~/.codeium/windsurf/` global or `./.devin/` local (canonical, #1085); `./.windsurf/` local is still recognized as legacy
|
||||
- **Augment Code:** `~/.augment/` global or `./.augment/` local
|
||||
- **Trae:** `~/.trae/` global or `./.trae/` local
|
||||
- **Qwen Code:** `~/.qwen/` global or `./.qwen/` local
|
||||
@@ -825,7 +825,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 |
|
||||
| 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) |
|
||||
| Windsurf | `~/.codeium/windsurf` | `./.windsurf` | `skills/gsd-*/SKILL.md` (flat) | `agents/gsd-*.md` | Rule references under `rules/`; no GSD hooks |
|
||||
| Windsurf | `~/.codeium/windsurf` | `./.devin` (canonical, #1085); `./.windsurf` legacy recognized | `skills/gsd-*/SKILL.md` (flat) | `agents/gsd-*.md` | Rule references under `rules/`; 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -318,7 +318,7 @@ npx @opengsd/gsd-core@latest --windsurf --global
|
||||
npx @opengsd/gsd-core@latest --devin-desktop --global
|
||||
```
|
||||
|
||||
Skills land in `~/.codeium/windsurf/` (global path unchanged). GSD installs skills, agents, and workspace rules.
|
||||
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.
|
||||
|
||||
**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 |
|
||||
| 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 |
|
||||
| Windsurf | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/` | Global `WINDSURF_CONFIG_DIR` or `~/.codeium/windsurf`; local `./.windsurf` | GSD owns generated skills/agents and GSD rule files or references; no hook/statusline ownership | Windsurf public rule docs were source-limited in search results as of 2026-05-11; installer targets the common workspace rules convention `./.windsurf/rules` and must be rechecked before migrations rewrite rules |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -26,6 +26,8 @@ export const RUNTIME_DIRS: RuntimeDirEntry[] = [
|
||||
['antigravity', '.gemini/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)
|
||||
['windsurf', '.devin'], // local Windsurf/Devin Desktop install dir canonical (#1085; bin/install.js getDirName('windsurf'))
|
||||
['windsurf', '.windsurf'], // local Windsurf install dir legacy (#1085; backward-compat with pre-#1085 installs)
|
||||
['gemini', '.gemini'],
|
||||
['kilo', '.config/kilo'],
|
||||
['kilo', '.kilo'],
|
||||
|
||||
@@ -29,24 +29,24 @@ const {
|
||||
// ─── Windsurf converter bare-form tests ─────────────────────────────────────
|
||||
|
||||
describe('convertClaudeToWindsurfMarkdown — bare ~/.claude and CLAUDE_CONFIG_DIR (#983)', () => {
|
||||
test('bare ~/.claude rewritten to ~/.windsurf', () => {
|
||||
test('bare ~/.claude rewritten to ~/.devin (#1085: workspace dir is now .devin)', () => {
|
||||
const input = 'Config dir: (~/.claude), skills at ~/.claude/skills';
|
||||
const result = convertClaudeToWindsurfMarkdown(input);
|
||||
assert.ok(
|
||||
!/~\/\.claude(?![\w-])/.test(result),
|
||||
`bare ~/.claude must be rewritten; got: ${result}`,
|
||||
);
|
||||
assert.ok(result.includes('~/.windsurf'), 'must rewrite to ~/.windsurf');
|
||||
assert.ok(result.includes('~/.devin'), 'must rewrite to ~/.devin');
|
||||
});
|
||||
|
||||
test('$HOME/.claude rewritten to $HOME/.windsurf', () => {
|
||||
test('$HOME/.claude rewritten to $HOME/.devin (#1085: workspace dir is now .devin)', () => {
|
||||
const input = 'RUNTIME_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"';
|
||||
const result = convertClaudeToWindsurfMarkdown(input);
|
||||
assert.ok(
|
||||
!/\$HOME\/\.claude(?![\w-])/.test(result),
|
||||
`bare $HOME/.claude must be rewritten; got: ${result}`,
|
||||
);
|
||||
assert.ok(result.includes('$HOME/.windsurf'), 'must rewrite to $HOME/.windsurf');
|
||||
assert.ok(result.includes('$HOME/.devin'), 'must rewrite to $HOME/.devin');
|
||||
});
|
||||
|
||||
test('CLAUDE_CONFIG_DIR rewritten to WINDSURF_CONFIG_DIR', () => {
|
||||
|
||||
@@ -54,7 +54,7 @@ const RUNTIME_META = {
|
||||
opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') },
|
||||
qwen: { localDir: '.qwen', globalSuffix: '.qwen' },
|
||||
trae: { localDir: '.trae', globalSuffix: '.trae' },
|
||||
windsurf: { localDir: '.windsurf', globalSuffix: path.join('.codeium', 'windsurf') },
|
||||
windsurf: { localDir: '.devin', globalSuffix: path.join('.codeium', 'windsurf') },
|
||||
};
|
||||
|
||||
// Runtimes that emit per-skill files under skills/ (not rules-based or commands-based)
|
||||
@@ -112,7 +112,7 @@ function runMinimalInstall({ runtime, scope, extraArgs = [] }) {
|
||||
const LOCAL_DIR_NAME = {
|
||||
claude: '.claude', opencode: '.opencode', gemini: '.gemini', kilo: '.kilo',
|
||||
codex: '.codex', copilot: '.github', antigravity: '.agents', cursor: '.cursor',
|
||||
windsurf: '.windsurf', augment: '.augment', trae: '.trae', qwen: '.qwen',
|
||||
windsurf: '.devin', augment: '.augment', trae: '.trae', qwen: '.qwen',
|
||||
codebuddy: '.codebuddy', cline: '.',
|
||||
};
|
||||
let configDir;
|
||||
|
||||
@@ -954,6 +954,155 @@ describe('install — --devin-desktop CLI flag routes to windsurf runtime (#792)
|
||||
assert.deepStrictEqual(selectRuntimesFromArgs(['--devin-desktop']), ['windsurf']);
|
||||
});
|
||||
});
|
||||
// ─── Section N: Windsurf .devin canonical workspace dir (#1085) ─────────────
|
||||
// allow-test-rule: runtime-contract-is-the-product
|
||||
// Reads deployed skill .md files whose text IS the product surface the
|
||||
// Windsurf/Devin Desktop runtime loads at startup (path references, command names).
|
||||
|
||||
describe('windsurf local install writes to .devin/ canonical dir (#1085)', () => {
|
||||
let tmpDir;
|
||||
let previousCwd;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempDir('gsd-windsurf-1085-');
|
||||
previousCwd = process.cwd();
|
||||
process.chdir(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(previousCwd);
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('install writes workspace skills under .devin/skills/', () => {
|
||||
const result = install(false, 'windsurf');
|
||||
const devinDir = path.join(tmpDir, '.devin');
|
||||
assert.strictEqual(result.runtime, 'windsurf');
|
||||
assert.ok(fs.existsSync(devinDir), '.devin/ must be created for local windsurf install');
|
||||
const skillsDir = path.join(devinDir, 'skills');
|
||||
assert.ok(fs.existsSync(skillsDir), '.devin/skills/ must exist after install');
|
||||
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
||||
assert.ok(skillEntries.length > 0, 'at least one gsd-* skill must be installed under .devin/skills/');
|
||||
const firstSkill = path.join(skillsDir, skillEntries[0].name, 'SKILL.md');
|
||||
assert.ok(fs.existsSync(firstSkill), `SKILL.md must exist at ${firstSkill}`);
|
||||
});
|
||||
|
||||
test('legacy .windsurf/ is NOT written on a fresh local install', () => {
|
||||
install(false, 'windsurf');
|
||||
const legacyDir = path.join(tmpDir, '.windsurf');
|
||||
assert.ok(!fs.existsSync(legacyDir),
|
||||
'.windsurf/ must not be created by a fresh install (new installs use .devin/)');
|
||||
});
|
||||
|
||||
test('installed skill content references .devin/ not bare .windsurf/ or ~/.claude/', () => {
|
||||
install(false, 'windsurf');
|
||||
const skillsDir = path.join(tmpDir, '.devin', 'skills');
|
||||
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
||||
assert.ok(skillEntries.length > 0, 'pre-condition: at least one gsd-* skill must be installed');
|
||||
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');
|
||||
assert.ok(
|
||||
!content.includes('~/.claude/') && !content.includes('$HOME/.claude/'),
|
||||
`${skillEntry.name}/SKILL.md must not contain ~/.claude/ or $HOME/.claude/ in a local install`,
|
||||
);
|
||||
// Local install must use workspace-relative .devin/ form, not the legacy .windsurf/ form
|
||||
assert.ok(
|
||||
!content.includes('~/.windsurf/') && !content.includes('.windsurf/skills/'),
|
||||
`${skillEntry.name}/SKILL.md must not contain bare .windsurf/ path in a local install (use .devin/ instead)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('global windsurf install still writes to ~/.codeium/windsurf/ (unchanged)', () => {
|
||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-global-'));
|
||||
const savedHome = process.env.HOME;
|
||||
const savedUserProfile = process.env.USERPROFILE;
|
||||
const savedWindsurfConfig = process.env.WINDSURF_CONFIG_DIR;
|
||||
delete process.env.WINDSURF_CONFIG_DIR;
|
||||
process.env.HOME = homeDir;
|
||||
process.env.USERPROFILE = homeDir;
|
||||
try {
|
||||
const result = install(true, 'windsurf');
|
||||
assert.strictEqual(result.runtime, 'windsurf');
|
||||
assert.ok(
|
||||
result.configDir.includes('codeium') || result.configDir.includes('windsurf'),
|
||||
`global windsurf install must go to codeium/windsurf path, got: ${result.configDir}`,
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(result.configDir, 'skills')),
|
||||
'global windsurf install must create skills/ under ~/.codeium/windsurf',
|
||||
);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(homeDir, '.devin')),
|
||||
'.devin/ must NOT be created by a global install (global path is ~/.codeium/windsurf)',
|
||||
);
|
||||
} finally {
|
||||
if (savedHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = savedHome;
|
||||
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = savedUserProfile;
|
||||
if (savedWindsurfConfig === undefined) delete process.env.WINDSURF_CONFIG_DIR;
|
||||
else process.env.WINDSURF_CONFIG_DIR = savedWindsurfConfig;
|
||||
cleanup(homeDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('global windsurf install skill content references codeium path not .devin/', () => {
|
||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-global-c-'));
|
||||
const savedHome = process.env.HOME;
|
||||
const savedUserProfile = process.env.USERPROFILE;
|
||||
const savedWindsurfConfig = process.env.WINDSURF_CONFIG_DIR;
|
||||
delete process.env.WINDSURF_CONFIG_DIR;
|
||||
process.env.HOME = homeDir;
|
||||
process.env.USERPROFILE = homeDir;
|
||||
try {
|
||||
const result = install(true, 'windsurf');
|
||||
const skillsDir = path.join(result.configDir, 'skills');
|
||||
if (!fs.existsSync(skillsDir)) return; // no skills emitted — skip
|
||||
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
||||
// 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 {
|
||||
if (savedHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = savedHome;
|
||||
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = savedUserProfile;
|
||||
if (savedWindsurfConfig === undefined) delete process.env.WINDSURF_CONFIG_DIR;
|
||||
else process.env.WINDSURF_CONFIG_DIR = savedWindsurfConfig;
|
||||
cleanup(homeDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
// ─── Section N+1: #767 — disallowedTools injection for read-only agents ──────
|
||||
//
|
||||
// Verifies (installer-behavioral test — drives install() to a temp dir):
|
||||
|
||||
@@ -105,17 +105,17 @@ describe('convertClaudeToWindsurfMarkdown', () => {
|
||||
assert.ok(!result.includes('Claude Code'), 'original brand removed');
|
||||
});
|
||||
|
||||
test('replaces CLAUDE.md with .windsurf/rules (no trailing slash)', () => {
|
||||
test('replaces CLAUDE.md with .devin/rules (no trailing slash)', () => {
|
||||
const input = 'See `CLAUDE.md` for configuration. Also check ./CLAUDE.md file.';
|
||||
const result = convertClaudeToWindsurfMarkdown(input);
|
||||
assert.ok(result.includes('.windsurf/rules'), 'CLAUDE.md replaced');
|
||||
assert.ok(!result.includes('.windsurf/rules/'), 'no trailing slash (Node v25 compat)');
|
||||
assert.ok(result.includes('.devin/rules'), 'CLAUDE.md replaced with .devin/rules (#1085)');
|
||||
assert.ok(!result.includes('.devin/rules/'), 'no trailing slash (Node v25 compat)');
|
||||
});
|
||||
|
||||
test('replaces .claude/skills/ with .windsurf/skills/', () => {
|
||||
test('replaces .claude/skills/ with .devin/skills/', () => {
|
||||
const input = 'Skills are stored in .claude/skills/ directory.';
|
||||
const result = convertClaudeToWindsurfMarkdown(input);
|
||||
assert.ok(result.includes('.windsurf/skills/'), 'skills path replaced');
|
||||
assert.ok(result.includes('.devin/skills/'), 'skills path replaced with .devin/skills/ (#1085)');
|
||||
});
|
||||
|
||||
test('replaces Bash( with Shell( and Edit( with StrReplace(', () => {
|
||||
|
||||
Reference in New Issue
Block a user