@theholocron/cli 3.62.1 → 3.64.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/README.md CHANGED
@@ -192,8 +192,16 @@ Axiom shipping activates when a token (`HOLOCRON_AXIOM_TOKEN` / `AXIOM_TOKEN`
192
192
  → OS keyring `axiom.<org>` → `axiom`) **and** a dataset
193
193
  (`HOLOCRON_AXIOM_DATASET` / `AXIOM_DATASET` → `log.axiom.dataset` in
194
194
  `holocron.config`) both resolve; env vars win. The token is never read from a
195
- config file. `HOLOCRON_TELEMETRY=false` disables the Axiom transport (and
196
- Sentry). See the [logging guide](https://docs.theholocron.dev/holocron/logging/).
195
+ config file. See the [logging guide](https://docs.theholocron.dev/holocron/logging/).
196
+
197
+ ### Telemetry
198
+
199
+ The CLI reports on itself through three sinks: **Sentry** (errors), **Axiom**
200
+ (logs), and **PostHog** (anonymous usage analytics — command frequency,
201
+ duration, failure rates, keyed to a one-way `sha256(hostname + username)`
202
+ fingerprint). PostHog activates from `HOLOCRON_POSTHOG_PROJECT_TOKEN` → `POSTHOG_PROJECT_TOKEN`
203
+ → a shipped ingest-only key (on by default). `HOLOCRON_TELEMETRY=false` disables
204
+ all three. See the [telemetry guide](https://docs.theholocron.dev/holocron/telemetry/).
197
205
 
198
206
  ## What's in here
199
207
 
package/dist/cli.mjs CHANGED
@@ -13,14 +13,15 @@ import { createLogger, parseLogLevel, resolveAxiomFromEnv } from "@theholocron/l
13
13
  import ora from "ora";
14
14
  import chalk from "chalk";
15
15
  import { execFile, execFileSync, spawnSync } from "node:child_process";
16
- import { homedir } from "node:os";
16
+ import { homedir, hostname, userInfo } from "node:os";
17
17
  import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
18
18
  import { createHash } from "node:crypto";
19
19
  import { generateReadme } from "@theholocron/components-doc/markdown";
20
20
  import { getClients, getConfigs, getDocs, getPlugins, getSkills, getThemes, getUtils } from "@theholocron/registry-doc";
21
21
  import { createGitHubClient } from "@theholocron/github-client";
22
- import { promisify } from "node:util";
23
22
  import * as Sentry from "@sentry/node";
23
+ import { PostHog } from "posthog-node";
24
+ import { promisify } from "node:util";
24
25
  //#region src/env.ts
25
26
  /** Singleton env for simple global lookups throughout the CLI. */
26
27
  const env = createEnvLookup();
@@ -2034,12 +2035,13 @@ export class ${inputs.capabilityClass} {
2034
2035
  readonly key = "${inputs.capability}" as const;
2035
2036
  readonly providerName = "${inputs.slug}";
2036
2037
 
2037
- constructor(private readonly rest: ${clientClass}) {}
2038
+ constructor(private readonly rest: () => ${clientClass}) {}
2038
2039
 
2039
2040
  // TODO: implement the ${capabilityInterface} interface methods
2040
2041
  // (see \`packages/cli/src/capabilities/index.ts\`). Each method
2041
2042
  // should hit a specific ${inputs.vendorName} REST endpoint via
2042
- // \`this.rest.request(...)\`. Once methods are stubbed, add
2043
+ // \`this.rest().request(...)\` — call the thunk so the token is
2044
+ // resolved lazily. Once methods are stubbed, add
2043
2045
  // \`implements ${capabilityInterface}\` to the class declaration
2044
2046
  // above and remove the \`as unknown as\` cast in src/index.ts.
2045
2047
  }
@@ -2060,7 +2062,7 @@ import { stubFetch } from "./helpers.js";
2060
2062
  function makeCapability() {
2061
2063
  const stub = stubFetch([]);
2062
2064
  const rest = new ${clientClass}({ token: "t", fetch: stub.fetch });
2063
- return new ${inputs.capabilityClass}(rest);
2065
+ return new ${inputs.capabilityClass}(() => rest);
2064
2066
  }
2065
2067
 
2066
2068
  describe("${inputs.capabilityClass}", () => {
@@ -2253,17 +2255,21 @@ export interface ${inputs.vendorName}PluginOptions extends ResolveTokenInput {
2253
2255
 
2254
2256
  export interface PluginContext {
2255
2257
  options: ${inputs.vendorName}PluginOptions;
2256
- rest: ${clientClass};
2258
+ /** Memoized client — the token is resolved on first use, not at plugin load. */
2259
+ rest: () => ${clientClass};
2257
2260
  }
2258
2261
 
2259
2262
  export function createContext(options: ${inputs.vendorName}PluginOptions): PluginContext {
2260
- const token = resolveToken(options);
2261
- const restOpts: ConstructorParameters<typeof ${clientClass}>[0] = { token };
2262
- if (options.baseUrl !== undefined) restOpts.baseUrl = options.baseUrl;
2263
- if (options.fetch !== undefined) restOpts.fetch = options.fetch;
2263
+ let rest: ${clientClass} | undefined;
2264
2264
  return {
2265
2265
  options,
2266
- rest: new ${clientClass}(restOpts),
2266
+ rest: () => {
2267
+ if (rest) return rest;
2268
+ const restOpts: ConstructorParameters<typeof ${clientClass}>[0] = { token: resolveToken(options) };
2269
+ if (options.baseUrl !== undefined) restOpts.baseUrl = options.baseUrl;
2270
+ if (options.fetch !== undefined) restOpts.fetch = options.fetch;
2271
+ return (rest = new ${clientClass}(restOpts));
2272
+ },
2267
2273
  };
2268
2274
  }
2269
2275
 
@@ -3750,7 +3756,7 @@ function deriveDeployPaths(raw) {
3750
3756
  * configured. Paths are gitignored and regenerated on every setup run so the
3751
3757
  * content always reflects the current CLI version.
3752
3758
  *
3753
- * Source: .notes/ai-engineering-workflow.spec.md
3759
+ * Source: docs/wiki/specifications/ai-engineering-workflow.spec.md
3754
3760
  */
3755
3761
  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";
3756
3762
  const AGENT_PROMPTS = {
@@ -5202,12 +5208,17 @@ const REGISTRY_MAP = {
5202
5208
  "@theholocron/themes": getThemes,
5203
5209
  "@theholocron/utils": getUtils
5204
5210
  };
5205
- function generateInstallBlock(pkg) {
5211
+ function generateInstallBlock(pkg, homepage = "") {
5206
5212
  const { name = "", bin, peerDependencies = {} } = pkg;
5207
5213
  const isCli = Boolean(bin);
5208
5214
  const isReact = Boolean(peerDependencies["react"]);
5215
+ const isConsumable = isCli || !pkg.private || Boolean(pkg.main) || pkg.exports !== void 0;
5209
5216
  const lines = [];
5210
5217
  lines.push("## Installation", "");
5218
+ if (!isConsumable) {
5219
+ lines.push("This repository is a workspace root — it is not published. See the", "packages under [`packages/`](./packages) for the tools it ships.");
5220
+ return lines.join("\n");
5221
+ }
5211
5222
  if (isCli) lines.push("```bash", `npm install --global ${name}`, "```");
5212
5223
  else lines.push("```bash", `pnpm install ${name}`, "```");
5213
5224
  lines.push("", "## Usage", "");
@@ -5216,8 +5227,9 @@ function generateInstallBlock(pkg) {
5216
5227
  lines.push("```bash");
5217
5228
  for (const cmd of commands) lines.push(`${cmd} --help`);
5218
5229
  lines.push("```");
5219
- } else if (isReact) lines.push("```tsx", `import { } from "${name}";`, "", "function App() {", ` return <></>;`, "}", "```");
5220
- else lines.push("```typescript", `import { } from "${name}";`, "```");
5230
+ } else if (isReact) lines.push("```tsx", `import {} from "${name}";`, "", "function App() {", ` return <></>;`, "}", "```");
5231
+ else if (homepage) lines.push(`See the [documentation](${homepage}) for the API.`);
5232
+ else lines.push("See the package documentation for the API.");
5221
5233
  return lines.join("\n");
5222
5234
  }
5223
5235
  /**
@@ -5299,9 +5311,10 @@ async function runSyncReadme(input) {
5299
5311
  };
5300
5312
  }
5301
5313
  const description = loaded.resolved.description ?? "";
5314
+ const homepage = loaded.resolved.homepage ?? pkg.homepage ?? "";
5302
5315
  const sections = generateReadme({
5303
5316
  description,
5304
- homepage: loaded.resolved.homepage ?? pkg.homepage ?? "",
5317
+ homepage,
5305
5318
  scripts: pkg.scripts ?? {},
5306
5319
  packages: pkg.name ? REGISTRY_MAP[pkg.name]?.() : void 0
5307
5320
  });
@@ -5310,7 +5323,7 @@ async function runSyncReadme(input) {
5310
5323
  development: sections.development,
5311
5324
  releases: sections.releases
5312
5325
  };
5313
- const updated = await updateReadme(repoRoot, generateInstallBlock(pkg), markerSections, dryRun, readFileFn, writeFileFn);
5326
+ const updated = await updateReadme(repoRoot, generateInstallBlock(pkg, homepage), markerSections, dryRun, readFileFn, writeFileFn);
5314
5327
  if (!updated) {
5315
5328
  print(style.warn("README.md not found or has no writable location for installation block"));
5316
5329
  logger.warn({
@@ -5960,6 +5973,168 @@ async function writeWorkflowFile(repoRoot, filename, content) {
5960
5973
  await writeFile(join(dir, filename), content, "utf8");
5961
5974
  }
5962
5975
  //#endregion
5976
+ //#region src/telemetry.ts
5977
+ const FALLBACK_DSN = "https://95cbb72ad5636c94e119a5405ee8f55f@o4508238154104832.ingest.us.sentry.io/4511810950791168";
5978
+ /**
5979
+ * Resolve the Sentry DSN — the `errors` capability's self-contained
5980
+ * activation, per ADR-0007:
5981
+ *
5982
+ * HOLOCRON_SENTRY_DSN → SENTRY_DSN → built-in fallback
5983
+ *
5984
+ * A consumer repo with `SENTRY_DSN` set gets CLI errors routed to its own
5985
+ * project with no config. Opt out entirely with `HOLOCRON_TELEMETRY=false`.
5986
+ */
5987
+ function resolveDsn() {
5988
+ return env.get("HOLOCRON_SENTRY_DSN") ?? env.get("SENTRY_DSN") ?? FALLBACK_DSN;
5989
+ }
5990
+ const FALLBACK_POSTHOG_PROJECT_TOKEN = "phc_AC4vFCvYzwnzmG7Vg5nEc3PZKZztoPyfKp4Lb9BbXcLK";
5991
+ const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
5992
+ /**
5993
+ * Resolve the PostHog project key — mirrors {@link resolveDsn} and the
5994
+ * `HOLOCRON_AXIOM_TOKEN` / `AXIOM_TOKEN` pattern, per ADR-0008:
5995
+ *
5996
+ * HOLOCRON_POSTHOG_PROJECT_TOKEN → POSTHOG_PROJECT_TOKEN → built-in fallback
5997
+ *
5998
+ * `POSTHOG_PROJECT_TOKEN` is the vendor-native name a consumer repo sets (as a
5999
+ * plain variable, not a secret — the `phc_…` key is publishable) to route CLI
6000
+ * usage to its own project.
6001
+ */
6002
+ function resolvePostHogKey() {
6003
+ return env.get("HOLOCRON_POSTHOG_PROJECT_TOKEN") ?? env.get("POSTHOG_PROJECT_TOKEN") ?? FALLBACK_POSTHOG_PROJECT_TOKEN;
6004
+ }
6005
+ function resolvePostHogHost() {
6006
+ return env.get("HOLOCRON_POSTHOG_HOST") ?? env.get("POSTHOG_HOST") ?? DEFAULT_POSTHOG_HOST;
6007
+ }
6008
+ function isEnabled() {
6009
+ return env.get("HOLOCRON_TELEMETRY") !== "false" && !env.get("NO_HOLOCRON_TELEMETRY");
6010
+ }
6011
+ const sentryEnabled = () => isEnabled() && resolveDsn() !== "";
6012
+ const posthogEnabled = () => isEnabled() && resolvePostHogKey() !== "";
6013
+ let posthog;
6014
+ let commandStartedAt = 0;
6015
+ let currentCommand = "unknown";
6016
+ /**
6017
+ * Anonymous, stable per-machine id — `sha256(hostname + username)` truncated
6018
+ * (ADR-0008). A one-way fingerprint: no raw hostname or username is ever
6019
+ * transmitted, and it is not personal data under this project's rule.
6020
+ */
6021
+ function machineId() {
6022
+ try {
6023
+ return createHash("sha256").update(`${hostname()} ${userInfo().username}`).digest("hex").slice(0, 32);
6024
+ } catch {
6025
+ return "unknown";
6026
+ }
6027
+ }
6028
+ /** Properties every event carries, so any event can be pivoted to its Axiom trace. */
6029
+ function baseProps() {
6030
+ const runId = getRunId();
6031
+ return {
6032
+ command: currentCommand,
6033
+ ci: Boolean(env.get("CI")),
6034
+ ...runId ? { runId } : {}
6035
+ };
6036
+ }
6037
+ /**
6038
+ * Capture a PostHog event. Runs every `properties` object through the same
6039
+ * token scrubber Sentry uses (defence-in-depth — values are controlled).
6040
+ */
6041
+ function event(name, properties = {}) {
6042
+ if (!posthogEnabled() || !posthog) return;
6043
+ const merged = {
6044
+ ...baseProps(),
6045
+ ...properties
6046
+ };
6047
+ posthog.capture({
6048
+ distinctId: machineId(),
6049
+ event: name,
6050
+ properties: JSON.parse(redact(JSON.stringify(merged)))
6051
+ });
6052
+ }
6053
+ function init(version) {
6054
+ initSentry(version);
6055
+ initPostHog(version);
6056
+ }
6057
+ function initSentry(version) {
6058
+ if (!sentryEnabled()) return;
6059
+ Sentry.init({
6060
+ dsn: resolveDsn(),
6061
+ release: `holocron@${version}`,
6062
+ environment: env.get("CI") ? "ci" : "local",
6063
+ tracesSampleRate: 1,
6064
+ beforeSend: scrubError
6065
+ });
6066
+ Sentry.startSession();
6067
+ Sentry.setTag("os", process.platform);
6068
+ Sentry.setTag("node", process.version);
6069
+ Sentry.setTag("ci", String(Boolean(env.get("CI"))));
6070
+ }
6071
+ function initPostHog(version) {
6072
+ if (!posthogEnabled()) return;
6073
+ posthog = new PostHog(resolvePostHogKey(), { host: resolvePostHogHost() });
6074
+ posthog.identify({
6075
+ distinctId: machineId(),
6076
+ properties: {
6077
+ ci: Boolean(env.get("CI")),
6078
+ os: process.platform,
6079
+ node: process.version,
6080
+ cli: version,
6081
+ ...env.get("HOLOCRON_ORG") ? { org: env.get("HOLOCRON_ORG") } : {}
6082
+ }
6083
+ });
6084
+ }
6085
+ let lastErrorType;
6086
+ /** Start a Sentry command span, or `undefined` when Sentry is off. */
6087
+ function startSpan(name) {
6088
+ if (!sentryEnabled()) return void 0;
6089
+ Sentry.setTag("command", name);
6090
+ return Sentry.startInactiveSpan({
6091
+ name,
6092
+ op: "holocron.command",
6093
+ forceTransaction: true
6094
+ });
6095
+ }
6096
+ function startCommand(name) {
6097
+ currentCommand = name;
6098
+ commandStartedAt = Date.now();
6099
+ lastErrorType = void 0;
6100
+ const span = startSpan(name);
6101
+ event("command_started", { dry_run: process.argv.includes("--dry-run") });
6102
+ return (ok) => {
6103
+ if (span) {
6104
+ span.setStatus({ code: ok ? 1 : 2 });
6105
+ span.end();
6106
+ }
6107
+ event(ok ? "command_completed" : "command_failed", {
6108
+ status: ok ? "ok" : "fail",
6109
+ duration_ms: Date.now() - commandStartedAt,
6110
+ ...ok || !lastErrorType ? {} : { error_type: lastErrorType }
6111
+ });
6112
+ };
6113
+ }
6114
+ function captureException(err) {
6115
+ lastErrorType = err instanceof Error ? err.constructor.name : "Error";
6116
+ if (!sentryEnabled()) return;
6117
+ Sentry.captureException(err);
6118
+ }
6119
+ function endSession() {
6120
+ if (!sentryEnabled()) return;
6121
+ Sentry.endSession();
6122
+ }
6123
+ async function flush() {
6124
+ if (sentryEnabled()) await Sentry.close(2e3);
6125
+ if (posthog) {
6126
+ await posthog.shutdown();
6127
+ posthog = void 0;
6128
+ }
6129
+ }
6130
+ const TOKEN_RE = /\b(ghp_|ghs_|glpat-|xoxb-|xoxp-|npm_|sk-|[A-Z][A-Z0-9_]{2,}_TOKEN[=\s])[^\s"]*/g;
6131
+ function redact(raw) {
6132
+ return raw.replace(TOKEN_RE, "[REDACTED]");
6133
+ }
6134
+ function scrubError(event, _hint) {
6135
+ return JSON.parse(redact(JSON.stringify(event)));
6136
+ }
6137
+ //#endregion
5963
6138
  //#region src/templates/actions/auto-commit.yml
5964
6139
  var auto_commit_default = "name: Auto Commit\ndescription: >\n Commit and push any file changes using stefanzweifel/git-auto-commit-action.\n If a branch name is provided and differs from the current branch, a new branch\n is created automatically.\n\ninputs:\n token:\n description: >\n GitHub token with push access to the target branch. Used to authenticate\n the git remote so the commit can be pushed.\n required: true\n branch:\n description: >\n Branch to commit to. Leave empty to commit to the current branch (useful\n in PR contexts). Provide an explicit name (e.g. chore/auto-sync) to push\n to a separate branch.\n required: false\n default: \"\"\n commit-message:\n description: Commit message.\n required: true\n commit-options:\n description: >\n Additional options passed to git commit (e.g. --no-verify).\n required: false\n default: \"\"\n commit-user-name:\n description: Git user.name for the commit.\n required: false\n default: \"github-actions[bot]\"\n commit-user-email:\n description: Git user.email for the commit.\n required: false\n default: \"41898282+github-actions[bot]@users.noreply.github.com\"\n commit-author:\n description: >\n Git author string in \"Name <email>\" format.\n required: false\n default: \"github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>\"\n\noutputs:\n changes-detected:\n description: \"'true' if any files were committed, 'false' otherwise.\"\n value: ${{ steps.commit.outputs.changes_detected }}\n\nruns:\n using: composite\n steps:\n - name: Configure git remote with token\n shell: bash\n run: |\n REMOTE_URL=$(git remote get-url origin)\n AUTHED_URL=$(echo \"$REMOTE_URL\" | sed \"s|https://|https://x-access-token:${TOKEN}@|\")\n git remote set-url origin \"$AUTHED_URL\"\n env:\n TOKEN: ${{ inputs.token }}\n\n - name: Reset branch to current HEAD\n if: ${{ inputs.branch != '' }}\n shell: bash\n run: git checkout -B \"$BRANCH\"\n env:\n BRANCH: ${{ inputs.branch }}\n\n - name: Build signed commit message\n id: msg\n shell: bash\n run: |\n TRAILER=\"Signed-off-by: ${COMMIT_USER_NAME} <${COMMIT_USER_EMAIL}>\"\n if echo \"$COMMIT_MESSAGE\" | grep -qF \"$TRAILER\"; then\n FINAL=\"$COMMIT_MESSAGE\"\n else\n FINAL=\"${COMMIT_MESSAGE}\"$'\\n\\n'\"${TRAILER}\"\n fi\n {\n echo \"value<<EOF_TRAILER\"\n printf '%s' \"$FINAL\"\n echo \"\"\n echo \"EOF_TRAILER\"\n } >> \"$GITHUB_OUTPUT\"\n env:\n COMMIT_MESSAGE: ${{ inputs.commit-message }}\n COMMIT_USER_NAME: ${{ inputs.commit-user-name }}\n COMMIT_USER_EMAIL: ${{ inputs.commit-user-email }}\n\n - uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0\n id: commit\n with:\n branch: ${{ inputs.branch }}\n commit_message: ${{ steps.msg.outputs.value }}\n commit_options: ${{ inputs.commit-options }}\n commit_user_name: ${{ inputs.commit-user-name }}\n commit_user_email: ${{ inputs.commit-user-email }}\n commit_author: ${{ inputs.commit-author }}\n";
5965
6140
  //#endregion
@@ -6006,13 +6181,13 @@ var security_default = "name: Security\n\non: # yamllint disable-line rule:truth
6006
6181
  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 or PR is marked stale (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 30\n days-before-close:\n description: Days after stale label before closing (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 5\n days-before-issue-stale:\n description: Days of inactivity before an issue is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-issue-close:\n description: Days after stale label before closing an issue; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-stale:\n description: Days of inactivity before a PR is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-close:\n description: Days after stale label before closing a PR; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n exempt-issue-labels:\n description: Comma-separated labels that exempt an issue from being marked stale\n type: string\n required: false\n default: \"in-progress,wip\"\n exempt-pr-labels:\n description: Comma-separated labels that exempt a PR from being marked stale\n type: string\n required: false\n default: \"\"\n exempt-all-issue-milestones:\n description: Issues assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-milestones:\n description: PRs assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-issue-projects:\n description: Issues assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-projects:\n description: PRs assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\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-issue-close: ${{ inputs.days-before-issue-close }}\n days-before-issue-stale: ${{ inputs.days-before-issue-stale }}\n days-before-pr-close: ${{ inputs.days-before-pr-close }}\n days-before-pr-stale: ${{ inputs.days-before-pr-stale }}\n days-before-stale: ${{ inputs.days-before-stale }}\n exempt-all-issue-milestones: ${{ inputs.exempt-all-issue-milestones }}\n exempt-all-issue-projects: ${{ inputs.exempt-all-issue-projects }}\n exempt-all-pr-milestones: ${{ inputs.exempt-all-pr-milestones }}\n exempt-all-pr-projects: ${{ inputs.exempt-all-pr-projects }}\n exempt-issue-labels: ${{ inputs.exempt-issue-labels }}\n exempt-pr-labels: ${{ inputs.exempt-pr-labels }}\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";
6007
6182
  //#endregion
6008
6183
  //#region src/templates/workflows/sync.yml
6009
- 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, wiki.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme wiki\".\n type: string\n required: false\n secrets:\n HOLOCRON_ADMIN_TOKEN:\n description: Fine-grained PAT with admin scopes (labels, properties, teams).\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n HOLOCRON_DEPLOY_TOKEN:\n description: Fine-grained PAT for GitHub Pages configuration.\n required: false\n HOLOCRON_ISSUES_TOKEN:\n description: Fine-grained PAT for issue management.\n required: false\n HOLOCRON_ORG_TOKEN:\n description: Org-scoped fine-grained PAT for team sync and org properties.\n required: false\n HOLOCRON_READ_TOKEN:\n description: Fine-grained PAT for read-only GitHub API calls.\n required: false\n HOLOCRON_SYNC_TOKEN:\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for gh CLI calls. Used when\n HOLOCRON_SYNC_TOKEN is not set.\n required: false\n\njobs:\n sync:\n name: 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 - run: pnpm build\n name: Build CLI\n\n - name: Run holocron sync\n # Call the built entry directly — `pnpm exec holocron` relies on a\n # node_modules/.bin/holocron symlink that pnpm cannot create at install\n # time (dist/ does not exist yet), same as sync-github.yml.\n run: |\n if [ -n \"$STEPS\" ]; then\n # shellcheck disable=SC2086\n node packages/cli/dist/cli.mjs sync --steps $STEPS\n else\n node packages/cli/dist/cli.mjs sync\n fi\n env:\n HOLOCRON_ADMIN_TOKEN: ${{ secrets.HOLOCRON_ADMIN_TOKEN }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n 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: |\n pnpm exec prettier --write README.md docs/src/content/docs/index.mdx 2>/dev/null || true\n # `holocron sync` writes package.json fields (keywords, description,\n # homepage) with a plain assignment, which appends new keys at the\n # end — re-apply the canonical order the lint config enforces.\n pnpm exec eslint --fix --no-warn-ignored package.json 2>/dev/null || true\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n id: auto-commit\n name: Commit sync changes\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n branch: chore/auto-sync\n commit-message: \"chore: sync from holocron.config\"\n commit-options: \"--no-verify\"\n\n - name: Open PR if changes were committed\n if: steps.auto-commit.outputs.changes-detected == 'true'\n run: |\n gh pr create \\\n --title \"chore: sync README and repo metadata\" \\\n --body \"Automated sync triggered by changes to config or package files. Merge to apply.\" \\\n --base main \\\n --head chore/auto-sync \\\n || echo \"PR already open — branch updated.\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Broadcast wiki sync if navbar changed\n if: >-\n steps.auto-commit.outputs.changes-detected == 'true' &&\n github.event_name == 'push' &&\n (inputs.steps == '' || contains(inputs.steps, 'wiki'))\n run: |\n if git diff HEAD~1 --name-only | grep -q 'fern/docs.yml'; then\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=wiki\" \\\n || echo \"skipping broadcast — insufficient permissions\"\n fi\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n";
6184
+ 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, wiki.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme wiki\".\n type: string\n required: false\n secrets:\n HOLOCRON_ADMIN_TOKEN:\n description: Fine-grained PAT with admin scopes (labels, properties, teams).\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n HOLOCRON_DEPLOY_TOKEN:\n description: Fine-grained PAT for GitHub Pages configuration.\n required: false\n HOLOCRON_ISSUES_TOKEN:\n description: Fine-grained PAT for issue management.\n required: false\n HOLOCRON_ORG_TOKEN:\n description: Org-scoped fine-grained PAT for team sync and org properties.\n required: false\n HOLOCRON_READ_TOKEN:\n description: Fine-grained PAT for read-only GitHub API calls.\n required: false\n HOLOCRON_SYNC_TOKEN:\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for gh CLI calls. Used when\n HOLOCRON_SYNC_TOKEN is not set.\n required: false\n\njobs:\n sync:\n name: 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 - run: pnpm build\n name: Build CLI\n\n - name: Run holocron sync\n # Call the built entry directly — `pnpm exec holocron` relies on a\n # node_modules/.bin/holocron symlink that pnpm cannot create at install\n # time (dist/ does not exist yet), same as sync-github.yml.\n run: |\n if [ -n \"$STEPS\" ]; then\n # shellcheck disable=SC2086\n node packages/cli/dist/cli.mjs sync --steps $STEPS\n else\n node packages/cli/dist/cli.mjs sync\n fi\n env:\n HOLOCRON_ADMIN_TOKEN: ${{ secrets.HOLOCRON_ADMIN_TOKEN }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n POSTHOG_PROJECT_TOKEN: ${{ vars.POSTHOG_PROJECT_TOKEN }}\n HOLOCRON_DEPLOY_TOKEN: ${{ secrets.HOLOCRON_DEPLOY_TOKEN }}\n HOLOCRON_ISSUES_TOKEN: ${{ secrets.HOLOCRON_ISSUES_TOKEN }}\n HOLOCRON_ORG_TOKEN: ${{ secrets.HOLOCRON_ORG_TOKEN }}\n HOLOCRON_READ_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n\n - name: Format generated files\n run: |\n pnpm exec prettier --write README.md docs/src/content/docs/index.mdx 2>/dev/null || true\n # `holocron sync` writes package.json fields (keywords, description,\n # homepage) with a plain assignment, which appends new keys at the\n # end — re-apply the canonical order the lint config enforces.\n pnpm exec eslint --fix --no-warn-ignored package.json 2>/dev/null || true\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n id: auto-commit\n name: Commit sync changes\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n branch: chore/auto-sync\n commit-message: \"chore: sync from holocron.config\"\n commit-options: \"--no-verify\"\n\n - name: Open PR if changes were committed\n if: steps.auto-commit.outputs.changes-detected == 'true'\n run: |\n gh pr create \\\n --title \"chore: sync README and repo metadata\" \\\n --body \"Automated sync triggered by changes to config or package files. Merge to apply.\" \\\n --base main \\\n --head chore/auto-sync \\\n || echo \"PR already open — branch updated.\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Broadcast wiki sync if navbar changed\n if: >-\n steps.auto-commit.outputs.changes-detected == 'true' &&\n github.event_name == 'push' &&\n (inputs.steps == '' || contains(inputs.steps, 'wiki'))\n run: |\n if git diff HEAD~1 --name-only | grep -q 'fern/docs.yml'; then\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=wiki\" \\\n || echo \"skipping broadcast — insufficient permissions\"\n fi\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n";
6010
6185
  //#endregion
6011
6186
  //#region src/templates/workflows/sync-dispatch.yml
6012
6187
  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";
6013
6188
  //#endregion
6014
6189
  //#region src/templates/workflows/sync-github.yml
6015
- 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 HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n 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 HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\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";
6190
+ 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 HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n 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 HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n POSTHOG_PROJECT_TOKEN: ${{ vars.POSTHOG_PROJECT_TOKEN }}\n 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";
6016
6191
  //#endregion
6017
6192
  //#region src/templates/workflows/tag.yml
6018
6193
  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";
@@ -6146,6 +6321,14 @@ async function runSyncGithub(input) {
6146
6321
  ...r.message ? { reason: r.message } : {},
6147
6322
  ...r.prUrl ? { pr: r.prUrl } : {}
6148
6323
  }, "sync-github: done");
6324
+ event("sync_github_run", {
6325
+ repo,
6326
+ status: r.status,
6327
+ ...branch ? { branch } : {},
6328
+ repos_targeted: 1,
6329
+ files_changed: r.created + r.updated,
6330
+ pr_opened: Boolean(r.prUrl)
6331
+ });
6149
6332
  return r;
6150
6333
  };
6151
6334
  if (input.outputDir) {
@@ -6631,70 +6814,6 @@ async function fileExists(path) {
6631
6814
  }
6632
6815
  }
6633
6816
  //#endregion
6634
- //#region src/telemetry.ts
6635
- const FALLBACK_DSN = "https://95cbb72ad5636c94e119a5405ee8f55f@o4508238154104832.ingest.us.sentry.io/4511810950791168";
6636
- /**
6637
- * Resolve the Sentry DSN — the `errors` capability's self-contained
6638
- * activation, per ADR-0007:
6639
- *
6640
- * HOLOCRON_SENTRY_DSN → SENTRY_DSN → built-in fallback
6641
- *
6642
- * A consumer repo with `SENTRY_DSN` set gets CLI errors routed to its own
6643
- * project with no config. Opt out entirely with `HOLOCRON_TELEMETRY=false`.
6644
- */
6645
- function resolveDsn() {
6646
- return env.get("HOLOCRON_SENTRY_DSN") ?? env.get("SENTRY_DSN") ?? FALLBACK_DSN;
6647
- }
6648
- function isEnabled() {
6649
- return !(env.get("HOLOCRON_TELEMETRY") === "false" || Boolean(env.get("NO_HOLOCRON_TELEMETRY"))) && resolveDsn() !== "";
6650
- }
6651
- function init(version) {
6652
- if (!isEnabled()) return;
6653
- Sentry.init({
6654
- dsn: resolveDsn(),
6655
- release: `holocron@${version}`,
6656
- environment: env.get("CI") ? "ci" : "local",
6657
- tracesSampleRate: 1,
6658
- beforeSend: scrubError
6659
- });
6660
- Sentry.startSession();
6661
- Sentry.setTag("os", process.platform);
6662
- Sentry.setTag("node", process.version);
6663
- Sentry.setTag("ci", String(Boolean(env.get("CI"))));
6664
- }
6665
- function startCommand(name) {
6666
- if (!isEnabled()) return () => {};
6667
- Sentry.setTag("command", name);
6668
- const span = Sentry.startInactiveSpan({
6669
- name,
6670
- op: "holocron.command",
6671
- forceTransaction: true
6672
- });
6673
- return (ok) => {
6674
- span.setStatus({ code: ok ? 1 : 2 });
6675
- span.end();
6676
- };
6677
- }
6678
- function captureException(err) {
6679
- if (!isEnabled()) return;
6680
- Sentry.captureException(err);
6681
- }
6682
- function endSession() {
6683
- if (!isEnabled()) return;
6684
- Sentry.endSession();
6685
- }
6686
- async function flush() {
6687
- if (!isEnabled()) return;
6688
- await Sentry.close(2e3);
6689
- }
6690
- const TOKEN_RE = /\b(ghp_|ghs_|glpat-|xoxb-|xoxp-|npm_|sk-|[A-Z][A-Z0-9_]{2,}_TOKEN[=\s])[^\s"]*/g;
6691
- function redact(raw) {
6692
- return raw.replace(TOKEN_RE, "[REDACTED]");
6693
- }
6694
- function scrubError(event, _hint) {
6695
- return JSON.parse(redact(JSON.stringify(event)));
6696
- }
6697
- //#endregion
6698
6817
  //#region src/update-notifier.ts
6699
6818
  const PACKAGE_NAME = "@theholocron/cli";
6700
6819
  const CACHE_TTL_MS = 1440 * 60 * 1e3;
@@ -6887,12 +7006,12 @@ try {
6887
7006
  describe: "Set the log level to error — suppress info and warn."
6888
7007
  }).middleware((argv) => {
6889
7008
  const name = argv._.slice(0, 2).join(" ") || "unknown";
6890
- finishCommand = startCommand(name);
6891
7009
  printRunId = Boolean(argv.debug || argv.verbose);
6892
7010
  buildCliLogger(argv, {
6893
7011
  command: name,
6894
7012
  org: argv.org
6895
7013
  });
7014
+ finishCommand = startCommand(name);
6896
7015
  }).command("version", "Print the CLI version", () => {}, () => {
6897
7016
  console.log(`holocron ${CLI_VERSION}`);
6898
7017
  }).command("clone", "Clone all repos in a GitHub org as siblings under a single directory", (y) => y.option("org", {