@git.zone/cli 2.19.0 → 2.19.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.
Files changed (35) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/gitzone.cli.d.ts +1 -1
  3. package/dist_ts/gitzone.cli.js +100 -126
  4. package/dist_ts/helpers.climode.d.ts +2 -0
  5. package/dist_ts/helpers.climode.js +31 -2
  6. package/dist_ts/helpers.smartconfigmigrations.js +34 -2
  7. package/dist_ts/mod_config/index.js +51 -10
  8. package/dist_ts/mod_format/classes.baseformatter.d.ts +3 -1
  9. package/dist_ts/mod_format/classes.baseformatter.js +7 -1
  10. package/dist_ts/mod_format/classes.formatplanner.d.ts +2 -0
  11. package/dist_ts/mod_format/classes.formatplanner.js +48 -4
  12. package/dist_ts/mod_format/formatters/license.formatter.d.ts +4 -1
  13. package/dist_ts/mod_format/formatters/license.formatter.js +32 -10
  14. package/dist_ts/mod_format/formatters/prettier.formatter.d.ts +0 -3
  15. package/dist_ts/mod_format/formatters/prettier.formatter.js +53 -62
  16. package/dist_ts/mod_format/index.d.ts +1 -1
  17. package/dist_ts/mod_format/index.js +237 -8
  18. package/dist_ts/mod_format/interfaces.format.d.ts +7 -11
  19. package/dist_ts/mod_format/interfaces.format.js +1 -1
  20. package/dist_ts/mod_standard/index.js +2 -1
  21. package/package.json +1 -1
  22. package/readme.hints.md +10 -0
  23. package/readme.md +11 -0
  24. package/ts/00_commitinfo_data.ts +1 -1
  25. package/ts/gitzone.cli.ts +104 -144
  26. package/ts/helpers.climode.ts +36 -1
  27. package/ts/helpers.smartconfigmigrations.ts +37 -1
  28. package/ts/mod_config/index.ts +60 -9
  29. package/ts/mod_format/classes.baseformatter.ts +13 -1
  30. package/ts/mod_format/classes.formatplanner.ts +66 -4
  31. package/ts/mod_format/formatters/license.formatter.ts +43 -15
  32. package/ts/mod_format/formatters/prettier.formatter.ts +54 -66
  33. package/ts/mod_format/index.ts +289 -8
  34. package/ts/mod_format/interfaces.format.ts +8 -11
  35. package/ts/mod_standard/index.ts +1 -0
package/ts/gitzone.cli.ts CHANGED
@@ -1,13 +1,107 @@
1
1
  import * as plugins from "./plugins.js";
2
2
  import * as paths from "./paths.js";
3
- import { GitzoneConfig } from "./classes.gitzoneconfig.js";
4
- import { getRawCliMode } from "./helpers.climode.js";
3
+ import {
4
+ getProcessUserArgv,
5
+ getRawCliMode,
6
+ parseCliArgv,
7
+ } from "./helpers.climode.js";
5
8
  import { commitinfo } from "./00_commitinfo_data.js";
6
9
 
7
- const gitzoneSmartcli = new plugins.smartcli.Smartcli();
10
+ const runParsedCommand = async (argvArg: any): Promise<void> => {
11
+ const command = argvArg._?.[0];
12
+
13
+ switch (command) {
14
+ case undefined:
15
+ case "help": {
16
+ const modStandard = await import("./mod_standard/index.js");
17
+ await modStandard.run(argvArg);
18
+ break;
19
+ }
20
+ case "commit": {
21
+ const modCommit = await import("./mod_commit/index.js");
22
+ await modCommit.run(argvArg);
23
+ break;
24
+ }
25
+ case "release": {
26
+ const modRelease = await import("./mod_release/index.js");
27
+ await modRelease.run(argvArg);
28
+ break;
29
+ }
30
+ case "deprecate": {
31
+ const modDeprecate = await import("./mod_deprecate/index.js");
32
+ await modDeprecate.run();
33
+ break;
34
+ }
35
+ case "docker": {
36
+ const modDocker = await import("./mod_docker/index.js");
37
+ await modDocker.run(argvArg);
38
+ break;
39
+ }
40
+ case "format": {
41
+ const modFormat = await import("./mod_format/index.js");
42
+ await modFormat.run({
43
+ ...argvArg,
44
+ write: argvArg.write || argvArg.w,
45
+ dryRun: argvArg["dry-run"],
46
+ yes: argvArg.yes || argvArg.y,
47
+ planOnly: argvArg["plan-only"] || argvArg.planOnly,
48
+ savePlan: argvArg["save-plan"] || argvArg.savePlan,
49
+ fromPlan: argvArg["from-plan"] || argvArg.fromPlan,
50
+ detailed: argvArg.detailed,
51
+ interactive: argvArg.interactive !== false,
52
+ verbose: argvArg.verbose,
53
+ diff: argvArg.diff,
54
+ });
55
+ break;
56
+ }
57
+ case "meta": {
58
+ const modMeta = await import("./mod_meta/index.js");
59
+ await modMeta.run(argvArg);
60
+ break;
61
+ }
62
+ case "open": {
63
+ const modOpen = await import("./mod_open/index.js");
64
+ await modOpen.run(argvArg);
65
+ break;
66
+ }
67
+ case "template": {
68
+ const modTemplate = await import("./mod_template/index.js");
69
+ await modTemplate.run(argvArg);
70
+ break;
71
+ }
72
+ case "start": {
73
+ const modStart = await import("./mod_start/index.js");
74
+ await modStart.run(argvArg);
75
+ break;
76
+ }
77
+ case "helpers": {
78
+ const modHelpers = await import("./mod_helpers/index.js");
79
+ await modHelpers.run(argvArg);
80
+ break;
81
+ }
82
+ case "tools": {
83
+ const modTools = await import("./mod_tools/index.js");
84
+ await modTools.run(argvArg);
85
+ break;
86
+ }
87
+ case "config": {
88
+ const modConfig = await import("./mod_config/index.js");
89
+ await modConfig.run(argvArg);
90
+ break;
91
+ }
92
+ case "services": {
93
+ const modServices = await import("./mod_services/index.js");
94
+ await modServices.run(argvArg);
95
+ break;
96
+ }
97
+ default: {
98
+ const modStandard = await import("./mod_standard/index.js");
99
+ await modStandard.run(argvArg);
100
+ }
101
+ }
102
+ };
8
103
 
9
104
  export let run = async () => {
10
- const done = plugins.smartpromise.defer();
11
105
  const rawCliMode = await getRawCliMode();
12
106
 
13
107
  // get packageInfo
@@ -34,144 +128,10 @@ export let run = async () => {
34
128
  if (rawCliMode.output === "human") {
35
129
  console.log("---------------------------------------------");
36
130
  }
37
- gitzoneSmartcli.addVersion(packageVersion);
38
-
39
- // ======> Standard task <======
40
-
41
- /**
42
- * standard task
43
- */
44
- gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => {
45
- const modStandard = await import("./mod_standard/index.js");
46
- await modStandard.run(argvArg);
47
- });
48
-
49
- gitzoneSmartcli.addCommand("help").subscribe(async (argvArg) => {
50
- const modStandard = await import("./mod_standard/index.js");
51
- await modStandard.run(argvArg);
52
- });
53
-
54
- // ======> Specific tasks <======
55
-
56
- /**
57
- * commit something
58
- */
59
- gitzoneSmartcli.addCommand("commit").subscribe(async (argvArg) => {
60
- const modCommit = await import("./mod_commit/index.js");
61
- await modCommit.run(argvArg);
62
- });
63
-
64
- /**
65
- * create a release from pending changelog entries
66
- */
67
- gitzoneSmartcli.addCommand("release").subscribe(async (argvArg) => {
68
- const modRelease = await import("./mod_release/index.js");
69
- await modRelease.run(argvArg);
70
- });
71
-
72
- /**
73
- * deprecate a package on npm
74
- */
75
- gitzoneSmartcli.addCommand("deprecate").subscribe(async (argvArg) => {
76
- const modDeprecate = await import("./mod_deprecate/index.js");
77
- await modDeprecate.run();
78
- });
79
-
80
- /**
81
- * docker
82
- */
83
- gitzoneSmartcli.addCommand("docker").subscribe(async (argvArg) => {
84
- const modDocker = await import("./mod_docker/index.js");
85
- await modDocker.run(argvArg);
86
- });
87
-
88
- /**
89
- * Update all files that comply with the gitzone standard
90
- */
91
- gitzoneSmartcli.addCommand("format").subscribe(async (argvArg) => {
92
- const config = GitzoneConfig.fromCwd();
93
- const modFormat = await import("./mod_format/index.js");
94
-
95
- // Handle format with options
96
- // Default is dry-mode, use --write/-w to apply changes
97
- await modFormat.run({
98
- ...argvArg,
99
- write: argvArg.write || argvArg.w,
100
- dryRun: argvArg["dry-run"],
101
- yes: argvArg.yes,
102
- planOnly: argvArg["plan-only"],
103
- savePlan: argvArg["save-plan"],
104
- fromPlan: argvArg["from-plan"],
105
- detailed: argvArg.detailed,
106
- interactive: argvArg.interactive !== false,
107
- verbose: argvArg.verbose,
108
- diff: argvArg.diff,
109
- });
110
- });
111
-
112
- /**
113
- * run meta commands
114
- */
115
- gitzoneSmartcli.addCommand("meta").subscribe(async (argvArg) => {
116
- const config = GitzoneConfig.fromCwd();
117
- const modMeta = await import("./mod_meta/index.js");
118
- modMeta.run(argvArg);
119
- });
120
-
121
- /**
122
- * open assets
123
- */
124
- gitzoneSmartcli.addCommand("open").subscribe(async (argvArg) => {
125
- const modOpen = await import("./mod_open/index.js");
126
- modOpen.run(argvArg);
127
- });
128
-
129
- /**
130
- * add a readme to a project
131
- */
132
- gitzoneSmartcli.addCommand("template").subscribe(async (argvArg) => {
133
- const modTemplate = await import("./mod_template/index.js");
134
- modTemplate.run(argvArg);
135
- });
136
-
137
- /**
138
- * start working on a project
139
- */
140
- gitzoneSmartcli.addCommand("start").subscribe(async (argvArg) => {
141
- const modTemplate = await import("./mod_start/index.js");
142
- modTemplate.run(argvArg);
143
- });
144
-
145
- gitzoneSmartcli.addCommand("helpers").subscribe(async (argvArg) => {
146
- const modHelpers = await import("./mod_helpers/index.js");
147
- modHelpers.run(argvArg);
148
- });
149
-
150
- /**
151
- * manage the global @git.zone toolchain
152
- */
153
- gitzoneSmartcli.addCommand("tools").subscribe(async (argvArg) => {
154
- const modTools = await import("./mod_tools/index.js");
155
- await modTools.run(argvArg);
156
- });
157
-
158
- /**
159
- * manage release configuration
160
- */
161
- gitzoneSmartcli.addCommand("config").subscribe(async (argvArg) => {
162
- const modConfig = await import("./mod_config/index.js");
163
- await modConfig.run(argvArg);
164
- });
165
-
166
- /**
167
- * manage development services (MongoDB, S3/MinIO)
168
- */
169
- gitzoneSmartcli.addCommand("services").subscribe(async (argvArg) => {
170
- const modServices = await import("./mod_services/index.js");
171
- await modServices.run(argvArg);
172
- });
173
-
174
- // start parsing of the cli
175
- gitzoneSmartcli.startParse();
176
- return await done.promise;
131
+ const argvArg = parseCliArgv(getProcessUserArgv());
132
+ if (argvArg.v || argvArg.version) {
133
+ console.log(packageVersion);
134
+ return;
135
+ }
136
+ await runParsedCommand(argvArg);
177
137
  };
@@ -88,6 +88,41 @@ const parseRawArgv = (argv: string[]): TArgSource => {
88
88
  return parsedArgv;
89
89
  };
90
90
 
91
+ export const parseCliArgv = parseRawArgv;
92
+
93
+ export const getProcessUserArgv = (): string[] => {
94
+ const rawArgv = process.argv;
95
+ const argv0Base = (rawArgv[0] || "").split(/[\\/]/).pop()?.toLowerCase();
96
+ const runtimeNames = new Set([
97
+ "node",
98
+ "node.exe",
99
+ "nodejs",
100
+ "nodejs.exe",
101
+ "bun",
102
+ "bun.exe",
103
+ "deno",
104
+ "deno.exe",
105
+ "tsx",
106
+ "tsx.exe",
107
+ "ts-node",
108
+ "ts-node.exe",
109
+ ]);
110
+
111
+ if (!runtimeNames.has(argv0Base || "")) {
112
+ return rawArgv.slice();
113
+ }
114
+
115
+ const firstUserArg = rawArgv[1] || "";
116
+ const firstUserArgLooksLikeScript =
117
+ firstUserArg.includes("/") ||
118
+ firstUserArg.endsWith(".js") ||
119
+ firstUserArg.endsWith(".ts") ||
120
+ firstUserArg.endsWith(".mjs") ||
121
+ firstUserArg.endsWith(".cjs");
122
+
123
+ return rawArgv.slice(firstUserArgLooksLikeScript ? 2 : 1);
124
+ };
125
+
91
126
  const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
92
127
  if (value === "human" || value === "plain" || value === "json") {
93
128
  return value;
@@ -171,7 +206,7 @@ export const getCliMode = async (
171
206
 
172
207
  export const getRawCliMode = async (): Promise<ICliMode> => {
173
208
  const cliConfig = await getCliModeConfig();
174
- const rawArgv = parseRawArgv(process.argv.slice(2));
209
+ const rawArgv = parseRawArgv(getProcessUserArgv());
175
210
  return resolveCliMode(rawArgv, cliConfig);
176
211
  };
177
212
 
@@ -19,6 +19,38 @@ const ensureObject = (parent: Record<string, any>, key: string): Record<string,
19
19
  return parent[key];
20
20
  };
21
21
 
22
+ const normalizeRegistryList = (registries: unknown[]): string[] => {
23
+ const result: string[] = [];
24
+ for (const registry of registries) {
25
+ if (typeof registry !== "string" || !registry.trim()) {
26
+ continue;
27
+ }
28
+ const normalizedRegistry = normalizeRegistryUrl(registry);
29
+ if (!result.includes(normalizedRegistry)) {
30
+ result.push(normalizedRegistry);
31
+ }
32
+ }
33
+ return result;
34
+ };
35
+
36
+ const migrateLegacyReleaseArray = (smartconfigJson: Record<string, any>): boolean => {
37
+ const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
38
+ if (!Array.isArray(cliConfig.release)) {
39
+ return false;
40
+ }
41
+
42
+ const registries = normalizeRegistryList(cliConfig.release);
43
+ cliConfig.release = {
44
+ targets: {
45
+ npm: {
46
+ enabled: registries.length > 0,
47
+ registries,
48
+ },
49
+ },
50
+ };
51
+ return true;
52
+ };
53
+
22
54
  const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean => {
23
55
  let migrated = false;
24
56
  const migrations = [
@@ -50,9 +82,9 @@ const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean =>
50
82
 
51
83
  const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
52
84
  const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
85
+ let migrated = migrateLegacyReleaseArray(smartconfigJson);
53
86
  const releaseConfig = ensureObject(cliConfig, "release");
54
87
 
55
- let migrated = false;
56
88
  const targets = ensureObject(releaseConfig, "targets");
57
89
  const shipzoneConfig = smartconfigJson["@ship.zone/szci"];
58
90
 
@@ -192,6 +224,10 @@ export const migrateSmartconfigData = (
192
224
  const fromVersion = typeof cliConfig.schemaVersion === "number" ? cliConfig.schemaVersion : 1;
193
225
  let currentVersion = fromVersion;
194
226
 
227
+ if (targetVersion >= 2) {
228
+ migrated = migrateLegacyReleaseArray(smartconfigJson) || migrated;
229
+ }
230
+
195
231
  if (currentVersion < 2 && targetVersion >= 2) {
196
232
  migrated = migrateToV2(smartconfigJson) || migrated;
197
233
  currentVersion = 2;
@@ -168,7 +168,7 @@ async function handleInteractiveMenu(): Promise<void> {
168
168
  { name: "Configure release workflow", value: "release" },
169
169
  { name: "Configure services", value: "services" },
170
170
  { name: "Validate configuration (doctor)", value: "doctor" },
171
- { name: "Fix configuration with opencode", value: "fix" },
171
+ { name: "Fix configuration", value: "fix" },
172
172
  { name: "Add an npm target registry", value: "add" },
173
173
  { name: "Remove an npm target registry", value: "remove" },
174
174
  { name: "Clear npm target registries", value: "clear" },
@@ -939,8 +939,8 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
939
939
  return;
940
940
  }
941
941
 
942
- const findings = await collectDoctorFindings();
943
- const counts = countDoctorFindings(findings);
942
+ let findings = await collectDoctorFindings();
943
+ let counts = countDoctorFindings(findings);
944
944
  const extraInstructions = (argvArg._?.slice(2).join(" ") || "").trim();
945
945
  const force = Boolean(argvArg.force);
946
946
 
@@ -954,10 +954,10 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
954
954
 
955
955
  if (!mode.yes) {
956
956
  if (!mode.interactive) {
957
- throw new Error("Config fix requires an interactive terminal or `-y` to run opencode non-interactively.");
957
+ throw new Error("Config fix requires an interactive terminal or `-y` to run non-interactively.");
958
958
  }
959
959
  const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
960
- `Run opencode to fix .smartconfig.json? (${counts.error} error, ${counts.warn} warning)`,
960
+ `Run configuration fixes for .smartconfig.json? (${counts.error} error, ${counts.warn} warning)`,
961
961
  true,
962
962
  );
963
963
  if (!confirmed) {
@@ -966,6 +966,16 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
966
966
  }
967
967
  }
968
968
 
969
+ const appliedKnownFixes = await applyKnownConfigFixes(mode);
970
+ if (appliedKnownFixes) {
971
+ findings = await collectDoctorFindings();
972
+ counts = countDoctorFindings(findings);
973
+ if (counts.error === 0 && counts.warn === 0 && !extraInstructions && !force) {
974
+ printDoctorResult(findings, mode);
975
+ return;
976
+ }
977
+ }
978
+
969
979
  const opencodeArgs = [
970
980
  "run",
971
981
  "--title",
@@ -1004,6 +1014,33 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
1004
1014
  printDoctorResult(finalFindings, mode);
1005
1015
  }
1006
1016
 
1017
+ async function applyKnownConfigFixes(mode: ICliMode): Promise<boolean> {
1018
+ const smartconfigPath = getSmartconfigPath();
1019
+ if (!(await plugins.smartfs.file(smartconfigPath).exists())) {
1020
+ return false;
1021
+ }
1022
+
1023
+ let smartconfigData: Record<string, any>;
1024
+ try {
1025
+ smartconfigData = await readSmartconfigFile();
1026
+ } catch {
1027
+ return false;
1028
+ }
1029
+
1030
+ const result = migrateSmartconfigData(smartconfigData);
1031
+ if (!result.migrated) {
1032
+ return false;
1033
+ }
1034
+
1035
+ await writeSmartconfigFile(smartconfigData);
1036
+ plugins.logger.log(
1037
+ "success",
1038
+ `Applied known .smartconfig.json migrations to schema v${result.toVersion}`,
1039
+ );
1040
+ await formatSmartconfigWithDiff(mode);
1041
+ return true;
1042
+ }
1043
+
1007
1044
  async function collectDoctorFindings(): Promise<IDoctorFinding[]> {
1008
1045
  const findings: IDoctorFinding[] = [];
1009
1046
  const smartconfigPath = getSmartconfigPath();
@@ -1071,7 +1108,7 @@ async function collectDoctorFindings(): Promise<IDoctorFinding[]> {
1071
1108
  await validateDetectedProjectType(cliConfig, findings);
1072
1109
 
1073
1110
  validateCommitConfig(cliConfig.commit || {}, findings);
1074
- await validateReleaseConfig(cliConfig.release || {}, smartconfigData, findings);
1111
+ await validateReleaseConfig(cliConfig.release, smartconfigData, findings);
1075
1112
 
1076
1113
  return findings;
1077
1114
  }
@@ -1570,10 +1607,24 @@ function validateCommitConfig(
1570
1607
  }
1571
1608
 
1572
1609
  async function validateReleaseConfig(
1573
- releaseConfig: Record<string, any>,
1610
+ rawReleaseConfig: unknown,
1574
1611
  smartconfigData: Record<string, any>,
1575
1612
  findings: IDoctorFinding[],
1576
1613
  ): Promise<void> {
1614
+ const releaseConfig = rawReleaseConfig === undefined ? {} : rawReleaseConfig;
1615
+ if (!isPlainObject(releaseConfig)) {
1616
+ findings.push({
1617
+ level: "error",
1618
+ message: `Release config must be an object, found ${
1619
+ Array.isArray(releaseConfig) ? "array" : typeof releaseConfig
1620
+ }`,
1621
+ fix: Array.isArray(releaseConfig)
1622
+ ? "Run `gitzone config migrate` to move legacy registry arrays into release.targets.npm.registries."
1623
+ : "Set @git.zone/cli.release to an object or remove it.",
1624
+ });
1625
+ return;
1626
+ }
1627
+
1577
1628
  const confirmation = releaseConfig.confirmation;
1578
1629
  if (confirmation === undefined || validConfirmationModes.includes(confirmation)) {
1579
1630
  findings.push({ level: "ok", message: "Release confirmation mode is valid" });
@@ -1993,7 +2044,7 @@ export function showHelp(mode?: ICliMode): void {
1993
2044
  { name: "cli", description: "Configure CLI behavior interactively" },
1994
2045
  { name: "release", description: "Configure release workflow interactively" },
1995
2046
  { name: "doctor", description: "Validate .smartconfig.json" },
1996
- { name: "fix [instructions]", description: "Use opencode to repair .smartconfig.json" },
2047
+ { name: "fix [instructions]", description: "Repair .smartconfig.json" },
1997
2048
  { name: "get <path>", description: "Read a single config value" },
1998
2049
  { name: "set <path> <value>", description: "Write a config value" },
1999
2050
  { name: "unset <path>", description: "Delete a config value" },
@@ -2038,7 +2089,7 @@ export function showHelp(mode?: ICliMode): void {
2038
2089
  console.log(" cli Configure CLI behavior interactively");
2039
2090
  console.log(" release Configure release workflow interactively");
2040
2091
  console.log(" doctor Validate .smartconfig.json");
2041
- console.log(" fix [instructions] Use opencode to repair .smartconfig.json");
2092
+ console.log(" fix [instructions] Repair .smartconfig.json");
2042
2093
  console.log(" get <path> Read a single config value");
2043
2094
  console.log(" set <path> <value> Write a config value");
2044
2095
  console.log(" unset <path> Delete a config value");
@@ -1,6 +1,10 @@
1
1
  import * as plugins from './mod.plugins.js';
2
2
  import { FormatContext } from './classes.formatcontext.js';
3
- import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
3
+ import type {
4
+ IPlannedChange,
5
+ ICheckResult,
6
+ IFormatWarning,
7
+ } from './interfaces.format.js';
4
8
  import { Project } from '../classes.project.js';
5
9
  import { FormatStats } from './classes.formatstats.js';
6
10
 
@@ -19,6 +23,14 @@ export abstract class BaseFormatter {
19
23
  abstract analyze(): Promise<IPlannedChange[]>;
20
24
  abstract applyChange(change: IPlannedChange): Promise<void>;
21
25
 
26
+ get runsWithoutChanges(): boolean {
27
+ return false;
28
+ }
29
+
30
+ async validate(): Promise<IFormatWarning[]> {
31
+ return [];
32
+ }
33
+
22
34
  async execute(changes: IPlannedChange[]): Promise<void> {
23
35
  const startTime = this.stats.moduleStartTime(this.name);
24
36
  this.stats.startModule(this.name);
@@ -1,7 +1,11 @@
1
1
  import * as plugins from './mod.plugins.js';
2
2
  import { FormatContext } from './classes.formatcontext.js';
3
3
  import { BaseFormatter } from './classes.baseformatter.js';
4
- import type { IFormatPlan, IPlannedChange } from './interfaces.format.js';
4
+ import type {
5
+ IFormatPlan,
6
+ IPlannedChange,
7
+ IFormatWarning,
8
+ } from './interfaces.format.js';
5
9
  import { getModuleIcon } from './interfaces.format.js';
6
10
  import { logger } from '../gitzone.logging.js';
7
11
  import { DiffReporter } from './classes.diffreporter.js';
@@ -42,15 +46,21 @@ export class FormatPlanner {
42
46
  break;
43
47
  }
44
48
  }
49
+
50
+ const warnings = await module.validate();
51
+ plan.warnings.push(...warnings);
45
52
  } catch (error) {
53
+ const errorMessage = error instanceof Error ? error.message : String(error);
46
54
  plan.warnings.push({
47
55
  level: 'error',
48
- message: `Failed to analyze module ${module.name}: ${error.message}`,
56
+ message: `Failed to analyze module ${module.name}: ${errorMessage}`,
49
57
  module: module.name,
50
58
  });
51
59
  }
52
60
  }
53
61
 
62
+ plan.warnings.push(...this.detectConflictingChanges(plan.changes));
63
+
54
64
  plan.summary.totalFiles =
55
65
  plan.summary.filesAdded +
56
66
  plan.summary.filesModified +
@@ -65,11 +75,12 @@ export class FormatPlanner {
65
75
  context: FormatContext,
66
76
  ): Promise<void> {
67
77
  const startTime = Date.now();
78
+ const changesByModule = this.groupChangesByModule(plan.changes);
68
79
 
69
80
  for (const module of modules) {
70
- const changes = this.plannedChanges.get(module.name) || [];
81
+ const changes = changesByModule.get(module.name) || [];
71
82
 
72
- if (changes.length > 0) {
83
+ if (changes.length > 0 || module.runsWithoutChanges) {
73
84
  logger.log('info', `Executing ${module.name} formatter...`);
74
85
  await module.execute(changes);
75
86
  }
@@ -138,4 +149,55 @@ export class FormatPlanner {
138
149
  return '❌';
139
150
  }
140
151
  }
152
+
153
+ private groupChangesByModule(
154
+ changes: IPlannedChange[],
155
+ ): Map<string, IPlannedChange[]> {
156
+ const changesByModule = new Map<string, IPlannedChange[]>();
157
+ for (const change of changes) {
158
+ const moduleChanges = changesByModule.get(change.module) || [];
159
+ moduleChanges.push(change);
160
+ changesByModule.set(change.module, moduleChanges);
161
+ }
162
+ return changesByModule;
163
+ }
164
+
165
+ private detectConflictingChanges(
166
+ changes: IPlannedChange[],
167
+ ): IFormatWarning[] {
168
+ const warnings: IFormatWarning[] = [];
169
+ const changesByPath = new Map<string, IPlannedChange[]>();
170
+
171
+ for (const change of changes) {
172
+ if (!change.path || change.path === '<various files>') {
173
+ continue;
174
+ }
175
+
176
+ const pathChanges = changesByPath.get(change.path) || [];
177
+ pathChanges.push(change);
178
+ changesByPath.set(change.path, pathChanges);
179
+ }
180
+
181
+ for (const [path, pathChanges] of changesByPath) {
182
+ const modules = [...new Set(pathChanges.map((change) => change.module))];
183
+ if (modules.length < 2) {
184
+ continue;
185
+ }
186
+
187
+ const hasDelete = pathChanges.some((change) => change.type === 'delete');
188
+ const plannedContents = pathChanges
189
+ .map((change) => change.content)
190
+ .filter((content): content is string => content !== undefined);
191
+ const uniqueContents = new Set(plannedContents);
192
+ const level = hasDelete || uniqueContents.size > 1 ? 'warning' : 'info';
193
+
194
+ warnings.push({
195
+ level,
196
+ module: 'planner',
197
+ message: `Multiple formatters plan changes for ${path}: ${modules.join(', ')}. They will run in formatter order.`,
198
+ });
199
+ }
200
+
201
+ return warnings;
202
+ }
141
203
  }