feat(format): add check and fix workflows
This commit is contained in:
@@ -193,12 +193,22 @@ gitzone format
|
|||||||
# Read-only JSON plan
|
# Read-only JSON plan
|
||||||
gitzone format plan --json
|
gitzone format plan --json
|
||||||
|
|
||||||
|
# CI-friendly check, exits non-zero when changes or validator errors remain
|
||||||
|
gitzone format check
|
||||||
|
|
||||||
# Dry run to preview changes
|
# Dry run to preview changes
|
||||||
gitzone format --dry-run
|
gitzone format --dry-run
|
||||||
|
|
||||||
|
# Limit formatter modules
|
||||||
|
gitzone format --only prettier,packagejson
|
||||||
|
gitzone format --skip license
|
||||||
|
|
||||||
# Non-interactive apply
|
# Non-interactive apply
|
||||||
gitzone format --write --yes
|
gitzone format --write --yes
|
||||||
|
|
||||||
|
# Deterministic format first, opencode for remaining issues
|
||||||
|
gitzone format fix
|
||||||
|
|
||||||
# Plan only (no execution)
|
# Plan only (no execution)
|
||||||
gitzone format --plan-only
|
gitzone format --plan-only
|
||||||
|
|
||||||
|
|||||||
@@ -309,15 +309,26 @@ gitzone format
|
|||||||
# Emit a machine-readable plan
|
# Emit a machine-readable plan
|
||||||
gitzone format plan --json
|
gitzone format plan --json
|
||||||
|
|
||||||
|
# Fail when formatting changes or validator errors remain
|
||||||
|
gitzone format check
|
||||||
|
|
||||||
|
# Run a subset of formatters
|
||||||
|
gitzone format --only prettier,packagejson
|
||||||
|
|
||||||
# Apply changes
|
# Apply changes
|
||||||
gitzone format --write
|
gitzone format --write
|
||||||
|
|
||||||
# Apply without prompt
|
# Apply without prompt
|
||||||
gitzone format --write --yes
|
gitzone format --write --yes
|
||||||
|
|
||||||
|
# Apply deterministic fixes, then use opencode for remaining issues
|
||||||
|
gitzone format fix
|
||||||
```
|
```
|
||||||
|
|
||||||
Formatters include cleanup, smartconfig normalization, dependency license checks, package metadata normalization, template updates, `.gitignore`, TypeScript config, Prettier, README existence checks, and configured copy operations.
|
Formatters include cleanup, smartconfig normalization, dependency license checks, package metadata normalization, template updates, `.gitignore`, TypeScript config, Prettier, README existence checks, and configured copy operations.
|
||||||
|
|
||||||
|
`gitzone format fix` intentionally lives outside the default format path. Normal format runs stay deterministic; the fix command uses opencode only after deterministic formatters have done what they can.
|
||||||
|
|
||||||
## Development Services
|
## Development Services
|
||||||
|
|
||||||
`gitzone services` manages local Docker-backed services for development projects.
|
`gitzone services` manages local Docker-backed services for development projects.
|
||||||
|
|||||||
+104
-144
@@ -1,13 +1,107 @@
|
|||||||
import * as plugins from "./plugins.js";
|
import * as plugins from "./plugins.js";
|
||||||
import * as paths from "./paths.js";
|
import * as paths from "./paths.js";
|
||||||
import { GitzoneConfig } from "./classes.gitzoneconfig.js";
|
import {
|
||||||
import { getRawCliMode } from "./helpers.climode.js";
|
getProcessUserArgv,
|
||||||
|
getRawCliMode,
|
||||||
|
parseCliArgv,
|
||||||
|
} from "./helpers.climode.js";
|
||||||
import { commitinfo } from "./00_commitinfo_data.js";
|
import { commitinfo } from "./00_commitinfo_data.js";
|
||||||
|
|
||||||
const gitzoneSmartcli = new plugins.smartcli.Smartcli();
|
const runParsedCommand = async (argvArg: any): Promise<void> => {
|
||||||
|
const command = argvArg._?.[0];
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case undefined:
|
||||||
|
case "help": {
|
||||||
|
const modStandard = await import("./mod_standard/index.js");
|
||||||
|
await modStandard.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "commit": {
|
||||||
|
const modCommit = await import("./mod_commit/index.js");
|
||||||
|
await modCommit.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "release": {
|
||||||
|
const modRelease = await import("./mod_release/index.js");
|
||||||
|
await modRelease.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "deprecate": {
|
||||||
|
const modDeprecate = await import("./mod_deprecate/index.js");
|
||||||
|
await modDeprecate.run();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "docker": {
|
||||||
|
const modDocker = await import("./mod_docker/index.js");
|
||||||
|
await modDocker.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "format": {
|
||||||
|
const modFormat = await import("./mod_format/index.js");
|
||||||
|
await modFormat.run({
|
||||||
|
...argvArg,
|
||||||
|
write: argvArg.write || argvArg.w,
|
||||||
|
dryRun: argvArg["dry-run"],
|
||||||
|
yes: argvArg.yes || argvArg.y,
|
||||||
|
planOnly: argvArg["plan-only"] || argvArg.planOnly,
|
||||||
|
savePlan: argvArg["save-plan"] || argvArg.savePlan,
|
||||||
|
fromPlan: argvArg["from-plan"] || argvArg.fromPlan,
|
||||||
|
detailed: argvArg.detailed,
|
||||||
|
interactive: argvArg.interactive !== false,
|
||||||
|
verbose: argvArg.verbose,
|
||||||
|
diff: argvArg.diff,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "meta": {
|
||||||
|
const modMeta = await import("./mod_meta/index.js");
|
||||||
|
await modMeta.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "open": {
|
||||||
|
const modOpen = await import("./mod_open/index.js");
|
||||||
|
await modOpen.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "template": {
|
||||||
|
const modTemplate = await import("./mod_template/index.js");
|
||||||
|
await modTemplate.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "start": {
|
||||||
|
const modStart = await import("./mod_start/index.js");
|
||||||
|
await modStart.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "helpers": {
|
||||||
|
const modHelpers = await import("./mod_helpers/index.js");
|
||||||
|
await modHelpers.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "tools": {
|
||||||
|
const modTools = await import("./mod_tools/index.js");
|
||||||
|
await modTools.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "config": {
|
||||||
|
const modConfig = await import("./mod_config/index.js");
|
||||||
|
await modConfig.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "services": {
|
||||||
|
const modServices = await import("./mod_services/index.js");
|
||||||
|
await modServices.run(argvArg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const modStandard = await import("./mod_standard/index.js");
|
||||||
|
await modStandard.run(argvArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export let run = async () => {
|
export let run = async () => {
|
||||||
const done = plugins.smartpromise.defer();
|
|
||||||
const rawCliMode = await getRawCliMode();
|
const rawCliMode = await getRawCliMode();
|
||||||
|
|
||||||
// get packageInfo
|
// get packageInfo
|
||||||
@@ -34,144 +128,10 @@ export let run = async () => {
|
|||||||
if (rawCliMode.output === "human") {
|
if (rawCliMode.output === "human") {
|
||||||
console.log("---------------------------------------------");
|
console.log("---------------------------------------------");
|
||||||
}
|
}
|
||||||
gitzoneSmartcli.addVersion(packageVersion);
|
const argvArg = parseCliArgv(getProcessUserArgv());
|
||||||
|
if (argvArg.v || argvArg.version) {
|
||||||
// ======> Standard task <======
|
console.log(packageVersion);
|
||||||
|
return;
|
||||||
/**
|
}
|
||||||
* standard task
|
await runParsedCommand(argvArg);
|
||||||
*/
|
|
||||||
gitzoneSmartcli.standardCommand().subscribe(async (argvArg) => {
|
|
||||||
const modStandard = await import("./mod_standard/index.js");
|
|
||||||
await modStandard.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
gitzoneSmartcli.addCommand("help").subscribe(async (argvArg) => {
|
|
||||||
const modStandard = await import("./mod_standard/index.js");
|
|
||||||
await modStandard.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ======> Specific tasks <======
|
|
||||||
|
|
||||||
/**
|
|
||||||
* commit something
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("commit").subscribe(async (argvArg) => {
|
|
||||||
const modCommit = await import("./mod_commit/index.js");
|
|
||||||
await modCommit.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create a release from pending changelog entries
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("release").subscribe(async (argvArg) => {
|
|
||||||
const modRelease = await import("./mod_release/index.js");
|
|
||||||
await modRelease.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* deprecate a package on npm
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("deprecate").subscribe(async (argvArg) => {
|
|
||||||
const modDeprecate = await import("./mod_deprecate/index.js");
|
|
||||||
await modDeprecate.run();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* docker
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("docker").subscribe(async (argvArg) => {
|
|
||||||
const modDocker = await import("./mod_docker/index.js");
|
|
||||||
await modDocker.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update all files that comply with the gitzone standard
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("format").subscribe(async (argvArg) => {
|
|
||||||
const config = GitzoneConfig.fromCwd();
|
|
||||||
const modFormat = await import("./mod_format/index.js");
|
|
||||||
|
|
||||||
// Handle format with options
|
|
||||||
// Default is dry-mode, use --write/-w to apply changes
|
|
||||||
await modFormat.run({
|
|
||||||
...argvArg,
|
|
||||||
write: argvArg.write || argvArg.w,
|
|
||||||
dryRun: argvArg["dry-run"],
|
|
||||||
yes: argvArg.yes,
|
|
||||||
planOnly: argvArg["plan-only"],
|
|
||||||
savePlan: argvArg["save-plan"],
|
|
||||||
fromPlan: argvArg["from-plan"],
|
|
||||||
detailed: argvArg.detailed,
|
|
||||||
interactive: argvArg.interactive !== false,
|
|
||||||
verbose: argvArg.verbose,
|
|
||||||
diff: argvArg.diff,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* run meta commands
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("meta").subscribe(async (argvArg) => {
|
|
||||||
const config = GitzoneConfig.fromCwd();
|
|
||||||
const modMeta = await import("./mod_meta/index.js");
|
|
||||||
modMeta.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* open assets
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("open").subscribe(async (argvArg) => {
|
|
||||||
const modOpen = await import("./mod_open/index.js");
|
|
||||||
modOpen.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add a readme to a project
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("template").subscribe(async (argvArg) => {
|
|
||||||
const modTemplate = await import("./mod_template/index.js");
|
|
||||||
modTemplate.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* start working on a project
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("start").subscribe(async (argvArg) => {
|
|
||||||
const modTemplate = await import("./mod_start/index.js");
|
|
||||||
modTemplate.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
gitzoneSmartcli.addCommand("helpers").subscribe(async (argvArg) => {
|
|
||||||
const modHelpers = await import("./mod_helpers/index.js");
|
|
||||||
modHelpers.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* manage the global @git.zone toolchain
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("tools").subscribe(async (argvArg) => {
|
|
||||||
const modTools = await import("./mod_tools/index.js");
|
|
||||||
await modTools.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* manage release configuration
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("config").subscribe(async (argvArg) => {
|
|
||||||
const modConfig = await import("./mod_config/index.js");
|
|
||||||
await modConfig.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* manage development services (MongoDB, S3/MinIO)
|
|
||||||
*/
|
|
||||||
gitzoneSmartcli.addCommand("services").subscribe(async (argvArg) => {
|
|
||||||
const modServices = await import("./mod_services/index.js");
|
|
||||||
await modServices.run(argvArg);
|
|
||||||
});
|
|
||||||
|
|
||||||
// start parsing of the cli
|
|
||||||
gitzoneSmartcli.startParse();
|
|
||||||
return await done.promise;
|
|
||||||
};
|
};
|
||||||
|
|||||||
+36
-1
@@ -88,6 +88,41 @@ const parseRawArgv = (argv: string[]): TArgSource => {
|
|||||||
return parsedArgv;
|
return parsedArgv;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parseCliArgv = parseRawArgv;
|
||||||
|
|
||||||
|
export const getProcessUserArgv = (): string[] => {
|
||||||
|
const rawArgv = process.argv;
|
||||||
|
const argv0Base = (rawArgv[0] || "").split(/[\\/]/).pop()?.toLowerCase();
|
||||||
|
const runtimeNames = new Set([
|
||||||
|
"node",
|
||||||
|
"node.exe",
|
||||||
|
"nodejs",
|
||||||
|
"nodejs.exe",
|
||||||
|
"bun",
|
||||||
|
"bun.exe",
|
||||||
|
"deno",
|
||||||
|
"deno.exe",
|
||||||
|
"tsx",
|
||||||
|
"tsx.exe",
|
||||||
|
"ts-node",
|
||||||
|
"ts-node.exe",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!runtimeNames.has(argv0Base || "")) {
|
||||||
|
return rawArgv.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstUserArg = rawArgv[1] || "";
|
||||||
|
const firstUserArgLooksLikeScript =
|
||||||
|
firstUserArg.includes("/") ||
|
||||||
|
firstUserArg.endsWith(".js") ||
|
||||||
|
firstUserArg.endsWith(".ts") ||
|
||||||
|
firstUserArg.endsWith(".mjs") ||
|
||||||
|
firstUserArg.endsWith(".cjs");
|
||||||
|
|
||||||
|
return rawArgv.slice(firstUserArgLooksLikeScript ? 2 : 1);
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
|
const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
|
||||||
if (value === "human" || value === "plain" || value === "json") {
|
if (value === "human" || value === "plain" || value === "json") {
|
||||||
return value;
|
return value;
|
||||||
@@ -171,7 +206,7 @@ export const getCliMode = async (
|
|||||||
|
|
||||||
export const getRawCliMode = async (): Promise<ICliMode> => {
|
export const getRawCliMode = async (): Promise<ICliMode> => {
|
||||||
const cliConfig = await getCliModeConfig();
|
const cliConfig = await getCliModeConfig();
|
||||||
const rawArgv = parseRawArgv(process.argv.slice(2));
|
const rawArgv = parseRawArgv(getProcessUserArgv());
|
||||||
return resolveCliMode(rawArgv, cliConfig);
|
return resolveCliMode(rawArgv, cliConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from './mod.plugins.js';
|
||||||
import { FormatContext } from './classes.formatcontext.js';
|
import { FormatContext } from './classes.formatcontext.js';
|
||||||
import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
|
import type {
|
||||||
|
IPlannedChange,
|
||||||
|
ICheckResult,
|
||||||
|
IFormatWarning,
|
||||||
|
} from './interfaces.format.js';
|
||||||
import { Project } from '../classes.project.js';
|
import { Project } from '../classes.project.js';
|
||||||
import { FormatStats } from './classes.formatstats.js';
|
import { FormatStats } from './classes.formatstats.js';
|
||||||
|
|
||||||
@@ -19,6 +23,14 @@ export abstract class BaseFormatter {
|
|||||||
abstract analyze(): Promise<IPlannedChange[]>;
|
abstract analyze(): Promise<IPlannedChange[]>;
|
||||||
abstract applyChange(change: IPlannedChange): Promise<void>;
|
abstract applyChange(change: IPlannedChange): Promise<void>;
|
||||||
|
|
||||||
|
get runsWithoutChanges(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(): Promise<IFormatWarning[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
async execute(changes: IPlannedChange[]): Promise<void> {
|
async execute(changes: IPlannedChange[]): Promise<void> {
|
||||||
const startTime = this.stats.moduleStartTime(this.name);
|
const startTime = this.stats.moduleStartTime(this.name);
|
||||||
this.stats.startModule(this.name);
|
this.stats.startModule(this.name);
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import * as plugins from './mod.plugins.js';
|
import * as plugins from './mod.plugins.js';
|
||||||
import { FormatContext } from './classes.formatcontext.js';
|
import { FormatContext } from './classes.formatcontext.js';
|
||||||
import { BaseFormatter } from './classes.baseformatter.js';
|
import { BaseFormatter } from './classes.baseformatter.js';
|
||||||
import type { IFormatPlan, IPlannedChange } from './interfaces.format.js';
|
import type {
|
||||||
|
IFormatPlan,
|
||||||
|
IPlannedChange,
|
||||||
|
IFormatWarning,
|
||||||
|
} from './interfaces.format.js';
|
||||||
import { getModuleIcon } from './interfaces.format.js';
|
import { getModuleIcon } from './interfaces.format.js';
|
||||||
import { logger } from '../gitzone.logging.js';
|
import { logger } from '../gitzone.logging.js';
|
||||||
import { DiffReporter } from './classes.diffreporter.js';
|
import { DiffReporter } from './classes.diffreporter.js';
|
||||||
@@ -42,15 +46,21 @@ export class FormatPlanner {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const warnings = await module.validate();
|
||||||
|
plan.warnings.push(...warnings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
plan.warnings.push({
|
plan.warnings.push({
|
||||||
level: 'error',
|
level: 'error',
|
||||||
message: `Failed to analyze module ${module.name}: ${error.message}`,
|
message: `Failed to analyze module ${module.name}: ${errorMessage}`,
|
||||||
module: module.name,
|
module: module.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plan.warnings.push(...this.detectConflictingChanges(plan.changes));
|
||||||
|
|
||||||
plan.summary.totalFiles =
|
plan.summary.totalFiles =
|
||||||
plan.summary.filesAdded +
|
plan.summary.filesAdded +
|
||||||
plan.summary.filesModified +
|
plan.summary.filesModified +
|
||||||
@@ -65,11 +75,12 @@ export class FormatPlanner {
|
|||||||
context: FormatContext,
|
context: FormatContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
const changesByModule = this.groupChangesByModule(plan.changes);
|
||||||
|
|
||||||
for (const module of modules) {
|
for (const module of modules) {
|
||||||
const changes = this.plannedChanges.get(module.name) || [];
|
const changes = changesByModule.get(module.name) || [];
|
||||||
|
|
||||||
if (changes.length > 0) {
|
if (changes.length > 0 || module.runsWithoutChanges) {
|
||||||
logger.log('info', `Executing ${module.name} formatter...`);
|
logger.log('info', `Executing ${module.name} formatter...`);
|
||||||
await module.execute(changes);
|
await module.execute(changes);
|
||||||
}
|
}
|
||||||
@@ -138,4 +149,55 @@ export class FormatPlanner {
|
|||||||
return '❌';
|
return '❌';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private groupChangesByModule(
|
||||||
|
changes: IPlannedChange[],
|
||||||
|
): Map<string, IPlannedChange[]> {
|
||||||
|
const changesByModule = new Map<string, IPlannedChange[]>();
|
||||||
|
for (const change of changes) {
|
||||||
|
const moduleChanges = changesByModule.get(change.module) || [];
|
||||||
|
moduleChanges.push(change);
|
||||||
|
changesByModule.set(change.module, moduleChanges);
|
||||||
|
}
|
||||||
|
return changesByModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
private detectConflictingChanges(
|
||||||
|
changes: IPlannedChange[],
|
||||||
|
): IFormatWarning[] {
|
||||||
|
const warnings: IFormatWarning[] = [];
|
||||||
|
const changesByPath = new Map<string, IPlannedChange[]>();
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
if (!change.path || change.path === '<various files>') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathChanges = changesByPath.get(change.path) || [];
|
||||||
|
pathChanges.push(change);
|
||||||
|
changesByPath.set(change.path, pathChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [path, pathChanges] of changesByPath) {
|
||||||
|
const modules = [...new Set(pathChanges.map((change) => change.module))];
|
||||||
|
if (modules.length < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDelete = pathChanges.some((change) => change.type === 'delete');
|
||||||
|
const plannedContents = pathChanges
|
||||||
|
.map((change) => change.content)
|
||||||
|
.filter((content): content is string => content !== undefined);
|
||||||
|
const uniqueContents = new Set(plannedContents);
|
||||||
|
const level = hasDelete || uniqueContents.size > 1 ? 'warning' : 'info';
|
||||||
|
|
||||||
|
warnings.push({
|
||||||
|
level,
|
||||||
|
module: 'planner',
|
||||||
|
message: `Multiple formatters plan changes for ${path}: ${modules.join(', ')}. They will run in formatter order.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseFormatter } from '../classes.baseformatter.js';
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
||||||
import type { IPlannedChange } from '../interfaces.format.js';
|
import type { IFormatWarning, IPlannedChange } from '../interfaces.format.js';
|
||||||
import * as plugins from '../mod.plugins.js';
|
import * as plugins from '../mod.plugins.js';
|
||||||
import * as paths from '../../paths.js';
|
import * as paths from '../../paths.js';
|
||||||
import { logger } from '../../gitzone.logging.js';
|
import { logger } from '../../gitzone.logging.js';
|
||||||
@@ -11,6 +11,10 @@ export class LicenseFormatter extends BaseFormatter {
|
|||||||
return 'license';
|
return 'license';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get runsWithoutChanges(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async analyze(): Promise<IPlannedChange[]> {
|
async analyze(): Promise<IPlannedChange[]> {
|
||||||
// License formatter only checks for incompatible licenses
|
// License formatter only checks for incompatible licenses
|
||||||
// It does not modify any files, so return empty array
|
// It does not modify any files, so return empty array
|
||||||
@@ -18,29 +22,34 @@ export class LicenseFormatter extends BaseFormatter {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validate(): Promise<IFormatWarning[]> {
|
||||||
|
const result = await this.checkLicenses();
|
||||||
|
if (!result || result.failingModules.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
level: 'error',
|
||||||
|
module: this.name,
|
||||||
|
message: `License check failed for ${result.failingModules.length} module(s): ${result.failingModules
|
||||||
|
.map((failedModule) => `${failedModule.name} (${failedModule.license})`)
|
||||||
|
.join(', ')}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
async execute(changes: IPlannedChange[]): Promise<void> {
|
async execute(changes: IPlannedChange[]): Promise<void> {
|
||||||
const startTime = this.stats.moduleStartTime(this.name);
|
const startTime = this.stats.moduleStartTime(this.name);
|
||||||
this.stats.startModule(this.name);
|
this.stats.startModule(this.name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if node_modules exists
|
const licenseCheckResult = await this.checkLicenses();
|
||||||
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
|
if (!licenseCheckResult) {
|
||||||
const nodeModulesExists = await plugins.smartfs
|
|
||||||
.directory(nodeModulesPath)
|
|
||||||
.exists();
|
|
||||||
|
|
||||||
if (!nodeModulesExists) {
|
|
||||||
logger.log('warn', 'No node_modules found. Skipping license check');
|
logger.log('warn', 'No node_modules found. Skipping license check');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run license check
|
|
||||||
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
|
|
||||||
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
|
|
||||||
paths.cwd,
|
|
||||||
INCOMPATIBLE_LICENSES,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (licenseCheckResult.failingModules.length === 0) {
|
if (licenseCheckResult.failingModules.length === 0) {
|
||||||
logger.log('info', 'License check passed - no incompatible licenses found');
|
logger.log('info', 'License check passed - no incompatible licenses found');
|
||||||
} else {
|
} else {
|
||||||
@@ -59,4 +68,23 @@ export class LicenseFormatter extends BaseFormatter {
|
|||||||
async applyChange(change: IPlannedChange): Promise<void> {
|
async applyChange(change: IPlannedChange): Promise<void> {
|
||||||
// No file changes for license formatter
|
// No file changes for license formatter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkLicenses(): Promise<{
|
||||||
|
failingModules: Array<{ name: string; license: string }>;
|
||||||
|
} | undefined> {
|
||||||
|
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
|
||||||
|
const nodeModulesExists = await plugins.smartfs
|
||||||
|
.directory(nodeModulesPath)
|
||||||
|
.exists();
|
||||||
|
|
||||||
|
if (!nodeModulesExists) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
|
||||||
|
return await licenseChecker.excludeLicenseWithinPath(
|
||||||
|
paths.cwd,
|
||||||
|
INCOMPATIBLE_LICENSES,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
);
|
);
|
||||||
allFiles.push(...filteredFiles);
|
allFiles.push(...filteredFiles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logVerbose(`Skipping directory ${dir}: ${error.message}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping directory ${dir}: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +73,8 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
|
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
|
||||||
allFiles.push(...rootLevelFiles);
|
allFiles.push(...rootLevelFiles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logVerbose(`Skipping pattern ${pattern}: ${error.message}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping pattern ${pattern}: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,20 +91,46 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Skip files that can't be accessed
|
// Skip files that can't be accessed
|
||||||
logVerbose(`Skipping ${file} - cannot access: ${error.message}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping ${file} - cannot access: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prettier = await import('prettier');
|
||||||
|
const prettierConfig = await this.getPrettierConfig();
|
||||||
|
|
||||||
for (const file of validFiles) {
|
for (const file of validFiles) {
|
||||||
changes.push({
|
try {
|
||||||
type: 'modify',
|
const fileExt = plugins.path.extname(file).toLowerCase();
|
||||||
path: file,
|
if (!fileExt) {
|
||||||
module: this.name,
|
continue;
|
||||||
description: 'Format with Prettier',
|
}
|
||||||
});
|
|
||||||
|
const content = (await plugins.smartfs
|
||||||
|
.file(file)
|
||||||
|
.encoding('utf8')
|
||||||
|
.read()) as string;
|
||||||
|
const formatted = await prettier.format(content, {
|
||||||
|
filepath: file,
|
||||||
|
...prettierConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (formatted !== content) {
|
||||||
|
changes.push({
|
||||||
|
type: 'modify',
|
||||||
|
path: file,
|
||||||
|
module: this.name,
|
||||||
|
description: 'Format with Prettier',
|
||||||
|
content: formatted,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logVerbose(`Skipping Prettier analysis for ${file}: ${errorMessage}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('info', `Found ${changes.length} files to format with Prettier`);
|
logger.log('info', `Found ${changes.length} files needing Prettier`);
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +155,10 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
this.stats.recordFileOperation(this.name, change.type, true);
|
this.stats.recordFileOperation(this.name, change.type, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.stats.recordFileOperation(this.name, change.type, false);
|
this.stats.recordFileOperation(this.name, change.type, false);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
logger.log(
|
logger.log(
|
||||||
'error',
|
'error',
|
||||||
`Failed to format ${change.path}: ${error.message}`,
|
`Failed to format ${change.path}: ${errorMessage}`,
|
||||||
);
|
);
|
||||||
// Don't throw - continue with other files
|
// Don't throw - continue with other files
|
||||||
}
|
}
|
||||||
@@ -192,28 +221,32 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
logVerbose(`No formatting changes for ${change.path}`);
|
logVerbose(`No formatting changes for ${change.path}`);
|
||||||
}
|
}
|
||||||
} catch (prettierError) {
|
} catch (prettierError) {
|
||||||
|
const prettierErrorMessage = prettierError instanceof Error
|
||||||
|
? prettierError.message
|
||||||
|
: String(prettierError);
|
||||||
// Check if it's a parser error
|
// Check if it's a parser error
|
||||||
if (
|
if (prettierErrorMessage.includes('No parser could be inferred')) {
|
||||||
prettierError.message &&
|
logVerbose(`Skipping ${change.path} - ${prettierErrorMessage}`);
|
||||||
prettierError.message.includes('No parser could be inferred')
|
|
||||||
) {
|
|
||||||
logVerbose(`Skipping ${change.path} - ${prettierError.message}`);
|
|
||||||
return; // Skip this file silently
|
return; // Skip this file silently
|
||||||
}
|
}
|
||||||
throw prettierError;
|
throw prettierError;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||||
// Log the full error stack for debugging mkdir issues
|
// Log the full error stack for debugging mkdir issues
|
||||||
if (error.message && error.message.includes('mkdir')) {
|
if (errorMessage.includes('mkdir')) {
|
||||||
logger.log(
|
logger.log(
|
||||||
'error',
|
'error',
|
||||||
`Failed to format ${change.path}: ${error.message}`,
|
`Failed to format ${change.path}: ${errorMessage}`,
|
||||||
);
|
);
|
||||||
logger.log('error', `Error stack: ${error.stack}`);
|
if (errorStack) {
|
||||||
|
logger.log('error', `Error stack: ${errorStack}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.log(
|
logger.log(
|
||||||
'error',
|
'error',
|
||||||
`Failed to format ${change.path}: ${error.message}`,
|
`Failed to format ${change.path}: ${errorMessage}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -234,52 +267,7 @@ export class PrettierFormatter extends BaseFormatter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Override check() to compute diffs on-the-fly by running prettier
|
|
||||||
*/
|
|
||||||
async check(): Promise<ICheckResult> {
|
async check(): Promise<ICheckResult> {
|
||||||
const changes = await this.analyze();
|
return await super.check();
|
||||||
const diffs: ICheckResult['diffs'] = [];
|
|
||||||
|
|
||||||
for (const change of changes) {
|
|
||||||
if (change.type !== 'modify') continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Read current content
|
|
||||||
const currentContent = (await plugins.smartfs
|
|
||||||
.file(change.path)
|
|
||||||
.encoding('utf8')
|
|
||||||
.read()) as string;
|
|
||||||
|
|
||||||
// Skip files without extension (prettier can't infer parser)
|
|
||||||
const fileExt = plugins.path.extname(change.path).toLowerCase();
|
|
||||||
if (!fileExt) continue;
|
|
||||||
|
|
||||||
// Format with prettier to get what it would produce
|
|
||||||
const prettier = await import('prettier');
|
|
||||||
const formatted = await prettier.format(currentContent, {
|
|
||||||
filepath: change.path,
|
|
||||||
...(await this.getPrettierConfig()),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only add to diffs if content differs
|
|
||||||
if (formatted !== currentContent) {
|
|
||||||
diffs.push({
|
|
||||||
path: change.path,
|
|
||||||
type: 'modify',
|
|
||||||
before: currentContent,
|
|
||||||
after: formatted,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Skip files that can't be processed
|
|
||||||
logVerbose(`Skipping diff for ${change.path}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasDiff: diffs.length > 0,
|
|
||||||
diffs,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+289
-8
@@ -22,6 +22,7 @@ import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
|
|||||||
import { PrettierFormatter } from "./formatters/prettier.formatter.js";
|
import { PrettierFormatter } from "./formatters/prettier.formatter.js";
|
||||||
import { ReadmeFormatter } from "./formatters/readme.formatter.js";
|
import { ReadmeFormatter } from "./formatters/readme.formatter.js";
|
||||||
import { CopyFormatter } from "./formatters/copy.formatter.js";
|
import { CopyFormatter } from "./formatters/copy.formatter.js";
|
||||||
|
import type { ICheckResult, IFormatPlan } from "./interfaces.format.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rename npmextra.json or smartconfig.json to .smartconfig.json
|
* Rename npmextra.json or smartconfig.json to .smartconfig.json
|
||||||
@@ -94,9 +95,39 @@ const getFormatConfig = async () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeModuleList = (value: unknown): string[] => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.flatMap((item) => normalizeModuleList(item));
|
||||||
|
}
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlanStatus = (plan: IFormatPlan) => {
|
||||||
|
const errorWarnings = plan.warnings.filter(
|
||||||
|
(warning) => warning.level === "error",
|
||||||
|
);
|
||||||
|
const hasChanges = plan.summary.totalFiles > 0;
|
||||||
|
const hasErrors = errorWarnings.length > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: !hasChanges && !hasErrors,
|
||||||
|
hasChanges,
|
||||||
|
hasErrors,
|
||||||
|
errorCount: errorWarnings.length,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const createActiveFormatters = async (options: {
|
const createActiveFormatters = async (options: {
|
||||||
interactive: boolean;
|
interactive: boolean;
|
||||||
jsonOutput: boolean;
|
jsonOutput: boolean;
|
||||||
|
only?: string[];
|
||||||
|
skip?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
const project = await Project.fromCwd({ requireProjectType: false });
|
const project = await Project.fromCwd({ requireProjectType: false });
|
||||||
const context = new FormatContext(options);
|
const context = new FormatContext(options);
|
||||||
@@ -107,11 +138,19 @@ const createActiveFormatters = async (options: {
|
|||||||
([, FormatterClass]) => new FormatterClass(context, project),
|
([, FormatterClass]) => new FormatterClass(context, project),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onlyModules = options.only?.length
|
||||||
|
? options.only
|
||||||
|
: formatConfig.modules.only;
|
||||||
|
const skipModules = [
|
||||||
|
...formatConfig.modules.skip,
|
||||||
|
...(options.skip || []),
|
||||||
|
];
|
||||||
|
|
||||||
const activeFormatters = formatters.filter((formatter) => {
|
const activeFormatters = formatters.filter((formatter) => {
|
||||||
if (formatConfig.modules.only.length > 0) {
|
if (onlyModules.length > 0) {
|
||||||
return formatConfig.modules.only.includes(formatter.name);
|
return onlyModules.includes(formatter.name);
|
||||||
}
|
}
|
||||||
if (formatConfig.modules.skip.includes(formatter.name)) {
|
if (skipModules.includes(formatter.name)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -129,11 +168,15 @@ const buildFormatPlan = async (options: {
|
|||||||
fromPlan?: string;
|
fromPlan?: string;
|
||||||
interactive: boolean;
|
interactive: boolean;
|
||||||
jsonOutput: boolean;
|
jsonOutput: boolean;
|
||||||
|
only?: string[];
|
||||||
|
skip?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
const { context, planner, formatConfig, activeFormatters } =
|
const { context, planner, formatConfig, activeFormatters } =
|
||||||
await createActiveFormatters({
|
await createActiveFormatters({
|
||||||
interactive: options.interactive,
|
interactive: options.interactive,
|
||||||
jsonOutput: options.jsonOutput,
|
jsonOutput: options.jsonOutput,
|
||||||
|
only: options.only,
|
||||||
|
skip: options.skip,
|
||||||
});
|
});
|
||||||
|
|
||||||
const plan = options.fromPlan
|
const plan = options.fromPlan
|
||||||
@@ -167,6 +210,182 @@ const serializePlan = (plan: any) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildFormatFixPrompt = (
|
||||||
|
plan: IFormatPlan,
|
||||||
|
extraInstructions: string,
|
||||||
|
): string => {
|
||||||
|
const promptParts = [
|
||||||
|
"Other /c-* commands can be found at ~/.config/opencode/commands/*",
|
||||||
|
"# gitzone format fix",
|
||||||
|
"",
|
||||||
|
`Working directory: ${process.cwd()}`,
|
||||||
|
"",
|
||||||
|
"Repair project formatting so `gitzone format check --json` passes.",
|
||||||
|
"",
|
||||||
|
"Rules:",
|
||||||
|
"- Read `.smartconfig.json`, `package.json`, `tsconfig.json`, and the current format plan before editing.",
|
||||||
|
"- Prefer deterministic gitzone standards, bundled assets, and existing project conventions.",
|
||||||
|
"- Keep changes focused on formatting, metadata normalization, templates, and config consistency.",
|
||||||
|
"- Do not commit, release, install dependencies, or modify unrelated files.",
|
||||||
|
"- Use pnpm commands only if commands are needed.",
|
||||||
|
"- Run `gitzone format --write --yes` after changes.",
|
||||||
|
"- Run `gitzone format check --json` after changes and keep fixing until it passes.",
|
||||||
|
"- Run `git diff --check` after changes to catch whitespace problems.",
|
||||||
|
"",
|
||||||
|
"Current format plan:",
|
||||||
|
JSON.stringify(serializePlan(plan), null, 2),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (extraInstructions) {
|
||||||
|
promptParts.push("", "Additional user instructions:", extraInstructions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promptParts.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormatFix = async (
|
||||||
|
options: Record<string, any>,
|
||||||
|
mode: ICliMode,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({
|
||||||
|
ok: false,
|
||||||
|
error:
|
||||||
|
"JSON output is not supported for `gitzone format fix`. Use `gitzone format check --json` for machine-readable diagnostics.",
|
||||||
|
});
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraInstructions = (options._?.slice(2).join(" ") || "").trim();
|
||||||
|
const force = Boolean(options.force);
|
||||||
|
const autoApprove = Boolean(options.yes || mode.yes);
|
||||||
|
const formatConfig = await getFormatConfig();
|
||||||
|
const interactive =
|
||||||
|
options.interactive ?? (mode.interactive && formatConfig.interactive);
|
||||||
|
const only = normalizeModuleList(options.only);
|
||||||
|
const skip = normalizeModuleList(options.skip);
|
||||||
|
|
||||||
|
const buildCurrentPlan = async () => {
|
||||||
|
return await buildFormatPlan({
|
||||||
|
interactive,
|
||||||
|
jsonOutput: false,
|
||||||
|
only,
|
||||||
|
skip,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.log("info", "Analyzing project for format fixes...");
|
||||||
|
let { plan } = await buildCurrentPlan();
|
||||||
|
let status = getPlanStatus(plan);
|
||||||
|
|
||||||
|
if (status.ok && !extraInstructions && !force) {
|
||||||
|
logger.log(
|
||||||
|
"success",
|
||||||
|
"Format check found no issues. Use `gitzone format fix --force` to run opencode anyway.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!autoApprove) {
|
||||||
|
if (!mode.interactive) {
|
||||||
|
throw new Error(
|
||||||
|
"Format fix requires an interactive terminal or `-y` to run non-interactively.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
`Run format fixes? (${plan.summary.totalFiles} planned change(s), ${status.errorCount} error warning(s))`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
logger.log("info", "Format fix cancelled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.hasChanges) {
|
||||||
|
logger.log("info", "Applying deterministic format changes first...");
|
||||||
|
await run({
|
||||||
|
_: ["format"],
|
||||||
|
write: true,
|
||||||
|
yes: true,
|
||||||
|
interactive: false,
|
||||||
|
verbose: options.verbose,
|
||||||
|
detailed: options.detailed,
|
||||||
|
only: options.only,
|
||||||
|
skip: options.skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
({ plan } = await buildCurrentPlan());
|
||||||
|
status = getPlanStatus(plan);
|
||||||
|
if (status.ok && !extraInstructions && !force) {
|
||||||
|
logger.log("success", "Format fix completed successfully.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const opencodeArgs = [
|
||||||
|
"run",
|
||||||
|
"--title",
|
||||||
|
"gitzone format fix",
|
||||||
|
"--dir",
|
||||||
|
process.cwd(),
|
||||||
|
];
|
||||||
|
if (autoApprove) {
|
||||||
|
opencodeArgs.push("--dangerously-skip-permissions");
|
||||||
|
}
|
||||||
|
opencodeArgs.push(buildFormatFixPrompt(plan, extraInstructions));
|
||||||
|
|
||||||
|
logger.log("info", "Starting opencode format fix...");
|
||||||
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
|
executor: "bash",
|
||||||
|
sourceFilePaths: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
let result: plugins.smartshell.IExecResult;
|
||||||
|
try {
|
||||||
|
result = await smartshellInstance.execSpawn("opencode", opencodeArgs, {
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to run opencode: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.log("error", `opencode exited with code ${result.exitCode}`);
|
||||||
|
process.exitCode = result.exitCode || 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("info", "Running deterministic format pass after opencode...");
|
||||||
|
await run({
|
||||||
|
_: ["format"],
|
||||||
|
write: true,
|
||||||
|
yes: true,
|
||||||
|
interactive: false,
|
||||||
|
verbose: options.verbose,
|
||||||
|
detailed: options.detailed,
|
||||||
|
only: options.only,
|
||||||
|
skip: options.skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { planner: finalPlanner, plan: finalPlan } = await buildCurrentPlan();
|
||||||
|
await finalPlanner.displayPlan(finalPlan, options.detailed);
|
||||||
|
const finalStatus = getPlanStatus(finalPlan);
|
||||||
|
if (finalStatus.ok) {
|
||||||
|
logger.log("success", "Format fix completed successfully.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
"error",
|
||||||
|
`Format fix left ${finalPlan.summary.totalFiles} planned change(s) and ${finalStatus.errorCount} error warning(s).`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
};
|
||||||
|
|
||||||
export let run = async (
|
export let run = async (
|
||||||
options: {
|
options: {
|
||||||
write?: boolean;
|
write?: boolean;
|
||||||
@@ -194,8 +413,25 @@ export let run = async (
|
|||||||
setVerboseMode(true);
|
setVerboseMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subcommand === "fix") {
|
||||||
|
await handleFormatFix(options, mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const shouldWrite = options.write ?? options.dryRun === false;
|
const shouldWrite = options.write ?? options.dryRun === false;
|
||||||
const treatAsPlan = subcommand === "plan";
|
const treatAsPlan = subcommand === "plan";
|
||||||
|
const treatAsCheck = subcommand === "check" || Boolean(options.check);
|
||||||
|
|
||||||
|
if (treatAsCheck && shouldWrite) {
|
||||||
|
const error = "`gitzone format check` is read-only and cannot be combined with --write.";
|
||||||
|
if (mode.json) {
|
||||||
|
printJson({ ok: false, error });
|
||||||
|
} else {
|
||||||
|
logger.log("error", error);
|
||||||
|
}
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (mode.json && shouldWrite) {
|
if (mode.json && shouldWrite) {
|
||||||
printJson({
|
printJson({
|
||||||
@@ -212,7 +448,9 @@ export let run = async (
|
|||||||
const formatConfig = await getFormatConfig();
|
const formatConfig = await getFormatConfig();
|
||||||
const interactive =
|
const interactive =
|
||||||
options.interactive ?? (mode.interactive && formatConfig.interactive);
|
options.interactive ?? (mode.interactive && formatConfig.interactive);
|
||||||
const autoApprove = options.yes ?? formatConfig.autoApprove;
|
const autoApprove = options.yes ?? (mode.yes || formatConfig.autoApprove);
|
||||||
|
const only = normalizeModuleList(options.only);
|
||||||
|
const skip = normalizeModuleList(options.skip);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const planBuilder = async () => {
|
const planBuilder = async () => {
|
||||||
@@ -220,6 +458,8 @@ export let run = async (
|
|||||||
fromPlan: options.fromPlan,
|
fromPlan: options.fromPlan,
|
||||||
interactive,
|
interactive,
|
||||||
jsonOutput: mode.json,
|
jsonOutput: mode.json,
|
||||||
|
only,
|
||||||
|
skip,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -231,7 +471,16 @@ export let run = async (
|
|||||||
: await planBuilder();
|
: await planBuilder();
|
||||||
|
|
||||||
if (mode.json) {
|
if (mode.json) {
|
||||||
printJson(serializePlan(plan));
|
const serializedPlan = serializePlan(plan);
|
||||||
|
if (treatAsCheck) {
|
||||||
|
const status = getPlanStatus(plan);
|
||||||
|
printJson({ ok: status.ok, ...serializedPlan });
|
||||||
|
if (!status.ok) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
printJson(serializedPlan);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +500,20 @@ export let run = async (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (treatAsCheck) {
|
||||||
|
const status = getPlanStatus(plan);
|
||||||
|
if (status.ok) {
|
||||||
|
logger.log("success", "Format check passed");
|
||||||
|
} else {
|
||||||
|
logger.log(
|
||||||
|
"error",
|
||||||
|
`Format check failed: ${plan.summary.totalFiles} planned change(s), ${status.errorCount} error warning(s)`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Show diffs if explicitly requested or before interactive write confirmation
|
// Show diffs if explicitly requested or before interactive write confirmation
|
||||||
const showDiffs =
|
const showDiffs =
|
||||||
options.diff || (shouldWrite && interactive && !autoApprove);
|
options.diff || (shouldWrite && interactive && !autoApprove);
|
||||||
@@ -314,7 +577,6 @@ export let run = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
import type { ICheckResult } from "./interfaces.format.js";
|
|
||||||
export type { ICheckResult };
|
export type { ICheckResult };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -363,7 +625,7 @@ export function showHelp(mode?: ICliMode): void {
|
|||||||
if (mode?.json) {
|
if (mode?.json) {
|
||||||
printJson({
|
printJson({
|
||||||
command: "format",
|
command: "format",
|
||||||
usage: "gitzone format [plan] [options]",
|
usage: "gitzone format [plan|check|fix] [options]",
|
||||||
description:
|
description:
|
||||||
"Plans formatting changes by default and applies them only with --write.",
|
"Plans formatting changes by default and applies them only with --write.",
|
||||||
flags: [
|
flags: [
|
||||||
@@ -393,19 +655,33 @@ export function showHelp(mode?: ICliMode): void {
|
|||||||
flag: "--diff",
|
flag: "--diff",
|
||||||
description: "Show per-file diffs before applying changes",
|
description: "Show per-file diffs before applying changes",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
flag: "--only <modules>",
|
||||||
|
description: "Run only the comma-separated formatter modules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--skip <modules>",
|
||||||
|
description: "Skip the comma-separated formatter modules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flag: "--force",
|
||||||
|
description: "Run `format fix` even when the deterministic plan is clean",
|
||||||
|
},
|
||||||
{ flag: "--json", description: "Emit a read-only format plan as JSON" },
|
{ flag: "--json", description: "Emit a read-only format plan as JSON" },
|
||||||
],
|
],
|
||||||
examples: [
|
examples: [
|
||||||
"gitzone format",
|
"gitzone format",
|
||||||
"gitzone format plan --json",
|
"gitzone format plan --json",
|
||||||
|
"gitzone format check",
|
||||||
"gitzone format --write --yes",
|
"gitzone format --write --yes",
|
||||||
|
"gitzone format fix",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("Usage: gitzone format [plan] [options]");
|
console.log("Usage: gitzone format [plan|check|fix] [options]");
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log(
|
console.log(
|
||||||
"Plans formatting changes by default and applies them only with --write.",
|
"Plans formatting changes by default and applies them only with --write.",
|
||||||
@@ -424,11 +700,16 @@ export function showHelp(mode?: ICliMode): void {
|
|||||||
console.log(
|
console.log(
|
||||||
" --diff Show per-file diffs before applying changes",
|
" --diff Show per-file diffs before applying changes",
|
||||||
);
|
);
|
||||||
|
console.log(" --only <modules> Run only comma-separated formatter modules");
|
||||||
|
console.log(" --skip <modules> Skip comma-separated formatter modules");
|
||||||
|
console.log(" --force Run format fix even when the plan is clean");
|
||||||
console.log(" --json Emit a read-only format plan as JSON");
|
console.log(" --json Emit a read-only format plan as JSON");
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("Examples:");
|
console.log("Examples:");
|
||||||
console.log(" gitzone format");
|
console.log(" gitzone format");
|
||||||
console.log(" gitzone format plan --json");
|
console.log(" gitzone format plan --json");
|
||||||
|
console.log(" gitzone format check");
|
||||||
console.log(" gitzone format --write --yes");
|
console.log(" gitzone format --write --yes");
|
||||||
|
console.log(" gitzone format fix");
|
||||||
console.log("");
|
console.log("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
export type IFormatWarning = {
|
||||||
|
level: 'info' | 'warning' | 'error';
|
||||||
|
message: string;
|
||||||
|
module: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type IFormatPlan = {
|
export type IFormatPlan = {
|
||||||
summary: {
|
summary: {
|
||||||
totalFiles: number;
|
totalFiles: number;
|
||||||
@@ -5,17 +11,8 @@ export type IFormatPlan = {
|
|||||||
filesModified: number;
|
filesModified: number;
|
||||||
filesRemoved: number;
|
filesRemoved: number;
|
||||||
};
|
};
|
||||||
changes: Array<{
|
changes: IPlannedChange[];
|
||||||
type: 'create' | 'modify' | 'delete';
|
warnings: IFormatWarning[];
|
||||||
path: string;
|
|
||||||
module: string;
|
|
||||||
description: string;
|
|
||||||
}>;
|
|
||||||
warnings: Array<{
|
|
||||||
level: 'info' | 'warning' | 'error';
|
|
||||||
message: string;
|
|
||||||
module: string;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IPlannedChange = {
|
export type IPlannedChange = {
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ export async function showHelp(
|
|||||||
console.log(" gitzone commit recommend --json");
|
console.log(" gitzone commit recommend --json");
|
||||||
console.log(" gitzone release --plan");
|
console.log(" gitzone release --plan");
|
||||||
console.log(" gitzone format plan --json");
|
console.log(" gitzone format plan --json");
|
||||||
|
console.log(" gitzone format check");
|
||||||
console.log(" gitzone services set mongodb,minio");
|
console.log(" gitzone services set mongodb,minio");
|
||||||
console.log(" gitzone tools update");
|
console.log(" gitzone tools update");
|
||||||
console.log("");
|
console.log("");
|
||||||
|
|||||||
Reference in New Issue
Block a user