@git.zone/cli 2.18.1 → 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 (41) hide show
  1. package/.smartconfig.json +2 -1
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/gitzone.cli.d.ts +1 -1
  4. package/dist_ts/gitzone.cli.js +100 -126
  5. package/dist_ts/helpers.climode.d.ts +2 -0
  6. package/dist_ts/helpers.climode.js +31 -2
  7. package/dist_ts/helpers.smartconfigmigrations.js +53 -4
  8. package/dist_ts/helpers.workflow.d.ts +12 -2
  9. package/dist_ts/helpers.workflow.js +9 -2
  10. package/dist_ts/mod_config/index.js +254 -29
  11. package/dist_ts/mod_format/classes.baseformatter.d.ts +3 -1
  12. package/dist_ts/mod_format/classes.baseformatter.js +7 -1
  13. package/dist_ts/mod_format/classes.formatplanner.d.ts +2 -0
  14. package/dist_ts/mod_format/classes.formatplanner.js +48 -4
  15. package/dist_ts/mod_format/formatters/license.formatter.d.ts +4 -1
  16. package/dist_ts/mod_format/formatters/license.formatter.js +32 -10
  17. package/dist_ts/mod_format/formatters/prettier.formatter.d.ts +0 -3
  18. package/dist_ts/mod_format/formatters/prettier.formatter.js +53 -62
  19. package/dist_ts/mod_format/index.d.ts +1 -1
  20. package/dist_ts/mod_format/index.js +237 -8
  21. package/dist_ts/mod_format/interfaces.format.d.ts +7 -11
  22. package/dist_ts/mod_format/interfaces.format.js +1 -1
  23. package/dist_ts/mod_release/index.js +50 -23
  24. package/dist_ts/mod_standard/index.js +2 -1
  25. package/package.json +1 -1
  26. package/readme.hints.md +10 -0
  27. package/readme.md +31 -4
  28. package/ts/00_commitinfo_data.ts +1 -1
  29. package/ts/gitzone.cli.ts +104 -144
  30. package/ts/helpers.climode.ts +36 -1
  31. package/ts/helpers.smartconfigmigrations.ts +57 -3
  32. package/ts/helpers.workflow.ts +20 -3
  33. package/ts/mod_config/index.ts +278 -29
  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_release/index.ts +46 -23
  41. package/ts/mod_standard/index.ts +1 -0
@@ -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
  }