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.
- package/__tests__/core/fs/fs.service.test.ts +72 -0
- package/__tests__/shared/cleaner/cleaner.service.test.ts +102 -0
- package/__tests__/shared/git/git.service.test.ts +84 -0
- package/__tests__/shared/tokenizer/tokenizer.service.test.ts +45 -0
- package/dist/package.json +14 -3
- package/dist/src/core/config/config.service.js +2 -4
- package/dist/src/core/config/config.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/lefthook.yml +9 -2
- package/package.json +14 -3
- package/skills/liteend-init/SKILL.md +84 -0
- package/skills/litefront-init/SKILL.md +96 -0
- package/skills/project-setup-standardizer/SKILL.md +285 -0
- package/src/core/config/config.service.ts +3 -6
- package/tsconfig.build.json +3 -0
- package/tsconfig.json +5 -2
- package/dist/scripts/generate-json-schema.d.ts +0 -1
- package/dist/scripts/generate-json-schema.js +0 -17
- package/dist/scripts/generate-json-schema.js.map +0 -1
- package/skills/kodu-ops/SKILL.md +0 -184
|
@@ -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.
|
|
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
|
-
|
|
29
|
-
|
|
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,
|
|
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"}
|