@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/gitzone.cli.d.ts +1 -1
- package/dist_ts/gitzone.cli.js +100 -126
- package/dist_ts/helpers.climode.d.ts +2 -0
- package/dist_ts/helpers.climode.js +31 -2
- package/dist_ts/helpers.smartconfigmigrations.js +34 -2
- package/dist_ts/mod_config/index.js +51 -10
- package/dist_ts/mod_format/classes.baseformatter.d.ts +3 -1
- package/dist_ts/mod_format/classes.baseformatter.js +7 -1
- package/dist_ts/mod_format/classes.formatplanner.d.ts +2 -0
- package/dist_ts/mod_format/classes.formatplanner.js +48 -4
- package/dist_ts/mod_format/formatters/license.formatter.d.ts +4 -1
- package/dist_ts/mod_format/formatters/license.formatter.js +32 -10
- package/dist_ts/mod_format/formatters/prettier.formatter.d.ts +0 -3
- package/dist_ts/mod_format/formatters/prettier.formatter.js +53 -62
- package/dist_ts/mod_format/index.d.ts +1 -1
- package/dist_ts/mod_format/index.js +237 -8
- package/dist_ts/mod_format/interfaces.format.d.ts +7 -11
- package/dist_ts/mod_format/interfaces.format.js +1 -1
- package/dist_ts/mod_standard/index.js +2 -1
- package/package.json +1 -1
- package/readme.hints.md +10 -0
- package/readme.md +11 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/gitzone.cli.ts +104 -144
- package/ts/helpers.climode.ts +36 -1
- package/ts/helpers.smartconfigmigrations.ts +37 -1
- package/ts/mod_config/index.ts +60 -9
- package/ts/mod_format/classes.baseformatter.ts +13 -1
- package/ts/mod_format/classes.formatplanner.ts +66 -4
- package/ts/mod_format/formatters/license.formatter.ts +43 -15
- package/ts/mod_format/formatters/prettier.formatter.ts +54 -66
- package/ts/mod_format/index.ts +289 -8
- package/ts/mod_format/interfaces.format.ts +8 -11
- 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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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}: ${
|
|
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
|
-
|
|
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 (
|
|
238
|
+
if (errorMessage.includes('mkdir')) {
|
|
208
239
|
logger.log(
|
|
209
240
|
'error',
|
|
210
|
-
`Failed to format ${change.path}: ${
|
|
241
|
+
`Failed to format ${change.path}: ${errorMessage}`,
|
|
211
242
|
);
|
|
212
|
-
|
|
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}: ${
|
|
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
|
-
|
|
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
|
}
|
package/ts/mod_format/index.ts
CHANGED
|
@@ -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 (
|
|
112
|
-
return
|
|
150
|
+
if (onlyModules.length > 0) {
|
|
151
|
+
return onlyModules.includes(formatter.name);
|
|
113
152
|
}
|
|
114
|
-
if (
|
|
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
|
-
|
|
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
|
}
|