From dbf66e694922dee15c8b0437b8c1f912fad89db2 Mon Sep 17 00:00:00 2001 From: Jairo Litman <130161309+jairo-litman@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:50:55 -0300 Subject: [PATCH 1/5] add kody rules skills --- .claude/commands/.kodus-managed-skills.json | 6 + .claude/commands/kodus-kody-rules.md | 48 +++++ README.md | 19 ++ package.json | 4 +- skills/kodus-kody-rules/SKILL.md | 48 +++++ .../rules/create-kody-rule.md | 42 ++++ .../rules/update-kody-rule.md | 49 +++++ .../kodus-kody-rules/rules/view-kody-rules.md | 50 +++++ src/cli.ts | 22 ++- src/commands/__tests__/rules.test.ts | 100 ++++++++++ src/commands/rules.ts | 181 ++++++++++++++++++ src/services/__tests__/rules.service.test.ts | 106 ++++++++++ src/services/api/__tests__/rules.api.test.ts | 87 +++++++++ src/services/api/api.interface.ts | 25 ++- src/services/api/api.real.ts | 18 +- src/services/api/index.ts | 7 +- src/services/api/rules.api.ts | 71 +++++++ src/services/rules.service.ts | 141 ++++++++++++++ src/types/index.ts | 1 + src/types/rules.ts | 33 ++++ src/utils/skills-sync.ts | 3 +- 21 files changed, 1036 insertions(+), 25 deletions(-) create mode 100644 .claude/commands/.kodus-managed-skills.json create mode 100644 .claude/commands/kodus-kody-rules.md create mode 100644 skills/kodus-kody-rules/SKILL.md create mode 100644 skills/kodus-kody-rules/rules/create-kody-rule.md create mode 100644 skills/kodus-kody-rules/rules/update-kody-rule.md create mode 100644 skills/kodus-kody-rules/rules/view-kody-rules.md create mode 100644 src/commands/__tests__/rules.test.ts create mode 100644 src/commands/rules.ts create mode 100644 src/services/__tests__/rules.service.test.ts create mode 100644 src/services/api/__tests__/rules.api.test.ts create mode 100644 src/services/api/rules.api.ts create mode 100644 src/services/rules.service.ts create mode 100644 src/types/rules.ts diff --git a/.claude/commands/.kodus-managed-skills.json b/.claude/commands/.kodus-managed-skills.json new file mode 100644 index 0000000..68f63d7 --- /dev/null +++ b/.claude/commands/.kodus-managed-skills.json @@ -0,0 +1,6 @@ +[ + "kodus-business-rules-validation", + "kodus-kody-rules", + "kodus-pr-suggestions-resolver", + "kodus-review" +] diff --git a/.claude/commands/kodus-kody-rules.md b/.claude/commands/kodus-kody-rules.md new file mode 100644 index 0000000..549453f --- /dev/null +++ b/.claude/commands/kodus-kody-rules.md @@ -0,0 +1,48 @@ +--- +name: kodus-kody-rules +description: Use when the user wants to create, update or view Kody Rules via `kodus rules` command. +--- + +# Kodus Kody Rules + +## Overview + +Kody Rules are a set of guidelines that Kody follows when generating code. They help ensure that the generated code is consistent, high-quality, and aligned with the user's preferences and project requirements. + +## How to Use + +Read individual rule files for detailed explanations and examples: + +- [rules/create-kody-rule.md](rules/create-kody-rule.md): Guidelines for creating new Kody Rules. +- [rules/update-kody-rule.md](rules/update-kody-rule.md): Guidelines for updating existing Kody Rules. +- [rules/view-kody-rules.md](rules/view-kody-rules.md): Guidelines for viewing and retrieving Kody Rules. + +## Structure of a Kody Rule + +A Kody Rule typically consists of the following components: + +- **Title**: A concise title that captures the essence of the rule. +- **Rule**: A detailed explanation of what the rule is and why it is important. +- **Severity**: A level indicating the importance of the rule (one of "low", "medium", "high" or "critical"). + - **Low**: The rule is a suggestion and can be ignored without significant consequences. + - **Medium**: The rule should be followed, but violations are not critical. Default severity level. + - **High**: The rule is important and should be followed to avoid potential issues. + - **Critical**: The rule is essential and must be followed to prevent severe issues or failures. +- **Scope**: The level at which the rule applies (one of "pull request" or "file"). + - **Pull Request**: The rule applies to the entire pull request and is evaluated based on the overall changes in the PR. + - **File**: The rule applies to individual files and is evaluated on a per-file basis. Default scope level. +- **Path**: An optional glob pattern indicating which files the rule applies to. + - For example, `src/**/*.js` would apply the rule to all JavaScript files in the `src` directory and its subdirectories. + - Default is all files, `**/*`. + +## Example of a Kody Rule + +**Title**: Use Async/Await for Asynchronous Operations + +**Rule**: Ensure that all asynchronous operations in the codebase use async/await syntax for better readability and error handling. Avoid using raw Promises or callback functions for asynchronous code. + +**Severity**: High + +**Scope**: File + +**Path**: `**/*.ts` diff --git a/README.md b/README.md index bd000da..0aa3dc8 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,25 @@ kodus review --prompt-only # Structured output for AI agents Reviews are **context-aware** — Kodus reads your `.cursorrules`, `claude.md`, and `.kodus.md` so suggestions follow your team's standards. [More on review modes](#review-modes) +### Kody Rules + +Create, update, and inspect the Kody Rules that guide Kodus behavior for your team. + +```bash +kodus rules create --title "Use async/await" --rule "Prefer async/await over raw promises" --severity high --scope file --path "**/*.ts" +kodus rules update --uuid --severity critical +kodus rules view +kodus rules view --title "Use async/await" +``` + +`kodus rules update` requires `--uuid`. + +Defaults: + +- `severity` defaults to `medium` +- `scope` defaults to `file` +- `path` is optional (omitted means all files) + ### PR Suggestions Fetch AI-powered suggestions for open pull requests directly from your terminal. diff --git a/package.json b/package.json index bc4529b..f6ab50c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kodus/cli", - "version": "0.4.9", + "version": "0.4.10", "description": "Kodus CLI - AI-powered code review from your terminal", "type": "module", "bin": { @@ -84,4 +84,4 @@ "dist", "skills" ] -} \ No newline at end of file +} diff --git a/skills/kodus-kody-rules/SKILL.md b/skills/kodus-kody-rules/SKILL.md new file mode 100644 index 0000000..549453f --- /dev/null +++ b/skills/kodus-kody-rules/SKILL.md @@ -0,0 +1,48 @@ +--- +name: kodus-kody-rules +description: Use when the user wants to create, update or view Kody Rules via `kodus rules` command. +--- + +# Kodus Kody Rules + +## Overview + +Kody Rules are a set of guidelines that Kody follows when generating code. They help ensure that the generated code is consistent, high-quality, and aligned with the user's preferences and project requirements. + +## How to Use + +Read individual rule files for detailed explanations and examples: + +- [rules/create-kody-rule.md](rules/create-kody-rule.md): Guidelines for creating new Kody Rules. +- [rules/update-kody-rule.md](rules/update-kody-rule.md): Guidelines for updating existing Kody Rules. +- [rules/view-kody-rules.md](rules/view-kody-rules.md): Guidelines for viewing and retrieving Kody Rules. + +## Structure of a Kody Rule + +A Kody Rule typically consists of the following components: + +- **Title**: A concise title that captures the essence of the rule. +- **Rule**: A detailed explanation of what the rule is and why it is important. +- **Severity**: A level indicating the importance of the rule (one of "low", "medium", "high" or "critical"). + - **Low**: The rule is a suggestion and can be ignored without significant consequences. + - **Medium**: The rule should be followed, but violations are not critical. Default severity level. + - **High**: The rule is important and should be followed to avoid potential issues. + - **Critical**: The rule is essential and must be followed to prevent severe issues or failures. +- **Scope**: The level at which the rule applies (one of "pull request" or "file"). + - **Pull Request**: The rule applies to the entire pull request and is evaluated based on the overall changes in the PR. + - **File**: The rule applies to individual files and is evaluated on a per-file basis. Default scope level. +- **Path**: An optional glob pattern indicating which files the rule applies to. + - For example, `src/**/*.js` would apply the rule to all JavaScript files in the `src` directory and its subdirectories. + - Default is all files, `**/*`. + +## Example of a Kody Rule + +**Title**: Use Async/Await for Asynchronous Operations + +**Rule**: Ensure that all asynchronous operations in the codebase use async/await syntax for better readability and error handling. Avoid using raw Promises or callback functions for asynchronous code. + +**Severity**: High + +**Scope**: File + +**Path**: `**/*.ts` diff --git a/skills/kodus-kody-rules/rules/create-kody-rule.md b/skills/kodus-kody-rules/rules/create-kody-rule.md new file mode 100644 index 0000000..10fb2f6 --- /dev/null +++ b/skills/kodus-kody-rules/rules/create-kody-rule.md @@ -0,0 +1,42 @@ +--- +name: create-kody-rule +description: Kody Rule Creation Guidelines - Use when the user wants to create a new Kody Rule for Kodus to follow when generating code. +--- + +# Kody Rule Creation Guidelines + +## Overview + +When creating a new Kody Rule, it's important to ensure that the rule is clear, actionable, and aligned with the overall goals of code generation. A well-defined Kody Rule helps Kody produce code that meets the user's expectations and project requirements. + +## Workflow for Creating a Kody Rule + +1. **Collect the user's intent**: Understand the specific coding practice, style, or requirement that the user wants to enforce with the new Kody Rule. Ask clarifying questions if necessary to ensure you have a clear understanding of the user's intent. + +2. **Draft the Kody Rule**: Based on the user's intent, draft a Kody Rule that includes a clear description, title, and any relevant metadata such as severity and scope. Use the guidelines outlined in the "Guidelines for Creating a Kody Rule" section to ensure the rule is well-structured and effective. + +3. **Review the Kody Rule with the user**: Present the drafted Kody Rule to the user for feedback. Discuss any potential edge cases, exceptions, or clarifications needed to ensure the rule is comprehensive and actionable. + +4. **Refine the Kody Rule**: Based on the user's feedback, refine the Kody Rule to address any concerns or suggestions. Ensure that the final version of the rule is clear, specific, and aligned with the user's goals. + +5. **Save and Implement the Kody Rule**: Once the Kody Rule is finalized and approved by the user, save it. Send the title, rule, and any optional fields such as severity, scope, and path. + +Use the following command to save the Kody Rule: + +``` +kodus rules create --title --rule <rule-content> [--severity <severity-level>] [--scope <scope-level>] [--path <glob-pattern>] +``` + +6. **Communicate the new Kody Rule**: Inform the user about the new Kody Rule and how it will be applied in future code generation. + +## Guidelines for Creating a Kody Rule + +1. **Identify the Purpose**: Clearly define what the Kody Rule is intended to achieve. Is it meant to enforce a coding style, ensure best practices, or address a specific use case? + +2. **Be Specific**: The rule should be specific and unambiguous. Avoid vague language and ensure that the rule can be easily understood and applied by Kody. + +3. **Consider Edge Cases**: Think about any edge cases or exceptions that might arise when applying the rule. Address these in the rule definition to ensure Kody can handle them appropriately. + +4. **Align with Project Goals**: Ensure that the Kody Rule aligns with the overall goals and requirements of the project. The rule should contribute to producing code that is maintainable, efficient, and meets the user's needs. + +5. **Review and Refine**: After drafting the Kody Rule, review it for clarity and completeness. Present it to the user for feedback and refine it as necessary to ensure it effectively guides Kody's code generation process. diff --git a/skills/kodus-kody-rules/rules/update-kody-rule.md b/skills/kodus-kody-rules/rules/update-kody-rule.md new file mode 100644 index 0000000..9d6dcf2 --- /dev/null +++ b/skills/kodus-kody-rules/rules/update-kody-rule.md @@ -0,0 +1,49 @@ +--- +name: update-kody-rule +description: Kody Rule Update Guidelines - Use when the user wants to update an existing Kody Rule to modify its behavior, scope, or severity for Kodus to follow when generating code. +--- + +# Kody Rule Update Guidelines + +## Overview + +When updating an existing Kody Rule, it's important to ensure that the changes are clear, justified, and aligned with the overall goals of code generation. Updating a Kody Rule can help refine its effectiveness and ensure that it continues to meet the user's expectations and project requirements. + +## Workflow for Updating a Kody Rule + +1. **Identify the Kody Rule to Update**: If the user did not specify which Kody Rule they want to update, ask for one of: + - `--uuid <uuid>` + +Updates must use `--uuid`. + +2. **Collect the user's intent for the update**: Understand the specific changes the user wants to make to the existing Kody Rule. Ask clarifying questions if necessary to ensure you have a clear understanding of the user's intent. + +3. **Review the existing Kody Rule**: Retrieve the current definition of the Kody Rule that is being updated. This will help you understand the existing behavior and identify what changes need to be made. + +4. **Draft the updated Kody Rule**: Based on the user's intent and the existing rule, draft an updated version of the Kody Rule that includes the desired changes. Use the guidelines outlined in the "Guidelines for Updating a Kody Rule" section to ensure the updated rule is well-structured and effective. + +5. **Review the updated Kody Rule with the user**: Present the drafted updated Kody Rule to the user for feedback. Discuss any potential edge cases, exceptions, or clarifications needed to ensure the updated rule is comprehensive and actionable. + +6. **Refine the updated Kody Rule**: Based on the user's feedback, refine the updated Kody Rule to address any concerns or suggestions. Ensure that the final version of the updated rule is clear, specific, and aligned with the user's goals. + +7. **Save and Implement the updated Kody Rule**: Once the updated Kody Rule is finalized and approved by the user, save it. Send only the fields that were updated, along with the `uuid` to identify which rule to update. + +Use the following command to save the updated Kody Rule: + +``` +kodus rules update --uuid <uuid> [--title <title>] [--rule <rule-content>] [--severity <severity-level>] [--scope <scope-level>] [--path <glob-pattern>] +``` + +8. **Communicate the updated Kody Rule**: Inform the user about the updated Kody Rule and how the changes will affect future code generation. + +## Guidelines for Updating a Kody Rule + +1. **Identify the Changes**: Clearly define what changes are being made to the existing Kody Rule. Are you modifying the rule's behavior, scope, severity, or other attributes? + +2. **Justify the Changes**: Ensure that there is a clear justification for the changes being made to the Kody Rule. The updates should contribute to producing code that is more maintainable, efficient, or better aligned with the user's needs. + +3. **Consider Edge Cases**: Think about any edge cases or exceptions that might arise from the updated rule. Address these in the updated rule definition to ensure Kody can handle them appropriately. + +4. **Align with Project Goals**: Ensure that the updated Kody Rule continues to align with the overall goals and requirements of the project. The updated rule should contribute to producing code that meets the user's expectations and project requirements. + +5. **Review and Refine**: After drafting the updated Kody Rule, review it for clarity and completeness. Present it to the user for feedback and refine it as necessary to ensure it effectively guides Kody's code generation process. diff --git a/skills/kodus-kody-rules/rules/view-kody-rules.md b/skills/kodus-kody-rules/rules/view-kody-rules.md new file mode 100644 index 0000000..cdda1f7 --- /dev/null +++ b/skills/kodus-kody-rules/rules/view-kody-rules.md @@ -0,0 +1,50 @@ +--- +name: view-kody-rules +description: Kody Rule Viewing Guidelines - Use when the user wants to view existing Kody Rules that Kodus follows when generating code. +--- + +# Kody Rule Viewing Guidelines + +## Overview + +When viewing existing Kody Rules, it's important to understand the details of each rule, including its UUID, title, rule content, severity, scope, and any applicable file patterns. This information helps you understand how Kody generates code and what guidelines it follows. + +## Workflow for Viewing Kody Rules + +1. **Retrieve Rule Target**: If the user specified a particular rule(s) to view, identify it using one of: + - `--title <title>` + - `--uuid <uuid>` + +If both are provided, prefer `--uuid`. If no specific rule is requested, prepare to display all existing Kody Rules. + +2. **Fetch Kody Rules**: Use the appropriate command to fetch the Kody Rule(s) based on the identified target(s). If no specific rule was requested, fetch all existing Kody Rules. + +Use the following command to fetch Kody Rules: + +``` +kodus rules view [--uuid <uuid>] [--title <title>] +``` + +3. **Display Kody Rules**: Present the retrieved Kody Rule(s) in a clear and organized manner. For each rule, display the following information: + - Rule UUID + - Rule Title + - Rule + - Severity (if specified) + - Scope (if specified) + - Path (if specified) + +Do not alter the content of the rules; display them as they are retrieved to ensure accuracy. + +4. **Provide Context**: If the user is viewing a specific rule, provide additional context about how that rule is applied in code generation and any relevant examples or use cases. + +5. **Answer Follow-up Questions**: Be prepared to answer any follow-up questions the user may have about the Kody Rules, such as how to create or update rules, or how specific rules affect code generation. + +## Guidelines for Viewing Kody Rules + +1. **Be Accurate**: When displaying Kody Rules, ensure that the information is accurate and reflects the current state of the rules as retrieved from the system. + +2. **Be Clear**: Present the Kody Rules in a clear and organized manner, making it easy for the user to understand the details of each rule. + +3. **Provide Context**: When appropriate, provide additional context about how specific Kody Rules are applied in code generation and how they affect the output. + +4. **Be Responsive**: Be prepared to answer any follow-up questions the user may have about the Kody Rules, and provide helpful information to guide them in understanding and utilizing the rules effectively. diff --git a/src/cli.ts b/src/cli.ts index 36768a4..9584e34 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,20 +1,21 @@ -import { createRequire } from 'node:module'; import { Command } from 'commander'; -import { reviewCommand } from './commands/review.js'; +import { createRequire } from 'node:module'; import { authCommand } from './commands/auth/index.js'; -import { subscribeCommand } from './commands/subscribe.js'; -import { updateCommand } from './commands/update.js'; -import { prCommand } from './commands/pr.js'; +import { configCommand } from './commands/config.js'; import { hookCommand } from './commands/hook/index.js'; import { decisionsCommand } from './commands/memory/index.js'; -import { statusCommand } from './commands/status.js'; -import { skillsCommand } from './commands/skills.js'; +import { prCommand } from './commands/pr.js'; +import { reviewCommand } from './commands/review.js'; +import { rulesCommand } from './commands/rules.js'; import { createSchemaCommand } from './commands/schema.js'; -import { configCommand } from './commands/config.js'; -import { checkForUpdates } from './utils/update-check.js'; +import { skillsCommand } from './commands/skills.js'; +import { statusCommand } from './commands/status.js'; +import { subscribeCommand } from './commands/subscribe.js'; +import { updateCommand } from './commands/update.js'; +import { applyCommanderBehavior } from './utils/commander-setup.js'; import { setCliOutputMode } from './utils/logger.js'; import { recordRecentActivity } from './utils/recent-activity.js'; -import { applyCommanderBehavior } from './utils/commander-setup.js'; +import { checkForUpdates } from './utils/update-check.js'; const require = createRequire(import.meta.url); const pkg = require('../package.json') as { version: string }; @@ -44,6 +45,7 @@ program.addCommand(hookCommand); program.addCommand(decisionsCommand); program.addCommand(statusCommand); program.addCommand(skillsCommand); +program.addCommand(rulesCommand); program.addCommand(configCommand); program.addCommand(createSchemaCommand(() => program)); applyCommanderBehavior(program); diff --git a/src/commands/__tests__/rules.test.ts b/src/commands/__tests__/rules.test.ts new file mode 100644 index 0000000..54b6e4c --- /dev/null +++ b/src/commands/__tests__/rules.test.ts @@ -0,0 +1,100 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { CliExitError } from '../../utils/cli-exit.js'; + +vi.mock('../../services/rules.service.js', () => ({ + rulesService: { + createRule: vi.fn(), + updateRule: vi.fn(), + viewRules: vi.fn(), + }, +})); + +import { rulesService } from '../../services/rules.service.js'; +import { + rulesCommand, + rulesCreateAction, + rulesUpdateAction, + rulesViewAction, +} from '../rules.js'; + +const mockRulesService = vi.mocked(rulesService); + +describe('rules command actions', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('creates a rule and prints success output', async () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + mockRulesService.createRule.mockResolvedValue({ + uuid: 'rule-1', + title: 'Use async/await', + rule: 'Prefer async/await', + severity: 'high', + scope: 'file', + path: '**/*.ts', + }); + + await rulesCreateAction({ + title: 'Use async/await', + rule: 'Prefer async/await', + severity: 'high', + scope: 'file', + path: '**/*.ts', + }); + + const output = logSpy.mock.calls + .map((call) => call.join(' ')) + .join('\n'); + expect(output).toContain('Kody Rule created successfully.'); + expect(output).toContain('Rule UUID: rule-1'); + }); + + it('prints JSON for view when requested', async () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + mockRulesService.viewRules.mockResolvedValue([ + { + uuid: 'rule-2', + title: 'Rule', + rule: 'Description', + }, + ]); + + await rulesViewAction({ json: true }); + + const output = logSpy.mock.calls + .map((call) => call.join(' ')) + .join('\n'); + expect(JSON.parse(output)).toEqual([ + { + uuid: 'rule-2', + title: 'Rule', + rule: 'Description', + }, + ]); + }); + + it('converts service errors to CLI exits', async () => { + const errorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + mockRulesService.updateRule.mockRejectedValue(new Error('boom')); + + await expect( + rulesUpdateAction({ + uuid: 'rule-1', + rule: 'x', + }), + ).rejects.toBeInstanceOf(CliExitError); + + expect(errorSpy).toHaveBeenCalled(); + }); + + it('requires --uuid on update subcommand', () => { + const updateSubcommand = rulesCommand.commands + .find((subcommand) => subcommand.name() === 'update') + ?.options.find((option) => option.long === '--uuid'); + + expect(updateSubcommand?.required).toBe(true); + }); +}); diff --git a/src/commands/rules.ts b/src/commands/rules.ts new file mode 100644 index 0000000..f07043e --- /dev/null +++ b/src/commands/rules.ts @@ -0,0 +1,181 @@ +import chalk from 'chalk'; +import { Command } from 'commander'; +import { rulesService } from '../services/rules.service.js'; +import type { + KodyRule, + KodyRuleScope, + KodyRuleSeverity, +} from '../types/rules.js'; +import { exitWithCode } from '../utils/cli-exit.js'; +import { normalizeCommandError } from '../utils/command-errors.js'; +import { cliError, cliInfo } from '../utils/logger.js'; + +export type RulesCreateOptions = { + title: string; + rule: string; + severity?: KodyRuleSeverity; + scope?: KodyRuleScope; + path?: string; + json?: boolean; +}; + +export type RulesUpdateOptions = { + uuid: string; + title?: string; + rule?: string; + severity?: KodyRuleSeverity; + scope?: KodyRuleScope; + path?: string; + json?: boolean; +}; + +export type RulesViewOptions = { + uuid?: string; + title?: string; + json?: boolean; +}; + +function printRule(rule: KodyRule): void { + cliInfo(`Rule UUID: ${rule.uuid}`); + cliInfo(`Rule Title: ${rule.title}`); + cliInfo(`Rule: ${rule.rule}`); + if (rule.severity) { + cliInfo(`Severity: ${rule.severity}`); + } + if (rule.scope) { + cliInfo(`Scope: ${rule.scope}`); + } + if (rule.path) { + cliInfo(`Path: ${rule.path}`); + } +} + +function printRuleList(rules: KodyRule[]): void { + if (rules.length === 0) { + cliInfo(chalk.yellow('No Kody Rules found.')); + return; + } + + rules.forEach((rule, index) => { + printRule(rule); + if (index < rules.length - 1) { + cliInfo(''); + } + }); +} + +export async function rulesCreateAction( + options: RulesCreateOptions, +): Promise<void> { + try { + const createdRule = await rulesService.createRule({ + title: options.title, + rule: options.rule, + severity: options.severity, + scope: options.scope, + path: options.path, + }); + + if (options.json) { + cliInfo(JSON.stringify(createdRule, null, 2)); + return; + } + + cliInfo(chalk.green('Kody Rule created successfully.')); + printRule(createdRule); + } catch (error) { + const normalized = normalizeCommandError(error); + cliError(chalk.red(normalized.message)); + exitWithCode(normalized.exitCode); + } +} + +export async function rulesUpdateAction( + options: RulesUpdateOptions, +): Promise<void> { + try { + const updatedRule = await rulesService.updateRule({ + ruleId: options.uuid, + title: options.title, + rule: options.rule, + severity: options.severity, + scope: options.scope, + path: options.path, + }); + + if (options.json) { + cliInfo(JSON.stringify(updatedRule, null, 2)); + return; + } + + cliInfo(chalk.green('Kody Rule updated successfully.')); + printRule(updatedRule); + } catch (error) { + const normalized = normalizeCommandError(error); + cliError(chalk.red(normalized.message)); + exitWithCode(normalized.exitCode); + } +} + +export async function rulesViewAction( + options: RulesViewOptions, +): Promise<void> { + try { + const rules = await rulesService.viewRules({ + ruleId: options.uuid, + ruleName: options.title, + }); + + if (options.json) { + cliInfo(JSON.stringify(rules, null, 2)); + return; + } + + printRuleList(rules); + } catch (error) { + const normalized = normalizeCommandError(error); + cliError(chalk.red(normalized.message)); + exitWithCode(normalized.exitCode); + } +} + +export const rulesCommand = new Command('rules') + .description('Create, update, and view Kody Rules') + .showHelpAfterError(); + +rulesCommand + .command('create') + .description('Create a new Kody Rule') + .requiredOption('--title <title>', 'Rule title') + .requiredOption('--rule <rule>', 'Rule content/description') + .option( + '--severity <severity>', + 'Rule severity (low, medium, high, critical)', + ) + .option('--scope <scope>', "Rule scope ('pull request' or 'file')") + .option('--path <glob>', 'Optional glob pattern for file targeting') + .option('--json', 'Output created rule as JSON') + .action(rulesCreateAction); + +rulesCommand + .command('update') + .description('Update an existing Kody Rule') + .requiredOption('--uuid <uuid>', 'Rule UUID to update') + .option('--title <title>', 'Updated rule title') + .option('--rule <rule>', 'Updated rule content/description') + .option( + '--severity <severity>', + 'Updated rule severity (low, medium, high, critical)', + ) + .option('--scope <scope>', "Updated rule scope ('pull request' or 'file')") + .option('--path <glob>', 'Updated glob pattern for file targeting') + .option('--json', 'Output updated rule as JSON') + .action(rulesUpdateAction); + +rulesCommand + .command('view') + .description('View Kody Rules') + .option('--uuid <uuid>', 'Rule UUID to fetch') + .option('--title <title>', 'Rule title to fetch when UUID is not provided') + .option('--json', 'Output rules as JSON') + .action(rulesViewAction); diff --git a/src/services/__tests__/rules.service.test.ts b/src/services/__tests__/rules.service.test.ts new file mode 100644 index 0000000..d08f9e1 --- /dev/null +++ b/src/services/__tests__/rules.service.test.ts @@ -0,0 +1,106 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('../api/index.js', () => ({ + api: { + rules: { + createRule: vi.fn(), + updateRule: vi.fn(), + viewRules: vi.fn(), + }, + }, +})); + +vi.mock('../auth.service.js', () => ({ + authService: { + getValidToken: vi.fn(), + }, +})); + +import { CommandError } from '../../utils/command-errors.js'; +import { api } from '../api/index.js'; +import { authService } from '../auth.service.js'; +import { rulesService } from '../rules.service.js'; + +const mockRulesApi = vi.mocked(api.rules); +const mockAuthService = vi.mocked(authService); + +describe('rulesService', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockAuthService.getValidToken.mockResolvedValue('kodus_team_key'); + }); + + it('applies severity and scope defaults on create', async () => { + mockRulesApi.createRule.mockResolvedValue({ + uuid: 'rule-1', + title: 'Use async/await', + rule: 'Prefer async/await', + severity: 'medium', + scope: 'file', + }); + + await rulesService.createRule({ + title: 'Use async/await', + rule: 'Prefer async/await', + }); + + expect(mockRulesApi.createRule).toHaveBeenCalledWith('kodus_team_key', { + title: 'Use async/await', + rule: 'Prefer async/await', + severity: 'medium', + scope: 'file', + }); + }); + + it('validates severity values', async () => { + await expect( + rulesService.createRule({ + title: 'Rule', + rule: 'Desc', + severity: 'urgent' as any, + }), + ).rejects.toEqual( + expect.objectContaining<Partial<CommandError>>({ + code: 'INVALID_INPUT', + }), + ); + }); + + it('requires at least one field for update', async () => { + await expect( + rulesService.updateRule({ + ruleId: 'rule-1', + }), + ).rejects.toEqual( + expect.objectContaining<Partial<CommandError>>({ + code: 'INVALID_INPUT', + }), + ); + }); + + it('requires rule-id for updates', async () => { + await expect( + rulesService.updateRule({ + ruleId: '', + rule: 'updated', + }), + ).rejects.toEqual( + expect.objectContaining<Partial<CommandError>>({ + code: 'INVALID_INPUT', + }), + ); + }); + + it('uses ruleId precedence for view queries', async () => { + mockRulesApi.viewRules.mockResolvedValue([]); + + await rulesService.viewRules({ + ruleId: 'rule-9', + ruleName: 'Ignored Rule Name', + }); + + expect(mockRulesApi.viewRules).toHaveBeenCalledWith('kodus_team_key', { + ruleId: 'rule-9', + }); + }); +}); diff --git a/src/services/api/__tests__/rules.api.test.ts b/src/services/api/__tests__/rules.api.test.ts new file mode 100644 index 0000000..9c9032a --- /dev/null +++ b/src/services/api/__tests__/rules.api.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it, vi } from 'vitest'; +import { RealRulesApi } from '../rules.api.js'; + +describe('RealRulesApi', () => { + it('creates a rule with team-key auth', async () => { + const requestWithRetry = vi.fn().mockResolvedValue({ + uuid: 'rule-1', + title: 'Use async/await', + rule: 'Prefer async/await over raw promises', + severity: 'high', + scope: 'file', + path: '**/*.ts', + }); + + const api = new RealRulesApi(requestWithRetry); + await api.createRule('kodus_team_key', { + title: 'Use async/await', + rule: 'Prefer async/await over raw promises', + severity: 'high', + scope: 'file', + path: '**/*.ts', + }); + + expect(requestWithRetry).toHaveBeenCalledWith('/cli/kody-rules', { + method: 'POST', + headers: { + 'X-Team-Key': 'kodus_team_key', + }, + body: JSON.stringify({ + title: 'Use async/await', + rule: 'Prefer async/await over raw promises', + severity: 'high', + scope: 'file', + path: '**/*.ts', + }), + }); + }); + + it('updates a rule with bearer auth', async () => { + const requestWithRetry = vi.fn().mockResolvedValue({ + uuid: 'rule-1', + title: 'Use async/await', + rule: 'Updated description', + severity: 'critical', + scope: 'file', + }); + + const api = new RealRulesApi(requestWithRetry); + await api.updateRule('eyJ.test.token', 'rule-1', { + rule: 'Updated description', + severity: 'critical', + }); + + expect(requestWithRetry).toHaveBeenCalledWith( + '/cli/kody-rules/rule-1', + { + method: 'PATCH', + headers: { + Authorization: 'Bearer eyJ.test.token', + }, + body: JSON.stringify({ + rule: 'Updated description', + severity: 'critical', + }), + }, + ); + }); + + it('prefers ruleId over ruleName when viewing rules', async () => { + const requestWithRetry = vi.fn().mockResolvedValue([]); + + const api = new RealRulesApi(requestWithRetry); + await api.viewRules('kodus_team_key', { + ruleId: 'rule-123', + ruleName: 'ignored-name', + }); + + expect(requestWithRetry).toHaveBeenCalledWith( + '/cli/kody-rules?ruleId=rule-123', + { + headers: { + 'X-Team-Key': 'kodus_team_key', + }, + }, + ); + }); +}); diff --git a/src/services/api/api.interface.ts b/src/services/api/api.interface.ts index 2fbffb9..3e815ac 100644 --- a/src/services/api/api.interface.ts +++ b/src/services/api/api.interface.ts @@ -17,8 +17,14 @@ import type { ReviewResult, TrialReviewResult, } from '../../types/review.js'; -import type { TrialStatus } from '../../types/trial.js'; +import type { + CreateKodyRuleRequest, + KodyRule, + UpdateKodyRuleRequest, + ViewKodyRulesRequest, +} from '../../types/rules.js'; import type { SessionApiEvent } from '../../types/session-events.js'; +import type { TrialStatus } from '../../types/trial.js'; export interface IAuthApi { login(email: string, password: string): Promise<AuthResponse>; @@ -122,6 +128,22 @@ export interface ISessionsApi { sendEvent(event: SessionApiEvent, repoRoot: string): Promise<void>; } +export interface IRulesApi { + createRule( + accessToken: string, + payload: CreateKodyRuleRequest, + ): Promise<KodyRule>; + updateRule( + accessToken: string, + ruleId: string, + payload: UpdateKodyRuleRequest, + ): Promise<KodyRule>; + viewRules( + accessToken: string, + query?: ViewKodyRulesRequest, + ): Promise<KodyRule[]>; +} + export interface IKodusApi { auth: IAuthApi; config: IConfigApi; @@ -129,4 +151,5 @@ export interface IKodusApi { trial: ITrialApi; memory: IMemoryApi; sessions: ISessionsApi; + rules: IRulesApi; } diff --git a/src/services/api/api.real.ts b/src/services/api/api.real.ts index 28cdfe8..fa6d9e9 100644 --- a/src/services/api/api.real.ts +++ b/src/services/api/api.real.ts @@ -1,23 +1,20 @@ -import type { - IKodusApi, - ITrialApi, - ISessionsApi, -} from './api.interface.js'; -import { RealSessionsApi } from './sessions.api.js'; import { + getCloudflareAccessHeaders, request, resetApiConfigCache, resolveApiBaseUrl, - getCloudflareAccessHeaders, } from './api-core.js'; +import type { IKodusApi, ISessionsApi, ITrialApi } from './api.interface.js'; +import { RealAuthApi } from './auth.api.js'; import { RealConfigApi } from './config.api.js'; +import { RealMemoryApi } from './memory.api.js'; import { RealReviewApi } from './review.api.js'; -import { RealAuthApi } from './auth.api.js'; +import { RealRulesApi } from './rules.api.js'; +import { RealSessionsApi } from './sessions.api.js'; import { RealTrialApi } from './trial.api.js'; -import { RealMemoryApi } from './memory.api.js'; export const _resetConfigCache = resetApiConfigCache; -export { request, resolveApiBaseUrl, getCloudflareAccessHeaders }; +export { getCloudflareAccessHeaders, request, resolveApiBaseUrl }; export class RealApi implements IKodusApi { auth = new RealAuthApi(); @@ -26,4 +23,5 @@ export class RealApi implements IKodusApi { trial: ITrialApi = new RealTrialApi(); memory = new RealMemoryApi(); sessions: ISessionsApi = new RealSessionsApi(); + rules = new RealRulesApi(); } diff --git a/src/services/api/index.ts b/src/services/api/index.ts index b2a0a13..92a8a2f 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -1,5 +1,10 @@ import { RealApi } from './api.real.js'; -export type { IKodusApi, IMemoryApi, ISessionsApi } from './api.interface.js'; +export type { + IKodusApi, + IMemoryApi, + IRulesApi, + ISessionsApi, +} from './api.interface.js'; export const api = new RealApi(); diff --git a/src/services/api/rules.api.ts b/src/services/api/rules.api.ts new file mode 100644 index 0000000..026b111 --- /dev/null +++ b/src/services/api/rules.api.ts @@ -0,0 +1,71 @@ +import type { + CreateKodyRuleRequest, + KodyRule, + UpdateKodyRuleRequest, + ViewKodyRulesRequest, +} from '../../types/rules.js'; +import { requestWithRetry } from './api-core.js'; +import type { IRulesApi } from './api.interface.js'; + +type RequestWithRetry = <T>( + endpoint: string, + options?: RequestInit, +) => Promise<T>; + +export class RealRulesApi implements IRulesApi { + constructor( + private readonly requester: RequestWithRetry = requestWithRetry, + ) {} + + private buildAuthHeaders(accessToken: string): Record<string, string> { + return accessToken.startsWith('kodus_') + ? { 'X-Team-Key': accessToken } + : { Authorization: `Bearer ${accessToken}` }; + } + + async createRule( + accessToken: string, + payload: CreateKodyRuleRequest, + ): Promise<KodyRule> { + return this.requester<KodyRule>('/cli/kody-rules', { + method: 'POST', + headers: this.buildAuthHeaders(accessToken), + body: JSON.stringify(payload), + }); + } + + async updateRule( + accessToken: string, + ruleId: string, + payload: UpdateKodyRuleRequest, + ): Promise<KodyRule> { + return this.requester<KodyRule>( + `/cli/kody-rules/${encodeURIComponent(ruleId)}`, + { + method: 'PATCH', + headers: this.buildAuthHeaders(accessToken), + body: JSON.stringify(payload), + }, + ); + } + + async viewRules( + accessToken: string, + query: ViewKodyRulesRequest = {}, + ): Promise<KodyRule[]> { + const params = new URLSearchParams(); + if (query.ruleId) { + params.set('ruleId', query.ruleId); + } else if (query.ruleName) { + // Rule ID is authoritative when both are provided. + params.set('ruleName', query.ruleName); + } + + const queryString = params.toString(); + const endpoint = `/cli/kody-rules${queryString ? `?${queryString}` : ''}`; + + return this.requester<KodyRule[]>(endpoint, { + headers: this.buildAuthHeaders(accessToken), + }); + } +} diff --git a/src/services/rules.service.ts b/src/services/rules.service.ts new file mode 100644 index 0000000..fbe6c75 --- /dev/null +++ b/src/services/rules.service.ts @@ -0,0 +1,141 @@ +import type { + CreateKodyRuleRequest, + KodyRule, + KodyRuleScope, + KodyRuleSeverity, + UpdateKodyRuleRequest, + ViewKodyRulesRequest, +} from '../types/rules.js'; +import { CommandError } from '../utils/command-errors.js'; +import { api } from './api/index.js'; +import { authService } from './auth.service.js'; + +export type UpdateKodyRuleInput = { + ruleId: string; +} & UpdateKodyRuleRequest; + +const VALID_SEVERITIES: KodyRuleSeverity[] = [ + 'low', + 'medium', + 'high', + 'critical', +]; + +const VALID_SCOPES: KodyRuleScope[] = ['pull request', 'file']; + +class RulesService { + async createRule(input: CreateKodyRuleRequest): Promise<KodyRule> { + const accessToken = await authService.getValidToken(); + const payload: CreateKodyRuleRequest = { + title: this.requireText(input.title, 'name'), + rule: this.requireText(input.rule, 'description'), + severity: this.normalizeSeverity(input.severity ?? 'medium'), + scope: this.normalizeScope(input.scope ?? 'file'), + }; + + const path = this.normalizeOptionalText(input.path); + if (path) { + payload.path = path; + } + + return api.rules.createRule(accessToken, payload); + } + + async updateRule(input: UpdateKodyRuleInput): Promise<KodyRule> { + const accessToken = await authService.getValidToken(); + const ruleId = this.requireText(input.ruleId, 'rule-id'); + + const payload: UpdateKodyRuleRequest = {}; + const title = this.normalizeOptionalText(input.title); + if (title) { + payload.title = title; + } + + const rule = this.normalizeOptionalText(input.rule); + if (rule) { + payload.rule = rule; + } + + if (input.severity !== undefined) { + payload.severity = this.normalizeSeverity(input.severity); + } + + if (input.scope !== undefined) { + payload.scope = this.normalizeScope(input.scope); + } + + const path = this.normalizeOptionalText(input.path); + if (path) { + payload.path = path; + } + + if (Object.keys(payload).length === 0) { + throw new CommandError( + 'INVALID_INPUT', + 'Provide at least one field to update: --name, --description, --severity, --scope, or --filepath.', + ); + } + + return api.rules.updateRule(accessToken, ruleId, payload); + } + + async viewRules(input: ViewKodyRulesRequest = {}): Promise<KodyRule[]> { + const accessToken = await authService.getValidToken(); + const query: ViewKodyRulesRequest = {}; + + const ruleId = this.normalizeOptionalText(input.ruleId); + const ruleName = this.normalizeOptionalText(input.ruleName); + + if (ruleId) { + query.ruleId = ruleId; + } else if (ruleName) { + query.ruleName = ruleName; + } + + return api.rules.viewRules(accessToken, query); + } + + private normalizeSeverity(value: string): KodyRuleSeverity { + const normalized = value.trim().toLowerCase() as KodyRuleSeverity; + if (!VALID_SEVERITIES.includes(normalized)) { + throw new CommandError( + 'INVALID_INPUT', + `Invalid severity '${value}'. Use one of: ${VALID_SEVERITIES.join(', ')}.`, + ); + } + + return normalized; + } + + private normalizeScope(value: string): KodyRuleScope { + const normalized = value.trim().toLowerCase() as KodyRuleScope; + if (!VALID_SCOPES.includes(normalized)) { + throw new CommandError( + 'INVALID_INPUT', + `Invalid scope '${value}'. Use one of: ${VALID_SCOPES.join(', ')}.`, + ); + } + + return normalized; + } + + private requireText(value: string, field: string): string { + const normalized = value.trim(); + if (!normalized) { + throw new CommandError( + 'INVALID_INPUT', + `--${field} cannot be empty.`, + ); + } + + return normalized; + } + + private normalizeOptionalText(value?: string): string | undefined { + const normalized = value?.trim(); + return normalized ? normalized : undefined; + } +} + +export { RulesService }; +export const rulesService = new RulesService(); diff --git a/src/types/index.ts b/src/types/index.ts index a81b3f9..4f17c37 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,4 +5,5 @@ export * from './errors.js'; export type * from './memory.js'; export type * from './repo-config.js'; export type * from './review.js'; +export type * from './rules.js'; export type * from './trial.js'; diff --git a/src/types/rules.ts b/src/types/rules.ts new file mode 100644 index 0000000..252541a --- /dev/null +++ b/src/types/rules.ts @@ -0,0 +1,33 @@ +export type KodyRuleSeverity = 'low' | 'medium' | 'high' | 'critical'; + +export type KodyRuleScope = 'pull request' | 'file'; + +export interface KodyRule { + uuid: string; + title: string; + rule: string; + severity?: KodyRuleSeverity; + scope?: KodyRuleScope; + path?: string; +} + +export interface CreateKodyRuleRequest { + title: string; + rule: string; + severity?: KodyRuleSeverity; + scope?: KodyRuleScope; + path?: string; +} + +export interface UpdateKodyRuleRequest { + title?: string; + rule?: string; + severity?: KodyRuleSeverity; + scope?: KodyRuleScope; + path?: string; +} + +export interface ViewKodyRulesRequest { + ruleId?: string; + ruleName?: string; +} diff --git a/src/utils/skills-sync.ts b/src/utils/skills-sync.ts index 1c2a8b2..5e499ea 100644 --- a/src/utils/skills-sync.ts +++ b/src/utils/skills-sync.ts @@ -1,7 +1,6 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { type BundledSkillDocument, readBundledSkills } from './skills.js'; import { readManagedSkillNames, resolveManagedManifestPath, @@ -13,11 +12,13 @@ import { resolveManagedSkillPath, } from './skills-sync-paths.js'; import { buildSkillSyncTargets } from './skills-sync-targets.js'; +import { type BundledSkillDocument, readBundledSkills } from './skills.js'; export const DEFAULT_SYNC_SKILL_NAMES = [ 'kodus-review', 'kodus-pr-suggestions-resolver', 'kodus-business-rules-validation', + 'kodus-kody-rules', ] as const; const LEGACY_BUSINESS_RULES_NAME = 'business-rules-validation'; From 8f5d40891243e103fb2b5f29610f06b5974f060d Mon Sep 17 00:00:00 2001 From: Jairo Litman <130161309+jairo-litman@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:59:41 -0300 Subject: [PATCH 2/5] add targets --- package.json | 2 +- .../__tests__/skills-sync-targets.test.ts | 21 +++++++++++++++++++ src/utils/skills-sync-targets.ts | 7 +++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f6ab50c..7c83d6c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "url": "https://github.com/kodustech/cli/issues" }, "scripts": { - "build": "yarn clean && tsc", + "build": "yarn clean && tsc && chmod +x dist/index.js", "dev": "tsc --watch", "start": "node dist/index.js", "start:local": "KODUS_API_URL=http://localhost:3001 node dist/index.js", diff --git a/src/utils/__tests__/skills-sync-targets.test.ts b/src/utils/__tests__/skills-sync-targets.test.ts index 299dd62..2619d14 100644 --- a/src/utils/__tests__/skills-sync-targets.test.ts +++ b/src/utils/__tests__/skills-sync-targets.test.ts @@ -12,6 +12,13 @@ describe('buildSkillSyncTargets', () => { baseDir: '/repo/workspace/.codex/skills', }); + expect(targets).toContainEqual({ + label: 'Copilot user skills', + type: 'skill', + activationPath: '/users/demo/.copilot', + baseDir: '/users/demo/.copilot/skills', + }); + expect(targets).toContainEqual({ label: 'Claude config commands', type: 'command', @@ -19,6 +26,20 @@ describe('buildSkillSyncTargets', () => { baseDir: '/users/demo/.config/claude/commands', }); + expect(targets).toContainEqual({ + label: 'Agents user skills', + type: 'skill', + activationPath: '/users/demo/.agents', + baseDir: '/users/demo/.agents/skills', + }); + + expect(targets).toContainEqual({ + label: 'Agents user skills (legacy config path)', + type: 'skill', + activationPath: '/users/demo/.config/agents', + baseDir: '/users/demo/.config/agents/skills', + }); + expect(targets).toContainEqual({ label: 'Gemini project skills', type: 'skill', diff --git a/src/utils/skills-sync-targets.ts b/src/utils/skills-sync-targets.ts index 90ef231..d1963b6 100644 --- a/src/utils/skills-sync-targets.ts +++ b/src/utils/skills-sync-targets.ts @@ -70,6 +70,13 @@ const SKILL_SYNC_TARGET_DEFINITIONS: SkillSyncTargetDefinition[] = [ label: 'Agents user skills', scope: 'user', type: 'skill', + activationSegments: ['.agents'], + baseSegments: ['.agents', 'skills'], + }, + { + label: 'Agents user skills (legacy config path)', + scope: 'user', + type: 'skill', activationSegments: ['.config', 'agents'], baseSegments: ['.config', 'agents', 'skills'], }, From 16d146489bb882a1b878b7c411e45cf746240230 Mon Sep 17 00:00:00 2001 From: Jairo Litman <130161309+jairo-litman@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:21:12 -0300 Subject: [PATCH 3/5] update kody rules skills --- .claude/commands/kodus-kody-rules.md | 53 ++++++++++++- README.md | 9 ++- skills/kodus-kody-rules/SKILL.md | 53 ++++++++++++- .../instructions/create-kody-rule.md | 46 +++++++++++ .../instructions/update-kody-rule.md | 53 +++++++++++++ .../instructions/view-kody-rules.md | 55 +++++++++++++ .../rules/create-kody-rule.md | 2 +- .../rules/update-kody-rule.md | 10 +-- .../kodus-kody-rules/rules/view-kody-rules.md | 16 ++-- src/commands/__tests__/rules.test.ts | 13 ++- src/commands/rules.ts | 34 +++++--- src/services/__tests__/rules.service.test.ts | 34 +++++++- src/services/api/__tests__/rules.api.test.ts | 50 +++++++++++- src/services/api/rules.api.ts | 6 +- src/services/rules.service.ts | 43 +++++----- src/types/rules.ts | 5 +- .../__tests__/skills-sync-targets.test.ts | 7 -- src/utils/__tests__/skills-sync.test.ts | 60 +++++++++++++- src/utils/skills-sync.ts | 79 +++++++++++++++++-- src/utils/skills.ts | 59 +++++++++++++- 20 files changed, 604 insertions(+), 83 deletions(-) create mode 100644 skills/kodus-kody-rules/instructions/create-kody-rule.md create mode 100644 skills/kodus-kody-rules/instructions/update-kody-rule.md create mode 100644 skills/kodus-kody-rules/instructions/view-kody-rules.md diff --git a/.claude/commands/kodus-kody-rules.md b/.claude/commands/kodus-kody-rules.md index 549453f..e6fe880 100644 --- a/.claude/commands/kodus-kody-rules.md +++ b/.claude/commands/kodus-kody-rules.md @@ -9,18 +9,61 @@ description: Use when the user wants to create, update or view Kody Rules via `k Kody Rules are a set of guidelines that Kody follows when generating code. They help ensure that the generated code is consistent, high-quality, and aligned with the user's preferences and project requirements. +## Goal + +Manage Kody Rules through Kodus CLI only. Do not suggest creating rule files manually. + +ALWAYS Use `kodus rules` subcommands for all create, update, and view operations. All these rules are ALWAYS managed by the `kodus rules` command from the CLI. Do NOT suggest creating files or storing rules in any other way. When the user wants to create, update or view Kody Rules, utilize the `kodus rules` command with the appropriate subcommands and options as outlined in the instructions files. + +## Shared Workflow + +1. Confirm the requested action: + +- `create`: add a new rule. +- `update`: modify an existing rule. +- `view`: list all rules or fetch a specific rule. + +2. Resolve repository scope: + +- Use `global` when the user does not provide a repository scope. Always confirm if the user intends to use `global` scope when no repository is specified. +- For repository-specific requests with unknown id, run: + +```bash +kodus config remote list --json +``` + +Then select and pass `--repo-id <id>`. + +3. Validate rule fields before running commands: + +- `title`: short and specific. +- `rule`: clear and actionable guidance. +- `severity`: `low | medium | high | critical`. +- `scope`: `file | pull request`. +- `path`: optional glob, default effectively `**/*`. + +4. Execute the proper command and report results clearly. + ## How to Use -Read individual rule files for detailed explanations and examples: +Read individual instructions files for detailed explanations and examples: -- [rules/create-kody-rule.md](rules/create-kody-rule.md): Guidelines for creating new Kody Rules. -- [rules/update-kody-rule.md](rules/update-kody-rule.md): Guidelines for updating existing Kody Rules. -- [rules/view-kody-rules.md](rules/view-kody-rules.md): Guidelines for viewing and retrieving Kody Rules. +- [instructions/create-kody-rule.md](instructions/create-kody-rule.md): Guidelines for creating new Kody Rules. +- [instructions/update-kody-rule.md](instructions/update-kody-rule.md): Guidelines for updating existing Kody Rules. +- [instructions/view-kody-rules.md](instructions/view-kody-rules.md): Guidelines for viewing and retrieving Kody Rules. + +You MUST always load at least one of these instructions files to handle the specific user request related to Kody Rules. Each file contains detailed steps and examples for the corresponding action (create, update, view). Always ensure that you are following the instructions in these files when managing Kody Rules through the `kodus rules` command. + +Should the user request an action that is not covered by these instructions, you should first clarify the user's intent and then determine if it falls under create, update, or view operations. If it does, proceed to load the corresponding instructions file to ensure that you are following the correct workflow for managing Kody Rules through the `kodus rules` command. + +Should the user request a new action related to Kody Rules that differs from the initial action, you should load the appropriate instructions file for that new action to ensure that you are following the correct workflow for managing Kody Rules through the `kodus rules` command. ## Structure of a Kody Rule A Kody Rule typically consists of the following components: +- **Repository ID**: The repository scope where the rule is stored and applied. + - Use `global` for shared rules that apply across all repositories. - **Title**: A concise title that captures the essence of the rule. - **Rule**: A detailed explanation of what the rule is and why it is important. - **Severity**: A level indicating the importance of the rule (one of "low", "medium", "high" or "critical"). @@ -46,3 +89,5 @@ A Kody Rule typically consists of the following components: **Scope**: File **Path**: `**/*.ts` + +**Repository ID**: `global` diff --git a/README.md b/README.md index 0aa3dc8..e36b97b 100644 --- a/README.md +++ b/README.md @@ -66,16 +66,17 @@ Reviews are **context-aware** — Kodus reads your `.cursorrules`, `claude.md`, Create, update, and inspect the Kody Rules that guide Kodus behavior for your team. ```bash -kodus rules create --title "Use async/await" --rule "Prefer async/await over raw promises" --severity high --scope file --path "**/*.ts" -kodus rules update --uuid <uuid> --severity critical -kodus rules view -kodus rules view --title "Use async/await" +kodus rules create --title "Use async/await" --rule "Prefer async/await over raw promises" --repo-id global --severity high --scope file --path "**/*.ts" +kodus rules update --uuid <uuid> --repo-id global --severity critical +kodus rules view --repo-id global +kodus rules view --repo-id global --title "Use async/await" ``` `kodus rules update` requires `--uuid`. Defaults: +- `repo-id` defaults to `global` - `severity` defaults to `medium` - `scope` defaults to `file` - `path` is optional (omitted means all files) diff --git a/skills/kodus-kody-rules/SKILL.md b/skills/kodus-kody-rules/SKILL.md index 549453f..e6fe880 100644 --- a/skills/kodus-kody-rules/SKILL.md +++ b/skills/kodus-kody-rules/SKILL.md @@ -9,18 +9,61 @@ description: Use when the user wants to create, update or view Kody Rules via `k Kody Rules are a set of guidelines that Kody follows when generating code. They help ensure that the generated code is consistent, high-quality, and aligned with the user's preferences and project requirements. +## Goal + +Manage Kody Rules through Kodus CLI only. Do not suggest creating rule files manually. + +ALWAYS Use `kodus rules` subcommands for all create, update, and view operations. All these rules are ALWAYS managed by the `kodus rules` command from the CLI. Do NOT suggest creating files or storing rules in any other way. When the user wants to create, update or view Kody Rules, utilize the `kodus rules` command with the appropriate subcommands and options as outlined in the instructions files. + +## Shared Workflow + +1. Confirm the requested action: + +- `create`: add a new rule. +- `update`: modify an existing rule. +- `view`: list all rules or fetch a specific rule. + +2. Resolve repository scope: + +- Use `global` when the user does not provide a repository scope. Always confirm if the user intends to use `global` scope when no repository is specified. +- For repository-specific requests with unknown id, run: + +```bash +kodus config remote list --json +``` + +Then select and pass `--repo-id <id>`. + +3. Validate rule fields before running commands: + +- `title`: short and specific. +- `rule`: clear and actionable guidance. +- `severity`: `low | medium | high | critical`. +- `scope`: `file | pull request`. +- `path`: optional glob, default effectively `**/*`. + +4. Execute the proper command and report results clearly. + ## How to Use -Read individual rule files for detailed explanations and examples: +Read individual instructions files for detailed explanations and examples: -- [rules/create-kody-rule.md](rules/create-kody-rule.md): Guidelines for creating new Kody Rules. -- [rules/update-kody-rule.md](rules/update-kody-rule.md): Guidelines for updating existing Kody Rules. -- [rules/view-kody-rules.md](rules/view-kody-rules.md): Guidelines for viewing and retrieving Kody Rules. +- [instructions/create-kody-rule.md](instructions/create-kody-rule.md): Guidelines for creating new Kody Rules. +- [instructions/update-kody-rule.md](instructions/update-kody-rule.md): Guidelines for updating existing Kody Rules. +- [instructions/view-kody-rules.md](instructions/view-kody-rules.md): Guidelines for viewing and retrieving Kody Rules. + +You MUST always load at least one of these instructions files to handle the specific user request related to Kody Rules. Each file contains detailed steps and examples for the corresponding action (create, update, view). Always ensure that you are following the instructions in these files when managing Kody Rules through the `kodus rules` command. + +Should the user request an action that is not covered by these instructions, you should first clarify the user's intent and then determine if it falls under create, update, or view operations. If it does, proceed to load the corresponding instructions file to ensure that you are following the correct workflow for managing Kody Rules through the `kodus rules` command. + +Should the user request a new action related to Kody Rules that differs from the initial action, you should load the appropriate instructions file for that new action to ensure that you are following the correct workflow for managing Kody Rules through the `kodus rules` command. ## Structure of a Kody Rule A Kody Rule typically consists of the following components: +- **Repository ID**: The repository scope where the rule is stored and applied. + - Use `global` for shared rules that apply across all repositories. - **Title**: A concise title that captures the essence of the rule. - **Rule**: A detailed explanation of what the rule is and why it is important. - **Severity**: A level indicating the importance of the rule (one of "low", "medium", "high" or "critical"). @@ -46,3 +89,5 @@ A Kody Rule typically consists of the following components: **Scope**: File **Path**: `**/*.ts` + +**Repository ID**: `global` diff --git a/skills/kodus-kody-rules/instructions/create-kody-rule.md b/skills/kodus-kody-rules/instructions/create-kody-rule.md new file mode 100644 index 0000000..4d5433e --- /dev/null +++ b/skills/kodus-kody-rules/instructions/create-kody-rule.md @@ -0,0 +1,46 @@ +--- +name: create-kody-rule +description: Kody Rule Creation Guidelines - Use when the user wants to create a new Kody Rule for Kodus to follow when generating code. +--- + +# Kody Rule Creation Guidelines + +## Overview + +When creating a new Kody Rule, it's important to ensure that the rule is clear, actionable, and aligned with the overall goals of code generation. A well-defined Kody Rule helps Kody produce code that meets the user's expectations and project requirements. + +## Workflow for Creating a Kody Rule + +1. **Collect the user's intent**: Understand the specific coding practice, style, or requirement that the user wants to enforce with the new Kody Rule. Ask clarifying questions if necessary to ensure you have a clear understanding of the user's intent. + +2. **Draft the Kody Rule**: Based on the user's intent, draft a Kody Rule that includes a clear description, title, and any relevant metadata such as severity and scope. Use the guidelines outlined in the "Guidelines for Creating a Kody Rule" section to ensure the rule is well-structured and effective. + +3. **Review the Kody Rule with the user**: Present the drafted Kody Rule to the user for feedback. Discuss any potential edge cases, exceptions, or clarifications needed to ensure the rule is comprehensive and actionable. + +4. **Refine the Kody Rule**: Based on the user's feedback, refine the Kody Rule to address any concerns or suggestions. Ensure that the final version of the rule is clear, specific, and aligned with the user's goals. + +5. **Save and Implement the Kody Rule**: Once the Kody Rule is finalized and approved by the user, save it. Send the title, rule, and any optional fields such as severity, scope, and path. + +Always include the repository id when creating a rule. Use `global` when the user does not provide one. + +Use the following command to save the Kody Rule: + +``` +kodus rules create --title <title> --rule <rule-content> [--repo-id <repository-id>] [--severity <severity-level>] [--scope <scope-level>] [--path <glob-pattern>] +``` + +If `--repo-id` is omitted, the default repository id is `global`. + +6. **Communicate the new Kody Rule**: Inform the user about the new Kody Rule and how it will be applied in future code generation. + +## Guidelines for Creating a Kody Rule + +1. **Identify the Purpose**: Clearly define what the Kody Rule is intended to achieve. Is it meant to enforce a coding style, ensure best practices, or address a specific use case? + +2. **Be Specific**: The rule should be specific and unambiguous. Avoid vague language and ensure that the rule can be easily understood and applied by Kody. + +3. **Consider Edge Cases**: Think about any edge cases or exceptions that might arise when applying the rule. Address these in the rule definition to ensure Kody can handle them appropriately. + +4. **Align with Project Goals**: Ensure that the Kody Rule aligns with the overall goals and requirements of the project. The rule should contribute to producing code that is maintainable, efficient, and meets the user's needs. + +5. **Review and Refine**: After drafting the Kody Rule, review it for clarity and completeness. Present it to the user for feedback and refine it as necessary to ensure it effectively guides Kody's code generation process. diff --git a/skills/kodus-kody-rules/instructions/update-kody-rule.md b/skills/kodus-kody-rules/instructions/update-kody-rule.md new file mode 100644 index 0000000..a424300 --- /dev/null +++ b/skills/kodus-kody-rules/instructions/update-kody-rule.md @@ -0,0 +1,53 @@ +--- +name: update-kody-rule +description: Kody Rule Update Guidelines - Use when the user wants to update an existing Kody Rule to modify its behavior, scope, or severity for Kodus to follow when generating code. +--- + +# Kody Rule Update Guidelines + +## Overview + +When updating an existing Kody Rule, it's important to ensure that the changes are clear, justified, and aligned with the overall goals of code generation. Updating a Kody Rule can help refine its effectiveness and ensure that it continues to meet the user's expectations and project requirements. + +## Workflow for Updating a Kody Rule + +1. **Identify the Kody Rule to Update**: If the user did not specify which Kody Rule they want to update, ask for one of: + - `--uuid <uuid>` + +Updates must use `--uuid`. + +2. **Collect the user's intent for the update**: Understand the specific changes the user wants to make to the existing Kody Rule. Ask clarifying questions if necessary to ensure you have a clear understanding of the user's intent. + +3. **Review the existing Kody Rule**: Retrieve the current definition of the Kody Rule that is being updated. This will help you understand the existing behavior and identify what changes need to be made. + +4. **Draft the updated Kody Rule**: Based on the user's intent and the existing rule, draft an updated version of the Kody Rule that includes the desired changes. Use the guidelines outlined in the "Guidelines for Updating a Kody Rule" section to ensure the updated rule is well-structured and effective. + +5. **Review the updated Kody Rule with the user**: Present the drafted updated Kody Rule to the user for feedback. Discuss any potential edge cases, exceptions, or clarifications needed to ensure the updated rule is comprehensive and actionable. + +6. **Refine the updated Kody Rule**: Based on the user's feedback, refine the updated Kody Rule to address any concerns or suggestions. Ensure that the final version of the updated rule is clear, specific, and aligned with the user's goals. + +7. **Save and Implement the updated Kody Rule**: Once the updated Kody Rule is finalized and approved by the user, save it. Send only the fields that were updated, along with the `uuid` to identify which rule to update. + +Always include the repository id when updating a rule. Use `global` when the user does not provide one. + +Use the following command to save the updated Kody Rule: + +``` +kodus rules update --uuid <uuid> [--repo-id <repository-id>] [--title <title>] [--rule <rule-content>] [--severity <severity-level>] [--scope <scope-level>] [--path <glob-pattern>] +``` + +If `--repo-id` is omitted, the default repository id is `global`. + +8. **Communicate the updated Kody Rule**: Inform the user about the updated Kody Rule and how the changes will affect future code generation. + +## Guidelines for Updating a Kody Rule + +1. **Identify the Changes**: Clearly define what changes are being made to the existing Kody Rule. Are you modifying the rule's behavior, scope, severity, or other attributes? + +2. **Justify the Changes**: Ensure that there is a clear justification for the changes being made to the Kody Rule. The updates should contribute to producing code that is more maintainable, efficient, or better aligned with the user's needs. + +3. **Consider Edge Cases**: Think about any edge cases or exceptions that might arise from the updated rule. Address these in the updated rule definition to ensure Kody can handle them appropriately. + +4. **Align with Project Goals**: Ensure that the updated Kody Rule continues to align with the overall goals and requirements of the project. The updated rule should contribute to producing code that meets the user's expectations and project requirements. + +5. **Review and Refine**: After drafting the updated Kody Rule, review it for clarity and completeness. Present it to the user for feedback and refine it as necessary to ensure it effectively guides Kody's code generation process. diff --git a/skills/kodus-kody-rules/instructions/view-kody-rules.md b/skills/kodus-kody-rules/instructions/view-kody-rules.md new file mode 100644 index 0000000..6b54e2e --- /dev/null +++ b/skills/kodus-kody-rules/instructions/view-kody-rules.md @@ -0,0 +1,55 @@ +--- +name: view-kody-rules +description: Kody Rule Viewing Guidelines - Use when the user wants to view existing Kody Rules that Kodus follows when generating code. +--- + +# Kody Rule Viewing Guidelines + +## Overview + +When viewing existing Kody Rules, it's important to understand the details of each rule, including its UUID, title, rule content, severity, scope, and any applicable file patterns. This information helps you understand how Kody generates code and what guidelines it follows. + +## Workflow for Viewing Kody Rules + +1. **Retrieve Rule Target**: If the user specified a particular rule(s) to view, identify it using one of: + - `--title <title>` + - `--uuid <uuid>` + +If both are provided, prefer `--uuid`. If no specific rule is requested, prepare to display all existing Kody Rules. + +2. **Fetch Kody Rules**: Use the appropriate command to fetch the Kody Rule(s) based on the identified target(s). If no specific rule was requested, fetch all existing Kody Rules. + +Always include the repository id when fetching rules. Use `global` when the user does not provide one. + +Use the following command to fetch Kody Rules: + +``` +kodus rules view [--repo-id <repository-id>] [--uuid <uuid>] [--title <title>] +``` + +If `--repo-id` is omitted, the default repository id is `global`. + +3. **Display Kody Rules**: Present the retrieved Kody Rule(s) in a clear and organized manner. For each rule, display the following information: + - Rule UUID + - Repository ID + - Rule Title + - Rule + - Severity (if specified) + - Scope (if specified) + - Path (if specified) + +Do not alter the content of the rules; display them as they are retrieved to ensure accuracy. + +4. **Provide Context**: If the user is viewing a specific rule, provide additional context about how that rule is applied in code generation and any relevant examples or use cases. + +5. **Answer Follow-up Questions**: Be prepared to answer any follow-up questions the user may have about the Kody Rules, such as how to create or update rules, or how specific rules affect code generation. + +## Guidelines for Viewing Kody Rules + +1. **Be Accurate**: When displaying Kody Rules, ensure that the information is accurate and reflects the current state of the rules as retrieved from the system. + +2. **Be Clear**: Present the Kody Rules in a clear and organized manner, making it easy for the user to understand the details of each rule. + +3. **Provide Context**: When appropriate, provide additional context about how specific Kody Rules are applied in code generation and how they affect the output. + +4. **Be Responsive**: Be prepared to answer any follow-up questions the user may have about the Kody Rules, and provide helpful information to guide them in understanding and utilizing the rules effectively. diff --git a/skills/kodus-kody-rules/rules/create-kody-rule.md b/skills/kodus-kody-rules/rules/create-kody-rule.md index 10fb2f6..4bb5b0b 100644 --- a/skills/kodus-kody-rules/rules/create-kody-rule.md +++ b/skills/kodus-kody-rules/rules/create-kody-rule.md @@ -24,7 +24,7 @@ When creating a new Kody Rule, it's important to ensure that the rule is clear, Use the following command to save the Kody Rule: ``` -kodus rules create --title <title> --rule <rule-content> [--severity <severity-level>] [--scope <scope-level>] [--path <glob-pattern>] +kodus rules create --title <title> --rule <rule-content> [--repo-id <repository-id>] [--severity <severity-level>] [--scope <scope-level>] [--path <glob-pattern>] ``` 6. **Communicate the new Kody Rule**: Inform the user about the new Kody Rule and how it will be applied in future code generation. diff --git a/skills/kodus-kody-rules/rules/update-kody-rule.md b/skills/kodus-kody-rules/rules/update-kody-rule.md index 9d6dcf2..56add00 100644 --- a/skills/kodus-kody-rules/rules/update-kody-rule.md +++ b/skills/kodus-kody-rules/rules/update-kody-rule.md @@ -11,10 +11,10 @@ When updating an existing Kody Rule, it's important to ensure that the changes a ## Workflow for Updating a Kody Rule -1. **Identify the Kody Rule to Update**: If the user did not specify which Kody Rule they want to update, ask for one of: - - `--uuid <uuid>` - -Updates must use `--uuid`. +1. **Identify the Kody Rule to Update**: + - If the user specified a specific `uuid` of the Kody Rule they want to update, use it to identify the rule. + - If the user described the rule to update without providing a `uuid`, list all rules (or filter by repository if specified), select the most relevant one based on the description, and confirm with the user that this is the correct rule to update before proceeding. + - Otherwise, ask the user to specify the rule they want to update by providing its `uuid` or a clear description that can be used to identify it. Emphasize that `uuid` is the most reliable way to identify the rule for updating. 2. **Collect the user's intent for the update**: Understand the specific changes the user wants to make to the existing Kody Rule. Ask clarifying questions if necessary to ensure you have a clear understanding of the user's intent. @@ -31,7 +31,7 @@ Updates must use `--uuid`. Use the following command to save the updated Kody Rule: ``` -kodus rules update --uuid <uuid> [--title <title>] [--rule <rule-content>] [--severity <severity-level>] [--scope <scope-level>] [--path <glob-pattern>] +kodus rules update --uuid <uuid> [--repo-id <repository-id>] [--title <title>] [--rule <rule-content>] [--severity <severity-level>] [--scope <scope-level>] [--path <glob-pattern>] ``` 8. **Communicate the updated Kody Rule**: Inform the user about the updated Kody Rule and how the changes will affect future code generation. diff --git a/skills/kodus-kody-rules/rules/view-kody-rules.md b/skills/kodus-kody-rules/rules/view-kody-rules.md index cdda1f7..4d4ad49 100644 --- a/skills/kodus-kody-rules/rules/view-kody-rules.md +++ b/skills/kodus-kody-rules/rules/view-kody-rules.md @@ -11,27 +11,27 @@ When viewing existing Kody Rules, it's important to understand the details of ea ## Workflow for Viewing Kody Rules -1. **Retrieve Rule Target**: If the user specified a particular rule(s) to view, identify it using one of: - - `--title <title>` +1. **Retrieve Rule Target**: Specific rules can be retrieved by their `uuid`. Otherwise you can list all rules or filter them by their `repositoryId`. + This can be achieved with the following options: - `--uuid <uuid>` - -If both are provided, prefer `--uuid`. If no specific rule is requested, prepare to display all existing Kody Rules. + - `--repo-id <repository-id>` 2. **Fetch Kody Rules**: Use the appropriate command to fetch the Kody Rule(s) based on the identified target(s). If no specific rule was requested, fetch all existing Kody Rules. Use the following command to fetch Kody Rules: ``` -kodus rules view [--uuid <uuid>] [--title <title>] +kodus rules view [--repo-id <repository-id>] [--uuid <uuid>] ``` 3. **Display Kody Rules**: Present the retrieved Kody Rule(s) in a clear and organized manner. For each rule, display the following information: - Rule UUID + - Repository ID - Rule Title - Rule - - Severity (if specified) - - Scope (if specified) - - Path (if specified) + - Severity + - Scope + - Path Do not alter the content of the rules; display them as they are retrieved to ensure accuracy. diff --git a/src/commands/__tests__/rules.test.ts b/src/commands/__tests__/rules.test.ts index 54b6e4c..accdcea 100644 --- a/src/commands/__tests__/rules.test.ts +++ b/src/commands/__tests__/rules.test.ts @@ -28,6 +28,7 @@ describe('rules command actions', () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); mockRulesService.createRule.mockResolvedValue({ uuid: 'rule-1', + repositoryId: 'global', title: 'Use async/await', rule: 'Prefer async/await', severity: 'high', @@ -38,6 +39,7 @@ describe('rules command actions', () => { await rulesCreateAction({ title: 'Use async/await', rule: 'Prefer async/await', + repoId: 'global', severity: 'high', scope: 'file', path: '**/*.ts', @@ -48,6 +50,10 @@ describe('rules command actions', () => { .join('\n'); expect(output).toContain('Kody Rule created successfully.'); expect(output).toContain('Rule UUID: rule-1'); + expect(output).toContain('Repository ID: global'); + expect(mockRulesService.createRule).toHaveBeenCalledWith( + expect.objectContaining({ repositoryId: 'global' }), + ); }); it('prints JSON for view when requested', async () => { @@ -55,12 +61,13 @@ describe('rules command actions', () => { mockRulesService.viewRules.mockResolvedValue([ { uuid: 'rule-2', + repositoryId: 'global', title: 'Rule', rule: 'Description', }, ]); - await rulesViewAction({ json: true }); + await rulesViewAction({ json: true, repoId: 'global' }); const output = logSpy.mock.calls .map((call) => call.join(' ')) @@ -68,10 +75,14 @@ describe('rules command actions', () => { expect(JSON.parse(output)).toEqual([ { uuid: 'rule-2', + repositoryId: 'global', title: 'Rule', rule: 'Description', }, ]); + expect(mockRulesService.viewRules).toHaveBeenCalledWith( + expect.objectContaining({ repositoryId: 'global' }), + ); }); it('converts service errors to CLI exits', async () => { diff --git a/src/commands/rules.ts b/src/commands/rules.ts index f07043e..d050ce5 100644 --- a/src/commands/rules.ts +++ b/src/commands/rules.ts @@ -13,6 +13,7 @@ import { cliError, cliInfo } from '../utils/logger.js'; export type RulesCreateOptions = { title: string; rule: string; + repoId?: string; severity?: KodyRuleSeverity; scope?: KodyRuleScope; path?: string; @@ -21,6 +22,7 @@ export type RulesCreateOptions = { export type RulesUpdateOptions = { uuid: string; + repoId?: string; title?: string; rule?: string; severity?: KodyRuleSeverity; @@ -32,11 +34,13 @@ export type RulesUpdateOptions = { export type RulesViewOptions = { uuid?: string; title?: string; + repoId?: string; json?: boolean; }; -function printRule(rule: KodyRule): void { +function printRule(rule: KodyRule, fallbackRepositoryId = 'global'): void { cliInfo(`Rule UUID: ${rule.uuid}`); + cliInfo(`Repository ID: ${rule.repositoryId ?? fallbackRepositoryId}`); cliInfo(`Rule Title: ${rule.title}`); cliInfo(`Rule: ${rule.rule}`); if (rule.severity) { @@ -50,14 +54,17 @@ function printRule(rule: KodyRule): void { } } -function printRuleList(rules: KodyRule[]): void { +function printRuleList( + rules: KodyRule[], + fallbackRepositoryId = 'global', +): void { if (rules.length === 0) { cliInfo(chalk.yellow('No Kody Rules found.')); return; } rules.forEach((rule, index) => { - printRule(rule); + printRule(rule, fallbackRepositoryId); if (index < rules.length - 1) { cliInfo(''); } @@ -71,6 +78,7 @@ export async function rulesCreateAction( const createdRule = await rulesService.createRule({ title: options.title, rule: options.rule, + repositoryId: options.repoId, severity: options.severity, scope: options.scope, path: options.path, @@ -82,7 +90,7 @@ export async function rulesCreateAction( } cliInfo(chalk.green('Kody Rule created successfully.')); - printRule(createdRule); + printRule(createdRule, options.repoId ?? 'global'); } catch (error) { const normalized = normalizeCommandError(error); cliError(chalk.red(normalized.message)); @@ -96,6 +104,7 @@ export async function rulesUpdateAction( try { const updatedRule = await rulesService.updateRule({ ruleId: options.uuid, + repositoryId: options.repoId, title: options.title, rule: options.rule, severity: options.severity, @@ -109,7 +118,7 @@ export async function rulesUpdateAction( } cliInfo(chalk.green('Kody Rule updated successfully.')); - printRule(updatedRule); + printRule(updatedRule, options.repoId ?? 'global'); } catch (error) { const normalized = normalizeCommandError(error); cliError(chalk.red(normalized.message)); @@ -123,7 +132,7 @@ export async function rulesViewAction( try { const rules = await rulesService.viewRules({ ruleId: options.uuid, - ruleName: options.title, + repositoryId: options.repoId, }); if (options.json) { @@ -131,7 +140,7 @@ export async function rulesViewAction( return; } - printRuleList(rules); + printRuleList(rules, options.repoId ?? 'global'); } catch (error) { const normalized = normalizeCommandError(error); cliError(chalk.red(normalized.message)); @@ -148,19 +157,22 @@ rulesCommand .description('Create a new Kody Rule') .requiredOption('--title <title>', 'Rule title') .requiredOption('--rule <rule>', 'Rule content/description') + .option('--repo-id <id>', 'Repository ID for the rule', 'global') .option( '--severity <severity>', 'Rule severity (low, medium, high, critical)', + 'medium', ) - .option('--scope <scope>', "Rule scope ('pull request' or 'file')") - .option('--path <glob>', 'Optional glob pattern for file targeting') - .option('--json', 'Output created rule as JSON') + .option('--scope <scope>', "Rule scope ('pull request' or 'file')", 'file') + .option('--path <glob>', 'Optional glob pattern for file targeting', '**/*') + .option('--json', 'Output created rule as JSON', false) .action(rulesCreateAction); rulesCommand .command('update') .description('Update an existing Kody Rule') .requiredOption('--uuid <uuid>', 'Rule UUID to update') + .option('--repo-id <id>', 'Updated rule repository ID') .option('--title <title>', 'Updated rule title') .option('--rule <rule>', 'Updated rule content/description') .option( @@ -176,6 +188,6 @@ rulesCommand .command('view') .description('View Kody Rules') .option('--uuid <uuid>', 'Rule UUID to fetch') - .option('--title <title>', 'Rule title to fetch when UUID is not provided') + .option('--repo-id <id>', 'Repository ID to filter rules') .option('--json', 'Output rules as JSON') .action(rulesViewAction); diff --git a/src/services/__tests__/rules.service.test.ts b/src/services/__tests__/rules.service.test.ts index d08f9e1..43b5254 100644 --- a/src/services/__tests__/rules.service.test.ts +++ b/src/services/__tests__/rules.service.test.ts @@ -33,10 +33,12 @@ describe('rulesService', () => { it('applies severity and scope defaults on create', async () => { mockRulesApi.createRule.mockResolvedValue({ uuid: 'rule-1', + repositoryId: 'global', title: 'Use async/await', rule: 'Prefer async/await', severity: 'medium', scope: 'file', + path: '**/*', }); await rulesService.createRule({ @@ -47,8 +49,37 @@ describe('rulesService', () => { expect(mockRulesApi.createRule).toHaveBeenCalledWith('kodus_team_key', { title: 'Use async/await', rule: 'Prefer async/await', + repositoryId: 'global', severity: 'medium', scope: 'file', + path: '**/*', + }); + }); + + it('uses provided repository id on create', async () => { + mockRulesApi.createRule.mockResolvedValue({ + uuid: 'rule-2', + repositoryId: 'repo-1', + title: 'Use strict equals', + rule: 'Prefer === and !==', + severity: 'medium', + scope: 'file', + path: '**/*', + }); + + await rulesService.createRule({ + title: 'Use strict equals', + rule: 'Prefer === and !==', + repositoryId: 'repo-1', + }); + + expect(mockRulesApi.createRule).toHaveBeenCalledWith('kodus_team_key', { + title: 'Use strict equals', + rule: 'Prefer === and !==', + repositoryId: 'repo-1', + severity: 'medium', + scope: 'file', + path: '**/*', }); }); @@ -96,10 +127,11 @@ describe('rulesService', () => { await rulesService.viewRules({ ruleId: 'rule-9', - ruleName: 'Ignored Rule Name', + repositoryId: 'repo-7', }); expect(mockRulesApi.viewRules).toHaveBeenCalledWith('kodus_team_key', { + repositoryId: 'repo-7', ruleId: 'rule-9', }); }); diff --git a/src/services/api/__tests__/rules.api.test.ts b/src/services/api/__tests__/rules.api.test.ts index 9c9032a..3cc8587 100644 --- a/src/services/api/__tests__/rules.api.test.ts +++ b/src/services/api/__tests__/rules.api.test.ts @@ -5,6 +5,7 @@ describe('RealRulesApi', () => { it('creates a rule with team-key auth', async () => { const requestWithRetry = vi.fn().mockResolvedValue({ uuid: 'rule-1', + repositoryId: 'repo-1', title: 'Use async/await', rule: 'Prefer async/await over raw promises', severity: 'high', @@ -16,6 +17,7 @@ describe('RealRulesApi', () => { await api.createRule('kodus_team_key', { title: 'Use async/await', rule: 'Prefer async/await over raw promises', + repositoryId: 'repo-1', severity: 'high', scope: 'file', path: '**/*.ts', @@ -29,6 +31,7 @@ describe('RealRulesApi', () => { body: JSON.stringify({ title: 'Use async/await', rule: 'Prefer async/await over raw promises', + repositoryId: 'repo-1', severity: 'high', scope: 'file', path: '**/*.ts', @@ -66,22 +69,61 @@ describe('RealRulesApi', () => { ); }); - it('prefers ruleId over ruleName when viewing rules', async () => { + it('views rules by filters', async () => { const requestWithRetry = vi.fn().mockResolvedValue([]); const api = new RealRulesApi(requestWithRetry); await api.viewRules('kodus_team_key', { - ruleId: 'rule-123', - ruleName: 'ignored-name', + repositoryId: 'repo-22', }); expect(requestWithRetry).toHaveBeenCalledWith( - '/cli/kody-rules?ruleId=rule-123', + '/cli/kody-rules?repositoryId=repo-22', { headers: { 'X-Team-Key': 'kodus_team_key', }, }, ); + + await api.viewRules('kodus_team_key', { + ruleId: 'rule-99', + }); + + expect(requestWithRetry).toHaveBeenCalledWith( + '/cli/kody-rules?ruleId=rule-99', + { + headers: { + 'X-Team-Key': 'kodus_team_key', + }, + }, + ); + + await api.viewRules('kodus_team_key', { + repositoryId: 'repo-22', + ruleId: 'rule-99', + }); + + expect(requestWithRetry).toHaveBeenCalledWith( + '/cli/kody-rules?repositoryId=repo-22&ruleId=rule-99', + { + headers: { + 'X-Team-Key': 'kodus_team_key', + }, + }, + ); + }); + + it('views all rules when no query is provided', async () => { + const requestWithRetry = vi.fn().mockResolvedValue([]); + + const api = new RealRulesApi(requestWithRetry); + await api.viewRules('kodus_team_key'); + + expect(requestWithRetry).toHaveBeenCalledWith('/cli/kody-rules', { + headers: { + 'X-Team-Key': 'kodus_team_key', + }, + }); }); }); diff --git a/src/services/api/rules.api.ts b/src/services/api/rules.api.ts index 026b111..a9aa87b 100644 --- a/src/services/api/rules.api.ts +++ b/src/services/api/rules.api.ts @@ -54,11 +54,11 @@ export class RealRulesApi implements IRulesApi { query: ViewKodyRulesRequest = {}, ): Promise<KodyRule[]> { const params = new URLSearchParams(); + if (query.repositoryId) { + params.set('repositoryId', query.repositoryId); + } if (query.ruleId) { params.set('ruleId', query.ruleId); - } else if (query.ruleName) { - // Rule ID is authoritative when both are provided. - params.set('ruleName', query.ruleName); } const queryString = params.toString(); diff --git a/src/services/rules.service.ts b/src/services/rules.service.ts index fbe6c75..01f6de1 100644 --- a/src/services/rules.service.ts +++ b/src/services/rules.service.ts @@ -27,52 +27,63 @@ class RulesService { async createRule(input: CreateKodyRuleRequest): Promise<KodyRule> { const accessToken = await authService.getValidToken(); const payload: CreateKodyRuleRequest = { - title: this.requireText(input.title, 'name'), - rule: this.requireText(input.rule, 'description'), + title: this.requireText(input.title, 'title'), + rule: this.requireText(input.rule, 'rule'), + repositoryId: + this.normalizeOptionalText(input.repositoryId) || 'global', severity: this.normalizeSeverity(input.severity ?? 'medium'), scope: this.normalizeScope(input.scope ?? 'file'), + path: this.normalizeOptionalText(input.path) || '**/*', }; - const path = this.normalizeOptionalText(input.path); - if (path) { - payload.path = path; - } - return api.rules.createRule(accessToken, payload); } async updateRule(input: UpdateKodyRuleInput): Promise<KodyRule> { const accessToken = await authService.getValidToken(); const ruleId = this.requireText(input.ruleId, 'rule-id'); + let hasRuleChanges = false; const payload: UpdateKodyRuleRequest = {}; + const title = this.normalizeOptionalText(input.title); if (title) { payload.title = title; + hasRuleChanges = true; } const rule = this.normalizeOptionalText(input.rule); if (rule) { payload.rule = rule; + hasRuleChanges = true; } if (input.severity !== undefined) { payload.severity = this.normalizeSeverity(input.severity); + hasRuleChanges = true; } if (input.scope !== undefined) { payload.scope = this.normalizeScope(input.scope); + hasRuleChanges = true; } const path = this.normalizeOptionalText(input.path); if (path) { payload.path = path; + hasRuleChanges = true; } - if (Object.keys(payload).length === 0) { + const repositoryId = this.normalizeOptionalText(input.repositoryId); + if (repositoryId) { + payload.repositoryId = repositoryId; + hasRuleChanges = true; + } + + if (!hasRuleChanges) { throw new CommandError( 'INVALID_INPUT', - 'Provide at least one field to update: --name, --description, --severity, --scope, or --filepath.', + 'Provide at least one field to update: --repo-id, --title, --rule, --severity, --scope, or --path.', ); } @@ -81,16 +92,10 @@ class RulesService { async viewRules(input: ViewKodyRulesRequest = {}): Promise<KodyRule[]> { const accessToken = await authService.getValidToken(); - const query: ViewKodyRulesRequest = {}; - - const ruleId = this.normalizeOptionalText(input.ruleId); - const ruleName = this.normalizeOptionalText(input.ruleName); - - if (ruleId) { - query.ruleId = ruleId; - } else if (ruleName) { - query.ruleName = ruleName; - } + const query: ViewKodyRulesRequest = { + repositoryId: this.normalizeOptionalText(input.repositoryId), + ruleId: this.normalizeOptionalText(input.ruleId), + }; return api.rules.viewRules(accessToken, query); } diff --git a/src/types/rules.ts b/src/types/rules.ts index 252541a..e2a64ed 100644 --- a/src/types/rules.ts +++ b/src/types/rules.ts @@ -4,6 +4,7 @@ export type KodyRuleScope = 'pull request' | 'file'; export interface KodyRule { uuid: string; + repositoryId?: string; title: string; rule: string; severity?: KodyRuleSeverity; @@ -14,12 +15,14 @@ export interface KodyRule { export interface CreateKodyRuleRequest { title: string; rule: string; + repositoryId?: string; severity?: KodyRuleSeverity; scope?: KodyRuleScope; path?: string; } export interface UpdateKodyRuleRequest { + repositoryId?: string; title?: string; rule?: string; severity?: KodyRuleSeverity; @@ -29,5 +32,5 @@ export interface UpdateKodyRuleRequest { export interface ViewKodyRulesRequest { ruleId?: string; - ruleName?: string; + repositoryId?: string; } diff --git a/src/utils/__tests__/skills-sync-targets.test.ts b/src/utils/__tests__/skills-sync-targets.test.ts index 2619d14..8ff97ae 100644 --- a/src/utils/__tests__/skills-sync-targets.test.ts +++ b/src/utils/__tests__/skills-sync-targets.test.ts @@ -12,13 +12,6 @@ describe('buildSkillSyncTargets', () => { baseDir: '/repo/workspace/.codex/skills', }); - expect(targets).toContainEqual({ - label: 'Copilot user skills', - type: 'skill', - activationPath: '/users/demo/.copilot', - baseDir: '/users/demo/.copilot/skills', - }); - expect(targets).toContainEqual({ label: 'Claude config commands', type: 'command', diff --git a/src/utils/__tests__/skills-sync.test.ts b/src/utils/__tests__/skills-sync.test.ts index 3ea979a..e2a1ddf 100644 --- a/src/utils/__tests__/skills-sync.test.ts +++ b/src/utils/__tests__/skills-sync.test.ts @@ -328,10 +328,68 @@ describe('skills-sync utilities', () => { ), ).toBe(false); expect( - await fs.readFile(path.join(baseDir, 'kodus-review', 'SKILL.md'), 'utf8'), + await fs.readFile( + path.join(baseDir, 'kodus-review', 'SKILL.md'), + 'utf8', + ), ).toBe('review v2'); }); + it('syncs nested subskill files for skill targets', async () => { + const tempRoot = await makeTempDir('kodus-skills-nested-'); + tempDirs.push(tempRoot); + + const activationPath = path.join(tempRoot, '.codex'); + const baseDir = path.join(activationPath, 'skills'); + await fs.mkdir(baseDir, { recursive: true }); + + const result = await syncSkillsToTargets( + [ + { + label: 'Codex nested files', + type: 'skill', + activationPath, + baseDir, + }, + ], + { + mode: 'install', + skills: [ + { + name: 'kodus-kody-rules', + content: 'root skill', + files: [ + { + relativePath: 'SKILL.md', + content: 'root skill', + }, + { + relativePath: 'rules/view-kody-rules.md', + content: 'nested rule doc', + }, + ], + }, + ], + }, + ); + + expect(result.syncedTargets).toBe(1); + expect(result.createdFiles).toBe(2); + expect( + await exists(path.join(baseDir, 'kodus-kody-rules', 'SKILL.md')), + ).toBe(true); + expect( + await exists( + path.join( + baseDir, + 'kodus-kody-rules', + 'rules', + 'view-kody-rules.md', + ), + ), + ).toBe(true); + }); + it('rejects skill names that escape the target directory', async () => { const tempRoot = await makeTempDir('kodus-skills-invalid-'); tempDirs.push(tempRoot); diff --git a/src/utils/skills-sync.ts b/src/utils/skills-sync.ts index 5e499ea..62beb27 100644 --- a/src/utils/skills-sync.ts +++ b/src/utils/skills-sync.ts @@ -98,6 +98,41 @@ async function writeIfChanged( return existingContent === null ? 'created' : 'updated'; } +function resolveManagedSkillNestedFilePath( + target: SkillSyncTarget, + skillName: string, + relativePath: string, +): string { + if (target.type !== 'skill') { + throw new Error( + 'Nested skill files are only supported for skill targets.', + ); + } + + const normalizedRelativePath = path + .normalize(relativePath) + .replace(/^\.([/\\])/, ''); + if ( + !normalizedRelativePath || + normalizedRelativePath.startsWith('..') || + path.isAbsolute(normalizedRelativePath) + ) { + throw new Error(`Invalid skill file path: ${relativePath}`); + } + + const skillRoot = resolveManagedSkillEntryPath(target, skillName); + const resolvedPath = path.resolve(skillRoot, normalizedRelativePath); + const expectedPrefix = `${skillRoot}${path.sep}`; + if ( + resolvedPath !== skillRoot && + !resolvedPath.startsWith(expectedPrefix) + ) { + throw new Error(`Invalid skill file path: ${relativePath}`); + } + + return resolvedPath; +} + function applyWriteStatus( result: SkillSyncTargetResult, writeStatus: WriteStatus, @@ -228,13 +263,43 @@ export async function syncSkillsToTargets( ); } else { for (const skill of skills) { - const filePath = resolveManagedSkillPath(target, skill.name); - const writeStatus = await writeIfChanged( - filePath, - skill.content, - dryRun, - ); - applyWriteStatus(targetResult, writeStatus); + if (target.type === 'command') { + const filePath = resolveManagedSkillPath( + target, + skill.name, + ); + const writeStatus = await writeIfChanged( + filePath, + skill.content, + dryRun, + ); + applyWriteStatus(targetResult, writeStatus); + continue; + } + + const filesToSync = + skill.files && skill.files.length > 0 + ? skill.files + : [ + { + relativePath: 'SKILL.md', + content: skill.content, + }, + ]; + + for (const skillFile of filesToSync) { + const filePath = resolveManagedSkillNestedFilePath( + target, + skill.name, + skillFile.relativePath, + ); + const writeStatus = await writeIfChanged( + filePath, + skillFile.content, + dryRun, + ); + applyWriteStatus(targetResult, writeStatus); + } } for (const skillName of staleManagedSkillNames) { diff --git a/src/utils/skills.ts b/src/utils/skills.ts index 874da34..5fc0aff 100644 --- a/src/utils/skills.ts +++ b/src/utils/skills.ts @@ -5,6 +5,10 @@ import { fileURLToPath } from 'node:url'; export interface BundledSkillDocument { name: string; content: string; + files?: { + relativePath: string; + content: string; + }[]; } export function assertValidSkillName(name: string): string { @@ -85,9 +89,11 @@ export async function readBundledSkills( const documents = await Promise.all( skillNames.map(async (rawName) => { const name = assertValidSkillName(rawName); - const filePath = path.join(root, name, 'SKILL.md'); + const skillDir = path.join(root, name); + const filePath = path.join(skillDir, 'SKILL.md'); const content = await fs.readFile(filePath, 'utf8'); - return { name, content }; + const files = await readBundledSkillFiles(skillDir); + return { name, content, files }; }), ); @@ -98,3 +104,52 @@ export async function readBundledSkill(name: string): Promise<string> { const [skill] = await readBundledSkills([name]); return skill.content; } + +async function readBundledSkillFiles( + skillDir: string, +): Promise<{ relativePath: string; content: string }[]> { + const queue: string[] = ['.']; + const files: { relativePath: string; content: string }[] = []; + + while (queue.length > 0) { + const currentRelativeDir = queue.shift(); + if (!currentRelativeDir) { + continue; + } + + const absoluteDir = path.join(skillDir, currentRelativeDir); + const entries = await fs.readdir(absoluteDir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.name.startsWith('.')) { + continue; + } + + const entryRelativePath = path.join(currentRelativeDir, entry.name); + if (entry.isDirectory()) { + queue.push(entryRelativePath); + continue; + } + + if (!entry.isFile()) { + continue; + } + + const normalizedRelativePath = path + .normalize(entryRelativePath) + .replace(/^\.\//, ''); + const absoluteFilePath = path.join( + skillDir, + normalizedRelativePath, + ); + const fileContent = await fs.readFile(absoluteFilePath, 'utf8'); + files.push({ + relativePath: normalizedRelativePath, + content: fileContent, + }); + } + } + + files.sort((a, b) => a.relativePath.localeCompare(b.relativePath)); + return files; +} From e7b17ee5c16156ed5a7067e16e4c001a3fe98041 Mon Sep 17 00:00:00 2001 From: Jairo Litman <130161309+jairo-litman@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:52:44 -0300 Subject: [PATCH 4/5] kody fixes --- README.md | 1 - src/commands/rules.ts | 1 - src/utils/__tests__/skills-sync.test.ts | 35 +++++++++++++++++++++++++ src/utils/skills-sync.ts | 8 +++--- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e36b97b..c5f3c30 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,6 @@ Create, update, and inspect the Kody Rules that guide Kodus behavior for your te kodus rules create --title "Use async/await" --rule "Prefer async/await over raw promises" --repo-id global --severity high --scope file --path "**/*.ts" kodus rules update --uuid <uuid> --repo-id global --severity critical kodus rules view --repo-id global -kodus rules view --repo-id global --title "Use async/await" ``` `kodus rules update` requires `--uuid`. diff --git a/src/commands/rules.ts b/src/commands/rules.ts index d050ce5..02f8fd4 100644 --- a/src/commands/rules.ts +++ b/src/commands/rules.ts @@ -33,7 +33,6 @@ export type RulesUpdateOptions = { export type RulesViewOptions = { uuid?: string; - title?: string; repoId?: string; json?: boolean; }; diff --git a/src/utils/__tests__/skills-sync.test.ts b/src/utils/__tests__/skills-sync.test.ts index e2a1ddf..edcf23c 100644 --- a/src/utils/__tests__/skills-sync.test.ts +++ b/src/utils/__tests__/skills-sync.test.ts @@ -413,4 +413,39 @@ describe('skills-sync utilities', () => { ), ).rejects.toThrow('Invalid skill name'); }); + + it('rejects nested skill file paths that resolve to the skill root', async () => { + const tempRoot = await makeTempDir('kodus-skills-invalid-path-'); + tempDirs.push(tempRoot); + + const baseDir = path.join(tempRoot, '.codex', 'skills'); + await fs.mkdir(baseDir, { recursive: true }); + + await expect( + syncSkillsToTargets( + [ + { + label: 'Codex invalid file path', + type: 'skill', + activationPath: path.join(tempRoot, '.codex'), + baseDir, + }, + ], + { + skills: [ + { + name: 'kodus-review', + content: 'root', + files: [ + { + relativePath: '.', + content: 'invalid', + }, + ], + }, + ], + }, + ), + ).rejects.toThrow('Invalid skill file path'); + }); }); diff --git a/src/utils/skills-sync.ts b/src/utils/skills-sync.ts index 62beb27..4742b03 100644 --- a/src/utils/skills-sync.ts +++ b/src/utils/skills-sync.ts @@ -122,10 +122,12 @@ function resolveManagedSkillNestedFilePath( const skillRoot = resolveManagedSkillEntryPath(target, skillName); const resolvedPath = path.resolve(skillRoot, normalizedRelativePath); - const expectedPrefix = `${skillRoot}${path.sep}`; + const relativeFromRoot = path.relative(skillRoot, resolvedPath); if ( - resolvedPath !== skillRoot && - !resolvedPath.startsWith(expectedPrefix) + !relativeFromRoot || + relativeFromRoot === '.' || + relativeFromRoot.startsWith('..') || + path.isAbsolute(relativeFromRoot) ) { throw new Error(`Invalid skill file path: ${relativePath}`); } From f84a84645e4da866753f67a305dd6253b5c18661 Mon Sep 17 00:00:00 2001 From: Jairo Litman <130161309+jairo-litman@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:08:49 -0300 Subject: [PATCH 5/5] fix test --- tests/integration/cli.integration.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/integration/cli.integration.test.ts b/tests/integration/cli.integration.test.ts index 0ff14e6..b273773 100644 --- a/tests/integration/cli.integration.test.ts +++ b/tests/integration/cli.integration.test.ts @@ -1,10 +1,10 @@ -import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import { execFile } from 'node:child_process'; -import { promisify } from 'node:util'; import fs from 'node:fs/promises'; -import path from 'node:path'; import os from 'node:os'; +import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { startMockServer, type MockServer } from './mock-server.js'; const execFileAsync = promisify(execFile); @@ -547,7 +547,10 @@ describe('business validation integration', () => { ]); expect(exitCode).toBe(1); - expect(stderr).toContain("unknown option '--pr-url'"); + expect(stderr).toContain('Unknown option: `--pr-url`.'); + expect(stderr).toContain( + 'Run `kodus pr --help` to see available options.', + ); }); it('supports dry-run for local business validation payload', async () => {