markdownlint-obsidian-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # markdownlint-obsidian-cli
2
+
3
+ Command-line interface for linting Obsidian Flavored Markdown. Wraps the
4
+ `markdownlint-obsidian` library with a `commander`-based CLI.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ # Global install
10
+ npm install -g markdownlint-obsidian-cli
11
+ bun add -g markdownlint-obsidian-cli
12
+
13
+ # Project install (use via npx / bunx)
14
+ npm install -D markdownlint-obsidian-cli
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ # Lint all markdown files
21
+ markdownlint-obsidian "**/*.md"
22
+
23
+ # Fix auto-fixable issues in place
24
+ markdownlint-obsidian --fix "**/*.md"
25
+
26
+ # Check what would be fixed (no writes)
27
+ markdownlint-obsidian --fix-check "**/*.md"
28
+
29
+ # Machine-readable output
30
+ markdownlint-obsidian --output-formatter sarif "**/*.md" > report.sarif
31
+ markdownlint-obsidian --output-formatter junit "**/*.md" > junit.xml
32
+
33
+ # Custom config location
34
+ markdownlint-obsidian --config /path/to/project "**/*.md"
35
+ ```
36
+
37
+ ## Exit codes
38
+
39
+ | Code | Meaning |
40
+ | --- | --- |
41
+ | `0` | Clean (no errors; warnings do not raise exit code) |
42
+ | `1` | One or more lint errors found |
43
+ | `2` | Tool or configuration failure |
44
+
45
+ ## License
46
+
47
+ MIT
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bun
2
+ // Dev entry: Bun executes the TypeScript source directly — no compilation step.
3
+ // The published hermetic entry is dist/bin.mjs (Node-compatible shebang,
4
+ // imports compiled dist/src/cli/main.js), generated by `bun run build`.
5
+ import { main } from "../src/main.ts";
6
+ const code = await main(process.argv);
7
+ process.exit(code);
package/dist/bin.mjs ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./src/main.js";
3
+ const code = await main(process.argv);
4
+ process.exit(code);
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "type": "module"
4
+ }
@@ -0,0 +1,34 @@
1
+ import { Command } from "commander";
2
+ /**
3
+ * Parsed CLI options returned by commander.
4
+ *
5
+ * Fields mirror markdownlint-cli2's flag set so downstream users can reuse
6
+ * existing muscle memory and automation.
7
+ */
8
+ export interface CLIArgs {
9
+ readonly globs: string[];
10
+ readonly config?: string;
11
+ readonly configPointer?: string;
12
+ readonly fix: boolean;
13
+ readonly fixCheck: boolean;
14
+ readonly format: boolean;
15
+ readonly noGlobs: boolean;
16
+ readonly vaultRoot?: string;
17
+ /**
18
+ * Commander maps `--no-resolve` to `resolve: false`. Left `undefined`
19
+ * when the flag is not supplied so config-level `resolve` can win.
20
+ */
21
+ readonly resolve?: boolean;
22
+ readonly outputFormatter: string;
23
+ }
24
+ /**
25
+ * Build the top-level commander {@link Command} for the CLI.
26
+ *
27
+ * The returned program has `exitOverride()` configured by the caller so
28
+ * `--help` and `--version` can be caught in tests without terminating the
29
+ * process.
30
+ *
31
+ * @returns A fully wired commander `Command`.
32
+ */
33
+ export declare function buildProgram(): Command;
34
+ //# sourceMappingURL=args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAqBtC"}
@@ -0,0 +1,32 @@
1
+ import { Command } from "commander";
2
+ import { createRequire } from "node:module";
3
+ const require = createRequire(import.meta.url);
4
+ const { version } = require("../package.json");
5
+ /**
6
+ * Build the top-level commander {@link Command} for the CLI.
7
+ *
8
+ * The returned program has `exitOverride()` configured by the caller so
9
+ * `--help` and `--version` can be caught in tests without terminating the
10
+ * process.
11
+ *
12
+ * @returns A fully wired commander `Command`.
13
+ */
14
+ export function buildProgram() {
15
+ const program = new Command();
16
+ program
17
+ .name("markdownlint-obsidian")
18
+ .description("Obsidian Flavored Markdown linter for CI pipelines")
19
+ .version(version)
20
+ .argument("[globs...]", "Glob patterns for files to lint")
21
+ .option("--config <path>", "Explicit config file path")
22
+ .option("--config-pointer <ptr>", "JSON Pointer into config (e.g. #/markdownlint)")
23
+ .option("--fix", "Auto-fix fixable errors in-place", false)
24
+ .option("--fix-check", "Check if fixes are needed without writing files", false)
25
+ .option("--format", "Read stdin, write linted content to stdout", false)
26
+ .option("--no-globs", "Ignore globs property in config file")
27
+ .option("--vault-root <path>", "Override auto-detected vault root")
28
+ .option("--no-resolve", "Disable wikilink resolution")
29
+ .option("--output-formatter <name>", "Output formatter (default, json, junit, sarif)", "default");
30
+ return program;
31
+ }
32
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,iBAAiB,CAAwB,CAAC;AAyBtE;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAC9B,OAAO;SACJ,IAAI,CAAC,uBAAuB,CAAC;SAC7B,WAAW,CAAC,oDAAoD,CAAC;SACjE,OAAO,CAAC,OAAO,CAAC;SAChB,QAAQ,CAAC,YAAY,EAAE,iCAAiC,CAAC;SACzD,MAAM,CAAC,iBAAiB,EAAE,2BAA2B,CAAC;SACtD,MAAM,CAAC,wBAAwB,EAAE,gDAAgD,CAAC;SAClF,MAAM,CAAC,OAAO,EAAE,kCAAkC,EAAE,KAAK,CAAC;SAC1D,MAAM,CAAC,aAAa,EAAE,iDAAiD,EAAE,KAAK,CAAC;SAC/E,MAAM,CAAC,UAAU,EAAE,4CAA4C,EAAE,KAAK,CAAC;SACvE,MAAM,CAAC,YAAY,EAAE,sCAAsC,CAAC;SAC5D,MAAM,CAAC,qBAAqB,EAAE,mCAAmC,CAAC;SAClE,MAAM,CAAC,cAAc,EAAE,6BAA6B,CAAC;SACrD,MAAM,CACL,2BAA2B,EAC3B,gDAAgD,EAChD,SAAS,CACV,CAAC;IACJ,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Exit codes used by every CLI path.
3
+ *
4
+ * 0 = clean, 1 = lint errors, 2 = tool or config failure.
5
+ */
6
+ export declare const EXIT_CODES: Readonly<{
7
+ readonly CLEAN: 0;
8
+ readonly LINT_ERRORS: 1;
9
+ readonly TOOL_FAILURE: 2;
10
+ }>;
11
+ /**
12
+ * Entry point called by `bin/markdownlint-obsidian.js`.
13
+ *
14
+ * Parses arguments, runs the linting pipeline via the engine API,
15
+ * prints formatter output, and returns the appropriate exit code.
16
+ *
17
+ * @param argv - Argument vector, typically `process.argv`.
18
+ * @returns Resolved exit code (0, 1, or 2).
19
+ */
20
+ export declare function main(argv: string[]): Promise<number>;
21
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"AA2BA;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;;EAIZ,CAAC;AAEZ;;;;;;;;GAQG;AACH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAY1D"}
@@ -0,0 +1,136 @@
1
+ import { buildProgram } from "./args.js";
2
+ import { lint, fix, getFormatter, loadConfig, } from "markdownlint-obsidian/engine";
3
+ /**
4
+ * Exit codes used by every CLI path.
5
+ *
6
+ * 0 = clean, 1 = lint errors, 2 = tool or config failure.
7
+ */
8
+ export const EXIT_CODES = Object.freeze({
9
+ CLEAN: 0,
10
+ LINT_ERRORS: 1,
11
+ TOOL_FAILURE: 2,
12
+ });
13
+ /**
14
+ * Entry point called by `bin/markdownlint-obsidian.js`.
15
+ *
16
+ * Parses arguments, runs the linting pipeline via the engine API,
17
+ * prints formatter output, and returns the appropriate exit code.
18
+ *
19
+ * @param argv - Argument vector, typically `process.argv`.
20
+ * @returns Resolved exit code (0, 1, or 2).
21
+ */
22
+ export async function main(argv) {
23
+ const program = buildProgram();
24
+ program.exitOverride();
25
+ const parsed = parseArgv(program, argv);
26
+ if (parsed.terminal !== null)
27
+ return parsed.terminal;
28
+ const opts = program.opts();
29
+ const cwd = process.cwd();
30
+ const globs = program.args;
31
+ return runPipeline(globs, opts, cwd);
32
+ }
33
+ function parseArgv(program, argv) {
34
+ try {
35
+ program.parse(argv);
36
+ return { terminal: null };
37
+ }
38
+ catch (err) {
39
+ const e = err;
40
+ if (e.code === "commander.helpDisplayed" || e.code === "commander.version") {
41
+ return { terminal: EXIT_CODES.CLEAN };
42
+ }
43
+ return { terminal: EXIT_CODES.TOOL_FAILURE };
44
+ }
45
+ }
46
+ function resolveFormatter(name) {
47
+ try {
48
+ return getFormatter(name);
49
+ }
50
+ catch (err) {
51
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
52
+ return null;
53
+ }
54
+ }
55
+ function emitAndExit(results, formatterName) {
56
+ const formatter = resolveFormatter(formatterName);
57
+ if (formatter === null)
58
+ return EXIT_CODES.TOOL_FAILURE;
59
+ const output = formatter(results);
60
+ if (output)
61
+ process.stdout.write(output + "\n");
62
+ return results.some((r) => r.hasErrors) ? EXIT_CODES.LINT_ERRORS : EXIT_CODES.CLEAN;
63
+ }
64
+ function fmtRange(col, del) {
65
+ return del === 0 ? `col ${col}` : `col ${col}–${col + del - 1}`;
66
+ }
67
+ function onCustomRuleError(modulePath, message) {
68
+ process.stderr.write(`OFM905: failed to load custom rule module "${modulePath}": ${message}\n`);
69
+ }
70
+ function buildEngineOptions(globArgs, config, opts, cwd) {
71
+ const effectiveGlobs = globArgs.length > 0 ? [...globArgs] : config.globs;
72
+ return {
73
+ globs: effectiveGlobs,
74
+ cwd,
75
+ ...(opts.vaultRoot !== undefined && { vaultRoot: opts.vaultRoot }),
76
+ ...(opts.resolve === false && { resolve: false }),
77
+ ...(opts.config !== undefined && { config: opts.config }),
78
+ onCustomRuleError,
79
+ };
80
+ }
81
+ async function runFixBranch(engineOptions, opts) {
82
+ try {
83
+ const outcome = await fix({
84
+ ...engineOptions,
85
+ check: opts.fixCheck,
86
+ });
87
+ if (outcome.filesFixed.length > 0) {
88
+ const verb = opts.fixCheck ? "Would fix" : "Fixed";
89
+ process.stderr.write(`${verb} ${outcome.filesFixed.length} file(s)\n`);
90
+ }
91
+ for (const conflict of outcome.conflicts) {
92
+ const colA = fmtRange(conflict.first.editColumn, conflict.first.deleteCount);
93
+ const colB = fmtRange(conflict.second.editColumn, conflict.second.deleteCount);
94
+ process.stderr.write(`[fix-conflict] ${conflict.filePath}: ${conflict.reason} (${colA} vs ${colB})\n`);
95
+ }
96
+ return emitAndExit(outcome.finalPass, opts.outputFormatter);
97
+ }
98
+ catch (err) {
99
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
100
+ return EXIT_CODES.TOOL_FAILURE;
101
+ }
102
+ }
103
+ async function runLintBranch(engineOptions, opts) {
104
+ try {
105
+ const results = await lint(engineOptions);
106
+ return emitAndExit(results, opts.outputFormatter);
107
+ }
108
+ catch (err) {
109
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
110
+ return EXIT_CODES.TOOL_FAILURE;
111
+ }
112
+ }
113
+ function checkMutualExclusion(opts) {
114
+ if (opts.fix && opts.fixCheck) {
115
+ process.stderr.write("OFM902: --fix and --fix-check are mutually exclusive\n");
116
+ return EXIT_CODES.TOOL_FAILURE;
117
+ }
118
+ return null;
119
+ }
120
+ async function runPipeline(globArgs, opts, cwd) {
121
+ const exclusionCode = checkMutualExclusion(opts);
122
+ if (exclusionCode !== null)
123
+ return exclusionCode;
124
+ if (resolveFormatter(opts.outputFormatter) === null)
125
+ return EXIT_CODES.TOOL_FAILURE;
126
+ const config = await loadConfig(opts.config ?? cwd).catch(() => null);
127
+ if (!config) {
128
+ process.stderr.write("OFM901: failed to load configuration\n");
129
+ return EXIT_CODES.TOOL_FAILURE;
130
+ }
131
+ const engineOptions = buildEngineOptions(globArgs, config, opts, cwd);
132
+ return opts.fix || opts.fixCheck
133
+ ? runFixBranch(engineOptions, opts)
134
+ : runLintBranch(engineOptions, opts);
135
+ }
136
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "markdownlint-obsidian-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI for markdownlint-obsidian — lint Obsidian Flavored Markdown from the command line",
5
+ "type": "module",
6
+ "bin": {
7
+ "markdownlint-obsidian": "./bin/markdownlint-obsidian.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/src/main.d.ts",
12
+ "default": "./dist/src/main.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist/",
17
+ "src/",
18
+ "bin/",
19
+ "README.md",
20
+ "CHANGELOG.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.build.json && node scripts/gen-dist-bin.mjs",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "bun test --timeout 30000",
27
+ "test:watch": "bun test --watch --timeout 30000",
28
+ "prepublishOnly": "bun run build && bun run test"
29
+ },
30
+ "dependencies": {
31
+ "markdownlint-obsidian": "workspace:*",
32
+ "commander": "^12.0.0"
33
+ },
34
+ "engines": {
35
+ "node": ">=20.0.0",
36
+ "bun": ">=1.1.30"
37
+ },
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/alisonaquinas/markdownlint-obsidian.git",
42
+ "directory": "packages/cli"
43
+ },
44
+ "homepage": "https://github.com/alisonaquinas/markdownlint-obsidian#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/alisonaquinas/markdownlint-obsidian/issues"
47
+ },
48
+ "keywords": [
49
+ "markdown",
50
+ "markdownlint",
51
+ "obsidian",
52
+ "linter",
53
+ "cli"
54
+ ]
55
+ }
package/src/args.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { Command } from "commander";
2
+ import { createRequire } from "node:module";
3
+
4
+ const require = createRequire(import.meta.url);
5
+ const { version } = require("../package.json") as { version: string };
6
+
7
+ /**
8
+ * Parsed CLI options returned by commander.
9
+ *
10
+ * Fields mirror markdownlint-cli2's flag set so downstream users can reuse
11
+ * existing muscle memory and automation.
12
+ */
13
+ export interface CLIArgs {
14
+ readonly globs: string[];
15
+ readonly config?: string;
16
+ readonly configPointer?: string;
17
+ readonly fix: boolean;
18
+ readonly fixCheck: boolean;
19
+ readonly format: boolean;
20
+ readonly noGlobs: boolean;
21
+ readonly vaultRoot?: string;
22
+ /**
23
+ * Commander maps `--no-resolve` to `resolve: false`. Left `undefined`
24
+ * when the flag is not supplied so config-level `resolve` can win.
25
+ */
26
+ readonly resolve?: boolean;
27
+ readonly outputFormatter: string;
28
+ }
29
+
30
+ /**
31
+ * Build the top-level commander {@link Command} for the CLI.
32
+ *
33
+ * The returned program has `exitOverride()` configured by the caller so
34
+ * `--help` and `--version` can be caught in tests without terminating the
35
+ * process.
36
+ *
37
+ * @returns A fully wired commander `Command`.
38
+ */
39
+ export function buildProgram(): Command {
40
+ const program = new Command();
41
+ program
42
+ .name("markdownlint-obsidian")
43
+ .description("Obsidian Flavored Markdown linter for CI pipelines")
44
+ .version(version)
45
+ .argument("[globs...]", "Glob patterns for files to lint")
46
+ .option("--config <path>", "Explicit config file path")
47
+ .option("--config-pointer <ptr>", "JSON Pointer into config (e.g. #/markdownlint)")
48
+ .option("--fix", "Auto-fix fixable errors in-place", false)
49
+ .option("--fix-check", "Check if fixes are needed without writing files", false)
50
+ .option("--format", "Read stdin, write linted content to stdout", false)
51
+ .option("--no-globs", "Ignore globs property in config file")
52
+ .option("--vault-root <path>", "Override auto-detected vault root")
53
+ .option("--no-resolve", "Disable wikilink resolution")
54
+ .option(
55
+ "--output-formatter <name>",
56
+ "Output formatter (default, json, junit, sarif)",
57
+ "default",
58
+ );
59
+ return program;
60
+ }
package/src/main.ts ADDED
@@ -0,0 +1,183 @@
1
+ import type { Command } from "commander";
2
+ import { buildProgram } from "./args.js";
3
+ import {
4
+ lint,
5
+ fix,
6
+ getFormatter,
7
+ loadConfig,
8
+ type Formatter,
9
+ type LintResult,
10
+ } from "markdownlint-obsidian/engine";
11
+
12
+ interface ParsedOptions {
13
+ readonly fix: boolean;
14
+ readonly fixCheck: boolean;
15
+ readonly format: boolean;
16
+ readonly vaultRoot?: string;
17
+ /**
18
+ * Commander maps `--no-resolve` to `resolve: false` and leaves the default
19
+ * at `true`. This is a tri-state in practice: `undefined` means the flag
20
+ * was never touched (fall back to config), `false` means `--no-resolve`
21
+ * was supplied.
22
+ */
23
+ readonly resolve?: boolean;
24
+ readonly outputFormatter: string;
25
+ readonly config?: string;
26
+ }
27
+
28
+ /**
29
+ * Exit codes used by every CLI path.
30
+ *
31
+ * 0 = clean, 1 = lint errors, 2 = tool or config failure.
32
+ */
33
+ export const EXIT_CODES = Object.freeze({
34
+ CLEAN: 0,
35
+ LINT_ERRORS: 1,
36
+ TOOL_FAILURE: 2,
37
+ } as const);
38
+
39
+ /**
40
+ * Entry point called by `bin/markdownlint-obsidian.js`.
41
+ *
42
+ * Parses arguments, runs the linting pipeline via the engine API,
43
+ * prints formatter output, and returns the appropriate exit code.
44
+ *
45
+ * @param argv - Argument vector, typically `process.argv`.
46
+ * @returns Resolved exit code (0, 1, or 2).
47
+ */
48
+ export async function main(argv: string[]): Promise<number> {
49
+ const program = buildProgram();
50
+ program.exitOverride();
51
+
52
+ const parsed = parseArgv(program, argv);
53
+ if (parsed.terminal !== null) return parsed.terminal;
54
+
55
+ const opts = program.opts<ParsedOptions>();
56
+ const cwd = process.cwd();
57
+ const globs = program.args as string[];
58
+
59
+ return runPipeline(globs, opts, cwd);
60
+ }
61
+
62
+ interface ParseResult {
63
+ readonly terminal: number | null;
64
+ }
65
+
66
+ function parseArgv(program: Command, argv: string[]): ParseResult {
67
+ try {
68
+ program.parse(argv);
69
+ return { terminal: null };
70
+ } catch (err: unknown) {
71
+ const e = err as { code?: string };
72
+ if (e.code === "commander.helpDisplayed" || e.code === "commander.version") {
73
+ return { terminal: EXIT_CODES.CLEAN };
74
+ }
75
+ return { terminal: EXIT_CODES.TOOL_FAILURE };
76
+ }
77
+ }
78
+
79
+ function resolveFormatter(name: string): Formatter | null {
80
+ try {
81
+ return getFormatter(name);
82
+ } catch (err) {
83
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
84
+ return null;
85
+ }
86
+ }
87
+
88
+ function emitAndExit(results: readonly LintResult[], formatterName: string): number {
89
+ const formatter = resolveFormatter(formatterName);
90
+ if (formatter === null) return EXIT_CODES.TOOL_FAILURE;
91
+ const output = formatter(results);
92
+ if (output) process.stdout.write(output + "\n");
93
+ return results.some((r) => r.hasErrors) ? EXIT_CODES.LINT_ERRORS : EXIT_CODES.CLEAN;
94
+ }
95
+
96
+ function fmtRange(col: number, del: number): string {
97
+ return del === 0 ? `col ${col}` : `col ${col}–${col + del - 1}`;
98
+ }
99
+
100
+ function onCustomRuleError(modulePath: string, message: string): void {
101
+ process.stderr.write(`OFM905: failed to load custom rule module "${modulePath}": ${message}\n`);
102
+ }
103
+
104
+ function buildEngineOptions(
105
+ globArgs: readonly string[],
106
+ config: Awaited<ReturnType<typeof loadConfig>>,
107
+ opts: ParsedOptions,
108
+ cwd: string,
109
+ ): object {
110
+ const effectiveGlobs = globArgs.length > 0 ? [...globArgs] : config.globs;
111
+ return {
112
+ globs: effectiveGlobs,
113
+ cwd,
114
+ ...(opts.vaultRoot !== undefined && { vaultRoot: opts.vaultRoot }),
115
+ ...(opts.resolve === false && { resolve: false }),
116
+ ...(opts.config !== undefined && { config: opts.config }),
117
+ onCustomRuleError,
118
+ };
119
+ }
120
+
121
+ async function runFixBranch(engineOptions: object, opts: ParsedOptions): Promise<number> {
122
+ try {
123
+ const outcome = await fix({
124
+ ...(engineOptions as Parameters<typeof fix>[0]),
125
+ check: opts.fixCheck,
126
+ });
127
+ if (outcome.filesFixed.length > 0) {
128
+ const verb = opts.fixCheck ? "Would fix" : "Fixed";
129
+ process.stderr.write(`${verb} ${outcome.filesFixed.length} file(s)\n`);
130
+ }
131
+ for (const conflict of outcome.conflicts) {
132
+ const colA = fmtRange(conflict.first.editColumn, conflict.first.deleteCount);
133
+ const colB = fmtRange(conflict.second.editColumn, conflict.second.deleteCount);
134
+ process.stderr.write(
135
+ `[fix-conflict] ${conflict.filePath}: ${conflict.reason} (${colA} vs ${colB})\n`,
136
+ );
137
+ }
138
+ return emitAndExit(outcome.finalPass, opts.outputFormatter);
139
+ } catch (err) {
140
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
141
+ return EXIT_CODES.TOOL_FAILURE;
142
+ }
143
+ }
144
+
145
+ async function runLintBranch(engineOptions: object, opts: ParsedOptions): Promise<number> {
146
+ try {
147
+ const results = await lint(engineOptions as Parameters<typeof lint>[0]);
148
+ return emitAndExit(results, opts.outputFormatter);
149
+ } catch (err) {
150
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
151
+ return EXIT_CODES.TOOL_FAILURE;
152
+ }
153
+ }
154
+
155
+ function checkMutualExclusion(opts: ParsedOptions): number | null {
156
+ if (opts.fix && opts.fixCheck) {
157
+ process.stderr.write("OFM902: --fix and --fix-check are mutually exclusive\n");
158
+ return EXIT_CODES.TOOL_FAILURE;
159
+ }
160
+ return null;
161
+ }
162
+
163
+ async function runPipeline(
164
+ globArgs: readonly string[],
165
+ opts: ParsedOptions,
166
+ cwd: string,
167
+ ): Promise<number> {
168
+ const exclusionCode = checkMutualExclusion(opts);
169
+ if (exclusionCode !== null) return exclusionCode;
170
+
171
+ if (resolveFormatter(opts.outputFormatter) === null) return EXIT_CODES.TOOL_FAILURE;
172
+
173
+ const config = await loadConfig(opts.config ?? cwd).catch(() => null);
174
+ if (!config) {
175
+ process.stderr.write("OFM901: failed to load configuration\n");
176
+ return EXIT_CODES.TOOL_FAILURE;
177
+ }
178
+
179
+ const engineOptions = buildEngineOptions(globArgs, config, opts, cwd);
180
+ return opts.fix || opts.fixCheck
181
+ ? runFixBranch(engineOptions, opts)
182
+ : runLintBranch(engineOptions, opts);
183
+ }