diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1d4c9b5..050b4c1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -13,7 +13,8 @@ vibe-rules/ │ ├── commands/ # CLI command action handlers (Added) │ │ └── install.ts # Action handler for the 'install' command (Added) │ ├── index.ts # Main exports -│ ├── types.ts # Type definitions (Updated: RuleType.UNIFIED) +│ ├── types.ts # Type definitions (Updated: RuleType.UNIFIED, explicit editor lists, clean type validation) +│ ├── genericTypeUtilities.ts # Reusable TypeScript utility types for validation (Added) │ ├── schemas.ts # Zod schema definitions │ ├── llms/ # Rule definitions for package export │ │ ├── index.ts # Public rule export (intentionally empty) @@ -88,8 +89,11 @@ It handles parsing of command-line arguments, options, and delegates the executi - Determines the target file path based on editor type, options (`-g, --global`, `-t, --target`), and context. - Uses the appropriate `RuleProvider` to format and apply the rule (`appendFormattedRule`). - Suggests similar rule names if the requested rule is not found. -- `install [packageName] [options]` +- `install [packageName] [options]` (Updated) - Defines the CLI options and arguments for the install command. + - Now supports `all` as a special editor argument to install rules to all supported editors at once. + - **New behavior:** When `` is `all`, installs rules to all supported editors (cursor, windsurf, claude-code, codex, clinerules, zed, unified) simultaneously. + - Includes guardrail preventing use of `--target` option with `all` since each editor has different file structures. - Delegates the action to `installCommandAction` from `src/commands/install.ts`. ### src/commands/install.ts (Added) @@ -98,12 +102,18 @@ Contains the action handler for the `vibe-rules install` command. #### Functions -- `installCommandAction(editor: string, packageName: string | undefined, options: { global?: boolean; target?: string; debug?: boolean }): Promise` (Exported) +- `installCommandAction(editor: string, packageName: string | undefined, options: { global?: boolean; target?: string; debug?: boolean }): Promise` (Exported, Updated) - Main handler for the `install` command. - - If `packageName` is provided, it calls `installSinglePackage` for that specific package. - - If `packageName` is not provided, it reads `package.json`, gets all dependencies and devDependencies, and calls `installSinglePackage` for each. - - Uses `getRuleProvider` to get the appropriate provider for the editor. - - Handles overall error reporting for the command. + - **New functionality:** Supports `editor` argument of `"all"` to install rules to all supported editors at once. + - **Multi-editor processing:** When `editor` is `"all"`, uses the explicit `ALL_SUPPORTED_EDITORS` array from `types.ts` for robust editor management. + - **Runtime validation:** Calls `validateAllSupportedEditorsHaveProviders()` to ensure all editors have working providers before processing. + - **Guardrails:** Prevents use of `--target` option with `"all"` since each editor has different file structures. + - **Enhanced logging:** Provides detailed progress output and summary when processing multiple editors. + - If `packageName` is provided, it calls `installSinglePackage` for that specific package for each editor. + - If `packageName` is not provided, it reads `package.json`, gets all dependencies and devDependencies, and calls `installSinglePackage` for each dependency for each editor. + - Uses `getRuleProvider` to get the appropriate provider for each editor. + - **Installation Summary:** When processing multiple editors, provides a comprehensive summary showing success/failure status and rule counts for each editor. + - Handles overall error reporting for the command with graceful degradation for individual editor failures. - `installSinglePackage(pkgName: string, editorType: RuleType, provider: RuleProvider, installOptions: { global?: boolean; target?: string; debug?: boolean }): Promise` - Installs rules from a single specified NPM package. - Dynamically imports `/llms` using `importModuleFromCwd`. @@ -148,6 +158,36 @@ Defines the core types and interfaces used throughout the application. - `UNIFIED`: "unified" - For the unified `.rules` file convention (Added) - `CUSTOM`: "custom" - For custom implementations +#### `ALL_SUPPORTED_EDITORS` (Added) + +- **Explicit array of editors included in "install all" functionality** +- **Critical for maintenance**: When adding a new editor to `RuleType`, developers **MUST** consciously decide whether to include it here +- Contains: `[CURSOR, WINDSURF, CLAUDE_CODE, CODEX, CLINERULES, ZED, UNIFIED]` +- **Purpose**: Ensures new editors are intentionally included or excluded from "all" operations + +#### `EXCLUDED_FROM_ALL_EDITORS` (Added) + +- **Explicit array of editors excluded from "install all" functionality** +- Contains excluded editors with documentation of why: + - `ROO`: Alias for CLINERULES, avoid duplication + - `CUSTOM`: Custom implementations, not standardized +- **Purpose**: Makes exclusions explicit and documented + +#### TypeScript Compile-Time Validation (Added) + +- **Automated enforcement**: Clean generic utility types ensure all `RuleType` values are accounted for +- **Implementation**: Uses `AssertAreEqual` from `src/genericTypeUtilities.ts` to validate that the union of `ALL_SUPPORTED_EDITORS` and `EXCLUDED_FROM_ALL_EDITORS` exactly equals all `RuleType` values +- **Compile-time safety**: Adding a new editor without updating the lists will show clear TypeScript errors +- **Error guidance**: TypeScript errors indicate which editors are missing and need to be addressed +- **Clean code**: Uses reusable generic utility types instead of complex inline type checking + +#### `validateAllSupportedEditorsHaveProviders()` (Added) + +- **Runtime validation helper** that ensures all editors in `ALL_SUPPORTED_EDITORS` have working providers +- Returns `{ valid: boolean; missing: RuleType[] }` +- **Usage**: Called during "install all" to detect configuration issues +- **Purpose**: Prevents runtime failures by validating provider availability upfront + #### `RuleProvider` - Interface that providers must implement @@ -168,6 +208,26 @@ Defines the core types and interfaces used throughout the application. - `alwaysApply?`: boolean - Cursor-specific metadata. - `globs?`: string | string[] - Cursor-specific metadata. +### src/genericTypeUtilities.ts (Added) + +Provides reusable TypeScript utility types for compile-time validation and type assertions. + +#### Generic Utility Types + +- `AssertIsEmpty`: Ensures a type is empty (`never`). Returns `true` if empty, otherwise returns an error object with details. +- `AssertAreEqual`: Ensures two types are exactly equal. Returns `true` if equal, otherwise returns an error object showing the differences. +- `Intersection`: Gets the intersection of two types using `T & U`. +- `Union`: Gets the union of two types using `T | U`. +- `ArrayIntersection`: Gets overlapping values between two readonly arrays as union types using `Extract`. +- `ArrayUnion`: Combines two readonly arrays into a single union type. + +#### Purpose + +- **Reusability**: These utilities can be used throughout the codebase for type validation +- **Clear error messages**: Assertion types provide helpful error details when validation fails +- **Compile-time safety**: Catches type issues during development rather than runtime +- **Clean code**: Eliminates complex inline type checking in favor of readable assertions + ### src/schemas.ts (Added) Defines Zod schemas for validating rule configurations. diff --git a/README.md b/README.md index eea7595..4f93f20 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ vibe-rules load my-rule-name cursor -t ./my-project/.cursor-rules/custom-rule.md Arguments: - ``: The name of the rule saved in the local store (`~/.vibe-rules/rules/`). -- ``: The target editor/tool type. Supported: `cursor`, `windsurf`, `claude-code`, `codex`, `clinerules`, `roo`. +- ``: The target editor/tool type. Supported: `cursor`, `windsurf`, `claude-code`, `codex`, `clinerules`, `roo`, `unified`, `zed`, `all`. Options: @@ -119,6 +119,12 @@ We anticipate more NPM packages will begin exporting standardized AI configurati Install rules _directly from NPM packages_ into an editor's configuration. `vibe-rules` automatically scans your project's dependencies or a specified package for compatible rule exports. ```bash +# Install rules from ALL dependencies for ALL supported editors +vibe-rules install all + +# Install rules from a specific package for ALL supported editors +vibe-rules install all my-rule-package + # Most common: Install rules from ALL dependencies/devDependencies for Cursor # Scans package.json, finds packages with 'llms' export, applies rules. vibe-rules install cursor @@ -138,13 +144,16 @@ Add the `--debug` global option to any `vibe-rules` command to enable detailed d Arguments: -- ``: The target editor/tool type (mandatory). Supported: `cursor`, `windsurf`, `claude-code`, `codex`, `clinerules`, `roo`. +- ``: The target editor/tool type (mandatory). Supported: `cursor`, `windsurf`, `claude-code`, `codex`, `clinerules`, `roo`, `unified`, `zed`, `all`. - `[packageName]` (Optional): The specific NPM package name to install rules from. If omitted, `vibe-rules` scans all dependencies and devDependencies in your project's `package.json`. Options: - `-g, --global`: Apply to the editor's global configuration path (if supported). -- `-t, --target `: Specify a custom target file path or directory. +- `-t, --target `: Specify a custom target file path or directory. **Note:** Cannot be used with `all` editor option since each editor has different file structures. + +**Special Editor Option:** +- `all`: Installs rules to ALL supported editors at once. This is perfect for setting up your entire development environment from a single command. When using `all`, rules are installed to each editor's default location using their respective formats. **How `install` finds rules:** @@ -198,6 +207,24 @@ bun link # Now you can use 'vibe-rules' command locally ``` +### Adding Support for New Editors + +`vibe-rules` uses a robust system to ensure new editors are properly handled. When adding support for a new editor: + +1. **Add the editor to `RuleType`** in `src/types.ts` +2. **Create a provider** implementing the `RuleProvider` interface in `src/providers/` +3. **Update the provider factory** in `src/providers/index.ts` +4. **Make a conscious decision** about "install all" behavior by adding the new editor to either: + - `ALL_SUPPORTED_EDITORS` (included in `vibe-rules install all`) + - `EXCLUDED_FROM_ALL_EDITORS` (excluded with documented reason) + +**TypeScript will enforce this workflow** - the build will fail with clear error messages if you forget to update the explicit editor lists. This ensures: +- All editors are consciously included or excluded from "all" operations +- New editors don't accidentally break existing functionality +- The reasoning for exclusions is documented + +See `ARCHITECTURE.md` for detailed information about the editor management system and provider interfaces. + ## License MIT diff --git a/src/cli.ts b/src/cli.ts index d6293e4..4c271e5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -242,7 +242,7 @@ program ) .argument( "", - "Target editor type (cursor, windsurf, claude-code, codex, clinerules, roo)" + "Target editor type (cursor, windsurf, claude-code, codex, clinerules, roo, unified, zed, all)" ) .argument("[packageName]", "Optional NPM package name to install rules from") .option( diff --git a/src/commands/install.ts b/src/commands/install.ts index d408431..2536053 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -500,10 +500,35 @@ async function installSinglePackage( return rulesSuccessfullyAppliedCount; } +/** + * Editors that should be included when using "install all" + * When adding a new editor to RuleType, you MUST decide whether to include it here. + * This explicit list ensures new editors are consciously included or excluded. + */ +export const INSTALL_ALL_INCLUDES = [ + RuleType.CURSOR, + RuleType.WINDSURF, + RuleType.CLAUDE_CODE, + RuleType.CODEX, + RuleType.CLINERULES, + RuleType.ZED, + RuleType.UNIFIED, +] as const satisfies RuleType[]; + +/** + * Editors that are aliases or should not be included in "all" functionality + * These are explicitly excluded with documentation of why. + */ +export const INSTALL_ALL_EXCLUDES = [ + RuleType.ROO, // Alias for CLINERULES, avoid duplication + RuleType.CUSTOM, // Custom implementations, not standardized +] as const satisfies RuleType[]; + + /** * Handles the 'install' command logic. * Installs rules from an NPM package or all dependencies into an editor configuration. - * @param editor Target editor type (cursor, windsurf, claude-code, codex, clinerules, roo) + * @param editor Target editor type (cursor, windsurf, claude-code, codex, clinerules, roo, unified, zed, all) * @param packageName Optional NPM package name to install rules from * @param options Command options including global, target, and debug */ @@ -512,100 +537,180 @@ export async function installCommandAction( packageName: string | undefined, options: { global?: boolean; target?: string; debug?: boolean } ): Promise { - const editorType = editor.toLowerCase() as RuleType; - let provider: RuleProvider; + const editorArg = editor.toLowerCase(); const combinedOptions = { ...options, debug: isDebugEnabled }; // Use isDebugEnabled from cli.ts - try { - provider = getRuleProvider(editorType); - } catch (e) { - console.error(chalk.red(`Invalid editor type specified: ${editor}`)); + // Guardrail: Disallow --target with 'all' + if (options.target && editorArg === 'all') { + console.error(chalk.red("The --target option cannot be used when installing for 'all' editors.")); + console.error(chalk.yellow("Each editor has different file structures and target path conventions.")); process.exit(1); } - if (packageName) { - const count = await installSinglePackage( - packageName, - editorType, - provider, - combinedOptions - ); - if (!isDebugEnabled && count > 0) { - console.log( - chalk.green( - `[vibe-rules] Successfully installed ${count} rule(s) from package '${packageName}'.` - ) - ); + // 1. Identify which editors to process + const editorsToProcess: RuleType[] = [...INSTALL_ALL_INCLUDES]; + if (editorArg === 'all') { + + console.log(chalk.blue(`Installing rules for all supported editors: ${editorsToProcess.join(', ')}`)); + + if (packageName) { + console.log(chalk.blue(`Package: ${packageName}`)); + } else { + console.log(chalk.blue(`Scanning all dependencies in package.json...`)); } } else { - console.log( - chalk.blue( - `[vibe-rules] Installing rules from all dependencies in package.json for ${editor}...` - ) - ); + // Logic for a single, specified editor + const editorType = editorArg as RuleType; + + // Validate editor type + if (!Object.values(RuleType).includes(editorType)) { + console.error(chalk.red(`Invalid editor type specified: ${editor}`)); + console.error(chalk.yellow(`Supported editors: ${Object.values(RuleType).join(', ')}`)); + process.exit(1); + } + + editorsToProcess.push(editorType); + } + + // 2. Loop through the list of editors + let totalRulesInstalled = 0; + const results: { editor: string; count: number; success: boolean }[] = []; + + for (const editorType of editorsToProcess) { + if (editorsToProcess.length > 1) { + console.log(chalk.cyan(`\n--- Processing editor: ${editorType} ---`)); + } + + let provider: RuleProvider; try { - const pkgJsonPath = path.join(process.cwd(), "package.json"); - if (!fs.existsSync(pkgJsonPath)) { - console.error( - chalk.red("package.json not found in the current directory.") - ); - process.exit(1); - } - const pkgJsonContent = await fs.readFile(pkgJsonPath, "utf-8"); - const { dependencies = {}, devDependencies = {} } = - JSON.parse(pkgJsonContent); - const allDeps = [ - ...Object.keys(dependencies), - ...Object.keys(devDependencies), - ]; - - if (allDeps.length === 0) { - console.log(chalk.yellow("No dependencies found in package.json.")); - return; - } + provider = getRuleProvider(editorType); + } catch (e) { + console.error(chalk.red(`Failed to get provider for editor: ${editorType}`)); + results.push({ editor: editorType, count: 0, success: false }); + continue; + } - debugLog( - chalk.blue( - `Found ${allDeps.length} dependencies. Checking for rules to install for ${editor}...` - ) - ); - let totalRulesInstalled = 0; - for (const depName of allDeps) { - totalRulesInstalled += await installSinglePackage( - depName, + try { + if (packageName) { + const count = await installSinglePackage( + packageName, editorType, provider, combinedOptions ); - } - if (!isDebugEnabled && totalRulesInstalled > 0) { - console.log( - chalk.green( - `[vibe-rules] Finished installing rules from dependencies. Total rules installed: ${totalRulesInstalled}.` - ) - ); - } else if ( - !isDebugEnabled && - totalRulesInstalled === 0 && - allDeps.length > 0 - ) { - console.log( - chalk.yellow( - "[vibe-rules] No rules found to install from dependencies." - ) - ); - } + results.push({ editor: editorType, count, success: true }); + totalRulesInstalled += count; + + if (!isDebugEnabled && count > 0 && editorsToProcess.length === 1) { + console.log( + chalk.green( + `[vibe-rules] Successfully installed ${count} rule(s) from package '${packageName}' for ${editorType}.` + ) + ); + } + } else { + // Install from all dependencies + const pkgJsonPath = path.join(process.cwd(), "package.json"); + if (!fs.existsSync(pkgJsonPath)) { + if (editorsToProcess.length === 1) { + console.error( + chalk.red("package.json not found in the current directory.") + ); + process.exit(1); + } else { + console.error( + chalk.red(`package.json not found - skipping all editor installations.`) + ); + return; + } + } + + const pkgJsonContent = await fs.readFile(pkgJsonPath, "utf-8"); + const { dependencies = {}, devDependencies = {} } = JSON.parse(pkgJsonContent); + const allDeps = [ + ...Object.keys(dependencies), + ...Object.keys(devDependencies), + ]; + + if (allDeps.length === 0) { + if (editorsToProcess.length === 1) { + console.log(chalk.yellow("No dependencies found in package.json.")); + return; + } else { + console.log(chalk.yellow(`No dependencies found for ${editorType}.`)); + results.push({ editor: editorType, count: 0, success: true }); + continue; + } + } - debugLog(chalk.green("Finished checking all dependencies.")); + let editorRulesInstalled = 0; + for (const depName of allDeps) { + editorRulesInstalled += await installSinglePackage( + depName, + editorType, + provider, + combinedOptions + ); + } + + results.push({ editor: editorType, count: editorRulesInstalled, success: true }); + totalRulesInstalled += editorRulesInstalled; + + if (!isDebugEnabled && editorRulesInstalled > 0 && editorsToProcess.length === 1) { + console.log( + chalk.green( + `[vibe-rules] Finished installing rules from dependencies for ${editorType}. Total rules installed: ${editorRulesInstalled}.` + ) + ); + } else if (!isDebugEnabled && editorRulesInstalled === 0 && allDeps.length > 0 && editorsToProcess.length === 1) { + console.log( + chalk.yellow( + `[vibe-rules] No rules found to install from dependencies for ${editorType}.` + ) + ); + } + } } catch (error) { console.error( chalk.red( - `Error processing package.json: ${ + `Error processing ${editorType}: ${ error instanceof Error ? error.message : error }` ) ); - process.exit(1); + results.push({ editor: editorType, count: 0, success: false }); + } + } + + // 3. Summary for "all" installations + if (editorsToProcess.length > 1) { + console.log(chalk.blue(`\n--- Installation Summary ---`)); + + const successful = results.filter(r => r.success); + const failed = results.filter(r => !r.success); + + if (successful.length > 0) { + console.log(chalk.green(`Successfully processed ${successful.length} editor(s):`)); + successful.forEach(r => { + if (r.count > 0) { + console.log(chalk.green(` • ${r.editor}: ${r.count} rule(s) installed`)); + } else { + console.log(chalk.yellow(` • ${r.editor}: no rules found to install`)); + } + }); + } + + if (failed.length > 0) { + console.log(chalk.red(`Failed to process ${failed.length} editor(s):`)); + failed.forEach(r => { + console.log(chalk.red(` • ${r.editor}: installation failed`)); + }); + } + + console.log(chalk.blue(`Total rules installed across all editors: ${totalRulesInstalled}`)); + + if (totalRulesInstalled === 0 && successful.length > 0) { + console.log(chalk.yellow("No rules were found to install from the specified source(s).")); } } } diff --git a/src/genericTypeUtilities.ts b/src/genericTypeUtilities.ts new file mode 100644 index 0000000..3d828b1 --- /dev/null +++ b/src/genericTypeUtilities.ts @@ -0,0 +1,59 @@ +/** + * Generic TypeScript utility types for compile-time validation and assertions + */ + +/** + * Get the distributive intersection of two types + */ +export type Intersection = T extends any ? U extends any ? U & T : never : never; + +/** + * Get the union of two types + */ +export type Union = T | U; + +/** + * Assert that a type is empty (never) + * Usage: const _check: AssertIsEmpty = true; + */ +export type AssertIsEmpty = [T] extends [never] + ? true + : { + ERROR: "Expected type to be empty (never), but it contains values"; + ACTUAL_TYPE: T; + }; + +/** + * Assert that two types are exactly equal + * Usage: const _check: AssertAreEqual = true; + */ +export type AssertAreEqual = [T] extends [U] + ? [U] extends [T] + ? true + : { + ERROR: "Types are not equal - second type is missing values from first"; + FIRST_TYPE: T; + SECOND_TYPE: U; + MISSING_FROM_SECOND: Exclude; + } + : { + ERROR: "Types are not equal - first type is missing values from second"; + FIRST_TYPE: T; + SECOND_TYPE: U; + MISSING_FROM_FIRST: Exclude; + }; + +/** + * Get the intersection of two array types (as union types) + * Useful for checking overlap between const arrays + * Returns never if there's no overlap, or the overlapping values if there is + */ +export type ArrayIntersection = + Extract; + +/** + * Get the union of two array types (as union types) + * Useful for combining const arrays into a single union type + */ +export type ArrayUnion = + T[number] | U[number]; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 3f8978c..a254f32 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,9 @@ /** * Rule type definitions for the vibe-rules utility */ +import type { INSTALL_ALL_EXCLUDES, INSTALL_ALL_INCLUDES } from './commands/install'; +import type { AssertAreEqual, AssertIsEmpty, Intersection } from './genericTypeUtilities'; + export type * from "./schemas"; export interface RuleConfig { name: string; @@ -24,6 +27,23 @@ export const RuleType = { export type RuleTypeArray = (typeof RuleType)[keyof typeof RuleType][]; export type RuleType = (typeof RuleType)[keyof typeof RuleType]; +/** + * TypeScript compile-time validation to ensure all editors are properly accounted for. + * Key benefit: When adding a new editor to RuleType, TypeScript will show a clear error + * indicating that the new editor must be added to either ALL_SUPPORTED_EDITORS or EXCLUDED_FROM_ALL_EDITORS. + */ +// Create type aliases for the array element unions +type AllSupportedEditorsUnion = (typeof INSTALL_ALL_INCLUDES)[number]; +type ExcludedEditorsUnion = (typeof INSTALL_ALL_EXCLUDES)[number]; +type AllRuleTypes = (typeof RuleType)[keyof typeof RuleType]; + +// Ensure the two editor arrays don't overlap (no editor should be in both lists) +// The arrays are expected to have no overlap +const _allEditorSetsDoNotOverlap: AssertIsEmpty> = true; + +// Ensure the union of both arrays exactly equals all RuleType values +const _allEditorSetsIncludeAllEditors: AssertAreEqual = true; + export interface RuleProvider { /** * Creates a new rule file with the given content