@sheplu/editorconfig 0.12.2 → 0.14.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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # editorconfig
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@sheplu/editorconfig.svg)](https://www.npmjs.com/package/@sheplu/editorconfig)
4
+ [![Quality gates](https://github.com/sheplu/editorconfig/actions/workflows/quality-gates.yaml/badge.svg)](https://github.com/sheplu/editorconfig/actions/workflows/quality-gates.yaml)
5
+ [![Node.js](https://img.shields.io/node/v/@sheplu/editorconfig.svg)](https://nodejs.org)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
3
8
  A small CLI to manage a **consistent `.editorconfig`** across your projects.
4
9
 
5
10
  - ✅ Generate a sane default `.editorconfig` in seconds
@@ -178,6 +183,39 @@ The custom template must follow the same semantics as the built-ins:
178
183
 
179
184
  URL fetching is constrained for safety: `https://` only (`http://` is rejected before any network call), redirects must stay on https (max 5 hops), 10-second timeout, 1 MB response cap. Templates are fetched on every invocation — there is no local cache.
180
185
 
186
+ ### `--json` (machine-readable check output)
187
+
188
+ `check` accepts a `--json` flag that swaps the human-readable report for a structured JSON document on stdout. The exit code is unchanged (`0` pass, `1` fail), so it stays drop-in for CI while letting dashboards and scripts parse the result.
189
+
190
+ ```bash
191
+ npx @sheplu/editorconfig --mode=check --json
192
+ npx @sheplu/editorconfig --mode=check --recursive --json
193
+ ```
194
+
195
+ Single-file shape:
196
+
197
+ ```json
198
+ {
199
+ "mode": "check",
200
+ "path": ".editorconfig",
201
+ "ok": true,
202
+ "summary": { "total": 1, "matched": 1, "failed": 0, "unknown": 0 },
203
+ "sections": [
204
+ { "header": "[*]", "status": "match", "detail": "" }
205
+ ]
206
+ }
207
+ ```
208
+
209
+ In `--recursive` mode the payload instead carries `recursive: true`, a `files` array (each with its `role`, `summary`, and `sections`) and a `crossFileIssues` array describing `child-root` / `redundant` / `contradiction` findings. When no JSON output is requested, the human-readable report is printed exactly as before.
210
+
211
+ ### `--version`
212
+
213
+ ```bash
214
+ npx @sheplu/editorconfig --version
215
+ ```
216
+
217
+ Prints the installed package version (also available as `-v`).
218
+
181
219
  ### `--help`
182
220
 
183
221
  ```bash
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { compareEditorConfig } from './src/check.js';
4
- import { parseCliArgs, printHelp } from './src/cli/options.js';
4
+ import { parseCliArgs, printHelp, printVersion } from './src/cli/options.js';
5
5
  import { dispatchValues } from './src/cli/dispatch.js';
6
6
  import { createEditorConfig } from './src/cli/write-flow.js';
7
7
 
@@ -16,6 +16,10 @@ async function main() {
16
16
  printHelp();
17
17
  return;
18
18
  }
19
+ if (parsed.values.version) {
20
+ printVersion();
21
+ return;
22
+ }
19
23
  await dispatchValues(parsed.values);
20
24
  };
21
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sheplu/editorconfig",
3
- "version": "0.12.2",
3
+ "version": "0.14.0",
4
4
  "description": "CLI to generate and validate a consistent .editorconfig across your projects",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -4,9 +4,11 @@ import { discoverEditorConfigs } from './discover.js';
4
4
  import { classify, crossFileIssues } from './cascade.js';
5
5
  import {
6
6
  compareEditorConfigForRole,
7
+ identityReplacer,
7
8
  NO_LANGUAGE_FILTER,
8
9
  printReport,
9
10
  reportIsFailing,
11
+ reportToSections,
10
12
  summarizeReport,
11
13
  } from './check.js';
12
14
  import { logger } from './utils/logger.js';
@@ -162,21 +164,23 @@ function formatGlobalSummary({ entries, crossIssues, strict }) {
162
164
  return { line: `${summaryHead(passed)} — ${parts.join('; ')}`, failed: !passed };
163
165
  }
164
166
 
165
- function processTree({ tree, parsedLanguages, overrides, startDir }) {
167
+ function processTree({ tree, parsedLanguages, overrides, startDir, json }) {
166
168
  const entries = buildFileEntries(tree, parsedLanguages, overrides);
167
169
  const [rootEntry, ...childEntries] = entries;
168
170
  const crossIssues = collectCrossIssues(rootEntry, childEntries);
169
- for (const entry of entries) {
170
- printFileBlock(entry, startDir);
171
+ if (!json) {
172
+ for (const entry of entries) {
173
+ printFileBlock(entry, startDir);
174
+ }
171
175
  }
172
176
  return { entries, crossIssues };
173
177
  }
174
178
 
175
- function gatherAll({ trees, parsedLanguages, overrides, startDir }) {
179
+ function gatherAll({ trees, parsedLanguages, overrides, startDir, json }) {
176
180
  const allEntries = [];
177
181
  const allCrossIssues = [];
178
182
  for (const tree of trees) {
179
- const { entries, crossIssues } = processTree({ tree, parsedLanguages, overrides, startDir });
183
+ const { entries, crossIssues } = processTree({ tree, parsedLanguages, overrides, startDir, json });
180
184
  allEntries.push(...entries);
181
185
  allCrossIssues.push(...crossIssues);
182
186
  }
@@ -192,18 +196,82 @@ function reportAndExit({ allEntries, allCrossIssues, strict, startDir }) {
192
196
  }
193
197
  }
194
198
 
195
- export function runCheckRecursive({ startDir: rawStart, parsedLanguages, strict, overrides }) {
196
- const startDir = resolveStartDir(rawStart);
197
- const paths = discoverEditorConfigs(startDir);
198
- if (paths.length === 0) {
199
- logger.log(`No .editorconfig files found under ${startDir}`);
199
+ function jsonFileEntry(entry, startDir, strict) {
200
+ return {
201
+ path: displayPath(entry.path, startDir),
202
+ role: entry.role,
203
+ failed: reportIsFailing(entry.report, strict),
204
+ summary: summarizeReport(entry.report),
205
+ sections: reportToSections(entry.report),
206
+ };
207
+ }
208
+
209
+ function jsonCrossIssue(issue, startDir) {
210
+ const copy = {};
211
+ for (const [key, value] of Object.entries(issue)) {
212
+ copy[key] = value;
213
+ }
214
+ copy.file = displayPath(issue.file, startDir);
215
+ return copy;
216
+ }
217
+
218
+ function buildRecursiveJson({ allEntries, allCrossIssues, strict, startDir }) {
219
+ const summary = formatGlobalSummary({ entries: allEntries, crossIssues: allCrossIssues, strict });
220
+ return {
221
+ mode: 'check',
222
+ recursive: true,
223
+ startDir,
224
+ ok: !summary.failed,
225
+ files: allEntries.map((entry) => jsonFileEntry(entry, startDir, strict)),
226
+ crossFileIssues: allCrossIssues.map((issue) => jsonCrossIssue(issue, startDir)),
227
+ };
228
+ }
229
+
230
+ function emitJson(payload) {
231
+ logger.log(JSON.stringify(payload, identityReplacer, 2));
232
+ }
233
+
234
+ function reportRecursiveJson(payload) {
235
+ const json = buildRecursiveJson(payload);
236
+ emitJson(json);
237
+ if (!json.ok) {
238
+ process.exitCode = 1;
239
+ }
240
+ }
241
+
242
+ function emitEmptyRecursiveJson(startDir) {
243
+ emitJson({ mode: 'check', recursive: true, startDir, ok: true, files: [], crossFileIssues: [] });
244
+ }
245
+
246
+ function reportEmpty(startDir, json) {
247
+ if (json) {
248
+ emitEmptyRecursiveJson(startDir);
200
249
  return;
201
250
  }
251
+ logger.log(`No .editorconfig files found under ${startDir}`);
252
+ }
253
+
254
+ function runWalk({ startDir, paths, parsedLanguages, strict, overrides, json }) {
202
255
  const { allEntries, allCrossIssues } = gatherAll({
203
256
  trees: classify(paths),
204
257
  parsedLanguages,
205
258
  overrides,
206
259
  startDir,
260
+ json,
207
261
  });
262
+ if (json) {
263
+ reportRecursiveJson({ allEntries, allCrossIssues, strict, startDir });
264
+ return;
265
+ }
208
266
  reportAndExit({ allEntries, allCrossIssues, strict, startDir });
209
267
  }
268
+
269
+ export function runCheckRecursive({ startDir: rawStart, parsedLanguages, strict, overrides, json }) {
270
+ const startDir = resolveStartDir(rawStart);
271
+ const paths = discoverEditorConfigs(startDir);
272
+ if (paths.length === 0) {
273
+ reportEmpty(startDir, json);
274
+ return;
275
+ }
276
+ runWalk({ startDir, paths, parsedLanguages, strict, overrides, json });
277
+ }
package/src/check.js CHANGED
@@ -143,7 +143,7 @@ function formatLine({ header, status }) {
143
143
  export function reportIsFailing({ baseIssues, results }, strict) {
144
144
  const failing = [...baseIssues, ...results].some((entry) => FAILING_STATUSES.has(entry.status));
145
145
  const hasUnknown = results.some((entry) => entry.status === 'unknown');
146
- return failing || (strict && hasUnknown);
146
+ return failing || Boolean(strict && hasUnknown);
147
147
  }
148
148
 
149
149
  function buildHeading(displayPath, label) {
@@ -208,11 +208,38 @@ function formatSummary(report, isFailure) {
208
208
  return formatPassSummary(counts);
209
209
  }
210
210
 
211
- export function runCheck({ path, parsedLanguages, strict, overrides }) {
211
+ export function identityReplacer(_key, value) {
212
+ return value;
213
+ }
214
+
215
+ export function reportToSections(report) {
216
+ return [...report.baseIssues, ...report.results].map(({ header, status }) => ({
217
+ header,
218
+ status,
219
+ detail: STATUS_DETAIL[status],
220
+ }));
221
+ }
222
+
223
+ export function buildCheckJson({ path, report, failed }) {
224
+ return {
225
+ mode: 'check',
226
+ path,
227
+ ok: !failed,
228
+ summary: summarizeReport(report),
229
+ sections: reportToSections(report),
230
+ };
231
+ }
232
+
233
+ export function runCheck({ path, parsedLanguages, strict, overrides, json }) {
212
234
  const report = compareEditorConfig(path, parsedLanguages, overrides);
213
- printReport(path, report);
214
235
  const failed = reportIsFailing(report, strict);
215
- logger.log(formatSummary(report, failed));
236
+ if (json) {
237
+ logger.log(JSON.stringify(buildCheckJson({ path, report, failed }), identityReplacer, 2));
238
+ }
239
+ else {
240
+ printReport(path, report);
241
+ logger.log(formatSummary(report, failed));
242
+ }
216
243
  if (failed) {
217
244
  process.exitCode = 1;
218
245
  }
@@ -32,17 +32,17 @@ function resolveCheckPath(values) {
32
32
  return '.editorconfig';
33
33
  }
34
34
 
35
- function dispatchCheck({ path, languages, strict, overrides, recursive }) {
35
+ function dispatchCheck({ path, languages, strict, overrides, recursive, json }) {
36
36
  let filter = languages;
37
37
  if (filter === NOT_PROVIDED) {
38
38
  filter = NO_LANGUAGE_FILTER;
39
39
  }
40
40
  try {
41
41
  if (recursive) {
42
- runCheckRecursive({ startDir: path, parsedLanguages: filter, strict, overrides });
42
+ runCheckRecursive({ startDir: path, parsedLanguages: filter, strict, overrides, json });
43
43
  return;
44
44
  }
45
- runCheck({ path, parsedLanguages: filter, strict, overrides });
45
+ runCheck({ path, parsedLanguages: filter, strict, overrides, json });
46
46
  }
47
47
  catch (error) {
48
48
  logger.error(error.message);
@@ -69,13 +69,13 @@ async function handleWrite({ path, overwrite, languages, overrides, recursive })
69
69
  await dispatchWrite({ path, overwrite, languages, overrides });
70
70
  }
71
71
 
72
- async function runCommand({ mode, path, overwrite, languages, strict, overrides, recursive }) {
72
+ async function runCommand({ mode, path, overwrite, languages, strict, overrides, recursive, json }) {
73
73
  if (mode === 'write') {
74
74
  await handleWrite({ path, overwrite, languages, overrides, recursive });
75
75
  return;
76
76
  }
77
77
  if (mode === 'check') {
78
- dispatchCheck({ path, languages, strict, overrides, recursive });
78
+ dispatchCheck({ path, languages, strict, overrides, recursive, json });
79
79
  return;
80
80
  }
81
81
  logger.error('invalid command');
@@ -95,5 +95,6 @@ export async function dispatchValues(values) {
95
95
  strict: values.strict,
96
96
  overrides,
97
97
  recursive: Boolean(values.recursive),
98
+ json: Boolean(values.json),
98
99
  });
99
100
  }
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from 'node:fs';
1
2
  import { parseArgs } from 'node:util';
2
3
  import {
3
4
  ALIASES,
@@ -10,10 +11,18 @@ export const options = {
10
11
  path: { type: 'string', short: 'p' },
11
12
  languages: { type: 'string', short: 'l' },
12
13
  help: { type: 'boolean', short: 'h' },
14
+ version: { type: 'boolean', short: 'v' },
13
15
  overwrite: { type: 'boolean', short: 'o' },
14
16
  strict: { type: 'boolean', short: 's' },
15
17
  template: { type: 'string', short: 't' },
16
18
  recursive: { type: 'boolean', short: 'r' },
19
+ json: { type: 'boolean' },
20
+ };
21
+
22
+ export function printVersion() {
23
+ const pkgUrl = new URL('../../package.json', import.meta.url);
24
+ const { version } = JSON.parse(readFileSync(pkgUrl, 'utf8'));
25
+ logger.log(version);
17
26
  };
18
27
 
19
28
  export const NOT_PROVIDED = Symbol('languages-not-provided');
@@ -41,7 +50,7 @@ function formatAliasList() {
41
50
  }
42
51
 
43
52
  export function printHelp() {
44
- logger.log(`Usage: editorconfig --mode=<command> [--path=<path>] [--languages=<list>] [--template=<path|url>] [--recursive]
53
+ logger.log(`Usage: editorconfig --mode=<command> [--path=<path>] [--languages=<list>] [--template=<path|url>] [--recursive] [--json]
45
54
 
46
55
  Commands:
47
56
  write Create a .editorconfig file with the selected language sections
@@ -55,6 +64,8 @@ Options:
55
64
  -s, --strict Treat unknown section headers as failures (check only)
56
65
  -t, --template Path or https URL to a custom .editorconfig-syntax file whose sections override the built-in ones
57
66
  -r, --recursive Check only. Walk the start directory and validate every .editorconfig (root + children) with cascade checks
67
+ --json Emit check results as JSON instead of human-readable text (check only)
68
+ -v, --version Show the installed version
58
69
  -h, --help Show this help message
59
70
 
60
71
  Languages: ${AVAILABLE_LANGUAGES.join(', ')}