@theholocron/cli 5.0.0-alpha.10 → 5.0.0-alpha.12
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 +30 -0
- package/dist/cli.mjs +147 -7
- package/dist/cli.mjs.map +1 -1
- package/package.json +4 -4
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
|
+
| `branch_protection_level` | the active `repo.protection` preset |
|
|
136
|
+
| `holocron_profile` | repo archetype: `library` / `cli` / `plugin` / `template` / `app` / `docs` / `platform` |
|
|
137
|
+
| `holocron_capabilities` | comma-joined provider capability keys actually wired in `providers: {}` |
|
|
138
|
+
| `holocron_stack` | comma-joined detected build/framework tooling (`next`, `vite`, `astro`, `tsdown`, …) |
|
|
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,94 @@ 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. Single-package repo with `package.json#bin` → `cli`.
|
|
3808
|
+
* 4. Docs-only site (astro/starlight present, root package private, no
|
|
3809
|
+
* publishable exports) → `docs`.
|
|
3810
|
+
* 5. Private, non-publishable, non-docs → `app`.
|
|
3811
|
+
* 6. Otherwise, a single publishable package → `library`.
|
|
3812
|
+
*/
|
|
3813
|
+
function deriveProfile(input) {
|
|
3814
|
+
if (/-template$/.test(input.repoName)) return "template";
|
|
3815
|
+
if (input.isMonorepo) {
|
|
3816
|
+
if (input.workspacePackageJsons.some((pkg) => Boolean(pkg.bin))) return "platform";
|
|
3817
|
+
}
|
|
3818
|
+
const pkg = input.rootPackageJson;
|
|
3819
|
+
if (pkg?.bin) return "cli";
|
|
3820
|
+
const deps = {
|
|
3821
|
+
...pkg?.dependencies,
|
|
3822
|
+
...pkg?.devDependencies
|
|
3823
|
+
};
|
|
3824
|
+
const isDocsSite = Boolean(deps["@theholocron/astro-config"] || deps["astro"]);
|
|
3825
|
+
if (pkg?.private && isDocsSite) return "docs";
|
|
3826
|
+
if (pkg?.private) return "app";
|
|
3827
|
+
return "library";
|
|
3828
|
+
}
|
|
3829
|
+
const KNOWN_STACK_DEPS = [
|
|
3830
|
+
"next",
|
|
3831
|
+
"vite",
|
|
3832
|
+
"astro",
|
|
3833
|
+
"tsdown",
|
|
3834
|
+
"rollup",
|
|
3835
|
+
"webpack",
|
|
3836
|
+
"storybook",
|
|
3837
|
+
"vitest",
|
|
3838
|
+
"playwright",
|
|
3839
|
+
"turbo"
|
|
3840
|
+
];
|
|
3841
|
+
/**
|
|
3842
|
+
* Detected build/framework tooling — the literal "does this repo need
|
|
3843
|
+
* next/vite/tsdown" signal `holocron.config.ts` has no field for today
|
|
3844
|
+
* (framework choice isn't a config concept, unlike `providers`/`tasks`).
|
|
3845
|
+
* Curated table, not exhaustive by design — add to `KNOWN_STACK_DEPS` as new
|
|
3846
|
+
* frameworks show up rather than trying to detect anything on npm.
|
|
3847
|
+
*/
|
|
3848
|
+
function deriveStack(pkg) {
|
|
3849
|
+
const deps = {
|
|
3850
|
+
...pkg?.dependencies,
|
|
3851
|
+
...pkg?.devDependencies
|
|
3852
|
+
};
|
|
3853
|
+
return KNOWN_STACK_DEPS.filter((name) => name in deps);
|
|
3854
|
+
}
|
|
3855
|
+
/** The provider capability keys actually wired in `providers: {}` — zero heuristics. */
|
|
3856
|
+
function deriveCapabilities(providers) {
|
|
3857
|
+
return Object.keys(providers ?? {}).sort();
|
|
3858
|
+
}
|
|
3859
|
+
/**
|
|
3860
|
+
* `source` + `ci` are the minimal baseline every repo `holocron setup`
|
|
3861
|
+
* touches wires — every `holocron.config.ts` in this org already satisfies
|
|
3862
|
+
* this, so `non-compliant` here means drift (a provider manually removed
|
|
3863
|
+
* after setup), not an unmet aspirational policy. Deliberately not a richer
|
|
3864
|
+
* per-profile policy yet (e.g. "a `library` must have `deployment`") — that's
|
|
3865
|
+
* a Phase B (GitHub App) refinement once it can post *why* on a check run.
|
|
3866
|
+
*/
|
|
3867
|
+
const REQUIRED_BASELINE = ["source", "ci"];
|
|
3868
|
+
function deriveCompliance(capabilities) {
|
|
3869
|
+
return REQUIRED_BASELINE.every((key) => capabilities.includes(key)) ? "compliant" : "non-compliant";
|
|
3870
|
+
}
|
|
3871
|
+
/**
|
|
3872
|
+
* Reads each workspace package's actual `package.json` content (not just
|
|
3873
|
+
* `readWorkspacePackages()`'s `{ slug, name, dir }` — `deriveProfile()`'s
|
|
3874
|
+
* `platform` detection needs `bin`, which that helper doesn't carry).
|
|
3875
|
+
* Missing/invalid files are skipped, matching `readWorkspacePackages()`'s
|
|
3876
|
+
* own soft-fail behavior.
|
|
3877
|
+
*/
|
|
3878
|
+
async function readWorkspacePackageJsons(repoRoot, workspacePackages) {
|
|
3879
|
+
const results = [];
|
|
3880
|
+
for (const { slug, dir = "packages" } of workspacePackages) try {
|
|
3881
|
+
const raw = await readFile(join(repoRoot, dir, slug, "package.json"), "utf8");
|
|
3882
|
+
results.push(JSON.parse(raw));
|
|
3883
|
+
} catch {}
|
|
3884
|
+
return results;
|
|
3885
|
+
}
|
|
3886
|
+
//#endregion
|
|
3799
3887
|
//#region src/commands/setup/labels.ts
|
|
3800
3888
|
const CANONICAL_LABELS = [
|
|
3801
3889
|
{
|
|
@@ -5118,20 +5206,31 @@ async function runSetup(input) {
|
|
|
5118
5206
|
print(formatStep(steps[steps.length - 1]));
|
|
5119
5207
|
}
|
|
5120
5208
|
{
|
|
5121
|
-
const
|
|
5209
|
+
const astromechTurbo = createAstromech({
|
|
5122
5210
|
cwd: input.context.repoRoot,
|
|
5123
5211
|
config: { tasks: config.tasks },
|
|
5124
5212
|
logger
|
|
5125
|
-
})
|
|
5213
|
+
});
|
|
5214
|
+
const content = astromechTurbo.turboConfig();
|
|
5126
5215
|
if (content === null) steps.push({
|
|
5127
5216
|
capability: "source",
|
|
5128
5217
|
step: "write turbo.json",
|
|
5129
5218
|
status: "skip",
|
|
5130
5219
|
message: "no fan-out-eligible tasks configured"
|
|
5131
5220
|
});
|
|
5132
|
-
else
|
|
5133
|
-
await
|
|
5134
|
-
|
|
5221
|
+
else {
|
|
5222
|
+
steps.push(await runStep("source", "write turbo.json", dryRun, async () => {
|
|
5223
|
+
await source.writeRepoFile("turbo.json", content);
|
|
5224
|
+
}));
|
|
5225
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5226
|
+
const rootPkg = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
5227
|
+
const workspaceYaml = await readFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), "utf8").catch(() => "");
|
|
5228
|
+
const rootScripts = Object.keys(rootPkg?.scripts ?? {});
|
|
5229
|
+
const workspaceFix = astromechTurbo.ensureRootWorkspaceMember(workspaceYaml, rootScripts);
|
|
5230
|
+
if (workspaceFix.changed) steps.push(await runStep("source", "fix pnpm-workspace.yaml (root workspace member, #692)", dryRun, async () => {
|
|
5231
|
+
await source.writeRepoFile("pnpm-workspace.yaml", workspaceFix.content);
|
|
5232
|
+
}));
|
|
5233
|
+
}
|
|
5135
5234
|
print(formatStep(steps[steps.length - 1]));
|
|
5136
5235
|
}
|
|
5137
5236
|
if (source.syncLabels) {
|
|
@@ -5149,6 +5248,21 @@ async function runSetup(input) {
|
|
|
5149
5248
|
if (manual.open_source !== void 0) properties["open_source"] = String(manual.open_source);
|
|
5150
5249
|
if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
|
|
5151
5250
|
if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
|
|
5251
|
+
{
|
|
5252
|
+
const rootPackageJson = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
5253
|
+
const workspacePackages = readWorkspacePackages(input.context.repoRoot);
|
|
5254
|
+
const workspacePackageJsons = await readWorkspacePackageJsons(input.context.repoRoot, workspacePackages);
|
|
5255
|
+
properties["holocron_profile"] = deriveProfile({
|
|
5256
|
+
rootPackageJson,
|
|
5257
|
+
repoName: basename(input.context.repoRoot),
|
|
5258
|
+
isMonorepo,
|
|
5259
|
+
workspacePackageJsons
|
|
5260
|
+
});
|
|
5261
|
+
properties["holocron_stack"] = deriveStack(rootPackageJson).join(", ");
|
|
5262
|
+
const capabilities = deriveCapabilities(config.providers);
|
|
5263
|
+
properties["holocron_capabilities"] = capabilities.join(", ");
|
|
5264
|
+
properties["holocron_compliance"] = deriveCompliance(capabilities);
|
|
5265
|
+
}
|
|
5152
5266
|
if (source.syncProperties) {
|
|
5153
5267
|
steps.push(await runStep("source", "sync properties", dryRun, () => source.syncProperties(properties)));
|
|
5154
5268
|
print(formatStep(steps[steps.length - 1]));
|
|
@@ -6075,6 +6189,21 @@ async function runSync(input) {
|
|
|
6075
6189
|
if (manual.open_source !== void 0) properties["open_source"] = String(manual.open_source);
|
|
6076
6190
|
if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
|
|
6077
6191
|
if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
|
|
6192
|
+
{
|
|
6193
|
+
const rootPackageJson = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
6194
|
+
const workspacePackages = readWorkspacePackages(input.context.repoRoot);
|
|
6195
|
+
const workspacePackageJsons = await readWorkspacePackageJsons(input.context.repoRoot, workspacePackages);
|
|
6196
|
+
properties["holocron_profile"] = deriveProfile({
|
|
6197
|
+
rootPackageJson,
|
|
6198
|
+
repoName: basename(input.context.repoRoot),
|
|
6199
|
+
isMonorepo,
|
|
6200
|
+
workspacePackageJsons
|
|
6201
|
+
});
|
|
6202
|
+
properties["holocron_stack"] = deriveStack(rootPackageJson).join(", ");
|
|
6203
|
+
const capabilities = deriveCapabilities(config.providers);
|
|
6204
|
+
properties["holocron_capabilities"] = capabilities.join(", ");
|
|
6205
|
+
properties["holocron_compliance"] = deriveCompliance(capabilities);
|
|
6206
|
+
}
|
|
6078
6207
|
steps.push(await runSyncStep("source", "sync properties", dryRun, () => source.syncProperties(properties)));
|
|
6079
6208
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
6080
6209
|
} else {
|
|
@@ -6321,11 +6450,12 @@ async function runSync(input) {
|
|
|
6321
6450
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
6322
6451
|
}
|
|
6323
6452
|
if (stepName === "turbo") {
|
|
6324
|
-
const
|
|
6453
|
+
const astromechTurbo = createAstromech({
|
|
6325
6454
|
cwd: input.context.repoRoot,
|
|
6326
6455
|
config: { tasks: config.tasks },
|
|
6327
6456
|
logger
|
|
6328
|
-
})
|
|
6457
|
+
});
|
|
6458
|
+
const content = astromechTurbo.turboConfig();
|
|
6329
6459
|
if (content === null) {
|
|
6330
6460
|
steps.push({
|
|
6331
6461
|
capability: "local",
|
|
@@ -6339,6 +6469,16 @@ async function runSync(input) {
|
|
|
6339
6469
|
await writeFile(join(input.context.repoRoot, "turbo.json"), content, "utf8");
|
|
6340
6470
|
}));
|
|
6341
6471
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
6472
|
+
const rootPkg = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
6473
|
+
const workspaceYaml = await readFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), "utf8").catch(() => "");
|
|
6474
|
+
const rootScripts = Object.keys(rootPkg?.scripts ?? {});
|
|
6475
|
+
const workspaceFix = astromechTurbo.ensureRootWorkspaceMember(workspaceYaml, rootScripts);
|
|
6476
|
+
if (workspaceFix.changed) {
|
|
6477
|
+
steps.push(await runSyncStep("local", "fix pnpm-workspace.yaml (root workspace member, #692)", dryRun, async () => {
|
|
6478
|
+
await writeFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), workspaceFix.content, "utf8");
|
|
6479
|
+
}));
|
|
6480
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6481
|
+
}
|
|
6342
6482
|
}
|
|
6343
6483
|
}
|
|
6344
6484
|
if (stepName === "wiki") {
|