Skip to content

Commit e60ff53

Browse files
akeyzclay-goodclaude
authored
update Kimi CLI to Kimi Code (#1208)
* update Kimi CLI to Kimi Code * feat(migration): migrate OpenSpec skills from legacy .kimi to .kimi-code Renaming the Kimi skillsDir stranded OpenSpec-managed skills under .kimi/skills: update and cleanup only inspect current AI_TOOLS paths, so old installs would never be detected or refreshed again. Add a legacy skillsDir migration (run by init and update before tool detection) that moves openspec-* skill directories to .kimi-code/skills, preserves user files, and removes the legacy directories only when empty. Keep .kimi as a detection path and cover the migration with focused init and update tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(specs): update cli-init Kimi scenario to .kimi-code Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Clay Good <hi@claygood.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent d423a59 commit e60ff53

10 files changed

Lines changed: 181 additions & 14 deletions

File tree

docs/commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,7 @@ Different AI tools use slightly different command syntax. Use the format that ma
671671
| Windsurf | `/opsx-propose`, `/opsx-apply` |
672672
| Copilot (IDE) | `/opsx-propose`, `/opsx-apply` |
673673
| Oh My Pi | `/opsx-propose`, `/opsx-apply` |
674-
| Kimi CLI | Skill-based invocations such as `/skill:openspec-propose`, `/skill:openspec-apply-change` (no generated `opsx-*` command files) |
674+
| Kimi Code | Skill-based invocations such as `/skill:openspec-propose`, `/skill:openspec-apply-change` (no generated `opsx-*` command files) |
675675
| Trae | `/opsx-propose`, `/opsx-apply` |
676676

677677
The intent is the same across tools, but how commands are surfaced can differ by integration.

docs/supported-tools.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-arch
4141
| iFlow (`iflow`) | `.iflow/skills/openspec-*/SKILL.md` | `.iflow/commands/opsx-<id>.md` |
4242
| Junie (`junie`) | `.junie/skills/openspec-*/SKILL.md` | `.junie/commands/opsx-<id>.md` |
4343
| Kilo Code (`kilocode`) | `.kilocode/skills/openspec-*/SKILL.md` | `.kilocode/workflows/opsx-<id>.md` |
44-
| Kimi CLI (`kimi`) | `.kimi/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/skill:openspec-*` invocations) |
44+
| Kimi Code (`kimi`) | `.kimi-code/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/skill:openspec-*` invocations) |
4545
| Kiro (`kiro`) | `.kiro/skills/openspec-*/SKILL.md` | `.kiro/prompts/opsx-<id>.prompt.md` |
4646
| Lingma (`lingma`) | `.lingma/skills/openspec-*/SKILL.md` | `.lingma/commands/opsx/<id>.md` |
4747
| Mistral Vibe (`vibe`) | `.vibe/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |

openspec/specs/ai-tool-paths/spec.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ The `AI_TOOLS` array SHALL include `skillsDir` for tools that support the Agent
3737
- **WHEN** looking up the `windsurf` tool
3838
- **THEN** `skillsDir` SHALL be `.windsurf`
3939

40-
#### Scenario: Kimi CLI paths defined
40+
#### Scenario: Kimi Code paths defined
4141

4242
- **WHEN** looking up the `kimi` tool
43-
- **THEN** `skillsDir` SHALL be `.kimi`
43+
- **THEN** `skillsDir` SHALL be `.kimi-code`
44+
- **AND** OpenSpec-managed skills remaining under the legacy `.kimi/skills` directory SHALL be migrated to `.kimi-code/skills` during init and update, preserving user files
4445

4546
#### Scenario: Tools without skillsDir
4647

openspec/specs/cli-init/spec.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,10 @@ The command SHALL generate opsx slash commands only for selected tools that have
226226
- **AND** command-file generation SHALL be skipped for that tool
227227
- **AND** the command output SHALL include `Commands skipped for: <tool-id> (no adapter)`
228228

229-
#### Scenario: Kimi CLI skips command-file generation
229+
#### Scenario: Kimi Code skips command-file generation
230230

231-
- **WHEN** the user selects Kimi CLI during initialization
232-
- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.kimi'`
231+
- **WHEN** the user selects Kimi Code during initialization
232+
- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.kimi-code'`
233233
- **AND** command-file generation SHALL be skipped because no Kimi adapter is registered
234234

235235
### Requirement: Config File Generation

src/core/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const AI_TOOLS: AIToolOption[] = [
3838
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' },
3939
{ name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie' },
4040
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
41-
{ name: 'Kimi CLI', value: 'kimi', available: true, successLabel: 'Kimi CLI', skillsDir: '.kimi' },
41+
{ name: 'Kimi Code', value: 'kimi', available: true, successLabel: 'Kimi Code', skillsDir: '.kimi-code', detectionPaths: ['.kimi-code', '.kimi'] },
4242
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
4343
{ name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' },
4444
{ name: 'Mistral Vibe', value: 'vibe', available: true, successLabel: 'Mistral Vibe', skillsDir: '.vibe' },

src/core/init.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import {
4646
import { getGlobalConfig, type Delivery, type Profile } from './global-config.js';
4747
import { getProfileWorkflows, CORE_WORKFLOWS, ALL_WORKFLOWS } from './profiles.js';
4848
import { getAvailableTools } from './available-tools.js';
49-
import { migrateIfNeeded } from './migration.js';
49+
import { migrateIfNeeded, migrateLegacySkillDirs } from './migration.js';
5050

5151
const require = createRequire(import.meta.url);
5252
const { version: OPENSPEC_VERSION } = require('../../package.json');
@@ -143,6 +143,10 @@ export class InitCommand {
143143
// Check for legacy artifacts and handle cleanup
144144
await this.handleLegacyCleanup(projectPath, extendMode);
145145

146+
// Migrate OpenSpec-managed skills left in renamed tool directories
147+
// (e.g. .kimi -> .kimi-code) before detection so they stay recognized.
148+
migrateLegacySkillDirs(projectPath);
149+
146150
// Detect available tools in the project (task 7.1)
147151
const detectedTools = getAvailableTools(projectPath);
148152

src/core/migration.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,94 @@
55
* Called by both init and update commands before profile resolution.
66
*/
77

8-
import type { AIToolOption } from './config.js';
8+
import { AI_TOOLS, type AIToolOption } from './config.js';
99
import { getGlobalConfig, getGlobalConfigPath, saveGlobalConfig, type Delivery } from './global-config.js';
1010
import { CommandAdapterRegistry } from './command-generation/index.js';
1111
import { WORKFLOW_TO_SKILL_DIR } from './profile-sync-drift.js';
1212
import { ALL_WORKFLOWS } from './profiles.js';
1313
import path from 'path';
1414
import * as fs from 'fs';
1515

16+
/**
17+
* Former skillsDir locations for tools whose directory was renamed.
18+
* OpenSpec-managed skill directories left in these locations are migrated
19+
* to the tool's current skillsDir; user files are never touched.
20+
*/
21+
export const LEGACY_SKILLS_DIRS: Record<string, string[]> = {
22+
// Kimi CLI became Kimi Code and moved from .kimi to .kimi-code
23+
kimi: ['.kimi'],
24+
};
25+
26+
export interface LegacySkillsMigration {
27+
toolId: string;
28+
/** Legacy tool root, e.g. '.kimi' */
29+
from: string;
30+
/** Current tool root, e.g. '.kimi-code' */
31+
to: string;
32+
/** Number of skill directories moved or removed */
33+
movedSkillDirs: number;
34+
}
35+
36+
/**
37+
* Moves OpenSpec-managed skill directories (openspec-*) from a tool's legacy
38+
* skillsDir to its current one. When the destination already exists the legacy
39+
* copy is removed instead. Legacy directories are deleted only when left empty,
40+
* so user files under the old location are preserved.
41+
*/
42+
export function migrateLegacySkillDirs(projectPath: string): LegacySkillsMigration[] {
43+
const migrations: LegacySkillsMigration[] = [];
44+
45+
for (const tool of AI_TOOLS) {
46+
if (!tool.skillsDir) continue;
47+
48+
for (const legacyRoot of LEGACY_SKILLS_DIRS[tool.value] ?? []) {
49+
if (legacyRoot === tool.skillsDir) continue;
50+
const legacySkillsDir = path.join(projectPath, legacyRoot, 'skills');
51+
if (!fs.existsSync(legacySkillsDir)) continue;
52+
const currentSkillsDir = path.join(projectPath, tool.skillsDir, 'skills');
53+
let movedSkillDirs = 0;
54+
55+
for (const workflowId of ALL_WORKFLOWS) {
56+
const dirName = WORKFLOW_TO_SKILL_DIR[workflowId];
57+
const source = path.join(legacySkillsDir, dirName);
58+
if (!fs.existsSync(path.join(source, 'SKILL.md'))) continue;
59+
60+
try {
61+
const destination = path.join(currentSkillsDir, dirName);
62+
if (fs.existsSync(destination)) {
63+
fs.rmSync(source, { recursive: true, force: true });
64+
} else {
65+
fs.mkdirSync(currentSkillsDir, { recursive: true });
66+
fs.renameSync(source, destination);
67+
}
68+
movedSkillDirs++;
69+
} catch {
70+
// Leave the legacy directory in place if it cannot be moved
71+
}
72+
}
73+
74+
removeDirIfEmpty(legacySkillsDir);
75+
removeDirIfEmpty(path.join(projectPath, legacyRoot));
76+
77+
if (movedSkillDirs > 0) {
78+
migrations.push({ toolId: tool.value, from: legacyRoot, to: tool.skillsDir, movedSkillDirs });
79+
}
80+
}
81+
}
82+
83+
return migrations;
84+
}
85+
86+
function removeDirIfEmpty(dirPath: string): void {
87+
try {
88+
if (fs.readdirSync(dirPath).length === 0) {
89+
fs.rmdirSync(dirPath);
90+
}
91+
} catch {
92+
// Missing or non-empty directory — nothing to do
93+
}
94+
}
95+
1696
interface InstalledWorkflowArtifacts {
1797
workflows: string[];
1898
hasSkills: boolean;

src/core/update.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
import {
4747
scanInstalledWorkflows as scanInstalledWorkflowsShared,
4848
migrateIfNeeded as migrateIfNeededShared,
49+
migrateLegacySkillDirs,
4950
} from './migration.js';
5051

5152
const require = createRequire(import.meta.url);
@@ -88,7 +89,14 @@ export class UpdateCommand {
8889
throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
8990
}
9091

91-
// 2. Perform one-time migration if needed before any legacy upgrade generation.
92+
// 2. Migrate OpenSpec-managed skills left in renamed tool directories
93+
// (e.g. .kimi -> .kimi-code) so they stay detected and get refreshed,
94+
// then perform the one-time profile migration if needed before any
95+
// legacy upgrade generation.
96+
for (const migration of migrateLegacySkillDirs(resolvedProjectPath)) {
97+
console.log(chalk.dim(`Migrated ${migration.movedSkillDirs} skill director${migration.movedSkillDirs === 1 ? 'y' : 'ies'}: ${migration.from}/skills → ${migration.to}/skills`));
98+
}
99+
92100
// Use detected tool directories to preserve existing opsx skills/commands.
93101
const detectedTools = getAvailableTools(resolvedProjectPath);
94102
migrateIfNeededShared(resolvedProjectPath, detectedTools);

test/core/init.test.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ describe('InitCommand', () => {
170170
expect(await fileExists(skillFile)).toBe(true);
171171
});
172172

173-
it('should support Kimi CLI as an adapterless skills-only tool', async () => {
173+
it('should support Kimi Code as an adapterless skills-only tool', async () => {
174174
saveGlobalConfig({
175175
featureFlags: {},
176176
profile: 'core',
@@ -180,10 +180,10 @@ describe('InitCommand', () => {
180180
const initCommand = new InitCommand({ tools: 'kimi', force: true });
181181
await initCommand.execute(testDir);
182182

183-
const skillFile = path.join(testDir, '.kimi', 'skills', 'openspec-explore', 'SKILL.md');
183+
const skillFile = path.join(testDir, '.kimi-code', 'skills', 'openspec-explore', 'SKILL.md');
184184
expect(await fileExists(skillFile)).toBe(true);
185185

186-
const commandsDir = path.join(testDir, '.kimi', 'commands');
186+
const commandsDir = path.join(testDir, '.kimi-code', 'commands');
187187
expect(await directoryExists(commandsDir)).toBe(false);
188188

189189
const logCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.flat().map(String);
@@ -194,6 +194,27 @@ describe('InitCommand', () => {
194194
).toBe(true);
195195
});
196196

197+
it('should migrate OpenSpec skills from legacy .kimi to .kimi-code during init', async () => {
198+
const legacySkillDir = path.join(testDir, '.kimi', 'skills', 'openspec-explore');
199+
await fs.mkdir(legacySkillDir, { recursive: true });
200+
await fs.writeFile(
201+
path.join(legacySkillDir, 'SKILL.md'),
202+
`---\nname: openspec-explore\nmetadata:\n author: openspec\n version: "0.9"\n---\n\nOld instructions content\n`
203+
);
204+
await fs.writeFile(path.join(testDir, '.kimi', 'config.toml'), 'user config');
205+
206+
const initCommand = new InitCommand({ tools: 'kimi', force: true });
207+
await initCommand.execute(testDir);
208+
209+
// Regenerated in the new location, legacy managed skill removed
210+
const newSkill = path.join(testDir, '.kimi-code', 'skills', 'openspec-explore', 'SKILL.md');
211+
expect(await fileExists(newSkill)).toBe(true);
212+
expect(await directoryExists(legacySkillDir)).toBe(false);
213+
214+
// User files under .kimi are preserved
215+
expect(await fileExists(path.join(testDir, '.kimi', 'config.toml'))).toBe(true);
216+
});
217+
197218
it('should create both skills and commands for Trae with adapter', async () => {
198219
saveGlobalConfig({
199220
configuredTools: [],

test/core/update.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,59 @@ Old instructions content
140140
consoleSpy.mockRestore();
141141
});
142142

143+
it('should migrate OpenSpec skills from legacy .kimi to .kimi-code, preserving user files', async () => {
144+
// Managed skill in the legacy Kimi CLI location
145+
const legacySkillDir = path.join(testDir, '.kimi', 'skills', 'openspec-explore');
146+
await fs.mkdir(legacySkillDir, { recursive: true });
147+
await fs.writeFile(
148+
path.join(legacySkillDir, 'SKILL.md'),
149+
`---\nname: openspec-explore\nmetadata:\n author: openspec\n version: "0.9"\n---\n\nOld instructions content\n`
150+
);
151+
152+
// User-owned files in the legacy location that must be preserved
153+
const userSkillDir = path.join(testDir, '.kimi', 'skills', 'my-custom-skill');
154+
await fs.mkdir(userSkillDir, { recursive: true });
155+
await fs.writeFile(path.join(userSkillDir, 'SKILL.md'), 'user skill');
156+
await fs.writeFile(path.join(testDir, '.kimi', 'config.toml'), 'user config');
157+
158+
const consoleSpy = vi.spyOn(console, 'log');
159+
160+
await updateCommand.execute(testDir);
161+
162+
// Managed skill migrated to .kimi-code and refreshed by the update
163+
const migratedSkill = await fs.readFile(
164+
path.join(testDir, '.kimi-code', 'skills', 'openspec-explore', 'SKILL.md'),
165+
'utf-8'
166+
);
167+
expect(migratedSkill).toContain('name: openspec-explore');
168+
expect(migratedSkill).not.toContain('Old instructions content');
169+
170+
// Legacy managed skill is gone; user files stay where they were
171+
await expect(fs.access(legacySkillDir)).rejects.toThrow();
172+
expect(await fs.readFile(path.join(userSkillDir, 'SKILL.md'), 'utf-8')).toBe('user skill');
173+
expect(await fs.readFile(path.join(testDir, '.kimi', 'config.toml'), 'utf-8')).toBe('user config');
174+
175+
const logCalls = consoleSpy.mock.calls.flat().map(String);
176+
expect(logCalls.some((entry) => entry.includes('.kimi/skills') && entry.includes('.kimi-code/skills'))).toBe(true);
177+
178+
consoleSpy.mockRestore();
179+
});
180+
181+
it('should remove the legacy .kimi directory entirely when it only held OpenSpec skills', async () => {
182+
const legacySkillDir = path.join(testDir, '.kimi', 'skills', 'openspec-explore');
183+
await fs.mkdir(legacySkillDir, { recursive: true });
184+
await fs.writeFile(
185+
path.join(legacySkillDir, 'SKILL.md'),
186+
`---\nname: openspec-explore\nmetadata:\n author: openspec\n version: "0.9"\n---\n\nOld instructions content\n`
187+
);
188+
189+
await updateCommand.execute(testDir);
190+
191+
await expect(fs.access(path.join(testDir, '.kimi'))).rejects.toThrow();
192+
const migratedSkill = path.join(testDir, '.kimi-code', 'skills', 'openspec-explore', 'SKILL.md');
193+
await expect(fs.access(migratedSkill)).resolves.toBeUndefined();
194+
});
195+
143196
it('should update core profile skill files when tool is configured', async () => {
144197
// Set up a configured tool with one skill directory
145198
const skillsDir = path.join(testDir, '.claude', 'skills');

0 commit comments

Comments
 (0)