@grunnverk/kilde 0.1.0

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 (75) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
  3. package/.github/pull_request_template.md +48 -0
  4. package/.github/workflows/deploy-docs.yml +59 -0
  5. package/.github/workflows/npm-publish.yml +48 -0
  6. package/.github/workflows/test.yml +48 -0
  7. package/CHANGELOG.md +92 -0
  8. package/CONTRIBUTING.md +438 -0
  9. package/LICENSE +190 -0
  10. package/PROJECT_SUMMARY.md +318 -0
  11. package/README.md +444 -0
  12. package/RELEASE_CHECKLIST.md +182 -0
  13. package/dist/application.js +166 -0
  14. package/dist/application.js.map +1 -0
  15. package/dist/commands/release.js +326 -0
  16. package/dist/commands/release.js.map +1 -0
  17. package/dist/constants.js +122 -0
  18. package/dist/constants.js.map +1 -0
  19. package/dist/logging.js +176 -0
  20. package/dist/logging.js.map +1 -0
  21. package/dist/main.js +24 -0
  22. package/dist/main.js.map +1 -0
  23. package/dist/mcp-server.js +17467 -0
  24. package/dist/mcp-server.js.map +7 -0
  25. package/dist/utils/config.js +89 -0
  26. package/dist/utils/config.js.map +1 -0
  27. package/docs/AI_GUIDE.md +618 -0
  28. package/eslint.config.mjs +85 -0
  29. package/guide/architecture.md +776 -0
  30. package/guide/commands.md +580 -0
  31. package/guide/configuration.md +779 -0
  32. package/guide/mcp-integration.md +708 -0
  33. package/guide/overview.md +225 -0
  34. package/package.json +91 -0
  35. package/scripts/build-mcp.js +115 -0
  36. package/scripts/test-mcp-compliance.js +254 -0
  37. package/src/application.ts +246 -0
  38. package/src/commands/release.ts +450 -0
  39. package/src/constants.ts +162 -0
  40. package/src/logging.ts +210 -0
  41. package/src/main.ts +25 -0
  42. package/src/mcp/prompts/index.ts +98 -0
  43. package/src/mcp/resources.ts +121 -0
  44. package/src/mcp/server.ts +195 -0
  45. package/src/mcp/tools.ts +219 -0
  46. package/src/types.ts +131 -0
  47. package/src/utils/config.ts +181 -0
  48. package/tests/application.test.ts +114 -0
  49. package/tests/commands/commit.test.ts +248 -0
  50. package/tests/commands/release.test.ts +325 -0
  51. package/tests/constants.test.ts +118 -0
  52. package/tests/logging.test.ts +142 -0
  53. package/tests/mcp/prompts/index.test.ts +202 -0
  54. package/tests/mcp/resources.test.ts +166 -0
  55. package/tests/mcp/tools.test.ts +211 -0
  56. package/tests/utils/config.test.ts +212 -0
  57. package/tsconfig.json +32 -0
  58. package/vite.config.ts +107 -0
  59. package/vitest.config.ts +40 -0
  60. package/website/index.html +14 -0
  61. package/website/src/App.css +142 -0
  62. package/website/src/App.tsx +34 -0
  63. package/website/src/components/Commands.tsx +182 -0
  64. package/website/src/components/Configuration.tsx +214 -0
  65. package/website/src/components/Examples.tsx +234 -0
  66. package/website/src/components/Footer.css +99 -0
  67. package/website/src/components/Footer.tsx +93 -0
  68. package/website/src/components/GettingStarted.tsx +94 -0
  69. package/website/src/components/Hero.css +95 -0
  70. package/website/src/components/Hero.tsx +50 -0
  71. package/website/src/components/Navigation.css +102 -0
  72. package/website/src/components/Navigation.tsx +57 -0
  73. package/website/src/index.css +36 -0
  74. package/website/src/main.tsx +10 -0
  75. package/website/vite.config.ts +12 -0
@@ -0,0 +1,181 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import { getLogger } from '../logging';
5
+ import { Config } from '@grunnverk/core';
6
+ import { KILDE_DEFAULTS } from '../constants';
7
+
8
+ const CONFIG_FILES = [
9
+ '.kilde/config.yaml',
10
+ '.kilde/config.yml',
11
+ '.kilderc.yaml',
12
+ '.kilderc.yml',
13
+ '.kilderc.json',
14
+ 'kilde.config.json',
15
+ ];
16
+
17
+ /**
18
+ * Load configuration from file
19
+ */
20
+ export async function loadConfig(cwd: string = process.cwd()): Promise<Config | null> {
21
+ const logger = getLogger();
22
+
23
+ for (const filename of CONFIG_FILES) {
24
+ const configPath = path.join(cwd, filename);
25
+
26
+ try {
27
+ const content = await fs.readFile(configPath, 'utf-8');
28
+
29
+ let config: any;
30
+ if (filename.endsWith('.json')) {
31
+ config = JSON.parse(content);
32
+ } else if (filename.endsWith('.yaml') || filename.endsWith('.yml')) {
33
+ config = yaml.load(content);
34
+ } else {
35
+ // Try JSON first, then YAML
36
+ try {
37
+ config = JSON.parse(content);
38
+ } catch {
39
+ config = yaml.load(content);
40
+ }
41
+ }
42
+
43
+ // Return config as-is
44
+ logger.verbose(`Loaded configuration from ${configPath}`);
45
+ return config as Config;
46
+ } catch (error: any) {
47
+ if (error.code !== 'ENOENT') {
48
+ logger.warn(`CONFIG_LOAD_FAILED: Failed to load configuration file | Path: ${configPath} | Error: ${error.message} | Action: Using defaults`);
49
+ }
50
+ }
51
+ }
52
+
53
+ logger.verbose('No configuration file found, using defaults');
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Get default configuration
59
+ */
60
+ export function getDefaultConfig(): Config {
61
+ return KILDE_DEFAULTS;
62
+ }
63
+
64
+ /**
65
+ * Deep merge two objects
66
+ */
67
+ function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
68
+ const result = { ...target };
69
+
70
+ for (const key in source) {
71
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
72
+ const sourceValue = source[key];
73
+ const targetValue = result[key];
74
+
75
+ if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue) &&
76
+ targetValue && typeof targetValue === 'object' && !Array.isArray(targetValue)) {
77
+ result[key] = deepMerge(targetValue, sourceValue) as any;
78
+ } else if (sourceValue !== undefined) {
79
+ result[key] = sourceValue as any;
80
+ }
81
+ }
82
+ }
83
+
84
+ return result;
85
+ }
86
+
87
+ /**
88
+ * Merge loaded config with defaults
89
+ */
90
+ export function mergeWithDefaults(config: Config | null): Config {
91
+ const defaults = getDefaultConfig();
92
+
93
+ if (!config) {
94
+ return defaults;
95
+ }
96
+
97
+ return deepMerge(defaults, config);
98
+ }
99
+
100
+ /**
101
+ * Get effective configuration (loaded + defaults)
102
+ */
103
+ export async function getEffectiveConfig(cwd: string = process.cwd()): Promise<Config> {
104
+ const loaded = await loadConfig(cwd);
105
+ return mergeWithDefaults(loaded);
106
+ }
107
+
108
+ /**
109
+ * Create a sample configuration file
110
+ */
111
+ export function createSampleConfig(): string {
112
+ const config = {
113
+ // Global settings
114
+ verbose: false,
115
+ debug: false,
116
+ model: 'gpt-4o-mini',
117
+ openaiReasoning: 'low',
118
+ outputDirectory: 'output/kilde',
119
+
120
+ // Commit configuration
121
+ commit: {
122
+ sendit: false,
123
+ interactive: false,
124
+ messageLimit: 3,
125
+ maxDiffBytes: 20480,
126
+ },
127
+
128
+ // Release configuration
129
+ release: {
130
+ interactive: false,
131
+ messageLimit: 3,
132
+ maxDiffBytes: 20480,
133
+ },
134
+ };
135
+
136
+ return yaml.dump(config, {
137
+ indent: 2,
138
+ lineWidth: 100,
139
+ noRefs: true,
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Save sample configuration to file
145
+ */
146
+ export async function saveSampleConfig(cwd: string = process.cwd()): Promise<string> {
147
+ const logger = getLogger();
148
+ const configDir = path.join(cwd, '.kilde');
149
+ const configPath = path.join(configDir, 'config.yaml');
150
+
151
+ try {
152
+ // Create directory if it doesn't exist
153
+ await fs.mkdir(configDir, { recursive: true });
154
+
155
+ // Write sample config
156
+ const sampleConfig = createSampleConfig();
157
+ await fs.writeFile(configPath, sampleConfig, 'utf-8');
158
+
159
+ logger.info(`Created sample configuration file at ${configPath}`);
160
+ return configPath;
161
+ } catch (error: any) {
162
+ logger.error(`Failed to create sample configuration: ${error.message}`);
163
+ throw error;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Check if configuration file exists
169
+ */
170
+ export async function configFileExists(cwd: string = process.cwd()): Promise<boolean> {
171
+ for (const filename of CONFIG_FILES) {
172
+ const configPath = path.join(cwd, filename);
173
+ try {
174
+ await fs.access(configPath);
175
+ return true;
176
+ } catch {
177
+ // File doesn't exist, continue checking
178
+ }
179
+ }
180
+ return false;
181
+ }
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { getVersionInfo, configureEarlyLogging } from '../src/application';
3
+ import { getLogger, setLogLevel } from '../src/logging';
4
+ import { VERSION, BUILD_HOSTNAME, BUILD_TIMESTAMP } from '../src/constants';
5
+
6
+ describe('application', () => {
7
+ let originalArgv: string[];
8
+
9
+ beforeEach(() => {
10
+ originalArgv = [...process.argv];
11
+ });
12
+
13
+ afterEach(() => {
14
+ process.argv = originalArgv;
15
+ setLogLevel('info');
16
+ });
17
+
18
+ describe('getVersionInfo', () => {
19
+ it('should return version information', () => {
20
+ const info = getVersionInfo();
21
+
22
+ expect(info.version).toBe(VERSION);
23
+ expect(info.buildHostname).toBe(BUILD_HOSTNAME);
24
+ expect(info.buildTimestamp).toBe(BUILD_TIMESTAMP);
25
+ expect(info.formatted).toContain(VERSION);
26
+ });
27
+
28
+ it('should include build metadata in formatted string', () => {
29
+ const info = getVersionInfo();
30
+
31
+ expect(info.formatted).toContain('Built on:');
32
+ expect(info.formatted).toContain('Build time:');
33
+ expect(info.formatted).toContain(BUILD_HOSTNAME);
34
+ expect(info.formatted).toContain(BUILD_TIMESTAMP);
35
+ });
36
+
37
+ it('should have all required fields', () => {
38
+ const info = getVersionInfo();
39
+
40
+ expect(info).toHaveProperty('version');
41
+ expect(info).toHaveProperty('buildHostname');
42
+ expect(info).toHaveProperty('buildTimestamp');
43
+ expect(info).toHaveProperty('formatted');
44
+ });
45
+
46
+ it('should return string types for all fields', () => {
47
+ const info = getVersionInfo();
48
+
49
+ expect(typeof info.version).toBe('string');
50
+ expect(typeof info.buildHostname).toBe('string');
51
+ expect(typeof info.buildTimestamp).toBe('string');
52
+ expect(typeof info.formatted).toBe('string');
53
+ });
54
+ });
55
+
56
+ describe('configureEarlyLogging', () => {
57
+ it('should set debug log level when --debug flag is present', () => {
58
+ process.argv = ['node', 'kilde', '--debug'];
59
+ configureEarlyLogging();
60
+
61
+ const logger = getLogger() as any;
62
+ expect(logger.level).toBe('debug');
63
+ });
64
+
65
+ it('should set debug log level when -d flag is present', () => {
66
+ process.argv = ['node', 'kilde', '-d'];
67
+ configureEarlyLogging();
68
+
69
+ const logger = getLogger() as any;
70
+ expect(logger.level).toBe('debug');
71
+ });
72
+
73
+ it('should set verbose log level when --verbose flag is present', () => {
74
+ process.argv = ['node', 'kilde', '--verbose'];
75
+ configureEarlyLogging();
76
+
77
+ const logger = getLogger() as any;
78
+ expect(logger.level).toBe('verbose');
79
+ });
80
+
81
+ it('should set verbose log level when -v flag is present', () => {
82
+ process.argv = ['node', 'kilde', '-v'];
83
+ configureEarlyLogging();
84
+
85
+ const logger = getLogger() as any;
86
+ expect(logger.level).toBe('verbose');
87
+ });
88
+
89
+ it('should prioritize debug over verbose when both are present', () => {
90
+ process.argv = ['node', 'kilde', '--verbose', '--debug'];
91
+ configureEarlyLogging();
92
+
93
+ const logger = getLogger() as any;
94
+ expect(logger.level).toBe('debug');
95
+ });
96
+
97
+ it('should not change log level when no flags are present', () => {
98
+ setLogLevel('info');
99
+ process.argv = ['node', 'kilde'];
100
+ configureEarlyLogging();
101
+
102
+ const logger = getLogger() as any;
103
+ expect(logger.level).toBe('info');
104
+ });
105
+
106
+ it('should handle flags in any position', () => {
107
+ process.argv = ['node', 'kilde', 'commit', '--debug', '--sendit'];
108
+ configureEarlyLogging();
109
+
110
+ const logger = getLogger() as any;
111
+ expect(logger.level).toBe('debug');
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,248 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import * as CommandsGit from '@grunnverk/commands-git';
3
+ import { Config } from '@grunnverk/core';
4
+
5
+ // Mock the commands-git module
6
+ vi.mock('@grunnverk/commands-git', () => ({
7
+ commit: vi.fn(),
8
+ }));
9
+
10
+ describe('commit command integration', () => {
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ });
14
+
15
+ describe('basic commit functionality', () => {
16
+ it('should call commands-git commit with proper config', async () => {
17
+ const mockConfig: Config = {
18
+ configDirectory: '/test/.kilde',
19
+ discoveredConfigDirs: [],
20
+ resolvedConfigDirs: [],
21
+ verbose: false,
22
+ debug: false,
23
+ dryRun: false,
24
+ commit: {
25
+ add: true,
26
+ sendit: true,
27
+ cached: false,
28
+ interactive: false,
29
+ amend: false,
30
+ },
31
+ } as Config;
32
+
33
+ vi.mocked(CommandsGit.commit).mockResolvedValue('Commit created successfully');
34
+
35
+ const result = await CommandsGit.commit(mockConfig);
36
+
37
+ expect(CommandsGit.commit).toHaveBeenCalledWith(mockConfig);
38
+ expect(result).toBe('Commit created successfully');
39
+ });
40
+
41
+ it('should handle commit with add flag', async () => {
42
+ const mockConfig: Config = {
43
+ configDirectory: '/test/.kilde',
44
+ discoveredConfigDirs: [],
45
+ resolvedConfigDirs: [],
46
+ commit: {
47
+ add: true,
48
+ },
49
+ } as Config;
50
+
51
+ vi.mocked(CommandsGit.commit).mockResolvedValue('Changes staged and committed');
52
+
53
+ const result = await CommandsGit.commit(mockConfig);
54
+
55
+ expect(result).toBe('Changes staged and committed');
56
+ expect(CommandsGit.commit).toHaveBeenCalledWith(
57
+ expect.objectContaining({
58
+ commit: expect.objectContaining({
59
+ add: true,
60
+ }),
61
+ })
62
+ );
63
+ });
64
+
65
+ it('should handle commit with sendit flag', async () => {
66
+ const mockConfig: Config = {
67
+ configDirectory: '/test/.kilde',
68
+ discoveredConfigDirs: [],
69
+ resolvedConfigDirs: [],
70
+ commit: {
71
+ sendit: true,
72
+ },
73
+ } as Config;
74
+
75
+ vi.mocked(CommandsGit.commit).mockResolvedValue('Commit created: abc1234');
76
+
77
+ const result = await CommandsGit.commit(mockConfig);
78
+
79
+ expect(result).toContain('Commit created');
80
+ });
81
+ });
82
+
83
+ describe('commit with context', () => {
84
+ it('should handle commit with context string', async () => {
85
+ const mockConfig: Config = {
86
+ configDirectory: '/test/.kilde',
87
+ discoveredConfigDirs: [],
88
+ resolvedConfigDirs: [],
89
+ commit: {
90
+ context: 'Fix authentication bug',
91
+ },
92
+ } as Config;
93
+
94
+ vi.mocked(CommandsGit.commit).mockResolvedValue('Commit with context created');
95
+
96
+ await CommandsGit.commit(mockConfig);
97
+
98
+ expect(CommandsGit.commit).toHaveBeenCalledWith(
99
+ expect.objectContaining({
100
+ commit: expect.objectContaining({
101
+ context: 'Fix authentication bug',
102
+ }),
103
+ })
104
+ );
105
+ });
106
+
107
+ it('should handle commit with context files', async () => {
108
+ const mockConfig: Config = {
109
+ configDirectory: '/test/.kilde',
110
+ discoveredConfigDirs: [],
111
+ resolvedConfigDirs: [],
112
+ commit: {
113
+ contextFiles: ['CHANGELOG.md', 'README.md'],
114
+ },
115
+ } as Config;
116
+
117
+ vi.mocked(CommandsGit.commit).mockResolvedValue('Commit with context files created');
118
+
119
+ await CommandsGit.commit(mockConfig);
120
+
121
+ expect(CommandsGit.commit).toHaveBeenCalledWith(
122
+ expect.objectContaining({
123
+ commit: expect.objectContaining({
124
+ contextFiles: ['CHANGELOG.md', 'README.md'],
125
+ }),
126
+ })
127
+ );
128
+ });
129
+ });
130
+
131
+ describe('commit modes', () => {
132
+ it('should handle interactive mode', async () => {
133
+ const mockConfig: Config = {
134
+ configDirectory: '/test/.kilde',
135
+ discoveredConfigDirs: [],
136
+ resolvedConfigDirs: [],
137
+ commit: {
138
+ interactive: true,
139
+ },
140
+ } as Config;
141
+
142
+ vi.mocked(CommandsGit.commit).mockResolvedValue('Interactive commit completed');
143
+
144
+ await CommandsGit.commit(mockConfig);
145
+
146
+ expect(CommandsGit.commit).toHaveBeenCalledWith(
147
+ expect.objectContaining({
148
+ commit: expect.objectContaining({
149
+ interactive: true,
150
+ }),
151
+ })
152
+ );
153
+ });
154
+
155
+ it('should handle amend mode', async () => {
156
+ const mockConfig: Config = {
157
+ configDirectory: '/test/.kilde',
158
+ discoveredConfigDirs: [],
159
+ resolvedConfigDirs: [],
160
+ commit: {
161
+ amend: true,
162
+ },
163
+ } as Config;
164
+
165
+ vi.mocked(CommandsGit.commit).mockResolvedValue('Commit amended');
166
+
167
+ await CommandsGit.commit(mockConfig);
168
+
169
+ expect(CommandsGit.commit).toHaveBeenCalledWith(
170
+ expect.objectContaining({
171
+ commit: expect.objectContaining({
172
+ amend: true,
173
+ }),
174
+ })
175
+ );
176
+ });
177
+
178
+ it('should handle cached mode', async () => {
179
+ const mockConfig: Config = {
180
+ configDirectory: '/test/.kilde',
181
+ discoveredConfigDirs: [],
182
+ resolvedConfigDirs: [],
183
+ commit: {
184
+ cached: true,
185
+ },
186
+ } as Config;
187
+
188
+ vi.mocked(CommandsGit.commit).mockResolvedValue('Cached changes committed');
189
+
190
+ await CommandsGit.commit(mockConfig);
191
+
192
+ expect(CommandsGit.commit).toHaveBeenCalledWith(
193
+ expect.objectContaining({
194
+ commit: expect.objectContaining({
195
+ cached: true,
196
+ }),
197
+ })
198
+ );
199
+ });
200
+ });
201
+
202
+ describe('dry-run mode', () => {
203
+ it('should handle dry-run mode', async () => {
204
+ const mockConfig: Config = {
205
+ configDirectory: '/test/.kilde',
206
+ discoveredConfigDirs: [],
207
+ resolvedConfigDirs: [],
208
+ dryRun: true,
209
+ commit: {
210
+ add: true,
211
+ },
212
+ } as Config;
213
+
214
+ vi.mocked(CommandsGit.commit).mockResolvedValue('[DRY-RUN] Would create commit');
215
+
216
+ const result = await CommandsGit.commit(mockConfig);
217
+
218
+ expect(result).toContain('DRY-RUN');
219
+ });
220
+ });
221
+
222
+ describe('error handling', () => {
223
+ it('should propagate errors from commit command', async () => {
224
+ const mockConfig: Config = {
225
+ configDirectory: '/test/.kilde',
226
+ discoveredConfigDirs: [],
227
+ resolvedConfigDirs: [],
228
+ } as Config;
229
+
230
+ const error = new Error('Git command failed');
231
+ vi.mocked(CommandsGit.commit).mockRejectedValue(error);
232
+
233
+ await expect(CommandsGit.commit(mockConfig)).rejects.toThrow('Git command failed');
234
+ });
235
+
236
+ it('should handle missing git repository error', async () => {
237
+ const mockConfig: Config = {
238
+ configDirectory: '/test/.kilde',
239
+ discoveredConfigDirs: [],
240
+ resolvedConfigDirs: [],
241
+ } as Config;
242
+
243
+ vi.mocked(CommandsGit.commit).mockRejectedValue(new Error('Not a git repository'));
244
+
245
+ await expect(CommandsGit.commit(mockConfig)).rejects.toThrow('Not a git repository');
246
+ });
247
+ });
248
+ });