@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.
Files changed (75) hide show
  1. package/.github/workflows/ci.yml +1 -3
  2. package/.github/workflows/release-please.yml +29 -0
  3. package/.markdownlint-cli2.yaml +14 -2
  4. package/.pi/extensions/pi-autoformat/config.json +3 -6
  5. package/.pi/prompts/README.md +59 -0
  6. package/.pi/prompts/plan-issue.md +64 -0
  7. package/.pi/prompts/retro.md +144 -0
  8. package/.pi/prompts/ship-issue.md +77 -0
  9. package/.pi/prompts/tdd-plan.md +67 -0
  10. package/.pi/skills/pi-extension-lifecycle/SKILL.md +256 -0
  11. package/.release-please-manifest.json +1 -1
  12. package/AGENTS.md +39 -0
  13. package/CHANGELOG.md +365 -0
  14. package/README.md +42 -109
  15. package/biome.json +1 -1
  16. package/docs/assets/logo.png +0 -0
  17. package/docs/assets/logo.svg +533 -0
  18. package/docs/configuration.md +358 -38
  19. package/docs/plans/0001-initial-implementation-plan.md +17 -9
  20. package/docs/plans/0002-richer-tui-formatter-summaries.md +220 -0
  21. package/docs/plans/0003-additional-pi-mutation-tools.md +273 -0
  22. package/docs/plans/0004-shell-driven-mutation-coverage.md +296 -0
  23. package/docs/plans/0010-acceptance-test-coverage.md +240 -0
  24. package/docs/plans/0012-remove-unused-formatter-extensions-field.md +152 -0
  25. package/docs/plans/0013-fallback-chain-step-type.md +280 -0
  26. package/docs/plans/0014-batch-by-default-formatter-dispatch.md +195 -0
  27. package/docs/plans/0015-builtin-treefmt-and-treefmt-nix-support.md +290 -0
  28. package/docs/plans/0016-detailed-formatter-output-on-failure.md +245 -0
  29. package/docs/plans/0022-pi-coding-agent-types.md +201 -0
  30. package/docs/plans/0027-format-before-agent-exit-follow-up-turn.md +355 -0
  31. package/docs/plans/0031-turn-end-flush-with-change-detection.md +365 -0
  32. package/docs/retro/0002-richer-tui-formatter-summaries.md +47 -0
  33. package/docs/retro/0013-fallback-chain-step-type.md +67 -0
  34. package/docs/retro/0015-builtin-treefmt-and-treefmt-nix-support.md +56 -0
  35. package/docs/retro/0016-detailed-formatter-output-on-failure.md +60 -0
  36. package/docs/retro/0022-pi-coding-agent-types.md +62 -0
  37. package/docs/testing.md +95 -0
  38. package/package.json +30 -11
  39. package/prek.toml +2 -2
  40. package/schemas/pi-autoformat.schema.json +145 -21
  41. package/src/builtin-formatters.ts +205 -0
  42. package/src/command-probe.ts +66 -0
  43. package/src/config-loader.ts +829 -90
  44. package/src/custom-mutation-tools.ts +125 -0
  45. package/src/extension.ts +469 -82
  46. package/src/format-scope.ts +118 -0
  47. package/src/formatter-config.ts +73 -36
  48. package/src/formatter-executor.ts +230 -34
  49. package/src/formatter-output-report.ts +149 -0
  50. package/src/formatter-registry.ts +139 -30
  51. package/src/index.ts +26 -5
  52. package/src/prompt-autoformatter.ts +148 -23
  53. package/src/shell-mutation-detector.ts +572 -0
  54. package/src/touched-files-queue.ts +72 -11
  55. package/test/acceptance-event-bus.test.ts +138 -0
  56. package/test/acceptance.test.ts +69 -0
  57. package/test/builtin-formatters.test.ts +382 -0
  58. package/test/command-probe.test.ts +79 -0
  59. package/test/config-loader.test.ts +640 -21
  60. package/test/custom-mutation-tools.test.ts +190 -0
  61. package/test/extension.test.ts +1535 -158
  62. package/test/fallback-acceptance.test.ts +98 -0
  63. package/test/fixtures/event-bus-emitter.ts +26 -0
  64. package/test/fixtures/formatter-recorder.mjs +25 -0
  65. package/test/format-scope.test.ts +139 -0
  66. package/test/formatter-config.test.ts +56 -5
  67. package/test/formatter-executor.test.ts +555 -35
  68. package/test/formatter-output-report.test.ts +178 -0
  69. package/test/formatter-registry.test.ts +330 -37
  70. package/test/helpers/rpc.ts +146 -0
  71. package/test/prompt-autoformatter.test.ts +315 -22
  72. package/test/schema.test.ts +149 -0
  73. package/test/shell-mutation-detector.test.ts +221 -0
  74. package/test/touched-files-queue.test.ts +40 -1
  75. package/test/types/theme-stub.test-d.ts +42 -0
@@ -0,0 +1,190 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ type CustomMutationToolSpec,
5
+ createCustomToolHandler,
6
+ createCustomToolHandlers,
7
+ extractPathsFromInput,
8
+ parseTouchedPayload,
9
+ } from "../src/custom-mutation-tools.js";
10
+
11
+ describe("extractPathsFromInput", () => {
12
+ it("returns the value at a top-level string field", () => {
13
+ expect(
14
+ extractPathsFromInput({ path: "src/index.ts" }, { pathField: "path" }),
15
+ ).toEqual(["src/index.ts"]);
16
+ });
17
+
18
+ it("resolves nested dotted paths", () => {
19
+ expect(
20
+ extractPathsFromInput(
21
+ { args: { target: "docs/readme.md" } },
22
+ { pathField: "args.target" },
23
+ ),
24
+ ).toEqual(["docs/readme.md"]);
25
+ });
26
+
27
+ it("returns an empty array when the field is missing", () => {
28
+ expect(
29
+ extractPathsFromInput({ other: "x" }, { pathField: "path" }),
30
+ ).toEqual([]);
31
+ });
32
+
33
+ it("returns an empty array when an intermediate path segment is missing", () => {
34
+ expect(extractPathsFromInput({}, { pathField: "args.target" })).toEqual([]);
35
+ });
36
+
37
+ it("does not coerce non-string scalar values", () => {
38
+ expect(extractPathsFromInput({ path: 42 }, { pathField: "path" })).toEqual(
39
+ [],
40
+ );
41
+ expect(
42
+ extractPathsFromInput({ path: null }, { pathField: "path" }),
43
+ ).toEqual([]);
44
+ expect(
45
+ extractPathsFromInput({ path: true }, { pathField: "path" }),
46
+ ).toEqual([]);
47
+ });
48
+
49
+ it("flattens string-array values for pathField (single dotted path)", () => {
50
+ // pathField and pathFields differ only in arity, not in value handling.
51
+ // A tool whose field is sometimes a string and sometimes a string[] should
52
+ // not require switching keys.
53
+ expect(
54
+ extractPathsFromInput({ path: ["a.ts", "b.ts"] }, { pathField: "path" }),
55
+ ).toEqual(["a.ts", "b.ts"]);
56
+ });
57
+
58
+ it("returns an empty array when input is not an object", () => {
59
+ expect(extractPathsFromInput(null, { pathField: "path" })).toEqual([]);
60
+ expect(extractPathsFromInput("foo", { pathField: "path" })).toEqual([]);
61
+ expect(extractPathsFromInput(undefined, { pathField: "path" })).toEqual([]);
62
+ });
63
+
64
+ it("flattens string-array values for pathFields entries", () => {
65
+ expect(
66
+ extractPathsFromInput(
67
+ { targets: ["a.ts", "b.ts"] },
68
+ { pathFields: ["targets"] },
69
+ ),
70
+ ).toEqual(["a.ts", "b.ts"]);
71
+ });
72
+
73
+ it("supports a mix of string and string-array fields in pathFields", () => {
74
+ expect(
75
+ extractPathsFromInput(
76
+ { primary: "a.ts", extras: ["b.ts", "c.ts"] },
77
+ { pathFields: ["primary", "extras"] },
78
+ ),
79
+ ).toEqual(["a.ts", "b.ts", "c.ts"]);
80
+ });
81
+
82
+ it("drops non-string entries from string-array values", () => {
83
+ expect(
84
+ extractPathsFromInput(
85
+ { targets: ["a.ts", 1, null, "b.ts"] },
86
+ { pathFields: ["targets"] },
87
+ ),
88
+ ).toEqual(["a.ts", "b.ts"]);
89
+ });
90
+
91
+ it("ignores missing fields inside pathFields without failing", () => {
92
+ expect(
93
+ extractPathsFromInput(
94
+ { primary: "a.ts" },
95
+ { pathFields: ["primary", "missing"] },
96
+ ),
97
+ ).toEqual(["a.ts"]);
98
+ });
99
+ });
100
+
101
+ describe("createCustomToolHandler", () => {
102
+ it("returns paths when the toolName matches", () => {
103
+ const spec: CustomMutationToolSpec = {
104
+ toolName: "mcp_files_write",
105
+ pathField: "path",
106
+ };
107
+ const handler = createCustomToolHandler(spec);
108
+
109
+ expect(handler("mcp_files_write", { path: "src/index.ts" }, "")).toEqual([
110
+ "src/index.ts",
111
+ ]);
112
+ });
113
+
114
+ it("returns an empty array when the toolName does not match", () => {
115
+ const handler = createCustomToolHandler({
116
+ toolName: "mcp_files_write",
117
+ pathField: "path",
118
+ });
119
+
120
+ expect(handler("write", { path: "src/index.ts" }, "")).toEqual([]);
121
+ expect(handler("bash", { command: "ls" }, "")).toEqual([]);
122
+ });
123
+
124
+ it("returns an empty array when the payload lacks the configured field", () => {
125
+ const handler = createCustomToolHandler({
126
+ toolName: "mcp_files_write",
127
+ pathField: "path",
128
+ });
129
+
130
+ expect(handler("mcp_files_write", { other: "x" }, "")).toEqual([]);
131
+ });
132
+ });
133
+
134
+ describe("createCustomToolHandlers", () => {
135
+ it("creates one handler per spec, preserving order", () => {
136
+ const handlers = createCustomToolHandlers([
137
+ { toolName: "tool_a", pathField: "path" },
138
+ { toolName: "tool_b", pathFields: ["destination"] },
139
+ ]);
140
+
141
+ expect(handlers).toHaveLength(2);
142
+ expect(handlers[0]("tool_a", { path: "a.ts" }, "")).toEqual(["a.ts"]);
143
+ expect(handlers[1]("tool_b", { destination: "b.ts" }, "")).toEqual([
144
+ "b.ts",
145
+ ]);
146
+ });
147
+
148
+ it("returns an empty array for an empty spec list", () => {
149
+ expect(createCustomToolHandlers([])).toEqual([]);
150
+ });
151
+ });
152
+
153
+ describe("parseTouchedPayload", () => {
154
+ it("accepts { path: string }", () => {
155
+ expect(parseTouchedPayload({ path: "src/a.ts" })).toEqual(["src/a.ts"]);
156
+ });
157
+
158
+ it("accepts { paths: string[] }", () => {
159
+ expect(parseTouchedPayload({ paths: ["a.ts", "b.ts"] })).toEqual([
160
+ "a.ts",
161
+ "b.ts",
162
+ ]);
163
+ });
164
+
165
+ it("drops non-string entries from paths", () => {
166
+ expect(parseTouchedPayload({ paths: ["a.ts", 1, null, "b.ts"] })).toEqual([
167
+ "a.ts",
168
+ "b.ts",
169
+ ]);
170
+ });
171
+
172
+ it("ignores empty-string paths", () => {
173
+ expect(parseTouchedPayload({ path: "" })).toEqual([]);
174
+ expect(parseTouchedPayload({ paths: ["", "a.ts"] })).toEqual(["a.ts"]);
175
+ });
176
+
177
+ it("returns an empty array for unknown payload shapes", () => {
178
+ expect(parseTouchedPayload({})).toEqual([]);
179
+ expect(parseTouchedPayload({ filename: "x" })).toEqual([]);
180
+ expect(parseTouchedPayload({ path: 42 })).toEqual([]);
181
+ expect(parseTouchedPayload({ paths: "not-an-array" })).toEqual([]);
182
+ });
183
+
184
+ it("returns an empty array for non-object payloads", () => {
185
+ expect(parseTouchedPayload(null)).toEqual([]);
186
+ expect(parseTouchedPayload(undefined)).toEqual([]);
187
+ expect(parseTouchedPayload("a.ts")).toEqual([]);
188
+ expect(parseTouchedPayload(["a.ts"])).toEqual([]);
189
+ });
190
+ });