@theholocron/cli 5.0.0-alpha.11 → 5.0.0-alpha.13

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
@@ -112,6 +112,36 @@ Additional `repo` fields recognised by `holocron setup`:
112
112
  | `repo.protection` | `"balanced" \| "strict" \| "none"` | Branch-protection preset applied by `holocron setup`. For `"strict"`, the required status checks are derived from the task manifest — see below. |
113
113
  | `repo.properties` | `RepoProperties` | Org-level custom property values synced to the GitHub dashboard. |
114
114
 
115
+ ### Custom properties synced to GitHub
116
+
117
+ `holocron setup` and `holocron sync` both call `syncProperties()` with two
118
+ kinds of fields — always one-way (`holocron.config.ts` → resolved →
119
+ properties; properties are never a second editable source of truth):
120
+
121
+ **Manual** — `repo.properties` states these explicitly:
122
+
123
+ | Property | Value |
124
+ | ------------------------ | ---------------------------------------------- |
125
+ | `lifecycle` | `"active" \| "experimental" \| "deprecated"` |
126
+ | `open_source` | `boolean` |
127
+ | `runtime_environment` | `"node" \| "browser" \| "universal" \| "none"` |
128
+ | `uses_external_packages` | `boolean` |
129
+
130
+ **Derived** — computed from resolved config + `package.json`, not a config field:
131
+
132
+ | Property | Value |
133
+ | ---------------------------------- | ---------------------------------------------------------------------------------------- |
134
+ | `monorepo` | `boolean` — whether `pnpm-workspace.yaml` exists |
135
+ | `holocron_branch_protection_level` | the active `repo.protection` preset |
136
+ | `holocron_profile` | repo archetype: `library` / `cli` / `plugin` / `template` / `app` / `docs` / `platform` |
137
+ | `holocron_capabilities` | provider capability keys actually wired in `providers: {}` (`multi_select`) |
138
+ | `holocron_stack` | detected build/framework tooling — `next`, `vite`, `astro`, `tsdown`, … (`multi_select`) |
139
+ | `holocron_compliance` | `"compliant" \| "non-compliant"` against a minimal `source` + `ci` baseline |
140
+
141
+ Field definitions and derivation heuristics:
142
+ `.notes/tech-holocron-platform.spec.md` → "Custom-properties sync — field
143
+ definitions (#677)".
144
+
115
145
  ### Required status checks (`protection: "strict"`)
116
146
 
117
147
  `holocron setup` builds the branch-protection required-check list from the
package/dist/cli.mjs CHANGED
@@ -3796,6 +3796,104 @@ function vaultProviderName(loader) {
3796
3796
  return loader.get("vault").providerName;
3797
3797
  }
3798
3798
  //#endregion
3799
+ //#region src/commands/setup/derived-properties.ts
3800
+ /**
3801
+ * Repo archetype, in priority order:
3802
+ * 1. `*-template` repo name → `template` (independent of package shape).
3803
+ * 2. Monorepo whose primary published artifact is a CLI (some workspace
3804
+ * package has `bin`) → `platform` — matches `holocron` itself: the CLI is
3805
+ * the deliverable, the plugin packages are supporting infrastructure for
3806
+ * it, not separate products.
3807
+ * 3. Monorepo with no CLI but at least one non-private workspace package →
3808
+ * `library` — matches `clients`/`configs`/`utils`/`themes`/
3809
+ * `observability`: a family of published packages, none of them a CLI.
3810
+ * Checked *before* falling into the root-package branch below, because
3811
+ * these repos' own root `package.json` is private and often carries
3812
+ * `@theholocron/astro-config` for a docs site built from the monorepo
3813
+ * root — without this check they'd wrongly resolve to `docs` off that
3814
+ * root-level signal alone, when the docs site is a secondary concern of
3815
+ * a library collection, not what the repo fundamentally is.
3816
+ * 4. Single-package repo with `package.json#bin` → `cli`.
3817
+ * 5. Docs-only site (astro/starlight present, root package private, no
3818
+ * publishable exports) → `docs`.
3819
+ * 6. Private, non-publishable, non-docs → `app`.
3820
+ * 7. Otherwise, a single publishable package → `library`.
3821
+ */
3822
+ function deriveProfile(input) {
3823
+ if (/-template$/.test(input.repoName)) return "template";
3824
+ if (input.isMonorepo) {
3825
+ if (input.workspacePackageJsons.some((pkg) => Boolean(pkg.bin))) return "platform";
3826
+ if (input.workspacePackageJsons.some((pkg) => pkg.private !== true)) return "library";
3827
+ }
3828
+ const pkg = input.rootPackageJson;
3829
+ if (pkg?.bin) return "cli";
3830
+ const deps = {
3831
+ ...pkg?.dependencies,
3832
+ ...pkg?.devDependencies
3833
+ };
3834
+ const isDocsSite = Boolean(deps["@theholocron/astro-config"] || deps["astro"]);
3835
+ if (pkg?.private && isDocsSite) return "docs";
3836
+ if (pkg?.private) return "app";
3837
+ return "library";
3838
+ }
3839
+ const KNOWN_STACK_DEPS = [
3840
+ "next",
3841
+ "vite",
3842
+ "astro",
3843
+ "tsdown",
3844
+ "rollup",
3845
+ "webpack",
3846
+ "storybook",
3847
+ "vitest",
3848
+ "playwright",
3849
+ "turbo"
3850
+ ];
3851
+ /**
3852
+ * Detected build/framework tooling — the literal "does this repo need
3853
+ * next/vite/tsdown" signal `holocron.config.ts` has no field for today
3854
+ * (framework choice isn't a config concept, unlike `providers`/`tasks`).
3855
+ * Curated table, not exhaustive by design — add to `KNOWN_STACK_DEPS` as new
3856
+ * frameworks show up rather than trying to detect anything on npm.
3857
+ */
3858
+ function deriveStack(pkg) {
3859
+ const deps = {
3860
+ ...pkg?.dependencies,
3861
+ ...pkg?.devDependencies
3862
+ };
3863
+ return KNOWN_STACK_DEPS.filter((name) => name in deps);
3864
+ }
3865
+ /** The provider capability keys actually wired in `providers: {}` — zero heuristics. */
3866
+ function deriveCapabilities(providers) {
3867
+ return Object.keys(providers ?? {}).sort();
3868
+ }
3869
+ /**
3870
+ * `source` + `ci` are the minimal baseline every repo `holocron setup`
3871
+ * touches wires — every `holocron.config.ts` in this org already satisfies
3872
+ * this, so `non-compliant` here means drift (a provider manually removed
3873
+ * after setup), not an unmet aspirational policy. Deliberately not a richer
3874
+ * per-profile policy yet (e.g. "a `library` must have `deployment`") — that's
3875
+ * a Phase B (GitHub App) refinement once it can post *why* on a check run.
3876
+ */
3877
+ const REQUIRED_BASELINE = ["source", "ci"];
3878
+ function deriveCompliance(capabilities) {
3879
+ return REQUIRED_BASELINE.every((key) => capabilities.includes(key)) ? "compliant" : "non-compliant";
3880
+ }
3881
+ /**
3882
+ * Reads each workspace package's actual `package.json` content (not just
3883
+ * `readWorkspacePackages()`'s `{ slug, name, dir }` — `deriveProfile()`'s
3884
+ * `platform` detection needs `bin`, which that helper doesn't carry).
3885
+ * Missing/invalid files are skipped, matching `readWorkspacePackages()`'s
3886
+ * own soft-fail behavior.
3887
+ */
3888
+ async function readWorkspacePackageJsons(repoRoot, workspacePackages) {
3889
+ const results = [];
3890
+ for (const { slug, dir = "packages" } of workspacePackages) try {
3891
+ const raw = await readFile(join(repoRoot, dir, slug, "package.json"), "utf8");
3892
+ results.push(JSON.parse(raw));
3893
+ } catch {}
3894
+ return results;
3895
+ }
3896
+ //#endregion
3799
3897
  //#region src/commands/setup/labels.ts
3800
3898
  const CANONICAL_LABELS = [
3801
3899
  {
@@ -5152,7 +5250,7 @@ async function runSetup(input) {
5152
5250
  print(formatStep(steps[steps.length - 1]));
5153
5251
  }
5154
5252
  const properties = {};
5155
- if (effectivePreset && effectivePreset !== "none") properties["branch_protection_level"] = effectivePreset;
5253
+ if (effectivePreset && effectivePreset !== "none") properties["holocron_branch_protection_level"] = effectivePreset;
5156
5254
  const isMonorepo = await access(join(input.context.repoRoot, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
5157
5255
  properties["monorepo"] = String(isMonorepo);
5158
5256
  const manual = repo?.properties ?? {};
@@ -5160,6 +5258,21 @@ async function runSetup(input) {
5160
5258
  if (manual.open_source !== void 0) properties["open_source"] = String(manual.open_source);
5161
5259
  if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
5162
5260
  if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
5261
+ {
5262
+ const rootPackageJson = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
5263
+ const workspacePackages = readWorkspacePackages(input.context.repoRoot);
5264
+ const workspacePackageJsons = await readWorkspacePackageJsons(input.context.repoRoot, workspacePackages);
5265
+ properties["holocron_profile"] = deriveProfile({
5266
+ rootPackageJson,
5267
+ repoName: basename(input.context.repoRoot),
5268
+ isMonorepo,
5269
+ workspacePackageJsons
5270
+ });
5271
+ properties["holocron_stack"] = deriveStack(rootPackageJson);
5272
+ const capabilities = deriveCapabilities(config.providers);
5273
+ properties["holocron_capabilities"] = capabilities;
5274
+ properties["holocron_compliance"] = deriveCompliance(capabilities);
5275
+ }
5163
5276
  if (source.syncProperties) {
5164
5277
  steps.push(await runStep("source", "sync properties", dryRun, () => source.syncProperties(properties)));
5165
5278
  print(formatStep(steps[steps.length - 1]));
@@ -6078,7 +6191,7 @@ async function runSync(input) {
6078
6191
  const repo = config.repo;
6079
6192
  const properties = {};
6080
6193
  const effectivePreset = repo?.protection;
6081
- if (effectivePreset && effectivePreset !== "none") properties["branch_protection_level"] = effectivePreset;
6194
+ if (effectivePreset && effectivePreset !== "none") properties["holocron_branch_protection_level"] = effectivePreset;
6082
6195
  const isMonorepo = await access(join(input.context.repoRoot, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
6083
6196
  properties["monorepo"] = String(isMonorepo);
6084
6197
  const manual = repo?.properties ?? {};
@@ -6086,6 +6199,21 @@ async function runSync(input) {
6086
6199
  if (manual.open_source !== void 0) properties["open_source"] = String(manual.open_source);
6087
6200
  if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
6088
6201
  if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
6202
+ {
6203
+ const rootPackageJson = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
6204
+ const workspacePackages = readWorkspacePackages(input.context.repoRoot);
6205
+ const workspacePackageJsons = await readWorkspacePackageJsons(input.context.repoRoot, workspacePackages);
6206
+ properties["holocron_profile"] = deriveProfile({
6207
+ rootPackageJson,
6208
+ repoName: basename(input.context.repoRoot),
6209
+ isMonorepo,
6210
+ workspacePackageJsons
6211
+ });
6212
+ properties["holocron_stack"] = deriveStack(rootPackageJson);
6213
+ const capabilities = deriveCapabilities(config.providers);
6214
+ properties["holocron_capabilities"] = capabilities;
6215
+ properties["holocron_compliance"] = deriveCompliance(capabilities);
6216
+ }
6089
6217
  steps.push(await runSyncStep("source", "sync properties", dryRun, () => source.syncProperties(properties)));
6090
6218
  print(formatSyncStep(steps[steps.length - 1]));
6091
6219
  } else {