@theholocron/cli 4.9.0 → 4.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,9 +72,49 @@ Additional `repo` fields recognised by `holocron setup`:
72
72
  | ----------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
73
  | `repo.teams` | `Array<string \| { slug, permission }>` | GitHub teams granted repo access. String shorthand defaults to `push` (Write). `holocron setup` also writes `.github/CODEOWNERS` for teams with `push`/`maintain`/`admin`. |
74
74
  | `repo.topics` | `string[]` | GitHub topics set on the repository. |
75
- | `repo.protection` | `"balanced" \| "strict" \| "none"` | Branch-protection preset applied by `holocron setup`. |
75
+ | `repo.protection` | `"balanced" \| "strict" \| "none"` | Branch-protection preset applied by `holocron setup`. For `"strict"`, the required status checks are derived from the task manifest — see below. |
76
76
  | `repo.properties` | `RepoProperties` | Org-level custom property values synced to the GitHub dashboard. |
77
77
 
78
+ ### Required status checks (`protection: "strict"`)
79
+
80
+ `holocron setup` builds the branch-protection required-check list from the
81
+ manifest, not a hand-maintained array:
82
+
83
+ - `"DCO"` (hard-prepended), then
84
+ - every `{ required: true }` task's workflow check context (`Lint / Conclusion`,
85
+ `Test / Conclusion`, `Typecheck / Conclusion`, `audit / Conclusion`), walked
86
+ in CI order, then
87
+ - the top-level `extraRequiredChecks` (codecov gates, a bundle-build check, …).
88
+
89
+ ```ts
90
+ export default defineConfig({
91
+ tasks: [
92
+ { name: "lint", required: true },
93
+ { name: "test", required: true },
94
+ { name: "typecheck", required: true },
95
+ ],
96
+ extraRequiredChecks: ["codecov/patch", "codecov/project"],
97
+ repo: { name: "acme/app", protection: "strict" },
98
+ providers: { source: "github" },
99
+ });
100
+ ```
101
+
102
+ ### Git hooks
103
+
104
+ ```ts
105
+ export default defineConfig({
106
+ hooks: true, // or { prePush: true }
107
+ repo: { protection: "strict" },
108
+ providers: { source: "github" },
109
+ });
110
+ ```
111
+
112
+ `holocron setup` writes `.husky/pre-push` (runs `holocron ci` before every
113
+ push) and sets `package.json#scripts.prepare` to `husky`. On by default for
114
+ `protection: "strict"`; `false` / `{ prePush: false }` opts out. `holocron
115
+ setup --hooks` / `--no-hooks` override per run. Bypass one push with `git push
116
+ --no-verify`.
117
+
78
118
  ### Task scripts
79
119
 
80
120
  `holocron sync` reconciles `package.json#scripts` from the `tasks` manifest
@@ -93,6 +133,21 @@ Writes `"holocron": "<holocronScript ?? 'holocron'>"` plus one
93
133
  `"<task>": "holocron run <task>"` per runnable task. `syncScripts: false`
94
134
  opts out entirely.
95
135
 
136
+ ### `holocron ci`
137
+
138
+ ```bash
139
+ holocron ci [--all] [--filter <pkg>] [--dry-run]
140
+ ```
141
+
142
+ Runs the merge-gating checks locally, in CI order, exiting non-zero on the
143
+ first failure — the "will my PR be green?" pre-flight, and what the
144
+ `pre-push` hook runs. Default scope is every `{ required: true }` task
145
+ (falling back to every `ci: true` task when nothing is marked required).
146
+ `--all` forces the full set; `--filter` is a `turbo --filter=` passthrough.
147
+ Order comes from the `CI_ORDER` constant in `@theholocron/astromech`
148
+ (`typecheck → lint → test → build → …`). Tasks with no local equivalent
149
+ (`audit`, `codeql`, `deploy`) are reported as enforced-in-CI, never failures.
150
+
96
151
  ### Lint parity
97
152
 
98
153
  The `lint` task takes an optional `linters` array — one list that drives
package/dist/cli.mjs CHANGED
@@ -284,6 +284,7 @@ function resolveConfig(raw) {
284
284
  syncScripts: raw.syncScripts,
285
285
  holocronScript: raw.holocronScript,
286
286
  extraRequiredChecks: raw.extraRequiredChecks,
287
+ hooks: raw.hooks,
287
288
  providers,
288
289
  apps: raw.apps ?? [],
289
290
  doctor: raw.doctor ?? {},
@@ -3374,12 +3375,12 @@ var alexrc_default = { allow: [
3374
3375
  ] };
3375
3376
  //#endregion
3376
3377
  //#region src/templates/configs/alexjs/create-config.ts
3377
- const { workflowHeader: workflowHeader$6 } = createHeader({ source: "packages/cli/src/templates/configs/alexjs/create-config.ts" });
3378
+ const { workflowHeader: workflowHeader$7 } = createHeader({ source: "packages/cli/src/templates/configs/alexjs/create-config.ts" });
3378
3379
  function createRcConfig() {
3379
3380
  return JSON.stringify(alexrc_default, null, 2) + "\n";
3380
3381
  }
3381
3382
  function createIgnoreConfig() {
3382
- return `${workflowHeader$6()}${alexignore_default}`;
3383
+ return `${workflowHeader$7()}${alexignore_default}`;
3383
3384
  }
3384
3385
  //#endregion
3385
3386
  //#region src/templates/configs/codecov/codecov.yml
@@ -3423,15 +3424,15 @@ async function readWorkspacePackages(repoRoot) {
3423
3424
  //#endregion
3424
3425
  //#region src/templates/configs/codecov/create-config.ts
3425
3426
  const { scaffoldHeader } = createHeader({ source: "packages/cli/src/templates/configs/codecov/create-config.ts" });
3426
- function createConfig$4(packages) {
3427
+ function createConfig$5(packages) {
3427
3428
  return `${scaffoldHeader()}${codecov_default.trimEnd()}${codecovComponentBlock(packages)}`;
3428
3429
  }
3429
3430
  //#endregion
3430
3431
  //#region src/templates/configs/devmoji/create-config.ts
3431
- const { workflowHeader: workflowHeader$5 } = createHeader({ source: "packages/cli/src/templates/configs/devmoji/create-config.ts" });
3432
- function createConfig$3() {
3432
+ const { workflowHeader: workflowHeader$6 } = createHeader({ source: "packages/cli/src/templates/configs/devmoji/create-config.ts" });
3433
+ function createConfig$4() {
3433
3434
  return [
3434
- workflowHeader$5("cjs"),
3435
+ workflowHeader$6("cjs"),
3435
3436
  `/* eslint-disable */`,
3436
3437
  `const { defineConfig } = require("@theholocron/devmoji-config");`,
3437
3438
  `module.exports = defineConfig();`,
@@ -3443,9 +3444,9 @@ function createConfig$3() {
3443
3444
  var editorconfig_default = "root = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = tab\nindent_size = 4\n\n[.gitattributes]\nindent_style = space\nindent_size = 2\n\n[*.{json,yml,yaml}]\nindent_style = space\nindent_size = 2\n\n[*.{md,mdx}]\ntrim_trailing_whitespace = false\n\n[.*{rc,ignore}]\nindent_style = space\nindent_size = 2\n";
3444
3445
  //#endregion
3445
3446
  //#region src/templates/configs/editorconfig/create-config.ts
3446
- const { workflowHeader: workflowHeader$4 } = createHeader({ source: "packages/cli/src/templates/configs/editorconfig/create-config.ts" });
3447
- function createConfig$2() {
3448
- return `${workflowHeader$4()}${editorconfig_default}`;
3447
+ const { workflowHeader: workflowHeader$5 } = createHeader({ source: "packages/cli/src/templates/configs/editorconfig/create-config.ts" });
3448
+ function createConfig$3() {
3449
+ return `${workflowHeader$5()}${editorconfig_default}`;
3449
3450
  }
3450
3451
  //#endregion
3451
3452
  //#region src/templates/configs/editorconfig-checker/editorconfig-checker.json
@@ -3476,10 +3477,27 @@ var editorconfig_checker_default = {
3476
3477
  };
3477
3478
  //#endregion
3478
3479
  //#region src/templates/configs/editorconfig-checker/create-config.ts
3479
- function createConfig$1() {
3480
+ function createConfig$2() {
3480
3481
  return JSON.stringify(editorconfig_checker_default, null, 2) + "\n";
3481
3482
  }
3482
3483
  //#endregion
3484
+ //#region src/templates/configs/pre-push/pre-push
3485
+ var pre_push_default = "# Run the merge-gating checks locally before pushing — the same set CI runs.\n# Bypass once with: git push --no-verify\n\n# CI runners (semantic-release, super-linter auto-commit, bots) push without a\n# built dev environment — the hook has nothing to add there.\n[ -n \"$CI\" ] && exit 0\n\n__HOLOCRON_SCRIPT__ ci\n";
3486
+ //#endregion
3487
+ //#region src/templates/configs/pre-push/create-config.ts
3488
+ const { workflowHeader: workflowHeader$4 } = createHeader({ source: "packages/cli/src/templates/configs/pre-push/create-config.ts" });
3489
+ /**
3490
+ * `.husky/pre-push` — runs `holocron ci` (the merge-gating checks, in CI order)
3491
+ * before every push. Installed by `holocron setup` when hooks are enabled
3492
+ * (default for `protection: "strict"`). `git push --no-verify` bypasses it.
3493
+ *
3494
+ * @param holocronScript - the command that invokes the CLI. Defaults to
3495
+ * `pnpm exec holocron`; the source repo passes `node packages/cli/dist/cli.mjs`.
3496
+ */
3497
+ function createConfig$1(holocronScript = "pnpm exec holocron") {
3498
+ return `${workflowHeader$4("shebang")}${pre_push_default.replace("__HOLOCRON_SCRIPT__", holocronScript)}`;
3499
+ }
3500
+ //#endregion
3483
3501
  //#region src/templates/configs/prepare-commit-msg/prepare-commit-msg
3484
3502
  var prepare_commit_msg_default = "NAME=$(git config user.name)\nEMAIL=$(git config user.email)\n\nif [ -z \"$NAME\" ]; then\n echo \"empty git config user.name\"\n exit 1\nfi\n\nif [ -z \"$EMAIL\" ]; then\n echo \"empty git config user.email\"\n exit 1\nfi\n\ngit interpret-trailers --if-exists doNothing --trailer \\\n \"Signed-off-by: $NAME <$EMAIL>\" \\\n --in-place \"$1\"\n\nnpx devmoji -e\n";
3485
3503
  //#endregion
@@ -4319,6 +4337,7 @@ async function runSetup(input) {
4319
4337
  const steps = [];
4320
4338
  const repo = config.repo;
4321
4339
  const effectivePreset = repo?.protection;
4340
+ const hooksEnabled = input.hooks ?? (typeof config.hooks === "boolean" ? config.hooks : typeof config.hooks === "object" ? config.hooks.prePush !== false : effectivePreset === "strict");
4322
4341
  print(style.header(`Holocron setup — ${config.name}${dryRun ? " (dry-run)" : ""}`));
4323
4342
  print(style.dim(` config: ${input.loaded.filepath}`));
4324
4343
  print("");
@@ -4454,21 +4473,42 @@ async function runSetup(input) {
4454
4473
  }));
4455
4474
  print(formatStep(steps[steps.length - 1]));
4456
4475
  steps.push(await runStep("source", "write .editorconfig", dryRun, async () => {
4457
- await source.writeRepoFile(".editorconfig", createConfig$2());
4476
+ await source.writeRepoFile(".editorconfig", createConfig$3());
4458
4477
  }));
4459
4478
  print(formatStep(steps[steps.length - 1]));
4460
4479
  steps.push(await runStep("source", "write .editorconfig-checker.json", dryRun, async () => {
4461
- await source.writeRepoFile(".editorconfig-checker.json", createConfig$1());
4480
+ await source.writeRepoFile(".editorconfig-checker.json", createConfig$2());
4462
4481
  }));
4463
4482
  print(formatStep(steps[steps.length - 1]));
4464
4483
  steps.push(await runStep("source", "write devmoji.config.cjs", dryRun, async () => {
4465
- await source.writeRepoFile("devmoji.config.cjs", createConfig$3());
4484
+ await source.writeRepoFile("devmoji.config.cjs", createConfig$4());
4466
4485
  }));
4467
4486
  print(formatStep(steps[steps.length - 1]));
4468
4487
  steps.push(await runStep("source", "write .husky/prepare-commit-msg", dryRun, async () => {
4469
4488
  await source.writeRepoFile(".husky/prepare-commit-msg", createConfig());
4470
4489
  }));
4471
4490
  print(formatStep(steps[steps.length - 1]));
4491
+ if (hooksEnabled) {
4492
+ steps.push(await runStep("source", "write .husky/pre-push", dryRun, async () => {
4493
+ await source.writeRepoFile(".husky/pre-push", createConfig$1(config.holocronScript));
4494
+ return "runs `holocron ci` before push — bypass with `git push --no-verify`";
4495
+ }));
4496
+ print(formatStep(steps[steps.length - 1]));
4497
+ if (config.syncScripts !== false) {
4498
+ steps.push(await runStep("source", "set package.json prepare script", dryRun, async () => {
4499
+ return await ensurePrepareScript(input.context.repoRoot) ? "prepare = \"husky\"" : "already set";
4500
+ }));
4501
+ print(formatStep(steps[steps.length - 1]));
4502
+ }
4503
+ } else {
4504
+ steps.push({
4505
+ capability: "source",
4506
+ step: "write .husky/pre-push",
4507
+ status: "skip",
4508
+ message: "git hooks disabled"
4509
+ });
4510
+ print(formatStep(steps[steps.length - 1]));
4511
+ }
4472
4512
  {
4473
4513
  const hasTestWorkflow = (config.tasks ?? []).map((e) => typeof e === "string" ? e : e.name).includes("test");
4474
4514
  const packages = await readWorkspacePackages(input.context.repoRoot);
@@ -4480,7 +4520,7 @@ async function runSetup(input) {
4480
4520
  message: "no test workflow configured"
4481
4521
  });
4482
4522
  else steps.push(await runStep("source", "write codecov.yml", dryRun, async () => {
4483
- const content = existing != null ? mergeCodecovComponents(existing, packages) : createConfig$4(packages);
4523
+ const content = existing != null ? mergeCodecovComponents(existing, packages) : createConfig$5(packages);
4484
4524
  await source.writeRepoFile("codecov.yml", content);
4485
4525
  return packages.length > 0 ? `${packages.length} components` : "no components";
4486
4526
  }));
@@ -4854,6 +4894,27 @@ async function runSetup(input) {
4854
4894
  summary
4855
4895
  };
4856
4896
  }
4897
+ /**
4898
+ * Ensure `package.json#scripts.prepare` is `"husky"` so git hooks register on
4899
+ * install. Merge-only: leaves every other script untouched. Returns `true` when
4900
+ * it wrote a change, `false` when already set or there is no `package.json`.
4901
+ */
4902
+ async function ensurePrepareScript(repoRoot) {
4903
+ const pkgPath = join(repoRoot, "package.json");
4904
+ let content;
4905
+ try {
4906
+ content = await readFile(pkgPath, "utf8");
4907
+ } catch {
4908
+ return false;
4909
+ }
4910
+ const pkg = JSON.parse(content);
4911
+ const scripts = pkg.scripts ?? {};
4912
+ if (scripts.prepare === "husky") return false;
4913
+ scripts.prepare = "husky";
4914
+ pkg.scripts = scripts;
4915
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
4916
+ return true;
4917
+ }
4857
4918
  //#endregion
4858
4919
  //#region src/commands/skills.ts
4859
4920
  /**
@@ -6834,6 +6895,9 @@ try {
6834
6895
  }).command("setup", "Apply infra setup actions across every configured capability", (y) => y.option("repo", {
6835
6896
  type: "string",
6836
6897
  describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
6898
+ }).option("hooks", {
6899
+ type: "boolean",
6900
+ describe: "Install git hooks (.husky/pre-push → holocron ci). Default: on for protection:'strict'. --no-hooks to skip."
6837
6901
  }), async (argv) => {
6838
6902
  const tokens = tokenContext(argv.token);
6839
6903
  if (!tokens) return;
@@ -6847,7 +6911,8 @@ try {
6847
6911
  ...argv.repo ? { repo: argv.repo } : {},
6848
6912
  ...tokens,
6849
6913
  org: resolveOrg(argv, loaded.resolved)
6850
- }
6914
+ },
6915
+ ...argv.hooks !== void 0 ? { hooks: argv.hooks } : {}
6851
6916
  })).summary.fail > 0) process.exitCode = 1;
6852
6917
  }).command("skills", "Manage agent skills from the @theholocron/skills registry", (y) => y.command("install", "Copy skills from @theholocron/skills into .agents/ with agent symlinks", () => {}, async (argv) => {
6853
6918
  await runSkillsInstall({