@micelio/eslint-config 0.2.4 → 0.2.6

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
@@ -30,3 +30,26 @@ version `AGENTS.md` pins (`` `standard: X.Y` ``), using the SHA-256 hashes in
30
30
  `@micelio/eslint-config/standard-hashes.json`. Add it as `"lint:standard": "micelio-lint-standard"`
31
31
  and to `npm run check`. A pinned version newer than the installed package knows fails with a message
32
32
  that says to upgrade the package.
33
+
34
+ A repository may also pin its profiles on the same line, comma-separated (`` `standard: 2.0` ``,
35
+ `` `profile: typescript, site-engine` ``; ADR-0001); with no `profile:` pin it is read as
36
+ `typescript`. In `standard-hashes.json` a version maps to one hash (1.4 to 1.8, one file) or to
37
+ `{ "core": "<sha256>", "profiles": { "<profile>": "<sha256>" } }`; for the second form the command
38
+ also compares `docs/standard/<profile>.md` for each pinned profile and names the file that differs.
39
+ A profile the pinned version does not have fails, naming the ones it has (a one-file version has
40
+ `typescript` and `site-engine`).
41
+
42
+ ## `lint:ee`
43
+
44
+ `micelio-lint-ee [--allow <path>]... [root]` fails when a file git tracks enables Strapi Enterprise
45
+ code, which is not AGPL-compatible (bogd3v/micelio-cms#96). It reads the tracked files with an
46
+ extension of code or configuration (`.js`, `.ts` and their variants, `.json`, `.yml`, `.sh`,
47
+ `.env.example`, `Dockerfile`) line by line and reports `path:line message` for three rules:
48
+
49
+ - the line names `STRAPI_LICENSE` (sets or reads the Enterprise licence key);
50
+ - the line imports `@strapi/review-workflows` or `@strapi/content-releases`;
51
+ - the line imports Strapi code under `ee/` (`@strapi/<package>/ee`).
52
+
53
+ `--allow <path>` (repeatable) names a tracked file, relative to the root, that may mention them, such
54
+ as `THIRD-PARTY.md` or `package-lock.json`; nothing else is skipped. A repository without Strapi
55
+ passes. Add it as `"lint:ee": "micelio-lint-ee"` and to `npm run check`.
package/dist/ee.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /** One rule of `micelio-lint-ee`: a line that matches `pattern` fails with `message`. */
2
+ export interface EeRule {
3
+ /** The text that marks the line as Strapi Enterprise code. */
4
+ pattern: RegExp;
5
+ /** What the line does, as it appears in the report. */
6
+ message: string;
7
+ }
8
+ /**
9
+ * The three rules of `micelio-cms`'s `scripts/check-no-ee.mjs`, none dropped or narrowed
10
+ * (Strapi Enterprise code is not AGPL-compatible, bogd3v/micelio-cms#96).
11
+ */
12
+ export declare const EE_RULES: readonly EeRule[];
13
+ /** What `lintEe` found. */
14
+ export interface EeResult {
15
+ /** True when no scanned file matches a rule. */
16
+ ok: boolean;
17
+ /** One `path:line message` per match. */
18
+ failures: string[];
19
+ /** How many files were scanned. */
20
+ checked: number;
21
+ /** One line for the terminal. */
22
+ message: string;
23
+ }
24
+ /**
25
+ * Scans the files git tracks in `root` for Strapi Enterprise code.
26
+ *
27
+ * @param root - The repository directory.
28
+ * @param allow - The tracked paths, relative to `root`, that may name the patterns (a licence list, the lockfile).
29
+ * @returns The matches, one per line and rule.
30
+ */
31
+ export declare function lintEe(root: string, allow?: readonly string[]): EeResult;
package/dist/ee.js ADDED
@@ -0,0 +1,52 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * The three rules of `micelio-cms`'s `scripts/check-no-ee.mjs`, none dropped or narrowed
6
+ * (Strapi Enterprise code is not AGPL-compatible, bogd3v/micelio-cms#96).
7
+ */
8
+ export const EE_RULES = [
9
+ {
10
+ pattern: /STRAPI_LICENSE/,
11
+ message: 'sets or reads STRAPI_LICENSE (Strapi Enterprise license key)',
12
+ },
13
+ {
14
+ pattern: /['"]@strapi\/(review-workflows|content-releases)['"/]/,
15
+ message: 'imports a Strapi Enterprise package',
16
+ },
17
+ { pattern: /['"]@strapi\/[^'"]+\/ee(\/|['"])/, message: 'imports Strapi code under ee/' },
18
+ ];
19
+ /** The tracked files that can hold code or configuration: the ones the script of `micelio-cms` reads. */
20
+ const SCANNED = /\.(m?[jt]sx?|json|ya?ml|sh|env\.example)$|(^|\/)(Dockerfile|\.env\.example)$/;
21
+ /**
22
+ * Scans the files git tracks in `root` for Strapi Enterprise code.
23
+ *
24
+ * @param root - The repository directory.
25
+ * @param allow - The tracked paths, relative to `root`, that may name the patterns (a licence list, the lockfile).
26
+ * @returns The matches, one per line and rule.
27
+ */
28
+ export function lintEe(root, allow = []) {
29
+ const files = execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8' })
30
+ .split('\n')
31
+ .filter((file) => file && !allow.includes(file) && SCANNED.test(file));
32
+ const failures = [];
33
+ for (const file of files) {
34
+ readFileSync(join(root, file), 'utf8')
35
+ .split('\n')
36
+ .forEach((line, index) => {
37
+ for (const rule of EE_RULES) {
38
+ if (rule.pattern.test(line))
39
+ failures.push(`${file}:${index + 1} ${rule.message}`);
40
+ }
41
+ });
42
+ }
43
+ const ok = failures.length === 0;
44
+ return {
45
+ ok,
46
+ failures,
47
+ checked: files.length,
48
+ message: ok
49
+ ? `No Strapi Enterprise code enabled (${files.length} files checked).`
50
+ : 'Strapi Enterprise code must stay disabled: see THIRD-PARTY.md.',
51
+ };
52
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { lintEe } from './ee.js';
3
+ // `micelio-lint-ee [--allow <path>]... [root]`: the `lint:ee` script of a repository born from `template/`.
4
+ const allow = [];
5
+ let root = process.cwd();
6
+ const args = process.argv.slice(2);
7
+ for (let i = 0; i < args.length; i++) {
8
+ const arg = args[i];
9
+ if (arg === '--allow') {
10
+ const path = args[++i];
11
+ if (path === undefined) {
12
+ console.error('error: --allow needs a path');
13
+ process.exit(2);
14
+ }
15
+ allow.push(path);
16
+ }
17
+ else {
18
+ root = arg;
19
+ }
20
+ }
21
+ const result = lintEe(root, allow);
22
+ if (result.ok) {
23
+ process.stdout.write(`${result.message}\n`);
24
+ }
25
+ else {
26
+ for (const failure of result.failures)
27
+ console.error(`error: ${failure}`);
28
+ console.error(result.message);
29
+ process.exitCode = 1;
30
+ }
@@ -1,7 +1,20 @@
1
1
  /** Where every Micelio repository keeps its copy of the standard. */
2
2
  export declare const STANDARD_PATH = "docs/engineering-standard.md";
3
- /** The SHA-256 of each released version of the standard, by `major.minor`. */
4
- export type StandardHashes = Record<string, string>;
3
+ /**
4
+ * The hashes of a version in the new form (ADR-0001, point 5): the core, at
5
+ * `docs/engineering-standard.md`, and each profile file, at `docs/standard/<profile>.md`.
6
+ */
7
+ export interface VersionHashes {
8
+ /** The SHA-256 of `docs/engineering-standard.md`. */
9
+ core: string;
10
+ /** The SHA-256 of `docs/standard/<profile>.md`, by profile name. */
11
+ profiles: Record<string, string>;
12
+ }
13
+ /**
14
+ * The hashes of each released version of the standard, by `major.minor`: one string for a version
15
+ * that is a single file (1.4 to 1.8), or {@link VersionHashes} for a version in the new form.
16
+ */
17
+ export type StandardHashes = Record<string, string | VersionHashes>;
5
18
  /** The SHA-256 of each synced file (agent prompts, skills, pull request template), by path in the repository. */
6
19
  export type FileHashes = Record<string, string>;
7
20
  /** What `lintStandard` found. */
@@ -16,21 +29,38 @@ export declare function hashOf(content: Buffer | string): string;
16
29
  /** Reads the version a repository pins in its `AGENTS.md`, or `null` when there is no pin. */
17
30
  export declare function readPin(agents: string): string | null;
18
31
  /**
19
- * Rewrites the pin of an `AGENTS.md`, and the sentence that repeats it, to `version`.
32
+ * Reads the profiles a repository pins in its `AGENTS.md`, in the order written: the names after
33
+ * `profile:`, in backticks, comma-separated (`profile: typescript, site-engine`), on the line of the
34
+ * version pin. A name is returned as written, malformed or not.
35
+ *
36
+ * @returns The profile names; `['typescript']` when the line of the version pin has no `profile:`
37
+ * pin, or there is no version pin (ADR-0001, point 3).
38
+ */
39
+ export declare function readProfiles(agents: string): string[];
40
+ /**
41
+ * Rewrites the pin of an `AGENTS.md`, and the sentence that repeats it, to `version`. A
42
+ * `profile:` pin is left as it is.
20
43
  *
21
44
  * @returns The new text, or `null` when `agents` has no pin to rewrite.
22
45
  */
23
46
  export declare function setPin(agents: string, version: string): string | null;
24
47
  /** Reads the version a copy of the standard says it is, or `null` when its header is missing. */
25
48
  export declare function standardVersion(standard: string): string | null;
26
- /** Loads the hash of each released version of the standard bundled in this package. */
27
- export declare function loadHashes(): StandardHashes;
49
+ /**
50
+ * Loads the hashes of each released version of the standard bundled in this package. An entry that
51
+ * is neither one hash nor a version in the new form is left out.
52
+ *
53
+ * @param file - The parsed `standard-hashes.json`; the one bundled in this package by default.
54
+ */
55
+ export declare function loadHashes(file?: Record<string, unknown>): StandardHashes;
28
56
  /** Loads the hash of each file the sync carries next to the standard, by path in the repository. */
29
57
  export declare function loadFileHashes(): FileHashes;
30
58
  /**
31
59
  * Checks that the copy of the standard in `root` is the one published for the version its
32
60
  * `AGENTS.md` pins (standard, section 16). A repository may pin an older version on purpose:
33
- * the pinned one is checked, not the latest.
61
+ * the pinned one is checked, not the latest. For a version in the new form it also checks the file
62
+ * of each profile `AGENTS.md` pins (`typescript` when it pins none), and fails on a profile that
63
+ * version does not have.
34
64
  *
35
65
  * @param root - The repository root.
36
66
  * It also checks the synced files the repository has (`.claude/agents/*.md`, the skills, the pull
package/dist/standard.js CHANGED
@@ -3,8 +3,25 @@ import { existsSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  /** Where every Micelio repository keeps its copy of the standard. */
5
5
  export const STANDARD_PATH = 'docs/engineering-standard.md';
6
+ /** Where a repository keeps the file of each profile it pins: `docs/standard/<profile>.md`. */
7
+ const PROFILE_DIR = 'docs/standard';
8
+ /** The profile a repository with no `profile:` pin is read as (ADR-0001, point 3). */
9
+ const DEFAULT_PROFILE = 'typescript';
6
10
  /** The machine-readable pin in `AGENTS.md`: the version in backticks after `standard:`. */
7
11
  const PIN = /(`standard:\s*)(\d+\.\d+)(`)/;
12
+ /**
13
+ * The profile pin in `AGENTS.md`: what follows `profile:` in backticks, on the line of the version
14
+ * pin (ADR-0001, point 3). The names are taken as written, so a malformed one fails as a profile the
15
+ * version does not have.
16
+ */
17
+ const PROFILE_PIN = /`profile:([^`]*)`/;
18
+ /**
19
+ * The profiles a version in the old form has: its one file holds the rules that the split moves to
20
+ * `typescript` and `site-engine` (ADR-0001, point 1).
21
+ */
22
+ const SINGLE_FILE_PROFILES = [DEFAULT_PROFILE, 'site-engine'];
23
+ /** A key of `standard-hashes.json` that names a version, as opposed to `files`. */
24
+ const VERSION_KEY = /^\d+\.\d+$/;
8
25
  /** The sentence of `AGENTS.md` that says the same in words, kept in step with the pin. */
9
26
  const PIN_IN_WORDS = /(follows standard version )(\d+\.\d+)/;
10
27
  /** The version line of the standard itself: `Version 1.4 (2026-10-20).` */
@@ -18,7 +35,21 @@ export function readPin(agents) {
18
35
  return PIN.exec(agents)?.[2] ?? null;
19
36
  }
20
37
  /**
21
- * Rewrites the pin of an `AGENTS.md`, and the sentence that repeats it, to `version`.
38
+ * Reads the profiles a repository pins in its `AGENTS.md`, in the order written: the names after
39
+ * `profile:`, in backticks, comma-separated (`profile: typescript, site-engine`), on the line of the
40
+ * version pin. A name is returned as written, malformed or not.
41
+ *
42
+ * @returns The profile names; `['typescript']` when the line of the version pin has no `profile:`
43
+ * pin, or there is no version pin (ADR-0001, point 3).
44
+ */
45
+ export function readProfiles(agents) {
46
+ const line = agents.split('\n').find((text) => PIN.test(text)) ?? '';
47
+ const pin = PROFILE_PIN.exec(line)?.[1];
48
+ return pin === undefined ? [DEFAULT_PROFILE] : pin.split(',').map((name) => name.trim());
49
+ }
50
+ /**
51
+ * Rewrites the pin of an `AGENTS.md`, and the sentence that repeats it, to `version`. A
52
+ * `profile:` pin is left as it is.
22
53
  *
23
54
  * @returns The new text, or `null` when `agents` has no pin to rewrite.
24
55
  */
@@ -36,11 +67,31 @@ function readHashesFile() {
36
67
  const file = new URL('../standard-hashes.json', import.meta.url);
37
68
  return JSON.parse(readFileSync(file, 'utf8'));
38
69
  }
39
- /** Loads the hash of each released version of the standard bundled in this package. */
40
- export function loadHashes() {
70
+ /** True when `value` is a map of strings to strings. */
71
+ function isStringMap(value) {
72
+ return (typeof value === 'object' &&
73
+ value !== null &&
74
+ Object.values(value).every((hash) => typeof hash === 'string'));
75
+ }
76
+ /** True when `value` is the entry of a version in the new form. */
77
+ function isVersionHashes(value) {
78
+ if (typeof value !== 'object' || value === null)
79
+ return false;
80
+ const entry = value;
81
+ return typeof entry.core === 'string' && isStringMap(entry.profiles);
82
+ }
83
+ /**
84
+ * Loads the hashes of each released version of the standard bundled in this package. An entry that
85
+ * is neither one hash nor a version in the new form is left out.
86
+ *
87
+ * @param file - The parsed `standard-hashes.json`; the one bundled in this package by default.
88
+ */
89
+ export function loadHashes(file = readHashesFile()) {
41
90
  const hashes = {};
42
- for (const [key, value] of Object.entries(readHashesFile())) {
43
- if (typeof value === 'string')
91
+ for (const [key, value] of Object.entries(file)) {
92
+ if (!VERSION_KEY.test(key))
93
+ continue;
94
+ if (typeof value === 'string' || isVersionHashes(value))
44
95
  hashes[key] = value;
45
96
  }
46
97
  return hashes;
@@ -64,10 +115,16 @@ function latestVersion(hashes) {
64
115
  });
65
116
  return latest ?? null;
66
117
  }
118
+ /** The path of the file of `profile` in a repository. */
119
+ function profilePath(profile) {
120
+ return `${PROFILE_DIR}/${profile}.md`;
121
+ }
67
122
  /**
68
123
  * Checks that the copy of the standard in `root` is the one published for the version its
69
124
  * `AGENTS.md` pins (standard, section 16). A repository may pin an older version on purpose:
70
- * the pinned one is checked, not the latest.
125
+ * the pinned one is checked, not the latest. For a version in the new form it also checks the file
126
+ * of each profile `AGENTS.md` pins (`typescript` when it pins none), and fails on a profile that
127
+ * version does not have.
71
128
  *
72
129
  * @param root - The repository root.
73
130
  * It also checks the synced files the repository has (`.claude/agents/*.md`, the skills, the pull
@@ -85,7 +142,8 @@ export function lintStandard(root, hashes = loadHashes(), fileHashes = loadFileH
85
142
  message: 'AGENTS.md is missing: it pins the standard version (`standard: X.Y`).',
86
143
  };
87
144
  }
88
- const pin = readPin(readFileSync(agentsPath, 'utf8'));
145
+ const agents = readFileSync(agentsPath, 'utf8');
146
+ const pin = readPin(agents);
89
147
  if (!pin) {
90
148
  return {
91
149
  ok: false,
@@ -104,13 +162,45 @@ export function lintStandard(root, hashes = loadHashes(), fileHashes = loadFileH
104
162
  message: `Standard ${pin}, pinned in AGENTS.md, is unknown to this version of @micelio/eslint-config (it knows ${known}): upgrade the package or fix the pin.`,
105
163
  };
106
164
  }
165
+ const core = typeof expected === 'string' ? expected : expected.core;
166
+ const profiles = readProfiles(agents);
167
+ const known = typeof expected === 'string' ? SINGLE_FILE_PROFILES : Object.keys(expected.profiles);
168
+ const unknown = profiles.filter((profile) => !known.includes(profile));
169
+ if (unknown.length > 0) {
170
+ return {
171
+ ok: false,
172
+ message: `AGENTS.md pins profile ${unknown.join(', ')}, which standard ${pin} does not have (it has ${known.join(', ')}): fix the profile pin.`,
173
+ };
174
+ }
107
175
  const actual = hashOf(readFileSync(copyPath));
108
- if (actual !== expected) {
176
+ if (actual !== core) {
109
177
  return {
110
178
  ok: false,
111
- message: `${STANDARD_PATH} does not match standard ${pin}, the version AGENTS.md pins (sha256 ${actual}, published ${expected}). Restore it, or sync it from bogd3v/micelio-engineering.`,
179
+ message: `${STANDARD_PATH} does not match standard ${pin}, the version AGENTS.md pins (sha256 ${actual}, published ${core}). Restore it, or sync it from bogd3v/micelio-engineering.`,
112
180
  };
113
181
  }
182
+ const checked = [STANDARD_PATH];
183
+ if (typeof expected !== 'string') {
184
+ for (const profile of profiles) {
185
+ const path = profilePath(profile);
186
+ const file = join(root, path);
187
+ if (!existsSync(file)) {
188
+ return {
189
+ ok: false,
190
+ message: `${path} is missing; AGENTS.md pins standard ${pin} with profile ${profile}.`,
191
+ };
192
+ }
193
+ const published = expected.profiles[profile];
194
+ const hash = hashOf(readFileSync(file));
195
+ if (hash !== published) {
196
+ return {
197
+ ok: false,
198
+ message: `${path} does not match profile ${profile} of standard ${pin}, the version AGENTS.md pins (sha256 ${hash}, published ${published}). Restore it, or sync it from bogd3v/micelio-engineering.`,
199
+ };
200
+ }
201
+ checked.push(path);
202
+ }
203
+ }
114
204
  const stale = pin === latestVersion(hashes)
115
205
  ? Object.entries(fileHashes)
116
206
  .filter(([path, hash]) => {
@@ -125,5 +215,11 @@ export function lintStandard(root, hashes = loadHashes(), fileHashes = loadFileH
125
215
  message: `${stale.join(', ')} ${stale.length === 1 ? 'does' : 'do'} not match the published copy of standard ${pin}. Restore ${stale.length === 1 ? 'it' : 'them'} if edited by hand, or, in a sync pull request, upgrade @micelio/eslint-config to the release that publishes the new hashes.`,
126
216
  };
127
217
  }
128
- return { ok: true, message: `Standard ${pin}: ${STANDARD_PATH} matches the published copy.` };
218
+ if (checked.length === 1) {
219
+ return { ok: true, message: `Standard ${pin}: ${STANDARD_PATH} matches the published copy.` };
220
+ }
221
+ return {
222
+ ok: true,
223
+ message: `Standard ${pin} (${profiles.join(', ')}): ${checked.join(', ')} match the published copy.`,
224
+ };
129
225
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micelio/eslint-config",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "The ESLint flat config of the Micelio repositories, the licence allowlist behind lint:licenses and lint:standard.",
5
5
  "license": "MIT-0",
6
6
  "repository": {
@@ -45,6 +45,7 @@
45
45
  "provenance": true
46
46
  },
47
47
  "bin": {
48
+ "micelio-lint-ee": "./dist/lint-ee.js",
48
49
  "micelio-lint-standard": "./dist/lint-standard.js"
49
50
  },
50
51
  "devDependencies": {
@@ -6,14 +6,14 @@
6
6
  "1.8": "bbdbd8b0f7d394ba24471a3b65dbcf7a82b9899a221a39e78ffe819ebbc79249",
7
7
  "files": {
8
8
  ".claude/agents/master.md": "35e9ce4cc637ca814352916dd24e64f25da66f0c3c36fb492e2172c40e6d6416",
9
- ".claude/agents/planner.md": "944a55dae33c1f4290c574923d12af1d3b1f24c4cd5dfb75f4e819ca24ecb727",
9
+ ".claude/agents/planner.md": "7bb5a71f7ff8ebf97a6e775c1e85744d668c7ef2601cd8dc52599b9a60c851e5",
10
10
  ".claude/agents/reviewer.md": "8edcd83631b191013217eb9f309b108f54e988708496182877077b65a9b6f1cf",
11
- ".claude/agents/verifier.md": "7fc1c73eec7c64ba669ece73075c0f967b316d81c9389a00ae729acf29505ae8",
12
- ".claude/agents/worker.md": "3f4dafea2266a752a5aba7f26035bdab1c5a4b6591916f0c2be4fdb68ad924ca",
13
- ".claude/skills/ship-pr/SKILL.md": "bd01f1bfcccfef9f50872425f1a2060554a36fa61e6915cfb137e07889c25f25",
11
+ ".claude/agents/verifier.md": "12af503892295de5da0496344b7760c5a349aa81ab7b61db4e9b9b7e1dd757e8",
12
+ ".claude/agents/worker.md": "e2faeb86c51418373978a15dd025f7800ad12a053c962fdb0c58b79251fd8290",
13
+ ".claude/skills/ship-pr/SKILL.md": "ced49dbd6ab9c4513afa2c3afb0350810d3693c4a758612512f9ecadab66f045",
14
14
  ".claude/skills/blind-review/SKILL.md": "62f43d0ccd762ad7197bb48e31e7041689cc1d07e16678dffd9c8e2715be7980",
15
- ".claude/skills/verify-pr/SKILL.md": "6143ac741580154a3121b0ec77c601bbb8345816f53e4947879365c187a6c6a5",
16
- ".github/pull_request_template.md": "10ab5b606b86dc13682387e7a9aed7ce2cf8ef1a227e80cafa1dd8fc50f4481c",
15
+ ".claude/skills/verify-pr/SKILL.md": "d53466d54e51b1e1ee09c47f3e1629f997163d5ea716c0c860f52492ddc9ce7b",
16
+ ".github/pull_request_template.md": "8ce62f8b6d59f14562e6d962d87491c2c3b1c9a8f640afe673f23eced6642ba7",
17
17
  "CLAUDE.md": "2fe9efbc0c4a6e0cbd446afb506911ba28b0068a42f8686676635986d968cefd"
18
18
  }
19
19
  }