@theholocron/astromech 4.1.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 +61 -3
- package/dist/config/index.d.mts +1 -1
- package/dist/config/index.mjs +1 -10
- package/dist/index.d.mts +212 -79
- package/dist/index.mjs +257 -40
- package/dist/schema-Cf5dfaDO.mjs +11 -0
- package/dist/{schema-4kyr9ILV.d.mts → schema-DbpiBZCP.d.mts} +8 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -79,9 +79,67 @@ export default defineConfig({
|
|
|
79
79
|
| `with` | per-repo overrides on the reusable-workflow channel |
|
|
80
80
|
| `linters` (`lint` only) | explicit linter list; omitted → auto-detect |
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
(
|
|
82
|
+
Top-level keys: `syncScripts: false` disables the `package.json` script
|
|
83
|
+
writes entirely; `holocronScript` sets the command the synced `"holocron"`
|
|
84
|
+
script runs (default `"holocron"`).
|
|
85
|
+
|
|
86
|
+
### Generated surfaces
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const astromech = createAstromech({ cwd, config, orgContext: { org, domain } });
|
|
90
|
+
|
|
91
|
+
astromech.thinCallers(); // Map<"<name>.yml", yaml> — one per templated, ci-enabled task
|
|
92
|
+
astromech.packageScripts(); // { holocron: "holocron", lint: "holocron run lint", … }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`thinCallers()` returns the raw `.github/workflows/*.yml` content (no
|
|
96
|
+
generated-by header — the caller prefixes its own). `deploy` with
|
|
97
|
+
`preview:` shorthand produces the combined push-to-Pages / PR-to-preview
|
|
98
|
+
workflow. `packageScripts()` emits the `holocron` entry
|
|
99
|
+
(`holocronScript ?? "holocron"`) plus one `"<task>": "holocron run <task>"`
|
|
100
|
+
per runnable task; it skips `local: false` entries and tasks with no local
|
|
101
|
+
runner (`codeql`, `deploy`), and returns `{}` when `syncScripts: false` or
|
|
102
|
+
there is no config.
|
|
103
|
+
|
|
104
|
+
`holocron run` itself does not read the config yet — that (and
|
|
105
|
+
`holocron ci`) come in later phases (epic #581).
|
|
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".
|
|
85
143
|
|
|
86
144
|
## Development
|
|
87
145
|
|
package/dist/config/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as normalizeTaskEntry, n as TaskEntry, r as TasksConfig, t as TaskConfigItem } from "../schema-
|
|
1
|
+
import { i as normalizeTaskEntry, n as TaskEntry, r as TasksConfig, t as TaskConfigItem } from "../schema-DbpiBZCP.mjs";
|
|
2
2
|
//#region src/config/define.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Typed identity helper for `astromech.config.ts`:
|
package/dist/config/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as normalizeTaskEntry } from "../schema-Cf5dfaDO.mjs";
|
|
1
2
|
import { createDefineConfig, loadConfigFile, mergeConfig } from "@theholocron/datapad";
|
|
2
3
|
//#region src/config/define.ts
|
|
3
4
|
/**
|
|
@@ -35,14 +36,4 @@ function coerce(value) {
|
|
|
35
36
|
return Array.isArray(value) ? { tasks: value } : value;
|
|
36
37
|
}
|
|
37
38
|
//#endregion
|
|
38
|
-
//#region src/config/schema.ts
|
|
39
|
-
/** Normalise a `TaskConfigItem` to a full {@link TaskEntry} with defaults applied. */
|
|
40
|
-
function normalizeTaskEntry(item) {
|
|
41
|
-
return {
|
|
42
|
-
ci: true,
|
|
43
|
-
local: true,
|
|
44
|
-
...typeof item === "string" ? { name: item } : item
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
//#endregion
|
|
48
39
|
export { defineConfig, loadTasksConfig, normalizeTaskEntry };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as TasksConfig } from "./schema-
|
|
1
|
+
import { r as TasksConfig } from "./schema-DbpiBZCP.mjs";
|
|
2
2
|
//#region src/run.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* `holocron run <task> [-- <passthrough>]` — run a registry task locally.
|
|
@@ -56,83 +56,6 @@ interface RunTaskReport {
|
|
|
56
56
|
}
|
|
57
57
|
declare function runTask(input: RunTaskInput): RunTaskReport;
|
|
58
58
|
//#endregion
|
|
59
|
-
//#region src/astromech.d.ts
|
|
60
|
-
interface AstromechOptions {
|
|
61
|
-
/** Repo root. */
|
|
62
|
-
cwd: string;
|
|
63
|
-
/**
|
|
64
|
-
* The resolved task manifest. Optional for `run` (which is
|
|
65
|
-
* filesystem-driven); later methods (`ci`, workflow generation) need it.
|
|
66
|
-
* Load it with `loadTasksConfig` from `@theholocron/astromech/config`.
|
|
67
|
-
*/
|
|
68
|
-
config?: TasksConfig;
|
|
69
|
-
/** Structured-logging sink. Defaults to a no-op. */
|
|
70
|
-
logger?: RunLogger;
|
|
71
|
-
/** User-facing line printer. Defaults to `console.log`. */
|
|
72
|
-
print?: (line: string) => void;
|
|
73
|
-
/** Injectable subprocess runner (tests). Defaults to `spawnSync` (stdio inherit). */
|
|
74
|
-
exec?: ExecFn;
|
|
75
|
-
/** Injectable fs (tests). Default to `node:fs`. */
|
|
76
|
-
readFile?: (path: string) => string;
|
|
77
|
-
fileExists?: (path: string) => boolean;
|
|
78
|
-
listDir?: (path: string) => string[];
|
|
79
|
-
}
|
|
80
|
-
interface RunOptions {
|
|
81
|
-
/** Args after `--`, forwarded to the tool / turbo / script. */
|
|
82
|
-
passthrough?: string[];
|
|
83
|
-
/** Print the resolved command without running it. */
|
|
84
|
-
dryRun?: boolean;
|
|
85
|
-
/** Fail (exit 1) instead of skipping when the repo has no such task. */
|
|
86
|
-
required?: boolean;
|
|
87
|
-
}
|
|
88
|
-
interface Astromech {
|
|
89
|
-
/** Run one task locally. */
|
|
90
|
-
run(task: string, opts?: RunOptions): RunTaskReport;
|
|
91
|
-
}
|
|
92
|
-
declare function createAstromech(options: AstromechOptions): Astromech;
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region src/registry.d.ts
|
|
95
|
-
/**
|
|
96
|
-
* The task registry — how each task runs *locally*, without GitHub
|
|
97
|
-
* Actions. `holocron run <task>` and `holocron ci` resolve against this;
|
|
98
|
-
* adding a task here gives every repo that task.
|
|
99
|
-
*
|
|
100
|
-
* Keyed identically to the workflow templates — a task IS a workflow.
|
|
101
|
-
*
|
|
102
|
-
* Spec: `.notes/tech-astromech-task-runner.spec.md` (epic #581).
|
|
103
|
-
*/
|
|
104
|
-
/** How to run one task (or job) locally. */
|
|
105
|
-
interface LocalRunner {
|
|
106
|
-
/** Binary to invoke — resolved from `node_modules/.bin` then PATH. */
|
|
107
|
-
tool?: string;
|
|
108
|
-
/** Args appended after the tool. */
|
|
109
|
-
args?: string[];
|
|
110
|
-
/** First entry whose `when` filename matches a repo-root file wins. */
|
|
111
|
-
detect?: Array<{
|
|
112
|
-
when: RegExp;
|
|
113
|
-
tool: string;
|
|
114
|
-
args?: string[];
|
|
115
|
-
}>;
|
|
116
|
-
/** The task is already a holocron subcommand (`sync`, `sync-wiki`). */
|
|
117
|
-
command?: string;
|
|
118
|
-
}
|
|
119
|
-
interface TaskDef {
|
|
120
|
-
/**
|
|
121
|
-
* `null` — no local equivalent (CodeQL, deploys). `holocron ci` reports
|
|
122
|
-
* it as skipped; `holocron run` treats it as "nothing to do".
|
|
123
|
-
*/
|
|
124
|
-
local: LocalRunner | null;
|
|
125
|
-
/** Sub-jobs, keyed by slug — `holocron run audit performance`. */
|
|
126
|
-
jobs?: Record<string, {
|
|
127
|
-
local: LocalRunner | null;
|
|
128
|
-
}>;
|
|
129
|
-
/** Org-default flags injected by tool name. Removed by a repo override. */
|
|
130
|
-
flags?: Record<string, string[]>;
|
|
131
|
-
}
|
|
132
|
-
declare const TASKS: Record<string, TaskDef>;
|
|
133
|
-
/** Every task name the registry knows. */
|
|
134
|
-
declare const KNOWN_TASKS: Set<string>;
|
|
135
|
-
//#endregion
|
|
136
59
|
//#region src/thin-callers.d.ts
|
|
137
60
|
/**
|
|
138
61
|
* Workflow templates + thin-caller generation.
|
|
@@ -231,4 +154,214 @@ declare function normalizeWorkflowWith(raw: Record<string, unknown>): Record<str
|
|
|
231
154
|
*/
|
|
232
155
|
declare function deriveDeployPaths(raw: Record<string, unknown>): string[];
|
|
233
156
|
//#endregion
|
|
234
|
-
|
|
157
|
+
//#region src/astromech.d.ts
|
|
158
|
+
interface AstromechOptions {
|
|
159
|
+
/** Repo root. */
|
|
160
|
+
cwd: string;
|
|
161
|
+
/**
|
|
162
|
+
* The resolved task manifest. Optional for `run` (which is
|
|
163
|
+
* filesystem-driven); `thinCallers` / `packageScripts` / `ci` need it.
|
|
164
|
+
* Load it with `loadTasksConfig` from `@theholocron/astromech/config`.
|
|
165
|
+
*/
|
|
166
|
+
config?: TasksConfig;
|
|
167
|
+
/**
|
|
168
|
+
* Org context for the `deploy` workflow's `preview:` shorthand — used to
|
|
169
|
+
* derive the Cloudflare Pages project / domain when they are not spelt out.
|
|
170
|
+
*/
|
|
171
|
+
orgContext?: OrgContext;
|
|
172
|
+
/** Structured-logging sink. Defaults to a no-op. */
|
|
173
|
+
logger?: RunLogger;
|
|
174
|
+
/** User-facing line printer. Defaults to `console.log`. */
|
|
175
|
+
print?: (line: string) => void;
|
|
176
|
+
/** Injectable subprocess runner (tests). Defaults to `spawnSync` (stdio inherit). */
|
|
177
|
+
exec?: ExecFn;
|
|
178
|
+
/** Injectable fs (tests). Default to `node:fs`. */
|
|
179
|
+
readFile?: (path: string) => string;
|
|
180
|
+
fileExists?: (path: string) => boolean;
|
|
181
|
+
listDir?: (path: string) => string[];
|
|
182
|
+
}
|
|
183
|
+
interface RunOptions {
|
|
184
|
+
/** Args after `--`, forwarded to the tool / turbo / script. */
|
|
185
|
+
passthrough?: string[];
|
|
186
|
+
/** Print the resolved command without running it. */
|
|
187
|
+
dryRun?: boolean;
|
|
188
|
+
/** Fail (exit 1) instead of skipping when the repo has no such task. */
|
|
189
|
+
required?: boolean;
|
|
190
|
+
}
|
|
191
|
+
interface Astromech {
|
|
192
|
+
/** Run one task locally. */
|
|
193
|
+
run(task: string, opts?: RunOptions): RunTaskReport;
|
|
194
|
+
/**
|
|
195
|
+
* The `.github/workflows/*.yml` thin callers for this repo's manifest —
|
|
196
|
+
* `filename` → YAML content (no generated-by header; the caller adds it).
|
|
197
|
+
* One entry per `config.tasks` item that has a workflow template and is
|
|
198
|
+
* not `ci: false`.
|
|
199
|
+
*/
|
|
200
|
+
thinCallers(): Map<string, string>;
|
|
201
|
+
/**
|
|
202
|
+
* `package.json` scripts for this repo's manifest — the `"holocron"` entry
|
|
203
|
+
* (`config.holocronScript ?? "holocron"`) plus `"<task>": "holocron run
|
|
204
|
+
* <task>"` for every `config.tasks` item that is a runnable registry task
|
|
205
|
+
* and not `local: false`. Merge into `package.json`; never clobber. Empty
|
|
206
|
+
* when there is no config or `syncScripts: false`.
|
|
207
|
+
*/
|
|
208
|
+
packageScripts(): Record<string, string>;
|
|
209
|
+
}
|
|
210
|
+
declare function createAstromech(options: AstromechOptions): Astromech;
|
|
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
|
|
279
|
+
//#region src/registry.d.ts
|
|
280
|
+
/**
|
|
281
|
+
* The task registry — how each task runs *locally*, without GitHub
|
|
282
|
+
* Actions. `holocron run <task>` and `holocron ci` resolve against this;
|
|
283
|
+
* adding a task here gives every repo that task.
|
|
284
|
+
*
|
|
285
|
+
* Keyed identically to the workflow templates — a task IS a workflow.
|
|
286
|
+
*
|
|
287
|
+
* Spec: `.notes/tech-astromech-task-runner.spec.md` (epic #581).
|
|
288
|
+
*/
|
|
289
|
+
/** How to run one task (or job) locally. */
|
|
290
|
+
interface LocalRunner {
|
|
291
|
+
/** Binary to invoke — resolved from `node_modules/.bin` then PATH. */
|
|
292
|
+
tool?: string;
|
|
293
|
+
/** Args appended after the tool. */
|
|
294
|
+
args?: string[];
|
|
295
|
+
/** First entry whose `when` filename matches a repo-root file wins. */
|
|
296
|
+
detect?: Array<{
|
|
297
|
+
when: RegExp;
|
|
298
|
+
tool: string;
|
|
299
|
+
args?: string[];
|
|
300
|
+
}>;
|
|
301
|
+
/** The task is already a holocron subcommand (`sync`, `sync-wiki`). */
|
|
302
|
+
command?: string;
|
|
303
|
+
}
|
|
304
|
+
interface TaskDef {
|
|
305
|
+
/**
|
|
306
|
+
* `null` — no local equivalent (CodeQL, deploys). `holocron ci` reports
|
|
307
|
+
* it as skipped; `holocron run` treats it as "nothing to do".
|
|
308
|
+
*/
|
|
309
|
+
local: LocalRunner | null;
|
|
310
|
+
/** Sub-jobs, keyed by slug — `holocron run audit performance`. */
|
|
311
|
+
jobs?: Record<string, {
|
|
312
|
+
local: LocalRunner | null;
|
|
313
|
+
}>;
|
|
314
|
+
/** Org-default flags injected by tool name. Removed by a repo override. */
|
|
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;
|
|
322
|
+
}
|
|
323
|
+
declare const TASKS: Record<string, TaskDef>;
|
|
324
|
+
/** Every task name the registry knows. */
|
|
325
|
+
declare const KNOWN_TASKS: Set<string>;
|
|
326
|
+
//#endregion
|
|
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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as normalizeTaskEntry } from "./schema-Cf5dfaDO.mjs";
|
|
1
2
|
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -14,10 +15,13 @@ const TASKS = {
|
|
|
14
15
|
tool: "tsc",
|
|
15
16
|
args: ["--noEmit"]
|
|
16
17
|
} },
|
|
17
|
-
lint: {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
lint: {
|
|
19
|
+
local: {
|
|
20
|
+
tool: "eslint",
|
|
21
|
+
args: ["."]
|
|
22
|
+
},
|
|
23
|
+
linters: true
|
|
24
|
+
},
|
|
21
25
|
build: { local: { detect: [
|
|
22
26
|
{
|
|
23
27
|
when: /^tsdown\.config\.(ts|js|mjs|cjs)$/,
|
|
@@ -199,41 +203,6 @@ function packageJsonScript(cwd, task, readFile, fileExists) {
|
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
//#endregion
|
|
202
|
-
//#region src/astromech.ts
|
|
203
|
-
/**
|
|
204
|
-
* `createAstromech(options)` — the self-contained task runner.
|
|
205
|
-
* `@theholocron/cli` instantiates it once and delegates the `run` /
|
|
206
|
-
* `ci` / workflow-generation commands to it (like `@theholocron/logger`).
|
|
207
|
-
*/
|
|
208
|
-
const noopLogger = {
|
|
209
|
-
debug() {},
|
|
210
|
-
warn() {}
|
|
211
|
-
};
|
|
212
|
-
const realExec = (cmd, args, opts) => {
|
|
213
|
-
return { exitCode: spawnSync(cmd, args, {
|
|
214
|
-
cwd: opts.cwd,
|
|
215
|
-
stdio: "inherit"
|
|
216
|
-
}).status ?? -1 };
|
|
217
|
-
};
|
|
218
|
-
function createAstromech(options) {
|
|
219
|
-
const deps = {
|
|
220
|
-
print: options.print ?? ((line) => console.log(line)),
|
|
221
|
-
logger: options.logger ?? noopLogger,
|
|
222
|
-
exec: options.exec ?? realExec,
|
|
223
|
-
readFile: options.readFile ?? ((path) => readFileSync(path, "utf8")),
|
|
224
|
-
fileExists: options.fileExists ?? ((path) => existsSync(path)),
|
|
225
|
-
listDir: options.listDir ?? ((path) => readdirSync(path))
|
|
226
|
-
};
|
|
227
|
-
return { run: (task, opts = {}) => runTask({
|
|
228
|
-
...deps,
|
|
229
|
-
task,
|
|
230
|
-
cwd: options.cwd,
|
|
231
|
-
passthrough: opts.passthrough ?? [],
|
|
232
|
-
dryRun: opts.dryRun ?? false,
|
|
233
|
-
required: opts.required ?? false
|
|
234
|
-
}) };
|
|
235
|
-
}
|
|
236
|
-
//#endregion
|
|
237
206
|
//#region src/thin-callers.ts
|
|
238
207
|
/**
|
|
239
208
|
* Workflow templates + thin-caller generation.
|
|
@@ -490,4 +459,252 @@ function deriveDeployPaths(raw) {
|
|
|
490
459
|
return paths;
|
|
491
460
|
}
|
|
492
461
|
//#endregion
|
|
493
|
-
|
|
462
|
+
//#region src/astromech.ts
|
|
463
|
+
/**
|
|
464
|
+
* `createAstromech(options)` — the self-contained task runner.
|
|
465
|
+
* `@theholocron/cli` instantiates it once and delegates the `run` /
|
|
466
|
+
* `ci` / workflow-generation commands to it (like `@theholocron/logger`).
|
|
467
|
+
*/
|
|
468
|
+
const noopLogger = {
|
|
469
|
+
debug() {},
|
|
470
|
+
warn() {}
|
|
471
|
+
};
|
|
472
|
+
const realExec = (cmd, args, opts) => {
|
|
473
|
+
return { exitCode: spawnSync(cmd, args, {
|
|
474
|
+
cwd: opts.cwd,
|
|
475
|
+
stdio: "inherit"
|
|
476
|
+
}).status ?? -1 };
|
|
477
|
+
};
|
|
478
|
+
function createAstromech(options) {
|
|
479
|
+
const deps = {
|
|
480
|
+
print: options.print ?? ((line) => console.log(line)),
|
|
481
|
+
logger: options.logger ?? noopLogger,
|
|
482
|
+
exec: options.exec ?? realExec,
|
|
483
|
+
readFile: options.readFile ?? ((path) => readFileSync(path, "utf8")),
|
|
484
|
+
fileExists: options.fileExists ?? ((path) => existsSync(path)),
|
|
485
|
+
listDir: options.listDir ?? ((path) => readdirSync(path))
|
|
486
|
+
};
|
|
487
|
+
const items = () => (options.config?.tasks ?? []).map((i) => normalizeTaskEntry(i));
|
|
488
|
+
return {
|
|
489
|
+
run: (task, opts = {}) => runTask({
|
|
490
|
+
...deps,
|
|
491
|
+
task,
|
|
492
|
+
cwd: options.cwd,
|
|
493
|
+
passthrough: opts.passthrough ?? [],
|
|
494
|
+
dryRun: opts.dryRun ?? false,
|
|
495
|
+
required: opts.required ?? false
|
|
496
|
+
}),
|
|
497
|
+
thinCallers: () => {
|
|
498
|
+
const orgCtx = options.orgContext ?? {};
|
|
499
|
+
const out = /* @__PURE__ */ new Map();
|
|
500
|
+
for (const entry of items()) {
|
|
501
|
+
if (entry.ci === false || !KNOWN_WORKFLOWS.has(entry.name)) continue;
|
|
502
|
+
const rawWith = entry.with;
|
|
503
|
+
const normalized = rawWith ? normalizeWorkflowWith(rawWith) : void 0;
|
|
504
|
+
const withOverrides = entry.name === "lint" ? {
|
|
505
|
+
"enable-auto-commit": true,
|
|
506
|
+
...normalized ?? {}
|
|
507
|
+
} : normalized;
|
|
508
|
+
const additionalPaths = entry.paths ?? (entry.name === "deploy" && rawWith ? deriveDeployPaths(rawWith) : void 0);
|
|
509
|
+
if (entry.name === "deploy" && rawWith) {
|
|
510
|
+
const preview = extractPreviewConfig(rawWith, orgCtx);
|
|
511
|
+
if (preview) {
|
|
512
|
+
const deployWith = normalizeWorkflowWith(rawWith);
|
|
513
|
+
const deployPaths = entry.paths ?? deriveDeployPaths(rawWith);
|
|
514
|
+
out.set("deploy.yml", generateCombinedDeployContent(deployWith, deployPaths, preview));
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
out.set(`${entry.name}.yml`, generateThinCallerContent(entry.name, withOverrides, additionalPaths, deps.logger));
|
|
519
|
+
}
|
|
520
|
+
return out;
|
|
521
|
+
},
|
|
522
|
+
packageScripts: () => {
|
|
523
|
+
const out = {};
|
|
524
|
+
if (!options.config || options.config.syncScripts === false) return out;
|
|
525
|
+
out.holocron = options.config.holocronScript ?? "holocron";
|
|
526
|
+
for (const entry of items()) {
|
|
527
|
+
if (entry.local === false || !KNOWN_TASKS.has(entry.name) || TASKS[entry.name]?.local === null) continue;
|
|
528
|
+
out[entry.name] = `holocron run ${entry.name}`;
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
//#endregion
|
|
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 };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/config/schema.ts
|
|
2
|
+
/** Normalise a `TaskConfigItem` to a full {@link TaskEntry} with defaults applied. */
|
|
3
|
+
function normalizeTaskEntry(item) {
|
|
4
|
+
return {
|
|
5
|
+
ci: true,
|
|
6
|
+
local: true,
|
|
7
|
+
...typeof item === "string" ? { name: item } : item
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { normalizeTaskEntry as t };
|
|
@@ -34,6 +34,8 @@ interface TaskEntry {
|
|
|
34
34
|
* from the config files present.
|
|
35
35
|
*/
|
|
36
36
|
linters?: string[];
|
|
37
|
+
/** Extra `on.push.paths` entries for the generated CI workflow. */
|
|
38
|
+
paths?: string[];
|
|
37
39
|
}
|
|
38
40
|
/** A task is either its bare name (all defaults) or an entry object. */
|
|
39
41
|
type TaskConfigItem = string | TaskEntry;
|
|
@@ -42,6 +44,12 @@ interface TasksConfig {
|
|
|
42
44
|
tasks?: TaskConfigItem[];
|
|
43
45
|
/** Opt out of the `package.json` script writes. Default `true`. */
|
|
44
46
|
syncScripts?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The command the synced `"holocron"` `package.json` script runs. Default
|
|
49
|
+
* `"holocron"` (the installed bin). The source repo overrides it to run
|
|
50
|
+
* its own build, e.g. `"node packages/cli/dist/cli.mjs"`.
|
|
51
|
+
*/
|
|
52
|
+
holocronScript?: string;
|
|
45
53
|
/**
|
|
46
54
|
* Required status-check contexts not backed by a task — DCO, semantic
|
|
47
55
|
* PR title, …
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/astromech",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
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.
|
|
56
|
+
"@theholocron/rollup-plugin-transform-template": "4.3.0"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=22"
|