kodu 2.1.3 → 3.0.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 (131) hide show
  1. package/README.md +24 -3
  2. package/bin/kodu.js +23 -0
  3. package/package.json +11 -67
  4. package/scripts/postinstall.js +69 -0
  5. package/AGENTS.md +0 -214
  6. package/__tests__/core/fs/fs.service.test.ts +0 -72
  7. package/__tests__/shared/cleaner/cleaner.service.test.ts +0 -102
  8. package/__tests__/shared/git/git.service.test.ts +0 -84
  9. package/__tests__/shared/tokenizer/tokenizer.service.test.ts +0 -45
  10. package/biome.json +0 -50
  11. package/dist/package.json +0 -96
  12. package/dist/src/app.module.d.ts +0 -2
  13. package/dist/src/app.module.js +0 -36
  14. package/dist/src/app.module.js.map +0 -1
  15. package/dist/src/commands/clean/clean.command.d.ts +0 -37
  16. package/dist/src/commands/clean/clean.command.js +0 -240
  17. package/dist/src/commands/clean/clean.command.js.map +0 -1
  18. package/dist/src/commands/clean/clean.module.d.ts +0 -2
  19. package/dist/src/commands/clean/clean.module.js +0 -26
  20. package/dist/src/commands/clean/clean.module.js.map +0 -1
  21. package/dist/src/commands/init/init.command.d.ts +0 -10
  22. package/dist/src/commands/init/init.command.js +0 -96
  23. package/dist/src/commands/init/init.command.js.map +0 -1
  24. package/dist/src/commands/init/init.module.d.ts +0 -2
  25. package/dist/src/commands/init/init.module.js +0 -22
  26. package/dist/src/commands/init/init.module.js.map +0 -1
  27. package/dist/src/commands/pack/pack.command.d.ts +0 -51
  28. package/dist/src/commands/pack/pack.command.js +0 -355
  29. package/dist/src/commands/pack/pack.command.js.map +0 -1
  30. package/dist/src/commands/pack/pack.module.d.ts +0 -2
  31. package/dist/src/commands/pack/pack.module.js +0 -27
  32. package/dist/src/commands/pack/pack.module.js.map +0 -1
  33. package/dist/src/core/config/config.module.d.ts +0 -2
  34. package/dist/src/core/config/config.module.js +0 -23
  35. package/dist/src/core/config/config.module.js.map +0 -1
  36. package/dist/src/core/config/config.schema.d.ts +0 -19
  37. package/dist/src/core/config/config.schema.js +0 -56
  38. package/dist/src/core/config/config.schema.js.map +0 -1
  39. package/dist/src/core/config/config.service.d.ts +0 -7
  40. package/dist/src/core/config/config.service.js +0 -49
  41. package/dist/src/core/config/config.service.js.map +0 -1
  42. package/dist/src/core/config/prompt.service.d.ts +0 -10
  43. package/dist/src/core/config/prompt.service.js +0 -80
  44. package/dist/src/core/config/prompt.service.js.map +0 -1
  45. package/dist/src/core/file-system/fs.module.d.ts +0 -2
  46. package/dist/src/core/file-system/fs.module.js +0 -21
  47. package/dist/src/core/file-system/fs.module.js.map +0 -1
  48. package/dist/src/core/file-system/fs.service.d.ts +0 -27
  49. package/dist/src/core/file-system/fs.service.js +0 -203
  50. package/dist/src/core/file-system/fs.service.js.map +0 -1
  51. package/dist/src/core/ui/ui.module.d.ts +0 -2
  52. package/dist/src/core/ui/ui.module.js +0 -22
  53. package/dist/src/core/ui/ui.module.js.map +0 -1
  54. package/dist/src/core/ui/ui.service.d.ts +0 -22
  55. package/dist/src/core/ui/ui.service.js +0 -43
  56. package/dist/src/core/ui/ui.service.js.map +0 -1
  57. package/dist/src/main.d.ts +0 -2
  58. package/dist/src/main.js +0 -16
  59. package/dist/src/main.js.map +0 -1
  60. package/dist/src/shared/cleaner/cleaner.service.d.ts +0 -23
  61. package/dist/src/shared/cleaner/cleaner.service.js +0 -223
  62. package/dist/src/shared/cleaner/cleaner.service.js.map +0 -1
  63. package/dist/src/shared/cleaner/cleaner.types.d.ts +0 -21
  64. package/dist/src/shared/cleaner/cleaner.types.js +0 -3
  65. package/dist/src/shared/cleaner/cleaner.types.js.map +0 -1
  66. package/dist/src/shared/constants.d.ts +0 -4
  67. package/dist/src/shared/constants.js +0 -113
  68. package/dist/src/shared/constants.js.map +0 -1
  69. package/dist/src/shared/deps/deps.module.d.ts +0 -2
  70. package/dist/src/shared/deps/deps.module.js +0 -21
  71. package/dist/src/shared/deps/deps.module.js.map +0 -1
  72. package/dist/src/shared/deps/deps.service.d.ts +0 -15
  73. package/dist/src/shared/deps/deps.service.js +0 -114
  74. package/dist/src/shared/deps/deps.service.js.map +0 -1
  75. package/dist/src/shared/git/git.module.d.ts +0 -2
  76. package/dist/src/shared/git/git.module.js +0 -21
  77. package/dist/src/shared/git/git.module.js.map +0 -1
  78. package/dist/src/shared/git/git.service.d.ts +0 -5
  79. package/dist/src/shared/git/git.service.js +0 -56
  80. package/dist/src/shared/git/git.service.js.map +0 -1
  81. package/dist/src/shared/tokenizer/tokenizer.module.d.ts +0 -2
  82. package/dist/src/shared/tokenizer/tokenizer.module.js +0 -21
  83. package/dist/src/shared/tokenizer/tokenizer.module.js.map +0 -1
  84. package/dist/src/shared/tokenizer/tokenizer.service.d.ts +0 -10
  85. package/dist/src/shared/tokenizer/tokenizer.service.js +0 -36
  86. package/dist/src/shared/tokenizer/tokenizer.service.js.map +0 -1
  87. package/dist/tsconfig.build.tsbuildinfo +0 -1
  88. package/docs/todo.md +0 -7
  89. package/knip.json +0 -10
  90. package/kodu.json +0 -63
  91. package/kodu.schema.json +0 -100
  92. package/lefthook.yml +0 -11
  93. package/nest-cli.json +0 -8
  94. package/scripts/generate-json-schema.ts +0 -18
  95. package/skills/doc-gen/SKILL.md +0 -490
  96. package/skills/doc-gen/scripts/doc_gen.py +0 -911
  97. package/skills/implement-project/SKILL.md +0 -409
  98. package/skills/liteend-init/SKILL.md +0 -84
  99. package/skills/litefront-init/SKILL.md +0 -96
  100. package/skills/litefront-prototype/SKILL.md +0 -484
  101. package/skills/project-setup-standardizer/SKILL.md +0 -285
  102. package/skills/start/SKILL.md +0 -319
  103. package/skills/tech-blueprint/SKILL.md +0 -890
  104. package/skills/tech-blueprint/scripts/blueprint_validator.py +0 -417
  105. package/src/app.module.ts +0 -23
  106. package/src/commands/clean/clean.command.ts +0 -235
  107. package/src/commands/clean/clean.module.ts +0 -13
  108. package/src/commands/init/init.command.ts +0 -92
  109. package/src/commands/init/init.module.ts +0 -9
  110. package/src/commands/pack/pack.command.ts +0 -347
  111. package/src/commands/pack/pack.module.ts +0 -14
  112. package/src/core/config/config.module.ts +0 -10
  113. package/src/core/config/config.schema.ts +0 -58
  114. package/src/core/config/config.service.ts +0 -43
  115. package/src/core/config/prompt.service.ts +0 -80
  116. package/src/core/file-system/fs.module.ts +0 -8
  117. package/src/core/file-system/fs.service.ts +0 -248
  118. package/src/core/ui/ui.module.ts +0 -9
  119. package/src/core/ui/ui.service.ts +0 -39
  120. package/src/main.ts +0 -12
  121. package/src/shared/cleaner/cleaner.service.ts +0 -289
  122. package/src/shared/cleaner/cleaner.types.ts +0 -23
  123. package/src/shared/constants.ts +0 -118
  124. package/src/shared/deps/deps.module.ts +0 -8
  125. package/src/shared/deps/deps.service.ts +0 -175
  126. package/src/shared/git/git.module.ts +0 -8
  127. package/src/shared/git/git.service.ts +0 -47
  128. package/src/shared/tokenizer/tokenizer.module.ts +0 -8
  129. package/src/shared/tokenizer/tokenizer.service.ts +0 -30
  130. package/tsconfig.build.json +0 -7
  131. package/tsconfig.json +0 -28
@@ -1,248 +0,0 @@
1
- import { promises as fs, type Stats } from 'node:fs';
2
- import path from 'node:path';
3
- import { Injectable } from '@nestjs/common';
4
- import ignore from 'ignore';
5
- import { glob } from 'tinyglobby';
6
- import {
7
- BINARY_EXTENSIONS,
8
- KNOWN_TEXT_EXTENSIONS,
9
- MAX_FILE_SIZE_BYTES,
10
- } from '../../shared/constants';
11
- import { ConfigService } from '../config/config.service';
12
- import { UiService } from '../ui/ui.service';
13
-
14
- const BINARY_PROBE_SIZE = 8192;
15
- const GLOB_IGNORE = ['.git/**'];
16
-
17
- type FindProjectFilesOptions = {
18
- ignore?: string[];
19
- useGitignore?: boolean;
20
- excludeBinary?: boolean;
21
- contentBasedBinaryDetection?: boolean;
22
- maxFileSizeBytes?: number;
23
- rootPaths?: string[];
24
- };
25
-
26
- @Injectable()
27
- export class FsService {
28
- constructor(
29
- private readonly configService: ConfigService,
30
- private readonly ui: UiService,
31
- ) {}
32
-
33
- async findProjectFiles(
34
- options: FindProjectFilesOptions = {},
35
- ): Promise<string[]> {
36
- const { packer } = this.configService.getConfig();
37
- const shouldUseGitignore = options.useGitignore ?? packer.useGitignore;
38
- const gitignorePatterns = shouldUseGitignore
39
- ? await this.readIgnoreFile('.gitignore')
40
- : [];
41
- const koduignorePatterns = await this.readIgnoreFile('.koduignore');
42
-
43
- const baseIgnore = options.ignore ?? packer.ignore ?? [];
44
- const normalizedBase = this.normalizeIgnorePatterns(baseIgnore);
45
- const combinedIgnore = [
46
- ...normalizedBase,
47
- ...gitignorePatterns,
48
- ...koduignorePatterns,
49
- ].map((pattern) => pattern.replace(/\\/g, '/'));
50
-
51
- const ig = ignore();
52
- if (combinedIgnore.length > 0) {
53
- ig.add(combinedIgnore);
54
- }
55
-
56
- const globIgnore = this.buildGlobIgnorePatterns(combinedIgnore);
57
-
58
- const patterns = options.rootPaths?.length
59
- ? options.rootPaths.map((p) => `${p}/**`)
60
- : ['**/*'];
61
-
62
- const entries = await glob(patterns, {
63
- onlyFiles: true,
64
- absolute: true,
65
- dot: true,
66
- ignore: [...GLOB_IGNORE, ...globIgnore],
67
- });
68
-
69
- const relativePaths = entries
70
- .map((entry) => path.relative(process.cwd(), entry))
71
- .map((relative) => this.toPosixPath(relative))
72
- .filter((relative) => relative.length > 0);
73
-
74
- const filtered = ig
75
- .filter(relativePaths)
76
- .sort((a, b) => a.localeCompare(b));
77
-
78
- // By default exclude binary files when collecting project files (so pack will skip them).
79
- // Consumers can override with options.excludeBinary = false.
80
- const excludeBinary = options.excludeBinary ?? true;
81
- const useContentDetection =
82
- options.contentBasedBinaryDetection ??
83
- packer.contentBasedBinaryDetection ??
84
- false;
85
- const maxFileSize = options.maxFileSizeBytes ?? MAX_FILE_SIZE_BYTES;
86
-
87
- const textFiles: string[] = [];
88
-
89
- for (const rel of filtered) {
90
- const abs = path.resolve(process.cwd(), rel);
91
- let stats: Stats;
92
-
93
- try {
94
- stats = await fs.stat(abs);
95
- } catch {
96
- continue;
97
- }
98
-
99
- if (stats.size > maxFileSize) {
100
- this.ui.log.warn(
101
- `Skipping large file: ${rel} (>${(maxFileSize / (1024 * 1024)).toFixed(0)}MB)`,
102
- );
103
- continue;
104
- }
105
-
106
- if (
107
- excludeBinary &&
108
- (await this.shouldExcludeBinary(rel, abs, useContentDetection))
109
- ) {
110
- continue;
111
- }
112
-
113
- textFiles.push(rel);
114
- }
115
-
116
- return textFiles;
117
- }
118
-
119
- async readFileRelative(relativePath: string): Promise<string> {
120
- const absolute = path.resolve(process.cwd(), relativePath);
121
- return fs.readFile(absolute, 'utf8');
122
- }
123
-
124
- private toPosixPath(relativePath: string): string {
125
- return relativePath.split(path.sep).join(path.posix.sep);
126
- }
127
-
128
- private normalizeIgnorePatterns(patterns: string[]): string[] {
129
- return patterns
130
- .map((pattern) => pattern.trim())
131
- .filter((pattern) => pattern.length > 0 && !pattern.startsWith('#'));
132
- }
133
-
134
- private buildGlobIgnorePatterns(patterns: string[]): string[] {
135
- const normalized = patterns
136
- .map((pattern) => pattern.trim())
137
- .filter(
138
- (pattern) =>
139
- pattern.length > 0 &&
140
- !pattern.startsWith('#') &&
141
- !pattern.startsWith('!'),
142
- )
143
- .map((pattern) => pattern.replace(/\\/g, '/'));
144
-
145
- const result = new Set<string>();
146
-
147
- for (const pattern of normalized) {
148
- const trimmed = pattern.replace(/\/+$/, '');
149
- result.add(pattern);
150
-
151
- if (trimmed.length === 0) {
152
- continue;
153
- }
154
-
155
- if (!pattern.includes('*')) {
156
- result.add(`${trimmed}/**`);
157
- result.add(`**/${trimmed}/**`);
158
- }
159
-
160
- if (!pattern.startsWith('**/')) {
161
- result.add(`**/${trimmed}`);
162
- }
163
-
164
- if (pattern.endsWith('/')) {
165
- result.add(`${trimmed}/**`);
166
- }
167
- }
168
-
169
- return [...result];
170
- }
171
-
172
- private async readIgnoreFile(fileName: string): Promise<string[]> {
173
- const target = path.join(process.cwd(), fileName);
174
-
175
- try {
176
- const content = await fs.readFile(target, 'utf8');
177
- return this.parseIgnoreContent(content);
178
- } catch {
179
- return [];
180
- }
181
- }
182
-
183
- private parseIgnoreContent(content: string): string[] {
184
- return content
185
- .split(/\r?\n/)
186
- .map((line) => line.trim())
187
- .filter((line) => line.length > 0 && !line.startsWith('#'));
188
- }
189
-
190
- private isBinaryExtension(relativePath: string): boolean {
191
- const ext = path.extname(relativePath).toLowerCase();
192
- return ext.length > 0 && BINARY_EXTENSIONS.has(ext);
193
- }
194
-
195
- private isKnownTextFile(relativePath: string): boolean {
196
- const ext = path.extname(relativePath).toLowerCase();
197
- if (ext && KNOWN_TEXT_EXTENSIONS.has(ext)) {
198
- return true;
199
- }
200
-
201
- const baseName = path.basename(relativePath).toLowerCase();
202
- return KNOWN_TEXT_EXTENSIONS.has(baseName);
203
- }
204
-
205
- private async shouldExcludeBinary(
206
- relativePath: string,
207
- absolutePath: string,
208
- detectByContent: boolean,
209
- ): Promise<boolean> {
210
- if (this.isKnownTextFile(relativePath)) {
211
- return false;
212
- }
213
-
214
- if (this.isBinaryExtension(relativePath)) {
215
- return true;
216
- }
217
-
218
- if (!detectByContent) {
219
- return false;
220
- }
221
-
222
- return this.hasNullByte(absolutePath);
223
- }
224
-
225
- private async hasNullByte(absolutePath: string): Promise<boolean> {
226
- let handle: fs.FileHandle | undefined;
227
-
228
- try {
229
- handle = await fs.open(absolutePath, 'r');
230
- const buffer = Buffer.alloc(BINARY_PROBE_SIZE);
231
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
232
-
233
- for (let i = 0; i < bytesRead; i += 1) {
234
- if (buffer[i] === 0) {
235
- return true;
236
- }
237
- }
238
-
239
- return false;
240
- } catch {
241
- return true;
242
- } finally {
243
- if (handle) {
244
- await handle.close();
245
- }
246
- }
247
- }
248
- }
@@ -1,9 +0,0 @@
1
- import { Global, Module } from '@nestjs/common';
2
- import { UiService } from './ui.service';
3
-
4
- @Global()
5
- @Module({
6
- providers: [UiService],
7
- exports: [UiService],
8
- })
9
- export class UiModule {}
@@ -1,39 +0,0 @@
1
- import confirm from '@inquirer/confirm';
2
- import input from '@inquirer/input';
3
- import select from '@inquirer/select';
4
- import { Injectable } from '@nestjs/common';
5
- import pc from 'picocolors';
6
- import yoctoSpinner, {
7
- type Spinner,
8
- type Options as SpinnerOptions,
9
- } from 'yocto-spinner';
10
-
11
- type InputOptions = Parameters<typeof input>[0];
12
- type ConfirmOptions = Parameters<typeof confirm>[0];
13
- type SelectOptions<TValue> = Parameters<typeof select<TValue>>[0];
14
-
15
- @Injectable()
16
- export class UiService {
17
- readonly log = {
18
- success: (message: string) => console.log(pc.green(`✔ ${message}`)),
19
- warn: (message: string) => console.log(pc.yellow(`⚠ ${message}`)),
20
- error: (message: string) => console.log(pc.red(`✖ ${message}`)),
21
- info: (message: string) => console.log(pc.cyan(`ℹ ${message}`)),
22
- };
23
-
24
- createSpinner(options?: SpinnerOptions & { text?: string }): Spinner {
25
- return yoctoSpinner({ text: options?.text ?? '', ...options });
26
- }
27
-
28
- promptInput(options: InputOptions): Promise<string> {
29
- return input(options);
30
- }
31
-
32
- promptConfirm(options: ConfirmOptions): Promise<boolean> {
33
- return confirm(options);
34
- }
35
-
36
- promptSelect<TValue>(options: SelectOptions<TValue>): Promise<TValue> {
37
- return select<TValue>(options);
38
- }
39
- }
package/src/main.ts DELETED
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { CommandFactory } from 'nest-commander';
4
- import packageJson from '../package.json';
5
- import { AppModule } from './app.module';
6
-
7
- async function bootstrap() {
8
- await CommandFactory.run(AppModule, {
9
- version: packageJson.version,
10
- });
11
- }
12
- bootstrap();
@@ -1,289 +0,0 @@
1
- import { promises as fs } from 'node:fs';
2
- import path from 'node:path';
3
- import { Injectable } from '@nestjs/common';
4
- import { Project, type SourceFile, SyntaxKind, ts } from 'ts-morph';
5
- import { ConfigService } from '../../core/config/config.service';
6
- import { FsService } from '../../core/file-system/fs.service';
7
- import {
8
- type CleanOptions,
9
- type CleanSummary,
10
- type FileCleanReport,
11
- } from './cleaner.types';
12
-
13
- type RemovalRange = {
14
- start: number;
15
- end: number;
16
- text: string;
17
- kind: 'comment' | 'jsx';
18
- };
19
-
20
- @Injectable()
21
- export class CleanerService {
22
- private readonly project = new Project({
23
- useInMemoryFileSystem: false,
24
- skipFileDependencyResolution: true,
25
- compilerOptions: {
26
- allowJs: true,
27
- jsx: ts.JsxEmit.Preserve,
28
- },
29
- });
30
-
31
- private readonly systemWhitelist = [
32
- '@ts-ignore',
33
- '@ts-expect-error',
34
- 'eslint-disable',
35
- 'prettier-ignore',
36
- 'biome-ignore',
37
- 'todo',
38
- 'fixme',
39
- ];
40
-
41
- constructor(
42
- private readonly configService: ConfigService,
43
- private readonly fsService: FsService,
44
- ) {}
45
-
46
- cleanContent(filename: string, content: string, keepJSDoc?: boolean): string {
47
- const config = this.configService.getConfig();
48
- const whitelist = this.buildWhitelist(config.cleaner.whitelist);
49
- const shouldKeepJSDoc = keepJSDoc ?? config.cleaner.keepJSDoc;
50
- const result = this.cleanSource(
51
- filename,
52
- content,
53
- whitelist,
54
- shouldKeepJSDoc,
55
- );
56
- return result.nextContent;
57
- }
58
-
59
- async cleanFiles(
60
- files: string[],
61
- options: CleanOptions = {},
62
- ): Promise<CleanSummary> {
63
- const config = this.configService.getConfig();
64
- const whitelist = this.buildWhitelist(config.cleaner.whitelist);
65
- const keepJSDoc = options.keepJSDoc ?? config.cleaner.keepJSDoc;
66
- let commentsRemoved = 0;
67
- let filesChanged = 0;
68
- let bytesBefore = 0;
69
- let bytesAfter = 0;
70
- const reports: FileCleanReport[] = [];
71
-
72
- for (let i = 0; i < files.length; i++) {
73
- const file = files[i] as string;
74
- options.onProgress?.(i + 1, files.length);
75
-
76
- const original = await this.fsService.readFileRelative(file);
77
- bytesBefore += Buffer.byteLength(original, 'utf8');
78
-
79
- const result = this.cleanSource(file, original, whitelist, keepJSDoc);
80
- bytesAfter += Buffer.byteLength(result.nextContent, 'utf8');
81
-
82
- if (result.removed > 0) {
83
- filesChanged += 1;
84
- commentsRemoved += result.removed;
85
-
86
- if (!options.dryRun) {
87
- if (options.backup) {
88
- await this.backupFile(file, original);
89
- }
90
- await this.writeFile(file, result.nextContent);
91
- }
92
- }
93
-
94
- reports.push({
95
- file,
96
- removed: result.removed,
97
- previews: result.previews,
98
- bytesBefore: Buffer.byteLength(original, 'utf8'),
99
- bytesAfter: Buffer.byteLength(result.nextContent, 'utf8'),
100
- });
101
- }
102
-
103
- return {
104
- filesProcessed: files.length,
105
- filesChanged,
106
- commentsRemoved,
107
- bytesBefore,
108
- bytesAfter,
109
- reports,
110
- };
111
- }
112
-
113
- private cleanSource(
114
- file: string,
115
- content: string,
116
- whitelist: Set<string>,
117
- keepJSDoc: boolean,
118
- ): { nextContent: string; removed: number; previews: string[] } {
119
- const sourceFile = this.project.createSourceFile(file, content, {
120
- overwrite: true,
121
- });
122
- const fullText = sourceFile.getFullText();
123
-
124
- const ranges = this.collectCommentRanges(sourceFile, file);
125
- const candidates = ranges.filter((range) =>
126
- this.shouldRemove(range, whitelist, keepJSDoc),
127
- );
128
-
129
- if (candidates.length === 0) {
130
- return { nextContent: content, removed: 0, previews: [] };
131
- }
132
-
133
- const previews = candidates.map((range) =>
134
- this.normalizePreview(range.text),
135
- );
136
-
137
- const sorted = [...candidates].sort((a, b) => b.start - a.start);
138
- let nextContent = fullText;
139
-
140
- for (const range of sorted) {
141
- const replacement = this.getReplacement(fullText, range);
142
- nextContent = `${nextContent.slice(0, range.start)}${replacement}${nextContent.slice(range.end)}`;
143
- }
144
-
145
- return { nextContent, removed: candidates.length, previews };
146
- }
147
-
148
- private collectCommentRanges(
149
- sourceFile: SourceFile,
150
- file: string,
151
- ): RemovalRange[] {
152
- const fullText = sourceFile.getFullText();
153
- const ranges = new Map<string, RemovalRange>();
154
-
155
- const addRanges = (items: readonly ts.CommentRange[] | undefined) => {
156
- if (!items) return;
157
-
158
- for (const item of items) {
159
- this.addRange(
160
- ranges,
161
- item.pos,
162
- item.end,
163
- fullText.slice(item.pos, item.end),
164
- );
165
- }
166
- };
167
-
168
- const visit = (node: ts.Node): void => {
169
- addRanges(ts.getLeadingCommentRanges(fullText, node.getFullStart()));
170
- addRanges(ts.getTrailingCommentRanges(fullText, node.getEnd()));
171
- ts.forEachChild(node, visit);
172
- };
173
-
174
- visit(sourceFile.compilerNode);
175
-
176
- const jsxExpressions = sourceFile.getDescendantsOfKind(
177
- SyntaxKind.JsxExpression,
178
- );
179
- for (const jsx of jsxExpressions) {
180
- if (jsx.getExpression()) continue;
181
- const text = jsx.getText();
182
- if (!text.includes('/*')) continue;
183
-
184
- const start = jsx.getPos();
185
- const end = jsx.getEnd();
186
- this.addRange(ranges, start, end, fullText.slice(start, end), 'jsx');
187
- }
188
-
189
- if (this.shouldCollectHtmlComments(file)) {
190
- this.collectHtmlCommentRanges(fullText, ranges);
191
- }
192
-
193
- return [...ranges.values()];
194
- }
195
-
196
- private shouldRemove(
197
- range: RemovalRange,
198
- whitelist: Set<string>,
199
- keepJSDoc: boolean,
200
- ): boolean {
201
- const trimmed = range.text.trimStart();
202
- if (keepJSDoc && trimmed.startsWith('/**')) {
203
- return false;
204
- }
205
-
206
- const lower = range.text.toLowerCase();
207
- for (const token of whitelist) {
208
- if (lower.includes(token)) {
209
- return false;
210
- }
211
- }
212
-
213
- return true;
214
- }
215
-
216
- private normalizePreview(text: string): string {
217
- const singleLine = text.replace(/\s+/g, ' ').trim();
218
- if (singleLine.length <= 60) return singleLine;
219
- return `${singleLine.slice(0, 57)}...`;
220
- }
221
-
222
- private getReplacement(original: string, range: RemovalRange): string {
223
- if (range.kind === 'jsx') {
224
- return '';
225
- }
226
-
227
- const before = range.start > 0 ? original[range.start - 1] : '';
228
- const after = range.end < original.length ? original[range.end] : '';
229
- const isIdentifier = (ch: string): boolean => /[A-Za-z0-9_$]/.test(ch);
230
-
231
- if (isIdentifier(before) && isIdentifier(after)) {
232
- return ' ';
233
- }
234
-
235
- return '';
236
- }
237
-
238
- private buildWhitelist(userList: string[]): Set<string> {
239
- const normalized = userList.map((item) => item.toLowerCase());
240
- return new Set([...this.systemWhitelist, ...normalized]);
241
- }
242
-
243
- private async writeFile(file: string, content: string): Promise<void> {
244
- const absolute = path.resolve(process.cwd(), file);
245
- await fs.writeFile(absolute, content, 'utf8');
246
- }
247
-
248
- private async backupFile(file: string, content: string): Promise<void> {
249
- const backupDir = path.join(process.cwd(), '.kodu', 'backup');
250
- const target = path.join(backupDir, file);
251
- await fs.mkdir(path.dirname(target), { recursive: true });
252
- await fs.writeFile(target, content, 'utf8');
253
- }
254
-
255
- private addRange(
256
- ranges: Map<string, RemovalRange>,
257
- start: number,
258
- end: number,
259
- text: string,
260
- kind: RemovalRange['kind'] = 'comment',
261
- ): void {
262
- const key = `${start}:${end}`;
263
- if (ranges.has(key)) return;
264
- ranges.set(key, { start, end, text, kind });
265
- }
266
-
267
- private collectHtmlCommentRanges(
268
- fullText: string,
269
- ranges: Map<string, RemovalRange>,
270
- ): void {
271
- const htmlCommentRegex = /<!--[\s\S]*?-->/g;
272
- let match: RegExpExecArray | null;
273
-
274
- while (true) {
275
- match = htmlCommentRegex.exec(fullText);
276
- if (!match) {
277
- break;
278
- }
279
- const [text] = match;
280
- if (!text) continue;
281
- this.addRange(ranges, match.index, match.index + text.length, text);
282
- }
283
- }
284
-
285
- private shouldCollectHtmlComments(file: string): boolean {
286
- const extension = path.extname(file).toLowerCase();
287
- return extension === '.html' || extension === '.htm';
288
- }
289
- }
@@ -1,23 +0,0 @@
1
- export type CleanOptions = {
2
- dryRun?: boolean;
3
- backup?: boolean;
4
- keepJSDoc?: boolean;
5
- onProgress?: (current: number, total: number) => void;
6
- };
7
-
8
- export type FileCleanReport = {
9
- file: string;
10
- removed: number;
11
- previews: string[];
12
- bytesBefore: number;
13
- bytesAfter: number;
14
- };
15
-
16
- export type CleanSummary = {
17
- filesProcessed: number;
18
- filesChanged: number;
19
- commentsRemoved: number;
20
- bytesBefore: number;
21
- bytesAfter: number;
22
- reports: FileCleanReport[];
23
- };