@git.zone/cli 2.19.0 → 2.19.2

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 (43) 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/dist_ts/mod_tools/classes.packagemanager.d.ts +36 -1
  22. package/dist_ts/mod_tools/classes.packagemanager.js +541 -34
  23. package/dist_ts/mod_tools/index.js +128 -26
  24. package/dist_ts/plugins.d.ts +2 -1
  25. package/dist_ts/plugins.js +3 -2
  26. package/package.json +1 -1
  27. package/readme.hints.md +10 -0
  28. package/readme.md +11 -0
  29. package/ts/00_commitinfo_data.ts +1 -1
  30. package/ts/gitzone.cli.ts +104 -144
  31. package/ts/helpers.climode.ts +36 -1
  32. package/ts/helpers.smartconfigmigrations.ts +37 -1
  33. package/ts/mod_config/index.ts +60 -9
  34. package/ts/mod_format/classes.baseformatter.ts +13 -1
  35. package/ts/mod_format/classes.formatplanner.ts +66 -4
  36. package/ts/mod_format/formatters/license.formatter.ts +43 -15
  37. package/ts/mod_format/formatters/prettier.formatter.ts +54 -66
  38. package/ts/mod_format/index.ts +289 -8
  39. package/ts/mod_format/interfaces.format.ts +8 -11
  40. package/ts/mod_standard/index.ts +1 -0
  41. package/ts/mod_tools/classes.packagemanager.ts +724 -34
  42. package/ts/mod_tools/index.ts +225 -45
  43. package/ts/plugins.ts +2 -0
@@ -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
  }
@@ -1,5 +1,5 @@
1
1
  import { BaseFormatter } from '../classes.baseformatter.js';
2
- import type { IPlannedChange } from '../interfaces.format.js';
2
+ import type { IFormatWarning, IPlannedChange } from '../interfaces.format.js';
3
3
  import * as plugins from '../mod.plugins.js';
4
4
  import * as paths from '../../paths.js';
5
5
  import { logger } from '../../gitzone.logging.js';
@@ -11,6 +11,10 @@ export class LicenseFormatter extends BaseFormatter {
11
11
  return 'license';
12
12
  }
13
13
 
14
+ get runsWithoutChanges(): boolean {
15
+ return true;
16
+ }
17
+
14
18
  async analyze(): Promise<IPlannedChange[]> {
15
19
  // License formatter only checks for incompatible licenses
16
20
  // It does not modify any files, so return empty array
@@ -18,29 +22,34 @@ export class LicenseFormatter extends BaseFormatter {
18
22
  return [];
19
23
  }
20
24
 
25
+ async validate(): Promise<IFormatWarning[]> {
26
+ const result = await this.checkLicenses();
27
+ if (!result || result.failingModules.length === 0) {
28
+ return [];
29
+ }
30
+
31
+ return [
32
+ {
33
+ level: 'error',
34
+ module: this.name,
35
+ message: `License check failed for ${result.failingModules.length} module(s): ${result.failingModules
36
+ .map((failedModule) => `${failedModule.name} (${failedModule.license})`)
37
+ .join(', ')}`,
38
+ },
39
+ ];
40
+ }
41
+
21
42
  async execute(changes: IPlannedChange[]): Promise<void> {
22
43
  const startTime = this.stats.moduleStartTime(this.name);
23
44
  this.stats.startModule(this.name);
24
45
 
25
46
  try {
26
- // Check if node_modules exists
27
- const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
28
- const nodeModulesExists = await plugins.smartfs
29
- .directory(nodeModulesPath)
30
- .exists();
31
-
32
- if (!nodeModulesExists) {
47
+ const licenseCheckResult = await this.checkLicenses();
48
+ if (!licenseCheckResult) {
33
49
  logger.log('warn', 'No node_modules found. Skipping license check');
34
50
  return;
35
51
  }
36
52
 
37
- // Run license check
38
- const licenseChecker = await plugins.smartlegal.createLicenseChecker();
39
- const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
40
- paths.cwd,
41
- INCOMPATIBLE_LICENSES,
42
- );
43
-
44
53
  if (licenseCheckResult.failingModules.length === 0) {
45
54
  logger.log('info', 'License check passed - no incompatible licenses found');
46
55
  } else {
@@ -59,4 +68,23 @@ export class LicenseFormatter extends BaseFormatter {
59
68
  async applyChange(change: IPlannedChange): Promise<void> {
60
69
  // No file changes for license formatter
61
70
  }
71
+
72
+ private async checkLicenses(): Promise<{
73
+ failingModules: Array<{ name: string; license: string }>;
74
+ } | undefined> {
75
+ const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
76
+ const nodeModulesExists = await plugins.smartfs
77
+ .directory(nodeModulesPath)
78
+ .exists();
79
+
80
+ if (!nodeModulesExists) {
81
+ return undefined;
82
+ }
83
+
84
+ const licenseChecker = await plugins.smartlegal.createLicenseChecker();
85
+ return await licenseChecker.excludeLicenseWithinPath(
86
+ paths.cwd,
87
+ INCOMPATIBLE_LICENSES,
88
+ );
89
+ }
62
90
  }
@@ -56,7 +56,8 @@ export class PrettierFormatter extends BaseFormatter {
56
56
  );
57
57
  allFiles.push(...filteredFiles);
58
58
  } catch (error) {
59
- logVerbose(`Skipping directory ${dir}: ${error.message}`);
59
+ const errorMessage = error instanceof Error ? error.message : String(error);
60
+ logVerbose(`Skipping directory ${dir}: ${errorMessage}`);
60
61
  }
61
62
  }
62
63
 
@@ -72,7 +73,8 @@ export class PrettierFormatter extends BaseFormatter {
72
73
  const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
73
74
  allFiles.push(...rootLevelFiles);
74
75
  } catch (error) {
75
- logVerbose(`Skipping pattern ${pattern}: ${error.message}`);
76
+ const errorMessage = error instanceof Error ? error.message : String(error);
77
+ logVerbose(`Skipping pattern ${pattern}: ${errorMessage}`);
76
78
  }
77
79
  }
78
80
 
@@ -89,20 +91,46 @@ export class PrettierFormatter extends BaseFormatter {
89
91
  }
90
92
  } catch (error) {
91
93
  // Skip files that can't be accessed
92
- logVerbose(`Skipping ${file} - cannot access: ${error.message}`);
94
+ const errorMessage = error instanceof Error ? error.message : String(error);
95
+ logVerbose(`Skipping ${file} - cannot access: ${errorMessage}`);
93
96
  }
94
97
  }
95
98
 
99
+ const prettier = await import('prettier');
100
+ const prettierConfig = await this.getPrettierConfig();
101
+
96
102
  for (const file of validFiles) {
97
- changes.push({
98
- type: 'modify',
99
- path: file,
100
- module: this.name,
101
- description: 'Format with Prettier',
102
- });
103
+ try {
104
+ const fileExt = plugins.path.extname(file).toLowerCase();
105
+ if (!fileExt) {
106
+ continue;
107
+ }
108
+
109
+ const content = (await plugins.smartfs
110
+ .file(file)
111
+ .encoding('utf8')
112
+ .read()) as string;
113
+ const formatted = await prettier.format(content, {
114
+ filepath: file,
115
+ ...prettierConfig,
116
+ });
117
+
118
+ if (formatted !== content) {
119
+ changes.push({
120
+ type: 'modify',
121
+ path: file,
122
+ module: this.name,
123
+ description: 'Format with Prettier',
124
+ content: formatted,
125
+ });
126
+ }
127
+ } catch (error) {
128
+ const errorMessage = error instanceof Error ? error.message : String(error);
129
+ logVerbose(`Skipping Prettier analysis for ${file}: ${errorMessage}`);
130
+ }
103
131
  }
104
132
 
105
- logger.log('info', `Found ${changes.length} files to format with Prettier`);
133
+ logger.log('info', `Found ${changes.length} files needing Prettier`);
106
134
  return changes;
107
135
  }
108
136
 
@@ -127,9 +155,10 @@ export class PrettierFormatter extends BaseFormatter {
127
155
  this.stats.recordFileOperation(this.name, change.type, true);
128
156
  } catch (error) {
129
157
  this.stats.recordFileOperation(this.name, change.type, false);
158
+ const errorMessage = error instanceof Error ? error.message : String(error);
130
159
  logger.log(
131
160
  'error',
132
- `Failed to format ${change.path}: ${error.message}`,
161
+ `Failed to format ${change.path}: ${errorMessage}`,
133
162
  );
134
163
  // Don't throw - continue with other files
135
164
  }
@@ -192,28 +221,32 @@ export class PrettierFormatter extends BaseFormatter {
192
221
  logVerbose(`No formatting changes for ${change.path}`);
193
222
  }
194
223
  } catch (prettierError) {
224
+ const prettierErrorMessage = prettierError instanceof Error
225
+ ? prettierError.message
226
+ : String(prettierError);
195
227
  // Check if it's a parser error
196
- if (
197
- prettierError.message &&
198
- prettierError.message.includes('No parser could be inferred')
199
- ) {
200
- logVerbose(`Skipping ${change.path} - ${prettierError.message}`);
228
+ if (prettierErrorMessage.includes('No parser could be inferred')) {
229
+ logVerbose(`Skipping ${change.path} - ${prettierErrorMessage}`);
201
230
  return; // Skip this file silently
202
231
  }
203
232
  throw prettierError;
204
233
  }
205
234
  } catch (error) {
235
+ const errorMessage = error instanceof Error ? error.message : String(error);
236
+ const errorStack = error instanceof Error ? error.stack : undefined;
206
237
  // Log the full error stack for debugging mkdir issues
207
- if (error.message && error.message.includes('mkdir')) {
238
+ if (errorMessage.includes('mkdir')) {
208
239
  logger.log(
209
240
  'error',
210
- `Failed to format ${change.path}: ${error.message}`,
241
+ `Failed to format ${change.path}: ${errorMessage}`,
211
242
  );
212
- logger.log('error', `Error stack: ${error.stack}`);
243
+ if (errorStack) {
244
+ logger.log('error', `Error stack: ${errorStack}`);
245
+ }
213
246
  } else {
214
247
  logger.log(
215
248
  'error',
216
- `Failed to format ${change.path}: ${error.message}`,
249
+ `Failed to format ${change.path}: ${errorMessage}`,
217
250
  );
218
251
  }
219
252
  throw error;
@@ -234,52 +267,7 @@ export class PrettierFormatter extends BaseFormatter {
234
267
  });
235
268
  }
236
269
 
237
- /**
238
- * Override check() to compute diffs on-the-fly by running prettier
239
- */
240
270
  async check(): Promise<ICheckResult> {
241
- const changes = await this.analyze();
242
- const diffs: ICheckResult['diffs'] = [];
243
-
244
- for (const change of changes) {
245
- if (change.type !== 'modify') continue;
246
-
247
- try {
248
- // Read current content
249
- const currentContent = (await plugins.smartfs
250
- .file(change.path)
251
- .encoding('utf8')
252
- .read()) as string;
253
-
254
- // Skip files without extension (prettier can't infer parser)
255
- const fileExt = plugins.path.extname(change.path).toLowerCase();
256
- if (!fileExt) continue;
257
-
258
- // Format with prettier to get what it would produce
259
- const prettier = await import('prettier');
260
- const formatted = await prettier.format(currentContent, {
261
- filepath: change.path,
262
- ...(await this.getPrettierConfig()),
263
- });
264
-
265
- // Only add to diffs if content differs
266
- if (formatted !== currentContent) {
267
- diffs.push({
268
- path: change.path,
269
- type: 'modify',
270
- before: currentContent,
271
- after: formatted,
272
- });
273
- }
274
- } catch (error) {
275
- // Skip files that can't be processed
276
- logVerbose(`Skipping diff for ${change.path}: ${error.message}`);
277
- }
278
- }
279
-
280
- return {
281
- hasDiff: diffs.length > 0,
282
- diffs,
283
- };
271
+ return await super.check();
284
272
  }
285
273
  }