@akanjs/devkit 2.1.1-rc.1 → 2.1.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/aiEditor.test.ts +68 -0
- package/aiEditor.ts +183 -57
- package/cloud/cloudApi.ts +83 -50
- package/cloud/constants.ts +48 -0
- package/cloud/globalConfig.ts +109 -0
- package/cloud/index.ts +2 -0
- package/executors.ts +748 -164
- package/index.ts +2 -3
- package/linter.ts +308 -97
- package/package.json +2 -2
- package/prompter.ts +17 -4
- package/typecheck/typecheck.proc.ts +21 -0
- package/auth.ts +0 -41
- package/constants.ts +0 -32
package/index.ts
CHANGED
|
@@ -6,11 +6,11 @@ export * from "./applicationBuildRunner";
|
|
|
6
6
|
export * from "./applicationReleasePackager";
|
|
7
7
|
export * from "./applicationTestPreload";
|
|
8
8
|
export * from "./artifact";
|
|
9
|
-
export * from "./auth";
|
|
10
9
|
export * from "./builder";
|
|
11
10
|
export * from "./capacitorApp";
|
|
11
|
+
export * from "./cloud";
|
|
12
|
+
export * from "./cloud";
|
|
12
13
|
export * from "./commandDecorators";
|
|
13
|
-
export * from "./constants";
|
|
14
14
|
export * from "./createTunnel";
|
|
15
15
|
export * from "./dependencyScanner";
|
|
16
16
|
export * from "./executors";
|
|
@@ -35,4 +35,3 @@ export * from "./types";
|
|
|
35
35
|
export * from "./ui";
|
|
36
36
|
export * from "./uploadRelease";
|
|
37
37
|
export * from "./useStdoutDimensions";
|
|
38
|
-
export * from "./cloud";
|
package/linter.ts
CHANGED
|
@@ -1,60 +1,257 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
import { Logger } from "akanjs/common";
|
|
3
|
+
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
type BiomeSeverity = "error" | "warning" | "information" | "hint";
|
|
7
|
+
|
|
8
|
+
interface BiomePosition {
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface BiomeDiagnostic {
|
|
14
|
+
severity: BiomeSeverity;
|
|
15
|
+
message: string;
|
|
16
|
+
category?: string;
|
|
17
|
+
location?: {
|
|
18
|
+
path?: string;
|
|
19
|
+
start?: BiomePosition;
|
|
20
|
+
end?: BiomePosition;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface BiomeReport {
|
|
25
|
+
summary?: {
|
|
26
|
+
changed?: number;
|
|
27
|
+
errors?: number;
|
|
28
|
+
warnings?: number;
|
|
29
|
+
infos?: number;
|
|
30
|
+
};
|
|
31
|
+
diagnostics?: BiomeDiagnostic[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface LintMessage {
|
|
35
|
+
line: number;
|
|
36
|
+
column: number;
|
|
37
|
+
endLine?: number;
|
|
38
|
+
endColumn?: number;
|
|
39
|
+
message: string;
|
|
40
|
+
ruleId: string | null;
|
|
41
|
+
severity: 1 | 2;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface LintResult {
|
|
45
|
+
filePath: string;
|
|
46
|
+
messages: LintMessage[];
|
|
47
|
+
errorCount: number;
|
|
48
|
+
warningCount: number;
|
|
49
|
+
fixableErrorCount: number;
|
|
50
|
+
fixableWarningCount: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface LintResponse {
|
|
54
|
+
fixed: boolean;
|
|
55
|
+
output?: string;
|
|
56
|
+
results: LintResult[];
|
|
57
|
+
errors: LintMessage[];
|
|
58
|
+
warnings: LintMessage[];
|
|
59
|
+
}
|
|
6
60
|
|
|
7
61
|
export class Linter {
|
|
8
|
-
#logger = new Logger("Linter");
|
|
9
|
-
// #eslint: any;
|
|
10
62
|
lintRoot: string;
|
|
63
|
+
#biomeBin: string;
|
|
11
64
|
|
|
12
65
|
constructor(cwdPath: string) {
|
|
13
|
-
this.lintRoot = this.#
|
|
14
|
-
|
|
66
|
+
this.lintRoot = this.#findBiomeRootPath(cwdPath);
|
|
67
|
+
const localBiomeBin = path.join(this.lintRoot, "node_modules/.bin/biome");
|
|
68
|
+
this.#biomeBin = existsSync(localBiomeBin) ? localBiomeBin : "biome";
|
|
15
69
|
}
|
|
16
|
-
|
|
17
|
-
|
|
70
|
+
|
|
71
|
+
#findBiomeRootPath(dir: string): string {
|
|
72
|
+
const configPath = path.join(dir, "biome.json");
|
|
18
73
|
if (existsSync(configPath)) return dir;
|
|
19
74
|
const parentDir = path.dirname(dir);
|
|
20
|
-
|
|
75
|
+
if (parentDir === dir) throw new Error(`biome.json not found from ${dir}`);
|
|
76
|
+
return this.#findBiomeRootPath(parentDir);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#toBiomePath(filePath: string): string {
|
|
80
|
+
const relativePath = path.relative(this.lintRoot, filePath);
|
|
81
|
+
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
|
|
82
|
+
return relativePath;
|
|
83
|
+
return filePath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#resolveFilePath(filePath: string): string {
|
|
87
|
+
return path.isAbsolute(filePath)
|
|
88
|
+
? filePath
|
|
89
|
+
: path.join(this.lintRoot, filePath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async #runBiome(args: string[], input?: string) {
|
|
93
|
+
return await new Promise<{
|
|
94
|
+
stdout: string;
|
|
95
|
+
stderr: string;
|
|
96
|
+
code: number | null;
|
|
97
|
+
}>((resolve, reject) => {
|
|
98
|
+
const proc = spawn(this.#biomeBin, args, {
|
|
99
|
+
cwd: this.lintRoot,
|
|
100
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
101
|
+
});
|
|
102
|
+
let stdout = "";
|
|
103
|
+
let stderr = "";
|
|
104
|
+
|
|
105
|
+
proc.stdout.on("data", (data: Buffer) => {
|
|
106
|
+
stdout += data.toString();
|
|
107
|
+
});
|
|
108
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
109
|
+
stderr += data.toString();
|
|
110
|
+
});
|
|
111
|
+
proc.on("error", reject);
|
|
112
|
+
proc.on("close", (code) => resolve({ stdout, stderr, code }));
|
|
113
|
+
proc.stdin.end(input);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#parseBiomeReport(output: string): BiomeReport {
|
|
118
|
+
const jsonStart = output.indexOf("{");
|
|
119
|
+
const jsonEnd = output.lastIndexOf("}");
|
|
120
|
+
if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart)
|
|
121
|
+
throw new Error(output.trim() || "No Biome JSON output");
|
|
122
|
+
return JSON.parse(output.slice(jsonStart, jsonEnd + 1)) as BiomeReport;
|
|
21
123
|
}
|
|
22
|
-
|
|
124
|
+
|
|
125
|
+
#diagnosticFilePath(diagnostic: BiomeDiagnostic, fallbackFilePath: string) {
|
|
126
|
+
const diagnosticPath = diagnostic.location?.path;
|
|
127
|
+
if (!diagnosticPath) return fallbackFilePath;
|
|
128
|
+
return path.isAbsolute(diagnosticPath)
|
|
129
|
+
? diagnosticPath
|
|
130
|
+
: path.join(this.lintRoot, diagnosticPath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#createLintMessage(diagnostic: BiomeDiagnostic): LintMessage {
|
|
134
|
+
const start = diagnostic.location?.start;
|
|
135
|
+
const end = diagnostic.location?.end;
|
|
136
|
+
return {
|
|
137
|
+
line: Math.max(1, start?.line ?? 1),
|
|
138
|
+
column: Math.max(1, start?.column ?? 1),
|
|
139
|
+
endLine: end?.line,
|
|
140
|
+
endColumn: end?.column,
|
|
141
|
+
message: diagnostic.message,
|
|
142
|
+
ruleId: diagnostic.category ?? null,
|
|
143
|
+
severity: diagnostic.severity === "error" ? 2 : 1,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#toLintResults(report: BiomeReport, filePath: string): LintResult[] {
|
|
148
|
+
const resultsByPath = new Map<string, LintResult>();
|
|
149
|
+
|
|
150
|
+
for (const diagnostic of report.diagnostics ?? []) {
|
|
151
|
+
if (diagnostic.severity !== "error" && diagnostic.severity !== "warning")
|
|
152
|
+
continue;
|
|
153
|
+
const diagnosticFilePath = this.#diagnosticFilePath(diagnostic, filePath);
|
|
154
|
+
const result =
|
|
155
|
+
resultsByPath.get(diagnosticFilePath) ??
|
|
156
|
+
({
|
|
157
|
+
filePath: diagnosticFilePath,
|
|
158
|
+
messages: [],
|
|
159
|
+
errorCount: 0,
|
|
160
|
+
warningCount: 0,
|
|
161
|
+
fixableErrorCount: 0,
|
|
162
|
+
fixableWarningCount: 0,
|
|
163
|
+
} satisfies LintResult);
|
|
164
|
+
const message = this.#createLintMessage(diagnostic);
|
|
165
|
+
result.messages.push(message);
|
|
166
|
+
if (message.severity === 2) result.errorCount += 1;
|
|
167
|
+
else result.warningCount += 1;
|
|
168
|
+
resultsByPath.set(diagnosticFilePath, result);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return [
|
|
172
|
+
resultsByPath.get(filePath) ??
|
|
173
|
+
({
|
|
174
|
+
filePath,
|
|
175
|
+
messages: [],
|
|
176
|
+
errorCount: 0,
|
|
177
|
+
warningCount: 0,
|
|
178
|
+
fixableErrorCount: 0,
|
|
179
|
+
fixableWarningCount: 0,
|
|
180
|
+
} satisfies LintResult),
|
|
181
|
+
...[...resultsByPath.entries()]
|
|
182
|
+
.filter(([resultPath]) => resultPath !== filePath)
|
|
183
|
+
.map(([, result]) => result),
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#splitMessages(results: LintResult[]) {
|
|
188
|
+
const messages = results.flatMap((result) => result.messages);
|
|
189
|
+
return {
|
|
190
|
+
errors: messages.filter((message) => message.severity === 2),
|
|
191
|
+
warnings: messages.filter((message) => message.severity === 1),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async #checkFile(
|
|
196
|
+
filePath: string,
|
|
197
|
+
{ write = false }: { write?: boolean } = {},
|
|
198
|
+
): Promise<LintResponse> {
|
|
199
|
+
const originalContent = existsSync(filePath)
|
|
200
|
+
? readFileSync(filePath, "utf8")
|
|
201
|
+
: "";
|
|
202
|
+
const { stdout, stderr } = await this.#runBiome([
|
|
203
|
+
"check",
|
|
204
|
+
...(write ? ["--write"] : []),
|
|
205
|
+
"--reporter=json",
|
|
206
|
+
"--max-diagnostics=none",
|
|
207
|
+
"--no-errors-on-unmatched",
|
|
208
|
+
"--config-path",
|
|
209
|
+
path.join(this.lintRoot, "biome.json"),
|
|
210
|
+
this.#toBiomePath(filePath),
|
|
211
|
+
]);
|
|
212
|
+
const report = this.#parseBiomeReport(stdout || stderr);
|
|
213
|
+
const results = this.#toLintResults(report, filePath);
|
|
214
|
+
const { errors, warnings } = this.#splitMessages(results);
|
|
215
|
+
const output =
|
|
216
|
+
write && existsSync(filePath)
|
|
217
|
+
? readFileSync(filePath, "utf8")
|
|
218
|
+
: undefined;
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
fixed: write && output !== originalContent,
|
|
222
|
+
output,
|
|
223
|
+
results,
|
|
224
|
+
errors,
|
|
225
|
+
warnings,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async lint(
|
|
230
|
+
filePath: string,
|
|
231
|
+
{ fix = false, dryRun = false }: { fix?: boolean; dryRun?: boolean } = {},
|
|
232
|
+
) {
|
|
23
233
|
if (fix) return await this.fixFile(filePath, dryRun);
|
|
24
234
|
return await this.lintFile(filePath);
|
|
25
235
|
}
|
|
26
236
|
|
|
27
237
|
/**
|
|
28
|
-
* Lint a single file using
|
|
238
|
+
* Lint a single file using Biome.
|
|
29
239
|
* @param filePath - Path to the file to lint
|
|
30
|
-
* @returns Array of
|
|
240
|
+
* @returns Array of Biome results in the legacy lint result shape
|
|
31
241
|
*/
|
|
32
|
-
async lintFile(filePath: string): Promise<{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
warnings: any[]; // ESLintLinter.LintMessage[];
|
|
38
|
-
}> {
|
|
39
|
-
if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
40
|
-
// const isIgnored = await this.#eslint.isPathIgnored(filePath);
|
|
41
|
-
// if (isIgnored) {
|
|
42
|
-
// this.#logger.warn(`File ${filePath} is ignored by ESLint configuration`);
|
|
43
|
-
// return { fixed: false, results: [], errors: [], warnings: [] };
|
|
44
|
-
// }
|
|
45
|
-
// const results = await this.#eslint.lintFiles([filePath]);
|
|
46
|
-
// const errors = results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
|
|
47
|
-
// const warnings = results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
|
|
48
|
-
return { fixed: false, results: [], errors: [], warnings: [] };
|
|
242
|
+
async lintFile(filePath: string): Promise<LintResponse> {
|
|
243
|
+
const resolvedFilePath = this.#resolveFilePath(filePath);
|
|
244
|
+
if (!existsSync(resolvedFilePath))
|
|
245
|
+
throw new Error(`File not found: ${filePath}`);
|
|
246
|
+
return await this.#checkFile(resolvedFilePath);
|
|
49
247
|
}
|
|
50
248
|
|
|
51
249
|
/**
|
|
52
250
|
* Format lint results for console output
|
|
53
|
-
* @param results - Array of
|
|
251
|
+
* @param results - Array of Biome results
|
|
54
252
|
* @returns Formatted string
|
|
55
253
|
*/
|
|
56
|
-
formatLintResults(results:
|
|
57
|
-
// ESLint.LintResult[]
|
|
254
|
+
formatLintResults(results: LintResult[]): string {
|
|
58
255
|
if (results.length === 0) return "No files to lint";
|
|
59
256
|
|
|
60
257
|
const output: string[] = [];
|
|
@@ -68,25 +265,30 @@ export class Linter {
|
|
|
68
265
|
if (result.messages.length > 0) {
|
|
69
266
|
output.push(`\n${chalk.cyan(result.filePath)}`);
|
|
70
267
|
|
|
71
|
-
// Read source file once
|
|
72
268
|
let sourceLines: string[] = [];
|
|
73
269
|
if (existsSync(result.filePath)) {
|
|
74
270
|
try {
|
|
75
271
|
const sourceContent = readFileSync(result.filePath, "utf8");
|
|
76
272
|
sourceLines = sourceContent.split("\n");
|
|
77
|
-
} catch
|
|
273
|
+
} catch {
|
|
78
274
|
// Ignore read errors
|
|
79
275
|
}
|
|
80
276
|
}
|
|
81
277
|
|
|
82
|
-
result.messages.forEach((message
|
|
278
|
+
result.messages.forEach((message) => {
|
|
83
279
|
const type = message.severity === 2 ? "error" : "warning";
|
|
84
280
|
const typeColor = message.severity === 2 ? chalk.red : chalk.yellow;
|
|
85
|
-
const icon = message.severity === 2 ? "
|
|
86
|
-
const ruleInfo = message.ruleId
|
|
281
|
+
const icon = message.severity === 2 ? "x" : "!";
|
|
282
|
+
const ruleInfo = message.ruleId
|
|
283
|
+
? chalk.dim(` (${message.ruleId})`)
|
|
284
|
+
: "";
|
|
87
285
|
|
|
88
|
-
output.push(
|
|
89
|
-
|
|
286
|
+
output.push(
|
|
287
|
+
`\n ${icon} ${typeColor(type)}: ${message.message}${ruleInfo}`,
|
|
288
|
+
);
|
|
289
|
+
output.push(
|
|
290
|
+
` ${chalk.gray("at")} ${result.filePath}:${chalk.bold(`${message.line}:${message.column}`)}`,
|
|
291
|
+
);
|
|
90
292
|
|
|
91
293
|
// Show source line with underline
|
|
92
294
|
if (sourceLines.length > 0 && message.line <= sourceLines.length) {
|
|
@@ -97,19 +299,28 @@ export class Linter {
|
|
|
97
299
|
|
|
98
300
|
// Create underline
|
|
99
301
|
const underlinePrefix = " ".repeat(message.column - 1);
|
|
100
|
-
const underlineLength = message.endColumn
|
|
302
|
+
const underlineLength = message.endColumn
|
|
303
|
+
? message.endColumn - message.column
|
|
304
|
+
: 1;
|
|
101
305
|
const underline = "^".repeat(Math.max(1, underlineLength));
|
|
102
306
|
|
|
103
|
-
output.push(
|
|
307
|
+
output.push(
|
|
308
|
+
`${chalk.dim(`${" ".repeat(lineNumber.length)} |`)} ${underlinePrefix}${typeColor(underline)}`,
|
|
309
|
+
);
|
|
104
310
|
}
|
|
105
311
|
});
|
|
106
312
|
}
|
|
107
313
|
});
|
|
108
314
|
|
|
109
|
-
if (totalErrors === 0 && totalWarnings === 0)
|
|
315
|
+
if (totalErrors === 0 && totalWarnings === 0)
|
|
316
|
+
return chalk.bold("No Biome errors or warnings found");
|
|
110
317
|
|
|
111
|
-
const errorText =
|
|
112
|
-
|
|
318
|
+
const errorText =
|
|
319
|
+
totalErrors > 0 ? chalk.red(`${totalErrors} error(s)`) : "0 errors";
|
|
320
|
+
const warningText =
|
|
321
|
+
totalWarnings > 0
|
|
322
|
+
? chalk.yellow(`${totalWarnings} warning(s)`)
|
|
323
|
+
: "0 warnings";
|
|
113
324
|
const summary = [`\n${errorText}, ${warningText} found`];
|
|
114
325
|
|
|
115
326
|
return summary.concat(output).join("\n");
|
|
@@ -121,24 +332,13 @@ export class Linter {
|
|
|
121
332
|
* @returns Object containing detailed lint information
|
|
122
333
|
*/
|
|
123
334
|
async getDetailedLintInfo(filePath: string): Promise<{
|
|
124
|
-
results:
|
|
335
|
+
results: LintResult[];
|
|
125
336
|
details: {
|
|
126
337
|
line: number;
|
|
127
338
|
column: number;
|
|
128
339
|
message: string;
|
|
129
340
|
ruleId: string | null;
|
|
130
341
|
severity: "error" | "warning";
|
|
131
|
-
fix?: {
|
|
132
|
-
range: [number, number];
|
|
133
|
-
text: string;
|
|
134
|
-
};
|
|
135
|
-
suggestions?: {
|
|
136
|
-
desc: string;
|
|
137
|
-
fix: {
|
|
138
|
-
range: [number, number];
|
|
139
|
-
text: string;
|
|
140
|
-
};
|
|
141
|
-
}[];
|
|
142
342
|
}[];
|
|
143
343
|
stats: {
|
|
144
344
|
errorCount: number;
|
|
@@ -150,14 +350,13 @@ export class Linter {
|
|
|
150
350
|
const { results } = await this.lintFile(filePath);
|
|
151
351
|
|
|
152
352
|
const details = results.flatMap((result) =>
|
|
153
|
-
result.messages.map((message
|
|
353
|
+
result.messages.map((message) => ({
|
|
154
354
|
line: message.line,
|
|
155
355
|
column: message.column,
|
|
156
356
|
message: message.message,
|
|
157
357
|
ruleId: message.ruleId,
|
|
158
|
-
severity:
|
|
159
|
-
|
|
160
|
-
suggestions: message.suggestions,
|
|
358
|
+
severity:
|
|
359
|
+
message.severity === 2 ? ("error" as const) : ("warning" as const),
|
|
161
360
|
})),
|
|
162
361
|
);
|
|
163
362
|
|
|
@@ -166,9 +365,15 @@ export class Linter {
|
|
|
166
365
|
errorCount: acc.errorCount + result.errorCount,
|
|
167
366
|
warningCount: acc.warningCount + result.warningCount,
|
|
168
367
|
fixableErrorCount: acc.fixableErrorCount + result.fixableErrorCount,
|
|
169
|
-
fixableWarningCount:
|
|
368
|
+
fixableWarningCount:
|
|
369
|
+
acc.fixableWarningCount + result.fixableWarningCount,
|
|
170
370
|
}),
|
|
171
|
-
{
|
|
371
|
+
{
|
|
372
|
+
errorCount: 0,
|
|
373
|
+
warningCount: 0,
|
|
374
|
+
fixableErrorCount: 0,
|
|
375
|
+
fixableWarningCount: 0,
|
|
376
|
+
},
|
|
172
377
|
);
|
|
173
378
|
|
|
174
379
|
return { results, details, stats };
|
|
@@ -183,7 +388,7 @@ export class Linter {
|
|
|
183
388
|
try {
|
|
184
389
|
const { results } = await this.lintFile(filePath);
|
|
185
390
|
return results.every((result) => result.errorCount === 0);
|
|
186
|
-
} catch
|
|
391
|
+
} catch {
|
|
187
392
|
return false;
|
|
188
393
|
}
|
|
189
394
|
}
|
|
@@ -193,10 +398,11 @@ export class Linter {
|
|
|
193
398
|
* @param filePath - Path to the file to lint
|
|
194
399
|
* @returns Array of error messages
|
|
195
400
|
*/
|
|
196
|
-
async getErrors(filePath: string): Promise<
|
|
197
|
-
// ESLintLinter.LintMessage[]
|
|
401
|
+
async getErrors(filePath: string): Promise<LintMessage[]> {
|
|
198
402
|
const { results } = await this.lintFile(filePath);
|
|
199
|
-
return results.flatMap((result) =>
|
|
403
|
+
return results.flatMap((result) =>
|
|
404
|
+
result.messages.filter((message) => message.severity === 2),
|
|
405
|
+
);
|
|
200
406
|
}
|
|
201
407
|
|
|
202
408
|
/**
|
|
@@ -204,10 +410,11 @@ export class Linter {
|
|
|
204
410
|
* @param filePath - Path to the file to lint
|
|
205
411
|
* @returns Array of warning messages
|
|
206
412
|
*/
|
|
207
|
-
async getWarnings(filePath: string): Promise<
|
|
208
|
-
// ESLintLinter.LintMessage[]
|
|
413
|
+
async getWarnings(filePath: string): Promise<LintMessage[]> {
|
|
209
414
|
const { results } = await this.lintFile(filePath);
|
|
210
|
-
return results.flatMap((result) =>
|
|
415
|
+
return results.flatMap((result) =>
|
|
416
|
+
result.messages.filter((message) => message.severity === 1),
|
|
417
|
+
);
|
|
211
418
|
}
|
|
212
419
|
|
|
213
420
|
/**
|
|
@@ -216,39 +423,42 @@ export class Linter {
|
|
|
216
423
|
* @param dryRun - If true, returns the fixed content without writing to file
|
|
217
424
|
* @returns Fixed content and remaining issues
|
|
218
425
|
*/
|
|
219
|
-
async fixFile(
|
|
220
|
-
filePath
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
fixed: boolean;
|
|
224
|
-
output?: string;
|
|
225
|
-
results: any[]; // ESLint.LintResult[];
|
|
226
|
-
errors: any[]; // ESLintLinter.LintMessage[];
|
|
227
|
-
warnings: any[]; // ESLintLinter.LintMessage[];
|
|
228
|
-
}> {
|
|
229
|
-
if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
426
|
+
async fixFile(filePath: string, dryRun = false): Promise<LintResponse> {
|
|
427
|
+
const resolvedFilePath = this.#resolveFilePath(filePath);
|
|
428
|
+
if (!existsSync(resolvedFilePath))
|
|
429
|
+
throw new Error(`File not found: ${filePath}`);
|
|
230
430
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// const errors = results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
|
|
234
|
-
// const warnings = results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
|
|
235
|
-
// if (!dryRun) await ESLint.outputFixes(results);
|
|
431
|
+
if (!dryRun)
|
|
432
|
+
return await this.#checkFile(resolvedFilePath, { write: true });
|
|
236
433
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
434
|
+
const source = readFileSync(resolvedFilePath, "utf8");
|
|
435
|
+
const { stdout } = await this.#runBiome(
|
|
436
|
+
[
|
|
437
|
+
"check",
|
|
438
|
+
"--write",
|
|
439
|
+
"--config-path",
|
|
440
|
+
path.join(this.lintRoot, "biome.json"),
|
|
441
|
+
"--stdin-file-path",
|
|
442
|
+
this.#toBiomePath(resolvedFilePath),
|
|
443
|
+
],
|
|
444
|
+
source,
|
|
445
|
+
);
|
|
446
|
+
const lintResult = await this.lintFile(resolvedFilePath);
|
|
447
|
+
return { ...lintResult, fixed: stdout !== source, output: stdout };
|
|
240
448
|
}
|
|
241
449
|
|
|
242
450
|
/**
|
|
243
|
-
* Get
|
|
451
|
+
* Get Biome configuration for a file
|
|
244
452
|
* @param filePath - Path to the file
|
|
245
|
-
* @returns
|
|
453
|
+
* @returns Biome configuration object
|
|
246
454
|
*/
|
|
247
455
|
async getConfigForFile(filePath: string): Promise<unknown> {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return
|
|
456
|
+
const resolvedFilePath = this.#resolveFilePath(filePath);
|
|
457
|
+
if (!existsSync(resolvedFilePath))
|
|
458
|
+
throw new Error(`File not found: ${filePath}`);
|
|
459
|
+
return JSON.parse(
|
|
460
|
+
readFileSync(path.join(this.lintRoot, "biome.json"), "utf8"),
|
|
461
|
+
) as unknown;
|
|
252
462
|
}
|
|
253
463
|
|
|
254
464
|
/**
|
|
@@ -261,8 +471,9 @@ export class Linter {
|
|
|
261
471
|
const ruleCounts: Record<string, number> = {};
|
|
262
472
|
|
|
263
473
|
results.forEach((result) => {
|
|
264
|
-
result.messages.forEach((message
|
|
265
|
-
if (message.ruleId)
|
|
474
|
+
result.messages.forEach((message) => {
|
|
475
|
+
if (message.ruleId)
|
|
476
|
+
ruleCounts[message.ruleId] = (ruleCounts[message.ruleId] || 0) + 1;
|
|
266
477
|
});
|
|
267
478
|
});
|
|
268
479
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "2.1.1
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@langchain/openai": "^1.4.6",
|
|
33
33
|
"@tailwindcss/node": "^4.3.0",
|
|
34
34
|
"@trapezedev/project": "^7.1.4",
|
|
35
|
-
"akanjs": "2.1.1
|
|
35
|
+
"akanjs": "2.1.1",
|
|
36
36
|
"chalk": "^5.6.2",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"daisyui": "^5.5.20",
|
package/prompter.ts
CHANGED
|
@@ -11,7 +11,10 @@ interface FileUpdateRequestProps {
|
|
|
11
11
|
export class Prompter {
|
|
12
12
|
static async #getGuidelineRoot() {
|
|
13
13
|
const dirname = getDirname(import.meta.url);
|
|
14
|
-
const candidates = [
|
|
14
|
+
const candidates = [
|
|
15
|
+
`${dirname}/guidelines`,
|
|
16
|
+
`${dirname}/../cli/guidelines`,
|
|
17
|
+
];
|
|
15
18
|
for (const candidate of candidates) {
|
|
16
19
|
try {
|
|
17
20
|
await fsPromise.access(candidate);
|
|
@@ -25,8 +28,13 @@ export class Prompter {
|
|
|
25
28
|
|
|
26
29
|
static async selectGuideline() {
|
|
27
30
|
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
28
|
-
const guideNames = (await fsPromise.readdir(guidelineRoot)).filter(
|
|
29
|
-
|
|
31
|
+
const guideNames = (await fsPromise.readdir(guidelineRoot)).filter(
|
|
32
|
+
(name) => !name.startsWith("_"),
|
|
33
|
+
);
|
|
34
|
+
return await select({
|
|
35
|
+
message: "Select a guideline",
|
|
36
|
+
choices: guideNames.map((name) => ({ name, value: name })),
|
|
37
|
+
});
|
|
30
38
|
}
|
|
31
39
|
static async getGuideJson(guideName: string): Promise<GuideGenerateJson> {
|
|
32
40
|
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
@@ -41,7 +49,9 @@ export class Prompter {
|
|
|
41
49
|
return content;
|
|
42
50
|
}
|
|
43
51
|
static async getUpdateRequest(guideName: string) {
|
|
44
|
-
return await input({
|
|
52
|
+
return await input({
|
|
53
|
+
message: `What do you want to update in ${guideName}?`,
|
|
54
|
+
});
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
async makeTsFileUpdatePrompt({ context, request }: FileUpdateRequestProps) {
|
|
@@ -49,6 +59,9 @@ export class Prompter {
|
|
|
49
59
|
${await this.getDocumentation("framework")}
|
|
50
60
|
Please understand the following background information, write code that meets the requirements, verify that it satisfies the validation conditions, and return the result.
|
|
51
61
|
|
|
62
|
+
# Code Style
|
|
63
|
+
- Use double quotes for all string literals in TypeScript/TSX code. Do not use single quotes.
|
|
64
|
+
|
|
52
65
|
# Background Information
|
|
53
66
|
\`\`\`markdown
|
|
54
67
|
${context}
|
|
@@ -1,6 +1,27 @@
|
|
|
1
|
+
import { Logger } from "akanjs/common";
|
|
1
2
|
import { TypeChecker } from "../typeChecker";
|
|
2
3
|
|
|
3
4
|
try {
|
|
5
|
+
const filePath = process.env.AKAN_TYPECHECK_FILE;
|
|
6
|
+
if (filePath) {
|
|
7
|
+
const cwdPath = process.env.AKAN_TYPECHECK_CWD;
|
|
8
|
+
if (!cwdPath) throw new Error("AKAN_TYPECHECK_CWD is required");
|
|
9
|
+
|
|
10
|
+
const typeChecker = new TypeChecker({ cwdPath } as never);
|
|
11
|
+
const { fileDiagnostics, fileErrors, fileWarnings } =
|
|
12
|
+
typeChecker.check(filePath);
|
|
13
|
+
const message = typeChecker.formatDiagnostics(fileDiagnostics);
|
|
14
|
+
Logger.rawLog(
|
|
15
|
+
JSON.stringify({
|
|
16
|
+
fileDiagnosticsCount: fileDiagnostics.length,
|
|
17
|
+
fileErrorsCount: fileErrors.length,
|
|
18
|
+
fileWarningsCount: fileWarnings.length,
|
|
19
|
+
message,
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
4
25
|
const configPath = process.env.AKAN_TYPECHECK_TSCONFIG;
|
|
5
26
|
if (!configPath) throw new Error("AKAN_TYPECHECK_TSCONFIG is required");
|
|
6
27
|
const result = TypeChecker.checkProject(configPath);
|