@gotgenes/pi-autoformat 0.1.0 → 4.0.3
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/.github/workflows/ci.yml +1 -3
- package/.github/workflows/release-please.yml +29 -0
- package/.markdownlint-cli2.yaml +14 -2
- package/.pi/extensions/pi-autoformat/config.json +3 -6
- package/.pi/prompts/README.md +59 -0
- package/.pi/prompts/plan-issue.md +64 -0
- package/.pi/prompts/retro.md +144 -0
- package/.pi/prompts/ship-issue.md +77 -0
- package/.pi/prompts/tdd-plan.md +67 -0
- package/.pi/skills/pi-extension-lifecycle/SKILL.md +256 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +39 -0
- package/CHANGELOG.md +365 -0
- package/README.md +42 -109
- package/biome.json +1 -1
- package/docs/assets/logo.png +0 -0
- package/docs/assets/logo.svg +533 -0
- package/docs/configuration.md +358 -38
- package/docs/plans/0001-initial-implementation-plan.md +17 -9
- package/docs/plans/0002-richer-tui-formatter-summaries.md +220 -0
- package/docs/plans/0003-additional-pi-mutation-tools.md +273 -0
- package/docs/plans/0004-shell-driven-mutation-coverage.md +296 -0
- package/docs/plans/0010-acceptance-test-coverage.md +240 -0
- package/docs/plans/0012-remove-unused-formatter-extensions-field.md +152 -0
- package/docs/plans/0013-fallback-chain-step-type.md +280 -0
- package/docs/plans/0014-batch-by-default-formatter-dispatch.md +195 -0
- package/docs/plans/0015-builtin-treefmt-and-treefmt-nix-support.md +290 -0
- package/docs/plans/0016-detailed-formatter-output-on-failure.md +245 -0
- package/docs/plans/0022-pi-coding-agent-types.md +201 -0
- package/docs/plans/0027-format-before-agent-exit-follow-up-turn.md +355 -0
- package/docs/plans/0031-turn-end-flush-with-change-detection.md +365 -0
- package/docs/retro/0002-richer-tui-formatter-summaries.md +47 -0
- package/docs/retro/0013-fallback-chain-step-type.md +67 -0
- package/docs/retro/0015-builtin-treefmt-and-treefmt-nix-support.md +56 -0
- package/docs/retro/0016-detailed-formatter-output-on-failure.md +60 -0
- package/docs/retro/0022-pi-coding-agent-types.md +62 -0
- package/docs/testing.md +95 -0
- package/package.json +30 -11
- package/prek.toml +2 -2
- package/schemas/pi-autoformat.schema.json +145 -21
- package/src/builtin-formatters.ts +205 -0
- package/src/command-probe.ts +66 -0
- package/src/config-loader.ts +829 -90
- package/src/custom-mutation-tools.ts +125 -0
- package/src/extension.ts +469 -82
- package/src/format-scope.ts +118 -0
- package/src/formatter-config.ts +73 -36
- package/src/formatter-executor.ts +230 -34
- package/src/formatter-output-report.ts +149 -0
- package/src/formatter-registry.ts +139 -30
- package/src/index.ts +26 -5
- package/src/prompt-autoformatter.ts +148 -23
- package/src/shell-mutation-detector.ts +572 -0
- package/src/touched-files-queue.ts +72 -11
- package/test/acceptance-event-bus.test.ts +138 -0
- package/test/acceptance.test.ts +69 -0
- package/test/builtin-formatters.test.ts +382 -0
- package/test/command-probe.test.ts +79 -0
- package/test/config-loader.test.ts +640 -21
- package/test/custom-mutation-tools.test.ts +190 -0
- package/test/extension.test.ts +1535 -158
- package/test/fallback-acceptance.test.ts +98 -0
- package/test/fixtures/event-bus-emitter.ts +26 -0
- package/test/fixtures/formatter-recorder.mjs +25 -0
- package/test/format-scope.test.ts +139 -0
- package/test/formatter-config.test.ts +56 -5
- package/test/formatter-executor.test.ts +555 -35
- package/test/formatter-output-report.test.ts +178 -0
- package/test/formatter-registry.test.ts +330 -37
- package/test/helpers/rpc.ts +146 -0
- package/test/prompt-autoformatter.test.ts +315 -22
- package/test/schema.test.ts +149 -0
- package/test/shell-mutation-detector.test.ts +221 -0
- package/test/touched-files-queue.test.ts +40 -1
- package/test/types/theme-stub.test-d.ts +42 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
matchWrapper,
|
|
5
|
+
parseKnownCommand,
|
|
6
|
+
SnapshotTracker,
|
|
7
|
+
} from "../src/shell-mutation-detector.js";
|
|
8
|
+
|
|
9
|
+
describe("parseKnownCommand", () => {
|
|
10
|
+
describe("sed -i", () => {
|
|
11
|
+
it("extracts a single target", () => {
|
|
12
|
+
expect(parseKnownCommand("sed -i 's/a/b/' foo.txt")).toEqual(["foo.txt"]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("extracts multiple targets", () => {
|
|
16
|
+
expect(parseKnownCommand("sed -i 's/a/b/' foo.txt bar.txt")).toEqual([
|
|
17
|
+
"foo.txt",
|
|
18
|
+
"bar.txt",
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("handles -i.bak suffix without reporting the backup", () => {
|
|
23
|
+
expect(parseKnownCommand("sed -i.bak 's/a/b/' foo.txt")).toEqual([
|
|
24
|
+
"foo.txt",
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("handles BSD sed -i '' form", () => {
|
|
29
|
+
expect(parseKnownCommand("sed -i '' 's/a/b/' foo.txt")).toEqual([
|
|
30
|
+
"foo.txt",
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns empty when -i is absent", () => {
|
|
35
|
+
expect(parseKnownCommand("sed 's/a/b/' foo.txt")).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("bails on unknown sed flags", () => {
|
|
39
|
+
expect(parseKnownCommand("sed -i -X 's/a/b/' foo.txt")).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("mv / cp", () => {
|
|
44
|
+
it("returns the destination of mv", () => {
|
|
45
|
+
expect(parseKnownCommand("mv a.txt b.txt")).toEqual(["b.txt"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns the destination of cp", () => {
|
|
49
|
+
expect(parseKnownCommand("cp a.txt b.txt")).toEqual(["b.txt"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("bails on multi-source mv", () => {
|
|
53
|
+
expect(parseKnownCommand("mv a.txt b.txt c/")).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("bails on unknown flags", () => {
|
|
57
|
+
expect(
|
|
58
|
+
parseKnownCommand("mv --strip-trailing-slash a.txt b.txt"),
|
|
59
|
+
).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("touch", () => {
|
|
64
|
+
it("returns each file argument", () => {
|
|
65
|
+
expect(parseKnownCommand("touch a.txt b.txt")).toEqual([
|
|
66
|
+
"a.txt",
|
|
67
|
+
"b.txt",
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("accepts allowlisted flags", () => {
|
|
72
|
+
expect(parseKnownCommand("touch -a -m foo.txt")).toEqual(["foo.txt"]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("bails on -r / -t / -d", () => {
|
|
76
|
+
expect(parseKnownCommand("touch -r ref.txt foo.txt")).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("redirections", () => {
|
|
81
|
+
it("captures > target alongside echo", () => {
|
|
82
|
+
expect(parseKnownCommand("echo hi > out.txt")).toEqual(["out.txt"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("captures >> append redirection", () => {
|
|
86
|
+
expect(parseKnownCommand("printf 'x' >> out.txt")).toEqual(["out.txt"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("ignores redirects on unknown commands", () => {
|
|
90
|
+
expect(parseKnownCommand("foo --bar > out.txt")).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("tee", () => {
|
|
95
|
+
it("captures tee targets", () => {
|
|
96
|
+
expect(parseKnownCommand("tee a.txt b.txt")).toEqual(["a.txt", "b.txt"]);
|
|
97
|
+
});
|
|
98
|
+
it("captures tee -a", () => {
|
|
99
|
+
expect(parseKnownCommand("tee -a a.txt")).toEqual(["a.txt"]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("bails on complex constructs", () => {
|
|
104
|
+
const cases = [
|
|
105
|
+
"sed -i 's/a/b/' foo.txt | tee log.txt",
|
|
106
|
+
"echo hi && touch foo.txt",
|
|
107
|
+
"echo $(cat names.txt) > out.txt",
|
|
108
|
+
"echo `pwd` > out.txt",
|
|
109
|
+
"(touch foo.txt)",
|
|
110
|
+
"cat <input.txt > out.txt",
|
|
111
|
+
"echo hi; touch foo.txt",
|
|
112
|
+
];
|
|
113
|
+
for (const command of cases) {
|
|
114
|
+
it(`returns [] for ${JSON.stringify(command)}`, () => {
|
|
115
|
+
expect(parseKnownCommand(command)).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("returns [] on completely unknown commands", () => {
|
|
121
|
+
expect(parseKnownCommand("rsync -a src/ dst/")).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("handles quoted paths with spaces", () => {
|
|
125
|
+
expect(parseKnownCommand("touch 'my file.txt'")).toEqual(["my file.txt"]);
|
|
126
|
+
expect(parseKnownCommand('touch "my file.txt"')).toEqual(["my file.txt"]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("matchWrapper", () => {
|
|
131
|
+
it("returns paths from stdout when prefix matches", () => {
|
|
132
|
+
expect(
|
|
133
|
+
matchWrapper(
|
|
134
|
+
"pnpm codegen --foo",
|
|
135
|
+
"src/generated/a.ts\nsrc/generated/b.ts\n",
|
|
136
|
+
[{ prefix: "pnpm codegen" }],
|
|
137
|
+
),
|
|
138
|
+
).toEqual(["src/generated/a.ts", "src/generated/b.ts"]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("returns [] when no wrapper matches", () => {
|
|
142
|
+
expect(
|
|
143
|
+
matchWrapper("pnpm test", "ignored", [{ prefix: "pnpm codegen" }]),
|
|
144
|
+
).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("matches exact prefix without arguments", () => {
|
|
148
|
+
expect(
|
|
149
|
+
matchWrapper("make gen", "out.ts\n", [{ prefix: "make gen" }]),
|
|
150
|
+
).toEqual(["out.ts"]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("trims surrounding whitespace and skips blank lines", () => {
|
|
154
|
+
expect(
|
|
155
|
+
matchWrapper("pnpm codegen", " a.ts \n\n b.ts\n", [
|
|
156
|
+
{ prefix: "pnpm codegen" },
|
|
157
|
+
]),
|
|
158
|
+
).toEqual(["a.ts", "b.ts"]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("SnapshotTracker", () => {
|
|
163
|
+
it("reports files whose mtime advanced", () => {
|
|
164
|
+
const mtimes = new Map<string, number>([
|
|
165
|
+
["/repo/a.ts", 100],
|
|
166
|
+
["/repo/b.ts", 200],
|
|
167
|
+
]);
|
|
168
|
+
const tracker = new SnapshotTracker({
|
|
169
|
+
cwd: "/repo",
|
|
170
|
+
globs: ["**/*.ts"],
|
|
171
|
+
resolveGlobs: () => ["/repo/a.ts", "/repo/b.ts"],
|
|
172
|
+
stat: (p) => {
|
|
173
|
+
const m = mtimes.get(p);
|
|
174
|
+
return m === undefined ? undefined : { mtimeMs: m };
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
tracker.before();
|
|
178
|
+
mtimes.set("/repo/a.ts", 150);
|
|
179
|
+
expect(tracker.after()).toEqual(["/repo/a.ts"]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("ignores files whose mtime did not change", () => {
|
|
183
|
+
const tracker = new SnapshotTracker({
|
|
184
|
+
cwd: "/repo",
|
|
185
|
+
globs: ["**/*.ts"],
|
|
186
|
+
resolveGlobs: () => ["/repo/a.ts"],
|
|
187
|
+
stat: () => ({ mtimeMs: 100 }),
|
|
188
|
+
});
|
|
189
|
+
tracker.before();
|
|
190
|
+
expect(tracker.after()).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("reports newly created files", () => {
|
|
194
|
+
let exists = false;
|
|
195
|
+
const tracker = new SnapshotTracker({
|
|
196
|
+
cwd: "/repo",
|
|
197
|
+
globs: ["**/*.ts"],
|
|
198
|
+
resolveGlobs: () => ["/repo/new.ts"],
|
|
199
|
+
stat: () => (exists ? { mtimeMs: 500 } : undefined),
|
|
200
|
+
});
|
|
201
|
+
tracker.before();
|
|
202
|
+
exists = true;
|
|
203
|
+
expect(tracker.after()).toEqual(["/repo/new.ts"]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("warns when the cap is exceeded", () => {
|
|
207
|
+
const onWarn = vi.fn();
|
|
208
|
+
const files = Array.from({ length: 10 }, (_, i) => `/repo/${i}.ts`);
|
|
209
|
+
const tracker = new SnapshotTracker({
|
|
210
|
+
cwd: "/repo",
|
|
211
|
+
globs: ["**/*.ts"],
|
|
212
|
+
resolveGlobs: () => files,
|
|
213
|
+
stat: () => ({ mtimeMs: 100 }),
|
|
214
|
+
maxEntries: 3,
|
|
215
|
+
onWarn,
|
|
216
|
+
});
|
|
217
|
+
tracker.before();
|
|
218
|
+
expect(onWarn).toHaveBeenCalledTimes(1);
|
|
219
|
+
expect(onWarn.mock.calls[0]?.[0]).toMatch(/truncated/);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { FormatScope } from "../src/format-scope.js";
|
|
4
|
+
import {
|
|
5
|
+
type MutationSourceHandler,
|
|
6
|
+
TouchedFilesQueue,
|
|
7
|
+
writeOrEditHandler,
|
|
8
|
+
} from "../src/touched-files-queue.js";
|
|
4
9
|
|
|
5
10
|
describe("TouchedFilesQueue", () => {
|
|
6
11
|
it("collects paths from write and edit tool results", () => {
|
|
@@ -43,4 +48,38 @@ describe("TouchedFilesQueue", () => {
|
|
|
43
48
|
expect(queue.flush()).toEqual(["/repo/src/index.ts"]);
|
|
44
49
|
expect(queue.flush()).toEqual([]);
|
|
45
50
|
});
|
|
51
|
+
|
|
52
|
+
it("filters paths outside the configured format scope", () => {
|
|
53
|
+
const scope: FormatScope = {
|
|
54
|
+
roots: ["/repo"],
|
|
55
|
+
caseInsensitive: false,
|
|
56
|
+
};
|
|
57
|
+
const queue = new TouchedFilesQueue({ cwd: "/repo", scope });
|
|
58
|
+
|
|
59
|
+
queue.recordToolResult("write", { path: "src/index.ts" });
|
|
60
|
+
queue.recordToolResult("write", { path: "/tmp/scratch.ts" });
|
|
61
|
+
queue.recordToolResult("write", { path: "../sibling/file.ts" });
|
|
62
|
+
|
|
63
|
+
expect(queue.flush()).toEqual(["/repo/src/index.ts"]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("runs custom mutation source handlers and dedupes across sources", () => {
|
|
67
|
+
const bashHandler: MutationSourceHandler = (toolName) =>
|
|
68
|
+
toolName === "bash" ? ["src/index.ts", "src/other.ts"] : [];
|
|
69
|
+
const queue = new TouchedFilesQueue({
|
|
70
|
+
cwd: "/repo",
|
|
71
|
+
handlers: [writeOrEditHandler, bashHandler],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
queue.recordToolResult("write", { path: "src/index.ts" });
|
|
75
|
+
queue.recordToolResult("bash", { command: "sed -i ..." });
|
|
76
|
+
|
|
77
|
+
expect(queue.flush()).toEqual(["/repo/src/index.ts", "/repo/src/other.ts"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("accepts externally added paths via addPath", () => {
|
|
81
|
+
const queue = new TouchedFilesQueue({ cwd: "/repo" });
|
|
82
|
+
queue.addPath("src/snapshot.ts");
|
|
83
|
+
expect(queue.flush()).toEqual(["/repo/src/snapshot.ts"]);
|
|
84
|
+
});
|
|
46
85
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-only test: Pi's real `Theme` class must reject the duck-typed
|
|
3
|
+
* `{ fg: (name, text) => text }` stub shape that hid the
|
|
4
|
+
* `themed()` `this`-binding regression (commits 6a6ec16 / 6ba7576,
|
|
5
|
+
* retro 0016).
|
|
6
|
+
*
|
|
7
|
+
* After issue #22 lands, `src/extension.ts` consumes the real `Theme`
|
|
8
|
+
* type from `@mariozechner/pi-coding-agent`, so any test stub built as
|
|
9
|
+
* a plain object literal will be rejected at compile time.
|
|
10
|
+
*
|
|
11
|
+
* This file is intentionally not run as a vitest test — it exists for
|
|
12
|
+
* `tsc --noEmit` to assert the type relationship.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
16
|
+
import { describe, it } from "vitest";
|
|
17
|
+
|
|
18
|
+
describe("Theme stub-shape expectations", () => {
|
|
19
|
+
it("rejects plain-arrow-function `fg` stubs as assignable to Theme", () => {
|
|
20
|
+
// @ts-expect-error - A plain object with only `fg` is missing the rest of
|
|
21
|
+
// the Theme class surface (fgColors, bgColors, instance methods, etc.) and
|
|
22
|
+
// must not be assignable to the real Theme type.
|
|
23
|
+
const _badStub: Theme = {
|
|
24
|
+
fg: (_name: string, text: string) => text,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Reference the binding to keep TS from flagging it as unused-only;
|
|
28
|
+
// the real assertion is the @ts-expect-error above.
|
|
29
|
+
void _badStub;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("accepts a real Theme instance shape via class-based stubs", () => {
|
|
33
|
+
// A class-based stub that structurally mirrors Theme's shape
|
|
34
|
+
// (including private-ish fgColors map referenced by `this`) is the
|
|
35
|
+
// pattern tests must use. We don't construct Theme directly here —
|
|
36
|
+
// its constructor pulls in TUI dependencies — but we record the
|
|
37
|
+
// requirement.
|
|
38
|
+
type RequiredKeys = "fg";
|
|
39
|
+
const _proof: RequiredKeys = "fg" satisfies keyof Theme;
|
|
40
|
+
void _proof;
|
|
41
|
+
});
|
|
42
|
+
});
|