@git.zone/cli 2.6.1 โ†’ 2.8.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.
@@ -8,9 +8,20 @@ import * as ui from './mod.ui.js';
8
8
  import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js';
9
9
 
10
10
  export const run = async (argvArg: any) => {
11
- // Check if release flag is set and validate registries early
11
+ // Read commit config from npmextra.json
12
+ const npmextraConfig = new plugins.npmextra.Npmextra();
13
+ const gitzoneConfig = npmextraConfig.dataFor<{
14
+ commit?: {
15
+ alwaysTest?: boolean;
16
+ alwaysBuild?: boolean;
17
+ };
18
+ }>('@git.zone/cli', {});
19
+ const commitConfig = gitzoneConfig.commit || {};
20
+
21
+ // Check flags and merge with config options
12
22
  const wantsRelease = !!(argvArg.r || argvArg.release);
13
- const wantsBuild = !!(argvArg.b || argvArg.build);
23
+ const wantsTest = !!(argvArg.t || argvArg.test || commitConfig.alwaysTest);
24
+ const wantsBuild = !!(argvArg.b || argvArg.build || commitConfig.alwaysBuild);
14
25
  let releaseConfig: ReleaseConfig | null = null;
15
26
 
16
27
  if (wantsRelease) {
@@ -28,6 +39,7 @@ export const run = async (argvArg: any) => {
28
39
  ui.printExecutionPlan({
29
40
  autoAccept: !!(argvArg.y || argvArg.yes),
30
41
  push: !!(argvArg.p || argvArg.push),
42
+ test: wantsTest,
31
43
  build: wantsBuild,
32
44
  release: wantsRelease,
33
45
  format: !!argvArg.format,
@@ -39,6 +51,21 @@ export const run = async (argvArg: any) => {
39
51
  await formatMod.run();
40
52
  }
41
53
 
54
+ // Run tests early to fail fast before analysis
55
+ if (wantsTest) {
56
+ ui.printHeader('๐Ÿงช Running tests...');
57
+ const smartshellForTest = new plugins.smartshell.Smartshell({
58
+ executor: 'bash',
59
+ sourceFilePaths: [],
60
+ });
61
+ const testResult = await smartshellForTest.exec('pnpm test');
62
+ if (testResult.exitCode !== 0) {
63
+ logger.log('error', 'Tests failed. Aborting commit.');
64
+ process.exit(1);
65
+ }
66
+ logger.log('success', 'All tests passed.');
67
+ }
68
+
42
69
  ui.printHeader('๐Ÿ” Analyzing repository changes...');
43
70
 
44
71
  const aidoc = new plugins.tsdoc.AiDoc();
@@ -161,6 +188,7 @@ export const run = async (argvArg: any) => {
161
188
  }
162
189
 
163
190
  // Determine total steps based on options
191
+ // Note: test runs early (like format) so not counted in numbered steps
164
192
  const willPush = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true');
165
193
  const willRelease = answerBucket.getAnswerFor('createRelease') && releaseConfig?.hasRegistries();
166
194
  let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version
@@ -21,6 +21,7 @@ interface ICommitSummary {
21
21
  interface IExecutionPlanOptions {
22
22
  autoAccept: boolean;
23
23
  push: boolean;
24
+ test: boolean;
24
25
  build: boolean;
25
26
  release: boolean;
26
27
  format: boolean;
@@ -64,6 +65,7 @@ export function printExecutionPlan(options: IExecutionPlanOptions): void {
64
65
  console.log(' Options:');
65
66
  console.log(` Auto-accept ${options.autoAccept ? 'โœ“ enabled (-y)' : 'โ—‹ interactive mode'}`);
66
67
  console.log(` Push to remote ${options.push ? 'โœ“ enabled (-p)' : 'โ—‹ disabled'}`);
68
+ console.log(` Test first ${options.test ? 'โœ“ enabled (-t)' : 'โ—‹ disabled'}`);
67
69
  console.log(` Build & verify ${options.build ? 'โœ“ enabled (-b)' : 'โ—‹ disabled'}`);
68
70
  console.log(` Release to npm ${options.release ? 'โœ“ enabled (-r)' : 'โ—‹ disabled'}`);
69
71
  if (options.format) {
@@ -77,6 +79,9 @@ export function printExecutionPlan(options: IExecutionPlanOptions): void {
77
79
  if (options.format) {
78
80
  console.log(` ${stepNum++}. Format project files`);
79
81
  }
82
+ if (options.test) {
83
+ console.log(` ${stepNum++}. Run tests`);
84
+ }
80
85
  console.log(` ${stepNum++}. Analyze repository changes`);
81
86
  console.log(` ${stepNum++}. Bake commit info into code`);
82
87
  console.log(` ${stepNum++}. Generate changelog.md`);
@@ -0,0 +1,104 @@
1
+ import * as plugins from './mod.plugins.js';
2
+
3
+ export interface ICommitConfig {
4
+ alwaysTest: boolean;
5
+ alwaysBuild: boolean;
6
+ }
7
+
8
+ /**
9
+ * Manages commit configuration stored in npmextra.json
10
+ * under @git.zone/cli.commit namespace
11
+ */
12
+ export class CommitConfig {
13
+ private cwd: string;
14
+ private config: ICommitConfig;
15
+
16
+ constructor(cwd: string = process.cwd()) {
17
+ this.cwd = cwd;
18
+ this.config = { alwaysTest: false, alwaysBuild: false };
19
+ }
20
+
21
+ /**
22
+ * Create a CommitConfig instance from current working directory
23
+ */
24
+ public static async fromCwd(cwd: string = process.cwd()): Promise<CommitConfig> {
25
+ const instance = new CommitConfig(cwd);
26
+ await instance.load();
27
+ return instance;
28
+ }
29
+
30
+ /**
31
+ * Load configuration from npmextra.json
32
+ */
33
+ public async load(): Promise<void> {
34
+ const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
35
+ const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
36
+
37
+ this.config = {
38
+ alwaysTest: gitzoneConfig?.commit?.alwaysTest ?? false,
39
+ alwaysBuild: gitzoneConfig?.commit?.alwaysBuild ?? false,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Save configuration to npmextra.json
45
+ */
46
+ public async save(): Promise<void> {
47
+ const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json');
48
+ let npmextraData: any = {};
49
+
50
+ // Read existing npmextra.json
51
+ if (await plugins.smartfs.file(npmextraPath).exists()) {
52
+ const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
53
+ npmextraData = JSON.parse(content as string);
54
+ }
55
+
56
+ // Ensure @git.zone/cli namespace exists
57
+ if (!npmextraData['@git.zone/cli']) {
58
+ npmextraData['@git.zone/cli'] = {};
59
+ }
60
+
61
+ // Ensure commit object exists
62
+ if (!npmextraData['@git.zone/cli'].commit) {
63
+ npmextraData['@git.zone/cli'].commit = {};
64
+ }
65
+
66
+ // Update commit settings
67
+ npmextraData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest;
68
+ npmextraData['@git.zone/cli'].commit.alwaysBuild = this.config.alwaysBuild;
69
+
70
+ // Write back to file
71
+ await plugins.smartfs
72
+ .file(npmextraPath)
73
+ .encoding('utf8')
74
+ .write(JSON.stringify(npmextraData, null, 2));
75
+ }
76
+
77
+ /**
78
+ * Get alwaysTest setting
79
+ */
80
+ public getAlwaysTest(): boolean {
81
+ return this.config.alwaysTest;
82
+ }
83
+
84
+ /**
85
+ * Set alwaysTest setting
86
+ */
87
+ public setAlwaysTest(value: boolean): void {
88
+ this.config.alwaysTest = value;
89
+ }
90
+
91
+ /**
92
+ * Get alwaysBuild setting
93
+ */
94
+ public getAlwaysBuild(): boolean {
95
+ return this.config.alwaysBuild;
96
+ }
97
+
98
+ /**
99
+ * Set alwaysBuild setting
100
+ */
101
+ public setAlwaysBuild(value: boolean): void {
102
+ this.config.alwaysBuild = value;
103
+ }
104
+ }
@@ -2,9 +2,32 @@
2
2
 
3
3
  import * as plugins from './mod.plugins.js';
4
4
  import { ReleaseConfig } from './classes.releaseconfig.js';
5
- import { runFormatter } from '../mod_format/index.js';
5
+ import { CommitConfig } from './classes.commitconfig.js';
6
+ import { runFormatter, type ICheckResult } from '../mod_format/index.js';
6
7
 
7
- export { ReleaseConfig };
8
+ export { ReleaseConfig, CommitConfig };
9
+
10
+ /**
11
+ * Format npmextra.json with diff preview
12
+ * Shows diff first, asks for confirmation, then applies
13
+ */
14
+ async function formatNpmextraWithDiff(): Promise<void> {
15
+ // Check for diffs first
16
+ const checkResult = await runFormatter('npmextra', {
17
+ checkOnly: true,
18
+ showDiff: true,
19
+ }) as ICheckResult | void;
20
+
21
+ if (checkResult && checkResult.hasDiff) {
22
+ const shouldApply = await plugins.smartinteract.SmartInteract.getCliConfirmation(
23
+ 'Apply formatting changes to npmextra.json?',
24
+ true
25
+ );
26
+ if (shouldApply) {
27
+ await runFormatter('npmextra', { silent: true });
28
+ }
29
+ }
30
+ }
8
31
 
9
32
  export const run = async (argvArg: any) => {
10
33
  const command = argvArg._?.[1];
@@ -33,6 +56,12 @@ export const run = async (argvArg: any) => {
33
56
  case 'accessLevel':
34
57
  await handleAccessLevel(value);
35
58
  break;
59
+ case 'commit':
60
+ await handleCommit(argvArg._?.[2], argvArg._?.[3]);
61
+ break;
62
+ case 'services':
63
+ await handleServices();
64
+ break;
36
65
  case 'help':
37
66
  showHelp();
38
67
  break;
@@ -48,7 +77,7 @@ export const run = async (argvArg: any) => {
48
77
  async function handleInteractiveMenu(): Promise<void> {
49
78
  console.log('');
50
79
  console.log('โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ');
51
- console.log('โ”‚ gitzone config - Release Configuration โ”‚');
80
+ console.log('โ”‚ gitzone config - Project Configuration โ”‚');
52
81
  console.log('โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ');
53
82
  console.log('');
54
83
 
@@ -64,6 +93,8 @@ async function handleInteractiveMenu(): Promise<void> {
64
93
  { name: 'Remove a registry', value: 'remove' },
65
94
  { name: 'Clear all registries', value: 'clear' },
66
95
  { name: 'Set access level (public/private)', value: 'access' },
96
+ { name: 'Configure commit options', value: 'commit' },
97
+ { name: 'Configure services', value: 'services' },
67
98
  { name: 'Show help', value: 'help' },
68
99
  ],
69
100
  });
@@ -86,6 +117,12 @@ async function handleInteractiveMenu(): Promise<void> {
86
117
  case 'access':
87
118
  await handleAccessLevel();
88
119
  break;
120
+ case 'commit':
121
+ await handleCommit();
122
+ break;
123
+ case 'services':
124
+ await handleServices();
125
+ break;
89
126
  case 'help':
90
127
  showHelp();
91
128
  break;
@@ -149,8 +186,8 @@ async function handleAdd(url?: string): Promise<void> {
149
186
 
150
187
  if (added) {
151
188
  await config.save();
152
- await runFormatter('npmextra', { silent: true });
153
189
  plugins.logger.log('success', `Added registry: ${url}`);
190
+ await formatNpmextraWithDiff();
154
191
  } else {
155
192
  plugins.logger.log('warn', `Registry already exists: ${url}`);
156
193
  }
@@ -185,8 +222,8 @@ async function handleRemove(url?: string): Promise<void> {
185
222
 
186
223
  if (removed) {
187
224
  await config.save();
188
- await runFormatter('npmextra', { silent: true });
189
225
  plugins.logger.log('success', `Removed registry: ${url}`);
226
+ await formatNpmextraWithDiff();
190
227
  } else {
191
228
  plugins.logger.log('warn', `Registry not found: ${url}`);
192
229
  }
@@ -212,8 +249,8 @@ async function handleClear(): Promise<void> {
212
249
  if (confirmed) {
213
250
  config.clearRegistries();
214
251
  await config.save();
215
- await runFormatter('npmextra', { silent: true });
216
252
  plugins.logger.log('success', 'All registries cleared.');
253
+ await formatNpmextraWithDiff();
217
254
  } else {
218
255
  plugins.logger.log('info', 'Operation cancelled.');
219
256
  }
@@ -252,8 +289,115 @@ async function handleAccessLevel(level?: string): Promise<void> {
252
289
 
253
290
  config.setAccessLevel(level as 'public' | 'private');
254
291
  await config.save();
255
- await runFormatter('npmextra', { silent: true });
256
292
  plugins.logger.log('success', `Access level set to: ${level}`);
293
+ await formatNpmextraWithDiff();
294
+ }
295
+
296
+ /**
297
+ * Handle commit configuration
298
+ */
299
+ async function handleCommit(setting?: string, value?: string): Promise<void> {
300
+ const config = await CommitConfig.fromCwd();
301
+
302
+ // No setting = interactive mode
303
+ if (!setting) {
304
+ await handleCommitInteractive(config);
305
+ return;
306
+ }
307
+
308
+ // Direct setting
309
+ switch (setting) {
310
+ case 'alwaysTest':
311
+ await handleCommitSetting(config, 'alwaysTest', value);
312
+ break;
313
+ case 'alwaysBuild':
314
+ await handleCommitSetting(config, 'alwaysBuild', value);
315
+ break;
316
+ default:
317
+ plugins.logger.log('error', `Unknown commit setting: ${setting}`);
318
+ showCommitHelp();
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Interactive commit configuration
324
+ */
325
+ async function handleCommitInteractive(config: CommitConfig): Promise<void> {
326
+ console.log('');
327
+ console.log('โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ');
328
+ console.log('โ”‚ Commit Configuration โ”‚');
329
+ console.log('โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ');
330
+ console.log('');
331
+
332
+ const interactInstance = new plugins.smartinteract.SmartInteract();
333
+ const response = await interactInstance.askQuestion({
334
+ type: 'checkbox',
335
+ name: 'commitOptions',
336
+ message: 'Select commit options to enable:',
337
+ choices: [
338
+ { name: 'Always run tests before commit (-t)', value: 'alwaysTest' },
339
+ { name: 'Always build after commit (-b)', value: 'alwaysBuild' },
340
+ ],
341
+ default: [
342
+ ...(config.getAlwaysTest() ? ['alwaysTest'] : []),
343
+ ...(config.getAlwaysBuild() ? ['alwaysBuild'] : []),
344
+ ],
345
+ });
346
+
347
+ const selected = (response as any).value || [];
348
+ config.setAlwaysTest(selected.includes('alwaysTest'));
349
+ config.setAlwaysBuild(selected.includes('alwaysBuild'));
350
+ await config.save();
351
+
352
+ plugins.logger.log('success', 'Commit configuration updated');
353
+ await formatNpmextraWithDiff();
354
+ }
355
+
356
+ /**
357
+ * Set a specific commit setting
358
+ */
359
+ async function handleCommitSetting(config: CommitConfig, setting: string, value?: string): Promise<void> {
360
+ // Parse boolean value
361
+ const boolValue = value === 'true' || value === '1' || value === 'on';
362
+
363
+ if (setting === 'alwaysTest') {
364
+ config.setAlwaysTest(boolValue);
365
+ } else if (setting === 'alwaysBuild') {
366
+ config.setAlwaysBuild(boolValue);
367
+ }
368
+
369
+ await config.save();
370
+ plugins.logger.log('success', `Set ${setting} to ${boolValue}`);
371
+ await formatNpmextraWithDiff();
372
+ }
373
+
374
+ /**
375
+ * Show help for commit subcommand
376
+ */
377
+ function showCommitHelp(): void {
378
+ console.log('');
379
+ console.log('Usage: gitzone config commit [setting] [value]');
380
+ console.log('');
381
+ console.log('Settings:');
382
+ console.log(' alwaysTest [true|false] Always run tests before commit');
383
+ console.log(' alwaysBuild [true|false] Always build after commit');
384
+ console.log('');
385
+ console.log('Examples:');
386
+ console.log(' gitzone config commit # Interactive mode');
387
+ console.log(' gitzone config commit alwaysTest true');
388
+ console.log(' gitzone config commit alwaysBuild false');
389
+ console.log('');
390
+ }
391
+
392
+ /**
393
+ * Handle services configuration
394
+ */
395
+ async function handleServices(): Promise<void> {
396
+ // Import and use ServiceManager's configureServices
397
+ const { ServiceManager } = await import('../mod_services/classes.servicemanager.js');
398
+ const serviceManager = new ServiceManager();
399
+ await serviceManager.init();
400
+ await serviceManager.configureServices();
257
401
  }
258
402
 
259
403
  /**
@@ -269,6 +413,8 @@ function showHelp(): void {
269
413
  console.log(' remove [url] Remove a registry URL');
270
414
  console.log(' clear Clear all registries');
271
415
  console.log(' access [public|private] Set npm access level for publishing');
416
+ console.log(' commit [setting] [value] Configure commit options');
417
+ console.log(' services Configure which services are enabled');
272
418
  console.log('');
273
419
  console.log('Examples:');
274
420
  console.log(' gitzone config show');
@@ -278,5 +424,8 @@ function showHelp(): void {
278
424
  console.log(' gitzone config clear');
279
425
  console.log(' gitzone config access public');
280
426
  console.log(' gitzone config access private');
427
+ console.log(' gitzone config commit # Interactive');
428
+ console.log(' gitzone config commit alwaysTest true');
429
+ console.log(' gitzone config services # Interactive');
281
430
  console.log('');
282
431
  }
@@ -1,6 +1,6 @@
1
1
  import * as plugins from './mod.plugins.js';
2
2
  import { FormatContext } from './classes.formatcontext.js';
3
- import type { IPlannedChange } from './interfaces.format.js';
3
+ import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
4
4
  import { Project } from '../classes.project.js';
5
5
 
6
6
  export abstract class BaseFormatter {
@@ -79,4 +79,94 @@ export abstract class BaseFormatter {
79
79
  protected async shouldProcessFile(filepath: string): Promise<boolean> {
80
80
  return true;
81
81
  }
82
+
83
+ /**
84
+ * Check for diffs without applying changes
85
+ * Returns information about what would change
86
+ */
87
+ async check(): Promise<ICheckResult> {
88
+ const changes = await this.analyze();
89
+ const diffs: ICheckResult['diffs'] = [];
90
+
91
+ for (const change of changes) {
92
+ // Skip generic changes that don't have actual content
93
+ if (change.path === '<various files>') {
94
+ continue;
95
+ }
96
+
97
+ if (change.type === 'modify' || change.type === 'create') {
98
+ // Read current content if file exists
99
+ let currentContent: string | undefined;
100
+ try {
101
+ currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
102
+ } catch {
103
+ // File doesn't exist yet
104
+ currentContent = undefined;
105
+ }
106
+
107
+ const newContent = change.content;
108
+
109
+ // Check if there's an actual diff
110
+ if (currentContent !== newContent && newContent !== undefined) {
111
+ diffs.push({
112
+ path: change.path,
113
+ type: change.type,
114
+ before: currentContent,
115
+ after: newContent,
116
+ });
117
+ }
118
+ } else if (change.type === 'delete') {
119
+ // Check if file exists before marking for deletion
120
+ try {
121
+ const currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
122
+ diffs.push({
123
+ path: change.path,
124
+ type: 'delete',
125
+ before: currentContent,
126
+ after: undefined,
127
+ });
128
+ } catch {
129
+ // File doesn't exist, nothing to delete
130
+ }
131
+ }
132
+ }
133
+
134
+ return {
135
+ hasDiff: diffs.length > 0,
136
+ diffs,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Display a single diff using smartdiff
142
+ */
143
+ displayDiff(diff: ICheckResult['diffs'][0]): void {
144
+ console.log(`\n--- ${diff.path}`);
145
+ if (diff.before && diff.after) {
146
+ console.log(plugins.smartdiff.formatLineDiffForConsole(diff.before, diff.after));
147
+ } else if (diff.after && !diff.before) {
148
+ console.log(' (new file)');
149
+ // Show first few lines of new content
150
+ const lines = diff.after.split('\n').slice(0, 10);
151
+ lines.forEach(line => console.log(` + ${line}`));
152
+ if (diff.after.split('\n').length > 10) {
153
+ console.log(' ... (truncated)');
154
+ }
155
+ } else if (diff.before && !diff.after) {
156
+ console.log(' (file will be deleted)');
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Display all diffs from a check result
162
+ */
163
+ displayAllDiffs(result: ICheckResult): void {
164
+ if (!result.hasDiff) {
165
+ console.log(' No changes detected');
166
+ return;
167
+ }
168
+ for (const diff of result.diffs) {
169
+ this.displayDiff(diff);
170
+ }
171
+ }
82
172
  }
@@ -19,7 +19,8 @@ import { CopyFormatter } from './formatters/copy.formatter.js';
19
19
 
20
20
  export let run = async (
21
21
  options: {
22
- dryRun?: boolean;
22
+ write?: boolean; // Explicitly write changes (default: false, dry-mode)
23
+ dryRun?: boolean; // Deprecated, kept for compatibility
23
24
  yes?: boolean;
24
25
  planOnly?: boolean;
25
26
  savePlan?: string;
@@ -35,7 +36,11 @@ export let run = async (
35
36
  setVerboseMode(true);
36
37
  }
37
38
 
38
- const project = await Project.fromCwd();
39
+ // Determine if we should write changes
40
+ // Default is dry-mode (no writing) unless --write/-w is specified
41
+ const shouldWrite = options.write ?? (options.dryRun === false);
42
+
43
+ const project = await Project.fromCwd({ requireProjectType: false });
39
44
  const context = new FormatContext();
40
45
  // Cache system removed - no longer needed
41
46
  const planner = new FormatPlanner();
@@ -127,9 +132,9 @@ export let run = async (
127
132
  return;
128
133
  }
129
134
 
130
- // Dry-run mode
131
- if (options.dryRun) {
132
- logger.log('info', 'Dry-run mode - no changes will be made');
135
+ // Dry-run mode (default behavior)
136
+ if (!shouldWrite) {
137
+ logger.log('info', 'Dry-run mode - use --write (-w) to apply changes');
133
138
  return;
134
139
  }
135
140
 
@@ -197,14 +202,27 @@ export const handleCleanBackups = async (): Promise<void> => {
197
202
  );
198
203
  };
199
204
 
205
+ // Import the ICheckResult type for external use
206
+ import type { ICheckResult } from './interfaces.format.js';
207
+ export type { ICheckResult };
208
+
209
+ // Formatters that don't require projectType to be set
210
+ const formattersNotRequiringProjectType = ['npmextra', 'prettier', 'cleanup', 'packagejson'];
211
+
200
212
  /**
201
213
  * Run a single formatter by name (for use by other modules)
202
214
  */
203
215
  export const runFormatter = async (
204
216
  formatterName: string,
205
- options: { silent?: boolean } = {}
206
- ): Promise<void> => {
207
- const project = await Project.fromCwd();
217
+ options: {
218
+ silent?: boolean;
219
+ checkOnly?: boolean; // Only check for diffs, don't apply
220
+ showDiff?: boolean; // Show the diff output
221
+ } = {}
222
+ ): Promise<ICheckResult | void> => {
223
+ // Determine if this formatter requires projectType
224
+ const requireProjectType = !formattersNotRequiringProjectType.includes(formatterName);
225
+ const project = await Project.fromCwd({ requireProjectType });
208
226
  const context = new FormatContext();
209
227
 
210
228
  // Map formatter names to classes
@@ -227,6 +245,17 @@ export const runFormatter = async (
227
245
  }
228
246
 
229
247
  const formatter = new FormatterClass(context, project);
248
+
249
+ // Check-only mode: just check for diffs and optionally display them
250
+ if (options.checkOnly) {
251
+ const result = await formatter.check();
252
+ if (result.hasDiff && options.showDiff) {
253
+ formatter.displayAllDiffs(result);
254
+ }
255
+ return result;
256
+ }
257
+
258
+ // Normal mode: analyze and apply changes
230
259
  const changes = await formatter.analyze();
231
260
 
232
261
  for (const change of changes) {
@@ -39,7 +39,18 @@ export type IPlannedChange = {
39
39
  path: string;
40
40
  module: string;
41
41
  description: string;
42
- content?: string; // For create/modify operations
42
+ content?: string; // New content for create/modify operations
43
+ originalContent?: string; // Original content for comparison
43
44
  diff?: string;
44
45
  size?: number;
45
46
  };
47
+
48
+ export interface ICheckResult {
49
+ hasDiff: boolean;
50
+ diffs: Array<{
51
+ path: string;
52
+ type: 'create' | 'modify' | 'delete';
53
+ before?: string;
54
+ after?: string;
55
+ }>;
56
+ }