@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
|
@@ -1,61 +1,170 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
type BuiltinFormatter,
|
|
5
|
+
getBuiltinFormatter,
|
|
6
|
+
} from "./builtin-formatters.js";
|
|
7
|
+
|
|
3
8
|
export type FormatterDefinition = {
|
|
4
9
|
command: string[];
|
|
5
|
-
extensions: string[];
|
|
6
10
|
environment?: Record<string, string>;
|
|
7
11
|
disabled?: boolean;
|
|
8
12
|
};
|
|
9
13
|
|
|
14
|
+
export type FallbackChainStep = {
|
|
15
|
+
fallback: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ChainStep = string | FallbackChainStep;
|
|
19
|
+
|
|
20
|
+
export function isFallbackChainStep(
|
|
21
|
+
step: ChainStep,
|
|
22
|
+
): step is FallbackChainStep {
|
|
23
|
+
return typeof step === "object" && step !== null && "fallback" in step;
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
export type FormatterConfig = {
|
|
11
27
|
formatters: Record<string, FormatterDefinition>;
|
|
12
|
-
chains?: Record<string,
|
|
28
|
+
chains?: Record<string, ChainStep[]>;
|
|
13
29
|
};
|
|
14
30
|
|
|
15
31
|
export type ResolvedFormatter = {
|
|
16
32
|
name: string;
|
|
17
33
|
command: string[];
|
|
18
34
|
environment?: Record<string, string>;
|
|
35
|
+
builtin?: BuiltinFormatter;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ResolvedSingleStep = {
|
|
39
|
+
kind: "single";
|
|
40
|
+
formatter: ResolvedFormatter;
|
|
19
41
|
};
|
|
20
42
|
|
|
21
|
-
export
|
|
22
|
-
|
|
43
|
+
export type ResolvedFallbackStep = {
|
|
44
|
+
kind: "fallback";
|
|
45
|
+
alternatives: ResolvedFormatter[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type ResolvedChainStep = ResolvedSingleStep | ResolvedFallbackStep;
|
|
49
|
+
|
|
50
|
+
export type ChainGroup = {
|
|
51
|
+
chain: ChainStep[];
|
|
52
|
+
files: string[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function encodeChainStep(step: ChainStep): string {
|
|
56
|
+
if (typeof step === "string") {
|
|
57
|
+
return `S:${step}`;
|
|
58
|
+
}
|
|
59
|
+
return `F:${step.fallback.join("|")}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function encodeChain(chain: ChainStep[]): string {
|
|
63
|
+
return chain.map(encodeChainStep).join("\u0000");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const WILDCARD_CHAIN_KEY = "*";
|
|
67
|
+
|
|
68
|
+
export function groupFilesByChain(
|
|
69
|
+
files: string[],
|
|
23
70
|
config: FormatterConfig,
|
|
24
|
-
):
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
71
|
+
): ChainGroup[] {
|
|
72
|
+
const groups: ChainGroup[] = [];
|
|
73
|
+
const indexByKey = new Map<string, number>();
|
|
74
|
+
|
|
75
|
+
const wildcardChain = config.chains?.[WILDCARD_CHAIN_KEY];
|
|
76
|
+
if (wildcardChain && wildcardChain.length > 0 && files.length > 0) {
|
|
77
|
+
groups.push({ chain: [...wildcardChain], files: [...files] });
|
|
78
|
+
indexByKey.set(`W:${encodeChain(wildcardChain)}`, 0);
|
|
28
79
|
}
|
|
29
80
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
81
|
+
for (const filePath of files) {
|
|
82
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
83
|
+
if (!extension) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const chainSteps = config.chains?.[extension];
|
|
87
|
+
if (!chainSteps || chainSteps.length === 0) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const key = `E:${encodeChain(chainSteps)}`;
|
|
91
|
+
const existingIndex = indexByKey.get(key);
|
|
92
|
+
if (existingIndex === undefined) {
|
|
93
|
+
indexByKey.set(key, groups.length);
|
|
94
|
+
groups.push({ chain: [...chainSteps], files: [filePath] });
|
|
95
|
+
} else {
|
|
96
|
+
groups[existingIndex].files.push(filePath);
|
|
97
|
+
}
|
|
33
98
|
}
|
|
34
99
|
|
|
35
|
-
return
|
|
36
|
-
.map((formatterName) =>
|
|
37
|
-
resolveFormatterByName(formatterName, filePath, config),
|
|
38
|
-
)
|
|
39
|
-
.filter((formatter): formatter is ResolvedFormatter => formatter !== null);
|
|
100
|
+
return groups;
|
|
40
101
|
}
|
|
41
102
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
filePath: string,
|
|
103
|
+
export function resolveChain(
|
|
104
|
+
chainNames: string[],
|
|
45
105
|
config: FormatterConfig,
|
|
46
|
-
): ResolvedFormatter
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
106
|
+
): ResolvedFormatter[] {
|
|
107
|
+
const resolved: ResolvedFormatter[] = [];
|
|
108
|
+
for (const name of chainNames) {
|
|
109
|
+
const formatter = resolveFormatterByName(name, config);
|
|
110
|
+
if (formatter) {
|
|
111
|
+
resolved.push(formatter);
|
|
112
|
+
}
|
|
50
113
|
}
|
|
114
|
+
return resolved;
|
|
115
|
+
}
|
|
51
116
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
117
|
+
function resolveFormatterByName(
|
|
118
|
+
name: string,
|
|
119
|
+
config: FormatterConfig,
|
|
120
|
+
): ResolvedFormatter | undefined {
|
|
121
|
+
const formatter = config.formatters[name];
|
|
122
|
+
if (formatter) {
|
|
123
|
+
if (formatter.disabled) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
name,
|
|
128
|
+
command: [...formatter.command],
|
|
129
|
+
environment: formatter.environment,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const builtin = getBuiltinFormatter(name);
|
|
133
|
+
if (builtin) {
|
|
134
|
+
return {
|
|
135
|
+
name,
|
|
136
|
+
// Built-ins build their argv at execution time from the discovered
|
|
137
|
+
// config root. The placeholder here is replaced by the executor.
|
|
138
|
+
command: [builtin.name],
|
|
139
|
+
builtin,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
57
143
|
}
|
|
58
144
|
|
|
59
|
-
function
|
|
60
|
-
|
|
145
|
+
export function resolveChainSteps(
|
|
146
|
+
steps: ChainStep[],
|
|
147
|
+
config: FormatterConfig,
|
|
148
|
+
): ResolvedChainStep[] {
|
|
149
|
+
const resolved: ResolvedChainStep[] = [];
|
|
150
|
+
for (const step of steps) {
|
|
151
|
+
if (typeof step === "string") {
|
|
152
|
+
const formatter = resolveFormatterByName(step, config);
|
|
153
|
+
if (formatter) {
|
|
154
|
+
resolved.push({ kind: "single", formatter });
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const alternatives: ResolvedFormatter[] = [];
|
|
159
|
+
for (const name of step.fallback) {
|
|
160
|
+
const formatter = resolveFormatterByName(name, config);
|
|
161
|
+
if (formatter) {
|
|
162
|
+
alternatives.push(formatter);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (alternatives.length > 0) {
|
|
166
|
+
resolved.push({ kind: "fallback", alternatives });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return resolved;
|
|
61
170
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,28 +15,49 @@ export {
|
|
|
15
15
|
createAutoformatExtension,
|
|
16
16
|
default as autoformatExtension,
|
|
17
17
|
} from "./extension.js";
|
|
18
|
+
export {
|
|
19
|
+
type FormatScope,
|
|
20
|
+
type FormatScopeSetting,
|
|
21
|
+
isInFormatScope,
|
|
22
|
+
resolveFormatScope,
|
|
23
|
+
} from "./format-scope.js";
|
|
18
24
|
export {
|
|
19
25
|
type AutoformatConfig,
|
|
20
26
|
createFormatterConfig,
|
|
21
27
|
DEFAULT_FORMATTER_CONFIG,
|
|
22
|
-
type FormatMode,
|
|
23
28
|
type UserFormatterConfig,
|
|
24
29
|
} from "./formatter-config.js";
|
|
25
30
|
export {
|
|
31
|
+
type BatchRun,
|
|
32
|
+
type ChainGroupInput,
|
|
26
33
|
type CommandRunner,
|
|
27
34
|
type CommandRunnerOptions,
|
|
28
35
|
type CommandRunResult,
|
|
29
|
-
|
|
30
|
-
type FormatterExecutionResult,
|
|
36
|
+
executeChainGroup,
|
|
31
37
|
} from "./formatter-executor.js";
|
|
32
38
|
export {
|
|
39
|
+
type ChainGroup,
|
|
33
40
|
type FormatterConfig,
|
|
34
41
|
type FormatterDefinition,
|
|
42
|
+
groupFilesByChain,
|
|
35
43
|
type ResolvedFormatter,
|
|
36
|
-
|
|
44
|
+
resolveChain,
|
|
37
45
|
} from "./formatter-registry.js";
|
|
38
46
|
export {
|
|
47
|
+
type ChainGroupResult,
|
|
39
48
|
PromptAutoformatter,
|
|
40
49
|
type PromptAutoformatterResult,
|
|
41
50
|
} from "./prompt-autoformatter.js";
|
|
42
|
-
export {
|
|
51
|
+
export {
|
|
52
|
+
DEFAULT_SHELL_MUTATION_DETECTION,
|
|
53
|
+
matchWrapper,
|
|
54
|
+
parseKnownCommand,
|
|
55
|
+
type ShellMutationDetectionConfig,
|
|
56
|
+
SnapshotTracker,
|
|
57
|
+
type WrapperConfig,
|
|
58
|
+
} from "./shell-mutation-detector.js";
|
|
59
|
+
export {
|
|
60
|
+
type MutationSourceHandler,
|
|
61
|
+
TouchedFilesQueue,
|
|
62
|
+
writeOrEditHandler,
|
|
63
|
+
} from "./touched-files-queue.js";
|
|
@@ -1,58 +1,183 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import type { DiscoveryCache } from "./builtin-formatters.js";
|
|
5
|
+
import {
|
|
6
|
+
type CommandProbe,
|
|
7
|
+
createCachedCommandProbe,
|
|
8
|
+
defaultCommandProbe,
|
|
9
|
+
} from "./command-probe.js";
|
|
10
|
+
import type { FormatScope } from "./format-scope.js";
|
|
1
11
|
import {
|
|
12
|
+
type BatchRun,
|
|
2
13
|
type CommandRunner,
|
|
3
|
-
|
|
4
|
-
type FormatterExecutionResult,
|
|
14
|
+
executeChainGroupWithPartition,
|
|
5
15
|
} from "./formatter-executor.js";
|
|
16
|
+
import type { ChainStep } from "./formatter-registry.js";
|
|
6
17
|
import {
|
|
7
18
|
type FormatterConfig,
|
|
8
|
-
|
|
19
|
+
groupFilesByChain,
|
|
20
|
+
resolveChainSteps,
|
|
21
|
+
WILDCARD_CHAIN_KEY,
|
|
9
22
|
} from "./formatter-registry.js";
|
|
10
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
type MutationSourceHandler,
|
|
25
|
+
TouchedFilesQueue,
|
|
26
|
+
} from "./touched-files-queue.js";
|
|
27
|
+
|
|
28
|
+
export type ChainGroupResult = {
|
|
29
|
+
chain: ChainStep[];
|
|
30
|
+
files: string[];
|
|
31
|
+
runs: BatchRun[];
|
|
32
|
+
changedFiles: string[];
|
|
33
|
+
};
|
|
11
34
|
|
|
12
35
|
export type PromptAutoformatterResult = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
groups: ChainGroupResult[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type PromptAutoformatterOptions = {
|
|
40
|
+
scope?: FormatScope;
|
|
41
|
+
mutationHandlers?: MutationSourceHandler[];
|
|
42
|
+
/**
|
|
43
|
+
* Probe used to test whether a fallback alternative's command is on PATH.
|
|
44
|
+
* Wrapped in a per-flush cache so the same command is probed at most once
|
|
45
|
+
* per flush across all chain groups. Defaults to the synchronous PATH walker.
|
|
46
|
+
*/
|
|
47
|
+
commandProbe?: CommandProbe;
|
|
17
48
|
};
|
|
18
49
|
|
|
50
|
+
async function hashFile(filePath: string): Promise<string | undefined> {
|
|
51
|
+
try {
|
|
52
|
+
const content = await readFile(filePath);
|
|
53
|
+
return createHash("sha256").update(content).digest("hex");
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
19
59
|
export class PromptAutoformatter {
|
|
20
60
|
private readonly queue: TouchedFilesQueue;
|
|
61
|
+
private readonly commandProbe: CommandProbe;
|
|
62
|
+
/** Session-scoped cache for built-in config-root discovery. */
|
|
63
|
+
private readonly discoveryCache: DiscoveryCache = new Map();
|
|
21
64
|
|
|
22
65
|
constructor(
|
|
23
66
|
private readonly cwd: string,
|
|
24
67
|
private readonly config: FormatterConfig,
|
|
25
68
|
private readonly runner: CommandRunner,
|
|
69
|
+
options?: PromptAutoformatterOptions,
|
|
26
70
|
) {
|
|
27
|
-
this.queue = new TouchedFilesQueue(
|
|
71
|
+
this.queue = new TouchedFilesQueue({
|
|
72
|
+
cwd,
|
|
73
|
+
scope: options?.scope,
|
|
74
|
+
handlers: options?.mutationHandlers,
|
|
75
|
+
});
|
|
76
|
+
this.commandProbe = options?.commandProbe ?? defaultCommandProbe;
|
|
28
77
|
}
|
|
29
78
|
|
|
30
|
-
recordToolResult(toolName: string, payload: unknown): void {
|
|
31
|
-
this.queue.recordToolResult(toolName, payload);
|
|
79
|
+
recordToolResult(toolName: string, payload: unknown, output = ""): void {
|
|
80
|
+
this.queue.recordToolResult(toolName, payload, output);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addTouchedPath(filePath: string): void {
|
|
84
|
+
this.queue.addPath(filePath);
|
|
32
85
|
}
|
|
33
86
|
|
|
34
87
|
async flushPrompt(): Promise<PromptAutoformatterResult> {
|
|
35
88
|
const touchedFiles = this.queue.flush();
|
|
36
|
-
const
|
|
89
|
+
const fileGroups = groupFilesByChain(touchedFiles, this.config);
|
|
90
|
+
const groupResults: ChainGroupResult[] = [];
|
|
91
|
+
|
|
92
|
+
// One probe cache per flush, shared across all chain groups so the same
|
|
93
|
+
// fallback command is probed at most once even when many extensions share
|
|
94
|
+
// the same fallback step.
|
|
95
|
+
const cachedProbe = createCachedCommandProbe(this.commandProbe);
|
|
96
|
+
const wildcardChainSteps = this.config.chains?.[WILDCARD_CHAIN_KEY];
|
|
97
|
+
const hasWildcard =
|
|
98
|
+
Array.isArray(wildcardChainSteps) && wildcardChainSteps.length > 0;
|
|
99
|
+
const wildcardHandled = new Set<string>();
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < fileGroups.length; i += 1) {
|
|
102
|
+
const group = fileGroups[i];
|
|
103
|
+
// groupFilesByChain emits the wildcard group first when chains["*"] is
|
|
104
|
+
// configured, so the index is sufficient.
|
|
105
|
+
const isWildcard = hasWildcard && i === 0;
|
|
37
106
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
107
|
+
// For per-extension groups, drop any files claimed by the wildcard pass.
|
|
108
|
+
const inputFiles = isWildcard
|
|
109
|
+
? group.files
|
|
110
|
+
: group.files.filter((f) => !wildcardHandled.has(f));
|
|
111
|
+
if (inputFiles.length === 0) {
|
|
41
112
|
continue;
|
|
42
113
|
}
|
|
43
114
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
115
|
+
const resolved = resolveChainSteps(group.chain, this.config);
|
|
116
|
+
if (resolved.length === 0) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Snapshot file content hashes before formatting.
|
|
121
|
+
const preHashes = new Map<string, string>();
|
|
122
|
+
for (const file of inputFiles) {
|
|
123
|
+
const hash = await hashFile(file);
|
|
124
|
+
if (hash !== undefined) {
|
|
125
|
+
preHashes.set(file, hash);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { runs, unhandled } = await executeChainGroupWithPartition(
|
|
130
|
+
{ chain: resolved, files: inputFiles },
|
|
131
|
+
this.runner,
|
|
132
|
+
{
|
|
133
|
+
cwd: this.cwd,
|
|
134
|
+
commandProbe: cachedProbe,
|
|
135
|
+
builtinContext: { cache: this.discoveryCache },
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (isWildcard) {
|
|
140
|
+
// Files not in the unhandled tail were claimed by a built-in step;
|
|
141
|
+
// remove them from the per-extension pass.
|
|
142
|
+
const unhandledSet = new Set(unhandled);
|
|
143
|
+
for (const file of inputFiles) {
|
|
144
|
+
if (!unhandledSet.has(file)) {
|
|
145
|
+
wildcardHandled.add(file);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (runs.length === 0) {
|
|
151
|
+
// E.g. a chain consisting of a single fallback group whose
|
|
152
|
+
// alternatives are all absent from PATH, or a built-in that skipped
|
|
153
|
+
// the entire batch. Drop the group so it does not show up as a
|
|
154
|
+
// phantom "formatted nothing" entry downstream.
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Determine which files were actually changed by comparing post-format
|
|
159
|
+
// hashes to the pre-format snapshots.
|
|
160
|
+
const changedFiles: string[] = [];
|
|
161
|
+
for (const file of inputFiles) {
|
|
162
|
+
const afterHash = await hashFile(file);
|
|
163
|
+
if (afterHash === undefined) {
|
|
164
|
+
// File was deleted by the formatter — exclude.
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const beforeHash = preHashes.get(file);
|
|
168
|
+
if (beforeHash !== afterHash) {
|
|
169
|
+
changedFiles.push(file);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
47
172
|
|
|
48
|
-
|
|
49
|
-
|
|
173
|
+
groupResults.push({
|
|
174
|
+
chain: group.chain,
|
|
175
|
+
files: [...inputFiles],
|
|
50
176
|
runs,
|
|
177
|
+
changedFiles,
|
|
51
178
|
});
|
|
52
179
|
}
|
|
53
180
|
|
|
54
|
-
return {
|
|
55
|
-
files: fileResults,
|
|
56
|
-
};
|
|
181
|
+
return { groups: groupResults };
|
|
57
182
|
}
|
|
58
183
|
}
|