Skip to content

Commit ede5be4

Browse files
committed
chore: prepare 1.9.0 release
1 parent cdd3df0 commit ede5be4

33 files changed

Lines changed: 393 additions & 172 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.9.0] - 2025-11-15
9+
10+
### ⚠️ Breaking Changes / Migrations
11+
- Standardized flat-provider command filenames to `clavix-<command>` and relocated Cline workflows to `.clinerules/workflows/`; legacy filenames are auto-detected with an opt-out cleanup prompt during `clavix init` and `clavix update`.
12+
13+
### ✨ Enhancements
14+
- Added adapter filename overrides and shared template loader so all providers respect the new naming scheme while keeping namespaced folders (e.g., `.claude/commands/clavix/`).
15+
- Implemented legacy command cleanup utilities reused by init/update flows, preserving provider-specific formatting and supporting Gemini/Qwen namespace opt-outs.
16+
17+
### 🧪 Testing
18+
- `NODE_OPTIONS="--localstorage-file=.jest-localstorage" npm test`
19+
820
## [1.8.3] - 2025-11-15
921

1022
### 🐛 Fixes

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"name": "clavix",
3-
"version": "1.8.3",
3+
"version": "1.9.0",
44
"description": "AI prompt improvement and PRD generation CLI tool for developers",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
77
"bin": {
8-
"clavix": "./bin/clavix.js"
8+
"clavix": "bin/clavix.js"
99
},
1010
"scripts": {
1111
"build": "tsc && npm run copy-templates",

src/cli/commands/init.ts

Lines changed: 50 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { ClavixConfig, DEFAULT_CONFIG } from '../../types/config';
1212
import { CommandTemplate, AgentAdapter } from '../../types/agent';
1313
import { GeminiAdapter } from '../../core/adapters/gemini-adapter';
1414
import { QwenAdapter } from '../../core/adapters/qwen-adapter';
15-
import { parseTomlSlashCommand } from '../../utils/toml-templates';
15+
import { loadCommandTemplates } from '../../utils/template-loader';
16+
import { collectLegacyCommandFiles } from '../../utils/legacy-command-cleanup';
1617

1718
export default class Init extends Command {
1819
static description = 'Initialize Clavix in the current project';
@@ -60,7 +61,7 @@ export default class Init extends Command {
6061
value: 'claude-code',
6162
},
6263
{
63-
name: 'Cline (.cline/workflows/)',
64+
name: 'Cline (.clinerules/workflows/)',
6465
value: 'cline',
6566
},
6667
{
@@ -247,21 +248,24 @@ export default class Init extends Command {
247248
}
248249
}
249250

250-
// Migrate from old command structure if needed (Claude Code only)
251-
if (providerName === 'claude-code') {
252-
await this.migrateOldCommands(adapter);
253-
}
254-
255251
// Generate slash commands
256252
const generatedTemplates = await this.generateSlashCommands(adapter);
257253

254+
await this.handleLegacyCommands(adapter, generatedTemplates);
255+
258256
if (adapter.name === 'gemini' || adapter.name === 'qwen') {
259257
const commandPath = adapter.getCommandPath();
260258
const isNamespaced = commandPath.endsWith(path.join('commands', 'clavix'));
261259
const namespace = isNamespaced ? path.basename(commandPath) : undefined;
262-
const commandNames = generatedTemplates.map((template) =>
263-
isNamespaced ? `/${namespace}:${template.name}` : `/${template.name}`
264-
);
260+
const commandNames = generatedTemplates.map((template) => {
261+
if (isNamespaced) {
262+
return `/${namespace}:${template.name}`;
263+
}
264+
265+
const filename = adapter.getTargetFilename(template.name);
266+
const slashName = filename.slice(0, -adapter.fileExtension.length);
267+
return `/${slashName}`;
268+
});
265269

266270
console.log(chalk.green(` → Registered ${commandNames.join(', ')}`));
267271
console.log(chalk.gray(` Commands saved to ${commandPath}`));
@@ -371,97 +375,59 @@ See documentation for template format details.
371375
}
372376

373377
private async generateSlashCommands(adapter: AgentAdapter): Promise<CommandTemplate[]> {
374-
const templateDir = path.join(__dirname, '../../templates/slash-commands', adapter.name);
375-
const files = await FileSystem.listFiles(templateDir);
376-
const extension = adapter.fileExtension;
377-
const commandFiles = files.filter((file) => file.endsWith(extension));
378-
379-
const templates: CommandTemplate[] = [];
380-
381-
for (const file of commandFiles) {
382-
const content = await FileSystem.readFile(path.join(templateDir, file));
383-
const name = file.slice(0, -extension.length);
384-
385-
if (extension === '.toml') {
386-
const parsed = parseTomlSlashCommand(content, name, adapter.name);
387-
templates.push({
388-
name,
389-
content: parsed.prompt,
390-
description: parsed.description,
391-
});
392-
} else {
393-
templates.push({
394-
name,
395-
content,
396-
description: this.extractDescription(content),
397-
});
398-
}
399-
}
378+
const templates = await loadCommandTemplates(adapter);
400379

401380
await adapter.generateCommands(templates);
402381
return templates;
403382
}
404383

405-
private async injectDocumentation(adapter: AgentAdapter): Promise<void> {
406-
// Inject AGENTS.md
407-
const agentsContent = DocInjector.getDefaultAgentsContent();
408-
await DocInjector.injectBlock('AGENTS.md', this.extractClavixBlock(agentsContent));
409-
410-
// Inject CLAUDE.md if Claude Code selected
411-
if (adapter.name === 'claude-code') {
412-
const claudeContent = DocInjector.getDefaultClaudeContent();
413-
await DocInjector.injectBlock('CLAUDE.md', this.extractClavixBlock(claudeContent));
414-
}
415-
}
416-
417-
private async migrateOldCommands(_adapter: AgentAdapter): Promise<void> {
418-
// Check for old command structure (.claude/commands/clavix:*.md)
419-
const oldCommandsPath = '.claude/commands';
384+
private async handleLegacyCommands(adapter: AgentAdapter, templates: CommandTemplate[]): Promise<void> {
385+
const commandNames = templates.map((template) => template.name);
386+
const legacyFiles = await collectLegacyCommandFiles(adapter, commandNames);
420387

421-
if (!await FileSystem.exists(oldCommandsPath)) {
388+
if (legacyFiles.length === 0) {
422389
return;
423390
}
424391

425-
try {
426-
const files = await FileSystem.listFiles(oldCommandsPath, /^clavix:.*\.md$/);
392+
const relativePaths = legacyFiles
393+
.map((file) => path.relative(process.cwd(), file))
394+
.sort((a, b) => a.localeCompare(b));
427395

428-
if (files.length === 0) {
429-
return;
430-
}
431-
432-
console.log(chalk.cyan('🔄 Migrating old command structure...'));
396+
console.log(chalk.gray(` ⚠ Found ${relativePaths.length} deprecated command file(s):`));
397+
for (const file of relativePaths) {
398+
console.log(chalk.gray(` • ${file}`));
399+
}
433400

434-
let removed = 0;
435-
for (const file of files) {
436-
const filePath = path.join(oldCommandsPath, file);
437-
if (await FileSystem.exists(filePath)) {
438-
await FileSystem.remove(filePath);
439-
console.log(chalk.gray(` ✓ Removed old command: ${file}`));
440-
removed++;
441-
}
442-
}
401+
const { removeLegacy } = await inquirer.prompt([
402+
{
403+
type: 'confirm',
404+
name: 'removeLegacy',
405+
message: `Remove deprecated files for ${adapter.displayName}? Functionality is unchanged; filenames are being standardized.`,
406+
default: true,
407+
},
408+
]);
409+
410+
if (!removeLegacy) {
411+
console.log(chalk.gray(' ⊗ Kept legacy files (deprecated naming retained)'));
412+
return;
413+
}
443414

444-
if (removed > 0) {
445-
console.log(chalk.green(` ✓ Migration complete: removed ${removed} old command file(s)`));
446-
}
447-
} catch {
448-
// Non-fatal error - log but continue
449-
console.log(chalk.yellow(' ⚠ Could not migrate old commands (non-fatal)'));
415+
for (const file of legacyFiles) {
416+
await FileSystem.remove(file);
417+
console.log(chalk.gray(` ✓ Removed ${path.relative(process.cwd(), file)}`));
450418
}
451419
}
452420

453-
private extractDescription(content: string): string {
454-
const yamlMatch = content.match(/description:\s*(.+)/);
455-
if (yamlMatch) {
456-
return yamlMatch[1].trim().replace(/^['"]|['"]$/g, '');
457-
}
421+
private async injectDocumentation(adapter: AgentAdapter): Promise<void> {
422+
// Inject AGENTS.md
423+
const agentsContent = DocInjector.getDefaultAgentsContent();
424+
await DocInjector.injectBlock('AGENTS.md', this.extractClavixBlock(agentsContent));
458425

459-
const tomlMatch = content.match(/description\s*=\s*['"]?(.+?)['"]?(?:\r?\n|$)/);
460-
if (tomlMatch) {
461-
return tomlMatch[1].trim().replace(/^['"]|['"]$/g, '');
426+
// Inject CLAUDE.md if Claude Code selected
427+
if (adapter.name === 'claude-code') {
428+
const claudeContent = DocInjector.getDefaultClaudeContent();
429+
await DocInjector.injectBlock('CLAUDE.md', this.extractClavixBlock(claudeContent));
462430
}
463-
464-
return '';
465431
}
466432

467433

src/cli/commands/update.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Command, Flags } from '@oclif/core';
22
import chalk from 'chalk';
3+
import inquirer from 'inquirer';
34
import * as fs from 'fs-extra';
45
import * as path from 'path';
56
import JSON5 from 'json5';
@@ -8,6 +9,7 @@ import { AgentManager } from '../../core/agent-manager';
89
import { AgentsMdGenerator } from '../../core/adapters/agents-md-generator';
910
import { OctoMdGenerator } from '../../core/adapters/octo-md-generator';
1011
import { AgentAdapter } from '../../types/agent';
12+
import { collectLegacyCommandFiles } from '../../utils/legacy-command-cleanup';
1113

1214
export default class Update extends Command {
1315
static description = 'Update managed blocks and slash commands';
@@ -194,7 +196,8 @@ export default class Update extends Command {
194196
let updated = 0;
195197

196198
for (const command of templateFiles) {
197-
const commandFile = path.join(commandsPath, `${command}${extension}`);
199+
const filename = adapter.getTargetFilename(command);
200+
const commandFile = path.join(commandsPath, filename);
198201
const templatePath = path.join(templatesDir, `${command}${extension}`);
199202

200203
const newContent = fs.readFileSync(templatePath, 'utf-8');
@@ -204,21 +207,67 @@ export default class Update extends Command {
204207

205208
if (force || currentContent !== newContent) {
206209
fs.writeFileSync(commandFile, newContent);
207-
this.log(chalk.gray(` ✓ Updated ${command}.md`));
210+
this.log(chalk.gray(` ✓ Updated ${filename}`));
208211
updated++;
209212
} else {
210-
this.log(chalk.gray(` • ${command}.md already up to date`));
213+
this.log(chalk.gray(` • ${filename} already up to date`));
211214
}
212215
} else {
213216
fs.writeFileSync(commandFile, newContent);
214-
this.log(chalk.gray(` ✓ Created ${command}.md`));
217+
this.log(chalk.gray(` ✓ Created ${filename}`));
215218
updated++;
216219
}
217220
}
218221

222+
updated += await this.handleLegacyCommands(adapter, templateFiles);
223+
219224
return updated;
220225
}
221226

227+
private async handleLegacyCommands(adapter: AgentAdapter, commandNames: string[]): Promise<number> {
228+
if (commandNames.length === 0) {
229+
return 0;
230+
}
231+
232+
const legacyFiles = await collectLegacyCommandFiles(adapter, commandNames);
233+
234+
if (legacyFiles.length === 0) {
235+
return 0;
236+
}
237+
238+
const relativePaths = legacyFiles
239+
.map((file) => path.relative(process.cwd(), file))
240+
.sort((a, b) => a.localeCompare(b));
241+
242+
this.log(chalk.gray(` ⚠ Found ${relativePaths.length} deprecated command file(s):`));
243+
for (const file of relativePaths) {
244+
this.log(chalk.gray(` • ${file}`));
245+
}
246+
247+
const { removeLegacy } = await inquirer.prompt([
248+
{
249+
type: 'confirm',
250+
name: 'removeLegacy',
251+
message: `Remove deprecated files for ${adapter.displayName}? Functionality is unchanged; filenames are being standardized.`,
252+
default: true,
253+
},
254+
]);
255+
256+
if (!removeLegacy) {
257+
this.log(chalk.gray(' ⊗ Kept legacy files (deprecated naming retained)'));
258+
return 0;
259+
}
260+
261+
let removed = 0;
262+
for (const file of legacyFiles) {
263+
await fs.remove(file);
264+
this.log(chalk.gray(` ✓ Removed ${path.relative(process.cwd(), file)}`));
265+
removed++;
266+
}
267+
268+
return removed;
269+
}
270+
222271
private getAgentsContent(): string {
223272
return `## Clavix Integration
224273

src/core/adapters/amp-adapter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,9 @@ export class AmpAdapter extends BaseAdapter {
3131
return this.directory;
3232
}
3333

34+
getTargetFilename(name: string): string {
35+
return `clavix-${name}${this.fileExtension}`;
36+
}
37+
3438
// Uses default formatCommand from BaseAdapter (no special formatting)
3539
}

src/core/adapters/base-adapter.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ export abstract class BaseAdapter implements AgentAdapter {
2323
abstract detectProject(): Promise<boolean>;
2424
abstract getCommandPath(): string;
2525

26+
/**
27+
* Determine the target filename for a generated command
28+
* Providers can override to customize filename conventions
29+
*/
30+
getTargetFilename(name: string): string {
31+
return `${name}${this.fileExtension}`;
32+
}
33+
2634
/**
2735
* Default validation logic - can be overridden
2836
* Checks if directory can be created and is writable
@@ -91,7 +99,7 @@ export abstract class BaseAdapter implements AgentAdapter {
9199
// Generate each command file
92100
for (const template of templates) {
93101
const content = this.formatCommand(template);
94-
const filename = `${template.name}${this.fileExtension}`;
102+
const filename = this.getTargetFilename(template.name);
95103
const filePath = path.join(commandPath, filename);
96104
await FileSystem.writeFileAtomic(filePath, content);
97105
}

src/core/adapters/cline-adapter.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { FileSystem } from '../../utils/file-system';
1616
export class ClineAdapter extends BaseAdapter {
1717
readonly name = 'cline';
1818
readonly displayName = 'Cline';
19-
readonly directory = '.cline/workflows';
19+
readonly directory = '.clinerules/workflows';
2020
readonly fileExtension = '.md';
2121
readonly features = {
2222
supportsSubdirectories: false,
@@ -28,6 +28,10 @@ export class ClineAdapter extends BaseAdapter {
2828
* Checks for .cline directory
2929
*/
3030
async detectProject(): Promise<boolean> {
31+
if (await FileSystem.exists('.clinerules')) {
32+
return true;
33+
}
34+
3135
return await FileSystem.exists('.cline');
3236
}
3337

@@ -38,5 +42,9 @@ export class ClineAdapter extends BaseAdapter {
3842
return this.directory;
3943
}
4044

45+
getTargetFilename(name: string): string {
46+
return `clavix-${name}${this.fileExtension}`;
47+
}
48+
4149
// Uses default formatCommand and generateCommands from BaseAdapter
4250
}

0 commit comments

Comments
 (0)