@theholocron/cli 3.47.0 → 3.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -5,36 +5,33 @@ import path, { basename, dirname, join, relative, resolve } from "node:path";
5
5
  import { checkbox, input, select } from "@inquirer/prompts";
6
6
  import yargs from "yargs";
7
7
  import { hideBin } from "yargs/helpers";
8
- import { AuthError, ProviderApiError, ProviderApiError as ProviderApiError$1 } from "@theholocron/http-client";
8
+ import { AuthError, ProviderApiError, ProviderApiError as ProviderApiError$1, createRestClient } from "@theholocron/http-client";
9
+ import { createEnvLookup } from "@theholocron/env-utils";
9
10
  import { Entry, findCredentials } from "@napi-rs/keyring";
10
11
  import { pathToFileURL } from "node:url";
11
12
  import ora from "ora";
12
13
  import chalk from "chalk";
13
14
  import { execFile, execFileSync, spawnSync } from "node:child_process";
14
15
  import { homedir } from "node:os";
15
- import { createHash } from "node:crypto";
16
16
  import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
17
+ import { createHash } from "node:crypto";
17
18
  import { generateReadme } from "@theholocron/components-doc/markdown";
18
19
  import { getClients, getConfigs, getDocs, getPlugins, getSkills, getThemes, getUtils } from "@theholocron/registry-doc";
19
20
  import { createGitHubClient } from "@theholocron/github-client";
20
21
  import { promisify } from "node:util";
21
22
  import * as Sentry from "@sentry/node";
22
23
  //#region src/env.ts
23
- function createEnvLookup(source = process.env) {
24
- return {
25
- get(key) {
26
- return source[key] || void 0;
27
- },
28
- first(...keys) {
29
- for (const key of keys) {
30
- const val = source[key];
31
- if (val) return val;
32
- }
33
- }
34
- };
24
+ /** Singleton env for simple global lookups throughout the CLI. */
25
+ const env = createEnvLookup();
26
+ /**
27
+ * Create an injectable env for commands that accept a fake env in tests.
28
+ * Pass `input.env` when available, falls back to `process.env`.
29
+ */
30
+ function makeEnv(source) {
31
+ return createEnvLookup(source);
35
32
  }
36
33
  //#endregion
37
- //#region src/keyring.ts
34
+ //#region src/auth/keyring.ts
38
35
  /**
39
36
  * Keyring-backed bootstrap credential store.
40
37
  *
@@ -104,7 +101,7 @@ function listStoredProviders() {
104
101
  }
105
102
  }
106
103
  //#endregion
107
- //#region src/auth-resolver.ts
104
+ //#region src/auth/auth-resolver.ts
108
105
  /**
109
106
  * Build a strict, single-feature token resolver.
110
107
  *
@@ -114,7 +111,7 @@ function listStoredProviders() {
114
111
  */
115
112
  function createFeatureResolver(config) {
116
113
  return function resolveFeatureToken(input = {}) {
117
- const env = createEnvLookup(input.env);
114
+ const env = makeEnv(input.env);
118
115
  const keyring = input.keyring ?? getToken;
119
116
  const token = input.cliToken || env.get(config.envName) || keyring(config.keyringKey);
120
117
  if (!token) throw new AuthError(`no GitHub token found for this operation. Pass --token <PAT>, set ${config.envName}, or run: holocron auth set ${config.keyringKey} <PAT>`);
@@ -122,7 +119,45 @@ function createFeatureResolver(config) {
122
119
  };
123
120
  }
124
121
  //#endregion
125
- //#region src/capabilities/index.ts
122
+ //#region src/auth/token-args.ts
123
+ var TokenParseError = class extends Error {
124
+ name = "TokenParseError";
125
+ };
126
+ /**
127
+ * Converts raw --token CLI values into a typed result.
128
+ *
129
+ * Bare form: --token ghp_xxx → { cliToken: "ghp_xxx" }
130
+ * Keyed form: --token github=ghp_xxx → { cliTokens: { github: "ghp_xxx" } }
131
+ * Mixed: --token github=ghp_xxx --token v_yyy
132
+ * → { cliToken: "v_yyy", cliTokens: { github: "ghp_xxx" } }
133
+ *
134
+ * Values may contain "=" (e.g. base64 strings) — only the first "=" is treated as a separator.
135
+ */
136
+ function parseTokenArgs(tokens) {
137
+ if (tokens.length === 0) return {};
138
+ const cliTokens = {};
139
+ const bare = [];
140
+ for (const raw of tokens) {
141
+ const eqIdx = raw.indexOf("=");
142
+ if (eqIdx === -1) {
143
+ bare.push(raw);
144
+ continue;
145
+ }
146
+ const vendor = raw.slice(0, eqIdx);
147
+ const value = raw.slice(eqIdx + 1);
148
+ if (vendor.trim() === "") throw new TokenParseError(`invalid --token value "${raw}": vendor name must not be empty`);
149
+ if (/\s/.test(vendor)) throw new TokenParseError(`invalid --token value "${raw}": vendor name must not contain whitespace`);
150
+ if (value === "") throw new TokenParseError(`invalid --token value "${raw}": token value must not be empty`);
151
+ cliTokens[vendor] = value;
152
+ }
153
+ if (bare.length > 1) throw new TokenParseError(`only one bare --token value is allowed; got ${bare.length.toString()} — use vendor=value form for multiple tokens`);
154
+ const result = {};
155
+ if (bare.length === 1) result.cliToken = bare[0];
156
+ if (Object.keys(cliTokens).length > 0) result.cliTokens = cliTokens;
157
+ return result;
158
+ }
159
+ //#endregion
160
+ //#region src/plugin/capabilities.ts
126
161
  const CARDINALITY = {
127
162
  source: "single",
128
163
  ci: "single",
@@ -148,7 +183,7 @@ const CARDINALITY = {
148
183
  */
149
184
  const REQUIRED_CAPABILITIES = [];
150
185
  //#endregion
151
- //#region src/config.ts
186
+ //#region src/config/config.ts
152
187
  /**
153
188
  * `holocron.config.json` schema, parser, and provider resolution.
154
189
  *
@@ -336,10 +371,10 @@ const defaultImporter$1 = async (pkg) => {
336
371
  * pulling FROM it would just re-store the same value.
337
372
  */
338
373
  function resolveAuthSetToken(input) {
339
- const env = input.env ?? process.env;
374
+ const e = makeEnv(input.env);
340
375
  const upper = input.provider.toUpperCase();
341
376
  const holocronKey = `HOLOCRON_${upper}_TOKEN`;
342
- return input.positional || env[holocronKey] || env[upper + "_TOKEN"] || null;
377
+ return input.positional || e.get(holocronKey) || e.get(`${upper}_TOKEN`) || null;
343
378
  }
344
379
  async function runAuthSet(input) {
345
380
  const print = input.print ?? ((l) => console.log(l));
@@ -499,7 +534,7 @@ async function tryLoadHint(importer, packageName) {
499
534
  }
500
535
  }
501
536
  //#endregion
502
- //#region src/loader.ts
537
+ //#region src/plugin/loader.ts
503
538
  /**
504
539
  * `PluginLoader` — loads provider plugins per the resolved config and
505
540
  * builds a typed capability registry the runtime can query.
@@ -1324,11 +1359,11 @@ async function runNew(input) {
1324
1359
  const setupEnv = {};
1325
1360
  const adminToken = input.token ?? keychainLookup("github.admin");
1326
1361
  if (adminToken) setupArgs.push("--token", adminToken);
1327
- if (!process.env["HOLOCRON_ORG_TOKEN"]) {
1362
+ if (!env.get("HOLOCRON_ORG_TOKEN")) {
1328
1363
  const orgToken = keychainLookup("github.org");
1329
1364
  if (orgToken) setupEnv["HOLOCRON_ORG_TOKEN"] = orgToken;
1330
1365
  }
1331
- if (!process.env["HOLOCRON_DEPLOY_TOKEN"]) {
1366
+ if (!env.get("HOLOCRON_DEPLOY_TOKEN")) {
1332
1367
  const deployToken = keychainLookup("github.deploy");
1333
1368
  if (deployToken) setupEnv["HOLOCRON_DEPLOY_TOKEN"] = deployToken;
1334
1369
  }
@@ -1459,7 +1494,7 @@ async function runNpmPublishInitial(input = {}) {
1459
1494
  const tag = input.tag ?? "alpha";
1460
1495
  const dryRun = input.dryRun ?? false;
1461
1496
  const otp = input.otp;
1462
- const env = input.env ?? process.env;
1497
+ const env = makeEnv(input.env);
1463
1498
  const exec = input.exec ?? defaultExec$2;
1464
1499
  const publishArgs = [
1465
1500
  "-r",
@@ -1554,7 +1589,7 @@ function printNextSteps$1(print, env, packageNames, repoName) {
1554
1589
  print(" → next: configure Trusted Publisher for each package on npm:");
1555
1590
  for (const name of packageNames) print(` https://www.npmjs.com/package/${name}/access`);
1556
1591
  print(` Publisher: GitHub Actions Org: theholocron Repo: ${repoName} Workflow: release.yml`);
1557
- if (env.NPM_TOKEN) {
1592
+ if (env.get("NPM_TOKEN")) {
1558
1593
  print("");
1559
1594
  print(" → cleanup: $NPM_TOKEN was used. Revoke it now (no API for self-revoke; UI-only):");
1560
1595
  print(" https://www.npmjs.com/settings/~/tokens");
@@ -2689,9 +2724,8 @@ async function runSecretSet(input) {
2689
2724
  async function resolveValue(input) {
2690
2725
  if (input.value) return input.value;
2691
2726
  if (input.fromStdin) return (await (input.readStdin ?? defaultReadStdin)()).replace(/\r?\n$/, "");
2692
- const env = process.env;
2693
- if (input.fromEnv) return env[input.fromEnv];
2694
- return env[input.name];
2727
+ if (input.fromEnv) return env.get(input.fromEnv);
2728
+ return env.get(input.name);
2695
2729
  }
2696
2730
  async function defaultReadStdin() {
2697
2731
  const chunks = [];
@@ -2815,19 +2849,547 @@ function vaultProviderName(loader) {
2815
2849
  return loader.get("vault").providerName;
2816
2850
  }
2817
2851
  //#endregion
2818
- //#region src/agent-prompts.ts
2852
+ //#region src/commands/setup/labels.ts
2853
+ const CANONICAL_LABELS = [
2854
+ {
2855
+ name: "bug",
2856
+ color: "d73a4a",
2857
+ description: "Something isn't working"
2858
+ },
2859
+ {
2860
+ name: "chore",
2861
+ color: "ededed",
2862
+ description: "Maintenance, no user-facing change"
2863
+ },
2864
+ {
2865
+ name: "ci",
2866
+ color: "0075ca",
2867
+ description: "CI/CD pipeline changes"
2868
+ },
2869
+ {
2870
+ name: "dependencies",
2871
+ color: "0366d6",
2872
+ description: "Dependency update"
2873
+ },
2874
+ {
2875
+ name: "documentation",
2876
+ color: "0075ca",
2877
+ description: "Documentation only"
2878
+ },
2879
+ {
2880
+ name: "duplicate",
2881
+ color: "cfd3d7",
2882
+ description: "Already reported"
2883
+ },
2884
+ {
2885
+ name: "enhancement",
2886
+ color: "a2eeef",
2887
+ description: "New feature or request"
2888
+ },
2889
+ {
2890
+ name: "good first issue",
2891
+ color: "7057ff",
2892
+ description: "Good for newcomers"
2893
+ },
2894
+ {
2895
+ name: "help wanted",
2896
+ color: "008672",
2897
+ description: "Extra attention needed"
2898
+ },
2899
+ {
2900
+ name: "invalid",
2901
+ color: "e4e669",
2902
+ description: "Doesn't seem right"
2903
+ },
2904
+ {
2905
+ name: "performance",
2906
+ color: "fbca04",
2907
+ description: "Performance improvement"
2908
+ },
2909
+ {
2910
+ name: "question",
2911
+ color: "d876e3",
2912
+ description: "Further information requested"
2913
+ },
2914
+ {
2915
+ name: "refactor",
2916
+ color: "cfd3d7",
2917
+ description: "Code restructuring"
2918
+ },
2919
+ {
2920
+ name: "released",
2921
+ color: "ededed",
2922
+ description: "Included in a release"
2923
+ },
2924
+ {
2925
+ name: "test",
2926
+ color: "bfd4f2",
2927
+ description: "Test-related changes"
2928
+ },
2929
+ {
2930
+ name: "triage",
2931
+ color: "e4e669",
2932
+ description: "Needs investigation"
2933
+ },
2934
+ {
2935
+ name: "wontfix",
2936
+ color: "ffffff",
2937
+ description: "Won't be addressed"
2938
+ }
2939
+ ];
2940
+ const STALE_LABELS = [
2941
+ "github_actions",
2942
+ "javascript",
2943
+ "autorelease: pending",
2944
+ "autorelease: tagged",
2945
+ "released on @alpha"
2946
+ ];
2947
+ //#endregion
2948
+ //#region src/templates/config.yml
2949
+ var config_default = "# Configuration for sentiment-bot - https://github.com/behaviorbot/sentiment-bot\n\n# *Required* toxicity threshold between 0 and .99 with the higher numbers being the most toxic\n# Anything higher than this threshold will be marked as toxic and commented on\nsentimentBotToxicityThreshold: .7\n\n# *Required* Comment to reply with\nsentimentBotReplyComment: >\n Please be sure to review the [Code of Conduct](https://docs.theholocron.dev/reference/code-of-conduct/) and be respectful of other users.\n";
2950
+ //#endregion
2951
+ //#region src/utils/create-header.ts
2952
+ function createHeader(options) {
2953
+ const { source, tool = "holocron setup", forPrimary = false } = options;
2954
+ const doNotEdit = forPrimary ? `AUTO-GENERATED — do not edit in theholocron/.github directly.` : `AUTO-GENERATED — do not edit directly.`;
2955
+ return {
2956
+ workflowHeader(format = "yaml") {
2957
+ if (format === "cjs") return [
2958
+ `/* ${doNotEdit}`,
2959
+ ` * Source: theholocron/holocron · ${source}`,
2960
+ ` * Tool: ${tool}`,
2961
+ ` * Changes: edit source in theholocron/holocron`,
2962
+ ` */`,
2963
+ ``
2964
+ ].join("\n");
2965
+ const yamlLines = [
2966
+ `# ${doNotEdit}`,
2967
+ `# Source: theholocron/holocron · ${source}`,
2968
+ `# Tool: ${tool}`,
2969
+ `# Changes: edit source in theholocron/holocron`,
2970
+ ``
2971
+ ].join("\n");
2972
+ if (format === "shebang") return `#!/bin/sh\n\n${yamlLines}`;
2973
+ return yamlLines;
2974
+ },
2975
+ scaffoldHeader() {
2976
+ return [
2977
+ `# Scaffolded by holocron setup — edit this file freely.`,
2978
+ `# Source: theholocron/holocron · ${source}`,
2979
+ ``
2980
+ ].join("\n");
2981
+ }
2982
+ };
2983
+ }
2984
+ //#endregion
2985
+ //#region src/templates/configs/alexjs/alexignore
2986
+ var alexignore_default = ".github/*\nCHANGELOG.md\nLICENSE\n";
2987
+ //#endregion
2988
+ //#region src/templates/configs/alexjs/alexrc.json
2989
+ var alexrc_default = { allow: [
2990
+ "dead",
2991
+ "failure",
2992
+ "failures",
2993
+ "hook",
2994
+ "hooks",
2995
+ "husky",
2996
+ "period"
2997
+ ] };
2998
+ //#endregion
2999
+ //#region src/templates/configs/alexjs/create-config.ts
3000
+ const { workflowHeader: workflowHeader$6 } = createHeader({ source: "packages/cli/src/templates/configs/alexjs/create-config.ts" });
3001
+ function createRcConfig() {
3002
+ return JSON.stringify(alexrc_default, null, 2) + "\n";
3003
+ }
3004
+ function createIgnoreConfig() {
3005
+ return `${workflowHeader$6()}${alexignore_default}`;
3006
+ }
3007
+ //#endregion
3008
+ //#region src/templates/configs/codecov/codecov.yml
3009
+ 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 patch:\n default:\n target: 80%\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 individual_components:\n";
3010
+ //#endregion
3011
+ //#region src/templates/configs/codecov/utils.ts
3012
+ const INDIVIDUAL_COMPONENTS_MARKER = " individual_components:";
3013
+ function codecovComponentBlock(packages) {
3014
+ if (packages.length === 0) return "\n []\n";
3015
+ return "\n" + packages.flatMap(({ slug }) => [
3016
+ ` - component_id: ${slug}`,
3017
+ ` name: "${slug}"`,
3018
+ ` paths:`,
3019
+ ` - packages/${slug}/**`,
3020
+ ``
3021
+ ]).join("\n");
3022
+ }
3023
+ function mergeCodecovComponents(existing, packages) {
3024
+ const idx = existing.indexOf(INDIVIDUAL_COMPONENTS_MARKER);
3025
+ if (idx === -1) return existing;
3026
+ return existing.slice(0, idx + 24) + codecovComponentBlock(packages);
3027
+ }
3028
+ async function readWorkspacePackages(repoRoot) {
3029
+ const packagesDir = join(repoRoot, "packages");
3030
+ const entries = await readdir(packagesDir, { withFileTypes: true }).catch(() => null);
3031
+ if (!entries) return [];
3032
+ const packages = [];
3033
+ for (const entry of entries) {
3034
+ if (!entry.isDirectory()) continue;
3035
+ try {
3036
+ const raw = await readFile(join(packagesDir, entry.name, "package.json"), "utf8");
3037
+ const pkg = JSON.parse(raw);
3038
+ if (typeof pkg.name === "string") packages.push({
3039
+ slug: entry.name,
3040
+ name: pkg.name
3041
+ });
3042
+ } catch {}
3043
+ }
3044
+ return packages.sort((a, b) => a.slug.localeCompare(b.slug));
3045
+ }
3046
+ //#endregion
3047
+ //#region src/templates/configs/codecov/create-config.ts
3048
+ const { scaffoldHeader } = createHeader({ source: "packages/cli/src/templates/configs/codecov/create-config.ts" });
3049
+ function createConfig$4(packages) {
3050
+ return `${scaffoldHeader()}${codecov_default.trimEnd()}${codecovComponentBlock(packages)}`;
3051
+ }
3052
+ //#endregion
3053
+ //#region src/templates/configs/devmoji/create-config.ts
3054
+ const { workflowHeader: workflowHeader$5 } = createHeader({ source: "packages/cli/src/templates/configs/devmoji/create-config.ts" });
3055
+ function createConfig$3() {
3056
+ return [
3057
+ workflowHeader$5("cjs"),
3058
+ `/* eslint-disable */`,
3059
+ `const { defineConfig } = require("@theholocron/devmoji-config");`,
3060
+ `module.exports = defineConfig();`,
3061
+ ``
3062
+ ].join("\n");
3063
+ }
3064
+ //#endregion
3065
+ //#region src/templates/configs/editorconfig/editorconfig
3066
+ 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";
3067
+ //#endregion
3068
+ //#region src/templates/configs/editorconfig/create-config.ts
3069
+ const { workflowHeader: workflowHeader$4 } = createHeader({ source: "packages/cli/src/templates/configs/editorconfig/create-config.ts" });
3070
+ function createConfig$2() {
3071
+ return `${workflowHeader$4()}${editorconfig_default}`;
3072
+ }
3073
+ //#endregion
3074
+ //#region src/templates/configs/editorconfig-checker/editorconfig-checker.json
3075
+ var editorconfig_checker_default = {
3076
+ Version: "v3.7.0",
3077
+ Verbose: false,
3078
+ Format: "",
3079
+ Debug: false,
3080
+ IgnoreDefaults: false,
3081
+ SpacesAfterTabs: false,
3082
+ NoColor: false,
3083
+ Exclude: [
3084
+ "(^|.+/)LICENSE$",
3085
+ "^public/.*",
3086
+ "\\.md$",
3087
+ "\\.mdx$"
3088
+ ],
3089
+ AllowedContentTypes: [],
3090
+ PassedFiles: [],
3091
+ Disable: {
3092
+ "EndOfLine": false,
3093
+ "Indentation": false,
3094
+ "InsertFinalNewline": false,
3095
+ "TrimTrailingWhitespace": false,
3096
+ "IndentSize": false,
3097
+ "MaxLineLength": false
3098
+ }
3099
+ };
3100
+ //#endregion
3101
+ //#region src/templates/configs/editorconfig-checker/create-config.ts
3102
+ function createConfig$1() {
3103
+ return JSON.stringify(editorconfig_checker_default, null, 2) + "\n";
3104
+ }
3105
+ //#endregion
3106
+ //#region src/templates/configs/prepare-commit-msg/prepare-commit-msg
3107
+ 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";
3108
+ //#endregion
3109
+ //#region src/templates/configs/prepare-commit-msg/create-config.ts
3110
+ const { workflowHeader: workflowHeader$3 } = createHeader({ source: "packages/cli/src/templates/configs/prepare-commit-msg/create-config.ts" });
3111
+ function createConfig() {
3112
+ return `${workflowHeader$3("shebang")}${prepare_commit_msg_default}`;
3113
+ }
3114
+ //#endregion
3115
+ //#region src/templates/dco.yml
3116
+ var dco_default = "allowRemediationCommits:\n individual: true\n";
3117
+ //#endregion
3118
+ //#region src/templates/dependabot.yml
3119
+ var dependabot_default = "version: 2\nupdates:\n - package-ecosystem: npm\n directory: /\n schedule:\n interval: weekly\n commit-message:\n prefix: \"chore(deps)\"\n prefix-development: \"chore(deps-dev)\"\n groups:\n security-patches:\n applies-to: security-updates\n patterns:\n - \"*\"\n all-dependencies:\n update-types:\n - minor\n - patch\n\n - package-ecosystem: github-actions\n directory: /\n schedule:\n interval: weekly\n commit-message:\n prefix: \"chore(deps)\"\n groups:\n all-actions:\n patterns:\n - \"*\"\n";
3120
+ //#endregion
3121
+ //#region src/templates/labeler.yml
3122
+ var labeler_default = "bug:\n - '^fix'\n\nchore:\n - '^chore(?!\\(deps)'\n\nci:\n - '^ci'\n\ndependencies:\n - '^chore\\(deps'\n\ndocumentation:\n - '^docs'\n\nenhancement:\n - '^feat'\n\nperformance:\n - '^perf'\n\nrefactor:\n - '^refactor'\n\ntest:\n - '^test'\n";
3123
+ //#endregion
3124
+ //#region src/commands/setup-workflows/index.ts
2819
3125
  /**
2820
- * Canonical AI engineering workflow role prompts.
3126
+ * Thin workflow wrapper templates for `holocron setup`.
2821
3127
  *
2822
- * Written to `.agents/prompts/<role>.md` by `holocron setup` when `agent` is
2823
- * configured. Paths are gitignored and regenerated on every setup run so the
2824
- * content always reflects the current CLI version.
3128
+ * Each entry is a complete `.github/workflows/<name>.yml` that delegates
3129
+ * to the corresponding reusable `ci-<name>.yml` in `theholocron/.github`.
3130
+ * Files are overwritten on each setup run — they are generated artifacts.
3131
+ */
3132
+ const WORKFLOW_TEMPLATES = {
3133
+ lint: "name: Lint\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: lint-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n issues: write\n statuses: write\n\njobs:\n lint:\n name: Lint\n uses: theholocron/.github/.github/workflows/lint.yml@main\n secrets: inherit\n",
3134
+ test: "name: Test\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: test-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n id-token: write\n statuses: write\n\njobs:\n test:\n name: Test\n uses: theholocron/.github/.github/workflows/test.yml@main\n with:\n run-unit: true\n secrets: inherit\n",
3135
+ typecheck: "name: Typecheck\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: typecheck-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n typecheck:\n name: Typecheck\n uses: theholocron/.github/.github/workflows/typecheck.yml@main\n secrets: inherit\n",
3136
+ security: "name: Security\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n schedule:\n - cron: \"0 0 * * 1\"\n\npermissions:\n actions: read\n contents: read\n security-events: write\n\njobs:\n security:\n uses: theholocron/.github/.github/workflows/security.yml@main\n secrets: inherit\n",
3137
+ 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",
3138
+ 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: Review\n uses: theholocron/.github/.github/workflows/review.yml@main\n secrets: inherit\n",
3139
+ release: "name: Release\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n - alpha\n workflow_dispatch:\n inputs:\n dry_run:\n description: >\n Dry run — analyze commits and preview the release without git writes\n or publish. Push-triggered runs always run fully; this only applies\n to manual workflow_dispatch triggers.\n required: false\n default: true\n type: boolean\n\npermissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n release:\n uses: theholocron/.github/.github/workflows/release.yml@main\n with:\n dry-run: ${{ inputs.dry_run == true }}\n secrets: inherit\n",
3140
+ 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 secrets: inherit\n",
3141
+ sync: "name: Sync\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/sync.yml@main\n with:\n steps: ${{ inputs.steps }}\n secrets: inherit\n",
3142
+ greetings: "name: Greetings\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 uses: theholocron/.github/.github/workflows/greetings.yml@main\n secrets: inherit\n",
3143
+ dependencies: "name: Dependencies\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n dependencies:\n uses: theholocron/.github/.github/workflows/dependencies.yml@main\n secrets: inherit\n",
3144
+ bookkeeping: "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n pull_request:\n types:\n - opened\n - edited\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\n\njobs:\n bookkeeping:\n uses: theholocron/.github/.github/workflows/bookkeeping.yml@main\n secrets: inherit\n",
3145
+ audit: "name: Audit\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n audit:\n uses: theholocron/.github/.github/workflows/audit.yml@main\n secrets: inherit\n",
3146
+ deploy: "name: Deploy\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n workflow_dispatch:\n\nconcurrency:\n group: pages\n cancel-in-progress: false\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\njobs:\n deploy:\n name: Deploy\n uses: theholocron/.github/.github/workflows/deploy.yml@main\n secrets: inherit\n",
3147
+ wiki: "name: Wiki\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/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/wiki.yml@main\n with:\n preview: true\n preview-id: pr-${{ github.event.pull_request.number }}\n secrets: inherit\n"
3148
+ };
3149
+ const KNOWN_WORKFLOWS = new Set(Object.keys(WORKFLOW_TEMPLATES));
3150
+ /**
3151
+ * GitHub check context name each CI workflow produces on a PR.
2825
3152
  *
2826
- * Source: .notes/ai-engineering-workflow.spec.md
3153
+ * The format is "{caller-workflow-name} / {reusable-job-name}". The caller
3154
+ * job's own `name:` field does NOT appear in the external check name — only
3155
+ * the calling workflow's top-level `name:` and the inner reusable-workflow
3156
+ * job name matter. Only workflows that gate merges are listed here.
2827
3157
  */
2828
- const DECISIONS_TEMPLATE = "---\nid: ADR-XXXX\ntitle: \"\"\nstatus: proposed\ndate: YYYY-MM-DD\nowners: []\nspecs: []\ndiscussion:\n github:\nsupersedes: []\nsuperseded-by: []\ntags: []\n---\n\n# [Short title of the decision]\n\n- Status: [proposed | accepted | rejected | deprecated | superseded by ADR-XXXX]\n- Date: YYYY-MM-DD\n\n## Context and Problem Statement\n\n2–3 sentences describing the situation that forced this decision.\n\n## Decision Drivers\n\n- [driver 1 — a constraint, goal, or value]\n- [driver 2]\n\n## Considered Options\n\n- [Option A]\n- [Option B]\n- [Option C — do nothing]\n\n## Decision Outcome\n\nChosen option: **[Option A]**, because [one-sentence justification].\n\n### Positive Consequences\n\n- …\n\n### Negative Consequences\n\n- …\n\n## Pros and Cons of the Options\n\n### [Option A]\n\n- Good, because [argument]\n- Bad, because [argument]\n\n### [Option B]\n\n- Good, because [argument]\n- Bad, because [argument]\n";
2829
- const AGENT_PROMPTS = {
2830
- "discovery.md": `# Discovery Agent
3158
+ const WORKFLOW_CHECK_CONTEXTS = {
3159
+ lint: "Lint / Lint entire codebase",
3160
+ test: "Test / Run tests and collect coverage",
3161
+ typecheck: "Typecheck / tsc --noEmit"
3162
+ };
3163
+ /**
3164
+ * Generate the thin caller content for a workflow, optionally injecting or
3165
+ * merging `with:` overrides into the jobs block.
3166
+ *
3167
+ * Two strategies are used depending on the template:
3168
+ * - Templates that already have a `with:` block (e.g. lint):
3169
+ * the override entries are merged in, replacing existing keys and appending
3170
+ * new ones.
3171
+ * - Templates that end with ` secrets: inherit`: a new `with:` block is
3172
+ * injected immediately before `secrets: inherit`.
3173
+ * If neither pattern matches the template, a warning is emitted and the
3174
+ * base template is returned unchanged.
3175
+ */
3176
+ function generateThinCallerContent(name, withOverrides, additionalPaths) {
3177
+ const base = WORKFLOW_TEMPLATES[name];
3178
+ if (!base) return "";
3179
+ const yamlScalar = (v) => {
3180
+ if (v === true) return "true";
3181
+ if (v === false) return "false";
3182
+ const s = String(v);
3183
+ return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
3184
+ };
3185
+ const fmt = (k, v) => ` ${k}: ${yamlScalar(v)}`;
3186
+ let result = base;
3187
+ if (additionalPaths && additionalPaths.length > 0) {
3188
+ const pathsBlockRe = /( {4}paths:\n)((?:[ ]{6}- [^\n]+\n)+)/;
3189
+ if (pathsBlockRe.test(result)) result = result.replace(pathsBlockRe, (_, header, existing) => {
3190
+ const existingPaths = new Set([...existing.matchAll(/- (.+)/g)].map((m) => m[1]));
3191
+ const newEntries = additionalPaths.filter((p) => !existingPaths.has(p)).map((p) => ` - ${p}\n`).join("");
3192
+ return header + existing + newEntries;
3193
+ });
3194
+ else {
3195
+ const pathsBlock = ` paths:\n${additionalPaths.map((p) => ` - ${p}\n`).join("")}`;
3196
+ result = result.replace(/( {4}branches: \[main\]\n)/, `$1${pathsBlock}`);
3197
+ }
3198
+ }
3199
+ if (!withOverrides || Object.keys(withOverrides).length === 0) return result;
3200
+ const withBlockRe = /( {4}with:\n)((?:[ ]{6}[^\n]+\n)*)/;
3201
+ const existingMatch = result.match(withBlockRe);
3202
+ if (existingMatch) {
3203
+ const existingEntries = new Map(existingMatch[2].split("\n").filter(Boolean).map((line) => {
3204
+ const m = line.match(/^ {6}([^:]+):\s*(.*)/);
3205
+ return m ? [m[1].trim(), m[2].trim()] : null;
3206
+ }).filter((e) => e !== null));
3207
+ for (const [k, v] of Object.entries(withOverrides)) existingEntries.set(k, yamlScalar(v));
3208
+ const merged = [...existingEntries.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n");
3209
+ return result.replace(withBlockRe, ` with:\n${merged}\n`);
3210
+ }
3211
+ const withBlock = Object.entries(withOverrides).map(([k, v]) => fmt(k, v)).join("\n");
3212
+ const injected = result.replace(/ {4}secrets: inherit\n$/, ` with:\n${withBlock}\n secrets: inherit\n`);
3213
+ if (injected === result) console.warn(`[generateThinCallerContent] could not inject with: overrides into "${name}" template`);
3214
+ return injected;
3215
+ }
3216
+ /**
3217
+ * Extract the Cloudflare Pages preview config from a deploy workflow's `with:` object.
3218
+ *
3219
+ * Accepts three forms:
3220
+ * - `preview: true` — derive both project and domain from org context
3221
+ * - `preview: { project: "..." }` — explicit project; domain derived from context if omitted
3222
+ * - `preview: { project: "...", domain: "..." }` — fully explicit
3223
+ *
3224
+ * Returns null when `preview:` is absent, false, or can't be resolved.
3225
+ */
3226
+ function extractPreviewConfig(raw, ctx = {}) {
3227
+ const preview = raw["preview"];
3228
+ if (!preview) return null;
3229
+ if (preview === true) {
3230
+ const project = ctx.org ? `${ctx.org}-preview` : null;
3231
+ const domain = ctx.domain ? `preview.${ctx.domain}` : void 0;
3232
+ if (!project) return null;
3233
+ return {
3234
+ project,
3235
+ ...domain ? { domain } : {}
3236
+ };
3237
+ }
3238
+ if (typeof preview !== "object") return null;
3239
+ const p = preview;
3240
+ const project = typeof p["project"] === "string" && p["project"] ? p["project"] : ctx.org ? `${ctx.org}-preview` : null;
3241
+ if (!project) return null;
3242
+ const domain = typeof p["domain"] === "string" && p["domain"] ? p["domain"] : ctx.domain ? `preview.${ctx.domain}` : void 0;
3243
+ return {
3244
+ project,
3245
+ ...domain ? { domain } : {}
3246
+ };
3247
+ }
3248
+ /**
3249
+ * Generate the full thin-caller YAML for a `deploy.yml` that handles both
3250
+ * production (push to main → GitHub Pages) and preview (pull_request →
3251
+ * Cloudflare Pages) in a single file.
3252
+ *
3253
+ * Both jobs receive the same docs/storybook `with:` inputs. If the per-repo
3254
+ * config supplies `cloudflare-project` it is forwarded; otherwise the reusable
3255
+ * falls back to the `CLOUDFLARE_PAGES_PROJECT` org variable — set that once and
3256
+ * all repos with a `deploy` workflow get previews without per-repo config.
3257
+ */
3258
+ function generateCombinedDeployContent(deployWith, paths, preview) {
3259
+ const yamlScalar = (v) => {
3260
+ if (v === true) return "true";
3261
+ if (v === false) return "false";
3262
+ const s = String(v);
3263
+ return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
3264
+ };
3265
+ const withLines = (entries) => Object.entries(entries).map(([k, v]) => ` ${k}: ${yamlScalar(v)}`).join("\n");
3266
+ const pathsBlock = paths.length > 0 ? ` paths:\n${paths.map((p) => ` - ${p}\n`).join("")}` : "";
3267
+ const previewWith = {
3268
+ ...deployWith,
3269
+ "cloudflare-project": preview.project
3270
+ };
3271
+ const deployWithBlock = Object.keys(deployWith).length > 0 ? ` with:\n${withLines(deployWith)}\n` : "";
3272
+ const previewWithBlock = ` with:\n${withLines(previewWith)}\n`;
3273
+ return [
3274
+ `name: Deploy`,
3275
+ ``,
3276
+ `on: # yamllint disable-line rule:truthy`,
3277
+ ` push:`,
3278
+ ` branches: [main]`,
3279
+ ...pathsBlock ? [`${pathsBlock}`] : [],
3280
+ ` pull_request:`,
3281
+ ` branches: [main]`,
3282
+ ` types: [opened, synchronize, reopened, closed]`,
3283
+ ...pathsBlock ? [`${pathsBlock}`] : [],
3284
+ ` workflow_dispatch:`,
3285
+ ``,
3286
+ `concurrency:`,
3287
+ ` group: $\{{ github.event_name == 'pull_request' && format('preview-{0}', github.event.pull_request.number) || 'pages' }}`,
3288
+ ` cancel-in-progress: $\{{ github.event_name == 'pull_request' && github.event.action != 'closed' }}`,
3289
+ ``,
3290
+ `permissions:`,
3291
+ ` contents: read`,
3292
+ ` deployments: write`,
3293
+ ` pages: write`,
3294
+ ` id-token: write`,
3295
+ ` pull-requests: write`,
3296
+ ``,
3297
+ `jobs:`,
3298
+ ` deploy:`,
3299
+ ` name: Deploy`,
3300
+ ` if: \${{ github.event_name != 'pull_request' }}`,
3301
+ ` uses: theholocron/.github/.github/workflows/deploy.yml@main`,
3302
+ ...deployWithBlock ? [deployWithBlock.trimEnd()] : [],
3303
+ ` secrets: inherit`,
3304
+ ``,
3305
+ ` preview:`,
3306
+ ` name: Preview`,
3307
+ ` if: \${{ github.event_name == 'pull_request' }}`,
3308
+ ` uses: theholocron/.github/.github/workflows/preview.yml@main`,
3309
+ previewWithBlock.trimEnd(),
3310
+ ` secrets: inherit`,
3311
+ ``
3312
+ ].join("\n");
3313
+ }
3314
+ /**
3315
+ * Expand structured with-values to flat GitHub Actions inputs before
3316
+ * generating the thin caller. Handles:
3317
+ * - deploy shorthand: docs/storybook → type + storybook-projects
3318
+ * - preview: stripped (handled separately via extractPreviewConfig)
3319
+ * - run-chromatic object → run-chromatic: true + chromatic-projects
3320
+ * - plain arrays → JSON-stringified for YAML scalar quoting
3321
+ *
3322
+ * Used by both `holocron setup` and `sync-workflow-templates`.
3323
+ */
3324
+ function normalizeWorkflowWith(raw) {
3325
+ const result = { ...raw };
3326
+ delete result["preview"];
3327
+ const hasDocs = raw["docs"] === true || raw["docs"] !== null && typeof raw["docs"] === "object";
3328
+ const storybookProjects = raw["storybook"];
3329
+ if (hasDocs) {
3330
+ result["type"] = "docs";
3331
+ delete result["docs"];
3332
+ }
3333
+ if (Array.isArray(storybookProjects)) {
3334
+ if (!hasDocs) result["type"] = "storybook";
3335
+ result["storybook-projects"] = JSON.stringify(storybookProjects.map(({ name, path = "." }) => ({
3336
+ name,
3337
+ workingDir: path
3338
+ })));
3339
+ delete result["storybook"];
3340
+ }
3341
+ const runChromatic = raw["run-chromatic"];
3342
+ if (runChromatic !== null && typeof runChromatic === "object" && "projects" in runChromatic) {
3343
+ result["run-chromatic"] = true;
3344
+ const projects = runChromatic.projects.map((p) => ({
3345
+ ...p,
3346
+ ...Array.isArray(p.untraced) ? { untraced: p.untraced.join("\n") } : {}
3347
+ }));
3348
+ result["chromatic-projects"] = JSON.stringify(projects);
3349
+ }
3350
+ for (const [k, v] of Object.entries(result)) if (Array.isArray(v)) result[k] = JSON.stringify(v);
3351
+ return result;
3352
+ }
3353
+ /**
3354
+ * Derive on.push.paths entries from the deploy with: shorthand.
3355
+ * Used by both `holocron setup` and `sync-workflow-templates`.
3356
+ */
3357
+ function deriveDeployPaths(raw) {
3358
+ const paths = [];
3359
+ const docs = raw["docs"];
3360
+ if (docs === true) {
3361
+ paths.push("docs/**");
3362
+ paths.push("astro.config.ts");
3363
+ paths.push("pnpm-workspace.yaml");
3364
+ paths.push("pnpm-lock.yaml");
3365
+ } else if (docs !== null && typeof docs === "object" && "path" in docs) {
3366
+ const p = docs.path;
3367
+ if (p && p !== ".") paths.push(`${p}/**`);
3368
+ }
3369
+ const storybookProjects = raw["storybook"];
3370
+ if (Array.isArray(storybookProjects)) for (const s of storybookProjects) {
3371
+ const p = s.path || ".";
3372
+ if (p === ".") {
3373
+ paths.push("src/**");
3374
+ paths.push(".storybook/**");
3375
+ } else paths.push(`${p}/**`);
3376
+ }
3377
+ return paths;
3378
+ }
3379
+ //#endregion
3380
+ //#region src/commands/setup/agent-prompts-data.ts
3381
+ /**
3382
+ * Canonical AI engineering workflow role prompts.
3383
+ *
3384
+ * Written to `.agents/prompts/<role>.md` by `holocron setup` when `agent` is
3385
+ * configured. Paths are gitignored and regenerated on every setup run so the
3386
+ * content always reflects the current CLI version.
3387
+ *
3388
+ * Source: .notes/ai-engineering-workflow.spec.md
3389
+ */
3390
+ const DECISIONS_TEMPLATE = "---\nid: ADR-XXXX\ntitle: \"\"\nstatus: proposed\ndate: YYYY-MM-DD\nowners: []\nspecs: []\ndiscussion:\n github:\nsupersedes: []\nsuperseded-by: []\ntags: []\n---\n\n# [Short title of the decision]\n\n- Status: [proposed | accepted | rejected | deprecated | superseded by ADR-XXXX]\n- Date: YYYY-MM-DD\n\n## Context and Problem Statement\n\n2–3 sentences describing the situation that forced this decision.\n\n## Decision Drivers\n\n- [driver 1 — a constraint, goal, or value]\n- [driver 2]\n\n## Considered Options\n\n- [Option A]\n- [Option B]\n- [Option C — do nothing]\n\n## Decision Outcome\n\nChosen option: **[Option A]**, because [one-sentence justification].\n\n### Positive Consequences\n\n- …\n\n### Negative Consequences\n\n- …\n\n## Pros and Cons of the Options\n\n### [Option A]\n\n- Good, because [argument]\n- Bad, because [argument]\n\n### [Option B]\n\n- Good, because [argument]\n- Bad, because [argument]\n";
3391
+ const AGENT_PROMPTS = {
3392
+ "discovery.md": `# Discovery Agent
2831
3393
 
2832
3394
  You are performing engineering discovery.
2833
3395
 
@@ -3203,672 +3765,34 @@ the system must do — not how to implement it.
3203
3765
  > **Drafts** live in \`.notes/*.spec.md\` until accepted, then graduate here.
3204
3766
  `;
3205
3767
  //#endregion
3206
- //#region src/commands/dependabot.yml
3207
- var dependabot_default = "version: 2\nupdates:\n - package-ecosystem: npm\n directory: /\n schedule:\n interval: weekly\n commit-message:\n prefix: \"chore(deps)\"\n prefix-development: \"chore(deps-dev)\"\n groups:\n security-patches:\n applies-to: security-updates\n patterns:\n - \"*\"\n all-dependencies:\n update-types:\n - minor\n - patch\n\n - package-ecosystem: github-actions\n directory: /\n schedule:\n interval: weekly\n commit-message:\n prefix: \"chore(deps)\"\n groups:\n all-actions:\n patterns:\n - \"*\"\n";
3208
- //#endregion
3209
- //#region src/commands/workflows/audit.yml
3210
- var audit_default$1 = "name: Audit\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n audit:\n uses: theholocron/.github/.github/workflows/audit.yml@main\n secrets: inherit\n";
3768
+ //#region src/commands/setup/agent-prompts.ts
3769
+ const AGENTS_PROMPTS_ROOT = ".agents/prompts";
3770
+ const PROMPTS_GITIGNORE_START = "# managed by holocron setup — prompts";
3771
+ const PROMPTS_GITIGNORE_END = "# end managed by holocron setup — prompts";
3772
+ async function installAgentPrompts({ repoRoot }) {
3773
+ const promptsDir = join(repoRoot, AGENTS_PROMPTS_ROOT);
3774
+ await mkdir(promptsDir, { recursive: true });
3775
+ for (const [filename, content] of Object.entries(AGENT_PROMPTS)) await writeFile(join(promptsDir, filename), content, "utf8");
3776
+ const gitignorePath = join(repoRoot, ".gitignore");
3777
+ const existing = await readFile(gitignorePath, "utf8").catch(() => "");
3778
+ const block = [
3779
+ PROMPTS_GITIGNORE_START,
3780
+ `/${AGENTS_PROMPTS_ROOT}/`,
3781
+ PROMPTS_GITIGNORE_END
3782
+ ].join("\n");
3783
+ let updated;
3784
+ if (existing.includes(PROMPTS_GITIGNORE_START)) {
3785
+ const start = existing.indexOf(PROMPTS_GITIGNORE_START);
3786
+ const end = existing.indexOf(PROMPTS_GITIGNORE_END, start);
3787
+ const afterBlock = end !== -1 ? existing.slice(end + 41) : "\n";
3788
+ updated = existing.slice(0, start) + block + afterBlock;
3789
+ } else updated = (existing.trimEnd() ? existing.trimEnd() + "\n\n" : "") + block + "\n";
3790
+ await writeFile(gitignorePath, updated, "utf8");
3791
+ return `wrote ${Object.keys(AGENT_PROMPTS).length} prompt files to ${AGENTS_PROMPTS_ROOT}/`;
3792
+ }
3211
3793
  //#endregion
3212
- //#region src/commands/workflows/bookkeeping.yml
3213
- var bookkeeping_default$1 = "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n pull_request:\n types:\n - opened\n - edited\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\n\njobs:\n bookkeeping:\n uses: theholocron/.github/.github/workflows/bookkeeping.yml@main\n secrets: inherit\n";
3214
- //#endregion
3215
- //#region src/commands/workflows/codeql.yml
3216
- var codeql_default$1 = "name: CodeQL\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n schedule:\n - cron: \"0 0 * * 1\"\n\npermissions:\n actions: read\n contents: read\n security-events: write\n\njobs:\n codeql:\n uses: theholocron/.github/.github/workflows/codeql.yml@main\n secrets: inherit\n";
3217
- //#endregion
3218
- //#region src/commands/workflows/dependencies.yml
3219
- var dependencies_default$1 = "name: Dependencies\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n dependencies:\n uses: theholocron/.github/.github/workflows/dependencies.yml@main\n secrets: inherit\n";
3220
- //#endregion
3221
- //#region src/commands/workflows/deploy.yml
3222
- var deploy_default$1 = "name: Deploy\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n workflow_dispatch:\n\nconcurrency:\n group: pages\n cancel-in-progress: false\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\njobs:\n deploy:\n name: Deploy\n uses: theholocron/.github/.github/workflows/deploy.yml@main\n secrets: inherit\n";
3223
- //#endregion
3224
- //#region src/commands/workflows/greetings.yml
3225
- var greetings_default$1 = "name: Greetings\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 uses: theholocron/.github/.github/workflows/greetings.yml@main\n secrets: inherit\n";
3226
- //#endregion
3227
- //#region src/commands/workflows/lint.yml
3228
- var lint_default$1 = "name: Lint\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: lint-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n issues: write\n statuses: write\n\njobs:\n lint:\n name: Lint\n uses: theholocron/.github/.github/workflows/lint.yml@main\n secrets: inherit\n with:\n enable-auto-commit: true\n";
3229
- //#endregion
3230
- //#region src/commands/workflows/post-release.yml
3231
- var post_release_default$1 = "name: Post-release Sync\n\non: # yamllint disable-line rule:truthy\n release:\n types: [published]\n\npermissions:\n contents: read\n\njobs:\n broadcast:\n name: Post-release Sync\n uses: theholocron/.github/.github/workflows/post-release.yml@main\n secrets: inherit\n";
3232
- //#endregion
3233
- //#region src/commands/workflows/release.yml
3234
- var release_default$1 = "name: Release\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n - alpha\n workflow_dispatch:\n inputs:\n dry_run:\n description: >\n Dry run — analyze commits and preview the release without git writes\n or publish. Push-triggered runs always run fully; this only applies\n to manual workflow_dispatch triggers.\n required: false\n default: true\n type: boolean\n\npermissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n release:\n uses: theholocron/.github/.github/workflows/release.yml@main\n with:\n dry-run: ${{ inputs.dry_run == true }}\n secrets: inherit\n";
3235
- //#endregion
3236
- //#region src/commands/workflows/review.yml
3237
- var review_default$1 = "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: Review\n uses: theholocron/.github/.github/workflows/review.yml@main\n secrets: inherit\n";
3238
- //#endregion
3239
- //#region src/commands/workflows/stale.yml
3240
- var stale_default$1 = "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 secrets: inherit\n";
3241
- //#endregion
3242
- //#region src/commands/workflows/sync.yml
3243
- var sync_default$1 = "name: Sync\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/sync.yml@main\n with:\n steps: ${{ inputs.steps }}\n secrets: inherit\n";
3244
- //#endregion
3245
- //#region src/commands/workflows/test.yml
3246
- var test_default$1 = "name: Test\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: test-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n id-token: write\n statuses: write\n\njobs:\n test:\n name: Test\n uses: theholocron/.github/.github/workflows/test.yml@main\n with:\n run-unit: true\n secrets: inherit\n";
3247
- //#endregion
3248
- //#region src/commands/workflows/typecheck.yml
3249
- var typecheck_default$1 = "name: Typecheck\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: typecheck-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n typecheck:\n name: Typecheck\n uses: theholocron/.github/.github/workflows/typecheck.yml@main\n secrets: inherit\n";
3250
- //#endregion
3251
- //#region src/commands/workflows/wiki.yml
3252
- var wiki_default$1 = "name: Wiki\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/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/wiki.yml@main\n with:\n preview: true\n preview-id: pr-${{ github.event.pull_request.number }}\n secrets: inherit\n";
3253
- //#endregion
3254
- //#region src/commands/setup-workflows.ts
3255
- /**
3256
- * Header prepended to every auto-generated workflow thin caller.
3257
- *
3258
- * Used by both `holocron setup` (initial creation) and `holocron sync-github`
3259
- * (subsequent updates) so the header is always identical regardless of which
3260
- * command last wrote the file.
3261
- *
3262
- * @param source - path within theholocron/holocron that owns the template
3263
- * @param forPrimary - true only when writing to theholocron/.github itself
3264
- */
3265
- function workflowHeader(source = "packages/cli/src/commands/setup-workflows.ts", forPrimary = false, tool = "holocron sync-github") {
3266
- return [
3267
- forPrimary ? `# AUTO-GENERATED — do not edit in theholocron/.github directly.` : `# AUTO-GENERATED — do not edit directly.`,
3268
- `# Source: theholocron/holocron · ${source}`,
3269
- `# Tool: ${tool}`,
3270
- `# Changes: edit source in theholocron/holocron and push to alpha or main.`,
3271
- ``
3272
- ].join("\n");
3273
- }
3274
- function scaffoldHeader(source = "packages/cli/src/commands/setup.ts") {
3275
- return [
3276
- `# Scaffolded by holocron setup — edit this file freely.`,
3277
- `# Source: theholocron/holocron · ${source}`,
3278
- ``
3279
- ].join("\n");
3280
- }
3281
- const WORKFLOW_TEMPLATES = {
3282
- lint: lint_default$1,
3283
- test: test_default$1,
3284
- typecheck: typecheck_default$1,
3285
- codeql: codeql_default$1,
3286
- review: review_default$1,
3287
- "post-release": post_release_default$1,
3288
- release: release_default$1,
3289
- stale: stale_default$1,
3290
- sync: sync_default$1,
3291
- greetings: greetings_default$1,
3292
- dependencies: dependencies_default$1,
3293
- bookkeeping: bookkeeping_default$1,
3294
- audit: audit_default$1,
3295
- deploy: deploy_default$1,
3296
- wiki: wiki_default$1
3297
- };
3298
- const KNOWN_WORKFLOWS = new Set(Object.keys(WORKFLOW_TEMPLATES));
3299
- /**
3300
- * GitHub check context name each CI workflow produces on a PR.
3301
- *
3302
- * The format is "{caller-workflow-name} / {reusable-job-name}". The caller
3303
- * job's own `name:` field does NOT appear in the external check name — only
3304
- * the calling workflow's top-level `name:` and the inner reusable-workflow
3305
- * job name matter. Only workflows that gate merges are listed here.
3306
- */
3307
- const WORKFLOW_CHECK_CONTEXTS = {
3308
- lint: "Lint / Lint entire codebase",
3309
- test: "Test / Run tests and collect coverage",
3310
- typecheck: "Typecheck / tsc --noEmit"
3311
- };
3312
- /**
3313
- * Generate the thin caller content for a workflow, optionally injecting or
3314
- * merging `with:` overrides into the jobs block.
3315
- *
3316
- * Two strategies are used depending on the template:
3317
- * - Templates that already have a `with:` block (e.g. lint):
3318
- * the override entries are merged in, replacing existing keys and appending
3319
- * new ones.
3320
- * - Templates that end with ` secrets: inherit`: a new `with:` block is
3321
- * injected immediately before `secrets: inherit`.
3322
- * If neither pattern matches the template, a warning is emitted and the
3323
- * base template is returned unchanged.
3324
- */
3325
- function generateThinCallerContent(name, withOverrides, additionalPaths) {
3326
- const base = WORKFLOW_TEMPLATES[name];
3327
- if (!base) return "";
3328
- const yamlScalar = (v) => {
3329
- if (v === true) return "true";
3330
- if (v === false) return "false";
3331
- const s = String(v);
3332
- return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
3333
- };
3334
- const fmt = (k, v) => ` ${k}: ${yamlScalar(v)}`;
3335
- let result = base;
3336
- if (additionalPaths && additionalPaths.length > 0) {
3337
- const pathsBlockRe = /( {4}paths:\n)((?:[ ]{6}- [^\n]+\n)+)/;
3338
- if (pathsBlockRe.test(result)) result = result.replace(pathsBlockRe, (_, header, existing) => {
3339
- const existingPaths = new Set([...existing.matchAll(/- (.+)/g)].map((m) => m[1]));
3340
- const newEntries = additionalPaths.filter((p) => !existingPaths.has(p)).map((p) => ` - ${p}\n`).join("");
3341
- return header + existing + newEntries;
3342
- });
3343
- else {
3344
- const pathsBlock = ` paths:\n${additionalPaths.map((p) => ` - ${p}\n`).join("")}`;
3345
- result = result.replace(/( {4}branches: \[main\]\n)/, `$1${pathsBlock}`);
3346
- }
3347
- }
3348
- if (!withOverrides || Object.keys(withOverrides).length === 0) return result;
3349
- const withBlockRe = /( {4}with:\n)((?:[ ]{6}[^\n]+\n)*)/;
3350
- const existingMatch = result.match(withBlockRe);
3351
- if (existingMatch) {
3352
- const existingEntries = new Map(existingMatch[2].split("\n").filter(Boolean).map((line) => {
3353
- const m = line.match(/^ {6}([^:]+):\s*(.*)/);
3354
- return m ? [m[1].trim(), m[2].trim()] : null;
3355
- }).filter((e) => e !== null));
3356
- for (const [k, v] of Object.entries(withOverrides)) existingEntries.set(k, yamlScalar(v));
3357
- const merged = [...existingEntries.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n");
3358
- return result.replace(withBlockRe, ` with:\n${merged}\n`);
3359
- }
3360
- const withBlock = Object.entries(withOverrides).map(([k, v]) => fmt(k, v)).join("\n");
3361
- const injected = result.replace(/ {4}secrets: inherit\n$/, ` with:\n${withBlock}\n secrets: inherit\n`);
3362
- if (injected === result) console.warn(`[generateThinCallerContent] could not inject with: overrides into "${name}" template`);
3363
- return injected;
3364
- }
3365
- /**
3366
- * Extract the Cloudflare Pages preview config from a deploy workflow's `with:` object.
3367
- *
3368
- * Accepts three forms:
3369
- * - `preview: true` — derive both project and domain from org context
3370
- * - `preview: { project: "..." }` — explicit project; domain derived from context if omitted
3371
- * - `preview: { project: "...", domain: "..." }` — fully explicit
3372
- *
3373
- * Returns null when `preview:` is absent, false, or can't be resolved.
3374
- */
3375
- function extractPreviewConfig(raw, ctx = {}) {
3376
- const preview = raw["preview"];
3377
- if (!preview) return null;
3378
- if (preview === true) {
3379
- const project = ctx.org ? `${ctx.org}-preview` : null;
3380
- const domain = ctx.domain ? `preview.${ctx.domain}` : void 0;
3381
- if (!project) return null;
3382
- return {
3383
- project,
3384
- ...domain ? { domain } : {}
3385
- };
3386
- }
3387
- if (typeof preview !== "object") return null;
3388
- const p = preview;
3389
- const project = typeof p["project"] === "string" && p["project"] ? p["project"] : ctx.org ? `${ctx.org}-preview` : null;
3390
- if (!project) return null;
3391
- const domain = typeof p["domain"] === "string" && p["domain"] ? p["domain"] : ctx.domain ? `preview.${ctx.domain}` : void 0;
3392
- return {
3393
- project,
3394
- ...domain ? { domain } : {}
3395
- };
3396
- }
3397
- /**
3398
- * Generate the full thin-caller YAML for a `deploy.yml` that handles both
3399
- * production (push to main → GitHub Pages) and preview (pull_request →
3400
- * Cloudflare Pages) in a single file.
3401
- *
3402
- * Both jobs receive the same docs/storybook `with:` inputs. If the per-repo
3403
- * config supplies `cloudflare-project` it is forwarded; otherwise the reusable
3404
- * falls back to the `CLOUDFLARE_PAGES_PROJECT` org variable — set that once and
3405
- * all repos with a `deploy` workflow get previews without per-repo config.
3406
- */
3407
- function generateCombinedDeployContent(deployWith, paths, preview) {
3408
- const yamlScalar = (v) => {
3409
- if (v === true) return "true";
3410
- if (v === false) return "false";
3411
- const s = String(v);
3412
- return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
3413
- };
3414
- const withLines = (entries) => Object.entries(entries).map(([k, v]) => ` ${k}: ${yamlScalar(v)}`).join("\n");
3415
- const pathsBlock = paths.length > 0 ? ` paths:\n${paths.map((p) => ` - ${p}\n`).join("")}` : "";
3416
- const previewWith = {
3417
- ...deployWith,
3418
- "cloudflare-project": preview.project
3419
- };
3420
- const deployWithBlock = Object.keys(deployWith).length > 0 ? ` with:\n${withLines(deployWith)}\n` : "";
3421
- const previewWithBlock = ` with:\n${withLines(previewWith)}\n`;
3422
- const cleanupWithBlock = ` with:\n cloudflare-project: ${preview.project}`;
3423
- return [
3424
- `name: Deploy`,
3425
- ``,
3426
- `on: # yamllint disable-line rule:truthy`,
3427
- ` push:`,
3428
- ` branches: [main]`,
3429
- ...pathsBlock ? [`${pathsBlock}`] : [],
3430
- ` pull_request:`,
3431
- ` branches: [main]`,
3432
- ` types: [opened, synchronize, reopened, closed]`,
3433
- ...pathsBlock ? [`${pathsBlock}`] : [],
3434
- ` workflow_dispatch:`,
3435
- ``,
3436
- `concurrency:`,
3437
- ` group: $\{{ github.event_name == 'pull_request' && format('deploy-preview-{0}', github.event.pull_request.number) || 'pages' }}`,
3438
- ` cancel-in-progress: $\{{ github.event_name == 'pull_request' && github.event.action != 'closed' }}`,
3439
- ``,
3440
- `permissions:`,
3441
- ` contents: read`,
3442
- ` deployments: write`,
3443
- ` pages: write`,
3444
- ` id-token: write`,
3445
- ` pull-requests: write`,
3446
- ``,
3447
- `jobs:`,
3448
- ` deploy:`,
3449
- ` name: Deploy`,
3450
- ` if: \${{ github.event_name != 'pull_request' }}`,
3451
- ` uses: theholocron/.github/.github/workflows/deploy.yml@main`,
3452
- ...deployWithBlock ? [deployWithBlock.trimEnd()] : [],
3453
- ` secrets: inherit`,
3454
- ``,
3455
- ` preview:`,
3456
- ` name: Deploy Preview`,
3457
- ` if: \${{ github.event_name == 'pull_request' && github.event.action != 'closed' }}`,
3458
- ` uses: theholocron/.github/.github/workflows/deploy-preview.yml@main`,
3459
- previewWithBlock.trimEnd(),
3460
- ` secrets: inherit`,
3461
- ``,
3462
- ` cleanup:`,
3463
- ` name: Clean up Preview`,
3464
- ` if: \${{ github.event_name == 'pull_request' && github.event.action == 'closed' }}`,
3465
- ` uses: theholocron/.github/.github/workflows/cleanup-preview.yml@main`,
3466
- cleanupWithBlock,
3467
- ` secrets: inherit`,
3468
- ``
3469
- ].join("\n");
3470
- }
3471
- /**
3472
- * Expand structured with-values to flat GitHub Actions inputs before
3473
- * generating the thin caller. Handles:
3474
- * - deploy shorthand: docs/storybook → type + storybook-projects
3475
- * - preview: stripped (handled separately via extractPreviewConfig)
3476
- * - run-chromatic object → run-chromatic: true + chromatic-projects
3477
- * - plain arrays → JSON-stringified for YAML scalar quoting
3478
- *
3479
- * Used by both `holocron setup` and `sync-workflow-templates`.
3480
- */
3481
- function normalizeWorkflowWith(raw) {
3482
- const result = { ...raw };
3483
- delete result["preview"];
3484
- const hasDocs = raw["docs"] === true || raw["docs"] !== null && typeof raw["docs"] === "object";
3485
- const storybookProjects = raw["storybook"];
3486
- if (hasDocs) {
3487
- result["type"] = "docs";
3488
- delete result["docs"];
3489
- }
3490
- if (Array.isArray(storybookProjects)) {
3491
- if (!hasDocs) result["type"] = "storybook";
3492
- result["storybook-projects"] = JSON.stringify(storybookProjects.map(({ name, path = "." }) => ({
3493
- name,
3494
- workingDir: path
3495
- })));
3496
- delete result["storybook"];
3497
- }
3498
- const runChromatic = raw["run-chromatic"];
3499
- if (runChromatic !== null && typeof runChromatic === "object" && "projects" in runChromatic) {
3500
- result["run-chromatic"] = true;
3501
- const projects = runChromatic.projects.map((p) => ({
3502
- ...p,
3503
- ...Array.isArray(p.untraced) ? { untraced: p.untraced.join("\n") } : {}
3504
- }));
3505
- result["chromatic-projects"] = JSON.stringify(projects);
3506
- }
3507
- for (const [k, v] of Object.entries(result)) if (Array.isArray(v)) result[k] = JSON.stringify(v);
3508
- return result;
3509
- }
3510
- /**
3511
- * Derive on.push.paths entries from the deploy with: shorthand.
3512
- * Used by both `holocron setup` and `sync-workflow-templates`.
3513
- */
3514
- function deriveDeployPaths(raw) {
3515
- const paths = [];
3516
- const docs = raw["docs"];
3517
- if (docs === true) {
3518
- paths.push("docs/**");
3519
- paths.push("astro.config.ts");
3520
- paths.push("pnpm-workspace.yaml");
3521
- paths.push("pnpm-lock.yaml");
3522
- } else if (docs !== null && typeof docs === "object" && "path" in docs) {
3523
- const p = docs.path;
3524
- if (p && p !== ".") paths.push(`${p}/**`);
3525
- }
3526
- const storybookProjects = raw["storybook"];
3527
- if (Array.isArray(storybookProjects)) for (const s of storybookProjects) {
3528
- const p = s.path || ".";
3529
- if (p === ".") {
3530
- paths.push("src/**");
3531
- paths.push(".storybook/**");
3532
- } else paths.push(`${p}/**`);
3533
- }
3534
- return paths;
3535
- }
3536
- //#endregion
3537
- //#region src/commands/setup.ts
3538
- /**
3539
- * `holocron setup` — orchestrates per-capability setup actions across
3540
- * every plugin loaded from `holocron.config.json`.
3541
- *
3542
- * Per CLAUDE.md soft-skip: each step is wrapped in a try/catch and
3543
- * failures don't abort subsequent capabilities. The summary at the end
3544
- * reports counts so the operator can see what worked + what didn't.
3545
- *
3546
- * Per the Standards: when `ctx.dryRun` is true, mutating calls are
3547
- * replaced with "would" log lines. Read-only probes (e.g.,
3548
- * `vault.list`) still run so the operator sees real state.
3549
- *
3550
- * The orchestrator knows about specific capability methods by name
3551
- * (e.g., `source.enableVulnerabilityAlerts`). This deliberate coupling
3552
- * makes the "what does setup do" contract explicit and concrete —
3553
- * decoupling via a per-capability `setupSteps()` method would be more
3554
- * extensible but pushes the same knowledge into N plugins instead of
3555
- * one central place.
3556
- */
3557
- function editorconfigContent() {
3558
- return [
3559
- workflowHeader("packages/cli/src/commands/setup.ts"),
3560
- `root = true`,
3561
- ``,
3562
- `[*]`,
3563
- `end_of_line = lf`,
3564
- `charset = utf-8`,
3565
- `trim_trailing_whitespace = true`,
3566
- `insert_final_newline = true`,
3567
- `indent_style = tab`,
3568
- `indent_size = 4`,
3569
- ``,
3570
- `[.gitattributes]`,
3571
- `indent_style = space`,
3572
- `indent_size = 2`,
3573
- ``,
3574
- `[*.{json,yml,yaml}]`,
3575
- `indent_style = space`,
3576
- `indent_size = 2`,
3577
- ``,
3578
- `[*.{md,mdx}]`,
3579
- `trim_trailing_whitespace = false`,
3580
- ``,
3581
- `[.*{rc,ignore}]`,
3582
- `indent_style = space`,
3583
- `indent_size = 2`,
3584
- ``
3585
- ].join("\n");
3586
- }
3587
- const INDIVIDUAL_COMPONENTS_MARKER = " individual_components:";
3588
- function codecovComponentBlock(packages) {
3589
- if (packages.length === 0) return "\n []\n";
3590
- return "\n" + packages.flatMap(({ slug }) => [
3591
- ` - component_id: ${slug}`,
3592
- ` name: "${slug}"`,
3593
- ` paths:`,
3594
- ` - packages/${slug}/**`,
3595
- ``
3596
- ]).join("\n");
3597
- }
3598
- function mergeCodecovComponents(existing, packages) {
3599
- const idx = existing.indexOf(INDIVIDUAL_COMPONENTS_MARKER);
3600
- if (idx === -1) return existing;
3601
- return existing.slice(0, idx + 24) + codecovComponentBlock(packages);
3602
- }
3603
- function codecovContent(packages) {
3604
- return [
3605
- scaffoldHeader(),
3606
- `codecov:`,
3607
- ` require_ci_to_pass: true`,
3608
- ``,
3609
- `coverage:`,
3610
- ` precision: 2`,
3611
- ` round: down`,
3612
- ` status:`,
3613
- ` project:`,
3614
- ` default:`,
3615
- ` target: auto`,
3616
- ` threshold: 2%`,
3617
- ` patch:`,
3618
- ` default:`,
3619
- ` target: 80%`,
3620
- ``,
3621
- `comment:`,
3622
- ` layout: "reach,diff,flags,components"`,
3623
- ` behavior: default`,
3624
- ` require_changes: true`,
3625
- ``,
3626
- `component_management:`,
3627
- ` default_rules:`,
3628
- ` statuses:`,
3629
- ` - type: patch`,
3630
- ` target: 80%`,
3631
- ` individual_components:`
3632
- ].join("\n") + codecovComponentBlock(packages);
3633
- }
3634
- async function readWorkspacePackages(repoRoot) {
3635
- const packagesDir = join(repoRoot, "packages");
3636
- const entries = await readdir(packagesDir, { withFileTypes: true }).catch(() => null);
3637
- if (!entries) return [];
3638
- const packages = [];
3639
- for (const entry of entries) {
3640
- if (!entry.isDirectory()) continue;
3641
- try {
3642
- const raw = await readFile(join(packagesDir, entry.name, "package.json"), "utf8");
3643
- const pkg = JSON.parse(raw);
3644
- if (typeof pkg.name === "string") packages.push({
3645
- slug: entry.name,
3646
- name: pkg.name
3647
- });
3648
- } catch {}
3649
- }
3650
- return packages.sort((a, b) => a.slug.localeCompare(b.slug));
3651
- }
3652
- const EDITORCONFIG_CHECKER_CONFIG = JSON.stringify({
3653
- Version: "v3.7.0",
3654
- Verbose: false,
3655
- Format: "",
3656
- Debug: false,
3657
- IgnoreDefaults: false,
3658
- SpacesAfterTabs: false,
3659
- NoColor: false,
3660
- Exclude: [
3661
- "(^|.+/)LICENSE$",
3662
- "^public/.*",
3663
- "\\.md$",
3664
- "\\.mdx$"
3665
- ],
3666
- AllowedContentTypes: [],
3667
- PassedFiles: [],
3668
- Disable: {
3669
- EndOfLine: false,
3670
- Indentation: false,
3671
- InsertFinalNewline: false,
3672
- TrimTrailingWhitespace: false,
3673
- IndentSize: false,
3674
- MaxLineLength: false
3675
- }
3676
- }, null, 2) + "\n";
3677
- const SENTIMENT_BOT_CONFIG = [
3678
- `# Configuration for sentiment-bot - https://github.com/behaviorbot/sentiment-bot`,
3679
- ``,
3680
- `# *Required* toxicity threshold between 0 and .99 with the higher numbers being the most toxic`,
3681
- `# Anything higher than this threshold will be marked as toxic and commented on`,
3682
- `sentimentBotToxicityThreshold: .7`,
3683
- ``,
3684
- `# *Required* Comment to reply with`,
3685
- `sentimentBotReplyComment: >`,
3686
- ` Please be sure to review the [Code of Conduct](https://docs.theholocron.dev/reference/code-of-conduct/) and be respectful of other users.`,
3687
- ``
3688
- ].join("\n");
3689
- const ALEX_CONFIG = JSON.stringify({ allow: [
3690
- "dead",
3691
- "failure",
3692
- "failures",
3693
- "hook",
3694
- "hooks",
3695
- "husky",
3696
- "period"
3697
- ] }, null, 2) + "\n";
3698
- function devmojiConfigContent() {
3699
- return [
3700
- `/* eslint-disable */`,
3701
- `// devmoji.config.cjs — generated by holocron setup, do not edit`,
3702
- `// https://github.com/folke/devmoji`,
3703
- `const { defineConfig } = require("@theholocron/devmoji-config");`,
3704
- `module.exports = defineConfig();`,
3705
- ``
3706
- ].join("\n");
3707
- }
3708
- function prepareCommitMsgHookContent() {
3709
- return [
3710
- `#!/bin/sh`,
3711
- ``,
3712
- `NAME=$(git config user.name)`,
3713
- `EMAIL=$(git config user.email)`,
3714
- ``,
3715
- `if [ -z "$NAME" ]; then`,
3716
- `\techo "empty git config user.name"`,
3717
- `\texit 1`,
3718
- `fi`,
3719
- ``,
3720
- `if [ -z "$EMAIL" ]; then`,
3721
- `\techo "empty git config user.email"`,
3722
- `\texit 1`,
3723
- `fi`,
3724
- ``,
3725
- `git interpret-trailers --if-exists doNothing --trailer \\`,
3726
- `\t"Signed-off-by: $NAME <$EMAIL>" \\`,
3727
- `\t--in-place "$1"`,
3728
- ``,
3729
- `npx devmoji -e`,
3730
- ``
3731
- ].join("\n");
3732
- }
3733
- const CANONICAL_LABELS = [
3734
- {
3735
- name: "bug",
3736
- color: "d73a4a",
3737
- description: "Something isn't working"
3738
- },
3739
- {
3740
- name: "chore",
3741
- color: "ededed",
3742
- description: "Maintenance, no user-facing change"
3743
- },
3744
- {
3745
- name: "ci",
3746
- color: "0075ca",
3747
- description: "CI/CD pipeline changes"
3748
- },
3749
- {
3750
- name: "dependencies",
3751
- color: "0366d6",
3752
- description: "Dependency update"
3753
- },
3754
- {
3755
- name: "documentation",
3756
- color: "0075ca",
3757
- description: "Documentation only"
3758
- },
3759
- {
3760
- name: "duplicate",
3761
- color: "cfd3d7",
3762
- description: "Already reported"
3763
- },
3764
- {
3765
- name: "enhancement",
3766
- color: "a2eeef",
3767
- description: "New feature or request"
3768
- },
3769
- {
3770
- name: "good first issue",
3771
- color: "7057ff",
3772
- description: "Good for newcomers"
3773
- },
3774
- {
3775
- name: "help wanted",
3776
- color: "008672",
3777
- description: "Extra attention needed"
3778
- },
3779
- {
3780
- name: "invalid",
3781
- color: "e4e669",
3782
- description: "Doesn't seem right"
3783
- },
3784
- {
3785
- name: "performance",
3786
- color: "fbca04",
3787
- description: "Performance improvement"
3788
- },
3789
- {
3790
- name: "question",
3791
- color: "d876e3",
3792
- description: "Further information requested"
3793
- },
3794
- {
3795
- name: "refactor",
3796
- color: "cfd3d7",
3797
- description: "Code restructuring"
3798
- },
3799
- {
3800
- name: "released",
3801
- color: "ededed",
3802
- description: "Included in a release"
3803
- },
3804
- {
3805
- name: "test",
3806
- color: "bfd4f2",
3807
- description: "Test-related changes"
3808
- },
3809
- {
3810
- name: "triage",
3811
- color: "e4e669",
3812
- description: "Needs investigation"
3813
- },
3814
- {
3815
- name: "wontfix",
3816
- color: "ffffff",
3817
- description: "Won't be addressed"
3818
- }
3819
- ];
3820
- const STALE_LABELS = [
3821
- "github_actions",
3822
- "javascript",
3823
- "autorelease: pending",
3824
- "autorelease: tagged",
3825
- "released on @alpha"
3826
- ];
3827
- function labelerConfig() {
3828
- return [
3829
- workflowHeader("packages/cli/src/commands/setup.ts"),
3830
- `bug:`,
3831
- ` - '^fix'`,
3832
- ``,
3833
- `chore:`,
3834
- ` - '^chore(?!\\(deps)'`,
3835
- ``,
3836
- `ci:`,
3837
- ` - '^ci'`,
3838
- ``,
3839
- `dependencies:`,
3840
- ` - '^chore\\(deps'`,
3841
- ``,
3842
- `documentation:`,
3843
- ` - '^docs'`,
3844
- ``,
3845
- `enhancement:`,
3846
- ` - '^feat'`,
3847
- ``,
3848
- `performance:`,
3849
- ` - '^perf'`,
3850
- ``,
3851
- `refactor:`,
3852
- ` - '^refactor'`,
3853
- ``,
3854
- `test:`,
3855
- ` - '^test'`,
3856
- ``
3857
- ].join("\n");
3858
- }
3794
+ //#region src/commands/setup/branch-protection.ts
3859
3795
  const RULESET_NAME = "holocron-default-branch";
3860
- const BALANCED_REPO_SETTINGS = {
3861
- allow_squash_merge: true,
3862
- allow_merge_commit: false,
3863
- allow_rebase_merge: false,
3864
- allow_auto_merge: true,
3865
- allow_update_branch: true,
3866
- delete_branch_on_merge: true,
3867
- has_issues: true,
3868
- has_discussions: true,
3869
- has_projects: true,
3870
- has_wiki: false
3871
- };
3872
3796
  function buildClassicProtectionPayload(requiredChecks = []) {
3873
3797
  return {
3874
3798
  required_status_checks: requiredChecks.length > 0 ? {
@@ -3981,6 +3905,278 @@ async function upsertBranchProtection(source, dryRun, requiredChecks) {
3981
3905
  };
3982
3906
  }
3983
3907
  }
3908
+ //#endregion
3909
+ //#region src/commands/setup/engineering.ts
3910
+ async function writeIfAbsent(filePath, content) {
3911
+ try {
3912
+ await access(filePath);
3913
+ return false;
3914
+ } catch {
3915
+ await mkdir(dirname(filePath), { recursive: true });
3916
+ await writeFile(filePath, content, "utf8");
3917
+ return true;
3918
+ }
3919
+ }
3920
+ async function installEngineeringStructure({ repoRoot }) {
3921
+ const results = [];
3922
+ const writes = [
3923
+ [join(repoRoot, "docs/wiki/decisions/template.md"), DECISIONS_TEMPLATE],
3924
+ [join(repoRoot, "docs/wiki/decisions/README.md"), DECISIONS_README],
3925
+ [join(repoRoot, "docs/wiki/standards/README.md"), STANDARDS_README],
3926
+ [join(repoRoot, "docs/wiki/specifications/README.md"), SPECIFICATIONS_README]
3927
+ ];
3928
+ for (const [path, content] of writes) if (await writeIfAbsent(path, content)) results.push(path.replace(repoRoot + "/", ""));
3929
+ return results.length > 0 ? `created: ${results.join(", ")}` : "all files already exist — nothing to write";
3930
+ }
3931
+ //#endregion
3932
+ //#region src/commands/setup/repo-settings.ts
3933
+ const BALANCED_REPO_SETTINGS = {
3934
+ allow_squash_merge: true,
3935
+ allow_merge_commit: false,
3936
+ allow_rebase_merge: false,
3937
+ allow_auto_merge: true,
3938
+ allow_update_branch: true,
3939
+ delete_branch_on_merge: true,
3940
+ has_issues: true,
3941
+ has_discussions: true,
3942
+ has_projects: true,
3943
+ has_wiki: false
3944
+ };
3945
+ //#endregion
3946
+ //#region src/commands/setup/run-step.ts
3947
+ async function runStep(capability, step, dryRun, body, opts = {}) {
3948
+ if (dryRun) return {
3949
+ capability,
3950
+ step,
3951
+ status: "dry-run"
3952
+ };
3953
+ try {
3954
+ const note = await body();
3955
+ const result = {
3956
+ capability,
3957
+ step,
3958
+ status: "ok"
3959
+ };
3960
+ if (typeof note === "string") result.message = note;
3961
+ return result;
3962
+ } catch (err) {
3963
+ if (err instanceof ProviderApiError$1) {
3964
+ if (err.status !== void 0 && opts.skipCodes?.includes(err.status)) return {
3965
+ capability,
3966
+ step,
3967
+ status: "skip",
3968
+ message: err.message
3969
+ };
3970
+ if (err.status === 403) {
3971
+ const reason = classify403(err);
3972
+ return {
3973
+ capability,
3974
+ step,
3975
+ status: reason === "plan" ? "skip" : "fail",
3976
+ message: err.message,
3977
+ reason
3978
+ };
3979
+ }
3980
+ }
3981
+ return {
3982
+ capability,
3983
+ step,
3984
+ status: "fail",
3985
+ message: err instanceof Error ? err.message : String(err)
3986
+ };
3987
+ }
3988
+ }
3989
+ function classify403(err) {
3990
+ const detailText = typeof err.details === "string" ? err.details : typeof err.details === "object" && err.details !== null && "message" in err.details ? String(err.details.message) : "";
3991
+ const text = `${err.message} ${detailText}`.toLowerCase();
3992
+ if (text.includes("advanced security") || text.includes("not enabled for this repository") || text.includes("upgrade") || text.includes("not available on")) return "plan";
3993
+ return "permissions";
3994
+ }
3995
+ function formatStep(step) {
3996
+ const tag = step.reason === "permissions" ? " [permissions]" : step.reason === "plan" ? " [plan restriction]" : "";
3997
+ const detail = step.message ? style.dim(` (${step.message})`) : "";
3998
+ const label = `${step.step}${tag}${detail}`;
3999
+ if (step.status === "ok") return ` ${style.success(label)}`;
4000
+ if (step.status === "fail") return ` ${style.fail(label)}`;
4001
+ if (step.status === "dry-run") return ` ${style.dim(`… ${label}`)}`;
4002
+ return ` ${style.dim(`· ${label}`)}`;
4003
+ }
4004
+ //#endregion
4005
+ //#region src/commands/setup/skills.ts
4006
+ async function fetchExternalSkill(entry) {
4007
+ if (entry.sourceType !== "github")
4008
+ /* c8 ignore next */
4009
+ throw new Error(`unsupported sourceType: ${entry.sourceType}`);
4010
+ const url = `https://raw.githubusercontent.com/${entry.source}/HEAD/${entry.skillPath}`;
4011
+ const res = await fetch(url);
4012
+ if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
4013
+ const content = await res.text();
4014
+ return {
4015
+ content,
4016
+ stale: !!entry.computedHash && createHash("sha256").update(content).digest("hex") !== entry.computedHash
4017
+ };
4018
+ }
4019
+ const AGENTS_SKILLS_ROOT = ".agents/skills";
4020
+ /** Relative path of the agent-specific symlink. undefined = unsupported agent. */
4021
+ const AGENT_SYMLINK_PATHS = { claude: (name) => `.claude/skills/${name}` };
4022
+ const GITIGNORE_BLOCK_START = "# managed by holocron setup — skills";
4023
+ const GITIGNORE_BLOCK_END = "# end managed by holocron setup — skills";
4024
+ async function installSkills({ agent, skills, repoRoot }) {
4025
+ const symlinkFn = AGENT_SYMLINK_PATHS[agent];
4026
+ if (!symlinkFn) return `agent "${agent}" has no known skill install path — skipping`;
4027
+ const require = createRequire(pathToFileURL(join(repoRoot, "package.json")));
4028
+ let skillsRoot;
4029
+ try {
4030
+ skillsRoot = dirname(require.resolve("@theholocron/skills/package.json"));
4031
+ } catch {
4032
+ if (spawnSync("pnpm", [
4033
+ "add",
4034
+ "-D",
4035
+ "@theholocron/skills"
4036
+ ], {
4037
+ cwd: repoRoot,
4038
+ stdio: "inherit"
4039
+ }).status !== 0) throw new Error("failed to auto-install @theholocron/skills");
4040
+ try {
4041
+ skillsRoot = dirname(require.resolve("@theholocron/skills/package.json"));
4042
+ } catch {
4043
+ throw new Error("failed to auto-install @theholocron/skills");
4044
+ }
4045
+ }
4046
+ const gitignorePath = join(repoRoot, ".gitignore");
4047
+ const existingContent = await readFile(gitignorePath, "utf8").catch(() => "");
4048
+ const previouslyInstalled = parsePreviousSkills(existingContent, symlinkFn);
4049
+ const currentSet = new Set(skills);
4050
+ const stale = previouslyInstalled.filter((n) => !currentSet.has(n));
4051
+ for (const name of stale) {
4052
+ await rm(join(repoRoot, symlinkFn(name)), { force: true }).catch(() => void 0);
4053
+ await rm(join(repoRoot, AGENTS_SKILLS_ROOT, name), {
4054
+ recursive: true,
4055
+ force: true
4056
+ }).catch(() => void 0);
4057
+ }
4058
+ const installed = [];
4059
+ const missing = [];
4060
+ for (const name of skills) {
4061
+ const srcDir = join(skillsRoot, "src", name);
4062
+ try {
4063
+ await stat(srcDir);
4064
+ } catch {
4065
+ missing.push(name);
4066
+ continue;
4067
+ }
4068
+ const agentsDir = join(repoRoot, AGENTS_SKILLS_ROOT, name);
4069
+ await copyDirRecursive(srcDir, agentsDir);
4070
+ const symlinkPath = join(repoRoot, symlinkFn(name));
4071
+ await mkdir(dirname(symlinkPath), { recursive: true });
4072
+ try {
4073
+ await unlink(symlinkPath);
4074
+ } catch {}
4075
+ await symlink(relative(dirname(symlinkPath), agentsDir).replace(/\\/g, "/"), symlinkPath);
4076
+ installed.push(name);
4077
+ }
4078
+ const externalFailed = [];
4079
+ const externalStale = [];
4080
+ if (missing.length > 0) {
4081
+ let lock = null;
4082
+ try {
4083
+ lock = JSON.parse(await readFile(join(skillsRoot, "skills-lock.json"), "utf8"));
4084
+ } catch {}
4085
+ if (lock?.skills) for (const name of [...missing]) {
4086
+ const entry = lock.skills[name];
4087
+ if (!entry) continue;
4088
+ try {
4089
+ const { content, stale: isStale } = await fetchExternalSkill(entry);
4090
+ const agentsDir = join(repoRoot, AGENTS_SKILLS_ROOT, name);
4091
+ await mkdir(agentsDir, { recursive: true });
4092
+ await writeFile(join(agentsDir, "SKILL.md"), content);
4093
+ const symlinkPath = join(repoRoot, symlinkFn(name));
4094
+ await mkdir(dirname(symlinkPath), { recursive: true });
4095
+ try {
4096
+ await unlink(symlinkPath);
4097
+ } catch {}
4098
+ await symlink(relative(dirname(symlinkPath), agentsDir).replace(/\\/g, "/"), symlinkPath);
4099
+ missing.splice(missing.indexOf(name), 1);
4100
+ installed.push(name);
4101
+ if (isStale) externalStale.push(name);
4102
+ } catch {
4103
+ missing.splice(missing.indexOf(name), 1);
4104
+ externalFailed.push(name);
4105
+ }
4106
+ }
4107
+ }
4108
+ if (installed.length > 0 || stale.length > 0 || missing.length > 0 || externalFailed.length > 0) await updateSkillsGitignore(gitignorePath, existingContent, [
4109
+ ...installed,
4110
+ ...missing,
4111
+ ...externalFailed
4112
+ ], symlinkFn);
4113
+ const parts = [`installed ${installed.length}`];
4114
+ if (stale.length > 0) parts.push(`pruned: ${stale.join(", ")}`);
4115
+ if (externalStale.length > 0) parts.push(`stale: ${externalStale.join(", ")} (run \`holocron skills update\` to refresh)`);
4116
+ if (externalFailed.length > 0) parts.push(`fetch failed: ${externalFailed.join(", ")}`);
4117
+ if (missing.length > 0) parts.push(`unknown: ${missing.join(", ")}`);
4118
+ return parts.join("; ");
4119
+ }
4120
+ function parsePreviousSkills(gitignoreContent, symlinkFn) {
4121
+ if (!gitignoreContent.includes(GITIGNORE_BLOCK_START)) return [];
4122
+ const startIdx = gitignoreContent.indexOf(GITIGNORE_BLOCK_START);
4123
+ const endIdx = gitignoreContent.indexOf(GITIGNORE_BLOCK_END, startIdx);
4124
+ const block = endIdx !== -1 ? gitignoreContent.slice(startIdx, endIdx) : gitignoreContent.slice(startIdx);
4125
+ const placeholder = "__placeholder__";
4126
+ const symlinkPrefix = `/${symlinkFn(placeholder)}`.replace(placeholder, "");
4127
+ return block.split("\n").filter((line) => line.startsWith(symlinkPrefix)).map((line) => line.slice(symlinkPrefix.length));
4128
+ }
4129
+ async function copyDirRecursive(src, dest) {
4130
+ await mkdir(dest, { recursive: true });
4131
+ const entries = await readdir(src, { withFileTypes: true });
4132
+ for (const entry of entries) {
4133
+ const srcPath = join(src, entry.name);
4134
+ const destPath = join(dest, entry.name);
4135
+ if (entry.isDirectory()) await copyDirRecursive(srcPath, destPath);
4136
+ else await copyFile(srcPath, destPath);
4137
+ }
4138
+ }
4139
+ async function updateSkillsGitignore(gitignorePath, existingContent, skills, symlinkFn) {
4140
+ const entries = [`/${AGENTS_SKILLS_ROOT}/`, ...skills.map((n) => `/${symlinkFn(n)}`)];
4141
+ const block = [
4142
+ GITIGNORE_BLOCK_START,
4143
+ ...entries,
4144
+ GITIGNORE_BLOCK_END
4145
+ ].join("\n");
4146
+ let content;
4147
+ if (existingContent.includes(GITIGNORE_BLOCK_START)) {
4148
+ const start = existingContent.indexOf(GITIGNORE_BLOCK_START);
4149
+ const end = existingContent.indexOf(GITIGNORE_BLOCK_END, start);
4150
+ const afterBlock = end !== -1 ? existingContent.slice(end + 40) : "\n";
4151
+ content = existingContent.slice(0, start) + block + afterBlock;
4152
+ } else content = (existingContent.trimEnd() ? existingContent.trimEnd() + "\n\n" : "") + block + "\n";
4153
+ await writeFile(gitignorePath, content, "utf8");
4154
+ }
4155
+ //#endregion
4156
+ //#region src/commands/setup/run-setup.ts
4157
+ /**
4158
+ * `holocron setup` — orchestrates per-capability setup actions across
4159
+ * every plugin loaded from `holocron.config.json`.
4160
+ *
4161
+ * Per CLAUDE.md soft-skip: each step is wrapped in a try/catch and
4162
+ * failures don't abort subsequent capabilities. The summary at the end
4163
+ * reports counts so the operator can see what worked + what didn't.
4164
+ *
4165
+ * Per the Standards: when `ctx.dryRun` is true, mutating calls are
4166
+ * replaced with "would" log lines. Read-only probes (e.g.,
4167
+ * `vault.list`) still run so the operator sees real state.
4168
+ *
4169
+ * The orchestrator knows about specific capability methods by name
4170
+ * (e.g., `source.enableVulnerabilityAlerts`). This deliberate coupling
4171
+ * makes the "what does setup do" contract explicit and concrete —
4172
+ * decoupling via a per-capability `setupSteps()` method would be more
4173
+ * extensible but pushes the same knowledge into N plugins instead of
4174
+ * one central place.
4175
+ */
4176
+ const { workflowHeader: workflowHeader$2 } = createHeader({
4177
+ source: "packages/cli/src/commands/setup/run-setup.ts",
4178
+ tool: "holocron setup"
4179
+ });
3984
4180
  async function runSetup(input) {
3985
4181
  const print = input.print ?? ((line) => console.log(line));
3986
4182
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
@@ -4043,7 +4239,11 @@ async function runSetup(input) {
4043
4239
  for (const entry of workflows) {
4044
4240
  const name = typeof entry === "string" ? entry : entry.name;
4045
4241
  const rawWith = typeof entry === "object" ? entry.with : void 0;
4046
- const withOverrides = rawWith ? normalizeWorkflowWith(rawWith) : void 0;
4242
+ const normalized = rawWith ? normalizeWorkflowWith(rawWith) : void 0;
4243
+ const withOverrides = name === "lint" ? {
4244
+ "enable-auto-commit": true,
4245
+ ...normalized ?? {}
4246
+ } : normalized;
4047
4247
  const additionalPaths = (typeof entry === "object" ? entry.paths : void 0) ?? (name === "deploy" && rawWith ? deriveDeployPaths(rawWith) : void 0);
4048
4248
  if (name === "test" && withOverrides) {
4049
4249
  const runUnit = withOverrides["run-unit"];
@@ -4068,14 +4268,14 @@ async function runSetup(input) {
4068
4268
  if (previewCfg) {
4069
4269
  const paths = additionalPaths;
4070
4270
  steps.push(await runStep("source", "write workflow deploy (with preview)", dryRun, async () => {
4071
- await source.writeWorkflowFile("deploy.yml", workflowHeader() + generateCombinedDeployContent(withOverrides, paths, previewCfg));
4271
+ await source.writeWorkflowFile("deploy.yml", `${workflowHeader$2()}${generateCombinedDeployContent(withOverrides, paths, previewCfg)}`);
4072
4272
  }));
4073
4273
  print(formatStep(steps[steps.length - 1]));
4074
4274
  continue;
4075
4275
  }
4076
4276
  }
4077
4277
  steps.push(await runStep("source", `write workflow ${name}`, dryRun, async () => {
4078
- await source.writeWorkflowFile(`${name}.yml`, workflowHeader() + generateThinCallerContent(name, withOverrides, additionalPaths));
4278
+ await source.writeWorkflowFile(`${name}.yml`, `${workflowHeader$2()}${generateThinCallerContent(name, withOverrides, additionalPaths)}`);
4079
4279
  }));
4080
4280
  print(formatStep(steps[steps.length - 1]));
4081
4281
  }
@@ -4083,41 +4283,49 @@ async function runSetup(input) {
4083
4283
  if (loader.has("source") && (config.workflows ?? []).map((e) => typeof e === "string" ? e : e.name).includes("bookkeeping")) {
4084
4284
  const source = loader.get("source");
4085
4285
  steps.push(await runStep("source", "write .github/labeler.yml", dryRun, async () => {
4086
- await source.writeRepoFile(".github/labeler.yml", labelerConfig());
4286
+ await source.writeRepoFile(".github/labeler.yml", `${workflowHeader$2()}${labeler_default}`);
4087
4287
  }));
4088
4288
  print(formatStep(steps[steps.length - 1]));
4089
4289
  }
4090
4290
  if (loader.has("source") && effectivePreset !== "none") {
4091
4291
  const source = loader.get("source");
4092
4292
  steps.push(await runStep("source", "write .github/dependabot.yml", dryRun, async () => {
4093
- await source.writeRepoFile(".github/dependabot.yml", workflowHeader("packages/cli/src/commands/setup.ts") + dependabot_default);
4293
+ await source.writeRepoFile(".github/dependabot.yml", `${workflowHeader$2()}${dependabot_default}`);
4294
+ }));
4295
+ print(formatStep(steps[steps.length - 1]));
4296
+ steps.push(await runStep("source", "write .github/dco.yml", dryRun, async () => {
4297
+ await source.writeRepoFile(".github/dco.yml", `${workflowHeader$2()}${dco_default}`);
4094
4298
  }));
4095
4299
  print(formatStep(steps[steps.length - 1]));
4096
4300
  }
4097
4301
  if (loader.has("source")) {
4098
4302
  const source = loader.get("source");
4099
4303
  steps.push(await runStep("source", "write .github/config.yml", dryRun, async () => {
4100
- await source.writeRepoFile(".github/config.yml", SENTIMENT_BOT_CONFIG);
4304
+ await source.writeRepoFile(".github/config.yml", `${workflowHeader$2()}${config_default}`);
4101
4305
  }));
4102
4306
  print(formatStep(steps[steps.length - 1]));
4103
4307
  steps.push(await runStep("source", "write .alexrc.json", dryRun, async () => {
4104
- await source.writeRepoFile(".alexrc.json", ALEX_CONFIG);
4308
+ await source.writeRepoFile(".alexrc.json", createRcConfig());
4309
+ }));
4310
+ print(formatStep(steps[steps.length - 1]));
4311
+ steps.push(await runStep("source", "write .alexignore", dryRun, async () => {
4312
+ await source.writeRepoFile(".alexignore", createIgnoreConfig());
4105
4313
  }));
4106
4314
  print(formatStep(steps[steps.length - 1]));
4107
4315
  steps.push(await runStep("source", "write .editorconfig", dryRun, async () => {
4108
- await source.writeRepoFile(".editorconfig", editorconfigContent());
4316
+ await source.writeRepoFile(".editorconfig", createConfig$2());
4109
4317
  }));
4110
4318
  print(formatStep(steps[steps.length - 1]));
4111
4319
  steps.push(await runStep("source", "write .editorconfig-checker.json", dryRun, async () => {
4112
- await source.writeRepoFile(".editorconfig-checker.json", EDITORCONFIG_CHECKER_CONFIG);
4320
+ await source.writeRepoFile(".editorconfig-checker.json", createConfig$1());
4113
4321
  }));
4114
4322
  print(formatStep(steps[steps.length - 1]));
4115
4323
  steps.push(await runStep("source", "write devmoji.config.cjs", dryRun, async () => {
4116
- await source.writeRepoFile("devmoji.config.cjs", devmojiConfigContent());
4324
+ await source.writeRepoFile("devmoji.config.cjs", createConfig$3());
4117
4325
  }));
4118
4326
  print(formatStep(steps[steps.length - 1]));
4119
4327
  steps.push(await runStep("source", "write .husky/prepare-commit-msg", dryRun, async () => {
4120
- await source.writeRepoFile(".husky/prepare-commit-msg", prepareCommitMsgHookContent());
4328
+ await source.writeRepoFile(".husky/prepare-commit-msg", createConfig());
4121
4329
  }));
4122
4330
  print(formatStep(steps[steps.length - 1]));
4123
4331
  {
@@ -4131,7 +4339,7 @@ async function runSetup(input) {
4131
4339
  message: "no test workflow configured"
4132
4340
  });
4133
4341
  else steps.push(await runStep("source", "write codecov.yml", dryRun, async () => {
4134
- const content = existing != null ? mergeCodecovComponents(existing, packages) : codecovContent(packages);
4342
+ const content = existing != null ? mergeCodecovComponents(existing, packages) : createConfig$4(packages);
4135
4343
  await source.writeRepoFile("codecov.yml", content);
4136
4344
  return packages.length > 0 ? `${packages.length} components` : "no components";
4137
4345
  }));
@@ -4434,296 +4642,42 @@ async function runSetup(input) {
4434
4642
  const summaryLine = ` ${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`;
4435
4643
  print(summary.fail > 0 ? style.fail(summaryLine.trim()) : style.success(summaryLine.trim()));
4436
4644
  const skippedSteps = steps.filter((s) => s.status === "skip");
4437
- if (skippedSteps.length > 0) {
4438
- print("");
4439
- print(style.hint(" Skipped:"));
4440
- for (const s of skippedSteps)
4441
- /* v8 ignore next -- all skip steps set message; empty fallback is defensive */
4442
- print(style.hint(` · ${s.step}${s.message ? ` (${s.message})` : ""}`));
4443
- }
4444
- if (steps.some((s) => s.reason === "permissions")) {
4445
- print("");
4446
- print(style.warn("Some steps failed with 403 (insufficient token permissions)."));
4447
- print(style.hint(" Repo-scoped operations (rulesets, settings, workflows) require a"));
4448
- print(style.hint(" fine-grained PAT passed via --token or HOLOCRON_ADMIN_TOKEN:"));
4449
- print("");
4450
- print(style.hint(" · Administration — read and write"));
4451
- print(style.hint(" · Code scanning alerts — read and write"));
4452
- print(style.hint(" · Contents — read and write"));
4453
- print(style.hint(" · Secret scanning alerts — read and write"));
4454
- print(style.hint(" · Workflows — read and write"));
4455
- print(style.hint(" · Metadata — read (added automatically)"));
4456
- print("");
4457
- print(style.hint(" Org-scoped operations (teams, custom properties) require"));
4458
- print(style.hint(" HOLOCRON_ORG_TOKEN — a fine-grained PAT with resource owner set to the org:"));
4459
- print("");
4460
- print(style.hint(" · Administration — read and write (repository permission)"));
4461
- print(style.hint(" · Members — read (organization permission)"));
4462
- print(style.hint(" · Organization custom properties — read and write (organization permission)"));
4463
- print(style.hint(" · Metadata — read (repository permission, auto-included)"));
4464
- print("");
4465
- print(style.hint(" Create tokens at: https://github.com/settings/personal-access-tokens/new"));
4466
- print(style.hint(" Then re-run: holocron setup --token <your-admin-pat>"));
4467
- print(style.hint(" Store org token: HOLOCRON_ORG_TOKEN env var or keyring key github.org"));
4468
- }
4469
- return {
4470
- steps,
4471
- summary
4472
- };
4473
- }
4474
- /**
4475
- * Fetch a single SKILL.md from its upstream GitHub source.
4476
- * Verifies the SHA-256 hash when `computedHash` is present in the lock entry.
4477
- */
4478
- async function fetchExternalSkill(entry) {
4479
- if (entry.sourceType !== "github") throw new Error(`unsupported sourceType: ${entry.sourceType}`);
4480
- const url = `https://raw.githubusercontent.com/${entry.source}/HEAD/${entry.skillPath}`;
4481
- const res = await fetch(url);
4482
- if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
4483
- const content = await res.text();
4484
- return {
4485
- content,
4486
- stale: !!entry.computedHash && createHash("sha256").update(content).digest("hex") !== entry.computedHash
4487
- };
4488
- }
4489
- const AGENTS_SKILLS_ROOT = ".agents/skills";
4490
- /** Relative path of the agent-specific symlink. undefined = unsupported agent. */
4491
- const AGENT_SYMLINK_PATHS = { claude: (name) => `.claude/skills/${name}` };
4492
- const GITIGNORE_BLOCK_START = "# managed by holocron setup — skills";
4493
- const GITIGNORE_BLOCK_END = "# end managed by holocron setup — skills";
4494
- async function installSkills({ agent, skills, repoRoot }) {
4495
- const symlinkFn = AGENT_SYMLINK_PATHS[agent];
4496
- if (!symlinkFn) return `agent "${agent}" has no known skill install path — skipping`;
4497
- const require = createRequire(pathToFileURL(join(repoRoot, "package.json")));
4498
- let skillsRoot;
4499
- try {
4500
- skillsRoot = dirname(require.resolve("@theholocron/skills/package.json"));
4501
- } catch {
4502
- if (spawnSync("pnpm", [
4503
- "add",
4504
- "-D",
4505
- "@theholocron/skills"
4506
- ], {
4507
- cwd: repoRoot,
4508
- stdio: "inherit"
4509
- }).status !== 0) throw new Error("failed to auto-install @theholocron/skills");
4510
- try {
4511
- skillsRoot = dirname(require.resolve("@theholocron/skills/package.json"));
4512
- } catch {
4513
- throw new Error("failed to auto-install @theholocron/skills");
4514
- }
4515
- }
4516
- const gitignorePath = join(repoRoot, ".gitignore");
4517
- const existingContent = await readFile(gitignorePath, "utf8").catch(() => "");
4518
- const previouslyInstalled = parsePreviousSkills(existingContent, symlinkFn);
4519
- const currentSet = new Set(skills);
4520
- const stale = previouslyInstalled.filter((n) => !currentSet.has(n));
4521
- for (const name of stale) {
4522
- await rm(join(repoRoot, symlinkFn(name)), { force: true }).catch(() => void 0);
4523
- await rm(join(repoRoot, AGENTS_SKILLS_ROOT, name), {
4524
- recursive: true,
4525
- force: true
4526
- }).catch(() => void 0);
4527
- }
4528
- const installed = [];
4529
- const missing = [];
4530
- for (const name of skills) {
4531
- const srcDir = join(skillsRoot, "src", name);
4532
- try {
4533
- await stat(srcDir);
4534
- } catch {
4535
- missing.push(name);
4536
- continue;
4537
- }
4538
- const agentsDir = join(repoRoot, AGENTS_SKILLS_ROOT, name);
4539
- await copyDirRecursive(srcDir, agentsDir);
4540
- const symlinkPath = join(repoRoot, symlinkFn(name));
4541
- await mkdir(dirname(symlinkPath), { recursive: true });
4542
- try {
4543
- await unlink(symlinkPath);
4544
- } catch {}
4545
- await symlink(relative(dirname(symlinkPath), agentsDir).replace(/\\/g, "/"), symlinkPath);
4546
- installed.push(name);
4547
- }
4548
- const externalFailed = [];
4549
- const externalStale = [];
4550
- if (missing.length > 0) {
4551
- let lock = null;
4552
- try {
4553
- lock = JSON.parse(await readFile(join(skillsRoot, "skills-lock.json"), "utf8"));
4554
- } catch {}
4555
- if (lock?.skills) for (const name of [...missing]) {
4556
- const entry = lock.skills[name];
4557
- if (!entry) continue;
4558
- try {
4559
- const { content, stale: isStale } = await fetchExternalSkill(entry);
4560
- const agentsDir = join(repoRoot, AGENTS_SKILLS_ROOT, name);
4561
- await mkdir(agentsDir, { recursive: true });
4562
- await writeFile(join(agentsDir, "SKILL.md"), content);
4563
- const symlinkPath = join(repoRoot, symlinkFn(name));
4564
- await mkdir(dirname(symlinkPath), { recursive: true });
4565
- try {
4566
- await unlink(symlinkPath);
4567
- } catch {}
4568
- await symlink(relative(dirname(symlinkPath), agentsDir).replace(/\\/g, "/"), symlinkPath);
4569
- missing.splice(missing.indexOf(name), 1);
4570
- installed.push(name);
4571
- if (isStale) externalStale.push(name);
4572
- } catch {
4573
- missing.splice(missing.indexOf(name), 1);
4574
- externalFailed.push(name);
4575
- }
4576
- }
4577
- }
4578
- if (installed.length > 0 || stale.length > 0 || missing.length > 0 || externalFailed.length > 0) await updateSkillsGitignore(gitignorePath, existingContent, [
4579
- ...installed,
4580
- ...missing,
4581
- ...externalFailed
4582
- ], symlinkFn);
4583
- const parts = [`installed ${installed.length}`];
4584
- if (stale.length > 0) parts.push(`pruned: ${stale.join(", ")}`);
4585
- if (externalStale.length > 0) parts.push(`stale: ${externalStale.join(", ")} (run \`holocron skills update\` to refresh)`);
4586
- if (externalFailed.length > 0) parts.push(`fetch failed: ${externalFailed.join(", ")}`);
4587
- if (missing.length > 0) parts.push(`unknown: ${missing.join(", ")}`);
4588
- return parts.join("; ");
4589
- }
4590
- /** Extract skill names from the previous gitignore block so stale dirs can be pruned. */
4591
- function parsePreviousSkills(gitignoreContent, symlinkFn) {
4592
- if (!gitignoreContent.includes(GITIGNORE_BLOCK_START)) return [];
4593
- const startIdx = gitignoreContent.indexOf(GITIGNORE_BLOCK_START);
4594
- const endIdx = gitignoreContent.indexOf(GITIGNORE_BLOCK_END, startIdx);
4595
- const block = endIdx !== -1 ? gitignoreContent.slice(startIdx, endIdx) : gitignoreContent.slice(startIdx);
4596
- const placeholder = "__placeholder__";
4597
- const symlinkPrefix = `/${symlinkFn(placeholder)}`.replace(placeholder, "");
4598
- return block.split("\n").filter((line) => line.startsWith(symlinkPrefix)).map((line) => line.slice(symlinkPrefix.length));
4599
- }
4600
- async function copyDirRecursive(src, dest) {
4601
- await mkdir(dest, { recursive: true });
4602
- const entries = await readdir(src, { withFileTypes: true });
4603
- for (const entry of entries) {
4604
- const srcPath = join(src, entry.name);
4605
- const destPath = join(dest, entry.name);
4606
- if (entry.isDirectory()) await copyDirRecursive(srcPath, destPath);
4607
- else await copyFile(srcPath, destPath);
4645
+ if (skippedSteps.length > 0) {
4646
+ print("");
4647
+ print(style.hint(" Skipped:"));
4648
+ for (const s of skippedSteps)
4649
+ /* v8 ignore next -- all skip steps set message; empty fallback is defensive */
4650
+ print(style.hint(` · ${s.step}${s.message ? ` (${s.message})` : ""}`));
4608
4651
  }
4609
- }
4610
- async function updateSkillsGitignore(gitignorePath, existingContent, skills, symlinkFn) {
4611
- const entries = [`/${AGENTS_SKILLS_ROOT}/`, ...skills.map((n) => `/${symlinkFn(n)}`)];
4612
- const block = [
4613
- GITIGNORE_BLOCK_START,
4614
- ...entries,
4615
- GITIGNORE_BLOCK_END
4616
- ].join("\n");
4617
- let content;
4618
- if (existingContent.includes(GITIGNORE_BLOCK_START)) {
4619
- const start = existingContent.indexOf(GITIGNORE_BLOCK_START);
4620
- const end = existingContent.indexOf(GITIGNORE_BLOCK_END, start);
4621
- const afterBlock = end !== -1 ? existingContent.slice(end + 40) : "\n";
4622
- content = existingContent.slice(0, start) + block + afterBlock;
4623
- } else content = (existingContent.trimEnd() ? existingContent.trimEnd() + "\n\n" : "") + block + "\n";
4624
- await writeFile(gitignorePath, content, "utf8");
4625
- }
4626
- const AGENTS_PROMPTS_ROOT = ".agents/prompts";
4627
- const PROMPTS_GITIGNORE_START = "# managed by holocron setup prompts";
4628
- const PROMPTS_GITIGNORE_END = "# end managed by holocron setup — prompts";
4629
- async function installAgentPrompts({ repoRoot }) {
4630
- const promptsDir = join(repoRoot, AGENTS_PROMPTS_ROOT);
4631
- await mkdir(promptsDir, { recursive: true });
4632
- for (const [filename, content] of Object.entries(AGENT_PROMPTS)) await writeFile(join(promptsDir, filename), content, "utf8");
4633
- const gitignorePath = join(repoRoot, ".gitignore");
4634
- const existing = await readFile(gitignorePath, "utf8").catch(() => "");
4635
- const block = [
4636
- PROMPTS_GITIGNORE_START,
4637
- `/${AGENTS_PROMPTS_ROOT}/`,
4638
- PROMPTS_GITIGNORE_END
4639
- ].join("\n");
4640
- let updated;
4641
- if (existing.includes(PROMPTS_GITIGNORE_START)) {
4642
- const start = existing.indexOf(PROMPTS_GITIGNORE_START);
4643
- const end = existing.indexOf(PROMPTS_GITIGNORE_END, start);
4644
- const afterBlock = end !== -1 ? existing.slice(end + 41) : "\n";
4645
- updated = existing.slice(0, start) + block + afterBlock;
4646
- } else updated = (existing.trimEnd() ? existing.trimEnd() + "\n\n" : "") + block + "\n";
4647
- await writeFile(gitignorePath, updated, "utf8");
4648
- return `wrote ${Object.keys(AGENT_PROMPTS).length} prompt files to ${AGENTS_PROMPTS_ROOT}/`;
4649
- }
4650
- async function writeIfAbsent(filePath, content) {
4651
- try {
4652
- await access(filePath);
4653
- return false;
4654
- } catch {
4655
- await mkdir(dirname(filePath), { recursive: true });
4656
- await writeFile(filePath, content, "utf8");
4657
- return true;
4652
+ if (steps.some((s) => s.reason === "permissions")) {
4653
+ print("");
4654
+ print(style.warn("Some steps failed with 403 (insufficient token permissions)."));
4655
+ print(style.hint(" Repo-scoped operations (rulesets, settings, workflows) require a"));
4656
+ print(style.hint(" fine-grained PAT passed via --token or HOLOCRON_ADMIN_TOKEN:"));
4657
+ print("");
4658
+ print(style.hint(" · Administration — read and write"));
4659
+ print(style.hint(" · Code scanning alerts — read and write"));
4660
+ print(style.hint(" · Contents — read and write"));
4661
+ print(style.hint(" · Secret scanning alerts — read and write"));
4662
+ print(style.hint(" · Workflows — read and write"));
4663
+ print(style.hint(" · Metadata — read (added automatically)"));
4664
+ print("");
4665
+ print(style.hint(" Org-scoped operations (teams, custom properties) require"));
4666
+ print(style.hint(" HOLOCRON_ORG_TOKEN a fine-grained PAT with resource owner set to the org:"));
4667
+ print("");
4668
+ print(style.hint(" · Administration — read and write (repository permission)"));
4669
+ print(style.hint(" · Members — read (organization permission)"));
4670
+ print(style.hint(" · Organization custom properties read and write (organization permission)"));
4671
+ print(style.hint(" · Metadata — read (repository permission, auto-included)"));
4672
+ print("");
4673
+ print(style.hint(" Create tokens at: https://github.com/settings/personal-access-tokens/new"));
4674
+ print(style.hint(" Then re-run: holocron setup --token <your-admin-pat>"));
4675
+ print(style.hint(" Store org token: HOLOCRON_ORG_TOKEN env var or keyring key github.org"));
4658
4676
  }
4659
- }
4660
- async function installEngineeringStructure({ repoRoot }) {
4661
- const results = [];
4662
- const writes = [
4663
- [join(repoRoot, "docs/wiki/decisions/template.md"), DECISIONS_TEMPLATE],
4664
- [join(repoRoot, "docs/wiki/decisions/README.md"), DECISIONS_README],
4665
- [join(repoRoot, "docs/wiki/standards/README.md"), STANDARDS_README],
4666
- [join(repoRoot, "docs/wiki/specifications/README.md"), SPECIFICATIONS_README]
4667
- ];
4668
- for (const [path, content] of writes) if (await writeIfAbsent(path, content)) results.push(path.replace(repoRoot + "/", ""));
4669
- return results.length > 0 ? `created: ${results.join(", ")}` : "all files already exist — nothing to write";
4670
- }
4671
- async function runStep(capability, step, dryRun, body, opts = {}) {
4672
- if (dryRun) return {
4673
- capability,
4674
- step,
4675
- status: "dry-run"
4677
+ return {
4678
+ steps,
4679
+ summary
4676
4680
  };
4677
- try {
4678
- const note = await body();
4679
- const result = {
4680
- capability,
4681
- step,
4682
- status: "ok"
4683
- };
4684
- if (typeof note === "string") result.message = note;
4685
- return result;
4686
- } catch (err) {
4687
- if (err instanceof ProviderApiError$1) {
4688
- if (err.status !== void 0 && opts.skipCodes?.includes(err.status)) return {
4689
- capability,
4690
- step,
4691
- status: "skip",
4692
- message: err.message
4693
- };
4694
- if (err.status === 403) {
4695
- const reason = classify403(err);
4696
- return {
4697
- capability,
4698
- step,
4699
- status: reason === "plan" ? "skip" : "fail",
4700
- message: err.message,
4701
- reason
4702
- };
4703
- }
4704
- }
4705
- return {
4706
- capability,
4707
- step,
4708
- status: "fail",
4709
- message: err instanceof Error ? err.message : String(err)
4710
- };
4711
- }
4712
- }
4713
- function classify403(err) {
4714
- const detailText = typeof err.details === "string" ? err.details : typeof err.details === "object" && err.details !== null && "message" in err.details ? String(err.details.message) : "";
4715
- const text = `${err.message} ${detailText}`.toLowerCase();
4716
- if (text.includes("advanced security") || text.includes("not enabled for this repository") || text.includes("upgrade") || text.includes("not available on")) return "plan";
4717
- return "permissions";
4718
- }
4719
- function formatStep(step) {
4720
- const tag = step.reason === "permissions" ? " [permissions]" : step.reason === "plan" ? " [plan restriction]" : "";
4721
- const detail = step.message ? style.dim(` (${step.message})`) : "";
4722
- const label = `${step.step}${tag}${detail}`;
4723
- if (step.status === "ok") return ` ${style.success(label)}`;
4724
- if (step.status === "fail") return ` ${style.fail(label)}`;
4725
- if (step.status === "dry-run") return ` ${style.dim(`… ${label}`)}`;
4726
- return ` ${style.dim(`· ${label}`)}`;
4727
4681
  }
4728
4682
  //#endregion
4729
4683
  //#region src/commands/skills.ts
@@ -4930,7 +4884,193 @@ async function runSyncReadme(input) {
4930
4884
  };
4931
4885
  }
4932
4886
  //#endregion
4887
+ //#region src/commands/sync-wiki.ts
4888
+ function resolveToken(input) {
4889
+ return input.token ?? input.context.cliToken ?? process.env.HOLOCRON_READ_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN;
4890
+ }
4891
+ function deriveBasepath(domain, repoName) {
4892
+ if (domain) {
4893
+ const slashIdx = domain.indexOf("/");
4894
+ if (slashIdx !== -1) return domain.slice(slashIdx + 1);
4895
+ }
4896
+ return repoName;
4897
+ }
4898
+ function titleCase(s) {
4899
+ return s.charAt(0).toUpperCase() + s.slice(1);
4900
+ }
4901
+ function extractFromJson(raw, repoName) {
4902
+ let config;
4903
+ try {
4904
+ config = JSON.parse(raw);
4905
+ } catch {
4906
+ return null;
4907
+ }
4908
+ const providers = config.providers;
4909
+ if (!providers?.wiki) return null;
4910
+ const wikiEntry = providers.wiki;
4911
+ let domain;
4912
+ let subtitle;
4913
+ let icon;
4914
+ if (Array.isArray(wikiEntry) && wikiEntry.length === 2) {
4915
+ const opts = wikiEntry[1];
4916
+ domain = typeof opts.domain === "string" ? opts.domain : void 0;
4917
+ subtitle = typeof opts.subtitle === "string" ? opts.subtitle : void 0;
4918
+ icon = typeof opts.icon === "string" ? opts.icon : void 0;
4919
+ }
4920
+ if (!subtitle && typeof config.description === "string") subtitle = config.description;
4921
+ const basepath = deriveBasepath(domain, repoName);
4922
+ return {
4923
+ displayName: titleCase(basepath),
4924
+ basepath,
4925
+ ...subtitle ? { subtitle } : {},
4926
+ ...icon ? { icon } : {}
4927
+ };
4928
+ }
4929
+ function extractFromTs(raw, repoName) {
4930
+ const hasWikiProvider = /providers\s*:\s*\{[^}]*\bwiki\b/s.test(raw);
4931
+ const hasWikiPreset = /\bwikiCapability\b|\bwiki\s*\(\s*\)/.test(raw);
4932
+ if (!hasWikiProvider && !hasWikiPreset) return null;
4933
+ const domain = raw.match(/\bdomain\s*:\s*["']([^"']+)["']/)?.[1];
4934
+ let subtitle = raw.match(/\bsubtitle\s*:\s*["']([^"']+)["']/)?.[1];
4935
+ const icon = raw.match(/\bicon\s*:\s*["']([^"']+)["']/)?.[1];
4936
+ if (!subtitle) subtitle = raw.match(/\bdescription\s*:\s*["']([^"']+)["']/)?.[1];
4937
+ const basepath = deriveBasepath(domain, repoName);
4938
+ return {
4939
+ displayName: titleCase(basepath),
4940
+ basepath,
4941
+ ...subtitle ? { subtitle } : {},
4942
+ ...icon ? { icon } : {}
4943
+ };
4944
+ }
4945
+ async function discoverWikiProducts(org, token, fetchFn) {
4946
+ const rest = createRestClient({
4947
+ baseUrl: "https://api.github.com",
4948
+ token,
4949
+ extraHeaders: {
4950
+ accept: "application/vnd.github+json",
4951
+ "x-github-api-version": "2022-11-28"
4952
+ },
4953
+ vendor: "GitHub",
4954
+ fetch: fetchFn
4955
+ });
4956
+ const allRepos = [];
4957
+ let page = 1;
4958
+ while (true) {
4959
+ const batch = await rest.request(`/orgs/${org}/repos`, { query: {
4960
+ per_page: "100",
4961
+ page: String(page),
4962
+ type: "all"
4963
+ } });
4964
+ allRepos.push(...batch);
4965
+ if (batch.length < 100) break;
4966
+ page++;
4967
+ }
4968
+ const products = [];
4969
+ for (const repo of allRepos.filter((r) => !r.archived)) {
4970
+ let product = null;
4971
+ try {
4972
+ const contents = await rest.request(`/repos/${repo.full_name}/contents/holocron.config.json`);
4973
+ if (contents.encoding === "base64") product = extractFromJson(Buffer.from(contents.content.replace(/\s/g, ""), "base64").toString("utf8"), repo.name);
4974
+ } catch {}
4975
+ if (!product) try {
4976
+ const contents = await rest.request(`/repos/${repo.full_name}/contents/holocron.config.ts`);
4977
+ if (contents.encoding === "base64") product = extractFromTs(Buffer.from(contents.content.replace(/\s/g, ""), "base64").toString("utf8"), repo.name);
4978
+ } catch {}
4979
+ if (product) products.push(product);
4980
+ }
4981
+ products.sort((a, b) => a.basepath.localeCompare(b.basepath));
4982
+ return products;
4983
+ }
4984
+ function buildProductsBlock(products) {
4985
+ const lines = ["products:"];
4986
+ for (const p of products) {
4987
+ lines.push(` - display-name: ${p.displayName}`);
4988
+ if (p.subtitle) lines.push(` subtitle: ${p.subtitle}`);
4989
+ if (p.icon) lines.push(` icon: ${p.icon}`);
4990
+ lines.push(` href: /${p.basepath}`);
4991
+ }
4992
+ return lines.join("\n");
4993
+ }
4994
+ async function mergeProducts(docsYmlPath, products) {
4995
+ let content;
4996
+ try {
4997
+ content = await readFile(docsYmlPath, "utf8");
4998
+ } catch {
4999
+ throw new Error(`fern/docs.yml not found at ${docsYmlPath}`);
5000
+ }
5001
+ const newBlock = buildProductsBlock(products);
5002
+ const productBlockRe = /^products:(?:\n[ \t][^\n]*)*/m;
5003
+ if (productBlockRe.test(content)) {
5004
+ const updated = content.replace(productBlockRe, newBlock);
5005
+ if (updated !== content) await writeFile(docsYmlPath, updated, "utf8");
5006
+ return;
5007
+ }
5008
+ const instancesIdx = content.indexOf("\ninstances:");
5009
+ if (instancesIdx !== -1) {
5010
+ await writeFile(docsYmlPath, content.slice(0, instancesIdx + 1) + newBlock + "\n\n" + content.slice(instancesIdx + 1), "utf8");
5011
+ return;
5012
+ }
5013
+ await writeFile(docsYmlPath, content.trimEnd() + "\n\n" + newBlock + "\n", "utf8");
5014
+ }
5015
+ async function runSyncWiki(input) {
5016
+ const config = input.loaded.resolved;
5017
+ const dryRun = input.context.dryRun ?? false;
5018
+ if (!config.providers.wiki) return {
5019
+ capability: "local",
5020
+ step: "sync wiki",
5021
+ status: "skip",
5022
+ message: "no wiki provider configured"
5023
+ };
5024
+ const org = config.org ?? input.context.repo?.split("/")[0];
5025
+ if (!org) return {
5026
+ capability: "local",
5027
+ step: "sync wiki",
5028
+ status: "skip",
5029
+ message: "no org configured"
5030
+ };
5031
+ const token = resolveToken(input);
5032
+ if (!token) return {
5033
+ capability: "local",
5034
+ step: "sync wiki",
5035
+ status: "skip",
5036
+ message: "no GitHub token available (set HOLOCRON_READ_TOKEN or GH_TOKEN)"
5037
+ };
5038
+ if (dryRun) return {
5039
+ capability: "local",
5040
+ step: "sync wiki",
5041
+ status: "dry-run"
5042
+ };
5043
+ const docsYmlPath = join(input.context.repoRoot, "fern", "docs.yml");
5044
+ try {
5045
+ const products = await discoverWikiProducts(org, token, input.fetch);
5046
+ if (products.length === 0) return {
5047
+ capability: "local",
5048
+ step: "sync wiki",
5049
+ status: "skip",
5050
+ message: "no wiki-enabled repos found"
5051
+ };
5052
+ await mergeProducts(docsYmlPath, products);
5053
+ return {
5054
+ capability: "local",
5055
+ step: "sync wiki",
5056
+ status: "ok",
5057
+ message: `${products.length} products`
5058
+ };
5059
+ } catch (err) {
5060
+ return {
5061
+ capability: "local",
5062
+ step: "sync wiki",
5063
+ status: "fail",
5064
+ message: err instanceof Error ? err.message : String(err)
5065
+ };
5066
+ }
5067
+ }
5068
+ //#endregion
4933
5069
  //#region src/commands/sync.ts
5070
+ const { workflowHeader: workflowHeader$1 } = createHeader({
5071
+ source: "packages/cli/src/commands/sync.ts",
5072
+ tool: "holocron sync"
5073
+ });
4934
5074
  const SYNC_STEPS = [
4935
5075
  "labels",
4936
5076
  "properties",
@@ -4940,14 +5080,16 @@ const SYNC_STEPS = [
4940
5080
  "description",
4941
5081
  "homepage",
4942
5082
  "readme",
4943
- "workflows"
5083
+ "workflows",
5084
+ "wiki"
4944
5085
  ];
4945
5086
  const LOCAL_STEPS = /* @__PURE__ */ new Set([
4946
5087
  "keywords",
4947
5088
  "description",
4948
5089
  "homepage",
4949
5090
  "readme",
4950
- "workflows"
5091
+ "workflows",
5092
+ "wiki"
4951
5093
  ]);
4952
5094
  async function runSync(input) {
4953
5095
  const print = input.print ?? ((line) => console.log(line));
@@ -5087,7 +5229,8 @@ async function runSync(input) {
5087
5229
  "description",
5088
5230
  "homepage",
5089
5231
  "readme",
5090
- "workflows"
5232
+ "workflows",
5233
+ "wiki"
5091
5234
  ]) {
5092
5235
  if (requestedSteps !== void 0 && !requestedSteps.includes(stepName)) continue;
5093
5236
  if (stepName === "keywords") {
@@ -5182,7 +5325,11 @@ async function runSync(input) {
5182
5325
  } else for (const entry of workflowEntries) {
5183
5326
  const name = typeof entry === "string" ? entry : entry.name;
5184
5327
  const rawWith = typeof entry === "object" ? entry.with : void 0;
5185
- const withOverrides = rawWith ? normalizeWorkflowWith(rawWith) : void 0;
5328
+ const normalized = rawWith ? normalizeWorkflowWith(rawWith) : void 0;
5329
+ const withOverrides = name === "lint" ? {
5330
+ "enable-auto-commit": true,
5331
+ ...normalized ?? {}
5332
+ } : normalized;
5186
5333
  const additionalPaths = (typeof entry === "object" ? entry.paths : void 0) ?? (name === "deploy" && rawWith ? deriveDeployPaths(rawWith) : void 0);
5187
5334
  if (!KNOWN_WORKFLOWS.has(name)) {
5188
5335
  steps.push({
@@ -5201,7 +5348,7 @@ async function runSync(input) {
5201
5348
  });
5202
5349
  if (previewCfg) {
5203
5350
  steps.push(await runSyncStep("local", "sync workflow deploy (with preview)", dryRun, async () => {
5204
- const content = workflowHeader() + generateCombinedDeployContent(withOverrides, additionalPaths, previewCfg);
5351
+ const content = `${workflowHeader$1()}${generateCombinedDeployContent(withOverrides, additionalPaths, previewCfg)}`;
5205
5352
  await writeWorkflowFile(input.context.repoRoot, "deploy.yml", content);
5206
5353
  }));
5207
5354
  print(formatSyncStep(steps[steps.length - 1]));
@@ -5209,12 +5356,20 @@ async function runSync(input) {
5209
5356
  }
5210
5357
  }
5211
5358
  steps.push(await runSyncStep("local", `sync workflow ${name}`, dryRun, async () => {
5212
- const content = workflowHeader() + generateThinCallerContent(name, withOverrides, additionalPaths);
5359
+ const content = `${workflowHeader$1()}${generateThinCallerContent(name, withOverrides, additionalPaths)}`;
5213
5360
  await writeWorkflowFile(input.context.repoRoot, `${name}.yml`, content);
5214
5361
  }));
5215
5362
  print(formatSyncStep(steps[steps.length - 1]));
5216
5363
  }
5217
5364
  }
5365
+ if (stepName === "wiki") {
5366
+ const result = await runSyncWiki({
5367
+ loaded: input.loaded,
5368
+ context: input.context
5369
+ });
5370
+ steps.push(result);
5371
+ print(formatSyncStep(result));
5372
+ }
5218
5373
  }
5219
5374
  const summary = steps.reduce((acc, s) => {
5220
5375
  if (s.status === "ok") acc.ok += 1;
@@ -5326,47 +5481,41 @@ var audit_default = "name: Audit\n\non: # yamllint disable-line rule:truthy\n w
5326
5481
  //#region src/templates/workflows/bookkeeping.yml
5327
5482
  var bookkeeping_default = "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";
5328
5483
  //#endregion
5329
- //#region src/templates/workflows/cleanup-preview.yml
5330
- var cleanup_preview_default = "name: Clean up Preview\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n cloudflare-project:\n description: >\n Cloudflare Pages project name. Falls back to the CLOUDFLARE_PAGES_PROJECT\n org variable when omitted.\n required: false\n type: string\n default: \"\"\n\njobs:\n cleanup:\n name: Clean up Preview\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - name: Delete Cloudflare Pages deployments for branch\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n env:\n CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n BRANCH: ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n DEPLOYMENTS=$(curl -s \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" \\\n | jq -r --arg b \"$BRANCH\" \\\n '.result[] | select(.deployment_trigger.metadata.branch == $b) | .id')\n\n if [ -z \"$DEPLOYMENTS\" ]; then\n echo \"No deployments found for branch ${BRANCH} — nothing to clean up.\"\n exit 0\n fi\n\n for id in $DEPLOYMENTS; do\n echo \"Deleting CF Pages deployment: $id\"\n curl -s -X DELETE \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments/${id}?force=true\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" | jq -r 'if .success then \" ✓ deleted\" else \" ✗ \\(.errors[0].message)\" end'\n done\n\n # Mark the GitHub Deployment environment as inactive.\n ENV_NAME=\"${PROJECT} (Preview)\"\n gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n | jq -r \".[] | select(.environment == \\\"${ENV_NAME}\\\") | .id\" \\\n | while read -r deploy_id; do\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${deploy_id}/statuses\" \\\n --method POST --field state=inactive 2>/dev/null || true\n done\n";
5331
- //#endregion
5332
- //#region src/templates/workflows/codeql.yml
5333
- var codeql_default = "name: CodeQL\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n language:\n description: CodeQL language to analyze\n type: string\n required: false\n default: javascript-typescript\n\njobs:\n analyze:\n name: Analyze (${{ inputs.language }})\n permissions:\n actions: read\n contents: read\n security-events: write\n runs-on: ubuntu-latest\n timeout-minutes: 45\n # Do not cancel in-progress security scans.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Initialize CodeQL\n with:\n languages: ${{ inputs.language }}\n\n - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Autobuild\n\n - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Analyze\n with:\n category: /language:${{ inputs.language }}\n";
5334
- //#endregion
5335
5484
  //#region src/templates/workflows/dependencies.yml
5336
5485
  var dependencies_default = "name: Dependencies\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n merge-token:\n description: >\n Optional privileged token for auto-merge. Falls back to GITHUB_TOKEN.\n Required when branch protection enforces required reviews — GITHUB_TOKEN\n cannot approve its own PRs.\n required: false\n\njobs:\n dependabot:\n name: Update the dependencies\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n if: github.event.pull_request.user.login == 'dependabot[bot]'\n steps:\n - uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0\n name: Fetch Dependabot metadata\n id: metadata\n\n - run: gh pr merge --auto --squash \"$PR_URL\"\n # --squash is intentional: repo protection sets allow_merge_commit: false,\n # so --merge would fail on any repo using the standard preset.\n name: Enable auto-merge for Dependabot PRs\n if: steps.metadata.outputs.update-type == 'version-update:semver-patch'\n env:\n PR_URL: ${{ github.event.pull_request.html_url }}\n GH_TOKEN: ${{ secrets.merge-token || github.token }}\n";
5337
5486
  //#endregion
5338
5487
  //#region src/templates/workflows/deploy.yml
5339
5488
  var deploy_default = "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";
5340
5489
  //#endregion
5341
- //#region src/templates/workflows/deploy-preview.yml
5342
- var deploy_preview_default = "name: Deploy Preview\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 name:\n description: Repo name prefix used to filter the docs site package (<name>-site); omit to run pnpm -C docs build\n required: false\n type: string\n default: \"\"\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 cloudflare-project:\n description: >\n Cloudflare Pages project name. Falls back to the CLOUDFLARE_PAGES_PROJECT\n org variable when omitted — set that variable once and all repos get previews\n without per-repo config.\n required: false\n type: string\n default: \"\"\n\njobs:\n deploy-preview:\n name: Deploy Preview\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\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' && inputs.name != '' }}\n env:\n SITE_NAME: ${{ inputs.name }}\n run: pnpm --filter @theholocron/\"$SITE_NAME\"-site build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name == '' }}\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 SITE_NAME: ${{ inputs.name }}\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 if [ -n \"$SITE_NAME\" ]; then\n # Site built with base: /<name>/ — nest under _site/<name>/ so asset\n # paths match (/name/_astro/... → served at /name/_astro/...).\n mkdir -p \"_site/${SITE_NAME}\"\n cp -r docs/dist/. \"_site/${SITE_NAME}/\"\n # Redirect root to the base path so the preview URL lands correctly.\n printf \"/ /%s/ 301\\n\" \"$SITE_NAME\" > _site/_redirects\n else\n cp -r docs/dist/. _site/\n fi\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 - name: Install Wrangler\n # The action's install step runs npm/pnpm in the repo root; pnpm fails\n # because it rejects adding to a workspace root without -w, and npm fails\n # because package.json contains pnpm catalog: references it cannot parse.\n # Pre-installing from /tmp (no package.json) lets the action find wrangler\n # already on PATH and skip its own install entirely.\n working-directory: /tmp\n run: npm install -g wrangler@4\n\n - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0\n id: deploy\n name: Deploy to Cloudflare Pages\n # Skip when neither the per-repo input nor the org variable is set.\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n with:\n apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n packageManager: npm\n command: >-\n pages deploy _site\n --project-name ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n --branch ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n --commit-dirty=true\n\n - name: Report preview URL\n if: ${{ steps.deploy.outputs.pages-deployment-alias-url != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n ALIAS: ${{ steps.deploy.outputs.pages-deployment-alias-url }}\n SITE_NAME: ${{ inputs.name }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n run: |\n PREVIEW_URL=\"${ALIAS%/}${SITE_NAME:+/${SITE_NAME}}/\"\n # GITHUB_HEAD_REF is the PR branch name — links the deployment to this PR's sidebar widget.\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"%s\",\"description\":\"Cloudflare Pages\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\" \"${PROJECT} (Preview)\")\n DEPLOY_ID=$(echo \"$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";
5343
- //#endregion
5344
5490
  //#region src/templates/workflows/greetings.yml
5345
5491
  var greetings_default = "name: Greetings\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n greeting:\n name: Greet first-time contributors\n permissions:\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n # Group by the issue/PR number so duplicate events don't race each other.\n steps:\n - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n name: Greet on first contribution\n with:\n script: |\n // Only greet on the initial open — ignore synchronize, reopened, etc.\n if (context.payload.action !== 'opened') return;\n\n const actor = context.actor;\n const { owner, repo } = context.repo;\n\n // Payload inspection is more reliable than context.eventName for detecting\n // whether this is an issue vs. PR event — works regardless of how GitHub\n // propagates event names through workflow_call chains.\n const isIssue = !!context.payload.issue && !context.payload.pull_request;\n // listForRepo returns both issues and PRs (GitHub treats PRs as issues),\n // sorted newest-first. Filter by type to track first-issue and first-PR\n // independently, and avoid search-index eventual-consistency lag.\n const { data: recent } = await github.rest.issues.listForRepo({\n owner, repo,\n creator: actor,\n state: 'all',\n per_page: 100\n });\n\n const sameType = recent.filter(item =>\n isIssue ? !item.pull_request : !!item.pull_request\n );\n\n if (sameType.length !== 1) return;\n const body = isIssue\n ? `Hey @${actor}!\\n\\nWe really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`\n : `Hey @${actor}!\\n\\nWe really appreciate you taking the time to help out with this PR. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`;\n\n await github.rest.issues.createComment({\n owner,\n repo,\n issue_number: context.issue.number,\n body\n });\n";
5346
5492
  //#endregion
5347
5493
  //#region src/templates/workflows/lint.yml
5348
5494
  var lint_default = "name: Lint\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n eslint-config:\n description: >\n Filename for the ESLint flat config used by super-linter for\n JavaScript, JSX, TSX, and TypeScript (ES) files. Defaults to\n eslint.config.ts (the org standard). Note: ESLint 9 requires\n --flag unstable_ts_config to load .ts configs; if super-linter\n cannot load it, override with eslint.config.mjs or eslint.config.js.\n type: string\n required: false\n default: eslint.config.ts\n prettier-config:\n description: >\n Filename for the Prettier config. Defaults to prettier.config.ts\n (the org standard). Prettier 3.x loads .ts configs natively.\n type: string\n required: false\n default: prettier.config.ts\n yaml-config:\n description: >\n Filename for the yamllint config. Defaults to yamllint.config.yml.\n type: string\n required: false\n default: yamllint.config.yml\n enable-auto-commit:\n description: >\n Auto-commit super-linter fixes as a verified commit via a GitHub App.\n Requires SUPER_LINTER_APP_ID and SUPER_LINTER_PRIVATE_KEY secrets.\n type: boolean\n required: false\n default: false\n secrets:\n SUPER_LINTER_APP_ID:\n required: false\n SUPER_LINTER_PRIVATE_KEY:\n required: false\n\njobs:\n super-lint:\n name: Lint entire codebase\n permissions:\n contents: write\n issues: write\n statuses: write\n runs-on: ubuntu-latest\n timeout-minutes: 30\n env:\n APP_ID_SET: ${{ secrets.SUPER_LINTER_APP_ID != '' }}\n steps:\n - name: Generate GitHub App token\n id: app-token\n # Runs before checkout so the token is used as the checkout credential,\n # which makes the subsequent push go through the App and produce a\n # Verified commit. Skipped when auto-commit is disabled or secrets unset.\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true'\n uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0\n with:\n app-id: ${{ secrets.SUPER_LINTER_APP_ID }}\n private-key: ${{ secrets.SUPER_LINTER_PRIVATE_KEY }}\n\n - name: Resolve App bot identity\n id: app-bot\n # GitHub marks commits as Verified when the author email matches the\n # App bot's noreply address (<numeric-id>+<slug>[bot]@users.noreply.github.com).\n # The numeric ID must be fetched via the API — it differs from the App ID.\n # app-slug is passed via env rather than interpolated into the script to\n # prevent code injection (CWE-78).\n if: steps.app-token.conclusion == 'success'\n run: |\n BOT_SLUG=\"${APP_SLUG}[bot]\"\n BOT_ID=$(gh api \"/users/${BOT_SLUG}\" --jq .id)\n echo \"name=${BOT_SLUG}\" >> \"$GITHUB_OUTPUT\"\n echo \"email=${BOT_ID}+${BOT_SLUG}@users.noreply.github.com\" >> \"$GITHUB_OUTPUT\"\n env:\n GH_TOKEN: ${{ steps.app-token.outputs.token }}\n APP_SLUG: ${{ steps.app-token.outputs.app-slug }}\n\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n # Use the App token when available so the push credential is the App\n # bot — GitHub marks those commits as Verified automatically.\n token: ${{ steps.app-token.outputs.token || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n\n - name: Detect project features\n # Writes VALIDATE_*/FIX_* to GITHUB_ENV only when the feature exists.\n # Also writes step outputs for values referenced in expression context\n # (GITHUB_ENV is not readable via steps.*.outputs — they need GITHUB_OUTPUT).\n # All values written are hardcoded 'true' — no user input in the script.\n # Config file paths (inputs.*) stay in GH Actions expression context in\n # the super-linter env: block below, never shell-evaluated (CWE-78).\n id: detect\n run: |\n has() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n\n if { has 'eslint.config.ts' || has 'eslint.config.mjs' || has 'eslint.config.js' || has '.eslintrc.json' || has '.eslintrc.yml'; }; then\n {\n echo \"VALIDATE_JAVASCRIPT_ES=true\"\n echo \"VALIDATE_TYPESCRIPT_ES=true\"\n } >> \"$GITHUB_ENV\"\n echo \"eslint=true\" >> \"$GITHUB_OUTPUT\"\n fi\n\n if { has '*.js' || has '*.jsx' || has '*.mjs' || has '*.cjs' || has '*.ts' || has '*.tsx'; }; then\n {\n echo \"VALIDATE_JAVASCRIPT_PRETTIER=true\"\n echo \"VALIDATE_JSX_PRETTIER=true\"\n echo \"VALIDATE_TYPESCRIPT_PRETTIER=true\"\n echo \"VALIDATE_TSX=true\"\n echo \"FIX_JAVASCRIPT_PRETTIER=true\"\n echo \"FIX_JSX_PRETTIER=true\"\n echo \"FIX_TYPESCRIPT_PRETTIER=true\"\n echo \"FIX_TSX=true\"\n } >> \"$GITHUB_ENV\"\n fi\n\n if { has '*.css' || has '*.scss' || has 'stylelint.config.ts' || has 'stylelint.config.mjs' || has 'stylelint.config.js'; }; then\n {\n echo \"VALIDATE_CSS=true\"\n echo \"STYLELINT_CONFIG_FILE=stylelint.config.ts\"\n } >> \"$GITHUB_ENV\"\n fi\n\n if { has '*.graphql' || has '*.gql'; }; then\n {\n echo \"VALIDATE_GRAPHQL_PRETTIER=true\"\n echo \"FIX_GRAPHQL_PRETTIER=true\"\n } >> \"$GITHUB_ENV\"\n fi\n\n if { has '*.html' || has '*.htm'; }; then\n {\n echo \"VALIDATE_HTML_PRETTIER=true\"\n echo \"FIX_HTML_PRETTIER=true\"\n } >> \"$GITHUB_ENV\"\n fi\n\n if { has '.env' || has '.env.example' || has '.env.local'; }; then\n {\n echo \"VALIDATE_ENV=true\"\n echo \"FIX_ENV=true\"\n } >> \"$GITHUB_ENV\"\n fi\n\n if { has 'Dockerfile' || has '*.Dockerfile'; }; then\n echo \"VALIDATE_DOCKERFILE=true\" >> \"$GITHUB_ENV\"\n fi\n\n - uses: super-linter/super-linter/slim@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0\n name: Run Super Linter\n env:\n GITHUB_TOKEN: ${{ github.token }}\n DEFAULT_BRANCH: ${{ github.event.pull_request.base.ref || github.event.repository.default_branch }}\n ANNOTATE_ONLY: true\n DISABLE_COMMENTS: false\n IGNORE_GITIGNORED_FILES: true\n LINTER_RULES_PATH: /\n EDITORCONFIG_FILE_NAME: \".editorconfig-checker.json\"\n # Config file paths — inputs stay in expression context, never shell-evaluated.\n # When VALIDATE_JAVASCRIPT_ES is not set by detect, ESLint doesn't run so\n # the empty-string fallback (→ eslint.config.mjs in container) is safe.\n JAVASCRIPT_ES_CONFIG_FILE: ${{ steps.detect.outputs.eslint == 'true' && inputs.eslint-config || '' }}\n TYPESCRIPT_ES_CONFIG_FILE: ${{ steps.detect.outputs.eslint == 'true' && inputs.eslint-config || '' }}\n PRETTIER_CONFIG: ${{ inputs.prettier-config }}\n YAML_CONFIG_FILE: ${{ inputs.yaml-config }}\n # Always-on linters\n FIX_MARKDOWN_PRETTIER: true\n VALIDATE_EDITORCONFIG: true\n VALIDATE_GIT_COMMITLINT: true\n VALIDATE_GIT_MERGE_CONFLICT_MARKERS: true\n VALIDATE_GITHUB_ACTIONS: true\n VALIDATE_GITLEAKS: true\n VALIDATE_MARKDOWN_PRETTIER: true\n VALIDATE_YAML: true\n\n - name: Validate ADR and spec frontmatter\n if: hashFiles('scripts/validate-adrs.mjs') != ''\n run: |\n mapfile -t changed < <(git diff --name-only \"$BASE_SHA\" HEAD -- \\\n 'docs/decisions/*.md' '.notes/*.spec.md' 2>/dev/null || true)\n if [ \"${#changed[@]}\" -eq 0 ]; then\n node scripts/validate-adrs.mjs\n else\n node scripts/validate-adrs.mjs \"${changed[@]}\"\n fi\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}\n\n - name: Validate registry consistency\n if: hashFiles('scripts/validate-registry.mjs') != ''\n run: node scripts/validate-registry.mjs\n\n - name: Validate docs presence for new packages\n if: hashFiles('scripts/validate-docs-presence.mjs') != ''\n run: node scripts/validate-docs-presence.mjs\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n name: Commit and push linting fixes\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true'\n with:\n token: ${{ steps.app-token.outputs.token }}\n branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}\n commit-message: \"chore: fix linting issues\\n\\nSigned-off-by: ${{ steps.app-bot.outputs.name }} <${{ steps.app-bot.outputs.email }}>\"\n commit-options: \"--no-verify\"\n commit-user-name: ${{ steps.app-bot.outputs.name }}\n commit-user-email: ${{ steps.app-bot.outputs.email }}\n commit-author: \"${{ steps.app-bot.outputs.name }} <${{ steps.app-bot.outputs.email }}>\"\n\n\n conclusion:\n name: Conclusion\n runs-on: ubuntu-latest\n if: always()\n needs: [super-lint]\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";
5349
5495
  //#endregion
5350
- //#region src/templates/workflows/post-release.yml
5351
- var post_release_default = "name: Post-release Sync\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\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 broadcast:\n name: Broadcast readme sync\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - name: Trigger broadcast readme sync\n run: |\n gh workflow run sync-broadcast.yml \\\n --repo theholocron/.github \\\n --field \"steps=readme\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n";
5496
+ //#region src/templates/workflows/preview.yml
5497
+ var preview_default = "name: Preview\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 name:\n description: Repo name prefix used to filter the docs site package (<name>-site); omit to run pnpm -C docs build\n required: false\n type: string\n default: \"\"\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 cloudflare-project:\n description: >\n Cloudflare Pages project name. Falls back to the CLOUDFLARE_PAGES_PROJECT\n org variable when omitted — set that variable once and all repos get previews\n without per-repo config.\n required: false\n type: string\n default: \"\"\n\njobs:\n deploy-preview:\n name: Preview\n if: ${{ github.event.action != 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\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' && inputs.name != '' }}\n env:\n SITE_NAME: ${{ inputs.name }}\n run: pnpm --filter @theholocron/\"$SITE_NAME\"-site build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name == '' }}\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 SITE_NAME: ${{ inputs.name }}\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 if [ -n \"$SITE_NAME\" ]; then\n # Site built with base: /<name>/ — nest under _site/<name>/ so asset\n # paths match (/name/_astro/... → served at /name/_astro/...).\n mkdir -p \"_site/${SITE_NAME}\"\n cp -r docs/dist/. \"_site/${SITE_NAME}/\"\n # Redirect root to the base path so the preview URL lands correctly.\n printf \"/ /%s/ 301\\n\" \"$SITE_NAME\" > _site/_redirects\n else\n cp -r docs/dist/. _site/\n fi\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 - name: Install Wrangler\n # The action's install step runs npm/pnpm in the repo root; pnpm fails\n # because it rejects adding to a workspace root without -w, and npm fails\n # because package.json contains pnpm catalog: references it cannot parse.\n # Pre-installing from /tmp (no package.json) lets the action find wrangler\n # already on PATH and skip its own install entirely.\n working-directory: /tmp\n run: npm install -g wrangler@4\n\n - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0\n id: deploy\n name: Deploy to Cloudflare Pages\n # Skip when neither the per-repo input nor the org variable is set.\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n with:\n apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n packageManager: npm\n command: >-\n pages deploy _site\n --project-name ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n --branch ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n --commit-dirty=true\n\n - name: Report preview URL\n if: ${{ steps.deploy.outputs.pages-deployment-alias-url != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n ALIAS: ${{ steps.deploy.outputs.pages-deployment-alias-url }}\n SITE_NAME: ${{ inputs.name }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n run: |\n PREVIEW_URL=\"${ALIAS%/}${SITE_NAME:+/${SITE_NAME}}/\"\n # GITHUB_HEAD_REF is the PR branch name — links the deployment to this PR's sidebar widget.\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"%s\",\"description\":\"Cloudflare Pages\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\" \"${PROJECT} (Preview)\")\n DEPLOY_ID=$(echo \"$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\n cleanup:\n name: Clean up Preview\n if: ${{ github.event.action == 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - name: Delete Cloudflare Pages deployments for branch\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n env:\n CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n BRANCH: ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n DEPLOYMENTS=$(curl -s \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" \\\n | jq -r --arg b \"$BRANCH\" \\\n '.result[] | select(.deployment_trigger.metadata.branch == $b) | .id')\n\n if [ -z \"$DEPLOYMENTS\" ]; then\n echo \"No deployments found for branch ${BRANCH} — nothing to clean up.\"\n exit 0\n fi\n\n for id in $DEPLOYMENTS; do\n echo \"Deleting CF Pages deployment: $id\"\n curl -s -X DELETE \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments/${id}?force=true\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" | jq -r 'if .success then \" ✓ deleted\" else \" ✗ \\(.errors[0].message)\" end'\n done\n\n # Mark the GitHub Deployment environment as inactive.\n ENV_NAME=\"${PROJECT} (Preview)\"\n gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n | jq -r \".[] | select(.environment == \\\"${ENV_NAME}\\\") | .id\" \\\n | while read -r deploy_id; do\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${deploy_id}/statuses\" \\\n --method POST --field state=inactive 2>/dev/null || true\n done\n";
5352
5498
  //#endregion
5353
5499
  //#region src/templates/workflows/release.yml
5354
- var release_default = "name: Release\n\n# Semantic-release with OIDC Trusted Publishing.\n# actions/setup-node writes a default NODE_AUTH_TOKEN=${{ github.token }}\n# which shadows OIDC auth. We explicitly clear it so npm falls through to\n# the Trusted Publisher OIDC exchange.\n# The calling repo must have a .releaserc.json that configures branches,\n# plugins, and any publish options. npm@11+ is installed to support OIDC.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n dry-run:\n description: >\n When true, runs semantic-release --dry-run: analyzes commits and\n previews the next release without git writes or publish. Only\n meaningful for workflow_dispatch triggers; push-triggered runs\n always run fully.\n type: boolean\n required: false\n default: false\n run-build:\n description: Run `pnpm build` before releasing\n type: boolean\n required: false\n default: true\n sentry-project:\n description: >\n Sentry project slug for sourcemap upload and release creation after\n publishing. Omit to skip the Sentry release step entirely.\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_RELEASE_TOKEN:\n description: >\n Fine-grained PAT (Contents + Issues + Pull requests: write) owned by\n an admin. Required when the default branch is protected by a ruleset —\n github.token cannot push through rulesets, but an admin PAT can.\n Takes priority over HOLOCRON_SYNC_TOKEN. Falls back to github.token.\n required: false\n HOLOCRON_SYNC_TOKEN:\n description: >\n Legacy alias for HOLOCRON_RELEASE_TOKEN — kept for backward compatibility.\n Prefer HOLOCRON_RELEASE_TOKEN for new repos.\n required: false\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to github.token.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when\n HOLOCRON_READ_TOKEN is not set.\n required: false\n SENTRY_AUTH_TOKEN:\n description: >\n Sentry auth token for sourcemap upload and release creation.\n Required when sentry-project is set. Use the org-level secret.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n release:\n name: Semantic release\n permissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 30\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Do not cancel in-progress releases — a partial release is worse than a slow one.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n persist-credentials: false\n # Use HOLOCRON_RELEASE_TOKEN when available — git push (tags, release commits)\n # uses the checkout credential, not GITHUB_TOKEN env var. The\n # built-in github.token cannot push through branch protection rulesets.\n token: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Configure git identity\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n git config --global user.name \"$GIT_NAME\"\n git config --global user.email \"$GIT_EMAIL\"\n git config --global format.signoff true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Upgrade npm for OIDC support\n run: npm install -g npm@11 sigstore\n # sigstore is required by libnpmpublish/provenance.js at module parse\n # time — before any config takes effect. Some npm 11.x builds stopped\n # bundling it; installing it globally into the same prefix ensures it\n # resolves regardless of npm version. (Discovered 2026-07-09.)\n\n - run: pnpm build\n name: Build\n if: ${{ inputs.run-build == true }}\n\n - name: Release\n run: |\n if [ \"$DRY_RUN\" = \"true\" ]; then\n npx semantic-release --dry-run\n else\n npx semantic-release\n fi\n env:\n DRY_RUN: ${{ inputs.dry-run }}\n # Prefer HOLOCRON_RELEASE_TOKEN (fine-grained PAT, Contents+Issues+PRs write,\n # owned by an admin with ruleset bypass) so @semantic-release/git can\n # push the version-bump commit through branch protection. Falls back to\n # HOLOCRON_SYNC_TOKEN (legacy) then github.token for unprotected repos.\n GITHUB_TOKEN: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n HUSKY: \"0\"\n NPM_CONFIG_PROVENANCE: true\n\n - name: Get release version\n id: release_version\n if: ${{ inputs.sentry-project != '' && inputs.dry-run != true }}\n env:\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n run: |\n TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo \"\")\n if [ -n \"$TAG\" ]; then\n echo \"release=${SENTRY_PROJECT}@${TAG#v}\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - name: Create Sentry release\n if: ${{ inputs.sentry-project != '' && steps.release_version.outputs.release != '' && inputs.dry-run != true }}\n uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3\n env:\n SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n SENTRY_ORG: theholocron\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n with:\n environment: production\n version: ${{ steps.release_version.outputs.release }}\n sourcemaps: \"**/dist\"\n";
5500
+ var release_default = "name: Release\n\n# Semantic-release with OIDC Trusted Publishing.\n# actions/setup-node writes a default NODE_AUTH_TOKEN=${{ github.token }}\n# which shadows OIDC auth. We explicitly clear it so npm falls through to\n# the Trusted Publisher OIDC exchange.\n# The calling repo must have a .releaserc.json that configures branches,\n# plugins, and any publish options. npm@11+ is installed to support OIDC.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n dry-run:\n description: >\n When true, runs semantic-release --dry-run: analyzes commits and\n previews the next release without git writes or publish. Only\n meaningful for workflow_dispatch triggers; push-triggered runs\n always run fully.\n type: boolean\n required: false\n default: false\n post-release:\n description: >\n When true, dispatches a readme sync broadcast after a successful\n release. Enable for packages consumed by other repos via the\n registry-doc system so downstream README installation blocks\n stay current.\n type: boolean\n required: false\n default: false\n run-build:\n description: Run `pnpm build` before releasing\n type: boolean\n required: false\n default: true\n sentry-project:\n description: >\n Sentry project slug for sourcemap upload and release creation after\n publishing. Omit to skip the Sentry release step entirely.\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_RELEASE_TOKEN:\n description: >\n Fine-grained PAT (Contents + Issues + Pull requests: write) owned by\n an admin. Required when the default branch is protected by a ruleset —\n github.token cannot push through rulesets, but an admin PAT can.\n Takes priority over HOLOCRON_SYNC_TOKEN. Falls back to github.token.\n required: false\n HOLOCRON_SYNC_TOKEN:\n description: >\n Legacy alias for HOLOCRON_RELEASE_TOKEN — kept for backward compatibility.\n Prefer HOLOCRON_RELEASE_TOKEN for new repos.\n required: false\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to github.token.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when\n HOLOCRON_READ_TOKEN is not set.\n required: false\n SENTRY_AUTH_TOKEN:\n description: >\n Sentry auth token for sourcemap upload and release creation.\n Required when sentry-project is set. Use the org-level secret.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n release:\n name: Semantic release\n permissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 30\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Do not cancel in-progress releases — a partial release is worse than a slow one.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n persist-credentials: false\n # Use HOLOCRON_RELEASE_TOKEN when available — git push (tags, release commits)\n # uses the checkout credential, not GITHUB_TOKEN env var. The\n # built-in github.token cannot push through branch protection rulesets.\n token: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Configure git identity\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n git config --global user.name \"$GIT_NAME\"\n git config --global user.email \"$GIT_EMAIL\"\n git config --global format.signoff true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Upgrade npm for OIDC support\n run: npm install -g npm@11 sigstore\n # sigstore is required by libnpmpublish/provenance.js at module parse\n # time — before any config takes effect. Some npm 11.x builds stopped\n # bundling it; installing it globally into the same prefix ensures it\n # resolves regardless of npm version. (Discovered 2026-07-09.)\n\n - run: pnpm build\n name: Build\n if: ${{ inputs.run-build == true }}\n\n - name: Release\n run: |\n if [ \"$DRY_RUN\" = \"true\" ]; then\n npx semantic-release --dry-run\n else\n npx semantic-release\n fi\n env:\n DRY_RUN: ${{ inputs.dry-run }}\n # Prefer HOLOCRON_RELEASE_TOKEN (fine-grained PAT, Contents+Issues+PRs write,\n # owned by an admin with ruleset bypass) so @semantic-release/git can\n # push the version-bump commit through branch protection. Falls back to\n # HOLOCRON_SYNC_TOKEN (legacy) then github.token for unprotected repos.\n GITHUB_TOKEN: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n HUSKY: \"0\"\n NPM_CONFIG_PROVENANCE: true\n\n - name: Get release version\n id: release_version\n if: ${{ inputs.sentry-project != '' && inputs.dry-run != true }}\n env:\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n run: |\n TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo \"\")\n if [ -n \"$TAG\" ]; then\n echo \"release=${SENTRY_PROJECT}@${TAG#v}\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - name: Create Sentry release\n if: ${{ inputs.sentry-project != '' && steps.release_version.outputs.release != '' && inputs.dry-run != true }}\n uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3\n env:\n SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n SENTRY_ORG: theholocron\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n with:\n environment: production\n version: ${{ steps.release_version.outputs.release }}\n sourcemaps: \"**/dist\"\n\n sync-readme:\n name: Post-release readme sync\n needs: release\n if: ${{ inputs.post-release == true && inputs.dry-run != true }}\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - name: Trigger broadcast readme sync\n run: |\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=readme\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n";
5355
5501
  //#endregion
5356
5502
  //#region src/templates/workflows/review.yml
5357
5503
  var review_default = "name: Review\n\n# ReviewDog is the annotation layer — posts inline PR diff annotations.\n# Runs on pull_request only: inline annotations require PR context,\n# and branch protection ensures all changes go through PRs anyway.\n# super-linter (lint.yml) is the CI gate covering push + PR events.\n# Gitleaks and YAML are intentionally duplicated: super-linter gates\n# merges; ReviewDog surfaces exact line annotations in the PR diff.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n reviewdog:\n name: Review PRs\n runs-on: ubuntu-latest\n timeout-minutes: 20\n permissions:\n contents: read\n pull-requests: write\n\n steps:\n - name: Checkout repository\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n fetch-depth: 0\n\n - name: Setup\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: theholocron/.github/.github/actions/setup@main\n\n - name: Install ReviewDog\n uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1\n with:\n reviewdog_version: latest\n\n # Detect which tools are relevant for this repo, excluding node_modules.\n # hashFiles('**/*') recurses into node_modules/.pnpm and produces false\n # positives for repos that don't own those file types.\n # -print -quit stops find after the first match without a pipe, avoiding\n # the SIGPIPE/pipefail exit-141 that find|head-1 triggers under\n # GitHub Actions' default bash --noprofile --norc -e -o pipefail mode.\n - name: Detect project features\n id: detect\n shell: bash\n run: |\n has() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n has_ext() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n { { has 'eslint.config.js' || has 'eslint.config.mjs' || has 'eslint.config.cjs' || \\\n has 'eslint.config.ts' || has '.eslintrc' || has '.eslintrc.js' || \\\n has '.eslintrc.cjs' || has '.eslintrc.json' || has '.eslintrc.yaml' || \\\n has '.eslintrc.yml'; } && grep -qF '\"eslint\":' package.json 2>/dev/null; } && echo \"eslint=true\" >> \"$GITHUB_OUTPUT\" || echo \"eslint=false\" >> \"$GITHUB_OUTPUT\"\n { has 'tsconfig.json' && grep -qF '\"typescript\":' package.json 2>/dev/null; } && echo \"tsconfig=true\" >> \"$GITHUB_OUTPUT\" || echo \"tsconfig=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.sh' && echo \"shell=true\" >> \"$GITHUB_OUTPUT\" || echo \"shell=false\" >> \"$GITHUB_OUTPUT\"\n has 'Dockerfile' || has_ext '*.Dockerfile' || has 'Containerfile' && \\\n echo \"docker=true\" >> \"$GITHUB_OUTPUT\" || echo \"docker=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '.env*' && echo \"dotenv=true\" >> \"$GITHUB_OUTPUT\" || echo \"dotenv=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.md' && echo \"markdown=true\" >> \"$GITHUB_OUTPUT\" || echo \"markdown=false\" >> \"$GITHUB_OUTPUT\"\n\n #\n # Always applicable\n #\n\n - name: Gitleaks (secrets)\n uses: reviewdog/action-gitleaks@2b7b5685e3e3eecddab5d30cfa04f18123031421 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / gitleaks\"\n fail_level: error\n gitleaks_flags: --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}\n\n - name: YamlLint\n if: ${{ hashFiles('yamllint.config.yml') != '' }}\n uses: reviewdog/action-yamllint@b5f7217d8c815ae374d1d55840d5e569d82f01f0 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / yamllint\"\n fail_level: error\n yamllint_flags: -c ${{ github.workspace }}/yamllint.config.yml ${{ github.workspace }}\n\n - name: ActionLint (GitHub Actions)\n if: ${{ hashFiles('.github/workflows/*.yml', '.github/workflows/*.yaml') != '' }}\n uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / actionlint\"\n fail_level: error\n\n #\n # TypeScript / JavaScript\n #\n\n - name: ESLint\n if: steps.detect.outputs.eslint == 'true'\n uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1.34.0\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / eslint\"\n fail_level: error\n eslint_flags: .\n\n - name: TypeScript\n if: steps.detect.outputs.tsconfig == 'true'\n uses: EPMatt/reviewdog-action-tsc@63d923a3c5b4497671940b8874f58a404e2351b5 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / tsc\"\n fail_level: error\n\n #\n # Shell\n #\n\n - name: ShellCheck\n if: steps.detect.outputs.shell == 'true'\n uses: reviewdog/action-shellcheck@4c07458293ac342d477251099501a718ae5ef86e # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / shellcheck\"\n fail_level: none\n\n #\n # Docker\n #\n\n - name: Hadolint\n if: steps.detect.outputs.docker == 'true'\n uses: reviewdog/action-hadolint@1b2cfa6ba72072ad35158d7ff3aa49bbdc03506d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / hadolint\"\n fail_level: none\n\n #\n # Environment files\n #\n\n - name: dotenv-linter\n if: steps.detect.outputs.dotenv == 'true'\n uses: dotenv-linter/action-dotenv-linter@afde61cfda2ecffe7bea35837b6f20b956c88689 # v3.0.0\n with:\n reporter: github-code-suggestions\n\n #\n # Documentation\n #\n\n - name: Alex (inclusive language)\n if: steps.detect.outputs.markdown == 'true'\n uses: reviewdog/action-alex@347481655add010a2ae302df34b57c9bcfa0d6e4 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / alex\"\n";
5358
5504
  //#endregion
5505
+ //#region src/templates/workflows/security.yml
5506
+ var security_default = "name: Security\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n language:\n description: CodeQL language to analyze\n type: string\n required: false\n default: javascript-typescript\n\njobs:\n analyze:\n name: Analyze (${{ inputs.language }})\n permissions:\n actions: read\n contents: read\n security-events: write\n runs-on: ubuntu-latest\n timeout-minutes: 45\n # Do not cancel in-progress security scans.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Initialize CodeQL\n with:\n languages: ${{ inputs.language }}\n\n - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Autobuild\n\n - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Analyze\n with:\n category: /language:${{ inputs.language }}\n";
5507
+ //#endregion
5359
5508
  //#region src/templates/workflows/stale.yml
5360
5509
  var stale_default = "name: Stale\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n days-before-stale:\n description: Days of inactivity before an issue is marked stale\n type: number\n required: false\n default: 30\n days-before-close:\n description: Days of inactivity after stale label before closing\n type: number\n required: false\n default: 5\n\njobs:\n stale:\n name: Mark stale issues and pull requests\n permissions:\n contents: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0\n name: Run Stale\n with:\n close-issue-message: >\n This issue was closed because it has been stalled for\n ${{ inputs.days-before-close }} days with no activity.\n days-before-close: ${{ inputs.days-before-close }}\n days-before-stale: ${{ inputs.days-before-stale }}\n exempt-all-pr-milestones: true\n stale-issue-label: wontfix\n stale-issue-message: >\n This issue is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n stale-pr-label: wontfix\n stale-pr-message: >\n This PR is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n";
5361
5510
  //#endregion
5362
5511
  //#region src/templates/workflows/sync.yml
5363
5512
  var sync_default = "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.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme description\".\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_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: Sync repo from config\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: write\n pull-requests: write\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 run: |\n if [ -n \"$STEPS\" ]; then\n # shellcheck disable=SC2086\n pnpm --workspace-root exec holocron sync --steps $STEPS\n else\n pnpm --workspace-root exec holocron sync\n fi\n env:\n HOLOCRON_ADMIN_TOKEN: ${{ secrets.HOLOCRON_ADMIN_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: ${{ inputs.steps }}\n\n - name: Format generated files\n run: pnpm exec prettier --write README.md docs/src/content/docs/index.mdx 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";
5364
5513
  //#endregion
5365
- //#region src/templates/workflows/sync-broadcast.yml
5366
- var sync_broadcast_default = "name: Sync Broadcast\n\non: # yamllint disable-line rule:truthy\n workflow_dispatch:\n inputs:\n steps:\n description: >\n Sync steps to pass to each repo's sync.yml. Default is \"readme\"\n (only README marker blocks are updated).\n type: string\n required: false\n default: readme\n\npermissions:\n contents: read\n\njobs:\n broadcast:\n name: Broadcast sync to all repos\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - name: Dispatch sync to all repos with sync.yml\n run: |\n gh api /orgs/theholocron/repos --paginate --jq '.[].name' \\\n | while IFS= read -r repo; do\n gh api \"/repos/theholocron/$repo/contents/.github/workflows/sync.yml\" --silent 2>/dev/null || continue\n echo \"Dispatching sync to theholocron/$repo\"\n gh workflow run sync.yml \\\n --repo \"theholocron/$repo\" \\\n --field \"steps=$STEPS\" \\\n || echo \"Warning: could not dispatch to theholocron/$repo — skipping\"\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n";
5514
+ //#region src/templates/workflows/sync-dispatch.yml
5515
+ var sync_dispatch_default = "name: Sync Dispatch\n\non: # yamllint disable-line rule:truthy\n workflow_dispatch:\n inputs:\n steps:\n description: >\n Sync steps to pass to each repo's sync.yml. Default is \"readme\"\n (only README marker blocks are updated).\n type: string\n required: false\n default: readme\n\npermissions:\n contents: read\n\njobs:\n broadcast:\n name: Broadcast sync to all repos\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - name: Dispatch sync to all repos with sync.yml\n run: |\n gh api /orgs/theholocron/repos --paginate --jq '.[].name' \\\n | while IFS= read -r repo; do\n gh api \"/repos/theholocron/$repo/contents/.github/workflows/sync.yml\" --silent 2>/dev/null || continue\n echo \"Dispatching sync to theholocron/$repo\"\n gh workflow run sync.yml \\\n --repo \"theholocron/$repo\" \\\n --field \"steps=$STEPS\" \\\n || echo \"Warning: could not dispatch to theholocron/$repo — skipping\"\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n";
5367
5516
  //#endregion
5368
5517
  //#region src/templates/workflows/sync-github.yml
5369
- var sync_github_default = "name: Sync GitHub Templates\n\n# Builds the holocron CLI from source and pushes updated workflow templates\n# and composite actions to downstream .github repos. Runs whenever the\n# template source files change on main or alpha.\n#\n# Secrets required:\n# HOLOCRON_SYNC_TOKEN — fine-grained PAT (resource owner: org) with:\n# Actions: Read and write (dispatch workflow runs via gh workflow run)\n# Contents: Read and write (git trees, blobs, refs)\n# Pull requests: Read and write (open sync PR)\n# Workflows: Read and write (write .github/workflows/*.yml)\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n primary-repo:\n description: >\n Primary .github repo — receives composite actions, reusable workflows,\n and thin-caller templates. Requires a PR (branch protection assumed).\n type: string\n required: false\n default: theholocron/.github\n secondary-repos:\n description: >\n Space-separated list of secondary repos (reusable workflows + thin\n callers only, no composite actions). Changes are delivered via pull\n request, same as the primary repo.\n type: string\n required: false\n default: \"\"\n sync-branch:\n description: Branch name used for the primary and secondary repo PRs\n type: string\n required: false\n default: chore/sync-templates\n secrets:\n HOLOCRON_SYNC_TOKEN:\n required: true\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to HOLOCRON_SYNC_TOKEN.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when neither\n HOLOCRON_READ_TOKEN nor HOLOCRON_SYNC_TOKEN is set.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n sync:\n name: Sync templates\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\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 build\n name: Build CLI\n\n - name: Cache actionlint\n id: cache-actionlint\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: /tmp/actionlint\n key: actionlint-v1.7.7-linux-amd64\n\n - name: Download actionlint\n if: steps.cache-actionlint.outputs.cache-hit != 'true'\n run: |\n curl -fsSL https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \\\n | tar -xz -C /tmp actionlint\n\n - name: Validate generated workflows\n run: |\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --output-dir /tmp/sync-validate\n /tmp/actionlint /tmp/sync-validate/.github/workflows/*.yml\n env:\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n\n - name: Sync primary repo (PR)\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$PRIMARY_REPO\" \"$SYNC_BRANCH\" 2>/dev/null || true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n\n - name: Sync secondary repos (PR)\n if: ${{ inputs.secondary-repos != '' }}\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n for repo in $SECONDARY_REPOS; do\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$repo\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$repo\" \"$SYNC_BRANCH\" 2>/dev/null || true\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n SECONDARY_REPOS: ${{ inputs.secondary-repos }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n";
5518
+ var sync_github_default = "name: Sync workflow templates\n\n# Builds the holocron CLI from source and pushes updated workflow templates\n# and composite actions to downstream .github repos. Runs whenever the\n# template source files change on main or alpha.\n#\n# Secrets required:\n# HOLOCRON_SYNC_TOKEN — fine-grained PAT (resource owner: org) with:\n# Actions: Read and write (dispatch workflow runs via gh workflow run)\n# Contents: Read and write (git trees, blobs, refs)\n# Pull requests: Read and write (open sync PR)\n# Workflows: Read and write (write .github/workflows/*.yml)\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n primary-repo:\n description: >\n Primary .github repo — receives composite actions, reusable workflows,\n and thin-caller templates. Requires a PR (branch protection assumed).\n type: string\n required: false\n default: theholocron/.github\n secondary-repos:\n description: >\n Space-separated list of secondary repos (reusable workflows + thin\n callers only, no composite actions). Changes are delivered via pull\n request, same as the primary repo.\n type: string\n required: false\n default: \"\"\n sync-branch:\n description: Branch name used for the primary and secondary repo PRs\n type: string\n required: false\n default: chore/sync-templates\n secrets:\n HOLOCRON_SYNC_TOKEN:\n required: true\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to HOLOCRON_SYNC_TOKEN.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when neither\n HOLOCRON_READ_TOKEN nor HOLOCRON_SYNC_TOKEN is set.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n sync:\n name: Sync templates\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\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 build\n name: Build CLI\n\n - name: Cache actionlint\n id: cache-actionlint\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: /tmp/actionlint\n key: actionlint-v1.7.7-linux-amd64\n\n - name: Download actionlint\n if: steps.cache-actionlint.outputs.cache-hit != 'true'\n run: |\n curl -fsSL https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \\\n | tar -xz -C /tmp actionlint\n\n - name: Validate generated workflows\n run: |\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --output-dir /tmp/sync-validate\n /tmp/actionlint /tmp/sync-validate/.github/workflows/*.yml\n env:\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n\n - name: Sync primary repo (PR)\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$PRIMARY_REPO\" \"$SYNC_BRANCH\" 2>/dev/null || true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n\n - name: Sync secondary repos (PR)\n if: ${{ inputs.secondary-repos != '' }}\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n for repo in $SECONDARY_REPOS; do\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$repo\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$repo\" \"$SYNC_BRANCH\" 2>/dev/null || true\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n SECONDARY_REPOS: ${{ inputs.secondary-repos }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n";
5370
5519
  //#endregion
5371
5520
  //#region src/templates/workflows/tag.yml
5372
5521
  var tag_default = "name: Tag\n\n# Release Please — fully automated tag and GitHub Release from Conventional Commits.\n# No package.json or npm publishing required. Operates in \"simple\" mode by default:\n# analyzes commits since the last tag, maintains a rolling Release PR, and creates\n# a tag + GitHub Release when that PR is merged.\n#\n# The calling repo must have two files at the root:\n# release-please-config.json — declares packages and release-type\n# .release-please-manifest.json — tracks the current version\n\non: # yamllint disable-line rule:truthy\n # Self-trigger: when this workflow lives in theholocron/.github itself,\n # push to main runs Release Please for that repo's own releases.\n push:\n branches:\n - main\n workflow_call:\n inputs:\n release-type:\n description: Release Please release type (simple, node, python, etc.)\n type: string\n required: false\n default: simple\n config-file:\n description: Path to release-please-config.json\n type: string\n required: false\n default: release-please-config.json\n manifest-file:\n description: Path to .release-please-manifest.json\n type: string\n required: false\n default: .release-please-manifest.json\n\njobs:\n tag:\n name: Tag release\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1\n name: Run Release Please\n with:\n release-type: ${{ inputs.release-type }}\n config-file: ${{ inputs.config-file }}\n manifest-file: ${{ inputs.manifest-file }}\n";
@@ -5378,7 +5527,7 @@ var test_default = "name: Test\n\non: # yamllint disable-line rule:truthy\n wor
5378
5527
  var typecheck_default = "name: Typecheck\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n typecheck:\n name: tsc --noEmit\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\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm typecheck\n name: Type check\n\n\n conclusion:\n name: Conclusion\n runs-on: ubuntu-latest\n if: always()\n needs: [typecheck]\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";
5379
5528
  //#endregion
5380
5529
  //#region src/templates/workflows/wiki.yml
5381
- var wiki_default = "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.35.4\"\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 URL. The preview is accessible at\n {fern-org}-preview-{id}.docs.buildwithfern.com. Use the PR number\n (e.g. \"pr-123\") so the same URL is reused on every push to\n the branch.\n type: string\n required: false\n default: \"\"\n fern-org:\n description: >\n Fern workspace org slug (e.g. \"holocron\"). When set, a GitHub\n deployment is created after a successful preview so the URL appears\n in the PR sidebar widget — the same pattern as Cloudflare Pages previews.\n type: string\n required: false\n default: \"\"\n base-path:\n description: >\n Basepath appended to the preview URL (e.g. \"holocron\" when using\n multi-source routing with wiki.theholocron.dev/holocron).\n Omit for single-instance Fern sites.\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: ${{ inputs.preview && 'Preview' || '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 if: ${{ inputs.preview && inputs.preview-id != '' }}\n run: fern generate --docs --preview --id \"$PREVIEW_ID\"\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n\n - name: Report preview URL\n if: ${{ inputs.preview && inputs.preview-id != '' && inputs.fern-org != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n FERN_ORG: ${{ inputs.fern-org }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n BASE_PATH: ${{ inputs.base-path }}\n run: |\n PREVIEW_URL=\"https://${FERN_ORG}-preview-${PREVIEW_ID}.docs.buildwithfern.com${BASE_PATH:+/${BASE_PATH}}\"\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"fern (Preview)\",\"description\":\"Fern Docs\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\")\n DEPLOY_ID=$(echo \"$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";
5530
+ var wiki_default = "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 URL. The preview is accessible at\n {fern-org}-preview-{id}.docs.buildwithfern.com. Use the PR number\n (e.g. \"pr-123\") so the same URL is reused on every push to\n the branch.\n type: string\n required: false\n default: \"\"\n fern-org:\n description: >\n Fern workspace org slug (e.g. \"holocron\"). When set, a GitHub\n deployment is created after a successful preview so the URL appears\n in the PR sidebar widget — the same pattern as Cloudflare Pages previews.\n type: string\n required: false\n default: \"\"\n base-path:\n description: >\n Basepath appended to the preview URL (e.g. \"holocron\" when using\n multi-source routing with wiki.theholocron.dev/holocron).\n Omit for single-instance Fern sites.\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: ${{ inputs.preview && 'Preview' || 'Publish' }} to Wiki\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 if: ${{ inputs.preview && inputs.preview-id != '' }}\n run: fern generate --docs --preview --id \"$PREVIEW_ID\"\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n\n - name: Report preview URL\n if: ${{ inputs.preview && inputs.preview-id != '' && inputs.fern-org != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n FERN_ORG: ${{ inputs.fern-org }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n BASE_PATH: ${{ inputs.base-path }}\n run: |\n PREVIEW_URL=\"https://${FERN_ORG}-preview-${PREVIEW_ID}.docs.buildwithfern.com${BASE_PATH:+/${BASE_PATH}}\"\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=$(echo \"$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";
5382
5531
  //#endregion
5383
5532
  //#region src/templates/index.ts
5384
5533
  /**
@@ -5398,18 +5547,16 @@ const ACTIONS = {
5398
5547
  const REUSABLE_WORKFLOWS = {
5399
5548
  audit: audit_default,
5400
5549
  bookkeeping: bookkeeping_default,
5401
- codeql: codeql_default,
5402
5550
  dependencies: dependencies_default,
5403
5551
  deploy: deploy_default,
5404
- "cleanup-preview": cleanup_preview_default,
5405
- "deploy-preview": deploy_preview_default,
5552
+ preview: preview_default,
5553
+ security: security_default,
5406
5554
  greetings: greetings_default,
5407
5555
  lint: lint_default,
5408
- "post-release": post_release_default,
5409
5556
  release: release_default,
5410
5557
  review: review_default,
5411
5558
  stale: stale_default,
5412
- "sync-broadcast": sync_broadcast_default,
5559
+ "sync-dispatch": sync_dispatch_default,
5413
5560
  tag: tag_default,
5414
5561
  "sync-github": sync_github_default,
5415
5562
  sync: sync_default,
@@ -5424,6 +5571,11 @@ const WORKFLOW_TEMPLATE_PROPERTIES = { bookkeeping: JSON.stringify({
5424
5571
  }, null, 2) };
5425
5572
  //#endregion
5426
5573
  //#region src/commands/sync-github.ts
5574
+ const { workflowHeader } = createHeader({
5575
+ source: "packages/cli/src/commands/sync-github.ts",
5576
+ tool: "holocron sync-github",
5577
+ forPrimary: true
5578
+ });
5427
5579
  const DEFAULT_REPO = "theholocron/.github";
5428
5580
  function reusableHeader(source) {
5429
5581
  return [
@@ -5440,17 +5592,17 @@ function buildBatch(repo) {
5440
5592
  const isPrimaryGithubRepo = repo === DEFAULT_REPO;
5441
5593
  if (isPrimaryGithubRepo) for (const [name, content] of Object.entries(ACTIONS)) files.push({
5442
5594
  path: `.github/actions/${name}.yml`,
5443
- content: reusableHeader(`packages/cli/src/templates/index.ts`) + content
5595
+ content: `${reusableHeader(`packages/cli/src/templates/index.ts`)}${content}`
5444
5596
  });
5445
5597
  if (isPrimaryGithubRepo) {
5446
5598
  for (const [name, content] of Object.entries(REUSABLE_WORKFLOWS)) files.push({
5447
5599
  path: `.github/workflows/${name}.yml`,
5448
- content: reusableHeader(`packages/cli/src/templates/index.ts`) + content
5600
+ content: `${reusableHeader(`packages/cli/src/templates/index.ts`)}${content}`
5449
5601
  });
5450
5602
  for (const [name, content] of Object.entries(WORKFLOW_TEMPLATES)) {
5451
5603
  files.push({
5452
5604
  path: `workflow-templates/${name}.yml`,
5453
- content: workflowHeader(void 0, true) + content
5605
+ content: `${workflowHeader()}${content}`
5454
5606
  });
5455
5607
  const props = WORKFLOW_TEMPLATE_PROPERTIES[name];
5456
5608
  if (props) files.push({
@@ -5805,7 +5957,7 @@ async function runUpgradeNode(input) {
5805
5957
  };
5806
5958
  }
5807
5959
  //#endregion
5808
- //#region src/load-config.ts
5960
+ //#region src/config/load-config.ts
5809
5961
  /**
5810
5962
  * `holocron.config.{json,js,ts}` file loader.
5811
5963
  *
@@ -5926,21 +6078,21 @@ async function fileExists(path) {
5926
6078
  //#region src/telemetry.ts
5927
6079
  const DSN = "https://95cbb72ad5636c94e119a5405ee8f55f@o4508238154104832.ingest.us.sentry.io/4511810950791168";
5928
6080
  function isEnabled() {
5929
- return !process.env.NO_HOLOCRON_TELEMETRY && true;
6081
+ return !env.get("NO_HOLOCRON_TELEMETRY") && true;
5930
6082
  }
5931
6083
  function init(version) {
5932
6084
  if (!isEnabled()) return;
5933
6085
  Sentry.init({
5934
6086
  dsn: DSN,
5935
6087
  release: `holocron@${version}`,
5936
- environment: process.env.CI ? "ci" : "local",
6088
+ environment: env.get("CI") ? "ci" : "local",
5937
6089
  tracesSampleRate: 1,
5938
6090
  beforeSend: scrubError
5939
6091
  });
5940
6092
  Sentry.startSession();
5941
6093
  Sentry.setTag("os", process.platform);
5942
6094
  Sentry.setTag("node", process.version);
5943
- Sentry.setTag("ci", String(Boolean(process.env.CI)));
6095
+ Sentry.setTag("ci", String(Boolean(env.get("CI"))));
5944
6096
  }
5945
6097
  function startCommand(name) {
5946
6098
  if (!isEnabled()) return () => {};
@@ -5975,50 +6127,12 @@ function scrubError(event, _hint) {
5975
6127
  return JSON.parse(redact(JSON.stringify(event)));
5976
6128
  }
5977
6129
  //#endregion
5978
- //#region src/token-args.ts
5979
- var TokenParseError = class extends Error {
5980
- name = "TokenParseError";
5981
- };
5982
- /**
5983
- * Converts raw --token CLI values into a typed result.
5984
- *
5985
- * Bare form: --token ghp_xxx → { cliToken: "ghp_xxx" }
5986
- * Keyed form: --token github=ghp_xxx → { cliTokens: { github: "ghp_xxx" } }
5987
- * Mixed: --token github=ghp_xxx --token v_yyy
5988
- * → { cliToken: "v_yyy", cliTokens: { github: "ghp_xxx" } }
5989
- *
5990
- * Values may contain "=" (e.g. base64 strings) — only the first "=" is treated as a separator.
5991
- */
5992
- function parseTokenArgs(tokens) {
5993
- if (tokens.length === 0) return {};
5994
- const cliTokens = {};
5995
- const bare = [];
5996
- for (const raw of tokens) {
5997
- const eqIdx = raw.indexOf("=");
5998
- if (eqIdx === -1) {
5999
- bare.push(raw);
6000
- continue;
6001
- }
6002
- const vendor = raw.slice(0, eqIdx);
6003
- const value = raw.slice(eqIdx + 1);
6004
- if (vendor.trim() === "") throw new TokenParseError(`invalid --token value "${raw}": vendor name must not be empty`);
6005
- if (/\s/.test(vendor)) throw new TokenParseError(`invalid --token value "${raw}": vendor name must not contain whitespace`);
6006
- if (value === "") throw new TokenParseError(`invalid --token value "${raw}": token value must not be empty`);
6007
- cliTokens[vendor] = value;
6008
- }
6009
- if (bare.length > 1) throw new TokenParseError(`only one bare --token value is allowed; got ${bare.length.toString()} — use vendor=value form for multiple tokens`);
6010
- const result = {};
6011
- if (bare.length === 1) result.cliToken = bare[0];
6012
- if (Object.keys(cliTokens).length > 0) result.cliTokens = cliTokens;
6013
- return result;
6014
- }
6015
- //#endregion
6016
6130
  //#region src/update-notifier.ts
6017
6131
  const PACKAGE_NAME = "@theholocron/cli";
6018
6132
  const CACHE_TTL_MS = 1440 * 60 * 1e3;
6019
6133
  const FETCH_TIMEOUT_MS = 3e3;
6020
6134
  function getCacheDir() {
6021
- return process.env["HOLOCRON_CACHE_DIR"] ?? join(homedir(), ".cache", "holocron");
6135
+ return env.get("HOLOCRON_CACHE_DIR") ?? join(homedir(), ".cache", "holocron");
6022
6136
  }
6023
6137
  function getCachePath() {
6024
6138
  return join(getCacheDir(), "update-check.json");
@@ -6106,7 +6220,7 @@ function formatNotice(current, latest) {
6106
6220
  ].join("\n");
6107
6221
  }
6108
6222
  async function checkForUpdates(currentVersion) {
6109
- if (process.env["CI"] || process.env["NO_UPDATE_NOTIFIER"]) return null;
6223
+ if (env.get("CI") || env.get("NO_UPDATE_NOTIFIER")) return null;
6110
6224
  const channel = getChannel(currentVersion);
6111
6225
  const cache = readCache();
6112
6226
  const now = Date.now();
@@ -6142,7 +6256,7 @@ const { version: CLI_VERSION } = JSON.parse(readFileSync(new URL("../package.jso
6142
6256
  * 3. `org` from `holocron.config.ts`
6143
6257
  */
6144
6258
  function resolveOrg(argv, config) {
6145
- return argv.org ?? process.env["HOLOCRON_ORG"] ?? config.org;
6259
+ return argv.org ?? env.get("HOLOCRON_ORG") ?? config.org;
6146
6260
  }
6147
6261
  /** Parses --token values and returns the context spread, or null on parse error (exits with code 1). */
6148
6262
  function tokenContext(rawTokens) {
@@ -6436,7 +6550,7 @@ try {
6436
6550
  },
6437
6551
  ...argv.steps && argv.steps.length > 0 ? { steps: argv.steps } : {}
6438
6552
  })).summary.fail > 0) process.exitCode = 1;
6439
- }).command("sync-github", "Sync workflow templates and composite actions to theholocron/.github via the GitHub API", (y) => y.option("repo", {
6553
+ }).command("sync-github", "Sync workflow templates and composite actions to theholocron/.github", (y) => y.option("repo", {
6440
6554
  type: "string",
6441
6555
  default: "theholocron/.github",
6442
6556
  describe: "Target org/repo (default: theholocron/.github)"