mejora 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 mejora
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # mejora
2
+
3
+ Prevent regressions by allowing only improvement.
4
+
5
+ `mejora` runs checks, compares their results to a stored baseline, and fails only when things get worse.
6
+
7
+ If results improve or stay the same, the run passes.
8
+
9
+ ## What problem it solves
10
+
11
+ Most tools ask: _“Is this perfect?”_
12
+ `mejora` asks: _“Did this regress?”_
13
+
14
+ This makes it practical for:
15
+
16
+ - large or legacy codebases
17
+ - incremental cleanup
18
+ - CI enforcement without blocking progress
19
+
20
+ ## How it works
21
+
22
+ 1. Each check produces a snapshot, a list of items
23
+ 2. The snapshot is compared to a baseline
24
+ 3. New items cause failure
25
+ 4. Removed items are treated as improvement
26
+
27
+ Baselines are explicit and should be committed to the repo.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pnpm add -D mejora
33
+ ```
34
+
35
+ > [!NOTE]
36
+ > `mejora` requires Node.js 22.18.0 or later.
37
+
38
+ ## Usage
39
+
40
+ Run checks:
41
+
42
+ ```bash
43
+ pnpm mejora
44
+ ```
45
+
46
+ Force the baseline to accept regressions:
47
+
48
+ ```bash
49
+ pnpm mejora --force
50
+ ```
51
+
52
+ JSON output for CI and automation:
53
+
54
+ ```bash
55
+ pnpm mejora --json
56
+ ```
57
+
58
+ Run only a subset of checks:
59
+
60
+ ```bash
61
+ pnpm mejora --only "eslint > *"
62
+ ```
63
+
64
+ Skip checks:
65
+
66
+ ```bash
67
+ pnpm mejora --skip typescript
68
+ ```
69
+
70
+ ## Configuration
71
+
72
+ Create one of:
73
+
74
+ - `mejora.config.ts`
75
+ - `mejora.config.js`
76
+ - `mejora.config.mjs`
77
+ - `mejora.config.mts`
78
+
79
+ Example:
80
+
81
+ ```ts
82
+ import { defineConfig, eslintCheck, typescriptCheck } from "mejora";
83
+
84
+ export default defineConfig({
85
+ checks: {
86
+ "eslint > no-nested-ternary": eslintCheck({
87
+ files: ["src/**/*.{ts,tsx,js,jsx}"],
88
+ overrides: {
89
+ rules: {
90
+ "no-nested-ternary": "error",
91
+ },
92
+ },
93
+ }),
94
+ "typescript": typescriptCheck({
95
+ overrides: {
96
+ compilerOptions: {
97
+ noImplicitAny: true,
98
+ },
99
+ },
100
+ }),
101
+ },
102
+ });
103
+ ```
104
+
105
+ Each entry in `checks` is an explicit check.
106
+ The object key is the check identifier and is used in the baseline.
107
+
108
+ ## Supported checks
109
+
110
+ ### ESLint
111
+
112
+ - Snapshot type: items
113
+ - Each lint message is treated as an item
114
+ - Regressions are new lint messages
115
+
116
+ > [!NOTE]
117
+ > `eslint` (^9.34.0) is required as a peer dependency when using the ESLint check
118
+
119
+ ### TypeScript
120
+
121
+ - Snapshot type: items
122
+ - Each compiler diagnostic is treated as an item
123
+ - Regressions are new diagnostics
124
+ - Uses the nearest `tsconfig.json` by default, or an explicit one if provided
125
+
126
+ > [!NOTE]
127
+ > `typescript` (^5.0.0) is required as a peer dependency when using the TypeScript check
128
+
129
+ ## Snapshot type
130
+
131
+ ### Items
132
+
133
+ ```json
134
+ { "type": "items", "items": ["file.ts:12", "file.ts:45"] }
135
+ ```
136
+
137
+ Fails if new items appear.
138
+ Order does not matter.
139
+
140
+ ## Baseline
141
+
142
+ Default location:
143
+
144
+ ```txt
145
+ .mejora/baseline.json
146
+ ```
147
+
148
+ Example:
149
+
150
+ ```json
151
+ {
152
+ "version": 1,
153
+ "checks": {
154
+ "eslint > no-nested-ternary": {
155
+ "type": "items",
156
+ "items": ["src/a.ts:1"]
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ The baseline represents the last accepted state.
163
+
164
+ ## Improvements and baseline updates
165
+
166
+ When a run produces fewer items than the baseline, the run passes and the baseline is updated automatically to reflect the improvement.
167
+
168
+ This includes items that no longer exist in the codebase.
169
+
170
+ Improvements are reported separately from regressions so progress is visible.
171
+
172
+ ## CI behavior
173
+
174
+ When running in CI, mejora does not write the baseline.
175
+
176
+ Instead, it compares the committed baseline to the expected results from the current codebase.
177
+
178
+ If there is any difference between the committed baseline and the expected results, the run fails.
179
+
180
+ ## Force mode
181
+
182
+ `mejora --force` updates the baseline even when regressions are present.
183
+
184
+ ## Exit codes
185
+
186
+ - `0` pass or improvement
187
+ - `1` regression detected or baseline out of sync
188
+ - `2` configuration or runtime error
189
+
190
+ ## Output
191
+
192
+ - Default output is plain text
193
+ - No prompts
194
+ - No interactive behavior
195
+ - `--json` produces structured, deterministic output
196
+
197
+ ## Merge Conflicts
198
+
199
+ mejora automatically resolves conflicts in both `baseline.json` and `baseline.md`:
200
+
201
+ ```bash
202
+ # After merging branches with baseline changes
203
+ $ git status
204
+ both modified: .mejora/baseline.json
205
+ both modified: .mejora/baseline.md
206
+
207
+ # Just run mejora - both files are auto-resolved
208
+ $ mejora
209
+ Merge conflict detected in baseline, auto-resolving...
210
+ ✓ Baseline conflict resolved
211
+
212
+ # Commit the resolved files
213
+ $ git add .mejora/
214
+ $ git commit -m "Merge feature branch"
215
+ ```
216
+
217
+ ## Inspiration
218
+
219
+ `mejora` is inspired by [betterer](https://phenomnomnominal.github.io/betterer/).
@@ -0,0 +1,4 @@
1
+ import{createRequire as e}from"node:module";import{relative as t,resolve as n}from"node:path";import{createHash as r}from"node:crypto";import{pathToFileURL as i}from"node:url";var a=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),o=e(import.meta.url);function s(e){let t=JSON.stringify(e??null,(e,t)=>{if(t&&typeof t==`object`&&!Array.isArray(t)){let e=t;return Object.keys(e).toSorted().reduce((t,n)=>({...t,[n]:e[n]}),{})}return t});return r(`sha256`).update(t).digest(`hex`)}const c=({column:e,cwd:n,filePath:r,line:i,ruleId:a})=>`${t(n,r)}:${i}:${e} - ${a}`;async function l(){try{await import(`eslint`)}catch{throw Error(`ESLint check requires eslint but it's not installed.`)}}async function u(e){let{ESLint:t}=await import(`eslint`),n=process.cwd(),r=await new t({cache:!0,cacheLocation:`node_modules/.cache/mejora/eslint/${s(e)}`,concurrency:`auto`,overrideConfig:e.overrides}).lintFiles(e.files),i=[];for(let{filePath:e,messages:t}of r)for(let{column:r,line:a,ruleId:o}of t)if(o){let t=c({column:r,cwd:n,filePath:e,line:a,ruleId:o});i.push(t)}return{items:i.toSorted(),type:`items`}}function d(e){return{type:`eslint`,...e}}async function f(){try{await import(`typescript`)}catch{throw Error(`TypeScript check requires typescript but it's not installed.`)}}const p=({character:e,code:n,cwd:r,fileName:i,line:a,message:o})=>`${t(r,i)}:${a+1}:${e+1} - TS${n}: ${o}`;async function m(e){let{createProgram:t,findConfigFile:r,flattenDiagnosticMessageText:i,getPreEmitDiagnostics:a,parseJsonConfigFileContent:o,readConfigFile:s,sys:c}=await import(`typescript`),l=process.cwd(),u=c.fileExists.bind(c),d=c.readFile.bind(c),f=e.tsconfig?n(e.tsconfig):r(l,u,`tsconfig.json`);if(!f)throw Error(`TypeScript config file not found`);let{config:m,error:h}=s(f,d);if(h){let e=typeof h.messageText==`string`?h.messageText:i(h.messageText,`
2
+ `);throw TypeError(`Failed to read TypeScript config: ${e}`)}let g=o(m,c,process.cwd(),e.overrides?.compilerOptions),_=a(t({options:g.options,rootNames:g.fileNames})),v=[];for(let e of _)if(e.file&&e.start!==void 0){let{character:t,line:n}=e.file.getLineAndCharacterOfPosition(e.start),r=i(e.messageText,`
3
+ `),a=p({character:t,code:e.code,cwd:l,fileName:e.file.fileName,line:n,message:r});v.push(a)}else{let t=i(e.messageText,`
4
+ `),n=`(global) - TS${e.code}: ${t}`;v.push(n)}return{items:v.toSorted(),type:`items`}}function h(e){return{type:`typescript`,...e}}var g=a(((e,t)=>{let n=o(`path`),r=o(`fs`),i=o(`os`),a=o(`url`),s=r.promises.readFile;function c(e,t){return[`package.json`,`.${e}rc.json`,`.${e}rc.js`,`.${e}rc.cjs`,...t?[]:[`.${e}rc.mjs`],`.config/${e}rc`,`.config/${e}rc.json`,`.config/${e}rc.js`,`.config/${e}rc.cjs`,...t?[]:[`.config/${e}rc.mjs`],`${e}.config.js`,`${e}.config.cjs`,...t?[]:[`${e}.config.mjs`]]}function l(e){return n.dirname(e)||n.sep}let u=(e,t)=>JSON.parse(t),d=typeof __webpack_require__==`function`?__non_webpack_require__:o,f=Object.freeze({".js":d,".json":d,".cjs":d,noExt:u});t.exports.defaultLoadersSync=f;let p=async e=>{try{return(await import(a.pathToFileURL(e).href)).default}catch(t){try{return d(e)}catch(e){throw e.code===`ERR_REQUIRE_ESM`||e instanceof SyntaxError&&e.toString().includes(`Cannot use import statement outside a module`)?t:e}}},m=Object.freeze({".js":p,".mjs":p,".cjs":p,".json":u,noExt:u});t.exports.defaultLoaders=m;function h(e,t,r){let a={stopDir:i.homedir(),searchPlaces:c(e,r),ignoreEmptySearchPlaces:!0,cache:!0,transform:e=>e,packageProp:[e],...t,loaders:{...r?f:m,...t.loaders}};return a.searchPlaces.forEach(e=>{let t=n.extname(e)||`noExt`,r=a.loaders[t];if(!r)throw Error(`Missing loader for extension "${e}"`);if(typeof r!=`function`)throw Error(`Loader for extension "${e}" is not a function: Received ${typeof r}.`)}),a}function g(e,t){return typeof e==`string`&&e in t?t[e]:(Array.isArray(e)?e:e.split(`.`)).reduce((e,t)=>e===void 0?e:e[t],t)||null}function _(e){if(!e)throw Error(`load must pass a non-empty string`)}function v(e,t){if(!e)throw Error(`No loader specified for extension "${t}"`);if(typeof e!=`function`)throw Error(`loader is not a function`)}let y=e=>(t,n,r)=>(e&&t.set(n,r),r);t.exports.lilconfig=function(e,t){let{ignoreEmptySearchPlaces:i,loaders:a,packageProp:o,searchPlaces:c,stopDir:u,transform:d,cache:f}=h(e,t??{},!1),p=new Map,m=new Map,b=y(f);return{async search(e=process.cwd()){let t={config:null,filepath:``},m=new Set,h=e;dirLoop:for(;;){if(f){let e=p.get(h);if(e!==void 0){for(let t of m)p.set(t,e);return e}m.add(h)}for(let e of c){let c=n.join(h,e);try{await r.promises.access(c)}catch{continue}let l=String(await s(c)),u=n.extname(e)||`noExt`,d=a[u];if(e===`package.json`){let e=g(o,await d(c,l));if(e!=null){t.config=e,t.filepath=c;break dirLoop}continue}let f=l.trim()===``;if(!(f&&i)){f?(t.isEmpty=!0,t.config=void 0):(v(d,u),t.config=await d(c,l)),t.filepath=c;break dirLoop}}if(h===u||h===l(h))break dirLoop;h=l(h)}let _=t.filepath===``&&t.config===null?d(null):d(t);if(f)for(let e of m)p.set(e,_);return _},async load(e){_(e);let t=n.resolve(process.cwd(),e);if(f&&m.has(t))return m.get(t);let{base:r,ext:c}=n.parse(t),l=c||`noExt`,u=a[l];v(u,l);let p=String(await s(t));if(r===`package.json`)return b(m,t,d({config:g(o,await u(t,p)),filepath:t}));let h={config:null,filepath:t},y=p.trim()===``;return y&&i?b(m,t,d({config:void 0,filepath:t,isEmpty:!0})):(h.config=y?void 0:await u(t,p),b(m,t,d(y?{...h,isEmpty:y,config:void 0}:h)))},clearLoadCache(){f&&m.clear()},clearSearchCache(){f&&p.clear()},clearCaches(){f&&(m.clear(),p.clear())}}},t.exports.lilconfigSync=function(e,t){let{ignoreEmptySearchPlaces:i,loaders:a,packageProp:o,searchPlaces:s,stopDir:c,transform:u,cache:d}=h(e,t??{},!0),f=new Map,p=new Map,m=y(d);return{search(e=process.cwd()){let t={config:null,filepath:``},p=new Set,m=e;dirLoop:for(;;){if(d){let e=f.get(m);if(e!==void 0){for(let t of p)f.set(t,e);return e}p.add(m)}for(let e of s){let s=n.join(m,e);try{r.accessSync(s)}catch{continue}let c=n.extname(e)||`noExt`,l=a[c],u=String(r.readFileSync(s));if(e===`package.json`){let e=g(o,l(s,u));if(e!=null){t.config=e,t.filepath=s;break dirLoop}continue}let d=u.trim()===``;if(!(d&&i)){d?(t.isEmpty=!0,t.config=void 0):(v(l,c),t.config=l(s,u)),t.filepath=s;break dirLoop}}if(m===c||m===l(m))break dirLoop;m=l(m)}let h=t.filepath===``&&t.config===null?u(null):u(t);if(d)for(let e of p)f.set(e,h);return h},load(e){_(e);let t=n.resolve(process.cwd(),e);if(d&&p.has(t))return p.get(t);let{base:s,ext:c}=n.parse(t),l=c||`noExt`,f=a[l];v(f,l);let h=String(r.readFileSync(t));if(s===`package.json`)return u({config:g(o,f(t,h)),filepath:t});let y={config:null,filepath:t},b=h.trim()===``;return b&&i?m(p,t,u({filepath:t,config:void 0,isEmpty:!0})):(y.config=b?void 0:f(t,h),m(p,t,u(b?{...y,isEmpty:b,config:void 0}:y)))},clearLoadCache(){d&&p.clear()},clearSearchCache(){d&&f.clear()},clearCaches(){d&&(p.clear(),f.clear())}}}}))();const _=async e=>{let t=await import(i(e).href);return t&&typeof t==`object`&&`default`in t?t.default:t},v=e=>e,y=async()=>{let e=await(0,g.lilconfig)(`mejora`,{loaders:{".js":_,".mjs":_,".mts":_,".ts":_},searchPlaces:[`mejora.config.ts`,`mejora.config.mts`,`mejora.config.js`,`mejora.config.mjs`]}).search(process.cwd());if(!e?.config)throw Error(`No configuration file found.`);return e.config};export{f as a,l as c,h as i,y as n,d as o,m as r,u as s,v as t};
@@ -0,0 +1,159 @@
1
+ import * as eslint0 from "eslint";
2
+ import { Linter } from "eslint";
3
+ import * as typescript0 from "typescript";
4
+ import { CompilerOptions } from "typescript";
5
+
6
+ //#region src/types.d.ts
7
+
8
+ /**
9
+ * Configuration for an ESLint check.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * eslintCheck({
14
+ * files: ["src/**\/*.{ts,tsx}"],
15
+ * overrides: {
16
+ * rules: {
17
+ * "no-nested-ternary": "error",
18
+ * },
19
+ * },
20
+ * })
21
+ * ```
22
+ */
23
+ interface ESLintCheckConfig {
24
+ /**
25
+ * Glob patterns for files to lint.
26
+ *
27
+ * Passed directly to ESLint's `lintFiles()` method.
28
+ *
29
+ * @example ["src/**\/*.ts", "src/**\/*.tsx"]
30
+ */
31
+ files: string[];
32
+ /**
33
+ * ESLint configuration to merge with the base config.
34
+ *
35
+ * This is passed to ESLint's `overrideConfig` option and merged with
36
+ * your existing ESLint configuration.
37
+ *
38
+ * Can be a single config object or an array of config objects.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * {
43
+ * rules: {
44
+ * "no-console": "error",
45
+ * },
46
+ * }
47
+ * ```
48
+ */
49
+ overrides?: Linter.Config | Linter.Config[];
50
+ }
51
+ /**
52
+ * Configuration for a TypeScript diagnostics check.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * typescriptCheck({
57
+ * overrides: {
58
+ * compilerOptions: {
59
+ * noImplicitAny: true,
60
+ * },
61
+ * },
62
+ * })
63
+ * ```
64
+ */
65
+ interface TypeScriptCheckConfig {
66
+ /**
67
+ * Compiler options to merge with the base tsconfig.
68
+ *
69
+ * These options are merged with (not replacing) the compiler options
70
+ * from your tsconfig file.
71
+ */
72
+ overrides?: {
73
+ compilerOptions?: CompilerOptions;
74
+ };
75
+ /**
76
+ * Path to a TypeScript config file.
77
+ *
78
+ * If not provided, mejora will search for the nearest `tsconfig.json`
79
+ * starting from the current working directory.
80
+ *
81
+ * @example "tsconfig.strict.json"
82
+ */
83
+ tsconfig?: string;
84
+ }
85
+ type CheckConfig = (ESLintCheckConfig & {
86
+ type: "eslint";
87
+ }) | (TypeScriptCheckConfig & {
88
+ type: "typescript";
89
+ });
90
+ /**
91
+ * mejora configuration.
92
+ *
93
+ * Define checks to run and track for regressions.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * import { defineConfig, eslintCheck, typescriptCheck } from "mejora";
98
+ *
99
+ * export default defineConfig({
100
+ * checks: {
101
+ * "eslint > no-nested-ternary": eslintCheck({
102
+ * files: ["src/**\/*.{ts,tsx}"],
103
+ * overrides: {
104
+ * rules: {
105
+ * "no-nested-ternary": "error",
106
+ * },
107
+ * },
108
+ * }),
109
+ * "typescript": typescriptCheck({
110
+ * overrides: {
111
+ * compilerOptions: {
112
+ * noImplicitAny: true,
113
+ * },
114
+ * },
115
+ * }),
116
+ * },
117
+ * });
118
+ * ```
119
+ */
120
+ interface Config {
121
+ /**
122
+ * Check definitions.
123
+ *
124
+ * Each key is a check identifier used in the baseline and output.
125
+ * The identifier can contain any characters.
126
+ *
127
+ * Use `eslintCheck()` and `typescriptCheck()` helpers to create check configs.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * {
132
+ * "eslint > no-console": eslintCheck({ ... }),
133
+ * "typescript": typescriptCheck({ ... }),
134
+ * }
135
+ * ```
136
+ */
137
+ checks: Record<string, CheckConfig>;
138
+ }
139
+ //#endregion
140
+ //#region src/checks/eslint.d.ts
141
+ declare function eslintCheck(config: ESLintCheckConfig): {
142
+ files: string[];
143
+ overrides?: eslint0.Linter.Config | eslint0.Linter.Config[];
144
+ type: "eslint";
145
+ };
146
+ //#endregion
147
+ //#region src/checks/typescript.d.ts
148
+ declare function typescriptCheck(config: TypeScriptCheckConfig): {
149
+ overrides?: {
150
+ compilerOptions?: typescript0.CompilerOptions;
151
+ };
152
+ tsconfig?: string;
153
+ type: "typescript";
154
+ };
155
+ //#endregion
156
+ //#region src/config.d.ts
157
+ declare const defineConfig: (config: Config) => Config;
158
+ //#endregion
159
+ export { type Config, defineConfig, eslintCheck, typescriptCheck };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import{i as e,o as t,t as n}from"./config-De8VvJ6A.mjs";export{n as defineConfig,t as eslintCheck,e as typescriptCheck};
package/dist/run.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/run.mjs ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import{a as e,c as t,n,r,s as i}from"./config-De8VvJ6A.mjs";import{dirname as a,relative as o}from"node:path";import{inspect as s,parseArgs as c,styleText as l}from"node:util";import{mkdir as u,readFile as d,writeFile as f}from"node:fs/promises";import{env as p}from"node:process";const m=e=>l(`blue`,e),h=e=>l(`bold`,e),g=e=>l(`cyan`,e),_=e=>l(`dim`,e),v=e=>l(`green`,e),y=e=>l(`greenBright`,e),b=e=>l(`red`,e),x=e=>l(`bgRed`,e),S=e=>l(`yellow`,e),C=e=>l(`black`,e);function w(e){if(e<1e3)return`${e}ms`;let t=e/1e3;if(t<60)return t%1==0?`${t}s`:`${t.toFixed(1)}s`;let n=e/6e4;if(n<60)return n%1==0?`${n}m`:`${n.toFixed(1)}m`;let r=e/36e5;return r%1==0?`${r}h`:`${r.toFixed(1)}h`}function T(e){let t=Math.round(e);if(t<1)return _(`<1ms`);let n=w(t);return t<100?y(n):t<1e3?S(n):b(n)}function E(e){let t={checks:e.results.map(e=>({checkId:e.checkId,duration:e.duration,hasImprovement:e.hasImprovement,hasRegression:e.hasRegression,isInitial:e.isInitial,newItems:e.newItems,removedItems:e.removedItems,totalIssues:e.snapshot.items.length||0})),exitCode:e.exitCode,hasImprovement:e.hasImprovement,hasRegression:e.hasRegression,totalDuration:e.totalDuration};return JSON.stringify(t,null,2)}function D(e,t=10){let n=[],r=e.slice(0,t);for(let e of r)n.push(` ${_(e)}`);let i=e.length-t;return i>0&&n.push(` ${_(`... and ${i} more`)}`),n}function ee(e,t){let n=[`${t?``:`
3
+ `}${h(e.checkId)}:`,` Initial baseline created with ${g(e.snapshot.items.length.toString())} issue(s)`];return e.snapshot.items.length>0&&n.push(...D(e.snapshot.items)),e.duration!==void 0&&n.push(` ${_(`Completed in`)} ${T(e.duration)}`),n}function O(e){return e.hasRegression?[` ${b(e.newItems.length.toString())} new issue(s) (regressions):`,...D(e.newItems)]:[]}function k(e){return e.hasImprovement?[` ${y(e.removedItems.length.toString())} issue(s) fixed (improvements):`,...D(e.removedItems)]:[]}function A(e,t){let n=[`${t?``:`
4
+ `}${h(e.checkId)}:`,...O(e),...k(e)];return e.duration!==void 0&&n.push(` ${_(`Completed in`)} ${T(e.duration)}`),n}function j(e,t){return e.isInitial?ee(e,t):e.hasRegression||e.hasImprovement?A(e,t):[]}function M(e){let t=e.results.some(e=>e.isInitial),n=[];return t?n.push(m(`Initial baseline created successfully.`)):e.hasRegression?n.push(`${b(`Regressions detected.`)} Run failed.`):e.hasImprovement?n.push(`${y(`Improvements detected.`)} Baseline updated.`):n.push(y(`All checks passed.`)),e.totalDuration!==void 0&&n.push(`${_(`Completed in`)} ${T(e.totalDuration)}`),n.join(`
5
+ `)}function N(e){let t=[];for(let n=0;n<e.results.length;n++){let r=e.results[n];t.push(...j(r,n===0))}return t.length>0&&t.push(``),t.push(M(e)),t.join(`
6
+ `)}const P=e=>e in p&&p[e]!==`0`&&p[e]!==`false`;var F=P(`CI`)||P(`CONTINUOUS_INTEGRATION`);function I(e,t){let n=e;for(let e=0;e<t&&n.includes(`}`);e++){let e=n.lastIndexOf(`}`);n=n.slice(0,e)+n.slice(e+1)}return n}function L(e,t){return`${e}\n${` }`.repeat(t)}`}function R(e){let t=0,n=0;for(let r of e)r===`{`&&t++,r===`}`&&n++;if(t===n)return e;let r=Math.abs(t-n);return n>t?I(e,r):L(e,r)}function z(e){let t=e.trim();return R(t.endsWith(`,`)?t.slice(0,-1):t)}function B(e){return`{
7
+ "version": 1,
8
+ ${e}
9
+ }`}function V(e){if(e.checks)return e;let t={};for(let[n,r]of Object.entries(e))n!==`version`&&(t[n]=r);return{checks:t,version:1}}function H(e){try{let t=B(z(e));return V(JSON.parse(t))}catch(e){let t=e instanceof Error?e.message:String(e);throw Error(`Failed to parse baseline during conflict resolution: ${t}`,{cause:e})}}function U(e){let t=[...e.matchAll(/<<<<<<< .*\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> .*$/gm)];if(t.length===0)throw Error(`Could not parse conflict markers in baseline`);return t.map(([,e=``,t=``])=>({ours:e,theirs:t}))}function te(e){let t=new Set;for(let n of e)for(let e of Object.keys(n.checks))t.add(e);return t}function W(e,t){let n=new Set;for(let r of e){let e=r.checks[t];if(e?.items)for(let t of e.items)n.add(t)}return[...n].toSorted()}function G(e){let t={checks:{},version:1},n=te(e);for(let r of n)t.checks[r]={items:W(e,r),type:`items`};return t}function K(e){let t=U(e),n=[];for(let{ours:e,theirs:r}of t)n.push(H(e),H(r));return G(n)}function q(e,t){let n=`# Mejora Baseline
10
+
11
+ `,r=Object.entries(e.checks);for(let e=0;e<r.length;e++){let[i,{items:a=[]}]=r[e],s=e===r.length-1;if(n+=`## ${i}\n\n`,a.length===0)n+=`No issues
12
+ `;else for(let e of a){let[r,...i]=e.split(` - `),a=r?.split(`:`),s=a?.[0],c=a?.[1];if(s){let e=o(t,s),a=c?`${e}#L${c}`:e,l=i.length>0?` - ${i.join(` - `)}`:``;n+=`- [${r}](${a})${l}\n`}}s||(n+=`
13
+ `)}return n}const J=(e,t)=>{if(!t)return!1;let n=e.items??[],r=t.items??[];if(n.length!==r.length)return!1;let i=n.toSorted(),a=r.toSorted();return i.every((e,t)=>e===a[t])};function Y(e){let t=[e.message];if(e.stack){let n=e.stack.split(`
14
+ `).slice(1).map(e=>_(e.trim())).join(`
15
+ `);t.push(n)}return t.join(`
16
+ `)}function X(...e){return e.map(e=>typeof e==`string`?e:e instanceof Error?Y(e):s(e,{colors:!1,depth:10})).join(` `)}const Z={error:(...e)=>{console.error(x(C(` ERROR `)),X(...e))},log:(...e)=>{console.log(X(...e))},start:(...e)=>{console.log(g(`◐`),X(...e))},success:(...e)=>{console.log(v(`✔`),X(...e))}};var Q=class e{baselinePath;constructor(e=`.mejora/baseline.json`){this.baselinePath=e}static create(e){return{checks:e,version:1}}static getEntry(e,t){return e?.checks[t]}static update(t,n,r){let i=t??e.create({}),a=i.checks[n];return J(r,a)?i:{...i,checks:{...i.checks,[n]:r}}}async load(){try{let e=await d(this.baselinePath,`utf8`);if(e.includes(`<<<<<<<`)){Z.start(`Merge conflict detected in baseline, auto-resolving...`);let t=K(e);return await this.save(t,!0),Z.success(`Baseline conflict resolved`),t}return JSON.parse(e)}catch(e){if(e.code===`ENOENT`)return null;throw e}}async save(e,t=!1){if(F&&!t)return;let n=`${JSON.stringify(e,null,2)}\n`,r=this.baselinePath.replace(`.json`,`.md`),i=q(e,a(this.baselinePath));await u(a(this.baselinePath),{recursive:!0}),await Promise.all([f(this.baselinePath,n,`utf8`),f(r,i,`utf8`)])}};function ne(){return{hasImprovement:!1,hasRegression:!1,isInitial:!0,newItems:[],removedItems:[]}}function re(e,t){return{hasImprovement:t.length>0,hasRegression:e.length>0,isInitial:!1,newItems:e.toSorted(),removedItems:t.toSorted()}}function ie(e,t){let n=[];for(let r of e)t.has(r)||n.push(r);return n}function ae(e,t){let n=[];for(let r of t)e.has(r)||n.push(r);return n}function oe(e,t){let n=new Set(e.items),r=new Set(t.items);return re(ie(n,r),ae(n,r))}function se(e,t){return t?oe(e,t):ne()}var ce=class n{baselineManager;constructor(e){this.baselineManager=new Q(e)}static filterChecks(e,t){let r={...e};if(t.only){let e=n.resolveRegex(t.only,`--only`);r=Object.fromEntries(Object.entries(r).filter(([t])=>e.test(t)))}if(t.skip){let e=n.resolveRegex(t.skip,`--skip`);r=Object.fromEntries(Object.entries(r).filter(([t])=>!e.test(t)))}return r}static resolveRegex(e,t){try{return new RegExp(e)}catch{throw Error(`Invalid regex pattern for ${t}: "${e}"`)}}static async runCheck(n){return n.type===`eslint`?(await t(),i(n)):(await e(),r(n))}async run(e,t={}){let r=performance.now(),i=await this.baselineManager.load(),a=[],o=!1,s=!1,c=!1,l=i,u=n.filterChecks(e.checks,t);for(let[e,d]of Object.entries(u))try{let r=performance.now(),u=await n.runCheck(d),f=performance.now()-r,p=Q.getEntry(i,e),m=se(u,p),h={baseline:p,checkId:e,duration:f,hasImprovement:m.hasImprovement,hasRegression:m.hasRegression,isInitial:m.isInitial,newItems:m.newItems,removedItems:m.removedItems,snapshot:u};a.push(h),m.hasRegression&&(o=!0),m.hasImprovement&&(s=!0),m.isInitial&&(c=!0),(m.hasImprovement||t.force||m.isInitial)&&(l=Q.update(l,e,{items:u.items,type:u.type}))}catch(t){return Z.error(`Error running check "${e}":`,t),{exitCode:2,hasImprovement:!1,hasRegression:!0,results:a,totalDuration:performance.now()-r}}l&&l!==i&&(!o||t.force||c)&&await this.baselineManager.save(l,t.force);let d=0;o&&!t.force&&(d=1);let f=performance.now()-r;return{exitCode:d,hasImprovement:s,hasRegression:o,results:a,totalDuration:f}}};const{values:$}=c({allowPositionals:!1,options:{force:{default:!1,short:`f`,type:`boolean`},help:{short:`h`,type:`boolean`},json:{default:!1,type:`boolean`},only:{type:`string`},skip:{type:`string`}},strict:!0});$.help&&(Z.log(`
17
+ mejora - Prevent regressions by allowing only improvement
18
+
19
+ Usage:
20
+ mejora [options]
21
+
22
+ Options:
23
+ -f, --force Update baseline even with regressions
24
+ --json Output results as JSON
25
+ --only <pattern> Run only checks matching pattern (e.g., "eslint > *")
26
+ --skip <pattern> Skip checks matching pattern
27
+ -h, --help Show this help message
28
+
29
+ Examples:
30
+ mejora
31
+ mejora --force
32
+ mejora --json
33
+ mejora --only "eslint > *"
34
+ mejora --skip typescript
35
+ `),process.exit(0));try{let e=new ce,t=await n(),r=await e.run(t,{force:$.force,json:$.json,only:$.only,skip:$.skip});$.json?Z.log(E(r)):Z.log(N(r)),process.exit(r.exitCode)}catch(e){e instanceof Error?Z.error(e.message):Z.error(e),process.exit(2)}export{};
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "mejora",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "keywords": [],
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/jimmy-guzman/mejora.git"
9
+ },
10
+ "license": "MIT",
11
+ "sideEffects": false,
12
+ "type": "module",
13
+ "exports": {
14
+ ".": "./dist/index.mjs"
15
+ },
16
+ "main": "./dist/index.mjs",
17
+ "types": "./dist/index.d.mts",
18
+ "bin": {
19
+ "mejora": "./dist/run.mjs"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "peerDependencies": {
25
+ "eslint": "^9.34.0",
26
+ "typescript": "^5.0.0"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "eslint": {
30
+ "optional": true
31
+ },
32
+ "typescript": {
33
+ "optional": true
34
+ }
35
+ },
36
+ "engines": {
37
+ "node": ">= 22.18.0"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }