cadr-cli 0.0.1 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/adr.d.ts +50 -0
  2. package/dist/adr.d.ts.map +1 -0
  3. package/dist/adr.js +156 -0
  4. package/dist/adr.js.map +1 -0
  5. package/dist/adr.test.d.ts +8 -0
  6. package/dist/adr.test.d.ts.map +1 -0
  7. package/dist/adr.test.js +256 -0
  8. package/dist/adr.test.js.map +1 -0
  9. package/dist/analysis.d.ts +24 -0
  10. package/dist/analysis.d.ts.map +1 -0
  11. package/dist/analysis.js +281 -0
  12. package/dist/analysis.js.map +1 -0
  13. package/dist/analysis.test.d.ts +8 -0
  14. package/dist/analysis.test.d.ts.map +1 -0
  15. package/dist/analysis.test.js +351 -0
  16. package/dist/analysis.test.js.map +1 -0
  17. package/dist/commands/analyze.d.ts +14 -0
  18. package/dist/commands/analyze.d.ts.map +1 -0
  19. package/dist/commands/analyze.js +56 -0
  20. package/dist/commands/analyze.js.map +1 -0
  21. package/dist/commands/init.d.ts +12 -0
  22. package/dist/commands/init.d.ts.map +1 -0
  23. package/dist/commands/init.js +93 -0
  24. package/dist/commands/init.js.map +1 -0
  25. package/dist/commands/init.test.d.ts +2 -0
  26. package/dist/commands/init.test.d.ts.map +1 -0
  27. package/dist/commands/init.test.js +56 -0
  28. package/dist/commands/init.test.js.map +1 -0
  29. package/dist/config.d.ts +40 -0
  30. package/dist/config.d.ts.map +1 -0
  31. package/dist/config.js +208 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/config.test.d.ts +2 -0
  34. package/dist/config.test.d.ts.map +1 -0
  35. package/dist/config.test.js +97 -0
  36. package/dist/config.test.js.map +1 -0
  37. package/dist/git.d.ts +42 -0
  38. package/dist/git.d.ts.map +1 -1
  39. package/dist/git.js +157 -0
  40. package/dist/git.js.map +1 -1
  41. package/dist/index.d.ts +2 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +78 -62
  44. package/dist/index.js.map +1 -1
  45. package/dist/index.test.d.ts +2 -0
  46. package/dist/index.test.d.ts.map +1 -0
  47. package/dist/index.test.js +51 -0
  48. package/dist/index.test.js.map +1 -0
  49. package/dist/llm.d.ts +73 -0
  50. package/dist/llm.d.ts.map +1 -0
  51. package/dist/llm.js +264 -0
  52. package/dist/llm.js.map +1 -0
  53. package/dist/llm.test.d.ts +2 -0
  54. package/dist/llm.test.d.ts.map +1 -0
  55. package/dist/llm.test.js +592 -0
  56. package/dist/llm.test.js.map +1 -0
  57. package/dist/logger.d.ts.map +1 -1
  58. package/dist/logger.js +5 -3
  59. package/dist/logger.js.map +1 -1
  60. package/dist/logger.test.d.ts +2 -0
  61. package/dist/logger.test.d.ts.map +1 -0
  62. package/dist/logger.test.js +78 -0
  63. package/dist/logger.test.js.map +1 -0
  64. package/dist/prompts.d.ts +49 -0
  65. package/dist/prompts.d.ts.map +1 -0
  66. package/dist/prompts.js +195 -0
  67. package/dist/prompts.js.map +1 -0
  68. package/dist/prompts.test.d.ts +2 -0
  69. package/dist/prompts.test.d.ts.map +1 -0
  70. package/dist/prompts.test.js +427 -0
  71. package/dist/prompts.test.js.map +1 -0
  72. package/dist/providers/gemini.d.ts +3 -0
  73. package/dist/providers/gemini.d.ts.map +1 -0
  74. package/dist/providers/gemini.js +38 -0
  75. package/dist/providers/gemini.js.map +1 -0
  76. package/dist/providers/index.d.ts +2 -0
  77. package/dist/providers/index.d.ts.map +1 -0
  78. package/dist/providers/index.js +6 -0
  79. package/dist/providers/index.js.map +1 -0
  80. package/dist/providers/openai.d.ts +3 -0
  81. package/dist/providers/openai.d.ts.map +1 -0
  82. package/dist/providers/openai.js +24 -0
  83. package/dist/providers/openai.js.map +1 -0
  84. package/dist/providers/registry.d.ts +4 -0
  85. package/dist/providers/registry.d.ts.map +1 -0
  86. package/dist/providers/registry.js +16 -0
  87. package/dist/providers/registry.js.map +1 -0
  88. package/dist/providers/types.d.ts +11 -0
  89. package/dist/providers/types.d.ts.map +1 -0
  90. package/dist/providers/types.js +3 -0
  91. package/dist/providers/types.js.map +1 -0
  92. package/dist/version.test.d.ts +3 -0
  93. package/dist/version.test.d.ts.map +1 -0
  94. package/dist/version.test.js +25 -0
  95. package/dist/version.test.js.map +1 -0
  96. package/package.json +14 -5
  97. package/src/adr.test.ts +278 -0
  98. package/src/adr.ts +136 -0
  99. package/src/analysis.test.ts +396 -0
  100. package/src/analysis.ts +262 -0
  101. package/src/commands/analyze.ts +56 -0
  102. package/src/commands/init.test.ts +27 -0
  103. package/src/commands/init.ts +99 -0
  104. package/src/config.test.ts +79 -0
  105. package/src/config.ts +214 -0
  106. package/src/git.ts +240 -0
  107. package/src/index.test.ts +59 -0
  108. package/src/index.ts +80 -60
  109. package/src/llm.test.ts +701 -0
  110. package/src/llm.ts +345 -0
  111. package/src/logger.test.ts +90 -0
  112. package/src/logger.ts +6 -3
  113. package/src/prompts.test.ts +515 -0
  114. package/src/prompts.ts +174 -0
  115. package/src/providers/gemini.ts +41 -0
  116. package/src/providers/index.ts +1 -0
  117. package/src/providers/openai.ts +22 -0
  118. package/src/providers/registry.ts +16 -0
  119. package/src/providers/types.ts +12 -0
  120. package/src/version.test.ts +29 -0
  121. package/bin/cadr.js +0 -16
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Analyze Command
3
+ *
4
+ * Triggers LLM-powered analysis of code changes.
5
+ * Thin wrapper around analysis orchestration module.
6
+ */
7
+
8
+ import { runAnalysis } from '../analysis';
9
+ import { DiffOptions } from '../git';
10
+ import { loggerInstance as logger } from '../logger';
11
+
12
+ /**
13
+ * Execute the analyze command
14
+ * Analyzes code changes for architectural significance
15
+ *
16
+ * @param args - Command line arguments (e.g., ['--staged'], ['--all'])
17
+ */
18
+ export async function analyzeCommand(args: string[] = []): Promise<void> {
19
+ try {
20
+ // Parse command line flags to determine diff options
21
+ const diffOptions: DiffOptions = { mode: 'all' }; // Default to all uncommitted
22
+
23
+ // Check for --base flag (implies branch-diff mode)
24
+ const baseIndex = args.indexOf('--base');
25
+ if (baseIndex !== -1 && baseIndex + 1 < args.length) {
26
+ diffOptions.mode = 'branch-diff';
27
+ diffOptions.base = args[baseIndex + 1];
28
+
29
+ // Check for optional --head flag
30
+ const headIndex = args.indexOf('--head');
31
+ if (headIndex !== -1 && headIndex + 1 < args.length) {
32
+ diffOptions.head = args[headIndex + 1];
33
+ }
34
+ } else if (args.includes('--staged')) {
35
+ diffOptions.mode = 'staged';
36
+ } else if (args.includes('--all')) {
37
+ diffOptions.mode = 'all';
38
+ }
39
+
40
+ logger.info('Analyze command started', {
41
+ mode: diffOptions.mode,
42
+ base: diffOptions.base,
43
+ head: diffOptions.head
44
+ });
45
+ await runAnalysis(diffOptions);
46
+ logger.info('Analyze command completed');
47
+ } catch (error) {
48
+ // Fail-open: log error but don't throw
49
+ logger.error('Analyze command failed', { error });
50
+ // eslint-disable-next-line no-console
51
+ console.error('\nāŒ Analysis command failed');
52
+ // eslint-disable-next-line no-console
53
+ console.error('Please check the logs for more details.\n');
54
+ }
55
+ }
56
+
@@ -0,0 +1,27 @@
1
+ import { initCommand } from './init';
2
+ import * as config from '../config';
3
+
4
+ // Mock dependencies
5
+ jest.mock('../config');
6
+
7
+ describe('Init Command', () => {
8
+ beforeEach(() => {
9
+ jest.clearAllMocks();
10
+ });
11
+
12
+ describe('initCommand', () => {
13
+ test('calls createConfig when no config exists', async () => {
14
+ (config.createConfig as jest.Mock).mockResolvedValue({ provider: 'openai' });
15
+
16
+ await initCommand();
17
+
18
+ expect(config.createConfig).toHaveBeenCalled();
19
+ });
20
+
21
+ test('handles config creation errors gracefully', async () => {
22
+ (config.createConfig as jest.Mock).mockRejectedValue(new Error('Permission denied'));
23
+
24
+ await expect(initCommand()).resolves.not.toThrow();
25
+ });
26
+ });
27
+ });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Init Command
3
+ *
4
+ * Interactive command to create cadr.yaml configuration file.
5
+ * Handles user prompts and configuration creation.
6
+ */
7
+
8
+ import { createConfig, getDefaultConfigPath, validateConfig } from '../config';
9
+ import { loggerInstance as logger } from '../logger';
10
+ import { existsSync } from 'fs';
11
+
12
+ /**
13
+ * Execute the init command
14
+ * Creates configuration file interactively
15
+ */
16
+ export async function initCommand(): Promise<void> {
17
+ try {
18
+ const configPath = getDefaultConfigPath();
19
+
20
+ // Check if config already exists
21
+ if (existsSync(configPath)) {
22
+ // eslint-disable-next-line no-console
23
+ console.log(`\nā„¹ļø Configuration file already exists: ${configPath}`);
24
+ // eslint-disable-next-line no-console
25
+ console.log('šŸ’” To reconfigure, delete the file and run `cadr init` again.\n');
26
+ return;
27
+ }
28
+
29
+ // Create configuration interactively
30
+ const config = await createConfig(configPath);
31
+
32
+ if (!config) {
33
+ // eslint-disable-next-line no-console
34
+ console.error('\nāŒ Failed to create configuration');
35
+ // eslint-disable-next-line no-console
36
+ console.error('Please try again or check file permissions.\n');
37
+ return;
38
+ }
39
+
40
+ // Validate the created config
41
+ const validation = validateConfig(config);
42
+ if (!validation.valid) {
43
+ logger.warn('Created config has validation warnings', {
44
+ errors: validation.errors,
45
+ });
46
+ }
47
+
48
+ // Display next steps
49
+ // eslint-disable-next-line no-console
50
+ console.log('šŸ“‹ Configuration Summary:');
51
+ // eslint-disable-next-line no-console
52
+ console.log(` Provider: ${config.provider}`);
53
+ // eslint-disable-next-line no-console
54
+ console.log(` Model: ${config.analysis_model}`);
55
+ // eslint-disable-next-line no-console
56
+ console.log(` API Key Env: ${config.api_key_env}`);
57
+ // eslint-disable-next-line no-console
58
+ console.log(` Timeout: ${config.timeout_seconds}s`);
59
+ if (config.ignore_patterns && config.ignore_patterns.length > 0) {
60
+ // eslint-disable-next-line no-console
61
+ console.log(` Ignore Patterns: ${config.ignore_patterns.join(', ')}`);
62
+ }
63
+
64
+ // Check if API key is set
65
+ if (!process.env[config.api_key_env]) {
66
+ // eslint-disable-next-line no-console
67
+ console.warn(`\nāš ļø Warning: ${config.api_key_env} is not set in your environment`);
68
+ // eslint-disable-next-line no-console
69
+ console.warn('Set it before using analysis:');
70
+ // eslint-disable-next-line no-console
71
+ console.warn(` export ${config.api_key_env}="your-api-key-here"`);
72
+ // eslint-disable-next-line no-console
73
+ const providerLink = config.provider === 'gemini'
74
+ ? 'https://aistudio.google.com/app/apikey'
75
+ : 'https://platform.openai.com/api-keys';
76
+ // eslint-disable-next-line no-console
77
+ console.warn(`\n Get your API key from: ${providerLink}`);
78
+ }
79
+
80
+ // eslint-disable-next-line no-console
81
+ console.log('\nšŸŽ‰ Ready to analyze!');
82
+ // eslint-disable-next-line no-console
83
+ console.log('Next steps:');
84
+ // eslint-disable-next-line no-console
85
+ console.log(' 1. Stage your changes: git add <files>');
86
+ // eslint-disable-next-line no-console
87
+ console.log(' 2. Run analysis: cadr --analyze\n');
88
+
89
+ logger.info('Init command completed successfully');
90
+ } catch (error) {
91
+ // Fail-open: log error but don't throw
92
+ logger.error('Init command failed', { error });
93
+ // eslint-disable-next-line no-console
94
+ console.error('\nāŒ An unexpected error occurred during initialization');
95
+ // eslint-disable-next-line no-console
96
+ console.error('Please check the logs for more details.\n');
97
+ }
98
+ }
99
+
@@ -0,0 +1,79 @@
1
+ import { loadConfig, validateConfig, AnalysisConfig } from './config';
2
+ import * as fs from 'fs';
3
+ import * as yaml from 'js-yaml';
4
+
5
+ // Mock fs and yaml modules
6
+ jest.mock('fs');
7
+ jest.mock('js-yaml');
8
+
9
+ describe('Configuration Module', () => {
10
+ const mockConfig: AnalysisConfig = {
11
+ provider: 'openai',
12
+ analysis_model: 'gpt-4',
13
+ api_key_env: 'OPENAI_API_KEY',
14
+ timeout_seconds: 15
15
+ };
16
+
17
+ beforeEach(() => {
18
+ jest.clearAllMocks();
19
+ });
20
+
21
+ describe('loadConfig', () => {
22
+ test('loads valid configuration successfully', async () => {
23
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
24
+ (fs.readFileSync as jest.Mock).mockReturnValue('provider: openai\nanalysis_model: gpt-4');
25
+ (yaml.load as jest.Mock).mockReturnValue(mockConfig);
26
+
27
+ const result = await loadConfig('/test/cadr.yaml');
28
+
29
+ expect(result).toEqual(mockConfig);
30
+ expect(fs.existsSync).toHaveBeenCalledWith('/test/cadr.yaml');
31
+ expect(fs.readFileSync).toHaveBeenCalledWith('/test/cadr.yaml', 'utf-8');
32
+ });
33
+
34
+ test('returns null when config file does not exist', async () => {
35
+ (fs.existsSync as jest.Mock).mockReturnValue(false);
36
+
37
+ const result = await loadConfig('/test/cadr.yaml');
38
+
39
+ expect(result).toBeNull();
40
+ });
41
+
42
+ test('returns null when config file is invalid', async () => {
43
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
44
+ (fs.readFileSync as jest.Mock).mockReturnValue('invalid yaml');
45
+ (yaml.load as jest.Mock).mockImplementation(() => {
46
+ throw new Error('Invalid YAML');
47
+ });
48
+
49
+ const result = await loadConfig('/test/cadr.yaml');
50
+
51
+ expect(result).toBeNull();
52
+ });
53
+ });
54
+
55
+ describe('validateConfig', () => {
56
+ test('validates correct configuration', () => {
57
+ const result = validateConfig(mockConfig);
58
+
59
+ expect(result.valid).toBe(true);
60
+ expect(result.errors).toHaveLength(0);
61
+ });
62
+
63
+ test('rejects invalid provider', () => {
64
+ const invalidConfig = { ...mockConfig, provider: 'invalid' };
65
+ const result = validateConfig(invalidConfig);
66
+
67
+ expect(result.valid).toBe(false);
68
+ expect(result.errors.join('\n')).toMatch(/provider must be one of the following values: openai, gemini/);
69
+ });
70
+
71
+ test('rejects missing required fields', () => {
72
+ const invalidConfig = { provider: 'openai' };
73
+ const result = validateConfig(invalidConfig);
74
+
75
+ expect(result.valid).toBe(false);
76
+ expect(result.errors.length).toBeGreaterThan(0);
77
+ });
78
+ });
79
+ });
package/src/config.ts ADDED
@@ -0,0 +1,214 @@
1
+ import * as yaml from 'js-yaml';
2
+ import * as yup from 'yup';
3
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
4
+ import * as readline from 'readline';
5
+ import { loggerInstance as logger } from './logger';
6
+
7
+ /**
8
+ * Configuration schema for LLM analysis
9
+ */
10
+ export interface AnalysisConfig {
11
+ provider: 'openai' | 'gemini';
12
+ analysis_model: string;
13
+ api_key_env: string;
14
+ timeout_seconds: number;
15
+ ignore_patterns?: string[];
16
+ }
17
+
18
+ /**
19
+ * Configuration validation result
20
+ */
21
+ export interface ConfigValidationResult {
22
+ valid: boolean;
23
+ errors: string[];
24
+ }
25
+
26
+ /**
27
+ * Yup validation schema for configuration
28
+ */
29
+ const configSchema = yup.object({
30
+ provider: yup
31
+ .string()
32
+ .oneOf(['openai', 'gemini'])
33
+ .required('Provider must be one of: "openai", "gemini"'),
34
+ analysis_model: yup.string().required('Analysis model is required'),
35
+ api_key_env: yup.string().required('API key environment variable name is required'),
36
+ timeout_seconds: yup
37
+ .number()
38
+ .min(1, 'timeout_seconds must be at least 1 second')
39
+ .max(60, 'timeout_seconds must not exceed 60 seconds')
40
+ .required('timeout_seconds is required'),
41
+ ignore_patterns: yup.array().of(yup.string()).optional(),
42
+ });
43
+
44
+ /**
45
+ * Load and validate configuration from YAML file
46
+ * @param configPath - Path to configuration file
47
+ * @returns Promise resolving to validated config or null on error
48
+ */
49
+ export async function loadConfig(configPath: string): Promise<AnalysisConfig | null> {
50
+ try {
51
+ // Check if config file exists
52
+ if (!existsSync(configPath)) {
53
+ logger.warn('Configuration file not found', { configPath });
54
+ return null;
55
+ }
56
+
57
+ // Read and parse YAML
58
+ const fileContent = readFileSync(configPath, 'utf-8');
59
+ const parsedConfig = yaml.load(fileContent);
60
+
61
+ if (!parsedConfig || typeof parsedConfig !== 'object') {
62
+ logger.error('Invalid YAML configuration', { configPath });
63
+ return null;
64
+ }
65
+
66
+ // Validate configuration
67
+ const validationResult = validateConfig(parsedConfig);
68
+ if (!validationResult.valid) {
69
+ logger.error('Configuration validation failed', {
70
+ configPath,
71
+ errors: validationResult.errors,
72
+ });
73
+ return null;
74
+ }
75
+
76
+ const config = parsedConfig as AnalysisConfig;
77
+
78
+ // Warn if API key environment variable is not set
79
+ if (!process.env[config.api_key_env]) {
80
+ logger.warn('API key environment variable is not set', {
81
+ api_key_env: config.api_key_env,
82
+ });
83
+ }
84
+
85
+ logger.info('Configuration loaded successfully', { configPath, provider: config.provider });
86
+ return config;
87
+ } catch (error) {
88
+ logger.error('Failed to load configuration', { error, configPath });
89
+ return null;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Validate configuration object against schema
95
+ * @param config - Configuration object to validate
96
+ * @returns Validation result with errors if any
97
+ */
98
+ export function validateConfig(config: unknown): ConfigValidationResult {
99
+ try {
100
+ configSchema.validateSync(config, { abortEarly: false });
101
+ return { valid: true, errors: [] };
102
+ } catch (error) {
103
+ if (error instanceof yup.ValidationError) {
104
+ return {
105
+ valid: false,
106
+ errors: error.errors,
107
+ };
108
+ }
109
+ return {
110
+ valid: false,
111
+ errors: ['Unknown validation error'],
112
+ };
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Prompt user for input with default value
118
+ */
119
+ function prompt(rl: readline.Interface, question: string, defaultValue?: string): Promise<string> {
120
+ return new Promise((resolve) => {
121
+ const promptText = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
122
+ rl.question(promptText, (answer) => {
123
+ resolve(answer.trim() || defaultValue || '');
124
+ });
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Create configuration interactively
130
+ * @param configPath - Path where config should be created
131
+ * @returns Promise resolving to created config or null on error
132
+ */
133
+ export async function createConfig(configPath: string): Promise<AnalysisConfig | null> {
134
+ try {
135
+ const rl = readline.createInterface({
136
+ input: process.stdin,
137
+ output: process.stdout,
138
+ });
139
+
140
+ // eslint-disable-next-line no-console
141
+ console.log('\nšŸ”§ cADR Configuration Setup\n');
142
+
143
+ // Prompt for configuration values
144
+ const provider = await prompt(rl, 'LLM Provider', 'openai');
145
+
146
+ // Choose sensible defaults based on provider
147
+ const defaultModel = provider === 'gemini' ? 'gemini-1.5-pro' : 'gpt-4';
148
+ const defaultApiKeyEnv = provider === 'gemini' ? 'GEMINI_API_KEY' : 'OPENAI_API_KEY';
149
+
150
+ const analysis_model = await prompt(rl, 'Analysis Model', defaultModel);
151
+ const api_key_env = await prompt(rl, 'API Key Environment Variable', defaultApiKeyEnv);
152
+ const timeoutInput = await prompt(rl, 'Timeout (seconds)', '15');
153
+ const timeout_seconds = parseInt(timeoutInput, 10) || 15;
154
+
155
+ const ignorePatternsInput = await prompt(
156
+ rl,
157
+ 'Ignore Patterns (comma-separated, optional)',
158
+ '*.md,package-lock.json'
159
+ );
160
+ const ignore_patterns =
161
+ ignorePatternsInput
162
+ .split(',')
163
+ .map((p) => p.trim())
164
+ .filter((p) => p.length > 0) || undefined;
165
+
166
+ rl.close();
167
+
168
+ // Build configuration object
169
+ const config: AnalysisConfig = {
170
+ provider: provider as 'openai' | 'gemini',
171
+ analysis_model,
172
+ api_key_env,
173
+ timeout_seconds,
174
+ ...(ignore_patterns && ignore_patterns.length > 0 ? { ignore_patterns } : {}),
175
+ };
176
+
177
+ // Validate configuration
178
+ const validationResult = validateConfig(config);
179
+ if (!validationResult.valid) {
180
+ // eslint-disable-next-line no-console
181
+ console.error('\nāŒ Configuration validation failed:');
182
+ // eslint-disable-next-line no-console
183
+ validationResult.errors.forEach((error) => console.error(` - ${error}`));
184
+ return null;
185
+ }
186
+
187
+ // Convert to YAML
188
+ const yamlContent = yaml.dump(config, {
189
+ indent: 2,
190
+ lineWidth: -1,
191
+ noRefs: true,
192
+ });
193
+
194
+ // Write to file
195
+ writeFileSync(configPath, yamlContent, 'utf-8');
196
+
197
+ logger.info('Configuration file created successfully', { configPath });
198
+ // eslint-disable-next-line no-console
199
+ console.log(`\nāœ… Configuration saved to: ${configPath}\n`);
200
+
201
+ return config;
202
+ } catch (error) {
203
+ logger.error('Failed to create configuration', { error, configPath });
204
+ return null;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Get default configuration file path
210
+ */
211
+ export function getDefaultConfigPath(): string {
212
+ return 'cadr.yaml';
213
+ }
214
+