@git.zone/cli 6.6.2 → 6.7.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 (50) hide show
  1. package/.smartconfig.json +2 -1
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/gitzone.cli.js +2 -2
  4. package/dist_ts/helpers.workflow.d.ts +3 -0
  5. package/dist_ts/helpers.workflow.js +14 -1
  6. package/dist_ts/mod_config/index.js +4 -1
  7. package/dist_ts/mod_deprecate/helpers.deprecation.d.ts +12 -0
  8. package/dist_ts/mod_deprecate/helpers.deprecation.js +87 -0
  9. package/dist_ts/mod_deprecate/index.d.ts +1 -1
  10. package/dist_ts/mod_deprecate/index.js +77 -40
  11. package/dist_ts/mod_format/classes.baseformatter.d.ts +6 -0
  12. package/dist_ts/mod_format/classes.baseformatter.js +61 -1
  13. package/dist_ts/mod_format/classes.formatplanner.js +14 -3
  14. package/dist_ts/mod_format/classes.formatstats.d.ts +4 -1
  15. package/dist_ts/mod_format/classes.formatstats.js +11 -1
  16. package/dist_ts/mod_format/formatters/readme.formatter.d.ts +2 -1
  17. package/dist_ts/mod_format/formatters/readme.formatter.js +80 -14
  18. package/dist_ts/mod_format/index.js +2 -1
  19. package/dist_ts/mod_format/interfaces.format.d.ts +6 -2
  20. package/dist_ts/mod_format/interfaces.format.js +1 -1
  21. package/dist_ts/mod_release/classes.releasejournal.d.ts +17 -3
  22. package/dist_ts/mod_release/classes.releasejournal.js +144 -12
  23. package/dist_ts/mod_release/helpers.npmartifact.d.ts +3 -1
  24. package/dist_ts/mod_release/helpers.npmartifact.js +64 -5
  25. package/dist_ts/mod_release/helpers.releasepublication.js +72 -20
  26. package/dist_ts/mod_release/index.d.ts +24 -0
  27. package/dist_ts/mod_release/index.js +82 -22
  28. package/dist_ts/plugins.d.ts +2 -1
  29. package/dist_ts/plugins.js +3 -2
  30. package/package.json +2 -2
  31. package/readme.md +86 -3
  32. package/ts/00_commitinfo_data.ts +1 -1
  33. package/ts/gitzone.cli.ts +1 -1
  34. package/ts/helpers.workflow.ts +17 -0
  35. package/ts/mod_config/index.ts +3 -0
  36. package/ts/mod_deprecate/helpers.deprecation.ts +151 -0
  37. package/ts/mod_deprecate/index.ts +92 -41
  38. package/ts/mod_format/classes.baseformatter.ts +71 -0
  39. package/ts/mod_format/classes.formatplanner.ts +16 -3
  40. package/ts/mod_format/classes.formatstats.ts +14 -1
  41. package/ts/mod_format/formatters/readme.formatter.ts +89 -14
  42. package/ts/mod_format/index.ts +1 -0
  43. package/ts/mod_format/interfaces.format.ts +7 -2
  44. package/ts/mod_release/classes.releasejournal.ts +219 -20
  45. package/ts/mod_release/helpers.npmartifact.ts +120 -23
  46. package/ts/mod_release/helpers.releasepublication.ts +131 -16
  47. package/ts/mod_release/index.ts +127 -30
  48. package/ts/plugins.ts +2 -0
  49. package/readme.hints.md +0 -596
  50. package/readme.plan.md +0 -176
@@ -39,6 +39,7 @@ export interface IReleaseGitTargetConfig {
39
39
 
40
40
  export interface IReleaseNpmTargetConfig {
41
41
  enabled?: boolean;
42
+ packageSource?: "root" | "tspublish";
42
43
  registries?: string[];
43
44
  accessLevel?: "public" | "private";
44
45
  alreadyPublished?: "success" | "error";
@@ -107,6 +108,7 @@ export interface IResolvedReleaseWorkflow {
107
108
  pushTags: boolean;
108
109
  npmEnabled: boolean;
109
110
  npmRegistries: string[];
111
+ npmPackageSource: "root" | "tspublish";
110
112
  npmAccessLevel: "public" | "private";
111
113
  npmAlreadyPublished: "success" | "error";
112
114
  dockerEnabled: boolean;
@@ -123,6 +125,7 @@ export interface IResolvedReleaseWorkflow {
123
125
 
124
126
  export interface IResolvedReleaseResumeConfiguration {
125
127
  gitRemote: string;
128
+ npmPackageSource: "root" | "tspublish";
126
129
  npmRegistries: string[];
127
130
  npmAccessLevel: "public" | "private";
128
131
  npmAlreadyPublished: "success" | "error";
@@ -581,6 +584,14 @@ export const resolveCommitWorkflow = async (
581
584
  };
582
585
  };
583
586
 
587
+ const resolveNpmPackageSource = (valueArg: unknown): "root" | "tspublish" => {
588
+ if (valueArg === undefined || valueArg === "root") return "root";
589
+ if (valueArg === "tspublish") return valueArg;
590
+ throw new Error(
591
+ "release.targets.npm.packageSource must be root or tspublish",
592
+ );
593
+ };
594
+
584
595
  export const resolveReleaseWorkflow = async (
585
596
  argvArg: any,
586
597
  ): Promise<IResolvedReleaseWorkflow> => {
@@ -690,6 +701,9 @@ export const resolveReleaseWorkflow = async (
690
701
  pushTags,
691
702
  npmEnabled,
692
703
  npmRegistries,
704
+ npmPackageSource: targets.includes("npm")
705
+ ? resolveNpmPackageSource(npmConfig.packageSource)
706
+ : "root",
693
707
  npmAccessLevel: npmConfig.accessLevel || "public",
694
708
  npmAlreadyPublished: npmConfig.alreadyPublished || "success",
695
709
  dockerEnabled,
@@ -759,6 +773,9 @@ export const resolveReleaseResumeConfiguration = async (optionsArg: {
759
773
  return {
760
774
  gitRemote,
761
775
  npmRegistries,
776
+ npmPackageSource: optionsArg.npm
777
+ ? resolveNpmPackageSource(npmConfig.packageSource)
778
+ : "root",
762
779
  npmAccessLevel,
763
780
  npmAlreadyPublished,
764
781
  };
@@ -2001,6 +2001,9 @@ async function validateNpmTarget(
2001
2001
  fix: "Use success or error.",
2002
2002
  });
2003
2003
  }
2004
+ if (npmTarget.packageSource !== undefined && !["root", "tspublish"].includes(npmTarget.packageSource)) {
2005
+ findings.push({ level: "error", message: `Invalid npm package source: ${npmTarget.packageSource}`, fix: "Use root or tspublish." });
2006
+ }
2004
2007
 
2005
2008
  const smartNetwork = new plugins.smartnetwork.SmartNetwork();
2006
2009
  await Promise.all(
@@ -0,0 +1,151 @@
1
+ import * as plugins from "./mod.plugins.js";
2
+ import { normalizeNpmRegistryUrl } from "../mod_release/helpers.npmartifact.js";
3
+
4
+ export interface IDeprecationPlan {
5
+ packageName: string;
6
+ replacement: string;
7
+ message: string;
8
+ registries: string[];
9
+ }
10
+
11
+ export const createDeprecationPlan = (
12
+ options: Omit<IDeprecationPlan, "message"> & { message?: string },
13
+ ): IDeprecationPlan => {
14
+ const namePattern = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
15
+ if (
16
+ typeof options.packageName !== "string" ||
17
+ typeof options.replacement !== "string" ||
18
+ !namePattern.test(options.packageName) ||
19
+ !namePattern.test(options.replacement) ||
20
+ options.packageName === options.replacement
21
+ )
22
+ throw new Error(
23
+ "Deprecation requires distinct valid package and replacement names.",
24
+ );
25
+ if (!Array.isArray(options.registries))
26
+ throw new Error("Invalid deprecation registries.");
27
+ const registries = options.registries.map(normalizeNpmRegistryUrl);
28
+ if (!registries.length || new Set(registries).size !== registries.length)
29
+ throw new Error("Deprecation requires unique explicit registries.");
30
+ const message =
31
+ options.message ??
32
+ `Use ${options.replacement}. ${options.packageName} is deprecated.`;
33
+ if (
34
+ typeof message !== "string" ||
35
+ !message.trim() ||
36
+ message.length > 2000 ||
37
+ /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(message)
38
+ ) {
39
+ throw new Error("Invalid deprecation message.");
40
+ }
41
+ return {
42
+ packageName: options.packageName,
43
+ replacement: options.replacement,
44
+ registries,
45
+ message,
46
+ };
47
+ };
48
+
49
+ interface IPackageMetadata {
50
+ name: string;
51
+ versions: Record<string, { deprecated?: string }>;
52
+ "dist-tags"?: Record<string, string>;
53
+ }
54
+
55
+ const readMetadata = async (
56
+ registry: string,
57
+ packageName: string,
58
+ fetchImplementation: typeof fetch,
59
+ ): Promise<IPackageMetadata> => {
60
+ const response = await fetchImplementation(
61
+ `${registry}/${encodeURIComponent(packageName)}`,
62
+ {
63
+ method: "GET",
64
+ redirect: "error",
65
+ signal: AbortSignal.timeout(30_000),
66
+ headers: { accept: "application/json" },
67
+ },
68
+ );
69
+ if (!response.ok)
70
+ throw new Error(
71
+ `Cannot verify ${packageName} on ${registry} (HTTP ${response.status}).`,
72
+ );
73
+ const metadata = await response.json();
74
+ if (
75
+ !metadata ||
76
+ metadata.name !== packageName ||
77
+ !metadata.versions ||
78
+ typeof metadata.versions !== "object" ||
79
+ Array.isArray(metadata.versions) ||
80
+ !Object.keys(metadata.versions).length ||
81
+ Object.values(metadata.versions).some(
82
+ (version) =>
83
+ !version || typeof version !== "object" || Array.isArray(version),
84
+ )
85
+ ) {
86
+ throw new Error(
87
+ `Invalid registry metadata for ${packageName} on ${registry}.`,
88
+ );
89
+ }
90
+ return metadata;
91
+ };
92
+
93
+ /** Verify replacements everywhere before changing metadata for any old package. */
94
+ export const executeDeprecation = async (
95
+ planArg: IDeprecationPlan,
96
+ shell: plugins.smartshell.Smartshell,
97
+ cwd: string,
98
+ fetchImplementation: typeof fetch = fetch,
99
+ ): Promise<void> => {
100
+ const plan = createDeprecationPlan(planArg);
101
+ for (const registry of plan.registries) {
102
+ const replacement = await readMetadata(
103
+ registry,
104
+ plan.replacement,
105
+ fetchImplementation,
106
+ );
107
+ const latestTag = replacement["dist-tags"]?.latest;
108
+ const latest =
109
+ typeof latestTag === "string"
110
+ ? replacement.versions[latestTag]
111
+ : undefined;
112
+ if (!latest || latest.deprecated)
113
+ throw new Error(
114
+ `Replacement ${plan.replacement} has no active latest version on ${registry}.`,
115
+ );
116
+ await readMetadata(registry, plan.packageName, fetchImplementation);
117
+ }
118
+ for (const registry of plan.registries) {
119
+ const result = await shell.execSpawn(
120
+ "pnpm",
121
+ [
122
+ "deprecate",
123
+ `${plan.packageName}@*`,
124
+ plan.message,
125
+ "--registry",
126
+ registry,
127
+ ],
128
+ {
129
+ cwd,
130
+ timeout: 120_000,
131
+ timeoutKillGraceMs: 5_000,
132
+ },
133
+ );
134
+ if (result.exitCode !== 0)
135
+ throw new Error(`Deprecation failed on ${registry}.`);
136
+ const metadata = await readMetadata(
137
+ registry,
138
+ plan.packageName,
139
+ fetchImplementation,
140
+ );
141
+ const versions = Object.values(metadata.versions);
142
+ if (
143
+ !versions.length ||
144
+ versions.some((version) => version.deprecated !== plan.message)
145
+ ) {
146
+ throw new Error(
147
+ `Deprecation is not yet verified for every version on ${registry}; rerunning the same deprecation is safe.`,
148
+ );
149
+ }
150
+ }
151
+ };
@@ -1,52 +1,103 @@
1
- import * as plugins from './mod.plugins.js';
1
+ import * as plugins from "./mod.plugins.js";
2
2
 
3
- import { logger } from '../gitzone.logging.js';
3
+ import { logger } from "../gitzone.logging.js";
4
+ import { getCliMode, printJson } from "../helpers.climode.js";
5
+ import {
6
+ createDeprecationPlan,
7
+ executeDeprecation,
8
+ } from "./helpers.deprecation.js";
4
9
 
5
- export const run = async () => {
6
- const smartInteract = new plugins.smartinteract.SmartInteract([
7
- {
8
- name: `registryUrls`,
9
- message: `What are the comma separated registry URLs?`,
10
- type: `input`,
11
- default: `https://registry.npmjs.org`,
12
- validate: (stringInput) => {
13
- return stringInput !== '' && !process.env.CI;
10
+ export const run = async (argvArg: any = {}) => {
11
+ const mode = await getCliMode(argvArg);
12
+ if (mode.help) {
13
+ console.log(
14
+ "gitzone deprecate --package <old> --replacement <new> --registries <url,url> [--message <text>] [--plan] [-y]",
15
+ );
16
+ console.log(
17
+ "Deprecates all versions after verifying the replacement on every registry.",
18
+ );
19
+ return;
20
+ }
21
+ let registryInput = argvArg.registries;
22
+ let oldPackageName = argvArg.package;
23
+ let newPackageName = argvArg.replacement;
24
+ if (!registryInput || !oldPackageName || !newPackageName) {
25
+ if (!mode.interactive || argvArg.plan || mode.json)
26
+ throw new Error("Specify --package, --replacement, and --registries.");
27
+ const smartInteract = new plugins.smartinteract.SmartInteract([
28
+ {
29
+ name: `registryUrls`,
30
+ message: `What are the comma separated registry URLs?`,
31
+ type: `input`,
32
+ default: `https://registry.npmjs.org`,
33
+ validate: (stringInput) => {
34
+ return stringInput !== "" && !process.env.CI;
35
+ },
14
36
  },
15
- },
16
- {
17
- name: `oldPackageName`,
18
- message: `Whats the name of the OLD package?`,
19
- type: `input`,
20
- default: ``,
21
- validate: (stringInput) => {
22
- return stringInput !== '' && !process.env.CI;
37
+ {
38
+ name: `oldPackageName`,
39
+ message: `Whats the name of the OLD package?`,
40
+ type: `input`,
41
+ default: ``,
42
+ validate: (stringInput) => {
43
+ return stringInput !== "" && !process.env.CI;
44
+ },
23
45
  },
24
- },
25
- {
26
- name: `newPackageName`,
27
- message: `Whats the name of the NEW package?`,
28
- type: `input`,
29
- default: ``,
30
- validate: (stringInput) => {
31
- return stringInput !== '' && !process.env.CI;
46
+ {
47
+ name: `newPackageName`,
48
+ message: `Whats the name of the NEW package?`,
49
+ type: `input`,
50
+ default: ``,
51
+ validate: (stringInput) => {
52
+ return stringInput !== "" && !process.env.CI;
53
+ },
32
54
  },
33
- },
34
- ]);
35
- const answerBucket = await smartInteract.runQueue();
36
- const registryUrls = answerBucket.getAnswerFor(`registryUrls`).split(',');
37
- const oldPackageName = answerBucket.getAnswerFor(`oldPackageName`);
38
- const newPackageName = answerBucket.getAnswerFor(`newPackageName`);
55
+ ]);
56
+ const answerBucket = await smartInteract.runQueue();
57
+ registryInput = answerBucket.getAnswerFor("registryUrls");
58
+ oldPackageName = answerBucket.getAnswerFor("oldPackageName");
59
+ newPackageName = answerBucket.getAnswerFor("newPackageName");
60
+ }
61
+ if (
62
+ typeof registryInput !== "string" ||
63
+ typeof oldPackageName !== "string" ||
64
+ typeof newPackageName !== "string"
65
+ ) {
66
+ throw new Error("Deprecation arguments must be strings.");
67
+ }
68
+ const plan = createDeprecationPlan({
69
+ packageName: oldPackageName,
70
+ replacement: newPackageName,
71
+ registries: registryInput.split(",").map((value) => value.trim()),
72
+ message: argvArg.message,
73
+ });
74
+ if (mode.json) {
75
+ printJson({ ok: true, plan });
76
+ return;
77
+ }
78
+ console.log(JSON.stringify(plan, null, 2));
79
+ if (argvArg.plan) return;
80
+ if (!mode.yes) {
81
+ if (!mode.interactive)
82
+ throw new Error("Deprecation requires -y or interactive confirmation.");
83
+ if (
84
+ !(await plugins.smartinteract.SmartInteract.getCliConfirmation(
85
+ `Deprecate every version of ${oldPackageName}?`,
86
+ false,
87
+ ))
88
+ )
89
+ return;
90
+ }
39
91
  logger.log(
40
- 'info',
92
+ "info",
41
93
  `Deprecating package ${oldPackageName} in favour of ${newPackageName}`,
42
94
  );
43
95
  const smartshellInstance = new plugins.smartshell.Smartshell({
44
- executor: 'bash',
96
+ executor: "bash",
45
97
  });
46
- for (const registryUrl of registryUrls) {
47
- await smartshellInstance.exec(
48
- `npm deprecate ${oldPackageName}@* ` +
49
- `"${oldPackageName} has been deprecated in favour of ${newPackageName} - please upgrade asap!!!" --registry ${registryUrl}`,
50
- );
51
- }
98
+ await executeDeprecation(plan, smartshellInstance, process.cwd());
99
+ logger.log(
100
+ "ok",
101
+ `Verified deprecation on ${plan.registries.length} registries.`,
102
+ );
52
103
  };
@@ -124,6 +124,60 @@ export abstract class BaseFormatter {
124
124
  await plugins.smartfs.file(filepath).delete();
125
125
  }
126
126
 
127
+ /**
128
+ * Rename a file, preserving git history where the repository tracks it.
129
+ * Uses `git mv` first and only falls back to a plain filesystem rename when
130
+ * git refuses (untracked file, no repository, ...).
131
+ */
132
+ protected async renameFile(fromPath: string, toPath: string): Promise<void> {
133
+ if (!fromPath || !toPath || fromPath === toPath) {
134
+ throw new Error(`Invalid rename from "${fromPath}" to "${toPath}"`);
135
+ }
136
+
137
+ const sourceExists = await plugins.smartfs.file(fromPath).exists();
138
+ if (!sourceExists) {
139
+ throw new Error(`Cannot rename ${fromPath}: file does not exist`);
140
+ }
141
+
142
+ const destinationExists = await plugins.smartfs.file(toPath).exists();
143
+ if (destinationExists) {
144
+ throw new Error(
145
+ `Cannot rename ${fromPath} to ${toPath}: destination already exists`,
146
+ );
147
+ }
148
+
149
+ const destinationDir = plugins.path.dirname(toPath);
150
+ if (destinationDir && destinationDir !== '.') {
151
+ await plugins.smartfs.directory(destinationDir).recursive().create();
152
+ }
153
+
154
+ let gitRenamed = false;
155
+ try {
156
+ const smartshellInstance = new plugins.smartshell.Smartshell({
157
+ executor: 'bash',
158
+ sourceFilePaths: [],
159
+ });
160
+ const result = await smartshellInstance.execSpawn(
161
+ 'git',
162
+ ['mv', fromPath, toPath],
163
+ { silent: true, timeout: 30_000, timeoutKillGraceMs: 5_000 },
164
+ );
165
+ gitRenamed = result.exitCode === 0;
166
+ } catch {
167
+ gitRenamed = false;
168
+ }
169
+
170
+ if (!gitRenamed) {
171
+ // A killed or timed out `git mv` may still have moved the file.
172
+ const movedAnyway =
173
+ !(await plugins.smartfs.file(fromPath).exists()) &&
174
+ (await plugins.smartfs.file(toPath).exists());
175
+ if (!movedAnyway) {
176
+ await plugins.fs.rename(fromPath, toPath);
177
+ }
178
+ }
179
+ }
180
+
127
181
  /**
128
182
  * Check for diffs without applying changes
129
183
  */
@@ -172,6 +226,19 @@ export abstract class BaseFormatter {
172
226
  } catch {
173
227
  // File doesn't exist, nothing to delete
174
228
  }
229
+ } else if (change.type === 'rename') {
230
+ if (!change.fromPath) {
231
+ continue;
232
+ }
233
+ const sourceExists = await plugins.smartfs.file(change.fromPath).exists();
234
+ if (!sourceExists) {
235
+ continue;
236
+ }
237
+ diffs.push({
238
+ path: change.path,
239
+ type: 'rename',
240
+ fromPath: change.fromPath,
241
+ });
175
242
  }
176
243
  }
177
244
 
@@ -183,6 +250,10 @@ export abstract class BaseFormatter {
183
250
 
184
251
  displayDiff(diff: ICheckResult['diffs'][0]): void {
185
252
  console.log(`\n--- ${diff.path}`);
253
+ if (diff.type === 'rename') {
254
+ console.log(` (file will be renamed from ${diff.fromPath})`);
255
+ return;
256
+ }
186
257
  if (diff.before !== undefined && diff.after !== undefined) {
187
258
  console.log(plugins.smartdiff.formatUnifiedDiffForConsole(diff.before, diff.after, {
188
259
  originalFileName: diff.path,
@@ -5,6 +5,7 @@ import type {
5
5
  IFormatPlan,
6
6
  IPlannedChange,
7
7
  IFormatWarning,
8
+ TPlannedChangeType,
8
9
  } from './interfaces.format.js';
9
10
  import { getModuleIcon } from './interfaces.format.js';
10
11
  import { logger } from '../gitzone.logging.js';
@@ -21,6 +22,7 @@ export class FormatPlanner {
21
22
  filesAdded: 0,
22
23
  filesModified: 0,
23
24
  filesRemoved: 0,
25
+ filesRenamed: 0,
24
26
  },
25
27
  changes: [],
26
28
  warnings: [],
@@ -44,6 +46,9 @@ export class FormatPlanner {
44
46
  case 'delete':
45
47
  plan.summary.filesRemoved++;
46
48
  break;
49
+ case 'rename':
50
+ plan.summary.filesRenamed++;
51
+ break;
47
52
  }
48
53
  }
49
54
 
@@ -64,7 +69,8 @@ export class FormatPlanner {
64
69
  plan.summary.totalFiles =
65
70
  plan.summary.filesAdded +
66
71
  plan.summary.filesModified +
67
- plan.summary.filesRemoved;
72
+ plan.summary.filesRemoved +
73
+ plan.summary.filesRenamed;
68
74
 
69
75
  return plan;
70
76
  }
@@ -100,6 +106,7 @@ export class FormatPlanner {
100
106
  console.log(` • ${plan.summary.filesAdded} new files`);
101
107
  console.log(` • ${plan.summary.filesModified} modified files`);
102
108
  console.log(` • ${plan.summary.filesRemoved} deleted files`);
109
+ console.log(` • ${plan.summary.filesRenamed} renamed files`);
103
110
  console.log('');
104
111
  console.log('Changes by module:');
105
112
 
@@ -117,7 +124,11 @@ export class FormatPlanner {
117
124
 
118
125
  for (const change of changes) {
119
126
  const icon = this.getChangeIcon(change.type);
120
- console.log(` ${icon} ${change.path} - ${change.description}`);
127
+ const pathLabel =
128
+ change.type === 'rename' && change.fromPath
129
+ ? `${change.fromPath} -> ${change.path}`
130
+ : change.path;
131
+ console.log(` ${icon} ${pathLabel} - ${change.description}`);
121
132
 
122
133
  if (detailed && change.type === 'modify') {
123
134
  const diff = await this.diffReporter.generateDiffForChange(change);
@@ -139,7 +150,7 @@ export class FormatPlanner {
139
150
  console.log('\n' + '━'.repeat(50));
140
151
  }
141
152
 
142
- private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
153
+ private getChangeIcon(type: TPlannedChangeType): string {
143
154
  switch (type) {
144
155
  case 'create':
145
156
  return '✅';
@@ -147,6 +158,8 @@ export class FormatPlanner {
147
158
  return '✏️';
148
159
  case 'delete':
149
160
  return '❌';
161
+ case 'rename':
162
+ return '🔁';
150
163
  }
151
164
  }
152
165
 
@@ -1,6 +1,7 @@
1
1
  import * as plugins from './mod.plugins.js';
2
2
  import { logger } from '../gitzone.logging.js';
3
3
  import { getModuleIcon } from './interfaces.format.js';
4
+ import type { TPlannedChangeType } from './interfaces.format.js';
4
5
 
5
6
  export interface IModuleStats {
6
7
  name: string;
@@ -11,6 +12,7 @@ export interface IModuleStats {
11
12
  filesCreated: number;
12
13
  filesModified: number;
13
14
  filesDeleted: number;
15
+ filesRenamed: number;
14
16
  }
15
17
 
16
18
  export interface IFormatStats {
@@ -23,6 +25,7 @@ export interface IFormatStats {
23
25
  totalCreated: number;
24
26
  totalModified: number;
25
27
  totalDeleted: number;
28
+ totalRenamed: number;
26
29
  totalErrors: number;
27
30
  };
28
31
  }
@@ -41,6 +44,7 @@ export class FormatStats {
41
44
  totalCreated: 0,
42
45
  totalModified: 0,
43
46
  totalDeleted: 0,
47
+ totalRenamed: 0,
44
48
  totalErrors: 0,
45
49
  },
46
50
  };
@@ -56,6 +60,7 @@ export class FormatStats {
56
60
  filesCreated: 0,
57
61
  filesModified: 0,
58
62
  filesDeleted: 0,
63
+ filesRenamed: 0,
59
64
  });
60
65
  }
61
66
 
@@ -72,7 +77,7 @@ export class FormatStats {
72
77
 
73
78
  recordFileOperation(
74
79
  moduleName: string,
75
- operation: 'create' | 'modify' | 'delete',
80
+ operation: TPlannedChangeType,
76
81
  success: boolean = true,
77
82
  ): void {
78
83
  const moduleStats = this.stats.moduleStats.get(moduleName);
@@ -97,6 +102,10 @@ export class FormatStats {
97
102
  moduleStats.filesDeleted++;
98
103
  this.stats.overallStats.totalDeleted++;
99
104
  break;
105
+ case 'rename':
106
+ moduleStats.filesRenamed++;
107
+ this.stats.overallStats.totalRenamed++;
108
+ break;
100
109
  }
101
110
  } else {
102
111
  moduleStats.errors++;
@@ -122,6 +131,7 @@ export class FormatStats {
122
131
  console.log(` • Created: ${this.stats.overallStats.totalCreated}`);
123
132
  console.log(` • Modified: ${this.stats.overallStats.totalModified}`);
124
133
  console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
134
+ console.log(` • Renamed: ${this.stats.overallStats.totalRenamed}`);
125
135
  console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
126
136
 
127
137
  // Module stats
@@ -150,6 +160,9 @@ export class FormatStats {
150
160
  if (moduleStats.filesDeleted > 0) {
151
161
  console.log(` • Deleted: ${moduleStats.filesDeleted}`);
152
162
  }
163
+ if (moduleStats.filesRenamed > 0) {
164
+ console.log(` • Renamed: ${moduleStats.filesRenamed}`);
165
+ }
153
166
 
154
167
  if (moduleStats.errors > 0) {
155
168
  console.log(` ❌ Errors: ${moduleStats.errors}`);