@theholocron/astromech 5.0.0-alpha.155 → 5.0.0-alpha.156

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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { r as normalizeTaskEntry, t as loadTasksConfig } from "./load-ToVjaKPr.mjs";
1
+ import { r as normalizeTaskEntry, t as loadTasksConfig } from "./load-DdHEVFlG.mjs";
2
2
  import { n as alexrc_default, t as editorconfig_checker_default } from "./editorconfig-checker-DPejtR5z.mjs";
3
3
  import { existsSync, readFileSync, readdirSync } from "node:fs";
4
4
  import { dependencyNames, detectPackageManager, listWorkspacePackagesSync, readPackageJsonSync } from "@theholocron/pkg-utils";
@@ -9,7 +9,7 @@ import { findExecutableSync } from "@theholocron/fs-utils";
9
9
  //#region src/templates/codecov/codecov.yml
10
10
  var codecov_default = "codecov:\n require_ci_to_pass: true\n\ncoverage:\n precision: 2\n round: down\n status:\n project:\n default:\n target: auto\n threshold: 2%\n # Infra-only PRs (dep bumps, config, lockfile) touch no coverable code,\n # so codecov produces no report to compare. Pass instead of blocking\n # forever on a required check that will never post.\n if_not_found: success\n patch:\n default:\n target: 80%\n if_not_found: success\n\ncomment:\n layout: \"reach,diff,flags,components\"\n behavior: default\n require_changes: true\n\ncomponent_management:\n default_rules:\n statuses:\n - type: patch\n target: 80%\n if_not_found: success\n individual_components:\n";
11
11
  //#endregion
12
- //#region src/generators/codecov.ts
12
+ //#region src/generators/codecov/codecov.ts
13
13
  /**
14
14
  * `codecov.yml` generation — a manifest-derived artifact (component `paths`
15
15
  * come from workspace packages; the status targets track the `test` task's
@@ -22,7 +22,7 @@ const INDIVIDUAL_COMPONENTS_MARKER = " individual_components:";
22
22
  function scaffoldHeader() {
23
23
  return [
24
24
  `# Scaffolded by holocron repo setup — edit this file freely.`,
25
- `# Source: theholocron/holocron · packages/astromech/src/generators/codecov.ts`,
25
+ `# Source: theholocron/holocron · packages/astromech/src/generators/codecov/codecov.ts`,
26
26
  ``
27
27
  ].join("\n");
28
28
  }
@@ -217,7 +217,7 @@ var verification_typeSafety_default$1 = "name: Typecheck\n\non: # yamllint disab
217
217
  //#region src/templates/reusable/verification.unitTests.yml
218
218
  var verification_unitTests_default$1 = "name: Test\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n run-unit:\n description: Run unit tests with coverage (disable for UI-only repos that use Storybook testing exclusively)\n type: boolean\n required: false\n default: true\n run-storybook:\n description: Run Storybook vitest interaction tests (requires .storybook/ setup)\n type: boolean\n required: false\n default: false\n run-interaction:\n description: Run Storybook interaction and accessibility tests with Playwright\n type: boolean\n required: false\n default: false\n run-chromatic:\n description: Publish Storybook to Chromatic for visual regression testing\n type: boolean\n required: false\n default: false\n chromatic-projects:\n description: >\n JSON array of Chromatic projects to build, one matrix job per entry.\n Each entry: { \"tokenName\": \"WEB\", \"workingDir\": \"apps/web\", \"buildScript\": \"build:storybook\" }.\n tokenName maps to secret CHROMATIC_PROJECT_TOKEN_<TOKENNAME>; use \"default\"\n (or omit for the legacy empty-string form) to use the bare CHROMATIC_PROJECT_TOKEN\n secret for single-project repos.\n buildScript defaults to \"build:storybook\" when omitted.\n Default runs a single job from the repo root using CHROMATIC_PROJECT_TOKEN.\n type: string\n required: false\n default: '[{\"tokenName\":\"default\",\"workingDir\":\".\",\"buildScript\":\"build:storybook\"}]'\n run-user-flow:\n description: Run Cypress E2E user-flow tests (requires cypress.config.*)\n type: boolean\n required: false\n default: false\n wait-on-url:\n description: URL to wait for before running Cypress tests (default is Vite dev server; override for non-Vite stacks e.g. http://localhost:3000 for Next.js)\n type: string\n required: false\n default: \"http://localhost:5173\"\n secrets:\n CHROMATIC_PROJECT_TOKEN:\n required: false\n CYPRESS_RECORD_KEY:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n unit:\n name: Run unit tests and collect coverage\n if: ${{ inputs.run-unit }}\n permissions:\n contents: read\n id-token: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\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\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run tests with coverage\n with:\n task: verification.unitTests\n\n # A Codecov upload must not fail silently. fail_ci_if_error makes a failed\n # upload fail this step (the action's default is to log the error and pass),\n # which fails the job and so the `Test / Conclusion` check. Whether that\n # blocks a merge is up to whether the check is required, not this workflow\n # (holocron#934).\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n fail_ci_if_error: true\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\n fail_ci_if_error: true\n report_type: test_results\n files: \"**/test-report.junit.xml\"\n\n storybook:\n name: Run Storybook interaction tests\n if: ${{ inputs.run-storybook }}\n permissions:\n contents: read\n id-token: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\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\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec playwright install chromium --with-deps\n name: Install Playwright\n\n - run: pnpm test:storybook\n name: Run Storybook tests\n\n # A Codecov upload must not fail silently. fail_ci_if_error makes a failed\n # upload fail this step (the action's default is to log the error and pass),\n # which fails the job and so the `Test / Conclusion` check. Whether that\n # blocks a merge is up to whether the check is required, not this workflow\n # (holocron#934).\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n fail_ci_if_error: true\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\n fail_ci_if_error: true\n report_type: test_results\n files: \"**/test-report.junit.xml\"\n\n visual-and-composition:\n # No explicit matrix reference here — a skipped job (run-chromatic:\n # false, the common case) never expands its matrix, so a `${{\n # matrix.project.tokenName }}` in name: has nothing to resolve and\n # shows as raw template text in the checks UI. GitHub auto-appends\n # the matrix values in parens when the matrix *does* expand, so this\n # loses no distinctness across multiple Chromatic projects.\n name: Run visual and composition tests\n if: ${{ inputs.run-chromatic }}\n strategy:\n fail-fast: false\n matrix:\n project: ${{ fromJSON(inputs.chromatic-projects) }}\n permissions:\n contents: read\n statuses: write\n runs-on: ubuntu-latest\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: chromaui/action@14cfaef73576e69f95f47f60058063f46ca38719 # v18\n name: Publish to Chromatic\n with:\n projectToken: ${{ (matrix.project.tokenName == 'default' || matrix.project.tokenName == '') && secrets.CHROMATIC_PROJECT_TOKEN || secrets[format('CHROMATIC_PROJECT_TOKEN_{0}', matrix.project.tokenName)] }}\n token: ${{ github.token }}\n buildScriptName: ${{ matrix.project.buildScript || 'build:storybook' }}\n workingDir: ${{ matrix.project.workingDir || '.' }}\n storybookBaseDir: ${{ matrix.project.storybookBaseDir || '' }}\n untraced: ${{ matrix.project.untraced || '' }}\n onlyStoryFiles: ${{ matrix.project.onlyStoryFiles || '' }}\n exitZeroOnChanges: ${{ matrix.project.exitZeroOnChanges || false }}\n\n interaction-and-accessibility:\n name: Run interactions and accessibility tests\n if: ${{ inputs.run-interaction }}\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 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 - run: pnpm exec playwright install --with-deps\n name: Install Playwright\n\n - run: pnpm test:storybook\n name: Run interaction and accessibility tests\n\n user-flow:\n name: Run user flow tests\n if: ${{ inputs.run-user-flow }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n strategy:\n fail-fast: false\n matrix:\n containers: [1, 2]\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\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec cypress install\n name: Install Cypress binary\n\n - uses: cypress-io/github-action@1052aa98bbbe4f55210f844878213c07d9c8c399 # v6.7.13\n name: Cypress run\n with:\n start: pnpm dev\n wait-on: ${{ inputs.wait-on-url }}\n record: true\n parallel: true\n env:\n CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n conclusion:\n name: Conclusion\n runs-on: ubuntu-latest\n if: always()\n needs: [unit, storybook, visual-and-composition, interaction-and-accessibility, user-flow]\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";
219
219
  //#endregion
220
- //#region src/generators/github-action-thin-callers.ts
220
+ //#region src/generators/github-action-thin-callers/github-action-thin-callers.ts
221
221
  /**
222
222
  * Workflow templates + thin-caller generation.
223
223
  *
@@ -244,7 +244,7 @@ const WORKFLOW_TEMPLATES = {
244
244
  "platform.repoSync": "name: Platform\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n paths:\n - holocron.config.ts\n - package.json\n - pnpm-workspace.yaml\n workflow_dispatch:\n inputs:\n steps:\n description: \"Sync steps to run (default: all)\"\n type: string\n required: false\n\nconcurrency:\n group: sync-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n sync:\n name: Sync\n uses: theholocron/.github/.github/workflows/platform.repoSync.yml@main\n with:\n steps: ${{ inputs.steps }}\n secrets: inherit\n",
245
245
  "platform.commitStandards": "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\nconcurrency:\n group: platform-commit-standards-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n commit-standards:\n name: Commit Standards\n uses: theholocron/.github/.github/workflows/platform.commitStandards.yml@main\n secrets: inherit\n",
246
246
  "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",
247
- "platform.syncTemplates": "name: Platform\n\n# Bootstrap note: this `uses:` a reusable workflow that must already exist\n# on theholocron/.github's main branch to resolve at all — a brand-new\n# reusable workflow here needs ONE manual `holocron sync github` run\n# (pushing directly to theholocron/.github) before this thin caller can\n# ever successfully trigger it, since GitHub resolves `uses:` at run time,\n# not at merge time. Same deadlock every reusable workflow added here hits\n# on its first introduction.\non: # yamllint disable-line rule:truthy\n push:\n # main only — this publishes to theholocron/.github, consumed by every\n # other repo's CI. alpha is the prerelease channel; publishing from it\n # would let unreleased/unstable template changes overwrite what main\n # just published, the same stable-vs-prerelease split this org already\n # enforces for npm's dist-tags and Sentinel's own deploy trigger.\n branches: [main]\n paths:\n - packages/astromech/src/templates/**\n - packages/astromech/src/generators/**\n - packages/astromech/src/registry.ts\n - packages/cli/src/templates/**\n workflow_dispatch: {}\n\nconcurrency:\n group: sync-templates\n cancel-in-progress: false\n\npermissions:\n contents: read\n\njobs:\n sync-templates:\n name: Sync Templates\n # workflow_dispatch can target ANY ref, not only main — without this,\n # anyone able to dispatch this workflow (a broader group than who can\n # push to main) could run it against an arbitrary branch, checking out\n # and building that ref's own code with the sync token present, then\n # publishing that ref's templates to theholocron/.github.\n if: github.ref == 'refs/heads/main'\n uses: theholocron/.github/.github/workflows/platform.syncTemplates.yml@main\n secrets: inherit\n",
247
+ "platform.syncTemplates": "name: Platform\n\n# Bootstrap note: this `uses:` a reusable workflow that must already exist\n# on theholocron/.github's main branch to resolve at all — a brand-new\n# reusable workflow here needs ONE manual `holocron sync github` run\n# (pushing directly to theholocron/.github) before this thin caller can\n# ever successfully trigger it, since GitHub resolves `uses:` at run time,\n# not at merge time. Same deadlock every reusable workflow added here hits\n# on its first introduction.\non: # yamllint disable-line rule:truthy\n push:\n # main only — this publishes to theholocron/.github, consumed by every\n # other repo's CI. alpha is the prerelease channel; publishing from it\n # would let unreleased/unstable template changes overwrite what main\n # just published, the same stable-vs-prerelease split this org already\n # enforces for npm's dist-tags and Sentinel's own deploy trigger.\n branches: [main]\n paths:\n - packages/astromech/src/templates/**\n - packages/astromech/src/generators/**\n - packages/astromech/src/registry/**\n - packages/cli/src/templates/**\n workflow_dispatch: {}\n\nconcurrency:\n group: sync-templates\n cancel-in-progress: false\n\npermissions:\n contents: read\n\njobs:\n sync-templates:\n name: Sync Templates\n # workflow_dispatch can target ANY ref, not only main — without this,\n # anyone able to dispatch this workflow (a broader group than who can\n # push to main) could run it against an arbitrary branch, checking out\n # and building that ref's own code with the sync token present, then\n # publishing that ref's templates to theholocron/.github.\n if: github.ref == 'refs/heads/main'\n uses: theholocron/.github/.github/workflows/platform.syncTemplates.yml@main\n secrets: inherit\n",
248
248
  "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",
249
249
  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",
250
250
  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",
@@ -504,7 +504,7 @@ function deriveDeployPaths(raw) {
504
504
  return paths;
505
505
  }
506
506
  //#endregion
507
- //#region src/generators/github-action-reusable-workflows.ts
507
+ //#region src/generators/github-action-reusable-workflows/github-action-reusable-workflows.ts
508
508
  /**
509
509
  * The **reusable** GitHub Actions surface pushed to `theholocron/.github` by
510
510
  * `holocron sync github` — the `workflow_call` implementations and composite
@@ -565,7 +565,7 @@ const WORKFLOW_TEMPLATE_PROPERTIES = { bookkeeping: JSON.stringify({
565
565
  description: "Label and track issues and pull requests.",
566
566
  iconName: "octicon tag"
567
567
  }, null, 2) };
568
- const SOURCE = "packages/astromech/src/generators/github-action-reusable-workflows.ts";
568
+ const SOURCE = "packages/astromech/src/generators/github-action-reusable-workflows/github-action-reusable-workflows.ts";
569
569
  /**
570
570
  * "AUTO-GENERATED — do not edit" header for the YAML pushed to
571
571
  * `theholocron/.github`. Matches `createHeader().workflowHeader({ forPrimary: true })`
@@ -599,7 +599,7 @@ function reusableTemplates() {
599
599
  return out;
600
600
  }
601
601
  //#endregion
602
- //#region src/registry.ts
602
+ //#region src/registry/registry.ts
603
603
  /**
604
604
  * The task registry — how each task runs *locally*, without GitHub
605
605
  * Actions. `holocron run <task>` and `holocron run ci` resolve against this;
@@ -813,7 +813,7 @@ const CI_ORDER = [
813
813
  "delivery.deploy"
814
814
  ];
815
815
  //#endregion
816
- //#region src/generators/required-checks.ts
816
+ //#region src/generators/required-checks/required-checks.ts
817
817
  /**
818
818
  * `requiredChecks(config)` — the branch-protection required-status-check list,
819
819
  * derived from the task manifest. Every `required: true` task contributes its
@@ -843,7 +843,75 @@ function requiredChecks(config) {
843
843
  return out;
844
844
  }
845
845
  //#endregion
846
- //#region src/generators/turbo.ts
846
+ //#region src/generators/tsconfig/tsconfig.ts
847
+ /**
848
+ * `tsconfig.json` generation — Bucket B (config-resolution workstream, #676):
849
+ * the file must stay committed (the TypeScript language server discovers it
850
+ * directly from disk, no `--config` override mechanism to hand it a path
851
+ * instead), but its content is fully derivable, so it's generated rather
852
+ * than hand-authored — the same category as `.editorconfig`
853
+ * ({@link "../../templates/configs/editorconfig/create-config.js" createConfig})
854
+ * and {@link codecovConfig}.
855
+ *
856
+ * Surveyed every package-level `tsconfig.json` across the org (`holocron`,
857
+ * `clients`, `utils` — 20+ packages). `extends: "@theholocron/tsconfig/
858
+ * node-lts"`, `compilerOptions.baseUrl: "./"`, `compilerOptions.outDir:
859
+ * "./dist"`, `include: ["src/**\/*.ts"]`, and `exclude: ["node_modules",
860
+ * "dist"]` are uniform in every one — safe defaults here. `paths` (a `@/*`
861
+ * → `./src/*` import alias) is a real per-package choice, present in
862
+ * roughly 40% of packages checked — an opt-in parameter, not a default.
863
+ *
864
+ * Two fields checked and deliberately *not* absorbed as defaults, unlike
865
+ * `eslint-config`'s `tsconfigRootDir`/`settings.node` (which turned out
866
+ * redundant with the shared package's own behavior, `configs`#461):
867
+ * `compilerOptions.module`/`moduleResolution` (only `@theholocron/cli`
868
+ * overrides these, to `esnext`/`bundler` — a genuine deviation from the
869
+ * shared `node-lts` preset's `nodenext`/`nodenext`, needed for its bundler
870
+ * tooling, not something every package should inherit) and
871
+ * `compilerOptions.rootDir` (present in exactly two packages, always the
872
+ * same value `./src` that `include` already implies — an unnecessary
873
+ * override wherever it appears, not a pattern worth generalizing).
874
+ *
875
+ * Monorepo *root* `tsconfig.json` (a TS project-references "solution
876
+ * file" — `holocron`'s own root is `{ files: [], references: [...] }`,
877
+ * listing which packages to build) is a structurally different, genuinely
878
+ * per-repo document — out of scope here, same as Bucket C content.
879
+ *
880
+ * Not yet wired into `holocron repo setup`'s per-package write loop (every
881
+ * other Bucket B file there is a single repo-root file, safely
882
+ * overwritten every run — `tsconfig.json` is per-package, and most
883
+ * packages' files still carry real hand-authored content today, not yet
884
+ * migrated to a fully-generated state). That per-package iteration +
885
+ * safe-migration design is `#680`'s job, not duplicated here — this is
886
+ * the generator itself, ready for it to call.
887
+ */
888
+ /**
889
+ * A package-level `tsconfig.json` — the uniform shape confirmed across
890
+ * every package checked, parameterized only by the two fields that
891
+ * genuinely vary (`display`, `paths`). No scaffold/workflow header (matches
892
+ * `.alexrc.json`'s existing precedent for a strict-JSON Bucket B file —
893
+ * JSON has no comment syntax to carry one, and TypeScript's own tolerance
894
+ * for JSONC comments in `tsconfig.json` isn't worth relying on for a file
895
+ * this thin).
896
+ */
897
+ function createTsconfig(options) {
898
+ const variant = options.variant ?? "node-lts";
899
+ const compilerOptions = {
900
+ baseUrl: "./",
901
+ outDir: "./dist"
902
+ };
903
+ if (options.paths) compilerOptions.paths = { "@/*": ["./src/*"] };
904
+ const config = {
905
+ display: options.display,
906
+ extends: `@theholocron/tsconfig/${variant}`,
907
+ compilerOptions,
908
+ include: ["src/**/*.ts"],
909
+ exclude: ["node_modules", "dist"]
910
+ };
911
+ return stringifyJson(config);
912
+ }
913
+ //#endregion
914
+ //#region src/generators/turbo/turbo.ts
847
915
  /**
848
916
  * `turboConfig(config)` — the generated `turbo.json` for a repo's task
849
917
  * manifest, epic #672 D9 (#681). Every workspace, monorepo or single-package
@@ -1033,7 +1101,7 @@ function ensureTurboDependency(packageJson, workspaceYaml) {
1033
1101
  };
1034
1102
  }
1035
1103
  //#endregion
1036
- //#region src/tasks/run/linters.ts
1104
+ //#region src/tasks/run/linters/linters.ts
1037
1105
  /**
1038
1106
  * The linter registry — one list of linter names (from `config.tasks`'
1039
1107
  * `{ name: "lint", linters: [...] }`, or auto-detected) maps to both the
@@ -1187,14 +1255,14 @@ function resolveLinters(opts) {
1187
1255
  }));
1188
1256
  }
1189
1257
  //#endregion
1190
- //#region src/tasks/run/resolve-bin.ts
1258
+ //#region src/tasks/run/resolve-bin/resolve-bin.ts
1191
1259
  /** `<cwd>/node_modules/.bin/<tool>` when it exists, else the bare `tool` name (left to `PATH`). */
1192
1260
  function resolveBin(cwd, tool, fileExists) {
1193
1261
  const local = join(cwd, "node_modules", ".bin", tool);
1194
1262
  return fileExists(local) ? local : tool;
1195
1263
  }
1196
1264
  //#endregion
1197
- //#region src/tasks/run/resolve-runner.ts
1265
+ //#region src/tasks/run/resolve-runner/resolve-runner.ts
1198
1266
  const mkRunner = (tool, args = []) => ({
1199
1267
  tool,
1200
1268
  args
@@ -1241,7 +1309,7 @@ function resolveRunner(local, cwd, listDir, readFile) {
1241
1309
  for (const candidate of local.detect) if (files.some((f) => candidate.when.test(f)) || deps.has(candidate.tool)) return mkRunner(candidate.tool, candidate.args);
1242
1310
  }
1243
1311
  //#endregion
1244
- //#region src/tasks/run/resolve-export-entry.ts
1312
+ //#region src/tasks/run/resolve-export-entry/resolve-export-entry.ts
1245
1313
  /**
1246
1314
  * Unwrap a `package.json#exports` subpath entry down to its file path —
1247
1315
  * either a bare string, or an object with condition keys (`import`,
@@ -1259,7 +1327,7 @@ function resolveExportEntry(entry) {
1259
1327
  if (typeof value === "object" && value !== null) return resolveExportEntry(value);
1260
1328
  }
1261
1329
  //#endregion
1262
- //#region src/tasks/run/tool-configs.ts
1330
+ //#region src/tasks/run/tool-configs/tool-configs.ts
1263
1331
  /**
1264
1332
  * `library` is every mapped tool's org-wide default variant today — every
1265
1333
  * currently-migrated repo (`holocron`, `clients`, `utils`, `configs`,
@@ -1337,7 +1405,7 @@ const LOCAL_CONFIG_FILES = {
1337
1405
  ]
1338
1406
  };
1339
1407
  //#endregion
1340
- //#region src/tasks/run/resolve-tool-config.ts
1408
+ //#region src/tasks/run/resolve-tool-config/resolve-tool-config.ts
1341
1409
  /**
1342
1410
  * Resolves the absolute path to a Bucket A tool's shared `@theholocron/*-config`
1343
1411
  * entry point, installed in the consuming repo's own `node_modules` — and the
@@ -1401,7 +1469,7 @@ function resolveToolConfig(tool, cwd, deps) {
1401
1469
  return [...spec.flag, absolute];
1402
1470
  }
1403
1471
  //#endregion
1404
- //#region src/tasks/run/turbo-defines-task.ts
1472
+ //#region src/tasks/run/turbo-defines-task/turbo-defines-task.ts
1405
1473
  /** `true` when `<cwd>/turbo.json` declares `task` (under `tasks`, or turbo 1.x's `pipeline`). */
1406
1474
  function turboDefinesTask(cwd, task, readFile, fileExists) {
1407
1475
  if (!fileExists(join(cwd, "turbo.json"))) return false;
@@ -1746,7 +1814,7 @@ function runLinterGroup(input, names, passthrough) {
1746
1814
  };
1747
1815
  }
1748
1816
  //#endregion
1749
- //#region src/tasks/ci.ts
1817
+ //#region src/tasks/ci/ci.ts
1750
1818
  /**
1751
1819
  * `holocron run ci` / `astromech.ci()` — run the merge-gating checks locally, in CI
1752
1820
  * order, and exit non-zero on the first failure. The "will CI pass?" command a
@@ -1816,71 +1884,85 @@ function taskName(item) {
1816
1884
  return typeof item === "string" ? item : item.name;
1817
1885
  }
1818
1886
  //#endregion
1819
- //#region src/tasks/match-namespace.ts
1820
- /** Every configured task whose name starts with `"<namespace>."`, in manifest declaration order. */
1821
- function matchNamespace(config, namespace) {
1822
- const prefix = `${namespace}.`;
1823
- return (config.tasks ?? []).map(normalizeTaskEntry).reduce((names, entry) => {
1824
- if (entry.name.startsWith(prefix) && !names.includes(entry.name)) names.push(entry.name);
1825
- return names;
1826
- }, []);
1827
- }
1828
- //#endregion
1829
- //#region src/tasks/group.ts
1830
- function runGroup(input) {
1831
- const { print, namespace } = input;
1832
- const names = matchNamespace(input.config, namespace);
1833
- if (names.length === 0) return {
1834
- status: "unknown",
1835
- jobs: []
1836
- };
1837
- print(`holocron run ${namespace} — ${names.length} task(s)`);
1838
- print("");
1839
- const jobs = [];
1840
- for (const name of names) {
1841
- print(`▶ ${name}`);
1842
- const r = runTask({
1843
- print: (line) => print(` ${line}`),
1844
- logger: input.logger,
1845
- exec: input.exec,
1846
- readFile: input.readFile,
1847
- fileExists: input.fileExists,
1848
- listDir: input.listDir,
1849
- lookPath: input.lookPath,
1850
- task: name,
1851
- cwd: input.cwd,
1852
- dryRun: input.dryRun ?? false,
1853
- ...input.filter ? { filter: input.filter } : {}
1854
- });
1855
- jobs.push({
1856
- task: name,
1857
- status: r.status,
1858
- command: r.command,
1859
- message: r.message
1860
- });
1861
- }
1887
+ //#region src/utils/create-header/create-header.ts
1888
+ /**
1889
+ * Factory that binds `source` and `tool` once and returns all header
1890
+ * constructors for auto-generated files. Prefer this over passing source
1891
+ * as a positional argument at every call site.
1892
+ *
1893
+ * @example
1894
+ * const { workflowHeader, scaffoldHeader } = createHeader({
1895
+ * source: "packages/astromech/src/templates/configs/editorconfig/create-config.ts",
1896
+ * });
1897
+ *
1898
+ * export function createConfig(): string {
1899
+ * return `${workflowHeader()}${body}`;
1900
+ * }
1901
+ *
1902
+ * ## Which header to use
1903
+ *
1904
+ * | Function | When to use | User can edit? |
1905
+ * |-------------------|----------------------------------------------------|----------------|
1906
+ * | `workflowHeader` | Files overwritten on every setup / sync-github run | No — "AUTO-GENERATED — do not edit" |
1907
+ * | `scaffoldHeader` | Files written once; user takes ownership after | Yes — "Scaffolded — edit this file freely" |
1908
+ *
1909
+ * ## `workflowHeader` formats
1910
+ *
1911
+ * | Format | Comment style | Used for |
1912
+ * |---------------------|------------------------|-------------------------------------------------------|
1913
+ * | `"yaml"` (default) | `# …` lines | YAML workflows, ignore files, `.editorconfig`, labeler|
1914
+ * | `"cjs"` | `/* … *\/` block | CommonJS config files (`.cjs`) |
1915
+ * | `"shebang"` | `#!/bin/sh` + `# …` | Shell scripts (`prepare-commit-msg`, hooks) |
1916
+ */
1917
+ function createHeader(options) {
1918
+ const { source, tool = "holocron repo setup", forPrimary = false } = options;
1919
+ const doNotEdit = forPrimary ? `AUTO-GENERATED — do not edit in theholocron/.github directly.` : `AUTO-GENERATED — do not edit directly.`;
1862
1920
  return {
1863
- status: jobs.some((j) => j.status === "fail") ? "fail" : jobs.every((j) => j.status === "dry-run") ? "dry-run" : "ok",
1864
- jobs
1921
+ workflowHeader(format = "yaml") {
1922
+ if (format === "cjs") return [
1923
+ `/* ${doNotEdit}`,
1924
+ ` * Source: theholocron/holocron · ${source}`,
1925
+ ` * Tool: ${tool}`,
1926
+ ` * Changes: edit source in theholocron/holocron`,
1927
+ ` */`,
1928
+ ``
1929
+ ].join("\n");
1930
+ const yamlLines = [
1931
+ `# ${doNotEdit}`,
1932
+ `# Source: theholocron/holocron · ${source}`,
1933
+ `# Tool: ${tool}`,
1934
+ `# Changes: edit source in theholocron/holocron`,
1935
+ ``
1936
+ ].join("\n");
1937
+ if (format === "shebang") return `#!/bin/sh\n\n${yamlLines}`;
1938
+ return yamlLines;
1939
+ },
1940
+ scaffoldHeader() {
1941
+ return [
1942
+ `# Scaffolded by holocron repo setup — edit this file freely.`,
1943
+ `# Source: theholocron/holocron · ${source}`,
1944
+ ``
1945
+ ].join("\n");
1946
+ }
1865
1947
  };
1866
1948
  }
1867
1949
  //#endregion
1868
- //#region src/utils/default-list-dirs.ts
1950
+ //#region src/utils/default-list-dirs/default-list-dirs.ts
1869
1951
  const defaultListDirs = (path) => readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1870
1952
  //#endregion
1871
- //#region src/utils/default-read-json.ts
1953
+ //#region src/utils/default-read-json/default-read-json.ts
1872
1954
  const defaultReadJson = (path) => JSON.parse(readFileSync(path, "utf8"));
1873
1955
  //#endregion
1874
- //#region src/utils/default-run.ts
1956
+ //#region src/utils/default-run/default-run.ts
1875
1957
  const defaultRun = (command, args, opts) => spawnSync(command, args, {
1876
1958
  encoding: "utf8",
1877
1959
  ...opts
1878
1960
  });
1879
1961
  //#endregion
1880
- //#region src/utils/default-sleep.ts
1962
+ //#region src/utils/default-sleep/default-sleep.ts
1881
1963
  const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1882
1964
  //#endregion
1883
- //#region src/utils/exec.ts
1965
+ //#region src/utils/exec/exec.ts
1884
1966
  /** Runs `cmd` with the parent's stdio; a signal-killed or unspawnable process reports `-1`. */
1885
1967
  const realExec = (cmd, args, opts) => {
1886
1968
  return { exitCode: spawnSync(cmd, args, {
@@ -1889,262 +1971,76 @@ const realExec = (cmd, args, opts) => {
1889
1971
  }).status ?? -1 };
1890
1972
  };
1891
1973
  //#endregion
1892
- //#region src/utils/look-path.ts
1974
+ //#region src/utils/look-path/look-path.ts
1893
1975
  /** `node_modules/.bin/<bin>`, else the first `PATH` entry that has it, else `null`. */
1894
1976
  const realLookPath = (cwd, bin) => findExecutableSync(bin, { path: join(cwd, "node_modules", ".bin") }) ?? findExecutableSync(bin) ?? null;
1895
1977
  //#endregion
1896
- //#region src/utils/noop-logger.ts
1978
+ //#region src/utils/noop-logger/noop-logger.ts
1897
1979
  /** The `RunLogger` used when the caller injects none: drops every record. */
1898
1980
  const noopLogger = {
1899
1981
  debug() {},
1900
1982
  warn() {}
1901
1983
  };
1902
1984
  //#endregion
1903
- //#region src/astromech.ts
1985
+ //#region src/tasks/deploy-on-release/constants.ts
1986
+ /** A stable release (no prerelease identifier) is on `main`, the default channel for a task. */
1987
+ const MAIN_CHANNEL = "main";
1988
+ const DEPLOY_TASK = "delivery.deploy";
1989
+ /** How long to wait for npm to serve the release's versions, and how often to ask. */
1990
+ const WAIT_TIMEOUT_MS = 3e5;
1991
+ const WAIT_INTERVAL_MS = 1e4;
1992
+ //#endregion
1993
+ //#region src/tasks/deploy-on-release/workspace-dependencies/workspace-dependencies.ts
1994
+ /** The `workspace:*` dependencies of `pkg`, as workspace packages (with their current version). */
1995
+ function workspaceDependencies(pkg, all) {
1996
+ const names = new Set(Object.entries(pkg.dependencies).filter(([, spec]) => String(spec).startsWith("workspace:")).map(([name]) => name));
1997
+ return all.filter((candidate) => names.has(candidate.name));
1998
+ }
1999
+ //#endregion
2000
+ //#region src/tasks/deploy-on-release/default-paths/default-paths.ts
1904
2001
  /**
1905
- * `createAstromech(options)` — the self-contained task runner.
1906
- * `@theholocron/cli` instantiates it once and delegates the `run` /
1907
- * `ci` / workflow-generation commands to it (like `@theholocron/observability`).
2002
+ * The paths a release must touch to redeploy `pkg` when its manifest doesn't
2003
+ * list them: its own folder plus the folder of every
2004
+ * {@link workspaceDependencies} entry. Derived rather than listed so a new or
2005
+ * dropped workspace dependency needs no second edit.
2006
+ *
2007
+ * @returns Repo-relative prefixes, each ending in `/`.
1908
2008
  */
1909
- function createAstromech(options) {
1910
- const deps = {
1911
- print: options.print ?? ((line) => console.log(line)),
1912
- logger: options.logger ?? noopLogger,
1913
- exec: options.exec ?? realExec,
1914
- readFile: options.readFile ?? ((path) => readFileSync(path, "utf8")),
1915
- fileExists: options.fileExists ?? ((path) => existsSync(path)),
1916
- listDir: options.listDir ?? ((path) => readdirSync(path)),
1917
- lookPath: options.lookPath ?? realLookPath
2009
+ function defaultPaths(repoRoot, pkg, all) {
2010
+ return [pkg, ...workspaceDependencies(pkg, all)].map(({ dir }) => `${relative(repoRoot, dir)}/`);
2011
+ }
2012
+ //#endregion
2013
+ //#region src/tasks/deploy-on-release/deploy-targets/deploy-targets.ts
2014
+ /**
2015
+ * The packages whose manifest declares a release-time `delivery.deploy`
2016
+ * (`with.on === "release"`), with the task's options resolved.
2017
+ */
2018
+ function deployTargets(packages, manifests) {
2019
+ return packages.flatMap((pkg) => (manifests.get(pkg.dir)?.tasks ?? []).flatMap((item) => {
2020
+ const entry = normalizeTaskEntry(item);
2021
+ if (entry.name !== "delivery.deploy" || entry.with?.["on"] !== "release") return [];
2022
+ const { channel, paths } = entry.with;
2023
+ return [{
2024
+ pkg,
2025
+ channel: typeof channel === "string" ? channel : MAIN_CHANNEL,
2026
+ paths: Array.isArray(paths) ? paths.map(String) : void 0
2027
+ }];
2028
+ }));
2029
+ }
2030
+ //#endregion
2031
+ //#region src/tasks/deploy-on-release/should-deploy/should-deploy.ts
2032
+ /**
2033
+ * @param channel The release's channel: its prerelease identifier (`alpha`), or `main` for a stable release.
2034
+ * @param changedFiles `undefined` when there's no previous release to diff against.
2035
+ */
2036
+ function shouldDeploy({ channel, configuredChannel, changedFiles, paths }) {
2037
+ if (channel !== configuredChannel) return {
2038
+ deploy: false,
2039
+ reason: `channel "${channel}" isn't "${configuredChannel}"`
1918
2040
  };
1919
- const items = () => (options.config?.tasks ?? []).map((i) => normalizeTaskEntry(i));
1920
- /**
1921
- * Task names this repo's manifest can actually run locally — shared by
1922
- * {@link packageScripts} and by `holocron`'s own interactive task picker.
1923
- * Deliberately independent of `syncScripts`/`holocronScript`/`hooks`:
1924
- * those govern whether/how `package.json` scripts get *written*, not
1925
- * which tasks exist to run. A repo with `syncScripts: false` (this repo
1926
- * itself, among others) still has real runnable tasks — found live when
1927
- * `packageScripts()`'s `{}` short-circuit silently emptied the picker too.
1928
- */
1929
- const runnableTaskNames = () => {
1930
- const out = [];
1931
- for (const entry of items()) {
1932
- const def = TASKS[entry.name];
1933
- const hasNoLocalRunner = def?.local === null && !def.linterGroup && !def.jobs;
1934
- if (entry.local === false || !KNOWN_TASKS.has(entry.name) || hasNoLocalRunner) continue;
1935
- out.push(entry.name);
1936
- }
1937
- return out;
1938
- };
1939
- return {
1940
- run: (task, opts = {}) => {
1941
- if (!TASKS[task] && options.config && matchNamespace(options.config, task).length > 0) return runGroup({
1942
- ...deps,
1943
- cwd: options.cwd,
1944
- config: options.config,
1945
- namespace: task,
1946
- dryRun: opts.dryRun ?? false,
1947
- ...opts.filter ? { filter: opts.filter } : {}
1948
- });
1949
- return runTask({
1950
- ...deps,
1951
- task,
1952
- cwd: options.cwd,
1953
- ...opts.job !== void 0 ? { job: opts.job } : {},
1954
- passthrough: opts.passthrough ?? [],
1955
- dryRun: opts.dryRun ?? false,
1956
- required: opts.required ?? false,
1957
- ...opts.filter ? { filter: opts.filter } : {}
1958
- });
1959
- },
1960
- ci: (opts = {}) => runCi({
1961
- ...deps,
1962
- cwd: options.cwd,
1963
- config: options.config ?? {},
1964
- ...opts
1965
- }),
1966
- thinCallers: () => {
1967
- const orgCtx = options.orgContext ?? {};
1968
- const out = /* @__PURE__ */ new Map();
1969
- for (const entry of items()) {
1970
- const isDocsSite = entry.name === "knowledge.docs";
1971
- const isComponents = entry.name === "knowledge.components";
1972
- if (entry.ci === false || !KNOWN_WORKFLOWS.has(entry.name) && !isDocsSite && !isComponents) continue;
1973
- if (entry.name === "delivery.deploy" && entry.with?.["on"] === "release") continue;
1974
- const rawWith = isDocsSite ? {
1975
- preview: true,
1976
- ...entry.with,
1977
- docs: entry.with?.["docs"] ?? true
1978
- } : isComponents ? {
1979
- preview: true,
1980
- ...entry.with,
1981
- storybook: entry.with?.["storybook"] ?? [{ workingDir: "." }]
1982
- } : entry.with;
1983
- const withOverrides = rawWith ? normalizeWorkflowWith(rawWith) : void 0;
1984
- if (entry.name === "verification.unitTests" && withOverrides?.["run-unit"] === false && withOverrides?.["run-storybook"] === false) throw new Error("test workflow: at least one of \"run-unit\" or \"run-storybook\" must be true. Library repos use run-unit: true; UI/Storybook repos use run-storybook: true.");
1985
- const isDeployFamily = entry.name === "delivery.deploy" || isDocsSite || isComponents;
1986
- const additionalPaths = entry.paths ?? (isDeployFamily && rawWith ? deriveDeployPaths(rawWith) : void 0);
1987
- if (isDeployFamily && rawWith) {
1988
- const preview = extractPreviewConfig(rawWith, orgCtx);
1989
- if (preview) {
1990
- const deployWith = normalizeWorkflowWith(rawWith);
1991
- const deployPaths = entry.paths ?? deriveDeployPaths(rawWith);
1992
- out.set(`${entry.name}.yml`, generateCombinedDeployContent(deployWith, deployPaths, preview));
1993
- continue;
1994
- }
1995
- }
1996
- out.set(`${entry.name}.yml`, generateThinCallerContent(entry.name, withOverrides, additionalPaths, deps.logger));
1997
- }
1998
- return out;
1999
- },
2000
- packageScripts: () => {
2001
- const out = {};
2002
- if (!options.config || options.config.syncScripts === false) return out;
2003
- out.holocron = options.config.holocronScript ?? "holocron";
2004
- for (const name of runnableTaskNames()) out[name] = `holocron run ${name} --`;
2005
- const hooks = options.config.hooks;
2006
- if (hooks === true || typeof hooks === "object" && hooks.prePush !== false) out.prepare = "husky";
2007
- return out;
2008
- },
2009
- runnableTasks: () => runnableTaskNames(),
2010
- reusableTemplates: () => reusableTemplates(),
2011
- requiredChecks: () => requiredChecks(options.config ?? {}),
2012
- codecovConfig: (existing) => codecovConfig(options.cwd, existing),
2013
- turboConfig: () => turboConfig(options.config ?? {}),
2014
- ensureRootWorkspaceMember: (workspaceYaml, rootScripts) => {
2015
- return ensureRootWorkspaceMember(workspaceYaml, rootScripts, (options.config?.tasks ?? []).map(normalizeTaskEntry).filter((e) => e.local !== false).map((e) => e.name));
2016
- },
2017
- ensureTurboDependency: (packageJson, workspaceYaml) => ensureTurboDependency(packageJson, workspaceYaml)
2018
- };
2019
- }
2020
- //#endregion
2021
- //#region src/generators/tsconfig.ts
2022
- /**
2023
- * `tsconfig.json` generation — Bucket B (config-resolution workstream, #676):
2024
- * the file must stay committed (the TypeScript language server discovers it
2025
- * directly from disk, no `--config` override mechanism to hand it a path
2026
- * instead), but its content is fully derivable, so it's generated rather
2027
- * than hand-authored — the same category as `.editorconfig`
2028
- * ({@link "./templates/configs/editorconfig/create-config.js" createConfig})
2029
- * and {@link codecovConfig}.
2030
- *
2031
- * Surveyed every package-level `tsconfig.json` across the org (`holocron`,
2032
- * `clients`, `utils` — 20+ packages). `extends: "@theholocron/tsconfig/
2033
- * node-lts"`, `compilerOptions.baseUrl: "./"`, `compilerOptions.outDir:
2034
- * "./dist"`, `include: ["src/**\/*.ts"]`, and `exclude: ["node_modules",
2035
- * "dist"]` are uniform in every one — safe defaults here. `paths` (a `@/*`
2036
- * → `./src/*` import alias) is a real per-package choice, present in
2037
- * roughly 40% of packages checked — an opt-in parameter, not a default.
2038
- *
2039
- * Two fields checked and deliberately *not* absorbed as defaults, unlike
2040
- * `eslint-config`'s `tsconfigRootDir`/`settings.node` (which turned out
2041
- * redundant with the shared package's own behavior, `configs`#461):
2042
- * `compilerOptions.module`/`moduleResolution` (only `@theholocron/cli`
2043
- * overrides these, to `esnext`/`bundler` — a genuine deviation from the
2044
- * shared `node-lts` preset's `nodenext`/`nodenext`, needed for its bundler
2045
- * tooling, not something every package should inherit) and
2046
- * `compilerOptions.rootDir` (present in exactly two packages, always the
2047
- * same value `./src` that `include` already implies — an unnecessary
2048
- * override wherever it appears, not a pattern worth generalizing).
2049
- *
2050
- * Monorepo *root* `tsconfig.json` (a TS project-references "solution
2051
- * file" — `holocron`'s own root is `{ files: [], references: [...] }`,
2052
- * listing which packages to build) is a structurally different, genuinely
2053
- * per-repo document — out of scope here, same as Bucket C content.
2054
- *
2055
- * Not yet wired into `holocron repo setup`'s per-package write loop (every
2056
- * other Bucket B file there is a single repo-root file, safely
2057
- * overwritten every run — `tsconfig.json` is per-package, and most
2058
- * packages' files still carry real hand-authored content today, not yet
2059
- * migrated to a fully-generated state). That per-package iteration +
2060
- * safe-migration design is `#680`'s job, not duplicated here — this is
2061
- * the generator itself, ready for it to call.
2062
- */
2063
- /**
2064
- * A package-level `tsconfig.json` — the uniform shape confirmed across
2065
- * every package checked, parameterized only by the two fields that
2066
- * genuinely vary (`display`, `paths`). No scaffold/workflow header (matches
2067
- * `.alexrc.json`'s existing precedent for a strict-JSON Bucket B file —
2068
- * JSON has no comment syntax to carry one, and TypeScript's own tolerance
2069
- * for JSONC comments in `tsconfig.json` isn't worth relying on for a file
2070
- * this thin).
2071
- */
2072
- function createTsconfig(options) {
2073
- const variant = options.variant ?? "node-lts";
2074
- const compilerOptions = {
2075
- baseUrl: "./",
2076
- outDir: "./dist"
2077
- };
2078
- if (options.paths) compilerOptions.paths = { "@/*": ["./src/*"] };
2079
- const config = {
2080
- display: options.display,
2081
- extends: `@theholocron/tsconfig/${variant}`,
2082
- compilerOptions,
2083
- include: ["src/**/*.ts"],
2084
- exclude: ["node_modules", "dist"]
2085
- };
2086
- return stringifyJson(config);
2087
- }
2088
- //#endregion
2089
- //#region src/tasks/deploy-on-release/constants.ts
2090
- /** A stable release (no prerelease identifier) is on `main`, the default channel for a task. */
2091
- const MAIN_CHANNEL = "main";
2092
- const DEPLOY_TASK = "delivery.deploy";
2093
- /** How long to wait for npm to serve the release's versions, and how often to ask. */
2094
- const WAIT_TIMEOUT_MS = 3e5;
2095
- const WAIT_INTERVAL_MS = 1e4;
2096
- //#endregion
2097
- //#region src/tasks/deploy-on-release/workspace-dependencies.ts
2098
- /** The `workspace:*` dependencies of `pkg`, as workspace packages (with their current version). */
2099
- function workspaceDependencies(pkg, all) {
2100
- const names = new Set(Object.entries(pkg.dependencies).filter(([, spec]) => String(spec).startsWith("workspace:")).map(([name]) => name));
2101
- return all.filter((candidate) => names.has(candidate.name));
2102
- }
2103
- //#endregion
2104
- //#region src/tasks/deploy-on-release/default-paths.ts
2105
- /**
2106
- * The paths a release must touch to redeploy `pkg` when its manifest doesn't
2107
- * list them: its own folder plus the folder of every
2108
- * {@link workspaceDependencies} entry. Derived rather than listed so a new or
2109
- * dropped workspace dependency needs no second edit.
2110
- *
2111
- * @returns Repo-relative prefixes, each ending in `/`.
2112
- */
2113
- function defaultPaths(repoRoot, pkg, all) {
2114
- return [pkg, ...workspaceDependencies(pkg, all)].map(({ dir }) => `${relative(repoRoot, dir)}/`);
2115
- }
2116
- //#endregion
2117
- //#region src/tasks/deploy-on-release/deploy-targets.ts
2118
- /**
2119
- * The packages whose manifest declares a release-time `delivery.deploy`
2120
- * (`with.on === "release"`), with the task's options resolved.
2121
- */
2122
- function deployTargets(packages, manifests) {
2123
- return packages.flatMap((pkg) => (manifests.get(pkg.dir)?.tasks ?? []).flatMap((item) => {
2124
- const entry = normalizeTaskEntry(item);
2125
- if (entry.name !== "delivery.deploy" || entry.with?.["on"] !== "release") return [];
2126
- const { channel, paths } = entry.with;
2127
- return [{
2128
- pkg,
2129
- channel: typeof channel === "string" ? channel : MAIN_CHANNEL,
2130
- paths: Array.isArray(paths) ? paths.map(String) : void 0
2131
- }];
2132
- }));
2133
- }
2134
- //#endregion
2135
- //#region src/tasks/deploy-on-release/should-deploy.ts
2136
- /**
2137
- * @param channel The release's channel: its prerelease identifier (`alpha`), or `main` for a stable release.
2138
- * @param changedFiles `undefined` when there's no previous release to diff against.
2139
- */
2140
- function shouldDeploy({ channel, configuredChannel, changedFiles, paths }) {
2141
- if (channel !== configuredChannel) return {
2142
- deploy: false,
2143
- reason: `channel "${channel}" isn't "${configuredChannel}"`
2144
- };
2145
- if (changedFiles === void 0) return {
2146
- deploy: true,
2147
- reason: "no previous release to diff against"
2041
+ if (changedFiles === void 0) return {
2042
+ deploy: true,
2043
+ reason: "no previous release to diff against"
2148
2044
  };
2149
2045
  const touched = changedFiles.find((file) => paths.some((path) => file.startsWith(path)));
2150
2046
  return touched ? {
@@ -2156,7 +2052,7 @@ function shouldDeploy({ channel, configuredChannel, changedFiles, paths }) {
2156
2052
  };
2157
2053
  }
2158
2054
  //#endregion
2159
- //#region src/tasks/deploy-on-release/wait-for-published.ts
2055
+ //#region src/tasks/deploy-on-release/wait-for-published/wait-for-published.ts
2160
2056
  /**
2161
2057
  * Waits until npm serves every `name@version` in `packages`, asking with
2162
2058
  * `npm view --prefer-online` (so npm's local cache can't answer for the
@@ -2183,7 +2079,7 @@ async function waitForPublished({ packages, run, sleep, now, cwd, timeoutMs = WA
2183
2079
  }
2184
2080
  }
2185
2081
  //#endregion
2186
- //#region src/tasks/deploy-on-release/workspace-packages.ts
2082
+ //#region src/tasks/deploy-on-release/workspace-packages/workspace-packages.ts
2187
2083
  /**
2188
2084
  * Every package under `<repoRoot>/packages/*` and `<repoRoot>/plugins/*`
2189
2085
  * with a readable, named `package.json` — `holocron`'s own layout splits
@@ -2349,66 +2245,182 @@ async function deployOnRelease(options, { run = defaultRun, readJson = defaultRe
2349
2245
  };
2350
2246
  }
2351
2247
  //#endregion
2352
- //#region src/utils/create-header.ts
2248
+ //#region src/tasks/match-namespace/match-namespace.ts
2249
+ /** Every configured task whose name starts with `"<namespace>."`, in manifest declaration order. */
2250
+ function matchNamespace(config, namespace) {
2251
+ const prefix = `${namespace}.`;
2252
+ return (config.tasks ?? []).map(normalizeTaskEntry).reduce((names, entry) => {
2253
+ if (entry.name.startsWith(prefix) && !names.includes(entry.name)) names.push(entry.name);
2254
+ return names;
2255
+ }, []);
2256
+ }
2257
+ //#endregion
2258
+ //#region src/tasks/group/group.ts
2353
2259
  /**
2354
- * Factory that binds `source` and `tool` once and returns all header
2355
- * constructors for auto-generated files. Prefer this over passing source
2356
- * as a positional argument at every call site.
2357
- *
2358
- * @example
2359
- * const { workflowHeader, scaffoldHeader } = createHeader({
2360
- * source: "packages/astromech/src/templates/configs/editorconfig/create-config.ts",
2361
- * });
2362
- *
2363
- * export function createConfig(): string {
2364
- * return `${workflowHeader()}${body}`;
2365
- * }
2260
+ * `holocron run <namespace>` — run every configured task under one
2261
+ * namespace prefix (`sourceQuality.*`, `verification.*`, …) in sequence.
2262
+ * `Astromech.run()` falls back here whenever the given name isn't an exact
2263
+ * `TASKS` entry but matches the prefix of one or more configured tasks —
2264
+ * same "aggregate several real tasks into one report" shape `runCi` already
2265
+ * established, scoped to one namespace instead of the merge-gating set.
2366
2266
  *
2367
- * ## Which header to use
2368
- *
2369
- * | Function | When to use | User can edit? |
2370
- * |-------------------|----------------------------------------------------|----------------|
2371
- * | `workflowHeader` | Files overwritten on every setup / sync-github run | No — "AUTO-GENERATED — do not edit" |
2372
- * | `scaffoldHeader` | Files written once; user takes ownership after | Yes — "Scaffolded — edit this file freely" |
2373
- *
2374
- * ## `workflowHeader` formats
2375
- *
2376
- * | Format | Comment style | Used for |
2377
- * |---------------------|------------------------|-------------------------------------------------------|
2378
- * | `"yaml"` (default) | `# …` lines | YAML workflows, ignore files, `.editorconfig`, labeler|
2379
- * | `"cjs"` | `/* … *\/` block | CommonJS config files (`.cjs`) |
2380
- * | `"shebang"` | `#!/bin/sh` + `# …` | Shell scripts (`prepare-commit-msg`, hooks) |
2267
+ * Every task in the namespace always runs — one task's failure never skips
2268
+ * the rest, same as `runCi`'s own loop — so a single invocation surfaces
2269
+ * every problem in the namespace at once instead of stopping at the first.
2381
2270
  */
2382
- function createHeader(options) {
2383
- const { source, tool = "holocron repo setup", forPrimary = false } = options;
2384
- const doNotEdit = forPrimary ? `AUTO-GENERATED — do not edit in theholocron/.github directly.` : `AUTO-GENERATED — do not edit directly.`;
2271
+ function runGroup(input) {
2272
+ const { print, namespace } = input;
2273
+ const names = matchNamespace(input.config, namespace);
2274
+ if (names.length === 0) return {
2275
+ status: "unknown",
2276
+ jobs: []
2277
+ };
2278
+ print(`holocron run ${namespace} — ${names.length} task(s)`);
2279
+ print("");
2280
+ const jobs = [];
2281
+ for (const name of names) {
2282
+ print(`▶ ${name}`);
2283
+ const r = runTask({
2284
+ print: (line) => print(` ${line}`),
2285
+ logger: input.logger,
2286
+ exec: input.exec,
2287
+ readFile: input.readFile,
2288
+ fileExists: input.fileExists,
2289
+ listDir: input.listDir,
2290
+ lookPath: input.lookPath,
2291
+ task: name,
2292
+ cwd: input.cwd,
2293
+ dryRun: input.dryRun ?? false,
2294
+ ...input.filter ? { filter: input.filter } : {}
2295
+ });
2296
+ jobs.push({
2297
+ task: name,
2298
+ status: r.status,
2299
+ command: r.command,
2300
+ message: r.message
2301
+ });
2302
+ }
2385
2303
  return {
2386
- workflowHeader(format = "yaml") {
2387
- if (format === "cjs") return [
2388
- `/* ${doNotEdit}`,
2389
- ` * Source: theholocron/holocron · ${source}`,
2390
- ` * Tool: ${tool}`,
2391
- ` * Changes: edit source in theholocron/holocron`,
2392
- ` */`,
2393
- ``
2394
- ].join("\n");
2395
- const yamlLines = [
2396
- `# ${doNotEdit}`,
2397
- `# Source: theholocron/holocron · ${source}`,
2398
- `# Tool: ${tool}`,
2399
- `# Changes: edit source in theholocron/holocron`,
2400
- ``
2401
- ].join("\n");
2402
- if (format === "shebang") return `#!/bin/sh\n\n${yamlLines}`;
2403
- return yamlLines;
2404
- },
2405
- scaffoldHeader() {
2406
- return [
2407
- `# Scaffolded by holocron repo setup — edit this file freely.`,
2408
- `# Source: theholocron/holocron · ${source}`,
2409
- ``
2410
- ].join("\n");
2304
+ status: jobs.some((j) => j.status === "fail") ? "fail" : jobs.every((j) => j.status === "dry-run") ? "dry-run" : "ok",
2305
+ jobs
2306
+ };
2307
+ }
2308
+ //#endregion
2309
+ //#region src/astromech.ts
2310
+ /**
2311
+ * `createAstromech(options)` — the self-contained task runner.
2312
+ * `@theholocron/cli` instantiates it once and delegates the `run` /
2313
+ * `ci` / workflow-generation commands to it (like `@theholocron/observability`).
2314
+ */
2315
+ function createAstromech(options) {
2316
+ const deps = {
2317
+ print: options.print ?? ((line) => console.log(line)),
2318
+ logger: options.logger ?? noopLogger,
2319
+ exec: options.exec ?? realExec,
2320
+ readFile: options.readFile ?? ((path) => readFileSync(path, "utf8")),
2321
+ fileExists: options.fileExists ?? ((path) => existsSync(path)),
2322
+ listDir: options.listDir ?? ((path) => readdirSync(path)),
2323
+ lookPath: options.lookPath ?? realLookPath
2324
+ };
2325
+ const items = () => (options.config?.tasks ?? []).map((i) => normalizeTaskEntry(i));
2326
+ /**
2327
+ * Task names this repo's manifest can actually run locally — shared by
2328
+ * {@link packageScripts} and by `holocron`'s own interactive task picker.
2329
+ * Deliberately independent of `syncScripts`/`holocronScript`/`hooks`:
2330
+ * those govern whether/how `package.json` scripts get *written*, not
2331
+ * which tasks exist to run. A repo with `syncScripts: false` (this repo
2332
+ * itself, among others) still has real runnable tasks — found live when
2333
+ * `packageScripts()`'s `{}` short-circuit silently emptied the picker too.
2334
+ */
2335
+ const runnableTaskNames = () => {
2336
+ const out = [];
2337
+ for (const entry of items()) {
2338
+ const def = TASKS[entry.name];
2339
+ const hasNoLocalRunner = def?.local === null && !def.linterGroup && !def.jobs;
2340
+ if (entry.local === false || !KNOWN_TASKS.has(entry.name) || hasNoLocalRunner) continue;
2341
+ out.push(entry.name);
2411
2342
  }
2343
+ return out;
2344
+ };
2345
+ return {
2346
+ run: (task, opts = {}) => {
2347
+ if (!TASKS[task] && options.config && matchNamespace(options.config, task).length > 0) return runGroup({
2348
+ ...deps,
2349
+ cwd: options.cwd,
2350
+ config: options.config,
2351
+ namespace: task,
2352
+ dryRun: opts.dryRun ?? false,
2353
+ ...opts.filter ? { filter: opts.filter } : {}
2354
+ });
2355
+ return runTask({
2356
+ ...deps,
2357
+ task,
2358
+ cwd: options.cwd,
2359
+ ...opts.job !== void 0 ? { job: opts.job } : {},
2360
+ passthrough: opts.passthrough ?? [],
2361
+ dryRun: opts.dryRun ?? false,
2362
+ required: opts.required ?? false,
2363
+ ...opts.filter ? { filter: opts.filter } : {}
2364
+ });
2365
+ },
2366
+ ci: (opts = {}) => runCi({
2367
+ ...deps,
2368
+ cwd: options.cwd,
2369
+ config: options.config ?? {},
2370
+ ...opts
2371
+ }),
2372
+ thinCallers: () => {
2373
+ const orgCtx = options.orgContext ?? {};
2374
+ const out = /* @__PURE__ */ new Map();
2375
+ for (const entry of items()) {
2376
+ const isDocsSite = entry.name === "knowledge.docs";
2377
+ const isComponents = entry.name === "knowledge.components";
2378
+ if (entry.ci === false || !KNOWN_WORKFLOWS.has(entry.name) && !isDocsSite && !isComponents) continue;
2379
+ if (entry.name === "delivery.deploy" && entry.with?.["on"] === "release") continue;
2380
+ const rawWith = isDocsSite ? {
2381
+ preview: true,
2382
+ ...entry.with,
2383
+ docs: entry.with?.["docs"] ?? true
2384
+ } : isComponents ? {
2385
+ preview: true,
2386
+ ...entry.with,
2387
+ storybook: entry.with?.["storybook"] ?? [{ workingDir: "." }]
2388
+ } : entry.with;
2389
+ const withOverrides = rawWith ? normalizeWorkflowWith(rawWith) : void 0;
2390
+ if (entry.name === "verification.unitTests" && withOverrides?.["run-unit"] === false && withOverrides?.["run-storybook"] === false) throw new Error("test workflow: at least one of \"run-unit\" or \"run-storybook\" must be true. Library repos use run-unit: true; UI/Storybook repos use run-storybook: true.");
2391
+ const isDeployFamily = entry.name === "delivery.deploy" || isDocsSite || isComponents;
2392
+ const additionalPaths = entry.paths ?? (isDeployFamily && rawWith ? deriveDeployPaths(rawWith) : void 0);
2393
+ if (isDeployFamily && rawWith) {
2394
+ const preview = extractPreviewConfig(rawWith, orgCtx);
2395
+ if (preview) {
2396
+ const deployWith = normalizeWorkflowWith(rawWith);
2397
+ const deployPaths = entry.paths ?? deriveDeployPaths(rawWith);
2398
+ out.set(`${entry.name}.yml`, generateCombinedDeployContent(deployWith, deployPaths, preview));
2399
+ continue;
2400
+ }
2401
+ }
2402
+ out.set(`${entry.name}.yml`, generateThinCallerContent(entry.name, withOverrides, additionalPaths, deps.logger));
2403
+ }
2404
+ return out;
2405
+ },
2406
+ packageScripts: () => {
2407
+ const out = {};
2408
+ if (!options.config || options.config.syncScripts === false) return out;
2409
+ out.holocron = options.config.holocronScript ?? "holocron";
2410
+ for (const name of runnableTaskNames()) out[name] = `holocron run ${name} --`;
2411
+ const hooks = options.config.hooks;
2412
+ if (hooks === true || typeof hooks === "object" && hooks.prePush !== false) out.prepare = "husky";
2413
+ return out;
2414
+ },
2415
+ runnableTasks: () => runnableTaskNames(),
2416
+ reusableTemplates: () => reusableTemplates(),
2417
+ requiredChecks: () => requiredChecks(options.config ?? {}),
2418
+ codecovConfig: (existing) => codecovConfig(options.cwd, existing),
2419
+ turboConfig: () => turboConfig(options.config ?? {}),
2420
+ ensureRootWorkspaceMember: (workspaceYaml, rootScripts) => {
2421
+ return ensureRootWorkspaceMember(workspaceYaml, rootScripts, (options.config?.tasks ?? []).map(normalizeTaskEntry).filter((e) => e.local !== false).map((e) => e.name));
2422
+ },
2423
+ ensureTurboDependency: (packageJson, workspaceYaml) => ensureTurboDependency(packageJson, workspaceYaml)
2412
2424
  };
2413
2425
  }
2414
2426
  //#endregion