pi-formatter 0.1.1 → 0.2.0
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 +28 -5
- package/extensions/formatter/config.ts +82 -0
- package/extensions/{format → formatter}/context.ts +58 -37
- package/extensions/{format → formatter}/dispatch.ts +114 -32
- package/extensions/{format → formatter}/path.ts +19 -3
- package/extensions/{format → formatter}/plan.ts +5 -5
- package/extensions/{format/runners/biome-check-write.ts → formatter/runners/biome.ts} +4 -4
- package/extensions/formatter/runners/config-patterns.ts +5 -0
- package/extensions/{format/runners/eslint-fix.ts → formatter/runners/eslint.ts} +3 -3
- package/extensions/{format → formatter}/runners/index.ts +10 -12
- package/extensions/{format/runners/markdownlint-fix.ts → formatter/runners/markdownlint.ts} +3 -3
- package/extensions/{format/runners/prettier-markdown.ts → formatter/runners/prettier.ts} +3 -3
- package/extensions/{format/runners/ruff-check-fix.ts → formatter/runners/ruff-check.ts} +3 -3
- package/extensions/{format → formatter}/types.ts +4 -6
- package/extensions/index.ts +91 -13
- package/package.json +1 -2
- package/DOCUMENTATION.md +0 -120
- package/extensions/format/config.ts +0 -11
- package/extensions/format/runners/config-patterns.ts +0 -6
- package/extensions/format/runners/prettier-config-write.ts +0 -11
- /package/extensions/{format → formatter}/runners/clang-format.ts +0 -0
- /package/extensions/{format → formatter}/runners/cmake-format.ts +0 -0
- /package/extensions/{format → formatter}/runners/helpers.ts +0 -0
- /package/extensions/{format → formatter}/runners/ruff-format.ts +0 -0
- /package/extensions/{format → formatter}/runners/shfmt.ts +0 -0
- /package/extensions/{format → formatter}/system.ts +0 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 🎨 pi-formatter
|
|
2
2
|
|
|
3
|
-
A [pi](https://
|
|
4
|
-
|
|
3
|
+
A [pi](https://pi.dev) extension that auto-formats files after every `write` and
|
|
4
|
+
`edit` tool call.
|
|
5
5
|
|
|
6
6
|
The extension hooks into successful tool results, detects the file type, and
|
|
7
7
|
runs the appropriate formatter. Failures never block the tool result, so
|
|
@@ -28,13 +28,36 @@ Supported file types:
|
|
|
28
28
|
- Python
|
|
29
29
|
- JavaScript/TypeScript
|
|
30
30
|
|
|
31
|
+
For JS/TS and JSON, project-configured tools are preferred first (Biome,
|
|
32
|
+
ESLint), with Prettier as a fallback.
|
|
33
|
+
|
|
31
34
|
## 🔧 Configuration
|
|
32
35
|
|
|
33
|
-
-
|
|
36
|
+
Create `<agent-dir>/formatter.json`, where `<agent-dir>` is pi's agent config
|
|
37
|
+
folder (default: `~/.pi/agent`, overridable via `PI_CODING_AGENT_DIR`):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"commandTimeoutMs": 10000,
|
|
42
|
+
"hideCallSummariesInTui": false
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `commandTimeoutMs`: timeout (ms) per formatter command (default: `10000`)
|
|
47
|
+
- `hideCallSummariesInTui`: hide formatter pass/fail summaries in the TUI (default: `false`)
|
|
48
|
+
|
|
49
|
+
## 🧩 Adding formatters
|
|
50
|
+
|
|
51
|
+
Each formatter is a _runner_ that wraps a CLI tool behind a common interface.
|
|
52
|
+
To add one:
|
|
34
53
|
|
|
35
|
-
|
|
54
|
+
1. Create a file in `extensions/formatter/runners/` using `defineRunner` and a
|
|
55
|
+
launcher helper (`direct`, `pypi`, or `goTool`).
|
|
56
|
+
2. Register it in `extensions/formatter/runners/index.ts`.
|
|
57
|
+
3. Add its id to a group in `extensions/formatter/plan.ts`.
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
The format plan maps file kinds to ordered runner groups. Each group runs in
|
|
60
|
+
`"all"` mode (every runner) or `"fallback"` mode (first match wins).
|
|
38
61
|
|
|
39
62
|
## 📄 License
|
|
40
63
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 10_000;
|
|
6
|
+
const DEFAULT_HIDE_CALL_SUMMARIES_IN_TUI = false;
|
|
7
|
+
const FORMATTER_CONFIG_FILE = "formatter.json";
|
|
8
|
+
|
|
9
|
+
type FormatterConfig = {
|
|
10
|
+
commandTimeoutMs: number;
|
|
11
|
+
hideCallSummariesInTui: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function getFormatterConfigPath(): string {
|
|
15
|
+
return join(getAgentDir(), FORMATTER_CONFIG_FILE);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readJsonObject(filePath: string): Record<string, unknown> | undefined {
|
|
19
|
+
try {
|
|
20
|
+
if (!existsSync(filePath)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const content = readFileSync(filePath, "utf8");
|
|
25
|
+
const parsed = JSON.parse(content);
|
|
26
|
+
|
|
27
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return parsed as Record<string, unknown>;
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parsePositiveInt(value: unknown, defaultValue: number): number {
|
|
38
|
+
if (typeof value !== "number") {
|
|
39
|
+
return defaultValue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
43
|
+
return defaultValue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseBooleanValue(value: unknown, defaultValue: boolean): boolean {
|
|
50
|
+
if (typeof value !== "boolean") {
|
|
51
|
+
return defaultValue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadFormatterConfig(): FormatterConfig {
|
|
58
|
+
const config = readJsonObject(getFormatterConfigPath());
|
|
59
|
+
|
|
60
|
+
if (!config) {
|
|
61
|
+
return {
|
|
62
|
+
commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
|
|
63
|
+
hideCallSummariesInTui: DEFAULT_HIDE_CALL_SUMMARIES_IN_TUI,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
commandTimeoutMs: parsePositiveInt(
|
|
69
|
+
config.commandTimeoutMs,
|
|
70
|
+
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
71
|
+
),
|
|
72
|
+
hideCallSummariesInTui: parseBooleanValue(
|
|
73
|
+
config.hideCallSummariesInTui,
|
|
74
|
+
DEFAULT_HIDE_CALL_SUMMARIES_IN_TUI,
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const formatterConfig = loadFormatterConfig();
|
|
80
|
+
|
|
81
|
+
export const commandTimeoutMs = formatterConfig.commandTimeoutMs;
|
|
82
|
+
export const hideCallSummariesInTui = formatterConfig.hideCallSummariesInTui;
|
|
@@ -3,7 +3,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
3
3
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { getPathForGit, isWithinDirectory, pathExists } from "./path.js";
|
|
5
5
|
import { hasCommand } from "./system.js";
|
|
6
|
-
import type { FileKind, RequiredMajorVersion, RunnerContext
|
|
6
|
+
import type { FileKind, RequiredMajorVersion, RunnerContext } from "./types.js";
|
|
7
7
|
|
|
8
8
|
const globRegexCache = new Map<string, RegExp>();
|
|
9
9
|
|
|
@@ -66,14 +66,44 @@ async function findConfigFileFromPath(
|
|
|
66
66
|
return undefined;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function getLineRangesFromDiff(diffOutput: string): string[] {
|
|
70
|
+
const ranges: string[] = [];
|
|
71
|
+
|
|
72
|
+
for (const line of diffOutput.split(/\r?\n/)) {
|
|
73
|
+
const match = line.match(/^@@ .* \+([0-9]+)(?:,([0-9]+))? @@/);
|
|
74
|
+
if (!match) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const start = Number.parseInt(match[1], 10);
|
|
79
|
+
const count = match[2] ? Number.parseInt(match[2], 10) : 1;
|
|
80
|
+
|
|
81
|
+
if (!Number.isFinite(start) || !Number.isFinite(count) || count <= 0) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ranges.push(`${start}:${start + count - 1}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return ranges;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function compareLineRanges(a: string, b: string): number {
|
|
92
|
+
const [aStart] = a.split(":", 1);
|
|
93
|
+
const [bStart] = b.split(":", 1);
|
|
94
|
+
return Number.parseInt(aStart, 10) - Number.parseInt(bStart, 10);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type FormatWarningReporter = (message: string) => void;
|
|
98
|
+
|
|
69
99
|
export class FormatRunContext implements RunnerContext {
|
|
70
100
|
readonly filePath: string;
|
|
71
101
|
readonly cwd: string;
|
|
72
|
-
readonly sourceTool: SourceTool;
|
|
73
102
|
readonly kind: FileKind;
|
|
74
103
|
|
|
75
104
|
private readonly pi: ExtensionAPI;
|
|
76
105
|
private readonly timeoutMs: number;
|
|
106
|
+
private readonly warningReporter?: FormatWarningReporter;
|
|
77
107
|
|
|
78
108
|
private readonly configLookupCache = new Map<
|
|
79
109
|
string,
|
|
@@ -95,23 +125,25 @@ export class FormatRunContext implements RunnerContext {
|
|
|
95
125
|
pi: ExtensionAPI,
|
|
96
126
|
cwd: string,
|
|
97
127
|
filePath: string,
|
|
98
|
-
sourceTool: SourceTool,
|
|
99
128
|
kind: FileKind,
|
|
100
129
|
timeoutMs: number,
|
|
130
|
+
warningReporter?: FormatWarningReporter,
|
|
101
131
|
) {
|
|
102
132
|
this.pi = pi;
|
|
103
133
|
this.cwd = cwd;
|
|
104
134
|
this.filePath = filePath;
|
|
105
|
-
this.sourceTool = sourceTool;
|
|
106
135
|
this.kind = kind;
|
|
107
136
|
this.timeoutMs = timeoutMs;
|
|
137
|
+
this.warningReporter = warningReporter;
|
|
108
138
|
}
|
|
109
139
|
|
|
110
140
|
async hasCommand(command: string): Promise<boolean> {
|
|
111
141
|
return hasCommand(command);
|
|
112
142
|
}
|
|
113
143
|
|
|
114
|
-
async findConfigFile(
|
|
144
|
+
async findConfigFile(
|
|
145
|
+
patterns: readonly string[],
|
|
146
|
+
): Promise<string | undefined> {
|
|
115
147
|
const key = patterns.join("\u0000");
|
|
116
148
|
let cached = this.configLookupCache.get(key);
|
|
117
149
|
|
|
@@ -136,14 +168,10 @@ export class FormatRunContext implements RunnerContext {
|
|
|
136
168
|
}
|
|
137
169
|
|
|
138
170
|
async exec(command: string, args: string[]) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
} catch {
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
171
|
+
return this.pi.exec(command, args, {
|
|
172
|
+
cwd: this.cwd,
|
|
173
|
+
timeout: this.timeoutMs,
|
|
174
|
+
});
|
|
147
175
|
}
|
|
148
176
|
|
|
149
177
|
async getChangedLines(): Promise<string[]> {
|
|
@@ -182,6 +210,11 @@ export class FormatRunContext implements RunnerContext {
|
|
|
182
210
|
}
|
|
183
211
|
|
|
184
212
|
warn(message: string): void {
|
|
213
|
+
if (this.warningReporter) {
|
|
214
|
+
this.warningReporter(message);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
185
218
|
console.warn(message);
|
|
186
219
|
}
|
|
187
220
|
|
|
@@ -207,37 +240,25 @@ export class FormatRunContext implements RunnerContext {
|
|
|
207
240
|
|
|
208
241
|
private async resolveChangedLines(): Promise<string[]> {
|
|
209
242
|
const gitPath = getPathForGit(this.filePath, this.cwd);
|
|
210
|
-
const
|
|
211
|
-
"diff",
|
|
212
|
-
"--cached",
|
|
213
|
-
"--unified=0",
|
|
214
|
-
"--",
|
|
215
|
-
gitPath,
|
|
216
|
-
]);
|
|
217
|
-
|
|
218
|
-
if (!diffResult || diffResult.code !== 0) {
|
|
219
|
-
return [];
|
|
220
|
-
}
|
|
243
|
+
const rangeSet = new Set<string>();
|
|
221
244
|
|
|
222
|
-
const
|
|
245
|
+
const diffArgSets = [
|
|
246
|
+
["diff", "--unified=0", "--", gitPath],
|
|
247
|
+
["diff", "--cached", "--unified=0", "--", gitPath],
|
|
248
|
+
];
|
|
223
249
|
|
|
224
|
-
for (const
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
250
|
+
for (const args of diffArgSets) {
|
|
251
|
+
const diffResult = await this.exec("git", args);
|
|
252
|
+
if (diffResult.code !== 0) {
|
|
227
253
|
continue;
|
|
228
254
|
}
|
|
229
255
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (!Number.isFinite(start) || !Number.isFinite(count) || count <= 0) {
|
|
234
|
-
continue;
|
|
256
|
+
for (const range of getLineRangesFromDiff(diffResult.stdout)) {
|
|
257
|
+
rangeSet.add(range);
|
|
235
258
|
}
|
|
236
|
-
|
|
237
|
-
ranges.push(`${start}:${start + count - 1}`);
|
|
238
259
|
}
|
|
239
260
|
|
|
240
|
-
return
|
|
261
|
+
return [...rangeSet].sort(compareLineRanges);
|
|
241
262
|
}
|
|
242
263
|
|
|
243
264
|
private async resolveRequiredMajorVersionFromConfig(
|
|
@@ -266,7 +287,7 @@ export class FormatRunContext implements RunnerContext {
|
|
|
266
287
|
command: string,
|
|
267
288
|
): Promise<string | undefined> {
|
|
268
289
|
const result = await this.exec(command, ["--version"]);
|
|
269
|
-
if (
|
|
290
|
+
if (result.code !== 0) {
|
|
270
291
|
return undefined;
|
|
271
292
|
}
|
|
272
293
|
|
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { FormatRunContext } from "./context.js";
|
|
1
|
+
import type { ExecResult, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { FormatRunContext, type FormatWarningReporter } from "./context.js";
|
|
3
3
|
import { detectFileKind } from "./path.js";
|
|
4
4
|
import { FORMAT_PLAN } from "./plan.js";
|
|
5
5
|
import { RUNNERS } from "./runners/index.js";
|
|
6
6
|
import {
|
|
7
7
|
isDynamicRunner,
|
|
8
8
|
type ResolvedLauncher,
|
|
9
|
+
type RunnerContext,
|
|
9
10
|
type RunnerDefinition,
|
|
10
11
|
type RunnerGroup,
|
|
11
12
|
type RunnerLauncher,
|
|
12
|
-
type RunnerContext,
|
|
13
|
-
type SourceTool,
|
|
14
13
|
} from "./types.js";
|
|
15
14
|
|
|
16
|
-
function summarizeExecResult(result: ExecResult): string {
|
|
17
|
-
const output = `${result.stderr}\n${result.stdout}`.trim();
|
|
18
|
-
if (!output) {
|
|
19
|
-
return "";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const firstLine = output.split(/\r?\n/, 1)[0];
|
|
23
|
-
return `: ${firstLine}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
15
|
async function resolveLauncher(
|
|
27
16
|
launcher: RunnerLauncher,
|
|
28
17
|
ctx: RunnerContext,
|
|
@@ -93,15 +82,17 @@ async function satisfiesRunnerRequirements(
|
|
|
93
82
|
const onInvalid = requirement.onInvalid ?? "warn-skip";
|
|
94
83
|
if (onInvalid === "warn-skip") {
|
|
95
84
|
ctx.warn(
|
|
96
|
-
|
|
85
|
+
`${runner.id} skipped: invalid version requirement in ${requirement.patterns.join(", ")}`,
|
|
97
86
|
);
|
|
98
87
|
}
|
|
99
88
|
|
|
100
89
|
return false;
|
|
101
90
|
}
|
|
102
91
|
|
|
103
|
-
const versionCommand =
|
|
104
|
-
|
|
92
|
+
const versionCommand =
|
|
93
|
+
requirement.command ?? defaultVersionCommand(runner.launcher);
|
|
94
|
+
const installedVersion =
|
|
95
|
+
await ctx.getInstalledToolMajorVersion(versionCommand);
|
|
105
96
|
|
|
106
97
|
if (installedVersion === requiredVersion) {
|
|
107
98
|
return true;
|
|
@@ -110,7 +101,7 @@ async function satisfiesRunnerRequirements(
|
|
|
110
101
|
const onMismatch = requirement.onMismatch ?? "warn-skip";
|
|
111
102
|
if (onMismatch === "warn-skip") {
|
|
112
103
|
ctx.warn(
|
|
113
|
-
|
|
104
|
+
`${runner.id} skipped: ${versionCommand} version mismatch (have ${installedVersion ?? "unknown"}, need ${requiredVersion})`,
|
|
114
105
|
);
|
|
115
106
|
}
|
|
116
107
|
|
|
@@ -135,9 +126,56 @@ async function resolveRunnerArgs(
|
|
|
135
126
|
|
|
136
127
|
type RunnerOutcome = "skipped" | "failed" | "succeeded";
|
|
137
128
|
|
|
129
|
+
export interface FormatCallSummary {
|
|
130
|
+
runnerId: string;
|
|
131
|
+
status: "succeeded" | "failed";
|
|
132
|
+
exitCode?: number;
|
|
133
|
+
failureMessage?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type FormatCallSummaryReporter = (summary: FormatCallSummary) => void;
|
|
137
|
+
|
|
138
|
+
const MAX_FAILURE_MESSAGE_LENGTH = 140;
|
|
139
|
+
const ANSI_ESCAPE = String.fromCharCode(27);
|
|
140
|
+
const ANSI_COLOR_SEQUENCE_PATTERN = new RegExp(
|
|
141
|
+
`${ANSI_ESCAPE}\\[[0-9;]*m`,
|
|
142
|
+
"g",
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
function normalizeFailureLine(line: string): string {
|
|
146
|
+
return line
|
|
147
|
+
.replace(ANSI_COLOR_SEQUENCE_PATTERN, "")
|
|
148
|
+
.replace(/^\s*\[error\]\s*/i, "")
|
|
149
|
+
.replace(/^\s*error:\s*/i, "")
|
|
150
|
+
.replace(/^\s*[×✖✘]\s*/u, "")
|
|
151
|
+
.replace(/\s+/g, " ")
|
|
152
|
+
.trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function summarizeFailureMessage(result: ExecResult): string | undefined {
|
|
156
|
+
const lines = `${result.stderr}\n${result.stdout}`
|
|
157
|
+
.split(/\r?\n/)
|
|
158
|
+
.map((line) => normalizeFailureLine(line))
|
|
159
|
+
.filter((line) => line.length > 0);
|
|
160
|
+
|
|
161
|
+
if (lines.length === 0) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const withMarker = lines.find((line) =>
|
|
166
|
+
/\b(error|failed|invalid|unexpected|expected|syntax)\b/i.test(line),
|
|
167
|
+
);
|
|
168
|
+
const message = withMarker ?? lines[0];
|
|
169
|
+
|
|
170
|
+
return message.length <= MAX_FAILURE_MESSAGE_LENGTH
|
|
171
|
+
? message
|
|
172
|
+
: `${message.slice(0, MAX_FAILURE_MESSAGE_LENGTH - 1)}…`;
|
|
173
|
+
}
|
|
174
|
+
|
|
138
175
|
async function runRunner(
|
|
139
176
|
ctx: RunnerContext,
|
|
140
177
|
runner: RunnerDefinition,
|
|
178
|
+
summaryReporter?: FormatCallSummaryReporter,
|
|
141
179
|
): Promise<RunnerOutcome> {
|
|
142
180
|
const launcher = await resolveLauncher(runner.launcher, ctx);
|
|
143
181
|
if (!launcher) {
|
|
@@ -157,60 +195,104 @@ async function runRunner(
|
|
|
157
195
|
return "skipped";
|
|
158
196
|
}
|
|
159
197
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return "failed";
|
|
164
|
-
}
|
|
198
|
+
const commandArgs = [...launcher.argsPrefix, ...args];
|
|
199
|
+
|
|
200
|
+
const result = await ctx.exec(launcher.command, commandArgs);
|
|
165
201
|
|
|
166
202
|
if (result.code !== 0) {
|
|
203
|
+
const failureMessage = summarizeFailureMessage(result);
|
|
204
|
+
|
|
205
|
+
summaryReporter?.({
|
|
206
|
+
runnerId: runner.id,
|
|
207
|
+
status: "failed",
|
|
208
|
+
exitCode: result.code,
|
|
209
|
+
failureMessage,
|
|
210
|
+
});
|
|
211
|
+
|
|
167
212
|
ctx.warn(
|
|
168
|
-
|
|
213
|
+
`${runner.id} failed (${result.code})${failureMessage ? `: ${failureMessage}` : ""}`,
|
|
169
214
|
);
|
|
170
215
|
return "failed";
|
|
171
216
|
}
|
|
172
217
|
|
|
218
|
+
summaryReporter?.({
|
|
219
|
+
runnerId: runner.id,
|
|
220
|
+
status: "succeeded",
|
|
221
|
+
});
|
|
222
|
+
|
|
173
223
|
return "succeeded";
|
|
174
224
|
}
|
|
175
225
|
|
|
176
226
|
async function runRunnerGroup(
|
|
177
227
|
ctx: RunnerContext,
|
|
178
228
|
group: RunnerGroup,
|
|
229
|
+
summaryReporter?: FormatCallSummaryReporter,
|
|
179
230
|
): Promise<void> {
|
|
180
231
|
if (group.mode === "all") {
|
|
181
232
|
for (const runnerId of group.runnerIds) {
|
|
182
233
|
const runner = RUNNERS.get(runnerId);
|
|
183
234
|
if (!runner) {
|
|
184
|
-
ctx.warn(`
|
|
235
|
+
ctx.warn(`unknown runner in format plan: ${runnerId}`);
|
|
185
236
|
continue;
|
|
186
237
|
}
|
|
187
238
|
|
|
188
|
-
await runRunner(ctx, runner);
|
|
239
|
+
await runRunner(ctx, runner, summaryReporter);
|
|
189
240
|
}
|
|
190
241
|
|
|
191
242
|
return;
|
|
192
243
|
}
|
|
193
244
|
|
|
245
|
+
const fallbackSummaries: FormatCallSummary[] = [];
|
|
246
|
+
const fallbackSummaryReporter = summaryReporter
|
|
247
|
+
? (summary: FormatCallSummary) => {
|
|
248
|
+
fallbackSummaries.push(summary);
|
|
249
|
+
}
|
|
250
|
+
: undefined;
|
|
251
|
+
|
|
194
252
|
for (const runnerId of group.runnerIds) {
|
|
195
253
|
const runner = RUNNERS.get(runnerId);
|
|
196
254
|
if (!runner) {
|
|
197
|
-
ctx.warn(`
|
|
255
|
+
ctx.warn(`unknown runner in format plan: ${runnerId}`);
|
|
198
256
|
continue;
|
|
199
257
|
}
|
|
200
258
|
|
|
201
|
-
const outcome = await runRunner(ctx, runner);
|
|
259
|
+
const outcome = await runRunner(ctx, runner, fallbackSummaryReporter);
|
|
202
260
|
if (outcome === "succeeded") {
|
|
203
|
-
|
|
261
|
+
if (!summaryReporter) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const successSummary = [...fallbackSummaries]
|
|
266
|
+
.reverse()
|
|
267
|
+
.find((summary) => summary.status === "succeeded");
|
|
268
|
+
if (successSummary) {
|
|
269
|
+
summaryReporter(successSummary);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return;
|
|
204
273
|
}
|
|
205
274
|
}
|
|
275
|
+
|
|
276
|
+
if (!summaryReporter) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const lastFailureSummary = [...fallbackSummaries]
|
|
281
|
+
.reverse()
|
|
282
|
+
.find((summary) => summary.status === "failed");
|
|
283
|
+
|
|
284
|
+
if (lastFailureSummary) {
|
|
285
|
+
summaryReporter(lastFailureSummary);
|
|
286
|
+
}
|
|
206
287
|
}
|
|
207
288
|
|
|
208
289
|
export async function formatFile(
|
|
209
290
|
pi: ExtensionAPI,
|
|
210
291
|
cwd: string,
|
|
211
|
-
sourceTool: SourceTool,
|
|
212
292
|
filePath: string,
|
|
213
293
|
timeoutMs: number,
|
|
294
|
+
summaryReporter?: FormatCallSummaryReporter,
|
|
295
|
+
warningReporter?: FormatWarningReporter,
|
|
214
296
|
): Promise<void> {
|
|
215
297
|
const kind = detectFileKind(filePath);
|
|
216
298
|
if (!kind) {
|
|
@@ -226,12 +308,12 @@ export async function formatFile(
|
|
|
226
308
|
pi,
|
|
227
309
|
cwd,
|
|
228
310
|
filePath,
|
|
229
|
-
sourceTool,
|
|
230
311
|
kind,
|
|
231
312
|
timeoutMs,
|
|
313
|
+
warningReporter,
|
|
232
314
|
);
|
|
233
315
|
|
|
234
316
|
for (const group of groups) {
|
|
235
|
-
await runRunnerGroup(runContext, group);
|
|
317
|
+
await runRunnerGroup(runContext, group, summaryReporter);
|
|
236
318
|
}
|
|
237
319
|
}
|
|
@@ -3,8 +3,17 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import type { FileKind } from "./types.js";
|
|
5
5
|
|
|
6
|
+
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
7
|
+
|
|
8
|
+
function normalizeUnicodeSpaces(value: string): string {
|
|
9
|
+
return value.replace(UNICODE_SPACES, " ");
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
export function normalizeToolPath(filePath: string): string {
|
|
7
|
-
const
|
|
13
|
+
const normalizedInput = normalizeUnicodeSpaces(filePath);
|
|
14
|
+
const normalizedAt = normalizedInput.startsWith("@")
|
|
15
|
+
? normalizedInput.slice(1)
|
|
16
|
+
: normalizedInput;
|
|
8
17
|
|
|
9
18
|
if (normalizedAt === "~") {
|
|
10
19
|
return homedir();
|
|
@@ -33,7 +42,10 @@ export async function pathExists(path: string): Promise<boolean> {
|
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
export function isWithinDirectory(
|
|
45
|
+
export function isWithinDirectory(
|
|
46
|
+
pathToCheck: string,
|
|
47
|
+
directory: string,
|
|
48
|
+
): boolean {
|
|
37
49
|
const relPath = relative(directory, pathToCheck);
|
|
38
50
|
return (
|
|
39
51
|
relPath === "" ||
|
|
@@ -57,7 +69,11 @@ export function getPathForGit(filePath: string, cwd: string): string {
|
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
export function detectFileKind(filePath: string): FileKind | undefined {
|
|
60
|
-
if (
|
|
72
|
+
if (
|
|
73
|
+
/(\.(c|h|cc|hh|cpp|hpp|cxx|hxx|ixx|ipp|inl|tpp)|\.(c|h|cc|hh|cpp|hpp|cxx|hxx)\.in)$/i.test(
|
|
74
|
+
filePath,
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
61
77
|
return "cxx";
|
|
62
78
|
}
|
|
63
79
|
|
|
@@ -5,22 +5,22 @@ export const FORMAT_PLAN: Record<FileKind, RunnerGroup[]> = {
|
|
|
5
5
|
cmake: [{ mode: "all", runnerIds: ["cmake-format"] }],
|
|
6
6
|
markdown: [
|
|
7
7
|
{
|
|
8
|
-
mode: "
|
|
9
|
-
runnerIds: ["
|
|
8
|
+
mode: "fallback",
|
|
9
|
+
runnerIds: ["prettier", "markdownlint"],
|
|
10
10
|
},
|
|
11
11
|
],
|
|
12
12
|
json: [
|
|
13
13
|
{
|
|
14
14
|
mode: "fallback",
|
|
15
|
-
runnerIds: ["biome
|
|
15
|
+
runnerIds: ["biome", "prettier"],
|
|
16
16
|
},
|
|
17
17
|
],
|
|
18
18
|
shell: [{ mode: "all", runnerIds: ["shfmt"] }],
|
|
19
|
-
python: [{ mode: "all", runnerIds: ["ruff-format", "ruff-check
|
|
19
|
+
python: [{ mode: "all", runnerIds: ["ruff-format", "ruff-check"] }],
|
|
20
20
|
jsts: [
|
|
21
21
|
{
|
|
22
22
|
mode: "fallback",
|
|
23
|
-
runnerIds: ["biome
|
|
23
|
+
runnerIds: ["biome", "eslint", "prettier"],
|
|
24
24
|
},
|
|
25
25
|
],
|
|
26
26
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { defineRunner, direct } from "./helpers.js";
|
|
2
1
|
import { BIOME_CONFIG_PATTERNS } from "./config-patterns.js";
|
|
2
|
+
import { defineRunner, direct } from "./helpers.js";
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
id: "biome
|
|
4
|
+
const biomeRunner = defineRunner({
|
|
5
|
+
id: "biome",
|
|
6
6
|
launcher: direct("biome"),
|
|
7
7
|
when: (ctx) => ctx.hasConfig(BIOME_CONFIG_PATTERNS),
|
|
8
8
|
args: ["check", "--write"],
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
export default
|
|
11
|
+
export default biomeRunner;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ESLINT_CONFIG_PATTERNS } from "./config-patterns.js";
|
|
2
2
|
import { defineRunner, direct } from "./helpers.js";
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
id: "eslint
|
|
4
|
+
const eslintRunner = defineRunner({
|
|
5
|
+
id: "eslint",
|
|
6
6
|
launcher: direct("eslint"),
|
|
7
7
|
when: (ctx) => ctx.hasConfig(ESLINT_CONFIG_PATTERNS),
|
|
8
8
|
args: ["--fix"],
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
export default
|
|
11
|
+
export default eslintRunner;
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import type { RunnerDefinition } from "../types.js";
|
|
2
|
-
import
|
|
2
|
+
import biomeRunner from "./biome.js";
|
|
3
3
|
import clangFormatRunner from "./clang-format.js";
|
|
4
4
|
import cmakeFormatRunner from "./cmake-format.js";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import ruffCheckFixRunner from "./ruff-check-fix.js";
|
|
5
|
+
import eslintRunner from "./eslint.js";
|
|
6
|
+
import markdownlintRunner from "./markdownlint.js";
|
|
7
|
+
import prettierRunner from "./prettier.js";
|
|
8
|
+
import ruffCheckRunner from "./ruff-check.js";
|
|
10
9
|
import ruffFormatRunner from "./ruff-format.js";
|
|
11
10
|
import shfmtRunner from "./shfmt.js";
|
|
12
11
|
|
|
13
12
|
export const RUNNER_DEFINITIONS: RunnerDefinition[] = [
|
|
14
13
|
clangFormatRunner,
|
|
15
14
|
cmakeFormatRunner,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
prettierConfigWriteRunner,
|
|
15
|
+
markdownlintRunner,
|
|
16
|
+
biomeRunner,
|
|
17
|
+
eslintRunner,
|
|
18
|
+
prettierRunner,
|
|
21
19
|
shfmtRunner,
|
|
22
20
|
ruffFormatRunner,
|
|
23
|
-
|
|
21
|
+
ruffCheckRunner,
|
|
24
22
|
];
|
|
25
23
|
|
|
26
24
|
function buildRunnerRegistry(
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { defineRunner, direct } from "./helpers.js";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
id: "markdownlint
|
|
3
|
+
const markdownlintRunner = defineRunner({
|
|
4
|
+
id: "markdownlint",
|
|
5
5
|
launcher: direct("markdownlint"),
|
|
6
6
|
args: ["--fix"],
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
export default
|
|
9
|
+
export default markdownlintRunner;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { defineRunner, direct } from "./helpers.js";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
id: "prettier
|
|
3
|
+
const prettierRunner = defineRunner({
|
|
4
|
+
id: "prettier",
|
|
5
5
|
launcher: direct("prettier"),
|
|
6
6
|
args: ["--write"],
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
export default
|
|
9
|
+
export default prettierRunner;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { defineRunner, pypi } from "./helpers.js";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
id: "ruff-check
|
|
3
|
+
const ruffCheckRunner = defineRunner({
|
|
4
|
+
id: "ruff-check",
|
|
5
5
|
launcher: pypi("ruff"),
|
|
6
6
|
args: ["check", "--fix"],
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
export default
|
|
9
|
+
export default ruffCheckRunner;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { ExecResult } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
export type SourceTool = "write" | "edit";
|
|
4
|
-
|
|
5
3
|
export type FileKind =
|
|
6
4
|
| "cxx"
|
|
7
5
|
| "cmake"
|
|
@@ -40,7 +38,6 @@ export interface ResolvedLauncher {
|
|
|
40
38
|
export interface RunnerContext {
|
|
41
39
|
readonly filePath: string;
|
|
42
40
|
readonly cwd: string;
|
|
43
|
-
readonly sourceTool: SourceTool;
|
|
44
41
|
readonly kind: FileKind;
|
|
45
42
|
|
|
46
43
|
hasCommand(command: string): Promise<boolean>;
|
|
@@ -48,7 +45,7 @@ export interface RunnerContext {
|
|
|
48
45
|
findConfigFile(patterns: readonly string[]): Promise<string | undefined>;
|
|
49
46
|
hasEditorConfigInCwd(): Promise<boolean>;
|
|
50
47
|
|
|
51
|
-
exec(command: string, args: string[]): Promise<ExecResult
|
|
48
|
+
exec(command: string, args: string[]): Promise<ExecResult>;
|
|
52
49
|
getChangedLines(): Promise<string[]>;
|
|
53
50
|
getRequiredMajorVersionFromConfig(
|
|
54
51
|
patterns: readonly string[],
|
|
@@ -94,8 +91,9 @@ export interface StaticRunnerDefinition extends RunnerBase {
|
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
export interface DynamicRunnerDefinition extends RunnerBase {
|
|
97
|
-
buildArgs:
|
|
98
|
-
|
|
94
|
+
buildArgs: (
|
|
95
|
+
ctx: RunnerContext,
|
|
96
|
+
) => Promise<string[] | undefined> | string[] | undefined;
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
export type RunnerDefinition = StaticRunnerDefinition | DynamicRunnerDefinition;
|
package/extensions/index.ts
CHANGED
|
@@ -1,13 +1,47 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
isEditToolResult,
|
|
5
|
+
isWriteToolResult,
|
|
6
|
+
} from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
commandTimeoutMs,
|
|
9
|
+
hideCallSummariesInTui,
|
|
10
|
+
} from "./formatter/config.js";
|
|
11
|
+
import { type FormatCallSummary, formatFile } from "./formatter/dispatch.js";
|
|
12
|
+
import {
|
|
13
|
+
getPathForGit,
|
|
14
|
+
pathExists,
|
|
15
|
+
resolveToolPath,
|
|
16
|
+
} from "./formatter/path.js";
|
|
6
17
|
|
|
7
18
|
function formatError(error: unknown): string {
|
|
8
19
|
return error instanceof Error ? error.message : String(error);
|
|
9
20
|
}
|
|
10
21
|
|
|
22
|
+
function formatSummaryPath(filePath: string, cwd: string): string {
|
|
23
|
+
const pathForDisplay = getPathForGit(filePath, cwd);
|
|
24
|
+
return pathForDisplay.startsWith("/")
|
|
25
|
+
? basename(pathForDisplay)
|
|
26
|
+
: pathForDisplay;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatCallSuccessSummary(summary: FormatCallSummary): string {
|
|
30
|
+
return `✔︎ ${summary.runnerId}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatCallFailureSummary(summary: FormatCallSummary): string {
|
|
34
|
+
if (summary.failureMessage) {
|
|
35
|
+
return `✘ ${summary.runnerId}: ${summary.failureMessage}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (summary.exitCode !== undefined) {
|
|
39
|
+
return `✘ ${summary.runnerId} (exit ${summary.exitCode})`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return `✘ ${summary.runnerId}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
11
45
|
export default function (pi: ExtensionAPI) {
|
|
12
46
|
const formatQueueByPath = new Map<string, Promise<void>>();
|
|
13
47
|
|
|
@@ -36,29 +70,73 @@ export default function (pi: ExtensionAPI) {
|
|
|
36
70
|
return;
|
|
37
71
|
}
|
|
38
72
|
|
|
39
|
-
if (event
|
|
73
|
+
if (!isWriteToolResult(event) && !isEditToolResult(event)) {
|
|
40
74
|
return;
|
|
41
75
|
}
|
|
42
76
|
|
|
43
|
-
const rawPath =
|
|
44
|
-
if (
|
|
77
|
+
const rawPath = event.input.path;
|
|
78
|
+
if (rawPath.length === 0) {
|
|
45
79
|
return;
|
|
46
80
|
}
|
|
47
81
|
|
|
48
|
-
const sourceTool = event.toolName as SourceTool;
|
|
49
82
|
const filePath = resolveToolPath(rawPath, ctx.cwd);
|
|
50
83
|
|
|
51
84
|
if (!(await pathExists(filePath))) {
|
|
52
85
|
return;
|
|
53
86
|
}
|
|
54
87
|
|
|
88
|
+
const showSummaries = !hideCallSummariesInTui && ctx.hasUI;
|
|
89
|
+
const notifyWarning = (message: string) => {
|
|
90
|
+
const normalizedMessage = message.replace(/\s+/g, " ").trim();
|
|
91
|
+
|
|
92
|
+
if (ctx.hasUI) {
|
|
93
|
+
ctx.ui.notify(normalizedMessage, "warning");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.warn(normalizedMessage);
|
|
98
|
+
};
|
|
99
|
+
|
|
55
100
|
await enqueueFormat(filePath, async () => {
|
|
101
|
+
const summaries: FormatCallSummary[] = [];
|
|
102
|
+
const summaryReporter = showSummaries
|
|
103
|
+
? (summary: FormatCallSummary) => {
|
|
104
|
+
summaries.push(summary);
|
|
105
|
+
}
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
const runnerWarningReporter =
|
|
109
|
+
showSummaries && ctx.hasUI
|
|
110
|
+
? () => {
|
|
111
|
+
// Summary mode already reports failures compactly.
|
|
112
|
+
}
|
|
113
|
+
: notifyWarning;
|
|
114
|
+
|
|
56
115
|
try {
|
|
57
|
-
await formatFile(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
116
|
+
await formatFile(
|
|
117
|
+
pi,
|
|
118
|
+
ctx.cwd,
|
|
119
|
+
filePath,
|
|
120
|
+
commandTimeoutMs,
|
|
121
|
+
summaryReporter,
|
|
122
|
+
runnerWarningReporter,
|
|
61
123
|
);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
const fileLabel = formatSummaryPath(filePath, ctx.cwd);
|
|
126
|
+
notifyWarning(`Failed to format ${fileLabel}: ${formatError(error)}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!showSummaries || summaries.length === 0) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const summary of summaries) {
|
|
134
|
+
if (summary.status === "succeeded") {
|
|
135
|
+
ctx.ui.notify(formatCallSuccessSummary(summary), "info");
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ctx.ui.notify(formatCallFailureSummary(summary), "info");
|
|
62
140
|
}
|
|
63
141
|
});
|
|
64
142
|
});
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-formatter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pi extension that auto-formats files after write/edit tool calls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"extensions",
|
|
8
|
-
"DOCUMENTATION.md",
|
|
9
8
|
"README.md",
|
|
10
9
|
"LICENSE"
|
|
11
10
|
],
|
package/DOCUMENTATION.md
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# Runner API
|
|
2
|
-
|
|
3
|
-
A *runner* wraps a single formatting tool (for example, `prettier` or `ruff`).
|
|
4
|
-
This document describes how runners work and how to add new ones.
|
|
5
|
-
|
|
6
|
-
## File layout
|
|
7
|
-
|
|
8
|
-
| File | Purpose |
|
|
9
|
-
| ---- | ------- |
|
|
10
|
-
| `extensions/format/runners/*.ts` | One file per runner |
|
|
11
|
-
| `extensions/format/runners/helpers.ts` | Helper constructors (`direct`, `pypi`, `goTool`, `defineRunner`) |
|
|
12
|
-
| `extensions/format/runners/index.ts` | Runner registry |
|
|
13
|
-
| `extensions/format/plan.ts` | File-kind → runner-group mapping |
|
|
14
|
-
| `extensions/format/types.ts` | Shared types |
|
|
15
|
-
|
|
16
|
-
## Add a new runner
|
|
17
|
-
|
|
18
|
-
1. Create a file under `extensions/format/runners/`.
|
|
19
|
-
2. Define the runner with the helper constructors from `helpers.ts`.
|
|
20
|
-
3. Register it in `extensions/format/runners/index.ts`.
|
|
21
|
-
4. Add its `id` to one or more groups in `extensions/format/plan.ts`.
|
|
22
|
-
|
|
23
|
-
## Runner definition
|
|
24
|
-
|
|
25
|
-
Every runner needs two things:
|
|
26
|
-
|
|
27
|
-
- `id`: a unique identifier
|
|
28
|
-
- `launcher`: how to locate and execute the tool
|
|
29
|
-
|
|
30
|
-
### Launchers
|
|
31
|
-
|
|
32
|
-
Use the helper constructors to create a launcher:
|
|
33
|
-
|
|
34
|
-
| Constructor | Behavior |
|
|
35
|
-
| ----------- | -------- |
|
|
36
|
-
| `direct("prettier")` | Run the command directly |
|
|
37
|
-
| `pypi("ruff")` | Run the tool natively, fall back to `uv tool run` |
|
|
38
|
-
| `goTool("shfmt", "mvdan.cc/sh/v3/cmd/shfmt@v3.10.0")` | Run the tool natively, fall back to `go run` |
|
|
39
|
-
|
|
40
|
-
All commands execute in the project working directory and respect the
|
|
41
|
-
configured timeout.
|
|
42
|
-
|
|
43
|
-
### Arguments
|
|
44
|
-
|
|
45
|
-
There are two ways to supply arguments:
|
|
46
|
-
|
|
47
|
-
- **Static** (preferred): Set `args: string[]`. The file path is appended
|
|
48
|
-
automatically unless you set `appendFile: false`.
|
|
49
|
-
- **Dynamic**: Implement `buildArgs(ctx)` and return the argument array, or
|
|
50
|
-
`undefined` to skip the runner.
|
|
51
|
-
|
|
52
|
-
### Optional predicates and requirements
|
|
53
|
-
|
|
54
|
-
- `when(ctx)`: Return `false` to skip the runner based on an extra condition.
|
|
55
|
-
- `requires.majorVersionFromConfig`: Gate execution on a major version match:
|
|
56
|
-
- `patterns`: Config-file globs to read the required major version from.
|
|
57
|
-
- `command` (optional): Command to inspect with `--version`.
|
|
58
|
-
- `onInvalid` / `onMismatch`: `"warn-skip"` (default) or `"skip"`.
|
|
59
|
-
|
|
60
|
-
### Runner context
|
|
61
|
-
|
|
62
|
-
The `ctx` object passed to `when` and `buildArgs` exposes:
|
|
63
|
-
|
|
64
|
-
| Member | Description |
|
|
65
|
-
| ------ | ----------- |
|
|
66
|
-
| `filePath`, `cwd`, `sourceTool`, `kind` | Current invocation metadata |
|
|
67
|
-
| `hasCommand(cmd)` | Check whether a command is available |
|
|
68
|
-
| `hasConfig(patterns)`, `findConfigFile(patterns)` | Look up configuration files |
|
|
69
|
-
| `hasEditorConfigInCwd()` | Check for an `.editorconfig` in the working directory |
|
|
70
|
-
| `getChangedLines()` | Get the line ranges that changed |
|
|
71
|
-
| `warn(msg)` | Emit a warning without failing the run |
|
|
72
|
-
|
|
73
|
-
## Format plan
|
|
74
|
-
|
|
75
|
-
`extensions/format/plan.ts` maps each file kind to an ordered list of runner
|
|
76
|
-
groups. Each group has a mode:
|
|
77
|
-
|
|
78
|
-
- **`"all"`**: Run every qualifying runner in order.
|
|
79
|
-
- **`"fallback"`**: Run the first qualifying runner and stop.
|
|
80
|
-
|
|
81
|
-
## Examples
|
|
82
|
-
|
|
83
|
-
### Static runner
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
import { defineRunner, direct } from "./helpers.js";
|
|
87
|
-
|
|
88
|
-
const myRunner = defineRunner({
|
|
89
|
-
id: "my-formatter",
|
|
90
|
-
launcher: direct("my-formatter"),
|
|
91
|
-
args: ["--write"],
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
export default myRunner;
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Dynamic runner with a version requirement
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
import { defineRunner, direct } from "./helpers.js";
|
|
101
|
-
|
|
102
|
-
const clangFormatRunner = defineRunner({
|
|
103
|
-
id: "clang-format",
|
|
104
|
-
launcher: direct("clang-format"),
|
|
105
|
-
requires: {
|
|
106
|
-
majorVersionFromConfig: {
|
|
107
|
-
patterns: [".clang-format-version"],
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
async buildArgs(ctx) {
|
|
111
|
-
const lines = await ctx.getChangedLines();
|
|
112
|
-
if (lines.length > 0) {
|
|
113
|
-
return [...lines.map((line) => `--lines=${line}`), "-i", ctx.filePath];
|
|
114
|
-
}
|
|
115
|
-
return ["-i", ctx.filePath];
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
export default clangFormatRunner;
|
|
120
|
-
```
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
const DEFAULT_COMMAND_TIMEOUT_MS = 10_000;
|
|
2
|
-
|
|
3
|
-
const configuredTimeoutMs = Number.parseInt(
|
|
4
|
-
process.env.PI_FORMAT_COMMAND_TIMEOUT_MS ?? "",
|
|
5
|
-
10,
|
|
6
|
-
);
|
|
7
|
-
|
|
8
|
-
export const commandTimeoutMs =
|
|
9
|
-
Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs > 0
|
|
10
|
-
? configuredTimeoutMs
|
|
11
|
-
: DEFAULT_COMMAND_TIMEOUT_MS;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { PRETTIER_CONFIG_PATTERNS } from "./config-patterns.js";
|
|
2
|
-
import { defineRunner, direct } from "./helpers.js";
|
|
3
|
-
|
|
4
|
-
const prettierConfigWriteRunner = defineRunner({
|
|
5
|
-
id: "prettier-config-write",
|
|
6
|
-
launcher: direct("prettier"),
|
|
7
|
-
when: (ctx) => ctx.hasConfig(PRETTIER_CONFIG_PATTERNS),
|
|
8
|
-
args: ["--write"],
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
export default prettierConfigWriteRunner;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|