跪拜 Guibai
← Back to the summary

A Single Config Syncs AI Rules, Skills, and MCP Servers Across Five IDEs

In today's world of rampant AI IDEs, it seems every team is talking about how to write good AI rules. The author's small team is developing an innovative project, with team members mixing various AI IDEs (IDE matrix: Cursor + CodeBuddy + Windsurf + Trae + Qoder, each new member picks their favorite), and has accumulated a lot of project-level AI context (TypeScript standards, React component conventions, MCP Server lists). As the project members increased, problems arose—

New team member: Bro, what AI rules does our project use? Me: They're written in .cursorrules... New member: But I'm using CodeBuddy. Me: ...Then copy a version to .codebuddy/rules/? New member: What about every time the rules are updated later? Me: ...Notify everyone in the group chat?

Good grief, this job has even less prestige than prettier. Thus, xingwu-ai-kit was born—a CLI that lets "one configuration sync to 5 IDEs."

Making Trouble 1.0: Get It Running First

The author has a bad habit when writing projects: they like to get the ugliest version running first, knowing they'll rewrite it anyway (the author doesn't like repeating past solutions and enjoys using new toys).

The core logic of the simplest version was just three lines—read config, read template, writeFileSync to the IDE directory:

// First version core logic: write files
for (const ide of config.ide) {
  for (const preset of config.rules.preset) {
    const content = readFileSync(getRuleTemplatePath(preset), 'utf-8')
    writeFileSync(resolve(cwd, adapters[ide].rulesDir, `${preset}.mdc`), content)
  }
}

init generates a config, sync once, and the rule files appear in the CodeBuddy / Cursor directories.

Yep, it works, good enough to hand in.

Making Trouble 1.1: symlink Makes "Change Once, Apply Everywhere"

When the team upgraded the standards for the first time, trouble came—

Colleague A: I see common.mdc doesn't have "prohibit unnecessary optional chaining," can we add that? Me: Changed. Colleague B: My side still has the old version? Me: Run npx xingwu-ai-kit sync. Colleague C: After I ran it, the rule files in my IDE were overwritten, and the comments I added are gone!

The first problem was that template upgrades required everyone to re-run sync, and the second was that writeFileSync would overwrite user's local changes.

So, writeFileSync was changed to symlinkSync:

// Second version: symlink, point IDE directory to template
symlinkSync(getRuleTemplatePath(preset), resolve(rulesDir, `${preset}.mdc'))

After getting it to work, it immediately became sweet:

But the good times didn't last, two new problems emerged:

  1. symlink nesting—old residual symlinks weren't cleaned up, leading to chains like common.mdc -> template.mdc -> real file
  2. Broken links—after pnpm install re-pulls node_modules, all symlinks point to thin air

safeLink handles the first:

// packages/cli/src/core/linker.ts
if (stat.isSymbolicLink()) {
  // Existing symlink → delete first then create, to avoid nesting
  unlinkSync(target)
} else {
  // Real file → respect user, decide based on overrideStrategy
  if (overrideStrategy === 'skip') {
    return { status: 'skipped', reason: 'Project-specific file, skipped' }
  } else if (overrideStrategy === 'override') {
    unlinkSync(target)
  }
}

doctor --fix handles the second:

npx xingwu-ai-kit doctor --fix

doctor scans all IDE directories for broken links, cleans them up as needed, and reports how many were fixed:

━━━ Cursor ━━━
  💔 [broken] symlink points to non-existent path
      Path: .cursor/rules/common.mdc
  🔧 Fixing...
  🗑️  Removed broken link: common.mdc
  ✅ Fixed 1 issue

Yep, it can drag, collapse, and catch errors, should be enough now.

Making Trouble 2.0: JSDoc Config Saves npx / Global Install

One day a colleague threw out a problem:

Colleague: I don't want to install xingwu-ai-kit in every project on my own laptop, can I install it globally? Me: Sure, just pnpm add -g xingwu-ai-kit. Colleague: Then the xingwu-ai-kit.config.ts generated by running init errors out, saying it can't find the xingwu-ai-kit/config package. Me: ...

Looking at the generated config, it looked like this:

// ❌ First version: depends on external package, global install fails
import { defineConfig } from 'xingwu-ai-kit/config'
export default defineConfig({ ... })

init wrote the type definition as an import, causing the config file's runtime to depend on the existence of the xingwu-ai-kit package. Not installed in the project → error. Installed globally → still not in the project → same error. Temporary call via npx → even less installed.

The solution: abandon runtime types, let import degrade to a "pure type annotation":

// ✅ Revised: JSDoc type annotation, zero runtime dependencies
/** @type {import('xingwu-ai-kit/config').AIKitConfig} */
export default { ... }

The import('xingwu-ai-kit/config') in the comment only takes effect during TypeScript type checking / in the editor, it won't actually require this package. So whether installed in-project, globally, or used temporarily via npx, the generated config file can run directly.

defineConfig is still kept, leaving an option for those who have xingwu-ai-kit installed in their project:

import { defineConfig } from 'xingwu-ai-kit/config'  // Requires project-local install too
export default defineConfig({ ... })

Alright, alright, this change isn't big, but it solved a key link for "zero-dependency CLI usage."

Making Trouble 2.1: The "Array Concatenation" Pitfall of c12 + defu

This section is the most "insidious" pitfall the author stepped into, written out as a warning to everyone.

xingwu-ai-kit uses c12 to load xingwu-ai-kit.config.ts, and defu to merge defaults. The first version of config loading looked like this:

import { loadConfig } from 'c12'

const DEFAULT_CONFIG = {
  ide: ['codebuddy', 'cursor'],
  rules: { preset: [], overrideStrategy: 'skip' },
  // ...
}

const { config } = await loadConfig({
  name: 'xingwu-ai-kit',
  cwd,
  defaults: DEFAULT_CONFIG,  // ← c12 internally uses defu to merge
})

Running sync --dry-run, the output baffled me—

━━━ CodeBuddy ━━━
  ✅ common.mdc
  ✅ typescript.mdc
  ✅ security.mdc
━━━ Cursor ━━━
  ✅ common.mdc
  ✅ typescript.mdc
  ✅ security.mdc
━━━ CodeBuddy ━━━   ← Why again?
  ✅ common.mdc
  ✅ typescript.mdc
  ✅ security.mdc
━━━ Cursor ━━━       ← ???
  ✅ common.mdc
  ✅ typescript.mdc
  ✅ security.mdc

✨ Sync complete! 12 rules total, 0 skills

The ide array was iterated 4 times, common.mdc was processed 4 times—supposed to be 2 IDEs, why did 4 appear?

After checking the defu documentation for a while, the reason was hidden in its default merge strategy—defu's default behavior for array fields is "concatenation" not "overwrite".

// User config
{ ide: ['codebuddy', 'cursor'] }

// Default config (also codebuddy, cursor)
{ ide: ['codebuddy', 'cursor'] }

// defu default merge
{ ide: ['codebuddy', 'cursor', 'codebuddy', 'cursor'] }  // Length becomes 4!

The solution: abandon c12's defaults option, control the merge logic yourself. Use createDefu to write a custom merger, applying "overwrite" for array fields:

// packages/cli/src/core/config-loader.ts
import { createDefu } from 'defu'

// Custom defu merger: array fields use higher priority source to overwrite, not concatenate
const defuOverrideArrays = createDefu((obj, key, value) => {
  if (Array.isArray(value)) {
    obj[key] = value
    return true
  }
  return undefined
})

export async function loadAIKitConfig(cwd?: string) {
  const { config } = await loadConfig<AIKitConfig>({
    name: 'xingwu-ai-kit',
    cwd: cwd || process.cwd(),
    // Note: intentionally not passing defaults here
  })
  const userConfig = (config || {}) as AIKitConfig
  return defuOverrideArrays(userConfig, DEFAULT_CONFIG) as AIKitConfig
}

After the change, running sync --dry-run:

━━━ CodeBuddy ━━━
  ✅ common.mdc
  ✅ typescript.mdc
  ✅ security.mdc
━━━ Cursor ━━━
  ✅ common.mdc
  ✅ typescript.mdc
  ✅ security.mdc

✨ Sync complete! 6 rules total, 0 skills

Yep, 6 rules, 2 IDEs, normal.

PS: Didn't notice this point the first time reading the defu documentation, almost thought my sync logic was wrong, wasted an evening. A heads-up for others also using c12 / defu—the merge semantics for array fields need to be controlled yourself.

Making Trouble 3.0: Templates Ship Together with the npm Package

The last pitfall, simpler than imagined.

The templates/ directory has 5 rules and 4 skills, originally placed under packages/cli/templates/, which vite build would bundle by default. Later, refactoring to a monorepo moved templates/ to the repository root (needed by CLI / scaffolding / documentation scripts), so vite build no longer recognized it—it only builds things under packages/cli/src/.

The resulting dist/ had no templates/, the published npm package also didn't, and after users installed it, running sync would error:

⚠️  Built-in rule not found: common
⚠️  Built-in rule not found: typescript
⚠️  Built-in rule not found: security

Solution: write a 30-line copy-templates.mjs, manually copy after vite build finishes:

// packages/cli/scripts/copy-templates.mjs
const PKG_ROOT = resolve(__dirname, '..')
const MONOREPO_ROOT = resolve(PKG_ROOT, '../..')
const SRC = resolve(MONOREPO_ROOT, 'templates')   // Repo root templates/
const DEST = resolve(PKG_ROOT, 'dist/templates')   // Follow dist

function copyDir(src, dest) {
  mkdirSync(dest, { recursive: true })
  for (const entry of readdirSync(src)) {
    const srcPath = join(src, entry)
    const destPath = join(dest, entry)
    if (statSync(srcPath).isDirectory()) {
      copyDir(srcPath, destPath)
    } else {
      copyFileSync(srcPath, destPath)
    }
  }
}

copyDir(SRC, DEST)

package.json's build strings it together:

{
  "scripts": {
    "build": "vite build && node scripts/copy-templates.mjs",
    "dev": "vite build --watch & node scripts/copy-templates.mjs --watch"
  }
}

dev mode also supports --watch, templates auto-sync to dist/ when changed.

To ensure getTemplatesDir() can find templates in all runtime scenarios, I listed 4 candidate paths, finding the first existing one by priority:

// packages/cli/src/core/templates.ts
const candidates = [
  resolve(hereDir, 'templates'),                  // ① inside dist (build artifact)
  resolve(hereDir, '../../templates'),            // ② node_modules/xingwu-ai-kit/templates
  resolve(hereDir, '../../../templates'),         // ③ running source directly
  resolve(hereDir, '../../../../templates'),      // ④ monorepo root
]

Actual test npm pack --dry-run verified: all 13 template files are packed into the tarball with dist/templates/... paths, after publishing to npm, node_modules/xingwu-ai-kit/dist/templates/ is the real location of the templates.

Architecture Overview

After the three rounds of making trouble above, the entire chain is clear, organized into a diagram:

flowchart TB
    UC[&#34;📝 xingwu-ai-kit.config.ts<br/><i>ide / rules / skills / mcp</i>&#34;]
    TPL[&#34;📚 templates/<br/><i>5 rules + 4 skills</i>&#34;]

    subgraph CLI[&#34;⚙️ CLI: xingwu-ai-kit sync&#34;]
        L[&#34;config-loader<br/>(c12 + defu)&#34;]
        A[&#34;ide adapters<br/>5 IDE adapters&#34;]
        RL[&#34;rules / skills linker<br/>(safeLink)&#34;]
        MW[&#34;mcp writer<br/>(JSON merge)&#34;]
    end

    subgraph IDE[&#34;🎯 5 IDE Working Directories&#34;]
        CB[&#34;.codebuddy/&#34;]
        CU[&#34;.cursor/&#34;]
        WS[&#34;.windsurf/&#34;]
        TR[&#34;.trae/&#34;]
        QD[&#34;.qoder/&#34;]
    end

    UC --> L --> A
    TPL --> RL
    A --> RL
    A --> MW
    RL -->|symlink| CB
    RL -->|symlink| CU
    RL -->|symlink| WS
    RL -->|symlink| TR
    RL -->|symlink| QD
    MW -->|write mcp.json| CB
    MW -->|write mcp.json| CU
    MW -->|write mcp.json| WS
    MW -->|write mcp.json| TR
    MW -->|write mcp.json| QD

The data flow has only one unidirectional path: User config → Load & Merge → IDE Adapt → Rules/Skills via symlink, MCP via JSON write → 5 IDEs simultaneously populated. Change one config, run sync once, 5 IDEs auto-align.

Final Result

After this round of making trouble, the final xingwu-ai-kit.config.ts looks like this:

/** @type {import('xingwu-ai-kit/config').AIKitConfig} */
export default {
  ide: ['codebuddy', 'cursor', 'windsurf', 'trae', 'qoder'],

  rules: {
    preset: ['common', 'typescript', 'security'],
    overrideStrategy: 'skip',
  },

  skills: {
    preset: ['code-review', 'figma2code', 'git-commit', 'refactor'],
  },

  mcp: {
    servers: {
      FramelinkFigmaMCP: {
        type: 'stdio',
        command: 'npx',
        args: ['-y', 'figma-developer-mcp', '--stdio'],
        env: { FIGMA_API_KEY: process.env.FIGMA_API_KEY || '' },
      },
      context7: {
        type: 'http',
        url: 'https://mcp.context7.com/mcp',
      },
    },
    target: 'user',
    onConflict: 'skip',
    preserveUserServers: true,
  },
}

Run npx xingwu-ai-kit sync, 5 IDEs populate simultaneously, rules / skills / MCP all in one go:

Starting sync...

━━━ CodeBuddy ━━━
📏 Rules:
  ✅ common.mdc
  ✅ typescript.mdc
  ✅ security.mdc
🧠 Skills:
  ✅ code-review
  ✅ figma2code
  ✅ git-commit
  ✅ refactor

━━━ Cursor / Windsurf / Trae / Qoder ... ━━━   (same as above)

🔌 MCP Servers:
  [CodeBuddy] Added: FramelinkFigmaMCP, context7
  [Cursor] Added: FramelinkFigmaMCP, context7
  ...

✨ Sync complete! 15 rules total, 20 skills

Screenshot:

Clipboard_Screenshot_1783477517.png

Colleague: How do we handle upgrading the standards this time? Me: Change xingwu-ai-kit.config.ts, commit to Git, everyone pull and run sync once more. Colleague: That's it? Me: That's it. Colleague: Damn, as convenient as prettier. Me: Told ya.

Summary

  1. For AI context infrastructure, there's no "de facto standard" in the community, where there's a gap, there's an opportunity—the core idea of xingwu-ai-kit is "declarative config + adapters + symlink sync";
  2. safeLink's symlink + conflict strategy, JSDoc config's zero-dependency, createDefu custom merging, are three "sweet when used right" little tricks, hope they bring some help to everyone;
  3. Currently 5 IDEs (CodeBuddy / Cursor / Windsurf / Trae / Qoder) work out of the box, Claude Code / VSCode Copilot are still on the way ^_^.

xingwu-ai-kit Portal, welcome to try, raise issues, contribute rule templates.