@theholocron/astromech 4.2.0 → 4.3.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
@@ -104,6 +104,43 @@ there is no config.
104
104
  `holocron run` itself does not read the config yet — that (and
105
105
  `holocron ci`) come in later phases (epic #581).
106
106
 
107
+ ## Lint parity
108
+
109
+ One linter list drives both CI and local — no asymmetry. Source: the
110
+ `lint` task's `linters` array, or auto-detection from the config files
111
+ present. The `linter name → super-linter VALIDATE_* keys` mapping lives in
112
+ one place, `src/linters.ts`.
113
+
114
+ | linter | `VALIDATE_*` | always-on | local binary |
115
+ | ---------------------------- | ----------------------------------------- | ----------------------------------- | ---------------------------------------- |
116
+ | `eslint` | `JAVASCRIPT_ES`, `TYPESCRIPT_ES` | on `eslint.config.*` / `.eslintrc*` | `eslint .` |
117
+ | `prettier` | `*_PRETTIER` (JS/JSX/TS/TSX/MD) + `FIX_*` | yes | `prettier --check .` |
118
+ | `yamllint` | `YAML` | yes | `yamllint .` (usually CI-only) |
119
+ | `actionlint` | `GITHUB_ACTIONS` | yes | `actionlint` (usually CI-only) |
120
+ | `gitleaks` | `GITLEAKS` | yes | `gitleaks dir` (usually CI-only) |
121
+ | `editorconfig` | `EDITORCONFIG` | yes | `editorconfig-checker` (usually CI-only) |
122
+ | `commitlint` | `GIT_COMMITLINT` | yes | `commitlint --last` |
123
+ | `git-merge-conflict-markers` | `GIT_MERGE_CONFLICT_MARKERS` | yes | — (CI only) |
124
+ | `markdownlint` | `MARKDOWN` | on `.markdownlint*` | `markdownlint-cli2` |
125
+
126
+ ```ts
127
+ import { superLinterConfig, resolveLinters } from "@theholocron/astromech";
128
+
129
+ superLinterConfig({ explicit: ["eslint", "prettier"], rootFiles: fs.readdirSync(cwd) });
130
+ // → { env: { VALIDATE_JAVASCRIPT_ES: "true", … }, linters: ["eslint","prettier"], configInputs: { … } }
131
+ ```
132
+
133
+ `superLinterConfig().env` is the exact `VALIDATE_*`/`FIX_*` map the CI
134
+ `lint` job needs — the CLI serializes it as the `super-linter-env` input on
135
+ each repo's generated `lint` thin caller. Setting any `VALIDATE_*` puts
136
+ super-linter in allow-list mode, so emitting only the enabled keys makes it
137
+ run exactly the resolved set.
138
+
139
+ `holocron run lint` (later phase) runs the same set natively: `turbo run
140
+ lint` for the eslint portion (cached), then each other linter whose binary
141
+ resolves; linters with a binary that is not on `PATH` are flagged with an
142
+ install hint; the rest print "CI only".
143
+
107
144
  ## Development
108
145
 
109
146
  | Script | Description |
package/dist/index.d.mts CHANGED
@@ -209,6 +209,73 @@ interface Astromech {
209
209
  }
210
210
  declare function createAstromech(options: AstromechOptions): Astromech;
211
211
  //#endregion
212
+ //#region src/linters.d.ts
213
+ /**
214
+ * The linter registry — one list of linter names (from `config.tasks`'
215
+ * `{ name: "lint", linters: [...] }`, or auto-detected) maps to both the
216
+ * CI super-linter `VALIDATE_*` env and the native local run. This table is
217
+ * the single source of truth for that mapping; {@link superLinterConfig}
218
+ * (CI) and the `holocron run lint` aggregate (local) both read it.
219
+ *
220
+ * Spec: `.notes/tech-astromech-task-runner.spec.md` "Lint parity".
221
+ */
222
+ interface LinterDef {
223
+ /**
224
+ * super-linter `VALIDATE_*` keys this linter turns on. Setting any
225
+ * `VALIDATE_*` puts super-linter in allow-list mode, so emitting only the
226
+ * enabled keys makes it run exactly this set.
227
+ */
228
+ validate: string[];
229
+ /** super-linter `FIX_*` keys (auto-commit / `--write` parity). */
230
+ fix?: string[];
231
+ /**
232
+ * Local binary, resolved from `node_modules/.bin` then `PATH`. Absent →
233
+ * the linter has no meaningful local run (`holocron run lint` prints
234
+ * "CI only"); CI still enforces it.
235
+ */
236
+ localBin?: string;
237
+ /** Args for `localBin` in CHECK mode — never `--write` / `--fix`. */
238
+ localArgs?: string[];
239
+ /**
240
+ * Shown by `holocron run lint` when `localBin` is set but not found on
241
+ * PATH — the "you're missing a tool the repo needs" nudge.
242
+ */
243
+ installHint?: string;
244
+ /**
245
+ * Repo-root filenames that auto-enable this linter when the `lint` task
246
+ * has no explicit `linters` list. Ignored when {@link always} is set.
247
+ */
248
+ detect?: string[];
249
+ /** Enabled regardless of detection (the org baseline). */
250
+ always?: boolean;
251
+ /** Reusable-workflow config-file input this linter honors, if any. */
252
+ configInput?: "eslint-config" | "prettier-config" | "yaml-config";
253
+ }
254
+ /**
255
+ * Known linters, in execution order. `always` entries are the current
256
+ * hard-coded super-linter baseline; `prettier` is always-on because the org
257
+ * applies it universally (super-linter only lints files that exist).
258
+ */
259
+ declare const LINTERS: Record<string, LinterDef>;
260
+ /** Every linter name the registry knows. */
261
+ declare const LINTER_NAMES: ReadonlySet<string>;
262
+ /**
263
+ * Resolve the linter set for a repo. An `explicit` list (from
264
+ * `config.tasks`) wins verbatim; otherwise every `always` linter plus every
265
+ * linter whose `detect` filenames are present at the repo root. Result is
266
+ * ordered by {@link LINTERS} declaration order.
267
+ *
268
+ * @throws when an `explicit` name is not in the registry — a typo is a
269
+ * config bug, not a linter to silently skip.
270
+ */
271
+ declare function resolveLinters(opts: {
272
+ explicit?: string[];
273
+ rootFiles: string[];
274
+ }): Array<{
275
+ name: string;
276
+ def: LinterDef;
277
+ }>;
278
+ //#endregion
212
279
  //#region src/registry.d.ts
213
280
  /**
214
281
  * The task registry — how each task runs *locally*, without GitHub
@@ -246,9 +313,55 @@ interface TaskDef {
246
313
  }>;
247
314
  /** Org-default flags injected by tool name. Removed by a repo override. */
248
315
  flags?: Record<string, string[]>;
316
+ /**
317
+ * This task is the linter aggregate: `holocron run lint` resolves the
318
+ * linter set (`config.tasks` `linters` or auto-detect) and runs each
319
+ * natively instead of using `local`. See `linters.ts` / `super-linter.ts`.
320
+ */
321
+ linters?: boolean;
249
322
  }
250
323
  declare const TASKS: Record<string, TaskDef>;
251
324
  /** Every task name the registry knows. */
252
325
  declare const KNOWN_TASKS: Set<string>;
253
326
  //#endregion
254
- export { type Astromech, type AstromechOptions, type ExecFn, KNOWN_TASKS, KNOWN_WORKFLOWS, type LocalRunner, type OrgContext, type PreviewConfig, type RunLogger, type RunOptions, type RunTaskInput, type RunTaskReport, TASKS, type TaskDef, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, createAstromech, deriveDeployPaths, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, normalizeWorkflowWith, runTask };
327
+ //#region src/super-linter.d.ts
328
+ /**
329
+ * `superLinterConfig()` — turn the resolved linter set into the exact
330
+ * super-linter `VALIDATE_*` / `FIX_*` env the CI `lint` job needs. The CLI
331
+ * serializes {@link SuperLinterConfig.env} as the `super-linter-env` input
332
+ * on each repo's generated `lint` thin caller; the reusable workflow
333
+ * expands it verbatim. This is the CI half of "lint parity" — the local
334
+ * half is the `holocron run lint` aggregate, driven by the same
335
+ * {@link resolveLinters}.
336
+ */
337
+ interface SuperLinterConfig {
338
+ /**
339
+ * Enabled `VALIDATE_*` / `FIX_*` keys → `"true"`. Only enabled keys are
340
+ * present (super-linter allow-list mode). Ready for `JSON.stringify`.
341
+ */
342
+ env: Record<string, string>;
343
+ /** Resolved linter names in execution order — for the human-readable comment. */
344
+ linters: string[];
345
+ /** Config-file inputs the resolved set honors (`eslint-config`, …). */
346
+ configInputs: Partial<Record<"eslint-config" | "prettier-config" | "yaml-config", true>>;
347
+ }
348
+ /**
349
+ * Resolve the super-linter env for a repo's `lint` task.
350
+ *
351
+ * @param opts.explicit the task's `linters` list, if any (else auto-detect)
352
+ * @param opts.rootFiles repo-root filenames (from `listDir(cwd)`)
353
+ * @param opts.includeFix emit `FIX_*` keys too (default `true`)
354
+ */
355
+ declare function superLinterConfig(opts: {
356
+ explicit?: string[];
357
+ rootFiles: string[];
358
+ includeFix?: boolean;
359
+ }): SuperLinterConfig;
360
+ /**
361
+ * The always-on baseline env — every `always` linter, no detection. This is
362
+ * what the reusable `lint.yml`'s `super-linter-env` input defaults to, so a
363
+ * repo whose thin caller has not been re-synced yet behaves exactly as before.
364
+ */
365
+ declare function baselineSuperLinterEnv(): Record<string, string>;
366
+ //#endregion
367
+ export { type Astromech, type AstromechOptions, type ExecFn, KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, type LinterDef, type LocalRunner, type OrgContext, type PreviewConfig, type RunLogger, type RunOptions, type RunTaskInput, type RunTaskReport, type SuperLinterConfig, TASKS, type TaskDef, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, baselineSuperLinterEnv, createAstromech, deriveDeployPaths, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, normalizeWorkflowWith, resolveLinters, runTask, superLinterConfig };
package/dist/index.mjs CHANGED
@@ -15,10 +15,13 @@ const TASKS = {
15
15
  tool: "tsc",
16
16
  args: ["--noEmit"]
17
17
  } },
18
- lint: { local: {
19
- tool: "eslint",
20
- args: ["."]
21
- } },
18
+ lint: {
19
+ local: {
20
+ tool: "eslint",
21
+ args: ["."]
22
+ },
23
+ linters: true
24
+ },
22
25
  build: { local: { detect: [
23
26
  {
24
27
  when: /^tsdown\.config\.(ts|js|mjs|cjs)$/,
@@ -529,4 +532,179 @@ function createAstromech(options) {
529
532
  };
530
533
  }
531
534
  //#endregion
532
- export { KNOWN_TASKS, KNOWN_WORKFLOWS, TASKS, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, createAstromech, deriveDeployPaths, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, normalizeWorkflowWith, runTask };
535
+ //#region src/linters.ts
536
+ /**
537
+ * Known linters, in execution order. `always` entries are the current
538
+ * hard-coded super-linter baseline; `prettier` is always-on because the org
539
+ * applies it universally (super-linter only lints files that exist).
540
+ */
541
+ const LINTERS = {
542
+ eslint: {
543
+ validate: ["VALIDATE_JAVASCRIPT_ES", "VALIDATE_TYPESCRIPT_ES"],
544
+ localBin: "eslint",
545
+ localArgs: ["."],
546
+ detect: [
547
+ "eslint.config.ts",
548
+ "eslint.config.js",
549
+ "eslint.config.mjs",
550
+ "eslint.config.cjs",
551
+ ".eslintrc",
552
+ ".eslintrc.json",
553
+ ".eslintrc.yml",
554
+ ".eslintrc.yaml",
555
+ ".eslintrc.cjs"
556
+ ],
557
+ configInput: "eslint-config"
558
+ },
559
+ prettier: {
560
+ validate: [
561
+ "VALIDATE_JAVASCRIPT_PRETTIER",
562
+ "VALIDATE_JSX_PRETTIER",
563
+ "VALIDATE_TYPESCRIPT_PRETTIER",
564
+ "VALIDATE_TSX",
565
+ "VALIDATE_MARKDOWN_PRETTIER"
566
+ ],
567
+ fix: [
568
+ "FIX_JAVASCRIPT_PRETTIER",
569
+ "FIX_JSX_PRETTIER",
570
+ "FIX_TYPESCRIPT_PRETTIER",
571
+ "FIX_TSX",
572
+ "FIX_MARKDOWN_PRETTIER"
573
+ ],
574
+ always: true,
575
+ localBin: "prettier",
576
+ localArgs: ["--check", "."],
577
+ configInput: "prettier-config"
578
+ },
579
+ yamllint: {
580
+ validate: ["VALIDATE_YAML"],
581
+ always: true,
582
+ localBin: "yamllint",
583
+ localArgs: ["."],
584
+ installHint: "brew install yamllint",
585
+ configInput: "yaml-config"
586
+ },
587
+ actionlint: {
588
+ validate: ["VALIDATE_GITHUB_ACTIONS"],
589
+ always: true,
590
+ localBin: "actionlint",
591
+ localArgs: [],
592
+ installHint: "brew install actionlint"
593
+ },
594
+ gitleaks: {
595
+ validate: ["VALIDATE_GITLEAKS"],
596
+ always: true,
597
+ localBin: "gitleaks",
598
+ localArgs: ["dir", "--no-banner"],
599
+ installHint: "brew install gitleaks"
600
+ },
601
+ editorconfig: {
602
+ validate: ["VALIDATE_EDITORCONFIG"],
603
+ always: true,
604
+ localBin: "editorconfig-checker",
605
+ localArgs: [],
606
+ installHint: "brew install editorconfig-checker"
607
+ },
608
+ commitlint: {
609
+ validate: ["VALIDATE_GIT_COMMITLINT"],
610
+ always: true,
611
+ localBin: "commitlint",
612
+ localArgs: ["--last"]
613
+ },
614
+ "git-merge-conflict-markers": {
615
+ validate: ["VALIDATE_GIT_MERGE_CONFLICT_MARKERS"],
616
+ always: true
617
+ },
618
+ markdownlint: {
619
+ validate: ["VALIDATE_MARKDOWN"],
620
+ localBin: "markdownlint-cli2",
621
+ localArgs: ["**/*.md"],
622
+ detect: [
623
+ ".markdownlint.json",
624
+ ".markdownlint.jsonc",
625
+ ".markdownlint.yaml",
626
+ ".markdownlint.yml",
627
+ ".markdownlint-cli2.jsonc",
628
+ ".markdownlint-cli2.yaml",
629
+ ".markdownlint-cli2.mjs"
630
+ ]
631
+ }
632
+ };
633
+ /** Every linter name the registry knows. */
634
+ const LINTER_NAMES = new Set(Object.keys(LINTERS));
635
+ /**
636
+ * Resolve the linter set for a repo. An `explicit` list (from
637
+ * `config.tasks`) wins verbatim; otherwise every `always` linter plus every
638
+ * linter whose `detect` filenames are present at the repo root. Result is
639
+ * ordered by {@link LINTERS} declaration order.
640
+ *
641
+ * @throws when an `explicit` name is not in the registry — a typo is a
642
+ * config bug, not a linter to silently skip.
643
+ */
644
+ function resolveLinters(opts) {
645
+ const order = Object.keys(LINTERS);
646
+ if (opts.explicit && opts.explicit.length > 0) {
647
+ const unknown = opts.explicit.filter((n) => !LINTER_NAMES.has(n));
648
+ if (unknown.length > 0) throw new Error(`unknown linter${unknown.length > 1 ? "s" : ""} ${unknown.map((n) => `"${n}"`).join(", ")} — known: ${order.join(", ")}`);
649
+ const wanted = new Set(opts.explicit);
650
+ return order.filter((n) => wanted.has(n)).map((name) => ({
651
+ name,
652
+ def: LINTERS[name]
653
+ }));
654
+ }
655
+ const present = new Set(opts.rootFiles);
656
+ return order.filter((name) => {
657
+ const def = LINTERS[name];
658
+ return def.always === true || def.detect.some((f) => present.has(f));
659
+ }).map((name) => ({
660
+ name,
661
+ def: LINTERS[name]
662
+ }));
663
+ }
664
+ //#endregion
665
+ //#region src/super-linter.ts
666
+ /**
667
+ * `superLinterConfig()` — turn the resolved linter set into the exact
668
+ * super-linter `VALIDATE_*` / `FIX_*` env the CI `lint` job needs. The CLI
669
+ * serializes {@link SuperLinterConfig.env} as the `super-linter-env` input
670
+ * on each repo's generated `lint` thin caller; the reusable workflow
671
+ * expands it verbatim. This is the CI half of "lint parity" — the local
672
+ * half is the `holocron run lint` aggregate, driven by the same
673
+ * {@link resolveLinters}.
674
+ */
675
+ /**
676
+ * Resolve the super-linter env for a repo's `lint` task.
677
+ *
678
+ * @param opts.explicit the task's `linters` list, if any (else auto-detect)
679
+ * @param opts.rootFiles repo-root filenames (from `listDir(cwd)`)
680
+ * @param opts.includeFix emit `FIX_*` keys too (default `true`)
681
+ */
682
+ function superLinterConfig(opts) {
683
+ const includeFix = opts.includeFix ?? true;
684
+ const resolved = resolveLinters({
685
+ explicit: opts.explicit,
686
+ rootFiles: opts.rootFiles
687
+ });
688
+ const env = {};
689
+ const configInputs = {};
690
+ for (const { def } of resolved) {
691
+ for (const key of def.validate) env[key] = "true";
692
+ if (includeFix) for (const key of def.fix ?? []) env[key] = "true";
693
+ if (def.configInput) configInputs[def.configInput] = true;
694
+ }
695
+ return {
696
+ env,
697
+ linters: resolved.map((r) => r.name),
698
+ configInputs
699
+ };
700
+ }
701
+ /**
702
+ * The always-on baseline env — every `always` linter, no detection. This is
703
+ * what the reusable `lint.yml`'s `super-linter-env` input defaults to, so a
704
+ * repo whose thin caller has not been re-synced yet behaves exactly as before.
705
+ */
706
+ function baselineSuperLinterEnv() {
707
+ return superLinterConfig({ rootFiles: [] }).env;
708
+ }
709
+ //#endregion
710
+ export { KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, TASKS, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, baselineSuperLinterEnv, createAstromech, deriveDeployPaths, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, normalizeWorkflowWith, resolveLinters, runTask, superLinterConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/astromech",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "The Holocron task runner — one task manifest drives `holocron run`, `holocron ci`, the CI workflows, package.json scripts, linters, and required checks.",
5
5
  "keywords": [
6
6
  "ci",
@@ -37,7 +37,7 @@
37
37
  "dist"
38
38
  ],
39
39
  "dependencies": {
40
- "@theholocron/datapad": "4.2.0"
40
+ "@theholocron/datapad": "4.3.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@theholocron/eslint-config": "^8.0.0",
@@ -53,7 +53,7 @@
53
53
  "tsdown": "^0.22.14",
54
54
  "typescript": "^5.9.3",
55
55
  "vitest": "^4.1.11",
56
- "@theholocron/rollup-plugin-transform-template": "4.2.0"
56
+ "@theholocron/rollup-plugin-transform-template": "4.3.0"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=22"