From 4507d6703d4beb117553bfcab0b14a3b82084940 Mon Sep 17 00:00:00 2001 From: "linton.cao" <> Date: Fri, 6 Feb 2026 03:10:55 +0800 Subject: [PATCH 1/3] feat(core): add Lingma IDE support --- docs/supported-tools.md | 3 +- src/core/command-generation/adapters/index.ts | 1 + .../command-generation/adapters/lingma.ts | 33 +++++++++++++++++++ src/core/command-generation/registry.ts | 2 ++ src/core/config.ts | 1 + src/core/legacy-cleanup.ts | 1 + test/core/command-generation/adapters.test.ts | 30 ++++++++++++++++- 7 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/core/command-generation/adapters/lingma.ts diff --git a/docs/supported-tools.md b/docs/supported-tools.md index 2f9b61526..d605ada69 100644 --- a/docs/supported-tools.md +++ b/docs/supported-tools.md @@ -28,6 +28,7 @@ For each tool you select, OpenSpec installs: | Gemini CLI | `.gemini/skills/` | `.gemini/commands/opsx/` | | GitHub Copilot | `.github/skills/` | `.github/prompts/` | | iFlow | `.iflow/skills/` | `.iflow/commands/` | +| Lingma | `.lingma/skills/` | `.lingma/commands/` | | Kilo Code | `.kilocode/skills/` | `.kilocode/workflows/` | | OpenCode | `.opencode/skills/` | `.opencode/command/` | | Qoder | `.qoder/skills/` | `.qoder/commands/opsx/` | @@ -53,7 +54,7 @@ openspec init --tools all openspec init --tools none ``` -**Available tool IDs:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codebuddy`, `codex`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `opencode`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf` +**Available tool IDs:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codebuddy`, `codex`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `lingma`, `kilocode`, `opencode`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf` ## What Gets Installed diff --git a/src/core/command-generation/adapters/index.ts b/src/core/command-generation/adapters/index.ts index 83de1f0d2..5eede0353 100644 --- a/src/core/command-generation/adapters/index.ts +++ b/src/core/command-generation/adapters/index.ts @@ -20,6 +20,7 @@ export { geminiAdapter } from './gemini.js'; export { githubCopilotAdapter } from './github-copilot.js'; export { iflowAdapter } from './iflow.js'; export { kilocodeAdapter } from './kilocode.js'; +export { lingmaAdapter } from './lingma.js'; export { opencodeAdapter } from './opencode.js'; export { qoderAdapter } from './qoder.js'; export { qwenAdapter } from './qwen.js'; diff --git a/src/core/command-generation/adapters/lingma.ts b/src/core/command-generation/adapters/lingma.ts new file mode 100644 index 000000000..893c4c3d9 --- /dev/null +++ b/src/core/command-generation/adapters/lingma.ts @@ -0,0 +1,33 @@ +/** + * Lingma Command Adapter + * + * Formats commands for Lingma following its skill specification. + */ + +import path from 'path'; +import type { CommandContent, ToolCommandAdapter } from '../types.js'; + +/** + * Lingma adapter for command generation. + * File path: .lingma/commands/opsx-.md + * Structure: Command markdown files with YAML frontmatter + */ +export const lingmaAdapter: ToolCommandAdapter = { + toolId: 'lingma', + + getFilePath(commandId: string): string { + return path.join('.lingma', 'commands', `opsx-${commandId}.md`); + }, + + formatFile(content: CommandContent): string { + return `--- +name: /opsx-${content.id} +id: opsx-${content.id} +category: ${content.category} +description: ${content.description} +--- + +${content.body} +`; + }, +}; \ No newline at end of file diff --git a/src/core/command-generation/registry.ts b/src/core/command-generation/registry.ts index f99edac61..22f942419 100644 --- a/src/core/command-generation/registry.ts +++ b/src/core/command-generation/registry.ts @@ -22,6 +22,7 @@ import { geminiAdapter } from './adapters/gemini.js'; import { githubCopilotAdapter } from './adapters/github-copilot.js'; import { iflowAdapter } from './adapters/iflow.js'; import { kilocodeAdapter } from './adapters/kilocode.js'; +import { lingmaAdapter } from './adapters/lingma.js'; import { opencodeAdapter } from './adapters/opencode.js'; import { qoderAdapter } from './adapters/qoder.js'; import { qwenAdapter } from './adapters/qwen.js'; @@ -52,6 +53,7 @@ export class CommandAdapterRegistry { CommandAdapterRegistry.register(githubCopilotAdapter); CommandAdapterRegistry.register(iflowAdapter); CommandAdapterRegistry.register(kilocodeAdapter); + CommandAdapterRegistry.register(lingmaAdapter); CommandAdapterRegistry.register(opencodeAdapter); CommandAdapterRegistry.register(qoderAdapter); CommandAdapterRegistry.register(qwenAdapter); diff --git a/src/core/config.ts b/src/core/config.ts index 05f1c3cf1..1564b904b 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -39,6 +39,7 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' }, { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' }, { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' }, + { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' }, { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf', skillsDir: '.windsurf' }, { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' } ]; diff --git a/src/core/legacy-cleanup.ts b/src/core/legacy-cleanup.ts index 498e49dd0..7505095d9 100644 --- a/src/core/legacy-cleanup.ts +++ b/src/core/legacy-cleanup.ts @@ -52,6 +52,7 @@ export const LEGACY_SLASH_COMMAND_PATHS: Record { }); }); + describe('lingmaAdapter', () => { + it('should have correct toolId', () => { + expect(lingmaAdapter.toolId).toBe('lingma'); + }); + + it('should generate correct file path', () => { + const filePath = lingmaAdapter.getFilePath('explore'); + expect(filePath).toBe(path.join('.lingma', 'commands', 'opsx-explore.md')); + }); + + it('should generate correct file paths for different commands', () => { + expect(lingmaAdapter.getFilePath('new')).toBe(path.join('.lingma', 'commands', 'opsx-new.md')); + expect(lingmaAdapter.getFilePath('apply')).toBe(path.join('.lingma', 'commands', 'opsx-apply.md')); + }); + + it('should format file with name, id, category, and description', () => { + const output = lingmaAdapter.formatFile(sampleContent); + expect(output).toContain('---\n'); + expect(output).toContain('name: /opsx-explore'); + expect(output).toContain('id: opsx-explore'); + expect(output).toContain('category: Workflow'); + expect(output).toContain('description: Enter explore mode for thinking'); + expect(output).toContain('---\n\n'); + expect(output).toContain('This is the command body.\n\nWith multiple lines.'); + }); + }); + describe('cross-platform path handling', () => { it('Claude adapter uses path.join for paths', () => { // path.join handles platform-specific separators @@ -566,7 +594,7 @@ describe('command-generation/adapters', () => { amazonQAdapter, antigravityAdapter, auggieAdapter, clineAdapter, codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter, crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter, - iflowAdapter, kilocodeAdapter, opencodeAdapter, qoderAdapter, + iflowAdapter, kilocodeAdapter, lingmaAdapter, opencodeAdapter, qoderAdapter, qwenAdapter, roocodeAdapter ]; for (const adapter of adapters) { From 03970b6fa27f856f523567b1a0a5ff1b19d81169 Mon Sep 17 00:00:00 2001 From: "linton.cao" <> Date: Fri, 6 Feb 2026 17:11:29 +0800 Subject: [PATCH 2/3] refactor(core): Updated the directory structure of the Lingma command. --- docs/supported-tools.md | 4 ++-- src/core/command-generation/adapters/lingma.ts | 14 +++++++------- src/core/config.ts | 2 +- src/core/legacy-cleanup.ts | 2 +- test/core/command-generation/adapters.test.ts | 14 +++++++------- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/supported-tools.md b/docs/supported-tools.md index d605ada69..f4f77dbf0 100644 --- a/docs/supported-tools.md +++ b/docs/supported-tools.md @@ -28,8 +28,8 @@ For each tool you select, OpenSpec installs: | Gemini CLI | `.gemini/skills/` | `.gemini/commands/opsx/` | | GitHub Copilot | `.github/skills/` | `.github/prompts/` | | iFlow | `.iflow/skills/` | `.iflow/commands/` | -| Lingma | `.lingma/skills/` | `.lingma/commands/` | | Kilo Code | `.kilocode/skills/` | `.kilocode/workflows/` | +| Lingma | `.lingma/skills/` | `.lingma/commands/opsx/` | | OpenCode | `.opencode/skills/` | `.opencode/command/` | | Qoder | `.qoder/skills/` | `.qoder/commands/opsx/` | | Qwen Code | `.qwen/skills/` | `.qwen/commands/` | @@ -54,7 +54,7 @@ openspec init --tools all openspec init --tools none ``` -**Available tool IDs:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codebuddy`, `codex`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `lingma`, `kilocode`, `opencode`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf` +**Available tool IDs:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codebuddy`, `codex`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `lingma`, `opencode`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf` ## What Gets Installed diff --git a/src/core/command-generation/adapters/lingma.ts b/src/core/command-generation/adapters/lingma.ts index 893c4c3d9..12dbbc3a0 100644 --- a/src/core/command-generation/adapters/lingma.ts +++ b/src/core/command-generation/adapters/lingma.ts @@ -9,25 +9,25 @@ import type { CommandContent, ToolCommandAdapter } from '../types.js'; /** * Lingma adapter for command generation. - * File path: .lingma/commands/opsx-.md + * File path: .lingma/commands/opsx/.md * Structure: Command markdown files with YAML frontmatter */ export const lingmaAdapter: ToolCommandAdapter = { toolId: 'lingma', getFilePath(commandId: string): string { - return path.join('.lingma', 'commands', `opsx-${commandId}.md`); + return path.join('.lingma', 'commands', 'opsx', `${commandId}.md`); }, formatFile(content: CommandContent): string { + const tagsStr = content.tags.join(', '); return `--- -name: /opsx-${content.id} -id: opsx-${content.id} -category: ${content.category} +name: ${content.name} description: ${content.description} +category: ${content.category} +tags: [${tagsStr}] --- -${content.body} -`; +${content.body}`; }, }; \ No newline at end of file diff --git a/src/core/config.ts b/src/core/config.ts index 1564b904b..319b8d032 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -34,12 +34,12 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github' }, { name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' }, { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' }, + { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' }, { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' }, { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' }, { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' }, { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' }, { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' }, - { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' }, { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf', skillsDir: '.windsurf' }, { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' } ]; diff --git a/src/core/legacy-cleanup.ts b/src/core/legacy-cleanup.ts index 7505095d9..2b70da8a6 100644 --- a/src/core/legacy-cleanup.ts +++ b/src/core/legacy-cleanup.ts @@ -33,6 +33,7 @@ export const LEGACY_SLASH_COMMAND_PATHS: Record { it('should generate correct file path', () => { const filePath = lingmaAdapter.getFilePath('explore'); - expect(filePath).toBe(path.join('.lingma', 'commands', 'opsx-explore.md')); + expect(filePath).toBe(path.join('.lingma', 'commands', 'opsx', 'explore.md')); }); it('should generate correct file paths for different commands', () => { - expect(lingmaAdapter.getFilePath('new')).toBe(path.join('.lingma', 'commands', 'opsx-new.md')); - expect(lingmaAdapter.getFilePath('apply')).toBe(path.join('.lingma', 'commands', 'opsx-apply.md')); + expect(lingmaAdapter.getFilePath('new')).toBe(path.join('.lingma', 'commands', 'opsx', 'new.md')); + expect(lingmaAdapter.getFilePath('apply')).toBe(path.join('.lingma', 'commands', 'opsx', 'apply.md')); }); - it('should format file with name, id, category, and description', () => { + it('should format file with name, description, category, and tags', () => { const output = lingmaAdapter.formatFile(sampleContent); expect(output).toContain('---\n'); - expect(output).toContain('name: /opsx-explore'); - expect(output).toContain('id: opsx-explore'); - expect(output).toContain('category: Workflow'); + expect(output).toContain('name: OpenSpec Explore'); expect(output).toContain('description: Enter explore mode for thinking'); + expect(output).toContain('category: Workflow'); + expect(output).toContain('tags: [workflow, explore, experimental]'); expect(output).toContain('---\n\n'); expect(output).toContain('This is the command body.\n\nWith multiple lines.'); }); From 37e51e0cd5513d1bf5412f85099cff3de7f1a70f Mon Sep 17 00:00:00 2001 From: "linton.cao" <> Date: Fri, 6 Feb 2026 17:45:25 +0800 Subject: [PATCH 3/3] fix(core): remove incorrect legacy cleanup entry for Lingma opsx directory --- docs/cli.md | 2 +- src/core/legacy-cleanup.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index e064e9dac..21fe18412 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -84,7 +84,7 @@ openspec init [path] [options] | `--tools ` | Configure AI tools non-interactively. Use `all`, `none`, or comma-separated list | | `--force` | Auto-cleanup legacy files without prompting | -**Supported tools:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `opencode`, `qoder`, `qwen`, `roocode`, `windsurf` +**Supported tools:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `lingma`, `opencode`, `qoder`, `qwen`, `roocode`, `windsurf` **Examples:** diff --git a/src/core/legacy-cleanup.ts b/src/core/legacy-cleanup.ts index 2b70da8a6..498e49dd0 100644 --- a/src/core/legacy-cleanup.ts +++ b/src/core/legacy-cleanup.ts @@ -33,7 +33,6 @@ export const LEGACY_SLASH_COMMAND_PATHS: Record