@theholocron/astromech 5.0.0-alpha.4 → 5.0.0-alpha.40

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/dist/index.d.mts CHANGED
@@ -221,6 +221,16 @@ declare function normalizeWorkflowWith(raw: Record<string, unknown>): Record<str
221
221
  */
222
222
  declare function deriveDeployPaths(raw: Record<string, unknown>): string[];
223
223
  //#endregion
224
+ //#region src/turbo.d.ts
225
+ interface EnsureRootWorkspaceMemberResult {
226
+ content: string;
227
+ changed: boolean;
228
+ }
229
+ interface EnsureTurboDependencyResult {
230
+ content: string;
231
+ changed: boolean;
232
+ }
233
+ //#endregion
224
234
  //#region src/astromech.d.ts
225
235
  interface AstromechOptions {
226
236
  /** Repo root. */
@@ -315,6 +325,27 @@ interface Astromech {
315
325
  * config (nothing to write). See {@link TurboTaskConfig} in `registry.ts`.
316
326
  */
317
327
  turboConfig(): string | null;
328
+ /**
329
+ * Fixes #692: whenever {@link turboConfig} isn't `null`, this repo's root
330
+ * package needs to actually be a workspace member for the tasks it fans
331
+ * out — otherwise a root that's genuinely a directly-buildable package
332
+ * (not just an orchestrator) silently vanishes from `turbo run <task>`.
333
+ * Pass `pnpm-workspace.yaml`'s current content (or `""` if none exists —
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.
337
+ */
338
+ ensureRootWorkspaceMember(workspaceYaml: string, rootScripts: readonly string[]): EnsureRootWorkspaceMemberResult;
339
+ /**
340
+ * `turboConfig()` writes a `turbo.json` but never touches `package.json`
341
+ * — a repo with no local `turbo` devDependency falls back to whatever
342
+ * (if anything) is globally on `PATH`, per `run.ts`'s `resolveBin()`.
343
+ * Pass root `package.json`'s raw content and `pnpm-workspace.yaml`'s
344
+ * current content (checked for an existing `turbo` catalog entry, which
345
+ * takes precedence over the hardcoded fallback version). `changed: false`
346
+ * means don't write anything — `devDependencies.turbo` already exists.
347
+ */
348
+ ensureTurboDependency(packageJson: string, workspaceYaml: string): EnsureTurboDependencyResult;
318
349
  }
319
350
  declare function createAstromech(options: AstromechOptions): Astromech;
320
351
  //#endregion
@@ -546,6 +577,58 @@ declare const CI_ORDER: string[];
546
577
  /** Ordered (task contexts in {@link CI_ORDER}, then extras), de-duplicated. */
547
578
  declare function requiredChecks(config: TasksConfig): string[];
548
579
  //#endregion
580
+ //#region src/resolver.d.ts
581
+ /**
582
+ * Resolves the absolute path to a Bucket A tool's shared `@theholocron/*-config`
583
+ * entry point, installed in the consuming repo's own `node_modules` — and the
584
+ * CLI flag(s) to hand it via (`--config`, `--extends`, …). `run.ts` splices the
585
+ * result into the command it builds for a tool/detect runner or a linter-group
586
+ * entry, right after the tool's own args.
587
+ *
588
+ * Every mapped package's resolved entry point already carries a ready-to-use
589
+ * default export (a plain re-export for `prettier`/`commitlint`; an invoked,
590
+ * zero-arg preset for `eslint`/`vitest`/`tsdown` — verified end-to-end against
591
+ * each tool's real `--config`/`--extends` loader, not just the file's shape).
592
+ * A repo doesn't need this package installed at all — every lookup degrades to
593
+ * `[]` (no flag added, the tool falls back to its own auto-discovery of a
594
+ * local file, unchanged from today) rather than throwing.
595
+ *
596
+ * **Local file wins.** A `<tool>.config.*` sitting in `cwd` (whether it's
597
+ * pure boilerplate or has real per-package customization on top of the
598
+ * shared bundle, e.g. `mergeConfig(base, { test: { coverage: { exclude }
599
+ * } })`) always skips the splice, even when the shared package is also
600
+ * installed — an explicit `--config <shared path>` flag beats a tool's own
601
+ * auto-discovery every time, so splicing it unconditionally would silently
602
+ * discard that customization the file is still sitting right there on disk.
603
+ * The shared bundle is only ever the fallback for a package with *no* local
604
+ * file at all — that's still the "delete the file to go fully shared" path
605
+ * this was designed for (#676); it just isn't unconditional (#749).
606
+ *
607
+ * `semantic-release`, `devmoji`, `editorconfig-checker`, and `knip` are
608
+ * deliberately absent — `semantic-release-config`'s `defineConfig()` needs
609
+ * real per-repo data (branches, npm options) a static `--extends <path>` can't
610
+ * carry; `devmoji` runs through a git hook template, not `holocron run
611
+ * <task>`; `editorconfig-checker` has no shared-config package yet; `knip` is
612
+ * repo-specific by nature, never a shared-config candidate. See
613
+ * `.notes/tech-config-resolution.spec.md` (#676).
614
+ */
615
+ interface ResolverDeps {
616
+ readFile: (path: string) => string;
617
+ fileExists: (path: string) => boolean;
618
+ }
619
+ /** Every tool name the resolver knows how to point at a shared config. */
620
+ declare const RESOLVABLE_TOOLS: ReadonlySet<string>;
621
+ /**
622
+ * `<flag> <absolute path>` for `tool`, resolved against `cwd`'s
623
+ * `node_modules` — or `[]` when the tool isn't mapped, `cwd` already has its
624
+ * own local config file for it (#749 — local always wins, customized or
625
+ * not), the shared package isn't installed, its `package.json` doesn't
626
+ * declare the export subpath this needs, or the resolved file doesn't
627
+ * actually exist on disk (a stale install, or a package version that
628
+ * predates the export existing).
629
+ */
630
+ declare function resolveToolConfig(tool: string, cwd: string, deps: ResolverDeps): string[];
631
+ //#endregion
549
632
  //#region src/reusable.d.ts
550
633
  /**
551
634
  * The **reusable** GitHub Actions surface pushed to `theholocron/.github` by
@@ -573,4 +656,73 @@ declare const WORKFLOW_TEMPLATE_PROPERTIES: Record<string, string>;
573
656
  */
574
657
  declare function reusableTemplates(): Map<string, string>;
575
658
  //#endregion
576
- export { type Astromech, type AstromechOptions, CI_ORDER, type CiJobReport, type CiOptions, type CiReport, type ExecFn, type JobDef, KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, type LinterDef, type LocalRunner, type OrgContext, type PreviewConfig, REUSABLE_ACTIONS, REUSABLE_WORKFLOWS, type RunLogger, type RunOptions, type RunTaskInput, type RunTaskReport, TASKS, type TaskDef, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, WORKFLOW_TEMPLATE_PROPERTIES, type WorkspacePackage, codecovComponentBlock, codecovConfig, createAstromech, createCodecovConfig, deriveDeployPaths, ensureIfNotFound, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, mergeCodecovComponents, normalizeWorkflowWith, readWorkspacePackages, requiredChecks, resolveLinters, reusableTemplates, runCi, runTask };
659
+ //#region src/tsconfig.d.ts
660
+ /**
661
+ * `tsconfig.json` generation — Bucket B (config-resolution workstream, #676):
662
+ * the file must stay committed (the TypeScript language server discovers it
663
+ * directly from disk, no `--config` override mechanism to hand it a path
664
+ * instead), but its content is fully derivable, so it's generated rather
665
+ * than hand-authored — the same category as `.editorconfig`
666
+ * ({@link "./templates/configs/editorconfig/create-config.js" createConfig})
667
+ * and {@link codecovConfig}.
668
+ *
669
+ * Surveyed every package-level `tsconfig.json` across the org (`holocron`,
670
+ * `clients`, `utils` — 20+ packages). `extends: "@theholocron/tsconfig/
671
+ * node-lts"`, `compilerOptions.baseUrl: "./"`, `compilerOptions.outDir:
672
+ * "./dist"`, `include: ["src/**\/*.ts"]`, and `exclude: ["node_modules",
673
+ * "dist"]` are uniform in every one — safe defaults here. `paths` (a `@/*`
674
+ * → `./src/*` import alias) is a real per-package choice, present in
675
+ * roughly 40% of packages checked — an opt-in parameter, not a default.
676
+ *
677
+ * Two fields checked and deliberately *not* absorbed as defaults, unlike
678
+ * `eslint-config`'s `tsconfigRootDir`/`settings.node` (which turned out
679
+ * redundant with the shared package's own behavior, `configs`#461):
680
+ * `compilerOptions.module`/`moduleResolution` (only `@theholocron/cli`
681
+ * overrides these, to `esnext`/`bundler` — a genuine deviation from the
682
+ * shared `node-lts` preset's `nodenext`/`nodenext`, needed for its bundler
683
+ * tooling, not something every package should inherit) and
684
+ * `compilerOptions.rootDir` (present in exactly two packages, always the
685
+ * same value `./src` that `include` already implies — an unnecessary
686
+ * override wherever it appears, not a pattern worth generalizing).
687
+ *
688
+ * Monorepo *root* `tsconfig.json` (a TS project-references "solution
689
+ * file" — `holocron`'s own root is `{ files: [], references: [...] }`,
690
+ * listing which packages to build) is a structurally different, genuinely
691
+ * per-repo document — out of scope here, same as Bucket C content.
692
+ *
693
+ * Not yet wired into `holocron setup`'s per-package write loop (every
694
+ * other Bucket B file there is a single repo-root file, safely
695
+ * overwritten every run — `tsconfig.json` is per-package, and most
696
+ * packages' files still carry real hand-authored content today, not yet
697
+ * migrated to a fully-generated state). That per-package iteration +
698
+ * safe-migration design is `#680`'s job, not duplicated here — this is
699
+ * the generator itself, ready for it to call.
700
+ */
701
+ interface TsconfigOptions {
702
+ /** Human-readable name for the `display` field — the package's own name is the usual choice. */
703
+ display: string;
704
+ /**
705
+ * `@theholocron/tsconfig` variant. Defaults to `"node-lts"` — every
706
+ * checked package uses it; the other three (`astro`, `nextjs`, `react`)
707
+ * exist for a template repo's app-shaped packages, not the plain
708
+ * Node.js library packages this org's currently-migrated repos ship.
709
+ */
710
+ variant?: "astro" | "nextjs" | "node-lts" | "react";
711
+ /**
712
+ * Add a `@/*` → `./src/*` path alias. A real per-package choice, not a
713
+ * default — roughly 40% of packages checked use one, the rest don't.
714
+ */
715
+ paths?: boolean;
716
+ }
717
+ /**
718
+ * A package-level `tsconfig.json` — the uniform shape confirmed across
719
+ * every package checked, parameterized only by the two fields that
720
+ * genuinely vary (`display`, `paths`). No scaffold/workflow header (matches
721
+ * `.alexrc.json`'s existing precedent for a strict-JSON Bucket B file —
722
+ * JSON has no comment syntax to carry one, and TypeScript's own tolerance
723
+ * for JSONC comments in `tsconfig.json` isn't worth relying on for a file
724
+ * this thin).
725
+ */
726
+ declare function createTsconfig(options: TsconfigOptions): string;
727
+ //#endregion
728
+ export { type Astromech, type AstromechOptions, CI_ORDER, type CiJobReport, type CiOptions, type CiReport, type ExecFn, type JobDef, KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, type LinterDef, type LocalRunner, type OrgContext, type PreviewConfig, RESOLVABLE_TOOLS, REUSABLE_ACTIONS, REUSABLE_WORKFLOWS, type ResolverDeps, type RunLogger, type RunOptions, type RunTaskInput, type RunTaskReport, TASKS, type TaskDef, type TsconfigOptions, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, WORKFLOW_TEMPLATE_PROPERTIES, type WorkspacePackage, codecovComponentBlock, codecovConfig, createAstromech, createCodecovConfig, createTsconfig, deriveDeployPaths, ensureIfNotFound, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, mergeCodecovComponents, normalizeWorkflowWith, readWorkspacePackages, requiredChecks, resolveLinters, resolveToolConfig, reusableTemplates, runCi, runTask };
package/dist/index.mjs CHANGED
@@ -327,6 +327,141 @@ function resolveLinters(opts) {
327
327
  }));
328
328
  }
329
329
  //#endregion
330
+ //#region src/resolver.ts
331
+ /**
332
+ * `library` is every mapped tool's org-wide default variant today — every
333
+ * currently-migrated repo (`holocron`, `clients`, `utils`, `configs`,
334
+ * `themes`, `observability`, `skills`) is a publishable library package, and
335
+ * `eslint-config`/`vitest-config`/`tsdown-config` all name their
336
+ * general-purpose preset exactly that. Picking a different variant per repo
337
+ * (an app-type template, say) needs new `holocron.config.ts` surface this
338
+ * doesn't add yet — deliberately out of scope until a real consumer needs it.
339
+ */
340
+ const TOOL_CONFIGS = {
341
+ eslint: {
342
+ package: "@theholocron/eslint-config",
343
+ exportPath: "./bundles/library",
344
+ flag: ["--config"]
345
+ },
346
+ prettier: {
347
+ package: "@theholocron/prettier-config",
348
+ exportPath: ".",
349
+ flag: ["--config"]
350
+ },
351
+ vitest: {
352
+ package: "@theholocron/vitest-config",
353
+ exportPath: "./bundles/library",
354
+ flag: ["--config"]
355
+ },
356
+ tsdown: {
357
+ package: "@theholocron/tsdown-config",
358
+ exportPath: "./presets/library",
359
+ flag: ["--config"]
360
+ },
361
+ commitlint: {
362
+ package: "@theholocron/commitlint-config",
363
+ exportPath: ".",
364
+ flag: ["--config"]
365
+ }
366
+ };
367
+ /**
368
+ * Every filename a tool's own auto-discovery would pick up from `cwd` —
369
+ * same extension set `registry.ts`'s `turbo.inputs`/`local.detect` entries
370
+ * already use for these tools, kept in sync by hand (no shared source of
371
+ * truth between the two today). Presence of any one of these means the
372
+ * package has its own local config — customized or not, `resolveToolConfig`
373
+ * can't tell the difference from the filename alone, so it defers to it
374
+ * either way (#749).
375
+ */
376
+ const LOCAL_CONFIG_FILES = {
377
+ eslint: [
378
+ "eslint.config.ts",
379
+ "eslint.config.js",
380
+ "eslint.config.mjs",
381
+ "eslint.config.cjs"
382
+ ],
383
+ prettier: [
384
+ "prettier.config.ts",
385
+ "prettier.config.js",
386
+ "prettier.config.mjs",
387
+ "prettier.config.cjs"
388
+ ],
389
+ vitest: [
390
+ "vitest.config.ts",
391
+ "vitest.config.js",
392
+ "vitest.config.mjs"
393
+ ],
394
+ tsdown: [
395
+ "tsdown.config.ts",
396
+ "tsdown.config.js",
397
+ "tsdown.config.mjs",
398
+ "tsdown.config.cjs"
399
+ ],
400
+ commitlint: [
401
+ "commitlint.config.ts",
402
+ "commitlint.config.js",
403
+ "commitlint.config.mjs",
404
+ "commitlint.config.cjs"
405
+ ]
406
+ };
407
+ /** Every tool name the resolver knows how to point at a shared config. */
408
+ const RESOLVABLE_TOOLS = new Set(Object.keys(TOOL_CONFIGS));
409
+ /**
410
+ * `<flag> <absolute path>` for `tool`, resolved against `cwd`'s
411
+ * `node_modules` — or `[]` when the tool isn't mapped, `cwd` already has its
412
+ * own local config file for it (#749 — local always wins, customized or
413
+ * not), the shared package isn't installed, its `package.json` doesn't
414
+ * declare the export subpath this needs, or the resolved file doesn't
415
+ * actually exist on disk (a stale install, or a package version that
416
+ * predates the export existing).
417
+ */
418
+ function resolveToolConfig(tool, cwd, deps) {
419
+ const spec = TOOL_CONFIGS[tool];
420
+ if (!spec) return [];
421
+ if ((LOCAL_CONFIG_FILES[tool] ?? []).some((name) => deps.fileExists(joinPath(cwd, name)))) return [];
422
+ const pkgDir = joinPath(cwd, "node_modules", ...spec.package.split("/"));
423
+ const pkgJsonPath = joinPath(pkgDir, "package.json");
424
+ if (!deps.fileExists(pkgJsonPath)) return [];
425
+ let pkg;
426
+ try {
427
+ pkg = JSON.parse(deps.readFile(pkgJsonPath));
428
+ } catch {
429
+ return [];
430
+ }
431
+ const relative = resolveExportEntry(pkg.exports?.[spec.exportPath]);
432
+ if (!relative) return [];
433
+ const absolute = joinPath(pkgDir, relative.replace(/^\.\//, ""));
434
+ if (!deps.fileExists(absolute)) return [];
435
+ return [...spec.flag, absolute];
436
+ }
437
+ /**
438
+ * Unwrap a `package.json#exports` subpath entry down to its file path —
439
+ * either a bare string, or an object with condition keys (`import`,
440
+ * `default`, `require`, …) that may themselves nest one level (`{ import: {
441
+ * types, default } }`, the shape every `@theholocron/*-config` package
442
+ * actually uses). Prefers `import` (this org ships ESM), then `default`,
443
+ * then `require` as a last resort.
444
+ */
445
+ function resolveExportEntry(entry) {
446
+ if (typeof entry === "string") return entry;
447
+ if (entry === null || typeof entry !== "object") return void 0;
448
+ const conditions = entry;
449
+ const value = conditions.import ?? conditions.default ?? conditions.require;
450
+ if (typeof value === "string") return value;
451
+ if (typeof value === "object" && value !== null) return resolveExportEntry(value);
452
+ }
453
+ /**
454
+ * A minimal, POSIX-safe `path.join` — avoids importing `node:path` just for
455
+ * this. Every path in play here is either already POSIX (a `package.json`
456
+ * `exports` value, always forward-slashed regardless of platform) or built
457
+ * from platform-neutral segments (`cwd`, `"node_modules"`, a scoped package
458
+ * name split on `/`), so a manual join is safe and keeps this module
459
+ * dependency-free like the rest of `run.ts`'s helpers.
460
+ */
461
+ function joinPath(...segments) {
462
+ return segments.map((s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
463
+ }
464
+ //#endregion
330
465
  //#region src/run.ts
331
466
  /**
332
467
  * `holocron run <task> [job] [-- <passthrough>]` — run a registry task (or one
@@ -448,8 +583,13 @@ function runTask(input) {
448
583
  const runner = resolveRunner(def.local, cwd, listDir);
449
584
  if (runner) {
450
585
  const flags = (def.flags?.[runner.tool] ?? []).filter((f) => !passthrough.includes(f));
586
+ const configFlags = resolveToolConfig(runner.tool, cwd, {
587
+ readFile,
588
+ fileExists
589
+ });
451
590
  return run(resolveBin(cwd, runner.tool, fileExists), [
452
591
  ...runner.args,
592
+ ...configFlags,
453
593
  ...flags,
454
594
  ...passthrough
455
595
  ]);
@@ -582,7 +722,7 @@ function runAllJobs(input, jobs, passthrough) {
582
722
  * turbo/a package script would have already caught in steps 1–2.
583
723
  */
584
724
  function runLinterGroup(input, names, passthrough) {
585
- const { print, logger, listDir, lookPath, cwd, task } = input;
725
+ const { print, logger, listDir, lookPath, cwd, task, readFile, fileExists } = input;
586
726
  const dryRun = input.dryRun ?? false;
587
727
  const runOne = makeRunOne(input);
588
728
  let rootFiles;
@@ -609,7 +749,15 @@ function runLinterGroup(input, names, passthrough) {
609
749
  print(`! ${name} — ${bin} not on PATH${def.installHint ? `. ${def.installHint}` : ""} (enforced in CI)`);
610
750
  continue;
611
751
  }
612
- reports.push(runOne(found, [...def.localArgs ?? [], ...passthrough]));
752
+ const configFlags = resolveToolConfig(name, cwd, {
753
+ readFile,
754
+ fileExists
755
+ });
756
+ reports.push(runOne(found, [
757
+ ...configFlags,
758
+ ...def.localArgs ?? [],
759
+ ...passthrough
760
+ ]));
613
761
  }
614
762
  if (reports.length === 0) {
615
763
  const required = input.required && anyHasLocalBin;
@@ -718,7 +866,7 @@ const WORKFLOW_TEMPLATES = {
718
866
  "platform.repoValidation": "name: Platform\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n repo-validation:\n name: Repo Validation\n uses: theholocron/.github/.github/workflows/platform.repoValidation.yml@main\n secrets: inherit\n",
719
867
  "knowledge.wiki": "name: Knowledge\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\nconcurrency:\n group: ${{ github.event_name == 'pull_request' && format('wiki-preview-{0}', github.event.pull_request.number) || 'wiki' }}\n cancel-in-progress: ${{ github.event_name == 'pull_request' }}\n\npermissions:\n contents: read\n deployments: write\n\njobs:\n publish:\n name: Publish\n if: ${{ github.event_name != 'pull_request' }}\n uses: theholocron/.github/.github/workflows/knowledge.wiki.yml@main\n secrets: inherit\n\n preview:\n name: Preview\n if: ${{ github.event_name == 'pull_request' }}\n uses: theholocron/.github/.github/workflows/knowledge.wiki.yml@main\n with:\n preview: true\n preview-id: pr-${{ github.event.pull_request.number }}\n secrets: inherit\n",
720
868
  preview: "name: Preview\n\non: # yamllint disable-line rule:truthy\n pull_request:\n branches: [main]\n\nconcurrency:\n group: preview-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n deployments: write\n pull-requests: write\n\njobs:\n preview:\n name: Preview\n uses: theholocron/.github/.github/workflows/preview.yml@main\n secrets: inherit\n",
721
- review: "name: Review\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\nconcurrency:\n group: review-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n checks: write\n pull-requests: write\n\njobs:\n review:\n name: Lint Annotations\n uses: theholocron/.github/.github/workflows/review.yml@main\n secrets: inherit\n",
869
+ review: "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\nconcurrency:\n group: review-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n checks: write\n pull-requests: write\n\njobs:\n review:\n name: Annotations\n uses: theholocron/.github/.github/workflows/review.yml@main\n secrets: inherit\n",
722
870
  stale: "name: Stale\n\non: # yamllint disable-line rule:truthy\n schedule:\n - cron: \"30 1 * * *\"\n\npermissions:\n contents: write\n issues: write\n pull-requests: write\n\njobs:\n stale:\n uses: theholocron/.github/.github/workflows/stale.yml@main\n with:\n exempt-issue-labels: \"in-progress,wip\"\n exempt-all-issue-milestones: true\n exempt-all-issue-projects: true\n exempt-all-pr-projects: true\n secrets: inherit\n",
723
871
  greetings: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n issues:\n\npermissions:\n issues: write\n pull-requests: write\n\njobs:\n greetings:\n name: Greetings\n uses: theholocron/.github/.github/workflows/greetings.yml@main\n secrets: inherit\n",
724
872
  dependencies: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n dependencies:\n name: Dependencies\n uses: theholocron/.github/.github/workflows/dependencies.yml@main\n secrets: inherit\n",
@@ -1197,7 +1345,7 @@ var holocron_default = "name: Holocron\ndescription: >\n Run the Holocron CLI
1197
1345
  var install_default = "name: Install dependencies\ndescription: Install project dependencies with pnpm frozen lockfile.\n\nruns:\n using: composite\n\n steps:\n - name: Install dependencies\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n shell: bash\n run: pnpm install --frozen-lockfile\n";
1198
1346
  //#endregion
1199
1347
  //#region src/templates/reusable/actions/setup.yml
1200
- var setup_default = "name: Setup\ndescription: Prepare the environment and install project dependencies.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\n\nruns:\n using: composite\n\n steps:\n - uses: theholocron/.github/.github/actions/setup-node@main\n with:\n node-version: ${{ inputs.node-version }}\n\n - uses: theholocron/.github/.github/actions/install@main\n\n # `@theholocron/cli`'s `dist/cli.mjs` doesn't exist yet at this point, so\n # pnpm's workspace bin-linking can't create `node_modules/.bin/holocron`\n # (silent `WARN ... ENOENT`, not a hard failure). Every package whose\n # scripts delegate via `holocron run <task> --` then fails at run time\n # with `holocron: not found` (#698). Build the CLI, then re-run install\n # so pnpm links the bin now that the target file exists — the second\n # install is cheap, the store is already warm from the first.\n - name: Build the Holocron CLI\n shell: bash\n run: pnpm --filter @theholocron/cli build\n\n - name: Re-link workspace bins\n uses: theholocron/.github/.github/actions/install@main\n\n - name: Cache Turbo artifacts\n if: ${{ hashFiles('turbo.json') != '' }}\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: .turbo\n key: turbo-${{ runner.os }}-${{ github.sha }}\n restore-keys: |\n turbo-${{ runner.os }}-\n";
1348
+ var setup_default = "name: Setup\ndescription: Prepare the environment and install project dependencies.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\n\nruns:\n using: composite\n\n steps:\n - uses: theholocron/.github/.github/actions/setup-node@main\n with:\n node-version: ${{ inputs.node-version }}\n\n - uses: theholocron/.github/.github/actions/install@main\n\n # `@theholocron/cli`'s `dist/cli.mjs` doesn't exist yet at this point, so\n # pnpm's workspace bin-linking can't create `node_modules/.bin/holocron`\n # (silent `WARN ... ENOENT`, not a hard failure). Every package whose\n # scripts delegate via `holocron run <task> --` then fails at run time\n # with `holocron: not found` (#698). Same fix the `holocron` composite\n # action already applies for its own direct invocation (see its \"Build\n # the workspace when the CLI resolves to an unbuilt checkout\" step) —\n # mirrored here so it also covers *nested* `holocron run <task> --`\n # calls from inside package.json scripts, which that action's own guard\n # doesn't reach. `pnpm build` (not a scoped `--filter`) so the CLI's own\n # workspace dependencies (e.g. rollup-plugin-transform-template) build\n # in topological order too — a scoped build skips them and fails.\n # No-op in every consumer repo, where the published tarball already\n # ships `dist/`.\n - name: Build the Holocron CLI\n shell: bash\n run: |\n if [ ! -f node_modules/@theholocron/cli/dist/cli.mjs ]; then\n echo \"→ @theholocron/cli dist/ missing — building the workspace from source\" >&2\n pnpm build\n echo \"→ re-linking node_modules/.bin — pnpm skipped it pre-build\" >&2\n pnpm install --frozen-lockfile\n fi\n\n - name: Cache Turbo artifacts\n if: ${{ hashFiles('turbo.json') != '' }}\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: .turbo\n key: turbo-${{ runner.os }}-${{ github.sha }}\n restore-keys: |\n turbo-${{ runner.os }}-\n";
1201
1349
  //#endregion
1202
1350
  //#region src/templates/reusable/actions/setup-node.yml
1203
1351
  var setup_node_default = "name: Setup Node\ndescription: Install pnpm and Node.js with pnpm dependency caching.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\nruns:\n using: composite\n\n steps:\n - name: Setup pnpm\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4\n\n - name: Setup Node.js\n uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0\n with:\n node-version: ${{ hashFiles('.node-version') != '' && '' || inputs.node-version }}\n node-version-file: ${{ hashFiles('.node-version') != '' && '.node-version' || '' }}\n cache: ${{ hashFiles('pnpm-lock.yaml') != '' && 'pnpm' || '' }}\n # Do NOT add registry-url here. setup-node writes .npmrc with\n # _authToken=${NODE_AUTH_TOKEN} and sets NPM_CONFIG_USERCONFIG to it.\n # pnpm reads that file, fails env-var substitution when the token is\n # empty, and loses auth entirely. Without registry-url, pnpm 10.15+\n # handles Trusted Publishing via its own native OIDC exchange.\n\n - name: Add node_modules/.bin to PATH\n shell: bash\n run: echo \"$GITHUB_WORKSPACE/node_modules/.bin\" >> $GITHUB_PATH\n";
@@ -1232,7 +1380,7 @@ const REUSABLE_WORKFLOWS = {
1232
1380
  "delivery.deploy": "name: Deploy\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n\njobs:\n deploy:\n name: Deploy\n runs-on: ubuntu-latest\n permissions:\n contents: read\n pages: write\n id-token: write\n environment:\n name: github-pages\n url: ${{ steps.deployment.outputs.page_url }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n cp -r docs/dist/. _site/\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1\n name: Upload pages artifact\n with:\n path: _site\n\n - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5\n id: deployment\n name: Deploy to GitHub Pages\n",
1233
1381
  "delivery.bundleSize": "name: Audit the Bundle Size\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n CODECOV_TOKEN:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n bundle-size:\n name: Upload bundle stats to Codecov\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Read by the bundle-stats uploader inside `holocron run delivery.build`.\n CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Build and upload bundle stats\n with:\n task: delivery.build\n",
1234
1382
  "platform.repoSync": "name: Sync\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n steps:\n description: >\n Sync steps to run (default: all). Valid values:\n labels, properties, teams, topics, keywords, description, homepage, readme, workflows, wiki.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme wiki\".\n type: string\n required: false\n secrets:\n HOLOCRON_ADMIN_TOKEN:\n description: Fine-grained PAT with admin scopes (labels, properties, teams).\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n HOLOCRON_DEPLOY_TOKEN:\n description: Fine-grained PAT for GitHub Pages configuration.\n required: false\n HOLOCRON_ISSUES_TOKEN:\n description: Fine-grained PAT for issue management.\n required: false\n HOLOCRON_ORG_TOKEN:\n description: Org-scoped fine-grained PAT for team sync and org properties.\n required: false\n HOLOCRON_READ_TOKEN:\n description: Fine-grained PAT for read-only GitHub API calls.\n required: false\n HOLOCRON_SYNC_TOKEN:\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for gh CLI calls. Used when\n HOLOCRON_SYNC_TOKEN is not set.\n required: false\n\njobs:\n sync:\n name: Run holocron sync\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: write\n pull-requests: write\n # Job-level so the `holocron` composite action's steps inherit them.\n env:\n HOLOCRON_ADMIN_TOKEN: ${{ secrets.HOLOCRON_ADMIN_TOKEN }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n POSTHOG_PROJECT_TOKEN: ${{ vars.POSTHOG_PROJECT_TOKEN }}\n HOLOCRON_DEPLOY_TOKEN: ${{ secrets.HOLOCRON_DEPLOY_TOKEN }}\n HOLOCRON_ISSUES_TOKEN: ${{ secrets.HOLOCRON_ISSUES_TOKEN }}\n HOLOCRON_ORG_TOKEN: ${{ secrets.HOLOCRON_ORG_TOKEN }}\n HOLOCRON_READ_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Run holocron sync\n # The `holocron` action resolves @theholocron/cli from node_modules (or\n # builds the workspace when it's an unbuilt checkout — the holocron repo\n # itself). `inputs.steps` is a space-separated list or empty; the\n # `--steps <list>` arg is omitted when empty.\n uses: theholocron/.github/.github/actions/holocron@main\n with:\n command: sync\n args: ${{ inputs.steps != '' && format('--steps {0}', inputs.steps) || '' }}\n\n - name: Format generated files\n run: |\n pnpm exec prettier --write README.md docs/src/content/docs/index.mdx 2>/dev/null || true\n # `holocron sync` writes package.json fields (keywords, description,\n # homepage) with a plain assignment, which appends new keys at the\n # end — re-apply the canonical order the lint config enforces.\n pnpm exec eslint --fix --no-warn-ignored package.json 2>/dev/null || true\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n id: auto-commit\n name: Commit sync changes\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n branch: chore/auto-sync\n commit-message: \"chore: sync from holocron.config\"\n commit-options: \"--no-verify\"\n\n - name: Open PR if changes were committed\n if: steps.auto-commit.outputs.changes-detected == 'true'\n run: |\n gh pr create \\\n --title \"chore: sync README and repo metadata\" \\\n --body \"Automated sync triggered by changes to config or package files. Merge to apply.\" \\\n --base main \\\n --head chore/auto-sync \\\n || echo \"PR already open — branch updated.\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Broadcast wiki sync if navbar changed\n if: >-\n steps.auto-commit.outputs.changes-detected == 'true' &&\n github.event_name == 'push' &&\n (inputs.steps == '' || contains(inputs.steps, 'wiki'))\n run: |\n if git diff HEAD~1 --name-only | grep -q 'fern/docs.yml'; then\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=wiki\" \\\n || echo \"skipping broadcast — insufficient permissions\"\n fi\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n",
1235
- "platform.commitStandards": "name: Commit Standards\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n commit-standards:\n name: Run commitlint\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # No local equivalent (linters.ts: commitlint has no localBin — enforced\n # only at commit time via the commit-msg hook, and here in CI over the\n # PR's commit range). Not run through the holocron composite action.\n - name: Run commitlint over the PR range\n if: github.event_name == 'pull_request'\n run: pnpm exec commitlint --from \"${{ github.event.pull_request.base.sha }}\" --to \"${{ github.sha }}\"\n",
1383
+ "platform.commitStandards": "name: Commit Standards\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n commit-standards:\n name: Run commitlint\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # No local equivalent (linters.ts: commitlint has no localBin — enforced\n # only at commit time via the commit-msg hook, and here in CI over the\n # PR's commit range). Not run through the holocron composite action.\n #\n # --config <resolved shared path> (config-resolution workstream, #676)\n # — falls back to commitlint's own auto-discovery of a local\n # commitlint.config.ts when the shared package's built dist/ isn't\n # present, same guard as the commit-msg hook.\n - name: Run commitlint over the PR range\n if: github.event_name == 'pull_request'\n run: |\n COMMITLINT_CONFIG=\"node_modules/@theholocron/commitlint-config/dist/index.js\"\n if [ -f \"$COMMITLINT_CONFIG\" ]; then\n pnpm exec commitlint --config \"$COMMITLINT_CONFIG\" --from \"${{ github.event.pull_request.base.sha }}\" --to \"${{ github.sha }}\"\n else\n pnpm exec commitlint --from \"${{ github.event.pull_request.base.sha }}\" --to \"${{ github.sha }}\"\n fi\n",
1236
1384
  "platform.repoValidation": "name: Repo Validation\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n adrs:\n name: Validate ADRs and specs\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate ADR and spec frontmatter\n with:\n task: platform.repoValidation\n job: adrs\n\n registry:\n name: Validate registry consistency\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate registry consistency\n with:\n task: platform.repoValidation\n job: registry\n\n docs-presence:\n name: Validate docs presence\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate docs presence for new packages\n with:\n task: platform.repoValidation\n job: docsPresence\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}\n\n conclusion:\n name: Conclusion\n runs-on: ubuntu-latest\n if: always()\n needs: [adrs, registry, docs-presence]\n steps:\n - name: Check job statuses\n run: |\n if [[ \"$RESULTS\" == *\"failure\"* ]] || [[ \"$RESULTS\" == *\"cancelled\"* ]]; then\n exit 1\n fi\n env:\n RESULTS: ${{ join(needs.*.result, ',') }}\n",
1237
1385
  "knowledge.wiki": "name: Wiki\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n fern-version:\n description: >\n Fern CLI version to install. Pin this to avoid breaking changes when\n Fern updates their config schema.\n type: string\n required: false\n default: \"5.114.1\"\n preview:\n description: >\n When true, publishes a preview instead of production.\n Requires preview-id to be set.\n type: boolean\n required: false\n default: false\n preview-id:\n description: >\n Stable ID for the preview build. Use the PR number (e.g. \"pr-123\")\n so the same preview URL is reused on every push to the branch.\n type: string\n required: false\n default: \"\"\n fern-org:\n description: >\n DEPRECATED — the preview URL is now read from Fern's own output.\n Kept as a fallback for building the URL when that parse fails, and so\n existing callers do not error.\n type: string\n required: false\n default: \"\"\n base-path:\n description: >\n DEPRECATED — see fern-org. Basepath appended to the fallback preview\n URL (e.g. \"holocron\" for multi-source routing).\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_FERN_TOKEN:\n required: false\n FERN_TOKEN:\n required: false\n\njobs:\n publish:\n name: Publish to Fern\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n deployments: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - name: Install Fern CLI\n run: npm install -g \"fern-api@$FERN_VERSION\"\n env:\n FERN_VERSION: ${{ inputs.fern-version }}\n\n - name: Publish docs\n if: ${{ !inputs.preview }}\n run: fern generate --docs\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n\n - name: Preview docs\n id: preview\n if: ${{ inputs.preview && inputs.preview-id != '' }}\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n run: |\n out=$(fern generate --docs --preview --id \"$PREVIEW_ID\" 2>&1) || { printf '%s\\n' \"$out\"; exit 1; }\n printf '%s\\n' \"$out\"\n # Fern prints \"Published docs to https://<org>-preview-<id>.docs.buildwithfern.com/<path>\"\n url=$(printf '%s\\n' \"$out\" | grep -oiE 'https://[a-z0-9.-]+\\.docs\\.buildwithfern\\.com[^ )]*' | head -n1)\n echo \"url=$url\" >> \"$GITHUB_OUTPUT\"\n\n - name: Report preview URL\n if: ${{ inputs.preview && inputs.preview-id != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n CAPTURED_URL: ${{ steps.preview.outputs.url }}\n FERN_ORG: ${{ inputs.fern-org }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n BASE_PATH: ${{ inputs.base-path }}\n run: |\n PREVIEW_URL=\"$CAPTURED_URL\"\n if [ -z \"$PREVIEW_URL\" ] && [ -n \"$FERN_ORG\" ]; then\n PREVIEW_URL=\"https://${FERN_ORG}-preview-${PREVIEW_ID}.docs.buildwithfern.com${BASE_PATH:+/${BASE_PATH}}\"\n fi\n if [ -z \"$PREVIEW_URL\" ]; then\n echo \"::warning::no preview URL parsed from Fern output and no fern-org fallback — skipping the deployment widget\"\n exit 0\n fi\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"wiki (Preview)\",\"description\":\"Wiki\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\")\n DEPLOY_ID=$(printf '%s\\n' \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"$PREVIEW_URL\"\n",
1238
1386
  bookkeeping: "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n configuration-path:\n description: Path to the labeler configuration file in the calling repo\n type: string\n required: false\n default: .github/labeler.yml\n\njobs:\n label:\n name: Apply Labels\n permissions:\n contents: read\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n sparse-checkout: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n sparse-checkout-cone-mode: false\n\n - uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4\n if: ${{ github.event_name == 'pull_request' && hashFiles(inputs.configuration-path || '.github/labeler.yml') != '' }}\n # v3.4 bundles Node 20; allow it to run under Actions' current default.\n env:\n ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true\n with:\n # Fall back to default path when triggered directly (not via workflow_call)\n # because inputs.* defaults only apply on workflow_call events.\n configuration-path: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n include-title: 1\n include-body: 0\n sync-labels: 1\n enable-versioned-regex: 0\n repo-token: ${{ github.token }}\n",
@@ -1339,6 +1487,120 @@ function turboConfig(config) {
1339
1487
  tasks
1340
1488
  }, null, 2)}\n`;
1341
1489
  }
1490
+ /**
1491
+ * Fixes #692: `turboConfig()` writes a `turbo.json` the moment *any* task in
1492
+ * the manifest has fan-out config — with no check for whether the repo's own
1493
+ * root package is actually covered by `pnpm-workspace.yaml`'s `packages:`
1494
+ * list. A repo shaped like `observability` (the library lives at repo root;
1495
+ * `packages:` lists only an unrelated `docs` site) silently breaks the
1496
+ * instant `turbo.json` exists: `holocron run <task>` switches from running
1497
+ * root's own script directly to `turbo run <task>`, which only sees declared
1498
+ * workspace members — root vanishes, the task "succeeds" with zero real work
1499
+ * done (confirmed empirically: `Packages in scope: docs`, root never
1500
+ * mentioned, exit 0).
1501
+ *
1502
+ * Detection is narrow and specific: root only needs to be an explicit
1503
+ * workspace member if its *own* `package.json` has a script literally named
1504
+ * after one of the tasks turbo.json is about to fan out — a plain
1505
+ * orchestrator root (`holocron`'s own `"build": "turbo run delivery.build"`,
1506
+ * which has no `delivery.build` script itself) never trips this; only a root
1507
+ * that's genuinely a directly-buildable package does. Confirmed the fix
1508
+ * empirically too — adding `.` to `packages:` makes turbo pick root back up
1509
+ * (`Packages in scope: docs, root-lib`) without disturbing the sibling
1510
+ * package's own caching.
1511
+ *
1512
+ * A targeted line-based edit, not a full YAML parse/reserialize — same
1513
+ * pattern `codecov.ts`'s `mergeCodecovComponents()` already uses for editing
1514
+ * an existing generated file. `pnpm-workspace.yaml` routinely carries a
1515
+ * `catalog:`/`catalogs:`/`overrides:` block after `packages:`; a real parser
1516
+ * round-trip risks reformatting or reordering content nobody asked to touch.
1517
+ * Only ever *adds* a line — never rewrites or reorders anything already
1518
+ * there — and is a no-op (`changed: false`) whenever root doesn't need it,
1519
+ * root is already listed, or `packages:` isn't in the plain block-list form
1520
+ * every repo checked actually uses.
1521
+ */
1522
+ function ensureRootWorkspaceMember(workspaceYaml, rootScripts, taskNames) {
1523
+ if (!taskNames.some((t) => rootScripts.includes(t))) return {
1524
+ content: workspaceYaml,
1525
+ changed: false
1526
+ };
1527
+ const lines = workspaceYaml.split("\n");
1528
+ const packagesLineIdx = lines.findIndex((l) => /^packages:\s*$/.test(l));
1529
+ if (packagesLineIdx === -1) return {
1530
+ content: workspaceYaml,
1531
+ changed: false
1532
+ };
1533
+ const items = [];
1534
+ let cursor = packagesLineIdx + 1;
1535
+ while (cursor < lines.length && /^\s*-\s*/.test(lines[cursor])) {
1536
+ items.push(lines[cursor]);
1537
+ cursor++;
1538
+ }
1539
+ if (items.some((item) => {
1540
+ const value = item.replace(/^\s*-\s*/, "").replace(/^["']|["']$/g, "").trim();
1541
+ return value === "." || value === "";
1542
+ })) return {
1543
+ content: workspaceYaml,
1544
+ changed: false
1545
+ };
1546
+ const newLine = `${items[0]?.match(/^(\s*-\s*)/)?.[1] ?? " - "}"."`;
1547
+ return {
1548
+ content: [
1549
+ ...lines.slice(0, packagesLineIdx + 1),
1550
+ newLine,
1551
+ ...lines.slice(packagesLineIdx + 1)
1552
+ ].join("\n"),
1553
+ changed: true
1554
+ };
1555
+ }
1556
+ /**
1557
+ * Fallback pin when a repo's own `pnpm-workspace.yaml` has no `turbo`
1558
+ * catalog entry yet — matches this repo's own catalog version (the
1559
+ * canonical source-of-truth repo for the whole org's tooling).
1560
+ */
1561
+ const DEFAULT_TURBO_VERSION = "^2.10.12";
1562
+ /**
1563
+ * Ensures `turbo` is declared in `package.json#devDependencies` — found
1564
+ * the hard way rolling this out to `observability` (predates #691):
1565
+ * `turboConfig()` writes a `turbo.json` the moment any task has fan-out
1566
+ * config, but never touches `package.json`, so a repo that predates this
1567
+ * feature (or never had `turbo` installed for any other reason) ends up
1568
+ * with a `turbo.json` and nothing local to run it. `resolveBin()`
1569
+ * (`run.ts`) falls back to a bare `turbo` on `PATH` when
1570
+ * `node_modules/.bin/turbo` doesn't exist — on a machine with no global
1571
+ * `turbo` at all that's a hard failure for every task; on one that
1572
+ * happens to have an unrelated global install, silent version skew
1573
+ * (confirmed empirically: a global 2.6.0 rejected this repo's generated
1574
+ * `turbo.json` outright — `Found an unknown key "globalDependencies"`,
1575
+ * a schema key from a newer version than the one actually resolved).
1576
+ *
1577
+ * Prefers `"catalog:"` when the repo's own `pnpm-workspace.yaml` already
1578
+ * has a `turbo` catalog entry — matches every already-migrated repo
1579
+ * (`holocron`, `clients`, …) — falling back to a real pinned version
1580
+ * otherwise. Never touches an existing `devDependencies.turbo` entry, so
1581
+ * a repo pinning its own version on purpose is left alone — idempotent,
1582
+ * a no-op once set either way.
1583
+ */
1584
+ function ensureTurboDependency(packageJson, workspaceYaml) {
1585
+ const pkg = JSON.parse(packageJson);
1586
+ if (typeof pkg.devDependencies?.turbo === "string") return {
1587
+ content: packageJson,
1588
+ changed: false
1589
+ };
1590
+ const hasTurboCatalogEntry = /^\s*turbo:\s*\S/m.test(workspaceYaml);
1591
+ const devDependencies = {
1592
+ ...pkg.devDependencies,
1593
+ turbo: hasTurboCatalogEntry ? "catalog:" : DEFAULT_TURBO_VERSION
1594
+ };
1595
+ const updated = {
1596
+ ...pkg,
1597
+ devDependencies
1598
+ };
1599
+ return {
1600
+ content: `${JSON.stringify(updated, null, 2)}\n`,
1601
+ changed: true
1602
+ };
1603
+ }
1342
1604
  //#endregion
1343
1605
  //#region src/astromech.ts
1344
1606
  /**
@@ -1441,8 +1703,39 @@ function createAstromech(options) {
1441
1703
  reusableTemplates: () => reusableTemplates(),
1442
1704
  requiredChecks: () => requiredChecks(options.config ?? {}),
1443
1705
  codecovConfig: (existing) => codecovConfig(options.cwd, existing),
1444
- turboConfig: () => turboConfig(options.config ?? {})
1706
+ turboConfig: () => turboConfig(options.config ?? {}),
1707
+ ensureRootWorkspaceMember: (workspaceYaml, rootScripts) => {
1708
+ return ensureRootWorkspaceMember(workspaceYaml, rootScripts, (options.config?.tasks ?? []).map(normalizeTaskEntry).filter((e) => e.local !== false).map((e) => e.name));
1709
+ },
1710
+ ensureTurboDependency: (packageJson, workspaceYaml) => ensureTurboDependency(packageJson, workspaceYaml)
1711
+ };
1712
+ }
1713
+ //#endregion
1714
+ //#region src/tsconfig.ts
1715
+ /**
1716
+ * A package-level `tsconfig.json` — the uniform shape confirmed across
1717
+ * every package checked, parameterized only by the two fields that
1718
+ * genuinely vary (`display`, `paths`). No scaffold/workflow header (matches
1719
+ * `.alexrc.json`'s existing precedent for a strict-JSON Bucket B file —
1720
+ * JSON has no comment syntax to carry one, and TypeScript's own tolerance
1721
+ * for JSONC comments in `tsconfig.json` isn't worth relying on for a file
1722
+ * this thin).
1723
+ */
1724
+ function createTsconfig(options) {
1725
+ const variant = options.variant ?? "node-lts";
1726
+ const compilerOptions = {
1727
+ baseUrl: "./",
1728
+ outDir: "./dist"
1729
+ };
1730
+ if (options.paths) compilerOptions.paths = { "@/*": ["./src/*"] };
1731
+ const config = {
1732
+ display: options.display,
1733
+ extends: `@theholocron/tsconfig/${variant}`,
1734
+ compilerOptions,
1735
+ include: ["src/**/*.ts"],
1736
+ exclude: ["node_modules", "dist"]
1445
1737
  };
1738
+ return JSON.stringify(config, null, 2) + "\n";
1446
1739
  }
1447
1740
  //#endregion
1448
- export { CI_ORDER, KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, REUSABLE_ACTIONS, REUSABLE_WORKFLOWS, TASKS, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, WORKFLOW_TEMPLATE_PROPERTIES, codecovComponentBlock, codecovConfig, createAstromech, createCodecovConfig, deriveDeployPaths, ensureIfNotFound, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, mergeCodecovComponents, normalizeWorkflowWith, readWorkspacePackages, requiredChecks, resolveLinters, reusableTemplates, runCi, runTask };
1741
+ export { CI_ORDER, KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, RESOLVABLE_TOOLS, REUSABLE_ACTIONS, REUSABLE_WORKFLOWS, TASKS, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, WORKFLOW_TEMPLATE_PROPERTIES, codecovComponentBlock, codecovConfig, createAstromech, createCodecovConfig, createTsconfig, deriveDeployPaths, ensureIfNotFound, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, mergeCodecovComponents, normalizeWorkflowWith, readWorkspacePackages, requiredChecks, resolveLinters, resolveToolConfig, reusableTemplates, runCi, runTask };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/astromech",
3
- "version": "5.0.0-alpha.4",
3
+ "version": "5.0.0-alpha.40",
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.4"
40
+ "@theholocron/datapad": "5.0.0-alpha.40"
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": "5.0.0-alpha.4"
56
+ "@theholocron/rollup-plugin-transform-template": "5.0.0-alpha.40"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=22"