716 lines
20 KiB
TypeScript
716 lines
20 KiB
TypeScript
import * as plugins from "./mod.plugins.js";
|
|
import { Project } from "../classes.project.js";
|
|
import { FormatContext } from "./classes.formatcontext.js";
|
|
import { FormatPlanner } from "./classes.formatplanner.js";
|
|
import { BaseFormatter } from "./classes.baseformatter.js";
|
|
import { logger, setVerboseMode } from "../gitzone.logging.js";
|
|
import type { ICliMode } from "../helpers.climode.js";
|
|
import {
|
|
getCliMode,
|
|
printJson,
|
|
runWithSuppressedOutput,
|
|
} from "../helpers.climode.js";
|
|
import { getCliConfigValue } from "../helpers.smartconfig.js";
|
|
|
|
import { CleanupFormatter } from "./formatters/cleanup.formatter.js";
|
|
import { SmartconfigFormatter } from "./formatters/smartconfig.formatter.js";
|
|
import { LicenseFormatter } from "./formatters/license.formatter.js";
|
|
import { PackageJsonFormatter } from "./formatters/packagejson.formatter.js";
|
|
import { TemplatesFormatter } from "./formatters/templates.formatter.js";
|
|
import { GitignoreFormatter } from "./formatters/gitignore.formatter.js";
|
|
import { TsconfigFormatter } from "./formatters/tsconfig.formatter.js";
|
|
import { PrettierFormatter } from "./formatters/prettier.formatter.js";
|
|
import { ReadmeFormatter } from "./formatters/readme.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
|
|
* before any formatter tries to read config.
|
|
*/
|
|
async function migrateConfigFile(allowWrite: boolean): Promise<void> {
|
|
const target = ".smartconfig.json";
|
|
const targetExists = await plugins.smartfs.file(target).exists();
|
|
if (targetExists) return;
|
|
|
|
for (const oldName of ["smartconfig.json", "npmextra.json"]) {
|
|
const exists = await plugins.smartfs.file(oldName).exists();
|
|
if (exists) {
|
|
if (!allowWrite) {
|
|
return;
|
|
}
|
|
const content = (await plugins.smartfs
|
|
.file(oldName)
|
|
.encoding("utf8")
|
|
.read()) as string;
|
|
await plugins.smartfs.file(`./${target}`).encoding("utf8").write(content);
|
|
await plugins.smartfs.file(oldName).delete();
|
|
logger.log("info", `Migrated ${oldName} to ${target}`);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shared formatter class map used by both run() and runFormatter()
|
|
const formatterMap: Record<
|
|
string,
|
|
new (ctx: FormatContext, proj: Project) => BaseFormatter
|
|
> = {
|
|
cleanup: CleanupFormatter,
|
|
smartconfig: SmartconfigFormatter,
|
|
license: LicenseFormatter,
|
|
packagejson: PackageJsonFormatter,
|
|
templates: TemplatesFormatter,
|
|
gitignore: GitignoreFormatter,
|
|
tsconfig: TsconfigFormatter,
|
|
prettier: PrettierFormatter,
|
|
readme: ReadmeFormatter,
|
|
copy: CopyFormatter,
|
|
};
|
|
|
|
// Formatters that don't require projectType to be set
|
|
const formattersNotRequiringProjectType = [
|
|
"smartconfig",
|
|
"prettier",
|
|
"cleanup",
|
|
"packagejson",
|
|
];
|
|
|
|
const getFormatConfig = async () => {
|
|
const rawFormatConfig = await getCliConfigValue<Record<string, any>>(
|
|
"format",
|
|
{},
|
|
);
|
|
return {
|
|
interactive: true,
|
|
showDiffs: false,
|
|
autoApprove: false,
|
|
showStats: true,
|
|
modules: {
|
|
skip: [],
|
|
only: [],
|
|
...(rawFormatConfig.modules || {}),
|
|
},
|
|
...rawFormatConfig,
|
|
};
|
|
};
|
|
|
|
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: {
|
|
interactive: boolean;
|
|
jsonOutput: boolean;
|
|
only?: string[];
|
|
skip?: string[];
|
|
}) => {
|
|
const project = await Project.fromCwd({ requireProjectType: false });
|
|
const context = new FormatContext(options);
|
|
const planner = new FormatPlanner();
|
|
|
|
const formatConfig = await getFormatConfig();
|
|
const formatters = Object.entries(formatterMap).map(
|
|
([, 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) => {
|
|
if (onlyModules.length > 0) {
|
|
return onlyModules.includes(formatter.name);
|
|
}
|
|
if (skipModules.includes(formatter.name)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
return {
|
|
context,
|
|
planner,
|
|
formatConfig,
|
|
activeFormatters,
|
|
};
|
|
};
|
|
|
|
const buildFormatPlan = async (options: {
|
|
fromPlan?: string;
|
|
interactive: boolean;
|
|
jsonOutput: boolean;
|
|
only?: string[];
|
|
skip?: string[];
|
|
}) => {
|
|
const { context, planner, formatConfig, activeFormatters } =
|
|
await createActiveFormatters({
|
|
interactive: options.interactive,
|
|
jsonOutput: options.jsonOutput,
|
|
only: options.only,
|
|
skip: options.skip,
|
|
});
|
|
|
|
const plan = options.fromPlan
|
|
? JSON.parse(
|
|
(await plugins.smartfs
|
|
.file(options.fromPlan)
|
|
.encoding("utf8")
|
|
.read()) as string,
|
|
)
|
|
: await planner.planFormat(activeFormatters);
|
|
|
|
return {
|
|
context,
|
|
planner,
|
|
formatConfig,
|
|
activeFormatters,
|
|
plan,
|
|
};
|
|
};
|
|
|
|
const serializePlan = (plan: any) => {
|
|
return {
|
|
summary: plan.summary,
|
|
warnings: plan.warnings,
|
|
changes: plan.changes.map((change: any) => ({
|
|
type: change.type,
|
|
path: change.path,
|
|
module: change.module,
|
|
description: change.description,
|
|
})),
|
|
};
|
|
};
|
|
|
|
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 (
|
|
options: {
|
|
write?: boolean;
|
|
dryRun?: boolean; // Deprecated, kept for compatibility
|
|
yes?: boolean;
|
|
planOnly?: boolean;
|
|
savePlan?: string;
|
|
fromPlan?: string;
|
|
detailed?: boolean;
|
|
interactive?: boolean;
|
|
verbose?: boolean;
|
|
diff?: boolean;
|
|
[key: string]: any;
|
|
} = {},
|
|
): Promise<any> => {
|
|
const mode = await getCliMode(options as any);
|
|
const subcommand = (options as any)?._?.[1];
|
|
|
|
if (mode.help || subcommand === "help") {
|
|
showHelp(mode);
|
|
return;
|
|
}
|
|
|
|
if (options.verbose) {
|
|
setVerboseMode(true);
|
|
}
|
|
|
|
if (subcommand === "fix") {
|
|
await handleFormatFix(options, mode);
|
|
return;
|
|
}
|
|
|
|
const shouldWrite = options.write ?? options.dryRun === false;
|
|
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) {
|
|
printJson({
|
|
ok: false,
|
|
error:
|
|
"JSON output is only supported for read-only format planning. Use `gitzone format plan --json` or omit `--json` when applying changes.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Migrate config file before anything reads it
|
|
await migrateConfigFile(shouldWrite);
|
|
|
|
const formatConfig = await getFormatConfig();
|
|
const interactive =
|
|
options.interactive ?? (mode.interactive && formatConfig.interactive);
|
|
const autoApprove = options.yes ?? (mode.yes || formatConfig.autoApprove);
|
|
const only = normalizeModuleList(options.only);
|
|
const skip = normalizeModuleList(options.skip);
|
|
|
|
try {
|
|
const planBuilder = async () => {
|
|
return await buildFormatPlan({
|
|
fromPlan: options.fromPlan,
|
|
interactive,
|
|
jsonOutput: mode.json,
|
|
only,
|
|
skip,
|
|
});
|
|
};
|
|
|
|
if (!mode.json) {
|
|
logger.log("info", "Analyzing project for format operations...");
|
|
}
|
|
const { context, planner, activeFormatters, plan } = mode.json
|
|
? await runWithSuppressedOutput(planBuilder)
|
|
: await planBuilder();
|
|
|
|
if (mode.json) {
|
|
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;
|
|
}
|
|
|
|
// Display plan
|
|
await planner.displayPlan(plan, options.detailed);
|
|
|
|
// Save plan if requested
|
|
if (options.savePlan) {
|
|
await plugins.smartfs
|
|
.file(options.savePlan)
|
|
.encoding("utf8")
|
|
.write(JSON.stringify(plan, null, 2));
|
|
logger.log("info", `Plan saved to ${options.savePlan}`);
|
|
}
|
|
|
|
if (options.planOnly || treatAsPlan) {
|
|
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
|
|
const showDiffs =
|
|
options.diff || (shouldWrite && interactive && !autoApprove);
|
|
if (showDiffs) {
|
|
logger.log("info", "Showing file diffs:");
|
|
console.log("");
|
|
|
|
for (const formatter of activeFormatters) {
|
|
const checkResult = await formatter.check();
|
|
if (checkResult.hasDiff) {
|
|
logger.log("info", `[${formatter.name}]`);
|
|
formatter.displayAllDiffs(checkResult);
|
|
console.log("");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dry-run mode (default behavior)
|
|
if (!shouldWrite) {
|
|
logger.log("info", "Dry-run mode - use --write (-w) to apply changes");
|
|
return;
|
|
}
|
|
|
|
// Interactive confirmation
|
|
if (interactive && !autoApprove) {
|
|
const interactInstance = new plugins.smartinteract.SmartInteract();
|
|
const response = await interactInstance.askQuestion({
|
|
type: "confirm",
|
|
name: "proceed",
|
|
message: "Proceed with formatting?",
|
|
default: true,
|
|
});
|
|
|
|
if (!(response as any).value) {
|
|
logger.log("info", "Format operation cancelled by user");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Execute phase
|
|
logger.log("info", "Executing format operations...");
|
|
await planner.executePlan(plan, activeFormatters, context);
|
|
|
|
context.getFormatStats().finish();
|
|
|
|
const showStats = formatConfig.showStats ?? true;
|
|
if (showStats) {
|
|
context.getFormatStats().displayStats();
|
|
}
|
|
|
|
if (options.detailed) {
|
|
const statsPath = `.nogit/format-stats-${Date.now()}.json`;
|
|
await context.getFormatStats().saveReport(statsPath);
|
|
}
|
|
|
|
logger.log("success", "Format operations completed successfully!");
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
logger.log("error", `Format operation failed: ${errorMessage}`);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export type { ICheckResult };
|
|
|
|
/**
|
|
* Run a single formatter by name (for use by other modules)
|
|
*/
|
|
export const runFormatter = async (
|
|
formatterName: string,
|
|
options: {
|
|
silent?: boolean;
|
|
checkOnly?: boolean;
|
|
showDiff?: boolean;
|
|
} = {},
|
|
): Promise<ICheckResult | void> => {
|
|
const requireProjectType =
|
|
!formattersNotRequiringProjectType.includes(formatterName);
|
|
const project = await Project.fromCwd({ requireProjectType });
|
|
const context = new FormatContext({ interactive: true, jsonOutput: false });
|
|
|
|
const FormatterClass = formatterMap[formatterName];
|
|
if (!FormatterClass) {
|
|
throw new Error(`Unknown formatter: ${formatterName}`);
|
|
}
|
|
|
|
const formatter = new FormatterClass(context, project);
|
|
|
|
if (options.checkOnly) {
|
|
const result = await formatter.check();
|
|
if (result.hasDiff && options.showDiff) {
|
|
formatter.displayAllDiffs(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const changes = await formatter.analyze();
|
|
|
|
for (const change of changes) {
|
|
await formatter.applyChange(change);
|
|
}
|
|
|
|
if (!options.silent) {
|
|
logger.log("success", `Formatter '${formatterName}' completed`);
|
|
}
|
|
};
|
|
|
|
export function showHelp(mode?: ICliMode): void {
|
|
if (mode?.json) {
|
|
printJson({
|
|
command: "format",
|
|
usage: "gitzone format [plan|check|fix] [options]",
|
|
description:
|
|
"Plans formatting changes by default and applies them only with --write.",
|
|
flags: [
|
|
{ flag: "--write, -w", description: "Apply planned changes" },
|
|
{
|
|
flag: "--yes",
|
|
description: "Skip the interactive confirmation before writing",
|
|
},
|
|
{
|
|
flag: "--plan-only",
|
|
description: "Show the plan without applying changes",
|
|
},
|
|
{
|
|
flag: "--save-plan <file>",
|
|
description: "Write the format plan to a file",
|
|
},
|
|
{
|
|
flag: "--from-plan <file>",
|
|
description: "Load a previously saved plan",
|
|
},
|
|
{
|
|
flag: "--detailed",
|
|
description: "Show detailed diffs and save stats",
|
|
},
|
|
{ flag: "--verbose", description: "Enable verbose logging" },
|
|
{
|
|
flag: "--diff",
|
|
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" },
|
|
],
|
|
examples: [
|
|
"gitzone format",
|
|
"gitzone format plan --json",
|
|
"gitzone format check",
|
|
"gitzone format --write --yes",
|
|
"gitzone format fix",
|
|
],
|
|
});
|
|
return;
|
|
}
|
|
|
|
console.log("");
|
|
console.log("Usage: gitzone format [plan|check|fix] [options]");
|
|
console.log("");
|
|
console.log(
|
|
"Plans formatting changes by default and applies them only with --write.",
|
|
);
|
|
console.log("");
|
|
console.log("Flags:");
|
|
console.log(" --write, -w Apply planned changes");
|
|
console.log(
|
|
" --yes Skip the interactive confirmation before writing",
|
|
);
|
|
console.log(" --plan-only Show the plan without applying changes");
|
|
console.log(" --save-plan <file> Write the format plan to a file");
|
|
console.log(" --from-plan <file> Load a previously saved plan");
|
|
console.log(" --detailed Show detailed diffs and save stats");
|
|
console.log(" --verbose Enable verbose logging");
|
|
console.log(
|
|
" --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("");
|
|
console.log("Examples:");
|
|
console.log(" gitzone format");
|
|
console.log(" gitzone format plan --json");
|
|
console.log(" gitzone format check");
|
|
console.log(" gitzone format --write --yes");
|
|
console.log(" gitzone format fix");
|
|
console.log("");
|
|
}
|