@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
@@ -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
  }
@@ -22,6 +22,7 @@ import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
22
22
  import { PrettierFormatter } from "./formatters/prettier.formatter.js";
23
23
  import { ReadmeFormatter } from "./formatters/readme.formatter.js";
24
24
  import { CopyFormatter } from "./formatters/copy.formatter.js";
25
+ import type { ICheckResult, IFormatPlan } from "./interfaces.format.js";
25
26
 
26
27
  /**
27
28
  * Rename npmextra.json or smartconfig.json to .smartconfig.json
@@ -94,9 +95,39 @@ const getFormatConfig = async () => {
94
95
  };
95
96
  };
96
97
 
98
+ const normalizeModuleList = (value: unknown): string[] => {
99
+ if (Array.isArray(value)) {
100
+ return value.flatMap((item) => normalizeModuleList(item));
101
+ }
102
+ if (typeof value !== "string") {
103
+ return [];
104
+ }
105
+ return value
106
+ .split(",")
107
+ .map((item) => item.trim())
108
+ .filter(Boolean);
109
+ };
110
+
111
+ const getPlanStatus = (plan: IFormatPlan) => {
112
+ const errorWarnings = plan.warnings.filter(
113
+ (warning) => warning.level === "error",
114
+ );
115
+ const hasChanges = plan.summary.totalFiles > 0;
116
+ const hasErrors = errorWarnings.length > 0;
117
+
118
+ return {
119
+ ok: !hasChanges && !hasErrors,
120
+ hasChanges,
121
+ hasErrors,
122
+ errorCount: errorWarnings.length,
123
+ };
124
+ };
125
+
97
126
  const createActiveFormatters = async (options: {
98
127
  interactive: boolean;
99
128
  jsonOutput: boolean;
129
+ only?: string[];
130
+ skip?: string[];
100
131
  }) => {
101
132
  const project = await Project.fromCwd({ requireProjectType: false });
102
133
  const context = new FormatContext(options);
@@ -107,11 +138,19 @@ const createActiveFormatters = async (options: {
107
138
  ([, FormatterClass]) => new FormatterClass(context, project),
108
139
  );
109
140
 
141
+ const onlyModules = options.only?.length
142
+ ? options.only
143
+ : formatConfig.modules.only;
144
+ const skipModules = [
145
+ ...formatConfig.modules.skip,
146
+ ...(options.skip || []),
147
+ ];
148
+
110
149
  const activeFormatters = formatters.filter((formatter) => {
111
- if (formatConfig.modules.only.length > 0) {
112
- return formatConfig.modules.only.includes(formatter.name);
150
+ if (onlyModules.length > 0) {
151
+ return onlyModules.includes(formatter.name);
113
152
  }
114
- if (formatConfig.modules.skip.includes(formatter.name)) {
153
+ if (skipModules.includes(formatter.name)) {
115
154
  return false;
116
155
  }
117
156
  return true;
@@ -129,11 +168,15 @@ const buildFormatPlan = async (options: {
129
168
  fromPlan?: string;
130
169
  interactive: boolean;
131
170
  jsonOutput: boolean;
171
+ only?: string[];
172
+ skip?: string[];
132
173
  }) => {
133
174
  const { context, planner, formatConfig, activeFormatters } =
134
175
  await createActiveFormatters({
135
176
  interactive: options.interactive,
136
177
  jsonOutput: options.jsonOutput,
178
+ only: options.only,
179
+ skip: options.skip,
137
180
  });
138
181
 
139
182
  const plan = options.fromPlan
@@ -167,6 +210,182 @@ const serializePlan = (plan: any) => {
167
210
  };
168
211
  };
169
212
 
213
+ const buildFormatFixPrompt = (
214
+ plan: IFormatPlan,
215
+ extraInstructions: string,
216
+ ): string => {
217
+ const promptParts = [
218
+ "Other /c-* commands can be found at ~/.config/opencode/commands/*",
219
+ "# gitzone format fix",
220
+ "",
221
+ `Working directory: ${process.cwd()}`,
222
+ "",
223
+ "Repair project formatting so `gitzone format check --json` passes.",
224
+ "",
225
+ "Rules:",
226
+ "- Read `.smartconfig.json`, `package.json`, `tsconfig.json`, and the current format plan before editing.",
227
+ "- Prefer deterministic gitzone standards, bundled assets, and existing project conventions.",
228
+ "- Keep changes focused on formatting, metadata normalization, templates, and config consistency.",
229
+ "- Do not commit, release, install dependencies, or modify unrelated files.",
230
+ "- Use pnpm commands only if commands are needed.",
231
+ "- Run `gitzone format --write --yes` after changes.",
232
+ "- Run `gitzone format check --json` after changes and keep fixing until it passes.",
233
+ "- Run `git diff --check` after changes to catch whitespace problems.",
234
+ "",
235
+ "Current format plan:",
236
+ JSON.stringify(serializePlan(plan), null, 2),
237
+ ];
238
+
239
+ if (extraInstructions) {
240
+ promptParts.push("", "Additional user instructions:", extraInstructions);
241
+ }
242
+
243
+ return promptParts.join("\n");
244
+ };
245
+
246
+ const handleFormatFix = async (
247
+ options: Record<string, any>,
248
+ mode: ICliMode,
249
+ ): Promise<void> => {
250
+ if (mode.json) {
251
+ printJson({
252
+ ok: false,
253
+ error:
254
+ "JSON output is not supported for `gitzone format fix`. Use `gitzone format check --json` for machine-readable diagnostics.",
255
+ });
256
+ process.exitCode = 1;
257
+ return;
258
+ }
259
+
260
+ const extraInstructions = (options._?.slice(2).join(" ") || "").trim();
261
+ const force = Boolean(options.force);
262
+ const autoApprove = Boolean(options.yes || mode.yes);
263
+ const formatConfig = await getFormatConfig();
264
+ const interactive =
265
+ options.interactive ?? (mode.interactive && formatConfig.interactive);
266
+ const only = normalizeModuleList(options.only);
267
+ const skip = normalizeModuleList(options.skip);
268
+
269
+ const buildCurrentPlan = async () => {
270
+ return await buildFormatPlan({
271
+ interactive,
272
+ jsonOutput: false,
273
+ only,
274
+ skip,
275
+ });
276
+ };
277
+
278
+ logger.log("info", "Analyzing project for format fixes...");
279
+ let { plan } = await buildCurrentPlan();
280
+ let status = getPlanStatus(plan);
281
+
282
+ if (status.ok && !extraInstructions && !force) {
283
+ logger.log(
284
+ "success",
285
+ "Format check found no issues. Use `gitzone format fix --force` to run opencode anyway.",
286
+ );
287
+ return;
288
+ }
289
+
290
+ if (!autoApprove) {
291
+ if (!mode.interactive) {
292
+ throw new Error(
293
+ "Format fix requires an interactive terminal or `-y` to run non-interactively.",
294
+ );
295
+ }
296
+ const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
297
+ `Run format fixes? (${plan.summary.totalFiles} planned change(s), ${status.errorCount} error warning(s))`,
298
+ true,
299
+ );
300
+ if (!confirmed) {
301
+ logger.log("info", "Format fix cancelled.");
302
+ return;
303
+ }
304
+ }
305
+
306
+ if (status.hasChanges) {
307
+ logger.log("info", "Applying deterministic format changes first...");
308
+ await run({
309
+ _: ["format"],
310
+ write: true,
311
+ yes: true,
312
+ interactive: false,
313
+ verbose: options.verbose,
314
+ detailed: options.detailed,
315
+ only: options.only,
316
+ skip: options.skip,
317
+ });
318
+
319
+ ({ plan } = await buildCurrentPlan());
320
+ status = getPlanStatus(plan);
321
+ if (status.ok && !extraInstructions && !force) {
322
+ logger.log("success", "Format fix completed successfully.");
323
+ return;
324
+ }
325
+ }
326
+
327
+ const opencodeArgs = [
328
+ "run",
329
+ "--title",
330
+ "gitzone format fix",
331
+ "--dir",
332
+ process.cwd(),
333
+ ];
334
+ if (autoApprove) {
335
+ opencodeArgs.push("--dangerously-skip-permissions");
336
+ }
337
+ opencodeArgs.push(buildFormatFixPrompt(plan, extraInstructions));
338
+
339
+ logger.log("info", "Starting opencode format fix...");
340
+ const smartshellInstance = new plugins.smartshell.Smartshell({
341
+ executor: "bash",
342
+ sourceFilePaths: [],
343
+ });
344
+
345
+ let result: plugins.smartshell.IExecResult;
346
+ try {
347
+ result = await smartshellInstance.execSpawn("opencode", opencodeArgs, {
348
+ stdio: "inherit",
349
+ });
350
+ } catch (error) {
351
+ throw new Error(
352
+ `Failed to run opencode: ${error instanceof Error ? error.message : String(error)}`,
353
+ );
354
+ }
355
+
356
+ if (result.exitCode !== 0) {
357
+ logger.log("error", `opencode exited with code ${result.exitCode}`);
358
+ process.exitCode = result.exitCode || 1;
359
+ return;
360
+ }
361
+
362
+ logger.log("info", "Running deterministic format pass after opencode...");
363
+ await run({
364
+ _: ["format"],
365
+ write: true,
366
+ yes: true,
367
+ interactive: false,
368
+ verbose: options.verbose,
369
+ detailed: options.detailed,
370
+ only: options.only,
371
+ skip: options.skip,
372
+ });
373
+
374
+ const { planner: finalPlanner, plan: finalPlan } = await buildCurrentPlan();
375
+ await finalPlanner.displayPlan(finalPlan, options.detailed);
376
+ const finalStatus = getPlanStatus(finalPlan);
377
+ if (finalStatus.ok) {
378
+ logger.log("success", "Format fix completed successfully.");
379
+ return;
380
+ }
381
+
382
+ logger.log(
383
+ "error",
384
+ `Format fix left ${finalPlan.summary.totalFiles} planned change(s) and ${finalStatus.errorCount} error warning(s).`,
385
+ );
386
+ process.exitCode = 1;
387
+ };
388
+
170
389
  export let run = async (
171
390
  options: {
172
391
  write?: boolean;
@@ -194,8 +413,25 @@ export let run = async (
194
413
  setVerboseMode(true);
195
414
  }
196
415
 
416
+ if (subcommand === "fix") {
417
+ await handleFormatFix(options, mode);
418
+ return;
419
+ }
420
+
197
421
  const shouldWrite = options.write ?? options.dryRun === false;
198
422
  const treatAsPlan = subcommand === "plan";
423
+ const treatAsCheck = subcommand === "check" || Boolean(options.check);
424
+
425
+ if (treatAsCheck && shouldWrite) {
426
+ const error = "`gitzone format check` is read-only and cannot be combined with --write.";
427
+ if (mode.json) {
428
+ printJson({ ok: false, error });
429
+ } else {
430
+ logger.log("error", error);
431
+ }
432
+ process.exitCode = 1;
433
+ return;
434
+ }
199
435
 
200
436
  if (mode.json && shouldWrite) {
201
437
  printJson({
@@ -212,7 +448,9 @@ export let run = async (
212
448
  const formatConfig = await getFormatConfig();
213
449
  const interactive =
214
450
  options.interactive ?? (mode.interactive && formatConfig.interactive);
215
- const autoApprove = options.yes ?? formatConfig.autoApprove;
451
+ const autoApprove = options.yes ?? (mode.yes || formatConfig.autoApprove);
452
+ const only = normalizeModuleList(options.only);
453
+ const skip = normalizeModuleList(options.skip);
216
454
 
217
455
  try {
218
456
  const planBuilder = async () => {
@@ -220,6 +458,8 @@ export let run = async (
220
458
  fromPlan: options.fromPlan,
221
459
  interactive,
222
460
  jsonOutput: mode.json,
461
+ only,
462
+ skip,
223
463
  });
224
464
  };
225
465
 
@@ -231,7 +471,16 @@ export let run = async (
231
471
  : await planBuilder();
232
472
 
233
473
  if (mode.json) {
234
- printJson(serializePlan(plan));
474
+ const serializedPlan = serializePlan(plan);
475
+ if (treatAsCheck) {
476
+ const status = getPlanStatus(plan);
477
+ printJson({ ok: status.ok, ...serializedPlan });
478
+ if (!status.ok) {
479
+ process.exitCode = 1;
480
+ }
481
+ return;
482
+ }
483
+ printJson(serializedPlan);
235
484
  return;
236
485
  }
237
486
 
@@ -251,6 +500,20 @@ export let run = async (
251
500
  return;
252
501
  }
253
502
 
503
+ if (treatAsCheck) {
504
+ const status = getPlanStatus(plan);
505
+ if (status.ok) {
506
+ logger.log("success", "Format check passed");
507
+ } else {
508
+ logger.log(
509
+ "error",
510
+ `Format check failed: ${plan.summary.totalFiles} planned change(s), ${status.errorCount} error warning(s)`,
511
+ );
512
+ process.exitCode = 1;
513
+ }
514
+ return;
515
+ }
516
+
254
517
  // Show diffs if explicitly requested or before interactive write confirmation
255
518
  const showDiffs =
256
519
  options.diff || (shouldWrite && interactive && !autoApprove);
@@ -314,7 +577,6 @@ export let run = async (
314
577
  }
315
578
  };
316
579
 
317
- import type { ICheckResult } from "./interfaces.format.js";
318
580
  export type { ICheckResult };
319
581
 
320
582
  /**
@@ -363,7 +625,7 @@ export function showHelp(mode?: ICliMode): void {
363
625
  if (mode?.json) {
364
626
  printJson({
365
627
  command: "format",
366
- usage: "gitzone format [plan] [options]",
628
+ usage: "gitzone format [plan|check|fix] [options]",
367
629
  description:
368
630
  "Plans formatting changes by default and applies them only with --write.",
369
631
  flags: [
@@ -393,19 +655,33 @@ export function showHelp(mode?: ICliMode): void {
393
655
  flag: "--diff",
394
656
  description: "Show per-file diffs before applying changes",
395
657
  },
658
+ {
659
+ flag: "--only <modules>",
660
+ description: "Run only the comma-separated formatter modules",
661
+ },
662
+ {
663
+ flag: "--skip <modules>",
664
+ description: "Skip the comma-separated formatter modules",
665
+ },
666
+ {
667
+ flag: "--force",
668
+ description: "Run `format fix` even when the deterministic plan is clean",
669
+ },
396
670
  { flag: "--json", description: "Emit a read-only format plan as JSON" },
397
671
  ],
398
672
  examples: [
399
673
  "gitzone format",
400
674
  "gitzone format plan --json",
675
+ "gitzone format check",
401
676
  "gitzone format --write --yes",
677
+ "gitzone format fix",
402
678
  ],
403
679
  });
404
680
  return;
405
681
  }
406
682
 
407
683
  console.log("");
408
- console.log("Usage: gitzone format [plan] [options]");
684
+ console.log("Usage: gitzone format [plan|check|fix] [options]");
409
685
  console.log("");
410
686
  console.log(
411
687
  "Plans formatting changes by default and applies them only with --write.",
@@ -424,11 +700,16 @@ export function showHelp(mode?: ICliMode): void {
424
700
  console.log(
425
701
  " --diff Show per-file diffs before applying changes",
426
702
  );
703
+ console.log(" --only <modules> Run only comma-separated formatter modules");
704
+ console.log(" --skip <modules> Skip comma-separated formatter modules");
705
+ console.log(" --force Run format fix even when the plan is clean");
427
706
  console.log(" --json Emit a read-only format plan as JSON");
428
707
  console.log("");
429
708
  console.log("Examples:");
430
709
  console.log(" gitzone format");
431
710
  console.log(" gitzone format plan --json");
711
+ console.log(" gitzone format check");
432
712
  console.log(" gitzone format --write --yes");
713
+ console.log(" gitzone format fix");
433
714
  console.log("");
434
715
  }