kodu 2.1.1 → 2.1.2

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.
@@ -0,0 +1,72 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { ConfigService } from '../../../src/core/config/config.service';
3
+ import { FsService } from '../../../src/core/file-system/fs.service';
4
+ import { UiService } from '../../../src/core/ui/ui.service';
5
+
6
+ vi.mock('../../../src/core/config/config.service', () => ({
7
+ ConfigService: vi.fn().mockImplementation(() => ({
8
+ getConfig: () => ({
9
+ cleaner: { whitelist: [], keepJSDoc: false },
10
+ packer: { ignore: ['node_modules', 'dist'], useGitignore: false },
11
+ }),
12
+ })),
13
+ }));
14
+
15
+ vi.mock('../../../src/core/ui/ui.service', () => ({
16
+ UiService: vi.fn().mockImplementation(() => ({
17
+ log: { warn: vi.fn() },
18
+ })),
19
+ }));
20
+
21
+ describe('FsService', () => {
22
+ let fsService: FsService;
23
+
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ const configService = new ConfigService() as never;
27
+ const uiService = new UiService() as never;
28
+ fsService = new FsService(configService, uiService);
29
+ });
30
+
31
+ describe('findProjectFiles', () => {
32
+ it('should find ts files in current directory', async () => {
33
+ const files = await fsService.findProjectFiles({
34
+ ignore: ['node_modules', 'dist'],
35
+ useGitignore: false,
36
+ });
37
+
38
+ const tsFiles = files.filter((f) => f.endsWith('.ts'));
39
+ expect(tsFiles.length).toBeGreaterThan(0);
40
+ });
41
+
42
+ it('should exclude node_modules by default', async () => {
43
+ const files = await fsService.findProjectFiles({
44
+ ignore: ['node_modules', 'dist'],
45
+ useGitignore: false,
46
+ });
47
+
48
+ const hasNodeModules = files.some((f) => f.includes('node_modules'));
49
+ expect(hasNodeModules).toBe(false);
50
+ });
51
+
52
+ it('should return relative paths', async () => {
53
+ const files = await fsService.findProjectFiles({
54
+ ignore: [],
55
+ useGitignore: false,
56
+ });
57
+
58
+ const hasAbsolute = files.some((f) => f.startsWith('/'));
59
+ expect(hasAbsolute).toBe(false);
60
+ });
61
+
62
+ it('should return sorted paths', async () => {
63
+ const files = await fsService.findProjectFiles({
64
+ ignore: [],
65
+ useGitignore: false,
66
+ });
67
+
68
+ const sorted = [...files].sort((a, b) => a.localeCompare(b));
69
+ expect(files).toEqual(sorted);
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,102 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { ConfigService } from '../../../src/core/config/config.service';
3
+
4
+ vi.mock('../../../src/core/config/config.service', () => ({
5
+ ConfigService: vi.fn().mockImplementation(() => ({
6
+ getConfig: () => ({
7
+ cleaner: {
8
+ whitelist: [],
9
+ keepJSDoc: false,
10
+ useGitignore: false,
11
+ },
12
+ packer: {
13
+ ignore: [],
14
+ useGitignore: false,
15
+ },
16
+ }),
17
+ })),
18
+ }));
19
+
20
+ vi.mock('../../../src/core/file-system/fs.service', () => ({
21
+ FsService: vi.fn().mockImplementation(() => ({})),
22
+ }));
23
+
24
+ describe('CleanerService', () => {
25
+ beforeEach(() => {
26
+ vi.clearAllMocks();
27
+ });
28
+
29
+ it('should remove single-line comments', async () => {
30
+ const { CleanerService } = await import(
31
+ '../../../src/shared/cleaner/cleaner.service'
32
+ );
33
+ const configService = new ConfigService() as never;
34
+ const fsService = {} as never;
35
+ const cleaner = new CleanerService(configService, fsService);
36
+
37
+ const input = `const x = 1; // comment
38
+ const y = 2;`;
39
+ const result = cleaner.cleanContent('test.ts', input);
40
+
41
+ expect(result).toBe('const x = 1; \nconst y = 2;');
42
+ });
43
+
44
+ it('should remove multi-line comments', async () => {
45
+ const { CleanerService } = await import(
46
+ '../../../src/shared/cleaner/cleaner.service'
47
+ );
48
+ const configService = new ConfigService() as never;
49
+ const fsService = {} as never;
50
+ const cleaner = new CleanerService(configService, fsService);
51
+
52
+ const input = `/* comment */
53
+ const x = 1;`;
54
+ const result = cleaner.cleanContent('test.ts', input);
55
+
56
+ expect(result).toBe('\nconst x = 1;');
57
+ });
58
+
59
+ it('should preserve comments in whitelist', async () => {
60
+ const { CleanerService } = await import(
61
+ '../../../src/shared/cleaner/cleaner.service'
62
+ );
63
+ const configService = new ConfigService() as never;
64
+ const fsService = {} as never;
65
+ const cleaner = new CleanerService(configService, fsService);
66
+
67
+ const input = `const x = 1; // eslint-disable-line
68
+ const y = 2;`;
69
+ const result = cleaner.cleanContent('test.ts', input);
70
+
71
+ expect(result).toBe('const x = 1; // eslint-disable-line\nconst y = 2;');
72
+ });
73
+
74
+ it('should keep JSDoc when option is set', async () => {
75
+ const { CleanerService } = await import(
76
+ '../../../src/shared/cleaner/cleaner.service'
77
+ );
78
+ const configService = new ConfigService() as never;
79
+ const fsService = {} as never;
80
+ const cleaner = new CleanerService(configService, fsService);
81
+
82
+ const input = `/** JSDoc */
83
+ const x = 1;`;
84
+ const result = cleaner.cleanContent('test.ts', input, true);
85
+
86
+ expect(result).toBe('/** JSDoc */\nconst x = 1;');
87
+ });
88
+
89
+ it('should remove JSX expression without content', async () => {
90
+ const { CleanerService } = await import(
91
+ '../../../src/shared/cleaner/cleaner.service'
92
+ );
93
+ const configService = new ConfigService() as never;
94
+ const fsService = {} as never;
95
+ const cleaner = new CleanerService(configService, fsService);
96
+
97
+ const input = `const x = <>{/* comment */}</>;`;
98
+ const result = cleaner.cleanContent('test.tsx', input);
99
+
100
+ expect(result).toBe('const x = <></>;');
101
+ });
102
+ });
@@ -0,0 +1,84 @@
1
+ import { execa } from 'execa';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { GitService } from '../../../src/shared/git/git.service';
4
+
5
+ vi.mock('execa');
6
+
7
+ const mockExeca = vi.mocked(execa);
8
+
9
+ describe('GitService', () => {
10
+ let gitService: GitService;
11
+
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ gitService = new GitService();
15
+ });
16
+
17
+ afterEach(() => {
18
+ vi.resetAllMocks();
19
+ });
20
+
21
+ describe('ensureRepo', () => {
22
+ it('should resolve when inside git repo', async () => {
23
+ mockExeca.mockResolvedValue({ stdout: '' } as never);
24
+
25
+ await expect(gitService.ensureRepo()).resolves.not.toThrow();
26
+ });
27
+
28
+ it('should reject when not in git repo', async () => {
29
+ mockExeca.mockRejectedValue(new Error('fatal: not a git repository'));
30
+
31
+ await expect(gitService.ensureRepo()).rejects.toThrow();
32
+ });
33
+ });
34
+
35
+ describe('getChangedFiles', () => {
36
+ it('should return empty array when no changes', async () => {
37
+ mockExeca.mockResolvedValue({ stdout: '' } as never);
38
+
39
+ const files = await gitService.getChangedFiles();
40
+
41
+ expect(files).toEqual([]);
42
+ });
43
+
44
+ it('should return changed files from all sources', async () => {
45
+ const mockCalls = [
46
+ { stdout: '' }, // ensureRepo
47
+ { stdout: 'src/a.ts\nsrc/b.ts' }, // diff
48
+ { stdout: '' }, // diff --staged
49
+ { stdout: '' }, // ls-files
50
+ ];
51
+ mockExeca
52
+ .mockResolvedValueOnce(mockCalls[0] as never)
53
+ .mockResolvedValueOnce(mockCalls[1] as never)
54
+ .mockResolvedValueOnce(mockCalls[2] as never)
55
+ .mockResolvedValueOnce(mockCalls[3] as never);
56
+
57
+ const files = await gitService.getChangedFiles();
58
+
59
+ expect(files).toEqual(['src/a.ts', 'src/b.ts']);
60
+ });
61
+ });
62
+
63
+ describe('getStagedFiles', () => {
64
+ it('should return empty array when no staged files', async () => {
65
+ mockExeca
66
+ .mockResolvedValueOnce({ stdout: '' } as never)
67
+ .mockResolvedValueOnce({ stdout: '' } as never);
68
+
69
+ const files = await gitService.getStagedFiles();
70
+
71
+ expect(files).toEqual([]);
72
+ });
73
+
74
+ it('should return staged files', async () => {
75
+ mockExeca
76
+ .mockResolvedValueOnce({ stdout: '' } as never)
77
+ .mockResolvedValueOnce({ stdout: 'src/new.ts' } as never);
78
+
79
+ const files = await gitService.getStagedFiles();
80
+
81
+ expect(files).toEqual(['src/new.ts']);
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,45 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ describe('TokenizerService', () => {
4
+ let tokenizer: {
5
+ count: (text: string) => { tokens: number; usdEstimate: number };
6
+ };
7
+
8
+ beforeEach(async () => {
9
+ const { TokenizerService } = await import(
10
+ '../../../src/shared/tokenizer/tokenizer.service'
11
+ );
12
+ tokenizer = new TokenizerService();
13
+ });
14
+
15
+ it('should count tokens for empty string', () => {
16
+ const result = tokenizer.count('');
17
+
18
+ expect(result.tokens).toBe(0);
19
+ expect(result.usdEstimate).toBe(0);
20
+ });
21
+
22
+ it('should count tokens for simple text', () => {
23
+ const result = tokenizer.count('hello world');
24
+
25
+ expect(result.tokens).toBeGreaterThan(0);
26
+ expect(result.usdEstimate).toBeGreaterThan(0);
27
+ });
28
+
29
+ it('should estimate cost correctly based on DEFAULT_PRICE_PER_MILLION', () => {
30
+ const result = tokenizer.count('a'.repeat(1000));
31
+
32
+ expect(result.tokens).toBeGreaterThan(0);
33
+ // DEFAULT_PRICE_PER_MILLION = 5 means $5 per 1M tokens
34
+ const expectedCost = (result.tokens / 1_000_000) * 5;
35
+ expect(result.usdEstimate).toBe(expectedCost);
36
+ });
37
+
38
+ it('should handle large text', () => {
39
+ const longText = 'test '.repeat(10000);
40
+ const result = tokenizer.count(longText);
41
+
42
+ expect(result.tokens).toBeGreaterThan(10000);
43
+ expect(result.usdEstimate).toBeGreaterThan(0);
44
+ });
45
+ });
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodu",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "High-performance CLI to prepare codebase for LLMs, automate reviews, and draft commits.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,6 +34,7 @@
34
34
  "generate:schema": "ts-node scripts/generate-json-schema.ts",
35
35
  "postbuild": "npm run generate:schema",
36
36
  "start:prod": "node dist/main.js",
37
+ "start:dev": "nest start --watch",
37
38
  "new:command": "nest g -c nest-commander-schematics command",
38
39
  "new:question": "nest g -c nest-commander-schematics question",
39
40
  "________________ FORMAT AND LINT ________________": "",
@@ -43,8 +44,14 @@
43
44
  "ts:check": "tsc --noEmit",
44
45
  "knip": "knip --production",
45
46
  "check": "run-p ts:check lint:fix knip",
47
+ "________________ TEST ________________": "",
48
+ "test": "vitest run",
49
+ "test:watch": "vitest",
50
+ "test:cov": "vitest run --coverage",
46
51
  "________________ OTHER ________________": "",
47
- "prepare": "lefthook install"
52
+ "prepare": "is-ci || lefthook install",
53
+ "update": "npx npm-check-updates -u && rimraf node_modules package-lock.json && npm i",
54
+ "postupdate": "npm run lint:fix && npm run check"
48
55
  },
49
56
  "dependencies": {
50
57
  "@inquirer/confirm": "^6.0.4",
@@ -73,13 +80,17 @@
73
80
  "@nestjs/schematics": "^11.0.0",
74
81
  "@nestjs/testing": "^11.0.1",
75
82
  "@types/node": "^22.10.7",
83
+ "is-ci": "^4.1.0",
76
84
  "knip": "^5.82.1",
77
85
  "lefthook": "^2.0.15",
78
86
  "nest-commander-schematics": "^3.2.0",
87
+ "npm-check-updates": "^18.3.1",
79
88
  "npm-run-all": "^4.1.5",
89
+ "rimraf": "^6.1.3",
80
90
  "ts-loader": "^9.5.2",
81
91
  "ts-node": "^10.9.2",
82
92
  "tsconfig-paths": "^4.2.0",
83
- "typescript": "^5.7.3"
93
+ "typescript": "^5.7.3",
94
+ "vitest": "^3.2.4"
84
95
  }
85
96
  }
@@ -25,10 +25,8 @@ let ConfigService = class ConfigService {
25
25
  loadConfig() {
26
26
  const explorer = (0, lilconfig_1.lilconfigSync)('kodu', { searchPlaces: ['kodu.json'] });
27
27
  const result = explorer.search(process.cwd());
28
- if (!result || result.isEmpty || !result.config) {
29
- this.terminate('kodu.json not found. Create it in the project root to configure kodu.');
30
- }
31
- const parsed = config_schema_1.configSchema.safeParse(result.config);
28
+ const rawConfig = result && !result.isEmpty && result.config ? result.config : {};
29
+ const parsed = config_schema_1.configSchema.safeParse(rawConfig);
32
30
  if (!parsed.success) {
33
31
  console.error(picocolors_1.default.red('kodu.json is invalid:'));
34
32
  parsed.error.issues.forEach((issue) => {
@@ -1 +1 @@
1
- {"version":3,"file":"config.service.js","sourceRoot":"","sources":["../../../../src/core/config/config.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,yCAA0C;AAC1C,4DAA4B;AAC5B,mDAAgE;AAGzD,IAAM,aAAa,GAAnB,MAAM,aAAa;IAChB,MAAM,CAAc;IAE5B,SAAS;QACP,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;QAClC,CAAC;QAED,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAEO,UAAU;QAChB,MAAM,QAAQ,GAAG,IAAA,yBAAa,EAAC,MAAM,EAAE,EAAE,YAAY,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACxE,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAE9C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YAChD,IAAI,CAAC,SAAS,CACZ,uEAAuE,CACxE,CAAC;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,4BAAY,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAErD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,CAAC,oBAAE,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC;YAC/C,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;gBACpC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC;gBAC9C,OAAO,CAAC,KAAK,CAAC,oBAAE,CAAC,GAAG,CAAC,KAAK,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YACvD,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,SAAS,CAAC,2CAA2C,CAAC,CAAC;QAC9D,CAAC;QAED,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;IAEO,SAAS,CAAC,OAAe;QAC/B,OAAO,CAAC,KAAK,CAAC,oBAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAC/B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;CACF,CAAA;AAvCY,sCAAa;wBAAb,aAAa;IADzB,IAAA,mBAAU,GAAE;GACA,aAAa,CAuCzB"}
1
+ {"version":3,"file":"config.service.js","sourceRoot":"","sources":["../../../../src/core/config/config.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,yCAA0C;AAC1C,4DAA4B;AAC5B,mDAAgE;AAGzD,IAAM,aAAa,GAAnB,MAAM,aAAa;IAChB,MAAM,CAAc;IAE5B,SAAS;QACP,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;QAClC,CAAC;QAED,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAEO,UAAU;QAChB,MAAM,QAAQ,GAAG,IAAA,yBAAa,EAAC,MAAM,EAAE,EAAE,YAAY,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACxE,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAE9C,MAAM,SAAS,GACb,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAElE,MAAM,MAAM,GAAG,4BAAY,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QAEjD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,CAAC,oBAAE,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC;YAC/C,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;gBACpC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC;gBAC9C,OAAO,CAAC,KAAK,CAAC,oBAAE,CAAC,GAAG,CAAC,KAAK,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YACvD,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,SAAS,CAAC,2CAA2C,CAAC,CAAC;QAC9D,CAAC;QAED,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;IAEO,SAAS,CAAC,OAAe;QAC/B,OAAO,CAAC,KAAK,CAAC,oBAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAC/B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;CACF,CAAA;AApCY,sCAAa;wBAAb,aAAa;IADzB,IAAA,mBAAU,GAAE;GACA,aAAa,CAoCzB"}