@theholocron/astromech 5.0.0-alpha.77 → 5.0.0-alpha.79

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
@@ -33,7 +33,7 @@ const report = astromech.run("verification.unitTests", { passthrough: ["--watch"
33
33
  `holocron run verification.unitTests` runs your tests — you don't tell it
34
34
  turbo vs pnpm vs npm, or which runner:
35
35
 
36
- ```
36
+ ```text
37
37
  1. turbo.json defines the task → turbo run <task>
38
38
  2. package.json has a <task> script → <detected pm> run <task>
39
39
  (a "holocron run …" thin caller is skipped — no recursion)
@@ -83,10 +83,12 @@ publish/deploy-shaped task can carry, toggled via that task's `with:`.
83
83
 
84
84
  **`linterGroup` tasks** (`sourceQuality.staticAnalysis`,
85
85
  `sourceQuality.formatting`) bundle more than one tool under a single
86
- required check. Each tool is still gated by its own detection rule (e.g.
87
- `eslint` only runs if `eslint.config.*` is present) — the
86
+ required check. Each tool is still gated by its own detection rule — e.g.
87
+ `eslint` runs when either a local `eslint.config.*` is present, or the
88
+ package has no local file but `@theholocron/eslint-config` resolves
89
+ (the same shared-config splice the `tool`/`detect` runner path uses) — the
88
90
  `tool name → detection rule → local binary` mapping lives in one place,
89
- `src/linters.ts`. A repo's choice of which of these run is just which
91
+ `src/linters.ts`. A repo's choice of which of these run is based on which
90
92
  tasks it includes in `tasks: [...]`, same as any other task.
91
93
 
92
94
  **No super-linter.** Each task above runs its own tool directly, through
package/dist/index.d.mts CHANGED
@@ -332,10 +332,14 @@ interface Astromech {
332
332
  * (not just an orchestrator) silently vanishes from `turbo run <task>`.
333
333
  * Pass `pnpm-workspace.yaml`'s current content (or `""` if none exists —
334
334
  * a no-op either way, since no `packages:` key means single-package mode
335
- * already) and root `package.json`'s own `scripts` keys. `changed: false`
336
- * means don't write anything — the file is already correct.
335
+ * already) and root `package.json`'s own `scripts` object — values and
336
+ * all, not just keys, so a task-name-matching script that's already the
337
+ * generated `holocron run <task> --` wrapper (predating this call, not
338
+ * written by it) is correctly excluded rather than treated as proof root
339
+ * is directly buildable (#846). `changed: false` means don't write
340
+ * anything — the file is already correct.
337
341
  */
338
- ensureRootWorkspaceMember(workspaceYaml: string, rootScripts: readonly string[]): EnsureRootWorkspaceMemberResult;
342
+ ensureRootWorkspaceMember(workspaceYaml: string, rootScripts: Readonly<Record<string, string>>): EnsureRootWorkspaceMemberResult;
339
343
  /**
340
344
  * `turboConfig()` writes a `turbo.json` but never touches `package.json`
341
345
  * — a repo with no local `turbo` devDependency falls back to whatever
@@ -448,11 +452,20 @@ declare const LINTER_NAMES: ReadonlySet<string>;
448
452
  * Resolve the linter set for a repo. An `explicit` list (from `config.tasks`)
449
453
  * picks the candidate set; otherwise the candidates are every `always` linter
450
454
  * plus every `detect`-gated linter whose filenames are present at the repo
451
- * root. Either way, a `detect`-gated linter (eslint, markdownlint) only makes
452
- * the final set when its config file is actually present — a repo can't run
453
- * eslint without an `eslint.config.*`, and super-linter FATALs if you ask it
454
- * to (theholocron/holocron#654). `always` linters are unconditional. Result
455
- * is ordered by {@link LINTERS} declaration order.
455
+ * root, OR whose mapped `@theholocron/*-config` package resolves per
456
+ * {@link sharedConfigAvailable} — a repo can't run eslint with genuinely no
457
+ * config anywhere (super-linter FATALs if you ask it to,
458
+ * theholocron/holocron#654), but a *shared* one it has installed counts
459
+ * (holocron#795: proving `clients`' eslint.config.ts files reduce to zero
460
+ * remaining content is only half the story — the file itself was still load-
461
+ * bearing here as the sole "eslint is even configured" signal until this
462
+ * fallback existed). `always` linters are unconditional. Result is ordered
463
+ * by {@link LINTERS} declaration order.
464
+ *
465
+ * `sharedConfigAvailable` is caller-computed (`run.ts`'s `runLinterGroup`
466
+ * already resolves each linter's `resolveToolConfig()` per member to build
467
+ * its actual command — reusing that instead of duplicating the package-
468
+ * resolution logic here keeps this function itself pure and file-I/O-free).
456
469
  *
457
470
  * @throws when an `explicit` name is not in the registry — a typo is a
458
471
  * config bug, not a linter to silently skip.
@@ -460,6 +473,7 @@ declare const LINTER_NAMES: ReadonlySet<string>;
460
473
  declare function resolveLinters(opts: {
461
474
  explicit?: string[];
462
475
  rootFiles: string[];
476
+ sharedConfigAvailable?: ReadonlySet<string>;
463
477
  }): Array<{
464
478
  name: string;
465
479
  def: LinterDef;
package/dist/index.mjs CHANGED
@@ -299,11 +299,20 @@ const LINTER_NAMES = new Set(Object.keys(LINTERS));
299
299
  * Resolve the linter set for a repo. An `explicit` list (from `config.tasks`)
300
300
  * picks the candidate set; otherwise the candidates are every `always` linter
301
301
  * plus every `detect`-gated linter whose filenames are present at the repo
302
- * root. Either way, a `detect`-gated linter (eslint, markdownlint) only makes
303
- * the final set when its config file is actually present — a repo can't run
304
- * eslint without an `eslint.config.*`, and super-linter FATALs if you ask it
305
- * to (theholocron/holocron#654). `always` linters are unconditional. Result
306
- * is ordered by {@link LINTERS} declaration order.
302
+ * root, OR whose mapped `@theholocron/*-config` package resolves per
303
+ * {@link sharedConfigAvailable} — a repo can't run eslint with genuinely no
304
+ * config anywhere (super-linter FATALs if you ask it to,
305
+ * theholocron/holocron#654), but a *shared* one it has installed counts
306
+ * (holocron#795: proving `clients`' eslint.config.ts files reduce to zero
307
+ * remaining content is only half the story — the file itself was still load-
308
+ * bearing here as the sole "eslint is even configured" signal until this
309
+ * fallback existed). `always` linters are unconditional. Result is ordered
310
+ * by {@link LINTERS} declaration order.
311
+ *
312
+ * `sharedConfigAvailable` is caller-computed (`run.ts`'s `runLinterGroup`
313
+ * already resolves each linter's `resolveToolConfig()` per member to build
314
+ * its actual command — reusing that instead of duplicating the package-
315
+ * resolution logic here keeps this function itself pure and file-I/O-free).
307
316
  *
308
317
  * @throws when an `explicit` name is not in the registry — a typo is a
309
318
  * config bug, not a linter to silently skip.
@@ -311,17 +320,18 @@ const LINTER_NAMES = new Set(Object.keys(LINTERS));
311
320
  function resolveLinters(opts) {
312
321
  const order = Object.keys(LINTERS);
313
322
  const present = new Set(opts.rootFiles);
314
- const configPresent = (def) => def.always === true || (def.detect ?? []).some((f) => present.has(f));
323
+ const shared = opts.sharedConfigAvailable ?? /* @__PURE__ */ new Set();
324
+ const configPresent = (name, def) => def.always === true || (def.detect ?? []).some((f) => present.has(f)) || shared.has(name);
315
325
  if (opts.explicit && opts.explicit.length > 0) {
316
326
  const unknown = opts.explicit.filter((n) => !LINTER_NAMES.has(n));
317
327
  if (unknown.length > 0) throw new Error(`unknown linter${unknown.length > 1 ? "s" : ""} ${unknown.map((n) => `"${n}"`).join(", ")} — known: ${order.join(", ")}`);
318
328
  const wanted = new Set(opts.explicit);
319
- return order.filter((n) => wanted.has(n) && configPresent(LINTERS[n])).map((name) => ({
329
+ return order.filter((n) => wanted.has(n) && configPresent(n, LINTERS[n])).map((name) => ({
320
330
  name,
321
331
  def: LINTERS[name]
322
332
  }));
323
333
  }
324
- return order.filter((name) => configPresent(LINTERS[name])).map((name) => ({
334
+ return order.filter((name) => configPresent(name, LINTERS[name])).map((name) => ({
325
335
  name,
326
336
  def: LINTERS[name]
327
337
  }));
@@ -731,9 +741,14 @@ function runLinterGroup(input, names, passthrough) {
731
741
  } catch {
732
742
  rootFiles = [];
733
743
  }
744
+ const sharedConfigAvailable = new Set(names.filter((name) => resolveToolConfig(name, cwd, {
745
+ readFile,
746
+ fileExists
747
+ }).length > 0));
734
748
  const resolved = resolveLinters({
735
749
  explicit: names,
736
- rootFiles
750
+ rootFiles,
751
+ sharedConfigAvailable
737
752
  });
738
753
  const reports = [];
739
754
  let anyHasLocalBin = false;
@@ -1544,6 +1559,17 @@ function turboConfig(config) {
1544
1559
  * (`Packages in scope: docs, root-lib`) without disturbing the sibling
1545
1560
  * package's own caching.
1546
1561
  *
1562
+ * A matching script key alone isn't enough — its *value* has to be a real
1563
+ * command, not the generated `holocron run <task> --` wrapper (#846, the
1564
+ * counterpart `mergePackageJsonScripts()` already had to learn for #747/
1565
+ * #748). A repo whose root predates this tooling can already have that exact
1566
+ * wrapper sitting in a task-name-matching script with nothing ever having
1567
+ * generated it this session; adding `.` there fires #747's recursion
1568
+ * immediately (`turbo run <task>` → root's own `<task>` script → the wrapper
1569
+ * → `holocron run <task>` sees `turbo.json` still defines it → `turbo run
1570
+ * <task>` again, forever). Only a matching script whose value is *not* that
1571
+ * wrapper counts as "root is genuinely a directly-buildable package".
1572
+ *
1547
1573
  * A targeted line-based edit, not a full YAML parse/reserialize — same
1548
1574
  * pattern `codecov.ts`'s `mergeCodecovComponents()` already uses for editing
1549
1575
  * an existing generated file. `pnpm-workspace.yaml` routinely carries a
@@ -1555,7 +1581,10 @@ function turboConfig(config) {
1555
1581
  * every repo checked actually uses.
1556
1582
  */
1557
1583
  function ensureRootWorkspaceMember(workspaceYaml, rootScripts, taskNames) {
1558
- if (!taskNames.some((t) => rootScripts.includes(t))) return {
1584
+ if (!taskNames.some((t) => {
1585
+ const script = rootScripts[t];
1586
+ return script !== void 0 && !isGeneratedRunWrapper(script, t);
1587
+ })) return {
1559
1588
  content: workspaceYaml,
1560
1589
  changed: false
1561
1590
  };
@@ -1589,6 +1618,21 @@ function ensureRootWorkspaceMember(workspaceYaml, rootScripts, taskNames) {
1589
1618
  };
1590
1619
  }
1591
1620
  /**
1621
+ * Matches the exact shape `packageScripts()` generates for `taskName` —
1622
+ * `<bin> run <taskName> --`, `<bin>` normally `holocron` but substitutable
1623
+ * (`holocronScript`, itself allowed to be multi-word, e.g. `"node
1624
+ * packages/cli/dist/cli.mjs"` — see its own test). Checking the *suffix*
1625
+ * rather than the whole string means an old wrapper survives a
1626
+ * `holocronScript` rename intact — it's still recognized as a wrapper (safe
1627
+ * to update to the new one), not mistaken for a real command. Mirrors
1628
+ * `sync.ts`'s private `isGeneratedRunWrapper()` — same predicate, needed on
1629
+ * both sides of #692/#747: whether to *write* the wrapper, and here, whether
1630
+ * an *existing* one should count as "root is directly buildable".
1631
+ */
1632
+ function isGeneratedRunWrapper(command, taskName) {
1633
+ return command.endsWith(` run ${taskName} --`);
1634
+ }
1635
+ /**
1592
1636
  * Fallback pin when a repo's own `pnpm-workspace.yaml` has no `turbo`
1593
1637
  * catalog entry yet — matches this repo's own catalog version (the
1594
1638
  * canonical source-of-truth repo for the whole org's tooling).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/astromech",
3
- "version": "5.0.0-alpha.77",
3
+ "version": "5.0.0-alpha.79",
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": "5.0.0-alpha.77"
40
+ "@theholocron/datapad": "5.0.0-alpha.79"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@theholocron/eslint-config": "^8.4.5",
@@ -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": "5.0.0-alpha.77"
56
+ "@theholocron/rollup-plugin-transform-template": "5.0.0-alpha.79"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=22"