@git.zone/cli 2.13.13 → 2.14.0

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 (39) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/gitzone.cli.js +46 -35
  3. package/dist_ts/helpers.climode.d.ts +22 -0
  4. package/dist_ts/helpers.climode.js +158 -0
  5. package/dist_ts/helpers.smartconfig.d.ts +11 -0
  6. package/dist_ts/helpers.smartconfig.js +139 -0
  7. package/dist_ts/mod_commit/index.d.ts +2 -0
  8. package/dist_ts/mod_commit/index.js +204 -92
  9. package/dist_ts/mod_config/index.d.ts +7 -2
  10. package/dist_ts/mod_config/index.js +390 -176
  11. package/dist_ts/mod_format/classes.formatcontext.d.ts +11 -2
  12. package/dist_ts/mod_format/classes.formatcontext.js +14 -4
  13. package/dist_ts/mod_format/formatters/packagejson.formatter.js +1 -67
  14. package/dist_ts/mod_format/formatters/smartconfig.formatter.d.ts +2 -7
  15. package/dist_ts/mod_format/formatters/smartconfig.formatter.js +57 -93
  16. package/dist_ts/mod_format/index.d.ts +4 -1
  17. package/dist_ts/mod_format/index.js +235 -67
  18. package/dist_ts/mod_format/mod.plugins.d.ts +1 -2
  19. package/dist_ts/mod_format/mod.plugins.js +2 -3
  20. package/dist_ts/mod_services/index.d.ts +2 -0
  21. package/dist_ts/mod_services/index.js +366 -168
  22. package/dist_ts/mod_standard/index.d.ts +3 -1
  23. package/dist_ts/mod_standard/index.js +159 -59
  24. package/package.json +1 -1
  25. package/readme.hints.md +25 -32
  26. package/readme.md +87 -65
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/gitzone.cli.ts +50 -38
  29. package/ts/helpers.climode.ts +212 -0
  30. package/ts/helpers.smartconfig.ts +192 -0
  31. package/ts/mod_commit/index.ts +325 -98
  32. package/ts/mod_config/index.ts +490 -182
  33. package/ts/mod_format/classes.formatcontext.ts +20 -3
  34. package/ts/mod_format/formatters/packagejson.formatter.ts +0 -91
  35. package/ts/mod_format/formatters/smartconfig.formatter.ts +67 -94
  36. package/ts/mod_format/index.ts +294 -81
  37. package/ts/mod_format/mod.plugins.ts +0 -2
  38. package/ts/mod_services/index.ts +550 -183
  39. package/ts/mod_standard/index.ts +191 -58
@@ -1,23 +1,60 @@
1
- import * as plugins from './mod.plugins.js';
2
- import { Project } from '../classes.project.js';
3
- import { FormatContext } from './classes.formatcontext.js';
4
- import { FormatPlanner } from './classes.formatplanner.js';
5
- import { BaseFormatter } from './classes.baseformatter.js';
6
- import { logger, setVerboseMode } from '../gitzone.logging.js';
7
-
8
- import { CleanupFormatter } from './formatters/cleanup.formatter.js';
9
- import { SmartconfigFormatter } from './formatters/smartconfig.formatter.js';
10
- import { LicenseFormatter } from './formatters/license.formatter.js';
11
- import { PackageJsonFormatter } from './formatters/packagejson.formatter.js';
12
- import { TemplatesFormatter } from './formatters/templates.formatter.js';
13
- import { GitignoreFormatter } from './formatters/gitignore.formatter.js';
14
- import { TsconfigFormatter } from './formatters/tsconfig.formatter.js';
15
- import { PrettierFormatter } from './formatters/prettier.formatter.js';
16
- import { ReadmeFormatter } from './formatters/readme.formatter.js';
17
- import { CopyFormatter } from './formatters/copy.formatter.js';
1
+ import * as plugins from "./mod.plugins.js";
2
+ import { Project } from "../classes.project.js";
3
+ import { FormatContext } from "./classes.formatcontext.js";
4
+ import { FormatPlanner } from "./classes.formatplanner.js";
5
+ import { BaseFormatter } from "./classes.baseformatter.js";
6
+ import { logger, setVerboseMode } from "../gitzone.logging.js";
7
+ import type { ICliMode } from "../helpers.climode.js";
8
+ import {
9
+ getCliMode,
10
+ printJson,
11
+ runWithSuppressedOutput,
12
+ } from "../helpers.climode.js";
13
+ import { getCliConfigValue } from "../helpers.smartconfig.js";
14
+
15
+ import { CleanupFormatter } from "./formatters/cleanup.formatter.js";
16
+ import { SmartconfigFormatter } from "./formatters/smartconfig.formatter.js";
17
+ import { LicenseFormatter } from "./formatters/license.formatter.js";
18
+ import { PackageJsonFormatter } from "./formatters/packagejson.formatter.js";
19
+ import { TemplatesFormatter } from "./formatters/templates.formatter.js";
20
+ import { GitignoreFormatter } from "./formatters/gitignore.formatter.js";
21
+ import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
22
+ import { PrettierFormatter } from "./formatters/prettier.formatter.js";
23
+ import { ReadmeFormatter } from "./formatters/readme.formatter.js";
24
+ import { CopyFormatter } from "./formatters/copy.formatter.js";
25
+
26
+ /**
27
+ * Rename npmextra.json or smartconfig.json to .smartconfig.json
28
+ * before any formatter tries to read config.
29
+ */
30
+ async function migrateConfigFile(allowWrite: boolean): Promise<void> {
31
+ const target = ".smartconfig.json";
32
+ const targetExists = await plugins.smartfs.file(target).exists();
33
+ if (targetExists) return;
34
+
35
+ for (const oldName of ["smartconfig.json", "npmextra.json"]) {
36
+ const exists = await plugins.smartfs.file(oldName).exists();
37
+ if (exists) {
38
+ if (!allowWrite) {
39
+ return;
40
+ }
41
+ const content = (await plugins.smartfs
42
+ .file(oldName)
43
+ .encoding("utf8")
44
+ .read()) as string;
45
+ await plugins.smartfs.file(`./${target}`).encoding("utf8").write(content);
46
+ await plugins.smartfs.file(oldName).delete();
47
+ logger.log("info", `Migrated ${oldName} to ${target}`);
48
+ return;
49
+ }
50
+ }
51
+ }
18
52
 
19
53
  // Shared formatter class map used by both run() and runFormatter()
20
- const formatterMap: Record<string, new (ctx: FormatContext, proj: Project) => BaseFormatter> = {
54
+ const formatterMap: Record<
55
+ string,
56
+ new (ctx: FormatContext, proj: Project) => BaseFormatter
57
+ > = {
21
58
  cleanup: CleanupFormatter,
22
59
  smartconfig: SmartconfigFormatter,
23
60
  license: LicenseFormatter,
@@ -31,7 +68,104 @@ const formatterMap: Record<string, new (ctx: FormatContext, proj: Project) => Ba
31
68
  };
32
69
 
33
70
  // Formatters that don't require projectType to be set
34
- const formattersNotRequiringProjectType = ['smartconfig', 'prettier', 'cleanup', 'packagejson'];
71
+ const formattersNotRequiringProjectType = [
72
+ "smartconfig",
73
+ "prettier",
74
+ "cleanup",
75
+ "packagejson",
76
+ ];
77
+
78
+ const getFormatConfig = async () => {
79
+ const rawFormatConfig = await getCliConfigValue<Record<string, any>>(
80
+ "format",
81
+ {},
82
+ );
83
+ return {
84
+ interactive: true,
85
+ showDiffs: false,
86
+ autoApprove: false,
87
+ showStats: true,
88
+ modules: {
89
+ skip: [],
90
+ only: [],
91
+ ...(rawFormatConfig.modules || {}),
92
+ },
93
+ ...rawFormatConfig,
94
+ };
95
+ };
96
+
97
+ const createActiveFormatters = async (options: {
98
+ interactive: boolean;
99
+ jsonOutput: boolean;
100
+ }) => {
101
+ const project = await Project.fromCwd({ requireProjectType: false });
102
+ const context = new FormatContext(options);
103
+ const planner = new FormatPlanner();
104
+
105
+ const formatConfig = await getFormatConfig();
106
+ const formatters = Object.entries(formatterMap).map(
107
+ ([, FormatterClass]) => new FormatterClass(context, project),
108
+ );
109
+
110
+ const activeFormatters = formatters.filter((formatter) => {
111
+ if (formatConfig.modules.only.length > 0) {
112
+ return formatConfig.modules.only.includes(formatter.name);
113
+ }
114
+ if (formatConfig.modules.skip.includes(formatter.name)) {
115
+ return false;
116
+ }
117
+ return true;
118
+ });
119
+
120
+ return {
121
+ context,
122
+ planner,
123
+ formatConfig,
124
+ activeFormatters,
125
+ };
126
+ };
127
+
128
+ const buildFormatPlan = async (options: {
129
+ fromPlan?: string;
130
+ interactive: boolean;
131
+ jsonOutput: boolean;
132
+ }) => {
133
+ const { context, planner, formatConfig, activeFormatters } =
134
+ await createActiveFormatters({
135
+ interactive: options.interactive,
136
+ jsonOutput: options.jsonOutput,
137
+ });
138
+
139
+ const plan = options.fromPlan
140
+ ? JSON.parse(
141
+ (await plugins.smartfs
142
+ .file(options.fromPlan)
143
+ .encoding("utf8")
144
+ .read()) as string,
145
+ )
146
+ : await planner.planFormat(activeFormatters);
147
+
148
+ return {
149
+ context,
150
+ planner,
151
+ formatConfig,
152
+ activeFormatters,
153
+ plan,
154
+ };
155
+ };
156
+
157
+ const serializePlan = (plan: any) => {
158
+ return {
159
+ summary: plan.summary,
160
+ warnings: plan.warnings,
161
+ changes: plan.changes.map((change: any) => ({
162
+ type: change.type,
163
+ path: change.path,
164
+ module: change.module,
165
+ description: change.description,
166
+ })),
167
+ };
168
+ };
35
169
 
36
170
  export let run = async (
37
171
  options: {
@@ -45,59 +179,61 @@ export let run = async (
45
179
  interactive?: boolean;
46
180
  verbose?: boolean;
47
181
  diff?: boolean;
182
+ [key: string]: any;
48
183
  } = {},
49
184
  ): Promise<any> => {
185
+ const mode = await getCliMode(options as any);
186
+ const subcommand = (options as any)?._?.[1];
187
+
188
+ if (mode.help || subcommand === "help") {
189
+ showHelp(mode);
190
+ return;
191
+ }
192
+
50
193
  if (options.verbose) {
51
194
  setVerboseMode(true);
52
195
  }
53
196
 
54
- const shouldWrite = options.write ?? (options.dryRun === false);
197
+ const shouldWrite = options.write ?? options.dryRun === false;
198
+ const treatAsPlan = subcommand === "plan";
55
199
 
56
- const project = await Project.fromCwd({ requireProjectType: false });
57
- const context = new FormatContext();
58
- const planner = new FormatPlanner();
200
+ if (mode.json && shouldWrite) {
201
+ printJson({
202
+ ok: false,
203
+ error:
204
+ "JSON output is only supported for read-only format planning. Use `gitzone format plan --json` or omit `--json` when applying changes.",
205
+ });
206
+ return;
207
+ }
59
208
 
60
- const smartconfigInstance = new plugins.smartconfig.Smartconfig();
61
- const formatConfig = smartconfigInstance.dataFor<any>('@git.zone/cli.format', {
62
- interactive: true,
63
- showDiffs: false,
64
- autoApprove: false,
65
- modules: {
66
- skip: [],
67
- only: [],
68
- },
69
- });
209
+ // Migrate config file before anything reads it
210
+ await migrateConfigFile(shouldWrite);
70
211
 
71
- const interactive = options.interactive ?? formatConfig.interactive;
212
+ const formatConfig = await getFormatConfig();
213
+ const interactive =
214
+ options.interactive ?? (mode.interactive && formatConfig.interactive);
72
215
  const autoApprove = options.yes ?? formatConfig.autoApprove;
73
216
 
74
217
  try {
75
- // Initialize formatters in execution order
76
- const formatters = Object.entries(formatterMap).map(
77
- ([, FormatterClass]) => new FormatterClass(context, project),
78
- );
79
-
80
- // Filter formatters based on configuration
81
- const activeFormatters = formatters.filter((formatter) => {
82
- if (formatConfig.modules.only.length > 0) {
83
- return formatConfig.modules.only.includes(formatter.name);
84
- }
85
- if (formatConfig.modules.skip.includes(formatter.name)) {
86
- return false;
87
- }
88
- return true;
89
- });
218
+ const planBuilder = async () => {
219
+ return await buildFormatPlan({
220
+ fromPlan: options.fromPlan,
221
+ interactive,
222
+ jsonOutput: mode.json,
223
+ });
224
+ };
90
225
 
91
- // Plan phase
92
- logger.log('info', 'Analyzing project for format operations...');
93
- let plan = options.fromPlan
94
- ? JSON.parse(
95
- (await plugins.smartfs
96
- .file(options.fromPlan)
97
- .encoding('utf8')
98
- .read()) as string,
99
- )
100
- : await planner.planFormat(activeFormatters);
226
+ if (!mode.json) {
227
+ logger.log("info", "Analyzing project for format operations...");
228
+ }
229
+ const { context, planner, activeFormatters, plan } = mode.json
230
+ ? await runWithSuppressedOutput(planBuilder)
231
+ : await planBuilder();
232
+
233
+ if (mode.json) {
234
+ printJson(serializePlan(plan));
235
+ return;
236
+ }
101
237
 
102
238
  // Display plan
103
239
  await planner.displayPlan(plan, options.detailed);
@@ -106,34 +242,35 @@ export let run = async (
106
242
  if (options.savePlan) {
107
243
  await plugins.smartfs
108
244
  .file(options.savePlan)
109
- .encoding('utf8')
245
+ .encoding("utf8")
110
246
  .write(JSON.stringify(plan, null, 2));
111
- logger.log('info', `Plan saved to ${options.savePlan}`);
247
+ logger.log("info", `Plan saved to ${options.savePlan}`);
112
248
  }
113
249
 
114
- if (options.planOnly) {
250
+ if (options.planOnly || treatAsPlan) {
115
251
  return;
116
252
  }
117
253
 
118
254
  // Show diffs if explicitly requested or before interactive write confirmation
119
- const showDiffs = options.diff || (shouldWrite && interactive && !autoApprove);
255
+ const showDiffs =
256
+ options.diff || (shouldWrite && interactive && !autoApprove);
120
257
  if (showDiffs) {
121
- logger.log('info', 'Showing file diffs:');
122
- console.log('');
258
+ logger.log("info", "Showing file diffs:");
259
+ console.log("");
123
260
 
124
261
  for (const formatter of activeFormatters) {
125
262
  const checkResult = await formatter.check();
126
263
  if (checkResult.hasDiff) {
127
- logger.log('info', `[${formatter.name}]`);
264
+ logger.log("info", `[${formatter.name}]`);
128
265
  formatter.displayAllDiffs(checkResult);
129
- console.log('');
266
+ console.log("");
130
267
  }
131
268
  }
132
269
  }
133
270
 
134
271
  // Dry-run mode (default behavior)
135
272
  if (!shouldWrite) {
136
- logger.log('info', 'Dry-run mode - use --write (-w) to apply changes');
273
+ logger.log("info", "Dry-run mode - use --write (-w) to apply changes");
137
274
  return;
138
275
  }
139
276
 
@@ -141,25 +278,25 @@ export let run = async (
141
278
  if (interactive && !autoApprove) {
142
279
  const interactInstance = new plugins.smartinteract.SmartInteract();
143
280
  const response = await interactInstance.askQuestion({
144
- type: 'confirm',
145
- name: 'proceed',
146
- message: 'Proceed with formatting?',
281
+ type: "confirm",
282
+ name: "proceed",
283
+ message: "Proceed with formatting?",
147
284
  default: true,
148
285
  });
149
286
 
150
287
  if (!(response as any).value) {
151
- logger.log('info', 'Format operation cancelled by user');
288
+ logger.log("info", "Format operation cancelled by user");
152
289
  return;
153
290
  }
154
291
  }
155
292
 
156
293
  // Execute phase
157
- logger.log('info', 'Executing format operations...');
294
+ logger.log("info", "Executing format operations...");
158
295
  await planner.executePlan(plan, activeFormatters, context);
159
296
 
160
297
  context.getFormatStats().finish();
161
298
 
162
- const showStats = smartconfigInstance.dataFor('gitzone.format.showStats', true);
299
+ const showStats = formatConfig.showStats ?? true;
163
300
  if (showStats) {
164
301
  context.getFormatStats().displayStats();
165
302
  }
@@ -169,14 +306,15 @@ export let run = async (
169
306
  await context.getFormatStats().saveReport(statsPath);
170
307
  }
171
308
 
172
- logger.log('success', 'Format operations completed successfully!');
309
+ logger.log("success", "Format operations completed successfully!");
173
310
  } catch (error) {
174
- logger.log('error', `Format operation failed: ${error.message}`);
311
+ const errorMessage = error instanceof Error ? error.message : String(error);
312
+ logger.log("error", `Format operation failed: ${errorMessage}`);
175
313
  throw error;
176
314
  }
177
315
  };
178
316
 
179
- import type { ICheckResult } from './interfaces.format.js';
317
+ import type { ICheckResult } from "./interfaces.format.js";
180
318
  export type { ICheckResult };
181
319
 
182
320
  /**
@@ -188,11 +326,12 @@ export const runFormatter = async (
188
326
  silent?: boolean;
189
327
  checkOnly?: boolean;
190
328
  showDiff?: boolean;
191
- } = {}
329
+ } = {},
192
330
  ): Promise<ICheckResult | void> => {
193
- const requireProjectType = !formattersNotRequiringProjectType.includes(formatterName);
331
+ const requireProjectType =
332
+ !formattersNotRequiringProjectType.includes(formatterName);
194
333
  const project = await Project.fromCwd({ requireProjectType });
195
- const context = new FormatContext();
334
+ const context = new FormatContext({ interactive: true, jsonOutput: false });
196
335
 
197
336
  const FormatterClass = formatterMap[formatterName];
198
337
  if (!FormatterClass) {
@@ -216,6 +355,80 @@ export const runFormatter = async (
216
355
  }
217
356
 
218
357
  if (!options.silent) {
219
- logger.log('success', `Formatter '${formatterName}' completed`);
358
+ logger.log("success", `Formatter '${formatterName}' completed`);
220
359
  }
221
360
  };
361
+
362
+ export function showHelp(mode?: ICliMode): void {
363
+ if (mode?.json) {
364
+ printJson({
365
+ command: "format",
366
+ usage: "gitzone format [plan] [options]",
367
+ description:
368
+ "Plans formatting changes by default and applies them only with --write.",
369
+ flags: [
370
+ { flag: "--write, -w", description: "Apply planned changes" },
371
+ {
372
+ flag: "--yes",
373
+ description: "Skip the interactive confirmation before writing",
374
+ },
375
+ {
376
+ flag: "--plan-only",
377
+ description: "Show the plan without applying changes",
378
+ },
379
+ {
380
+ flag: "--save-plan <file>",
381
+ description: "Write the format plan to a file",
382
+ },
383
+ {
384
+ flag: "--from-plan <file>",
385
+ description: "Load a previously saved plan",
386
+ },
387
+ {
388
+ flag: "--detailed",
389
+ description: "Show detailed diffs and save stats",
390
+ },
391
+ { flag: "--verbose", description: "Enable verbose logging" },
392
+ {
393
+ flag: "--diff",
394
+ description: "Show per-file diffs before applying changes",
395
+ },
396
+ { flag: "--json", description: "Emit a read-only format plan as JSON" },
397
+ ],
398
+ examples: [
399
+ "gitzone format",
400
+ "gitzone format plan --json",
401
+ "gitzone format --write --yes",
402
+ ],
403
+ });
404
+ return;
405
+ }
406
+
407
+ console.log("");
408
+ console.log("Usage: gitzone format [plan] [options]");
409
+ console.log("");
410
+ console.log(
411
+ "Plans formatting changes by default and applies them only with --write.",
412
+ );
413
+ console.log("");
414
+ console.log("Flags:");
415
+ console.log(" --write, -w Apply planned changes");
416
+ console.log(
417
+ " --yes Skip the interactive confirmation before writing",
418
+ );
419
+ console.log(" --plan-only Show the plan without applying changes");
420
+ console.log(" --save-plan <file> Write the format plan to a file");
421
+ console.log(" --from-plan <file> Load a previously saved plan");
422
+ console.log(" --detailed Show detailed diffs and save stats");
423
+ console.log(" --verbose Enable verbose logging");
424
+ console.log(
425
+ " --diff Show per-file diffs before applying changes",
426
+ );
427
+ console.log(" --json Emit a read-only format plan as JSON");
428
+ console.log("");
429
+ console.log("Examples:");
430
+ console.log(" gitzone format");
431
+ console.log(" gitzone format plan --json");
432
+ console.log(" gitzone format --write --yes");
433
+ console.log("");
434
+ }
@@ -5,7 +5,6 @@ import * as smartfile from '@push.rocks/smartfile';
5
5
  import * as smartinteract from '@push.rocks/smartinteract';
6
6
  import * as smartlegal from '@push.rocks/smartlegal';
7
7
  import * as smartobject from '@push.rocks/smartobject';
8
- import * as smartnpm from '@push.rocks/smartnpm';
9
8
  import * as smartconfig from '@push.rocks/smartconfig';
10
9
  import * as smartdiff from '@push.rocks/smartdiff';
11
10
  import * as smartscaf from '@push.rocks/smartscaf';
@@ -16,7 +15,6 @@ export {
16
15
  smartinteract,
17
16
  smartlegal,
18
17
  smartobject,
19
- smartnpm,
20
18
  smartconfig,
21
19
  smartdiff,
22
20
  smartscaf,