kodu 2.0.0 → 2.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.
- package/README.md +34 -3
- package/dist/package.json +1 -1
- package/dist/src/commands/clean/clean.command.d.ts +14 -1
- package/dist/src/commands/clean/clean.command.js +142 -32
- package/dist/src/commands/clean/clean.command.js.map +1 -1
- package/dist/src/commands/pack/pack.command.d.ts +5 -1
- package/dist/src/commands/pack/pack.command.js +25 -6
- package/dist/src/commands/pack/pack.command.js.map +1 -1
- package/dist/src/commands/pack/pack.module.js +2 -1
- package/dist/src/commands/pack/pack.module.js.map +1 -1
- package/dist/src/shared/cleaner/cleaner.service.d.ts +2 -0
- package/dist/src/shared/cleaner/cleaner.service.js +32 -7
- package/dist/src/shared/cleaner/cleaner.service.js.map +1 -1
- package/dist/src/shared/cleaner/cleaner.types.d.ts +7 -0
- package/dist/src/shared/git/git.service.d.ts +1 -0
- package/dist/src/shared/git/git.service.js +9 -0
- package/dist/src/shared/git/git.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/commands/clean/clean.command.ts +143 -33
- package/src/commands/pack/pack.command.ts +20 -3
- package/src/commands/pack/pack.module.ts +2 -1
- package/src/shared/cleaner/cleaner.service.ts +43 -12
- package/src/shared/cleaner/cleaner.types.ts +7 -0
- package/src/shared/git/git.service.ts +10 -0
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
1
2
|
import { Command, CommandRunner, Option } from 'nest-commander';
|
|
2
3
|
import { ConfigService } from '../../core/config/config.service';
|
|
3
4
|
import { FsService } from '../../core/file-system/fs.service';
|
|
@@ -5,9 +6,16 @@ import { UiService } from '../../core/ui/ui.service';
|
|
|
5
6
|
import { CleanerService } from '../../shared/cleaner/cleaner.service';
|
|
6
7
|
import { GitService } from '../../shared/git/git.service';
|
|
7
8
|
|
|
9
|
+
const SUPPORTED_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs|html|htm)$/i;
|
|
10
|
+
|
|
8
11
|
type CleanOptions = {
|
|
9
12
|
dryRun?: boolean;
|
|
10
13
|
changed?: boolean;
|
|
14
|
+
staged?: boolean;
|
|
15
|
+
backup?: boolean;
|
|
16
|
+
noJsdoc?: boolean;
|
|
17
|
+
verbose?: boolean;
|
|
18
|
+
stdin?: boolean;
|
|
11
19
|
};
|
|
12
20
|
|
|
13
21
|
@Command({ name: 'clean', description: 'Remove comments from code' })
|
|
@@ -22,23 +30,65 @@ export class CleanCommand extends CommandRunner {
|
|
|
22
30
|
super();
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
@Option({
|
|
26
|
-
flags: '-d, --dry-run',
|
|
27
|
-
description: 'Show what will be removed',
|
|
28
|
-
})
|
|
33
|
+
@Option({ flags: '-d, --dry-run', description: 'Show what will be removed' })
|
|
29
34
|
parseDryRun(): boolean {
|
|
30
35
|
return true;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
@Option({
|
|
34
39
|
flags: '-c, --changed',
|
|
35
|
-
description: 'Clean only changed files',
|
|
40
|
+
description: 'Clean only git-changed files (staged + unstaged + untracked)',
|
|
36
41
|
})
|
|
37
42
|
parseChanged(): boolean {
|
|
38
43
|
return true;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
@Option({
|
|
47
|
+
flags: '-s, --staged',
|
|
48
|
+
description: 'Clean only git-staged files',
|
|
49
|
+
})
|
|
50
|
+
parseStaged(): boolean {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Option({
|
|
55
|
+
flags: '-b, --backup',
|
|
56
|
+
description: 'Save originals to .kodu/backup/ before modifying',
|
|
57
|
+
})
|
|
58
|
+
parseBackup(): boolean {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Option({
|
|
63
|
+
flags: '-n, --no-jsdoc',
|
|
64
|
+
description: 'Remove JSDoc comments (overrides config keepJSDoc)',
|
|
65
|
+
})
|
|
66
|
+
parseNoJsdoc(): boolean {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Option({
|
|
71
|
+
flags: '-v, --verbose',
|
|
72
|
+
description: 'Show all removed comments in dry-run (not just first 3)',
|
|
73
|
+
})
|
|
74
|
+
parseVerbose(): boolean {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Option({
|
|
79
|
+
flags: '--stdin',
|
|
80
|
+
description: 'Read from stdin, write cleaned result to stdout',
|
|
81
|
+
})
|
|
82
|
+
parseStdin(): boolean {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async run(inputs: string[], options: CleanOptions = {}): Promise<void> {
|
|
87
|
+
if (options.stdin) {
|
|
88
|
+
await this.runStdin(options);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
42
92
|
const spinner = this.ui
|
|
43
93
|
.createSpinner({ text: this.buildSpinnerText(options) })
|
|
44
94
|
.start();
|
|
@@ -53,45 +103,62 @@ export class CleanCommand extends CommandRunner {
|
|
|
53
103
|
useGitignore: cleanerConfig.useGitignore,
|
|
54
104
|
ignore: ignorePatterns,
|
|
55
105
|
});
|
|
56
|
-
|
|
106
|
+
|
|
107
|
+
const targets = await this.collectTargets(allFiles, inputs, options);
|
|
57
108
|
|
|
58
109
|
if (targets.length === 0) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
spinner.stop(noFilesMessage);
|
|
63
|
-
this.ui.log.warn(noFilesMessage);
|
|
110
|
+
const msg = this.noFilesMessage(options);
|
|
111
|
+
spinner.stop(msg);
|
|
112
|
+
this.ui.log.warn(msg);
|
|
64
113
|
return;
|
|
65
114
|
}
|
|
66
115
|
|
|
67
116
|
const summary = await this.cleaner.cleanFiles(targets, {
|
|
68
117
|
dryRun: options.dryRun,
|
|
118
|
+
backup: options.backup,
|
|
119
|
+
keepJSDoc: options.noJsdoc ? false : undefined,
|
|
120
|
+
onProgress: (current, total) => {
|
|
121
|
+
spinner.text = `${this.buildSpinnerText(options)} (${current}/${total})`;
|
|
122
|
+
},
|
|
69
123
|
});
|
|
70
124
|
|
|
71
125
|
spinner.success(
|
|
72
126
|
options.dryRun ? 'Analysis complete' : 'Cleaning complete',
|
|
73
127
|
);
|
|
74
128
|
|
|
129
|
+
const bytesSaved = summary.bytesBefore - summary.bytesAfter;
|
|
130
|
+
const tokensSaved = Math.round(bytesSaved / 4);
|
|
131
|
+
|
|
75
132
|
if (options.dryRun) {
|
|
76
133
|
this.ui.log.info(
|
|
77
|
-
`Files
|
|
134
|
+
`Files affected: ${summary.filesChanged}/${summary.filesProcessed}, comments: ${summary.commentsRemoved}`,
|
|
78
135
|
);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
136
|
+
this.ui.log.info(`Bytes saved: ${bytesSaved} (~${tokensSaved} tokens)`);
|
|
137
|
+
|
|
138
|
+
const limit = options.verbose ? Number.POSITIVE_INFINITY : 3;
|
|
139
|
+
for (const report of summary.reports.filter((r) => r.removed > 0)) {
|
|
140
|
+
const previews = options.verbose
|
|
141
|
+
? report.previews
|
|
142
|
+
: report.previews.slice(0, limit);
|
|
143
|
+
const more =
|
|
144
|
+
!options.verbose && report.previews.length > limit
|
|
145
|
+
? ` +${report.previews.length - limit} more`
|
|
146
|
+
: '';
|
|
147
|
+
this.ui.log.info(
|
|
148
|
+
` ${report.file} (${report.removed}): ${previews.map((p) => `"${p}"`).join(', ')}${more}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
89
151
|
return;
|
|
90
152
|
}
|
|
91
153
|
|
|
92
154
|
this.ui.log.success(
|
|
93
155
|
`Files cleaned: ${summary.filesChanged}, comments removed: ${summary.commentsRemoved}`,
|
|
94
156
|
);
|
|
157
|
+
this.ui.log.info(`Bytes saved: ${bytesSaved} (~${tokensSaved} tokens)`);
|
|
158
|
+
|
|
159
|
+
if (options.backup && summary.filesChanged > 0) {
|
|
160
|
+
this.ui.log.info('Originals backed up to .kodu/backup/');
|
|
161
|
+
}
|
|
95
162
|
} catch (error) {
|
|
96
163
|
spinner.error('Error during cleaning');
|
|
97
164
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -100,26 +167,69 @@ export class CleanCommand extends CommandRunner {
|
|
|
100
167
|
}
|
|
101
168
|
}
|
|
102
169
|
|
|
170
|
+
private async runStdin(options: CleanOptions): Promise<void> {
|
|
171
|
+
try {
|
|
172
|
+
const input = await this.readStdin();
|
|
173
|
+
const cleaned = this.cleaner.cleanContent(
|
|
174
|
+
'stdin.ts',
|
|
175
|
+
input,
|
|
176
|
+
options.noJsdoc ? false : undefined,
|
|
177
|
+
);
|
|
178
|
+
process.stdout.write(cleaned);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
181
|
+
this.ui.log.error(message);
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private readStdin(): Promise<string> {
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
const chunks: Buffer[] = [];
|
|
189
|
+
const stream = createReadStream('/dev/stdin');
|
|
190
|
+
stream.on('data', (chunk) =>
|
|
191
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk),
|
|
192
|
+
);
|
|
193
|
+
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
194
|
+
stream.on('error', reject);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
103
198
|
private buildSpinnerText(options: CleanOptions): string {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return
|
|
199
|
+
if (options.staged) return 'Cleaning staged files...';
|
|
200
|
+
if (options.changed) return 'Cleaning changed files...';
|
|
201
|
+
return options.dryRun ? 'Analysing...' : 'Cleaning...';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private noFilesMessage(options: CleanOptions): string {
|
|
205
|
+
if (options.staged) return 'No staged files to clean.';
|
|
206
|
+
if (options.changed) return 'No changed files to clean.';
|
|
207
|
+
return 'No files to clean.';
|
|
107
208
|
}
|
|
108
209
|
|
|
109
210
|
private async collectTargets(
|
|
110
211
|
allFiles: string[],
|
|
212
|
+
inputs: string[],
|
|
111
213
|
options: CleanOptions,
|
|
112
214
|
): Promise<string[]> {
|
|
113
|
-
const
|
|
114
|
-
|
|
215
|
+
const supported = allFiles.filter((f) => SUPPORTED_EXTENSIONS.test(f));
|
|
216
|
+
|
|
217
|
+
if (inputs.length > 0) {
|
|
218
|
+
return supported.filter((f) =>
|
|
219
|
+
inputs.some((i) => f === i || f.startsWith(`${i.replace(/\/$/, '')}/`)),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
115
222
|
|
|
116
|
-
if (
|
|
117
|
-
|
|
223
|
+
if (options.staged) {
|
|
224
|
+
const staged = new Set(await this.git.getStagedFiles());
|
|
225
|
+
return supported.filter((f) => staged.has(f));
|
|
118
226
|
}
|
|
119
227
|
|
|
120
|
-
|
|
121
|
-
|
|
228
|
+
if (options.changed) {
|
|
229
|
+
const changed = new Set(await this.git.getChangedFiles());
|
|
230
|
+
return supported.filter((f) => changed.has(f));
|
|
231
|
+
}
|
|
122
232
|
|
|
123
|
-
return
|
|
233
|
+
return supported;
|
|
124
234
|
}
|
|
125
235
|
}
|
|
@@ -6,6 +6,7 @@ import { ConfigService } from '../../core/config/config.service';
|
|
|
6
6
|
import { PromptService } from '../../core/config/prompt.service';
|
|
7
7
|
import { FsService } from '../../core/file-system/fs.service';
|
|
8
8
|
import { UiService } from '../../core/ui/ui.service';
|
|
9
|
+
import { CleanerService } from '../../shared/cleaner/cleaner.service';
|
|
9
10
|
import { TokenizerService } from '../../shared/tokenizer/tokenizer.service';
|
|
10
11
|
|
|
11
12
|
type OutputFormat = 'xml' | 'text';
|
|
@@ -18,6 +19,7 @@ type PackOptions = {
|
|
|
18
19
|
exclude?: string[];
|
|
19
20
|
list?: boolean;
|
|
20
21
|
format?: OutputFormat;
|
|
22
|
+
clean?: boolean;
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
type TemplateContext = {
|
|
@@ -38,6 +40,7 @@ export class PackCommand extends CommandRunner {
|
|
|
38
40
|
private readonly promptService: PromptService,
|
|
39
41
|
private readonly fsService: FsService,
|
|
40
42
|
private readonly tokenizer: TokenizerService,
|
|
43
|
+
private readonly cleaner: CleanerService,
|
|
41
44
|
) {
|
|
42
45
|
super();
|
|
43
46
|
}
|
|
@@ -87,6 +90,14 @@ export class PackCommand extends CommandRunner {
|
|
|
87
90
|
return true;
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
@Option({
|
|
94
|
+
flags: '--clean',
|
|
95
|
+
description: 'Strip comments in-memory before packing (files not modified)',
|
|
96
|
+
})
|
|
97
|
+
parseClean(): boolean {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
@Option({
|
|
91
102
|
flags: '-f, --format <format>',
|
|
92
103
|
description: 'Output format: xml (default) or text',
|
|
@@ -130,7 +141,7 @@ export class PackCommand extends CommandRunner {
|
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
const format: OutputFormat = options.format ?? 'xml';
|
|
133
|
-
const context = await this.buildContext(files, format);
|
|
144
|
+
const context = await this.buildContext(files, format, options.clean);
|
|
134
145
|
const fileList = files.join('\n');
|
|
135
146
|
const { tokens, usdEstimate } = this.tokenizer.count(context);
|
|
136
147
|
|
|
@@ -160,7 +171,9 @@ export class PackCommand extends CommandRunner {
|
|
|
160
171
|
this.ui.log.info(`Files: ${files.length}`);
|
|
161
172
|
this.ui.log.info(`Tokens: ${tokens}`);
|
|
162
173
|
this.ui.log.info(`Cost estimate: ~$${usdEstimate.toFixed(4)}`);
|
|
163
|
-
this.ui.log.info(
|
|
174
|
+
this.ui.log.info(
|
|
175
|
+
`Format: ${format}${options.clean ? ' (comments stripped)' : ''}`,
|
|
176
|
+
);
|
|
164
177
|
this.ui.log.success(`Saved to ${outputPath}`);
|
|
165
178
|
|
|
166
179
|
if (options.copy) {
|
|
@@ -177,10 +190,14 @@ export class PackCommand extends CommandRunner {
|
|
|
177
190
|
private async buildContext(
|
|
178
191
|
files: string[],
|
|
179
192
|
format: OutputFormat,
|
|
193
|
+
clean = false,
|
|
180
194
|
): Promise<string> {
|
|
181
195
|
const chunks = await Promise.all(
|
|
182
196
|
files.map(async (file) => {
|
|
183
|
-
|
|
197
|
+
let content = await this.fsService.readFileRelative(file);
|
|
198
|
+
if (clean) {
|
|
199
|
+
content = this.cleaner.cleanContent(file, content);
|
|
200
|
+
}
|
|
184
201
|
if (format === 'xml') {
|
|
185
202
|
return `<file path="${file}">\n${content}\n</file>`;
|
|
186
203
|
}
|
|
@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
|
|
|
2
2
|
import { ConfigModule } from '../../core/config/config.module';
|
|
3
3
|
import { FsModule } from '../../core/file-system/fs.module';
|
|
4
4
|
import { UiModule } from '../../core/ui/ui.module';
|
|
5
|
+
import { CleanerService } from '../../shared/cleaner/cleaner.service';
|
|
5
6
|
import { TokenizerModule } from '../../shared/tokenizer/tokenizer.module';
|
|
6
7
|
import { PackCommand } from './pack.command';
|
|
7
8
|
|
|
8
9
|
@Module({
|
|
9
10
|
imports: [ConfigModule, UiModule, FsModule, TokenizerModule],
|
|
10
|
-
providers: [PackCommand],
|
|
11
|
+
providers: [PackCommand, CleanerService],
|
|
11
12
|
})
|
|
12
13
|
export class PackModule {}
|
|
@@ -43,30 +43,50 @@ export class CleanerService {
|
|
|
43
43
|
private readonly fsService: FsService,
|
|
44
44
|
) {}
|
|
45
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
|
+
|
|
46
59
|
async cleanFiles(
|
|
47
60
|
files: string[],
|
|
48
61
|
options: CleanOptions = {},
|
|
49
62
|
): Promise<CleanSummary> {
|
|
50
63
|
const config = this.configService.getConfig();
|
|
51
64
|
const whitelist = this.buildWhitelist(config.cleaner.whitelist);
|
|
65
|
+
const keepJSDoc = options.keepJSDoc ?? config.cleaner.keepJSDoc;
|
|
52
66
|
let commentsRemoved = 0;
|
|
53
67
|
let filesChanged = 0;
|
|
68
|
+
let bytesBefore = 0;
|
|
69
|
+
let bytesAfter = 0;
|
|
54
70
|
const reports: FileCleanReport[] = [];
|
|
55
71
|
|
|
56
|
-
for (
|
|
72
|
+
for (let i = 0; i < files.length; i++) {
|
|
73
|
+
const file = files[i] as string;
|
|
74
|
+
options.onProgress?.(i + 1, files.length);
|
|
75
|
+
|
|
57
76
|
const original = await this.fsService.readFileRelative(file);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
config.cleaner.keepJSDoc,
|
|
63
|
-
);
|
|
77
|
+
bytesBefore += Buffer.byteLength(original, 'utf8');
|
|
78
|
+
|
|
79
|
+
const result = this.cleanSource(file, original, whitelist, keepJSDoc);
|
|
80
|
+
bytesAfter += Buffer.byteLength(result.nextContent, 'utf8');
|
|
64
81
|
|
|
65
82
|
if (result.removed > 0) {
|
|
66
83
|
filesChanged += 1;
|
|
67
84
|
commentsRemoved += result.removed;
|
|
68
85
|
|
|
69
86
|
if (!options.dryRun) {
|
|
87
|
+
if (options.backup) {
|
|
88
|
+
await this.backupFile(file, original);
|
|
89
|
+
}
|
|
70
90
|
await this.writeFile(file, result.nextContent);
|
|
71
91
|
}
|
|
72
92
|
}
|
|
@@ -75,6 +95,8 @@ export class CleanerService {
|
|
|
75
95
|
file,
|
|
76
96
|
removed: result.removed,
|
|
77
97
|
previews: result.previews,
|
|
98
|
+
bytesBefore: Buffer.byteLength(original, 'utf8'),
|
|
99
|
+
bytesAfter: Buffer.byteLength(result.nextContent, 'utf8'),
|
|
78
100
|
});
|
|
79
101
|
}
|
|
80
102
|
|
|
@@ -82,6 +104,8 @@ export class CleanerService {
|
|
|
82
104
|
filesProcessed: files.length,
|
|
83
105
|
filesChanged,
|
|
84
106
|
commentsRemoved,
|
|
107
|
+
bytesBefore,
|
|
108
|
+
bytesAfter,
|
|
85
109
|
reports,
|
|
86
110
|
};
|
|
87
111
|
}
|
|
@@ -106,9 +130,9 @@ export class CleanerService {
|
|
|
106
130
|
return { nextContent: content, removed: 0, previews: [] };
|
|
107
131
|
}
|
|
108
132
|
|
|
109
|
-
const previews = candidates
|
|
110
|
-
.
|
|
111
|
-
|
|
133
|
+
const previews = candidates.map((range) =>
|
|
134
|
+
this.normalizePreview(range.text),
|
|
135
|
+
);
|
|
112
136
|
|
|
113
137
|
const sorted = [...candidates].sort((a, b) => b.start - a.start);
|
|
114
138
|
let nextContent = fullText;
|
|
@@ -191,8 +215,8 @@ export class CleanerService {
|
|
|
191
215
|
|
|
192
216
|
private normalizePreview(text: string): string {
|
|
193
217
|
const singleLine = text.replace(/\s+/g, ' ').trim();
|
|
194
|
-
if (singleLine.length <=
|
|
195
|
-
return `${singleLine.slice(0,
|
|
218
|
+
if (singleLine.length <= 60) return singleLine;
|
|
219
|
+
return `${singleLine.slice(0, 57)}...`;
|
|
196
220
|
}
|
|
197
221
|
|
|
198
222
|
private getReplacement(original: string, range: RemovalRange): string {
|
|
@@ -221,6 +245,13 @@ export class CleanerService {
|
|
|
221
245
|
await fs.writeFile(absolute, content, 'utf8');
|
|
222
246
|
}
|
|
223
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
|
+
|
|
224
255
|
private addRange(
|
|
225
256
|
ranges: Map<string, RemovalRange>,
|
|
226
257
|
start: number,
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
export type CleanOptions = {
|
|
2
2
|
dryRun?: boolean;
|
|
3
|
+
backup?: boolean;
|
|
4
|
+
keepJSDoc?: boolean;
|
|
5
|
+
onProgress?: (current: number, total: number) => void;
|
|
3
6
|
};
|
|
4
7
|
|
|
5
8
|
export type FileCleanReport = {
|
|
6
9
|
file: string;
|
|
7
10
|
removed: number;
|
|
8
11
|
previews: string[];
|
|
12
|
+
bytesBefore: number;
|
|
13
|
+
bytesAfter: number;
|
|
9
14
|
};
|
|
10
15
|
|
|
11
16
|
export type CleanSummary = {
|
|
12
17
|
filesProcessed: number;
|
|
13
18
|
filesChanged: number;
|
|
14
19
|
commentsRemoved: number;
|
|
20
|
+
bytesBefore: number;
|
|
21
|
+
bytesAfter: number;
|
|
15
22
|
reports: FileCleanReport[];
|
|
16
23
|
};
|
|
@@ -34,4 +34,14 @@ export class GitService {
|
|
|
34
34
|
await load(['ls-files', '--others', '--exclude-standard']);
|
|
35
35
|
return [...changed].sort();
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
async getStagedFiles(): Promise<string[]> {
|
|
39
|
+
await this.ensureRepo();
|
|
40
|
+
const { stdout } = await execa('git', ['diff', '--name-only', '--staged']);
|
|
41
|
+
return stdout
|
|
42
|
+
.split('\n')
|
|
43
|
+
.map((entry) => entry.trim())
|
|
44
|
+
.filter((entry) => entry.length > 0)
|
|
45
|
+
.sort();
|
|
46
|
+
}
|
|
37
47
|
}
|