@theholocron/cli 5.0.0-alpha.8 → 5.0.0-alpha.81
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 +66 -0
- package/dist/cli.mjs +610 -136
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +163 -2
- package/dist/index.mjs +233 -2
- package/dist/plugin/capabilities.d.mts +89 -23
- package/package.json +15 -10
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
-
import path, { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import path, { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { checkbox, input, select } from "@inquirer/prompts";
|
|
6
6
|
import { KNOWN_WORKFLOWS, createAstromech, extractPreviewConfig, readWorkspacePackages, reusableTemplates } from "@theholocron/astromech";
|
|
7
7
|
import { loadTasksConfig } from "@theholocron/astromech/config";
|
|
@@ -24,6 +24,8 @@ import { createGitHubClient } from "@theholocron/github-client";
|
|
|
24
24
|
import { PostHogSink } from "@theholocron/observability/analytics";
|
|
25
25
|
import { NoopAnalyticsSink, NoopErrorSink, redactObject } from "@theholocron/observability/core";
|
|
26
26
|
import { SentrySink } from "@theholocron/observability/errors";
|
|
27
|
+
import load from "@commitlint/load";
|
|
28
|
+
import lint from "@commitlint/lint";
|
|
27
29
|
import { promisify } from "node:util";
|
|
28
30
|
import { ConfigFileError, loadConfigFile } from "@theholocron/datapad";
|
|
29
31
|
import search from "@inquirer/search";
|
|
@@ -532,7 +534,8 @@ var PluginLoader = class {
|
|
|
532
534
|
/** Default importer — resolves from cwd first so the global CLI finds project plugins. */
|
|
533
535
|
const defaultImporter$1 = async (pkg) => {
|
|
534
536
|
try {
|
|
535
|
-
|
|
537
|
+
const resolved = createRequire(process.cwd() + "/").resolve(pkg);
|
|
538
|
+
return import(pathToFileURL(resolved).href);
|
|
536
539
|
} catch {
|
|
537
540
|
return import(pkg);
|
|
538
541
|
}
|
|
@@ -664,7 +667,8 @@ const style = {
|
|
|
664
667
|
*/
|
|
665
668
|
const defaultImporter = async (pkg) => {
|
|
666
669
|
try {
|
|
667
|
-
|
|
670
|
+
const resolved = createRequire(process.cwd() + "/").resolve(pkg);
|
|
671
|
+
return await import(pathToFileURL(resolved).href);
|
|
668
672
|
} catch {
|
|
669
673
|
return await import(pkg);
|
|
670
674
|
}
|
|
@@ -1106,8 +1110,10 @@ async function runClone(input) {
|
|
|
1106
1110
|
}).status };
|
|
1107
1111
|
});
|
|
1108
1112
|
print(style.header(`Holocron clone — org=${input.org} → ${targetDir}${dryRun ? " (dry-run)" : ""}`));
|
|
1109
|
-
if (!existsSync(targetDir))
|
|
1110
|
-
|
|
1113
|
+
if (!existsSync(targetDir)) {
|
|
1114
|
+
if (dryRun) print(style.dim(` would create ${targetDir}`));
|
|
1115
|
+
else mkdirSync(targetDir, { recursive: true });
|
|
1116
|
+
}
|
|
1111
1117
|
let repos;
|
|
1112
1118
|
try {
|
|
1113
1119
|
repos = await listOrgRepos(input.org, input.token, fetchFn);
|
|
@@ -1129,7 +1135,8 @@ async function runClone(input) {
|
|
|
1129
1135
|
let skipped = 0;
|
|
1130
1136
|
let failed = 0;
|
|
1131
1137
|
for (const repo of repos) {
|
|
1132
|
-
const
|
|
1138
|
+
const dirName = repo.name.startsWith(".") ? repo.name.slice(1) : repo.name;
|
|
1139
|
+
const dest = join(targetDir, dirName);
|
|
1133
1140
|
if (existsSync(dest)) {
|
|
1134
1141
|
print(style.dim(` skip ${repo.full_name}`));
|
|
1135
1142
|
skipped++;
|
|
@@ -1214,6 +1221,7 @@ const COMMAND_CONTEXTS = {
|
|
|
1214
1221
|
"skills install": "global",
|
|
1215
1222
|
"skills remove": "global",
|
|
1216
1223
|
"skills update": "global",
|
|
1224
|
+
"lint commit-msg": "global",
|
|
1217
1225
|
run: "repo-aware",
|
|
1218
1226
|
ci: "repo-aware",
|
|
1219
1227
|
"config show": "repo-aware",
|
|
@@ -1233,6 +1241,40 @@ function commandsInContext(context) {
|
|
|
1233
1241
|
}
|
|
1234
1242
|
//#endregion
|
|
1235
1243
|
//#region src/commands/deploy.ts
|
|
1244
|
+
/**
|
|
1245
|
+
* `holocron deploy` — trigger a deployment via the configured
|
|
1246
|
+
* `deployment` capability.
|
|
1247
|
+
*
|
|
1248
|
+
* Two modes, one command: `runDeploy` (the default — a Git branch,
|
|
1249
|
+
* `triggerDeployment()`) and `runDeployFromFiles` (`--files <dir>` —
|
|
1250
|
+
* a local directory of source files, `deployFunction()`, no linked
|
|
1251
|
+
* repo required — for a consumer with no Git history to deploy from,
|
|
1252
|
+
* e.g. Sentinel, shipped as an npm package). Separate functions, not
|
|
1253
|
+
* one branching `runDeploy`, because their inputs genuinely differ
|
|
1254
|
+
* (`branch` vs `dir`) and their result shapes differ too
|
|
1255
|
+
* (`DeploymentRecord` carries `status`/`branch`; `DeployFunctionResult`
|
|
1256
|
+
* is deliberately minimal, same as `DeployScriptResult` for Workers).
|
|
1257
|
+
*
|
|
1258
|
+
* Errors clearly when `deployment` isn't loaded (no provider declared
|
|
1259
|
+
* in `holocron.config.json`), or when the configured provider doesn't
|
|
1260
|
+
* implement `deployFunction` (it's optional on `Deployment` — not
|
|
1261
|
+
* every provider has a files-based deploy API).
|
|
1262
|
+
*
|
|
1263
|
+
* Dry-run skips the actual deploy call but still verifies the
|
|
1264
|
+
* deployment capability (and, for `--files`, `deployFunction`) is
|
|
1265
|
+
* present, so operators can sanity-check the wiring without spinning
|
|
1266
|
+
* up a build.
|
|
1267
|
+
*/
|
|
1268
|
+
/**
|
|
1269
|
+
* Provider `url` fields (Vercel's REST API, at least) come back as bare
|
|
1270
|
+
* hostnames — `"my-app.vercel.app"`, no scheme — which terminals don't
|
|
1271
|
+
* linkify and browsers won't navigate to unmodified. Printed output
|
|
1272
|
+
* should be a URL a human can click/paste; structured log fields keep
|
|
1273
|
+
* the raw provider value as-is.
|
|
1274
|
+
*/
|
|
1275
|
+
function withScheme(url) {
|
|
1276
|
+
return /^https?:\/\//.test(url) ? url : `https://${url}`;
|
|
1277
|
+
}
|
|
1236
1278
|
async function runDeploy(input) {
|
|
1237
1279
|
const print = input.print ?? ((line) => console.log(line));
|
|
1238
1280
|
const logger = input.logger ?? getLogger();
|
|
@@ -1264,7 +1306,7 @@ async function runDeploy(input) {
|
|
|
1264
1306
|
branch: input.branch,
|
|
1265
1307
|
...input.target ? { target: input.target } : {}
|
|
1266
1308
|
}));
|
|
1267
|
-
print(` ${style.success(`${record.status} — ${record.url}`)}`);
|
|
1309
|
+
print(` ${style.success(`${record.status} — ${withScheme(record.url)}`)}`);
|
|
1268
1310
|
logger.info({
|
|
1269
1311
|
status: record.status,
|
|
1270
1312
|
url: record.url,
|
|
@@ -1288,6 +1330,87 @@ async function runDeploy(input) {
|
|
|
1288
1330
|
};
|
|
1289
1331
|
}
|
|
1290
1332
|
}
|
|
1333
|
+
const SKIP_DIRS$2 = /* @__PURE__ */ new Set([
|
|
1334
|
+
".git",
|
|
1335
|
+
"node_modules",
|
|
1336
|
+
".turbo"
|
|
1337
|
+
]);
|
|
1338
|
+
/** Recursively lists absolute file paths under `dir`, skipping VCS/tooling noise. */
|
|
1339
|
+
function defaultWalkFiles$2(dir) {
|
|
1340
|
+
const results = [];
|
|
1341
|
+
for (const entry of readdirSync(dir)) {
|
|
1342
|
+
if (SKIP_DIRS$2.has(entry)) continue;
|
|
1343
|
+
const full = join(dir, entry);
|
|
1344
|
+
const stat = statSync(full);
|
|
1345
|
+
if (stat.isDirectory()) results.push(...defaultWalkFiles$2(full));
|
|
1346
|
+
else if (stat.isFile()) results.push(full);
|
|
1347
|
+
}
|
|
1348
|
+
return results;
|
|
1349
|
+
}
|
|
1350
|
+
/** `holocron deploy --files <dir>` — the non-git counterpart to `runDeploy`. */
|
|
1351
|
+
async function runDeployFromFiles(input) {
|
|
1352
|
+
const print = input.print ?? ((line) => console.log(line));
|
|
1353
|
+
const logger = input.logger ?? getLogger();
|
|
1354
|
+
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
1355
|
+
await loader.load();
|
|
1356
|
+
assertPluginsResolvable(loader, "deploy");
|
|
1357
|
+
const dryRun = input.context.dryRun ?? false;
|
|
1358
|
+
const walkFiles = input.walkFiles ?? defaultWalkFiles$2;
|
|
1359
|
+
const readFile = input.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
1360
|
+
const files = {};
|
|
1361
|
+
for (const abs of walkFiles(input.dir)) {
|
|
1362
|
+
const rel = relative(input.dir, abs).split(sep).join("/");
|
|
1363
|
+
files[rel] = readFile(abs);
|
|
1364
|
+
}
|
|
1365
|
+
const fileCount = Object.keys(files).length;
|
|
1366
|
+
logger.info({
|
|
1367
|
+
dir: input.dir,
|
|
1368
|
+
fileCount,
|
|
1369
|
+
target: input.target ?? "preview",
|
|
1370
|
+
projectId: input.projectId,
|
|
1371
|
+
dryRun: dryRun || void 0
|
|
1372
|
+
}, "deploy: start (files)");
|
|
1373
|
+
print(style.header(`Holocron deploy — files=${input.dir} (${fileCount} file${fileCount === 1 ? "" : "s"})${input.target ? `, target=${input.target}` : " (preview)"}${dryRun ? " (dry-run)" : ""}`));
|
|
1374
|
+
if (!loader.has("deployment")) throw new Error("deployment capability is not configured — add a `deployment` provider to holocron.config.json");
|
|
1375
|
+
const deploy = loader.get("deployment");
|
|
1376
|
+
if (!deploy.deployFunction) throw new Error(`deployment provider "${deploy.providerName}" does not support deploying from files — deployFunction() is not implemented`);
|
|
1377
|
+
if (dryRun) {
|
|
1378
|
+
const message = `would: ${deploy.providerName}.deployFunction(projectId=${input.projectId}, files=${fileCount}${input.target ? `, target=${input.target}` : ""})`;
|
|
1379
|
+
print(` ${style.dim(`… ${message}`)}`);
|
|
1380
|
+
return {
|
|
1381
|
+
deployment: null,
|
|
1382
|
+
status: "dry-run",
|
|
1383
|
+
message
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
try {
|
|
1387
|
+
const result = await withSpinner(`Deploying ${fileCount} file${fileCount === 1 ? "" : "s"} from ${input.dir}…`, () => deploy.deployFunction(input.projectId, {
|
|
1388
|
+
files,
|
|
1389
|
+
...input.target ? { target: input.target } : {}
|
|
1390
|
+
}));
|
|
1391
|
+
print(` ${style.success(withScheme(result.url))}`);
|
|
1392
|
+
logger.info({
|
|
1393
|
+
url: result.url,
|
|
1394
|
+
id: result.deploymentId
|
|
1395
|
+
}, "deploy: triggered (files)");
|
|
1396
|
+
return {
|
|
1397
|
+
deployment: result,
|
|
1398
|
+
status: "ok"
|
|
1399
|
+
};
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1402
|
+
print(` ${style.fail(message)}`);
|
|
1403
|
+
logger.warn({
|
|
1404
|
+
dir: input.dir,
|
|
1405
|
+
reason: message
|
|
1406
|
+
}, "deploy: failed (files)");
|
|
1407
|
+
return {
|
|
1408
|
+
deployment: null,
|
|
1409
|
+
status: "fail",
|
|
1410
|
+
message
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1291
1414
|
//#endregion
|
|
1292
1415
|
//#region src/commands/doctor.ts
|
|
1293
1416
|
async function runDoctor(input) {
|
|
@@ -3696,10 +3819,13 @@ async function runSecretsSync(input) {
|
|
|
3696
3819
|
const rows = [];
|
|
3697
3820
|
if (loader.has("secrets")) {
|
|
3698
3821
|
const secrets = loader.get("secrets");
|
|
3699
|
-
|
|
3822
|
+
const scope = input.githubSecretScope ?? { kind: "repo" };
|
|
3823
|
+
const allowedKeys = input.githubSecretKeys ? new Set(input.githubSecretKeys) : void 0;
|
|
3824
|
+
print(style.step("secrets"));
|
|
3700
3825
|
for (const key of keys) {
|
|
3701
|
-
|
|
3702
|
-
|
|
3826
|
+
if (allowedKeys && !allowedKeys.has(key)) continue;
|
|
3827
|
+
rows.push(await runRow(`secrets:${secrets.providerName}`, scopeLabel(scope), key, dryRun, async () => {
|
|
3828
|
+
await secrets.setSecret(scope, key, envVars[key]);
|
|
3703
3829
|
}));
|
|
3704
3830
|
print(formatRow(rows[rows.length - 1]));
|
|
3705
3831
|
}
|
|
@@ -3791,11 +3917,114 @@ function formatRow(row) {
|
|
|
3791
3917
|
if (row.status === "dry-run") return ` ${style.dim(`… ${label}`)}`;
|
|
3792
3918
|
return ` ${style.dim(`· ${label}`)}`;
|
|
3793
3919
|
}
|
|
3920
|
+
function scopeLabel(scope) {
|
|
3921
|
+
if (scope.kind === "repo") return "scope=repo";
|
|
3922
|
+
if (scope.kind === "organization") return `scope=organization,org=${scope.name}`;
|
|
3923
|
+
return `scope=environment,env=${scope.name}`;
|
|
3924
|
+
}
|
|
3794
3925
|
function vaultProviderName(loader) {
|
|
3795
3926
|
if (!loader.has("vault")) return "<missing>";
|
|
3796
3927
|
return loader.get("vault").providerName;
|
|
3797
3928
|
}
|
|
3798
3929
|
//#endregion
|
|
3930
|
+
//#region src/commands/setup/derived-properties.ts
|
|
3931
|
+
/**
|
|
3932
|
+
* Repo archetype, in priority order:
|
|
3933
|
+
* 1. `*-template` repo name → `template` (independent of package shape).
|
|
3934
|
+
* 2. Monorepo whose primary published artifact is a CLI (some workspace
|
|
3935
|
+
* package has `bin`) → `platform` — matches `holocron` itself: the CLI is
|
|
3936
|
+
* the deliverable, the plugin packages are supporting infrastructure for
|
|
3937
|
+
* it, not separate products.
|
|
3938
|
+
* 3. Monorepo with no CLI but at least one non-private workspace package →
|
|
3939
|
+
* `library` — matches `clients`/`configs`/`utils`/`themes`/
|
|
3940
|
+
* `observability`: a family of published packages, none of them a CLI.
|
|
3941
|
+
* Checked *before* falling into the root-package branch below, because
|
|
3942
|
+
* these repos' own root `package.json` is private and often carries
|
|
3943
|
+
* `@theholocron/astro-config` for a docs site built from the monorepo
|
|
3944
|
+
* root — without this check they'd wrongly resolve to `docs` off that
|
|
3945
|
+
* root-level signal alone, when the docs site is a secondary concern of
|
|
3946
|
+
* a library collection, not what the repo fundamentally is.
|
|
3947
|
+
* 4. Single-package repo with `package.json#bin` → `cli`.
|
|
3948
|
+
* 5. Docs-only site (astro/starlight present, root package private, no
|
|
3949
|
+
* publishable exports) → `docs`.
|
|
3950
|
+
* 6. Private, non-publishable, non-docs → `app`.
|
|
3951
|
+
* 7. Otherwise, a single publishable package → `library`.
|
|
3952
|
+
*/
|
|
3953
|
+
function deriveProfile(input) {
|
|
3954
|
+
if (/-template$/.test(input.repoName)) return "template";
|
|
3955
|
+
if (input.isMonorepo) {
|
|
3956
|
+
if (input.workspacePackageJsons.some((pkg) => Boolean(pkg.bin))) return "platform";
|
|
3957
|
+
if (input.workspacePackageJsons.some((pkg) => pkg.private !== true)) return "library";
|
|
3958
|
+
}
|
|
3959
|
+
const pkg = input.rootPackageJson;
|
|
3960
|
+
if (pkg?.bin) return "cli";
|
|
3961
|
+
const deps = {
|
|
3962
|
+
...pkg?.dependencies,
|
|
3963
|
+
...pkg?.devDependencies
|
|
3964
|
+
};
|
|
3965
|
+
const isDocsSite = Boolean(deps["@theholocron/astro-config"] || deps["astro"]);
|
|
3966
|
+
if (pkg?.private && isDocsSite) return "docs";
|
|
3967
|
+
if (pkg?.private) return "app";
|
|
3968
|
+
return "library";
|
|
3969
|
+
}
|
|
3970
|
+
const KNOWN_STACK_DEPS = [
|
|
3971
|
+
"next",
|
|
3972
|
+
"vite",
|
|
3973
|
+
"astro",
|
|
3974
|
+
"tsdown",
|
|
3975
|
+
"rollup",
|
|
3976
|
+
"webpack",
|
|
3977
|
+
"storybook",
|
|
3978
|
+
"vitest",
|
|
3979
|
+
"playwright",
|
|
3980
|
+
"turbo"
|
|
3981
|
+
];
|
|
3982
|
+
/**
|
|
3983
|
+
* Detected build/framework tooling — the literal "does this repo need
|
|
3984
|
+
* next/vite/tsdown" signal `holocron.config.ts` has no field for today
|
|
3985
|
+
* (framework choice isn't a config concept, unlike `providers`/`tasks`).
|
|
3986
|
+
* Curated table, not exhaustive by design — add to `KNOWN_STACK_DEPS` as new
|
|
3987
|
+
* frameworks show up rather than trying to detect anything on npm.
|
|
3988
|
+
*/
|
|
3989
|
+
function deriveStack(pkg) {
|
|
3990
|
+
const deps = {
|
|
3991
|
+
...pkg?.dependencies,
|
|
3992
|
+
...pkg?.devDependencies
|
|
3993
|
+
};
|
|
3994
|
+
return KNOWN_STACK_DEPS.filter((name) => name in deps);
|
|
3995
|
+
}
|
|
3996
|
+
/** The provider capability keys actually wired in `providers: {}` — zero heuristics. */
|
|
3997
|
+
function deriveCapabilities(providers) {
|
|
3998
|
+
return Object.keys(providers ?? {}).sort();
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* `source` + `ci` are the minimal baseline every repo `holocron setup`
|
|
4002
|
+
* touches wires — every `holocron.config.ts` in this org already satisfies
|
|
4003
|
+
* this, so `non-compliant` here means drift (a provider manually removed
|
|
4004
|
+
* after setup), not an unmet aspirational policy. Deliberately not a richer
|
|
4005
|
+
* per-profile policy yet (e.g. "a `library` must have `deployment`") — that's
|
|
4006
|
+
* a Phase B (GitHub App) refinement once it can post *why* on a check run.
|
|
4007
|
+
*/
|
|
4008
|
+
const REQUIRED_BASELINE = ["source", "ci"];
|
|
4009
|
+
function deriveCompliance(capabilities) {
|
|
4010
|
+
return REQUIRED_BASELINE.every((key) => capabilities.includes(key)) ? "compliant" : "non-compliant";
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* Reads each workspace package's actual `package.json` content (not just
|
|
4014
|
+
* `readWorkspacePackages()`'s `{ slug, name, dir }` — `deriveProfile()`'s
|
|
4015
|
+
* `platform` detection needs `bin`, which that helper doesn't carry).
|
|
4016
|
+
* Missing/invalid files are skipped, matching `readWorkspacePackages()`'s
|
|
4017
|
+
* own soft-fail behavior.
|
|
4018
|
+
*/
|
|
4019
|
+
async function readWorkspacePackageJsons(repoRoot, workspacePackages) {
|
|
4020
|
+
const results = [];
|
|
4021
|
+
for (const { slug, dir = "packages" } of workspacePackages) try {
|
|
4022
|
+
const raw = await readFile(join(repoRoot, dir, slug, "package.json"), "utf8");
|
|
4023
|
+
results.push(JSON.parse(raw));
|
|
4024
|
+
} catch {}
|
|
4025
|
+
return results;
|
|
4026
|
+
}
|
|
4027
|
+
//#endregion
|
|
3799
4028
|
//#region src/commands/setup/labels.ts
|
|
3800
4029
|
const CANONICAL_LABELS = [
|
|
3801
4030
|
{
|
|
@@ -3935,6 +4164,9 @@ var alexignore_default = ".github/*\nCHANGELOG.md\nLICENSE\n";
|
|
|
3935
4164
|
//#region src/templates/configs/alexjs/alexrc.json
|
|
3936
4165
|
var alexrc_default = { allow: [
|
|
3937
4166
|
"dead",
|
|
4167
|
+
"execute",
|
|
4168
|
+
"execution",
|
|
4169
|
+
"executes",
|
|
3938
4170
|
"failure",
|
|
3939
4171
|
"failures",
|
|
3940
4172
|
"hook",
|
|
@@ -3944,12 +4176,30 @@ var alexrc_default = { allow: [
|
|
|
3944
4176
|
] };
|
|
3945
4177
|
//#endregion
|
|
3946
4178
|
//#region src/templates/configs/alexjs/create-config.ts
|
|
3947
|
-
const { workflowHeader: workflowHeader$
|
|
4179
|
+
const { workflowHeader: workflowHeader$9 } = createHeader({ source: "packages/cli/src/templates/configs/alexjs/create-config.ts" });
|
|
3948
4180
|
function createRcConfig() {
|
|
3949
4181
|
return JSON.stringify(alexrc_default, null, 2) + "\n";
|
|
3950
4182
|
}
|
|
3951
4183
|
function createIgnoreConfig$1() {
|
|
3952
|
-
return `${workflowHeader$
|
|
4184
|
+
return `${workflowHeader$9()}${alexignore_default}`;
|
|
4185
|
+
}
|
|
4186
|
+
//#endregion
|
|
4187
|
+
//#region src/templates/configs/commit-msg/commit-msg
|
|
4188
|
+
var commit_msg_default = "__HOLOCRON_SCRIPT__ lint commit-msg \"$1\"\n";
|
|
4189
|
+
//#endregion
|
|
4190
|
+
//#region src/templates/configs/commit-msg/create-config.ts
|
|
4191
|
+
const { workflowHeader: workflowHeader$8 } = createHeader({ source: "packages/cli/src/templates/configs/commit-msg/create-config.ts" });
|
|
4192
|
+
/**
|
|
4193
|
+
* `.husky/commit-msg` — lints the commit message via `holocron lint
|
|
4194
|
+
* commit-msg` (holocron#789), the real programmatic replacement for
|
|
4195
|
+
* shelling out to `commitlint --edit`. Installed by `holocron setup` when
|
|
4196
|
+
* hooks are enabled. Same `__HOLOCRON_SCRIPT__` substitution as `pre-push`.
|
|
4197
|
+
*
|
|
4198
|
+
* @param holocronScript - the command that invokes the CLI. Defaults to
|
|
4199
|
+
* `pnpm exec holocron`; the source repo passes `node packages/cli/dist/cli.mjs`.
|
|
4200
|
+
*/
|
|
4201
|
+
function createConfig$7(holocronScript = "pnpm exec holocron") {
|
|
4202
|
+
return `${workflowHeader$8("shebang")}${commit_msg_default.replace("__HOLOCRON_SCRIPT__", holocronScript)}`;
|
|
3953
4203
|
}
|
|
3954
4204
|
//#endregion
|
|
3955
4205
|
//#region src/templates/configs/devmoji/create-config.ts
|
|
@@ -4054,7 +4304,7 @@ function createConfig$2(holocronScript = "pnpm exec holocron") {
|
|
|
4054
4304
|
}
|
|
4055
4305
|
//#endregion
|
|
4056
4306
|
//#region src/templates/configs/prepare-commit-msg/prepare-commit-msg
|
|
4057
|
-
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\
|
|
4307
|
+
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\n# --config <resolved shared path> (config-resolution workstream, #676) — falls\n# back to devmoji's own auto-discovery of a local devmoji.config.cjs when the\n# shared package's built dist/ isn't present (not installed, or a stale\n# install predating this file).\nDEVMOJI_CONFIG=\"node_modules/@theholocron/devmoji-config/dist/index.cjs\"\nif [ -f \"$DEVMOJI_CONFIG\" ]; then\n npx devmoji --config \"$DEVMOJI_CONFIG\" -e\nelse\n npx devmoji -e\nfi\n";
|
|
4058
4308
|
//#endregion
|
|
4059
4309
|
//#region src/templates/configs/prepare-commit-msg/create-config.ts
|
|
4060
4310
|
const { workflowHeader: workflowHeader$3 } = createHeader({ source: "packages/cli/src/templates/configs/prepare-commit-msg/create-config.ts" });
|
|
@@ -4945,7 +5195,10 @@ async function runSetup(input) {
|
|
|
4945
5195
|
print(formatStep(steps[steps.length - 1]));
|
|
4946
5196
|
if (effectivePreset && effectivePreset !== "none") {
|
|
4947
5197
|
steps.push(await runStep("source", "updateRepoSettings", dryRun, async () => {
|
|
4948
|
-
await source.updateRepoSettings(
|
|
5198
|
+
await source.updateRepoSettings({
|
|
5199
|
+
...BALANCED_REPO_SETTINGS,
|
|
5200
|
+
...repo?.defaultBranch ? { default_branch: repo.defaultBranch } : {}
|
|
5201
|
+
});
|
|
4949
5202
|
}));
|
|
4950
5203
|
print(formatStep(steps[steps.length - 1]));
|
|
4951
5204
|
const tasksConfig = { tasks: config.tasks };
|
|
@@ -5051,6 +5304,10 @@ async function runSetup(input) {
|
|
|
5051
5304
|
await source.writeRepoFile(".husky/prepare-commit-msg", createConfig$1());
|
|
5052
5305
|
}));
|
|
5053
5306
|
print(formatStep(steps[steps.length - 1]));
|
|
5307
|
+
steps.push(await runStep("source", "write .husky/commit-msg", dryRun, async () => {
|
|
5308
|
+
await source.writeRepoFile(".husky/commit-msg", createConfig$7(config.holocronScript));
|
|
5309
|
+
}));
|
|
5310
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5054
5311
|
if (hooksEnabled) {
|
|
5055
5312
|
steps.push(await runStep("source", "write .husky/pre-push", dryRun, async () => {
|
|
5056
5313
|
await source.writeRepoFile(".husky/pre-push", createConfig$2(config.holocronScript));
|
|
@@ -5105,20 +5362,38 @@ async function runSetup(input) {
|
|
|
5105
5362
|
print(formatStep(steps[steps.length - 1]));
|
|
5106
5363
|
}
|
|
5107
5364
|
{
|
|
5108
|
-
const
|
|
5365
|
+
const astromechTurbo = createAstromech({
|
|
5109
5366
|
cwd: input.context.repoRoot,
|
|
5110
5367
|
config: { tasks: config.tasks },
|
|
5111
5368
|
logger
|
|
5112
|
-
})
|
|
5369
|
+
});
|
|
5370
|
+
const content = astromechTurbo.turboConfig();
|
|
5113
5371
|
if (content === null) steps.push({
|
|
5114
5372
|
capability: "source",
|
|
5115
5373
|
step: "write turbo.json",
|
|
5116
5374
|
status: "skip",
|
|
5117
5375
|
message: "no fan-out-eligible tasks configured"
|
|
5118
5376
|
});
|
|
5119
|
-
else
|
|
5120
|
-
await
|
|
5121
|
-
|
|
5377
|
+
else {
|
|
5378
|
+
steps.push(await runStep("source", "write turbo.json", dryRun, async () => {
|
|
5379
|
+
await source.writeRepoFile("turbo.json", content);
|
|
5380
|
+
}));
|
|
5381
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5382
|
+
const rootPkgRaw = await readFile(join(input.context.repoRoot, "package.json"), "utf8").catch(() => null);
|
|
5383
|
+
const rootPkg = rootPkgRaw ? JSON.parse(rootPkgRaw) : null;
|
|
5384
|
+
const workspaceYaml = await readFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), "utf8").catch(() => "");
|
|
5385
|
+
const rootScripts = rootPkg?.scripts ?? {};
|
|
5386
|
+
const workspaceFix = astromechTurbo.ensureRootWorkspaceMember(workspaceYaml, rootScripts);
|
|
5387
|
+
if (workspaceFix.changed) steps.push(await runStep("source", "fix pnpm-workspace.yaml (root workspace member, #692)", dryRun, async () => {
|
|
5388
|
+
await source.writeRepoFile("pnpm-workspace.yaml", workspaceFix.content);
|
|
5389
|
+
}));
|
|
5390
|
+
if (rootPkgRaw) {
|
|
5391
|
+
const depFix = astromechTurbo.ensureTurboDependency(rootPkgRaw, workspaceFix.content);
|
|
5392
|
+
if (depFix.changed) steps.push(await runStep("source", "add turbo devDependency", dryRun, async () => {
|
|
5393
|
+
await source.writeRepoFile("package.json", depFix.content);
|
|
5394
|
+
}));
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5122
5397
|
print(formatStep(steps[steps.length - 1]));
|
|
5123
5398
|
}
|
|
5124
5399
|
if (source.syncLabels) {
|
|
@@ -5128,7 +5403,7 @@ async function runSetup(input) {
|
|
|
5128
5403
|
print(formatStep(steps[steps.length - 1]));
|
|
5129
5404
|
}
|
|
5130
5405
|
const properties = {};
|
|
5131
|
-
if (effectivePreset && effectivePreset !== "none") properties["
|
|
5406
|
+
if (effectivePreset && effectivePreset !== "none") properties["holocron_branch_protection_level"] = effectivePreset;
|
|
5132
5407
|
const isMonorepo = await access(join(input.context.repoRoot, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
|
|
5133
5408
|
properties["monorepo"] = String(isMonorepo);
|
|
5134
5409
|
const manual = repo?.properties ?? {};
|
|
@@ -5136,6 +5411,21 @@ async function runSetup(input) {
|
|
|
5136
5411
|
if (manual.open_source !== void 0) properties["open_source"] = String(manual.open_source);
|
|
5137
5412
|
if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
|
|
5138
5413
|
if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
|
|
5414
|
+
{
|
|
5415
|
+
const rootPackageJson = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
5416
|
+
const workspacePackages = readWorkspacePackages(input.context.repoRoot);
|
|
5417
|
+
const workspacePackageJsons = await readWorkspacePackageJsons(input.context.repoRoot, workspacePackages);
|
|
5418
|
+
properties["holocron_profile"] = deriveProfile({
|
|
5419
|
+
rootPackageJson,
|
|
5420
|
+
repoName: basename(input.context.repoRoot),
|
|
5421
|
+
isMonorepo,
|
|
5422
|
+
workspacePackageJsons
|
|
5423
|
+
});
|
|
5424
|
+
properties["holocron_stack"] = deriveStack(rootPackageJson);
|
|
5425
|
+
const capabilities = deriveCapabilities(config.providers);
|
|
5426
|
+
properties["holocron_capabilities"] = capabilities;
|
|
5427
|
+
properties["holocron_compliance"] = deriveCompliance(capabilities);
|
|
5428
|
+
}
|
|
5139
5429
|
if (source.syncProperties) {
|
|
5140
5430
|
steps.push(await runStep("source", "sync properties", dryRun, () => source.syncProperties(properties)));
|
|
5141
5431
|
print(formatStep(steps[steps.length - 1]));
|
|
@@ -5154,34 +5444,36 @@ async function runSetup(input) {
|
|
|
5154
5444
|
print(formatStep(steps[steps.length - 1]));
|
|
5155
5445
|
}
|
|
5156
5446
|
const teams = repo?.teams ?? [];
|
|
5157
|
-
if (teams.length > 0)
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5447
|
+
if (teams.length > 0) {
|
|
5448
|
+
if (source.syncTeams) {
|
|
5449
|
+
steps.push(await runStep("source", "sync teams", dryRun, () => source.syncTeams(teams), { skipCodes: [422] }));
|
|
5450
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5451
|
+
const repoCoord = input.context.repo ?? repo?.name ?? "";
|
|
5452
|
+
const org = repoCoord.includes("/") ? repoCoord.split("/")[0] : "";
|
|
5453
|
+
const writeableTeams = teams.map((t) => typeof t === "string" ? {
|
|
5454
|
+
slug: t,
|
|
5455
|
+
permission: "push"
|
|
5456
|
+
} : t).filter((t) => [
|
|
5457
|
+
"push",
|
|
5458
|
+
"maintain",
|
|
5459
|
+
"admin"
|
|
5460
|
+
].includes(t.permission));
|
|
5461
|
+
if (org && writeableTeams.length > 0) {
|
|
5462
|
+
steps.push(await runStep("source", "write .github/CODEOWNERS", dryRun, async () => {
|
|
5463
|
+
const content = writeableTeams.map((t) => `* @${org}/${t.slug}`).join("\n") + "\n";
|
|
5464
|
+
await source.writeRepoFile(".github/CODEOWNERS", content);
|
|
5465
|
+
}));
|
|
5466
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5467
|
+
}
|
|
5468
|
+
} else {
|
|
5469
|
+
steps.push({
|
|
5470
|
+
capability: "source",
|
|
5471
|
+
step: "sync teams",
|
|
5472
|
+
status: "skip",
|
|
5473
|
+
message: "provider does not implement syncTeams"
|
|
5474
|
+
});
|
|
5175
5475
|
print(formatStep(steps[steps.length - 1]));
|
|
5176
5476
|
}
|
|
5177
|
-
} else {
|
|
5178
|
-
steps.push({
|
|
5179
|
-
capability: "source",
|
|
5180
|
-
step: "sync teams",
|
|
5181
|
-
status: "skip",
|
|
5182
|
-
message: "provider does not implement syncTeams"
|
|
5183
|
-
});
|
|
5184
|
-
print(formatStep(steps[steps.length - 1]));
|
|
5185
5477
|
}
|
|
5186
5478
|
}
|
|
5187
5479
|
if (loader.has("environments")) {
|
|
@@ -5237,11 +5529,9 @@ async function runSetup(input) {
|
|
|
5237
5529
|
const wikiProxy = wiki.proxyConfig?.();
|
|
5238
5530
|
if (wikiDns && loader.has("dns")) {
|
|
5239
5531
|
const dns = loader.get("dns");
|
|
5240
|
-
steps.push(await runStep("dns", `upsertRecord ${wikiDns.
|
|
5532
|
+
steps.push(await runStep("dns", `upsertRecord ${wikiDns.record.name}`, dryRun, async () => {
|
|
5241
5533
|
await dns.upsertRecord(wikiDns.zone, {
|
|
5242
|
-
|
|
5243
|
-
name: wikiDns.cname,
|
|
5244
|
-
content: wikiDns.target,
|
|
5534
|
+
...wikiDns.record,
|
|
5245
5535
|
ttl: 1,
|
|
5246
5536
|
...wikiProxy ? { proxied: true } : {}
|
|
5247
5537
|
});
|
|
@@ -5250,8 +5540,8 @@ async function runSetup(input) {
|
|
|
5250
5540
|
}
|
|
5251
5541
|
if (wikiProxy && wikiDns && loader.has("workers")) {
|
|
5252
5542
|
const workers = loader.get("workers");
|
|
5253
|
-
steps.push(await runStep("workers", `upsertProxy ${wikiDns.
|
|
5254
|
-
await workers.upsertProxy(wikiDns.
|
|
5543
|
+
steps.push(await runStep("workers", `upsertProxy ${wikiDns.record.name}`, dryRun, async () => {
|
|
5544
|
+
await workers.upsertProxy(wikiDns.record.name, wikiProxy);
|
|
5255
5545
|
}));
|
|
5256
5546
|
print(formatStep(steps[steps.length - 1]));
|
|
5257
5547
|
}
|
|
@@ -5264,11 +5554,31 @@ async function runSetup(input) {
|
|
|
5264
5554
|
org: config.org,
|
|
5265
5555
|
domain: config.domain
|
|
5266
5556
|
}) : null;
|
|
5557
|
+
const ensureCustomDomain = async (projectId, hostname) => {
|
|
5558
|
+
if (!deploy.ensureCustomDomain) return;
|
|
5559
|
+
const customDomainDns = { value: null };
|
|
5560
|
+
steps.push(await runStep("deployment", `ensureCustomDomain ${hostname}`, dryRun, async () => {
|
|
5561
|
+
customDomainDns.value = await deploy.ensureCustomDomain(projectId, hostname);
|
|
5562
|
+
}));
|
|
5563
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5564
|
+
if (customDomainDns.value && loader.has("dns")) {
|
|
5565
|
+
const dns = loader.get("dns");
|
|
5566
|
+
const dnsRecord = customDomainDns.value;
|
|
5567
|
+
steps.push(await runStep("dns", `upsertRecord ${dnsRecord.record.name}`, dryRun, async () => {
|
|
5568
|
+
await dns.upsertRecord(dnsRecord.zone, {
|
|
5569
|
+
...dnsRecord.record,
|
|
5570
|
+
ttl: 1
|
|
5571
|
+
});
|
|
5572
|
+
}));
|
|
5573
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5574
|
+
}
|
|
5575
|
+
};
|
|
5267
5576
|
if (!previewCfg) {
|
|
5268
5577
|
steps.push(await runStep("deployment", `ensureProject ${config.name}`, dryRun, async () => {
|
|
5269
5578
|
await deploy.ensureProject({ name: config.name });
|
|
5270
5579
|
}));
|
|
5271
5580
|
print(formatStep(steps[steps.length - 1]));
|
|
5581
|
+
if (deploy.domain) await ensureCustomDomain(config.name, deploy.domain);
|
|
5272
5582
|
}
|
|
5273
5583
|
if (previewCfg) {
|
|
5274
5584
|
steps.push(await runStep("deployment", `ensureProject ${previewCfg.project}`, dryRun, async () => {
|
|
@@ -5276,13 +5586,8 @@ async function runSetup(input) {
|
|
|
5276
5586
|
}));
|
|
5277
5587
|
print(formatStep(steps[steps.length - 1]));
|
|
5278
5588
|
}
|
|
5279
|
-
if (previewCfg?.domain
|
|
5280
|
-
|
|
5281
|
-
await deploy.ensureCustomDomain(previewCfg.project, previewCfg.domain);
|
|
5282
|
-
}));
|
|
5283
|
-
print(formatStep(steps[steps.length - 1]));
|
|
5284
|
-
}
|
|
5285
|
-
if (previewCfg?.domain && loader.has("dns")) {
|
|
5589
|
+
if (previewCfg?.domain) await ensureCustomDomain(previewCfg.project, previewCfg.domain);
|
|
5590
|
+
if (previewCfg?.domain && deploy.providerName === "cloudflare" && loader.has("dns")) {
|
|
5286
5591
|
const dns = loader.get("dns");
|
|
5287
5592
|
const wildcardDomain = `*.${previewCfg.domain}`;
|
|
5288
5593
|
steps.push(await runStep("dns", `upsertRecord ${previewCfg.domain}`, dryRun, async () => {
|
|
@@ -5730,11 +6035,13 @@ async function runSyncReadme(input) {
|
|
|
5730
6035
|
}
|
|
5731
6036
|
const description = loaded.resolved.description ?? "";
|
|
5732
6037
|
const homepage = loaded.resolved.homepage ?? pkg.homepage ?? "";
|
|
6038
|
+
const scripts = pkg.scripts ?? {};
|
|
6039
|
+
const packages = pkg.name ? REGISTRY_MAP[pkg.name]?.() : void 0;
|
|
5733
6040
|
const sections = generateReadme({
|
|
5734
6041
|
description,
|
|
5735
6042
|
homepage,
|
|
5736
|
-
scripts
|
|
5737
|
-
packages
|
|
6043
|
+
scripts,
|
|
6044
|
+
packages
|
|
5738
6045
|
});
|
|
5739
6046
|
const markerSections = {
|
|
5740
6047
|
...sections.packages !== void 0 && { packages: sections.packages },
|
|
@@ -5976,7 +6283,7 @@ async function runSyncWiki(input) {
|
|
|
5976
6283
|
capability: "local",
|
|
5977
6284
|
step: "sync wiki",
|
|
5978
6285
|
status: "fail",
|
|
5979
|
-
message: err instanceof Error ? err.message :
|
|
6286
|
+
message: err instanceof Error ? err.message : /* c8 ignore next */ String(err)
|
|
5980
6287
|
};
|
|
5981
6288
|
}
|
|
5982
6289
|
}
|
|
@@ -6038,40 +6345,59 @@ async function runSync(input) {
|
|
|
6038
6345
|
for (const stepName of SYNC_STEPS) {
|
|
6039
6346
|
if (requestedSteps !== void 0 && !requestedSteps.includes(stepName)) continue;
|
|
6040
6347
|
if (LOCAL_STEPS.has(stepName)) continue;
|
|
6041
|
-
if (stepName === "labels")
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6348
|
+
if (stepName === "labels") {
|
|
6349
|
+
if (source.syncLabels) {
|
|
6350
|
+
steps.push(await runSyncStep("source", "sync labels", dryRun, () => source.syncLabels(CANONICAL_LABELS, STALE_LABELS)));
|
|
6351
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6352
|
+
} else {
|
|
6353
|
+
steps.push({
|
|
6354
|
+
capability: "source",
|
|
6355
|
+
step: "sync labels",
|
|
6356
|
+
status: "skip",
|
|
6357
|
+
message: "provider does not implement syncLabels"
|
|
6358
|
+
});
|
|
6359
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6360
|
+
}
|
|
6052
6361
|
}
|
|
6053
|
-
if (stepName === "properties")
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6362
|
+
if (stepName === "properties") {
|
|
6363
|
+
if (source.syncProperties) {
|
|
6364
|
+
const repo = config.repo;
|
|
6365
|
+
const properties = {};
|
|
6366
|
+
const effectivePreset = repo?.protection;
|
|
6367
|
+
if (effectivePreset && effectivePreset !== "none") properties["holocron_branch_protection_level"] = effectivePreset;
|
|
6368
|
+
const isMonorepo = await access(join(input.context.repoRoot, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
|
|
6369
|
+
properties["monorepo"] = String(isMonorepo);
|
|
6370
|
+
const manual = repo?.properties ?? {};
|
|
6371
|
+
if (manual.lifecycle) properties["lifecycle"] = manual.lifecycle;
|
|
6372
|
+
if (manual.open_source !== void 0) properties["open_source"] = String(manual.open_source);
|
|
6373
|
+
if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
|
|
6374
|
+
if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
|
|
6375
|
+
{
|
|
6376
|
+
const rootPackageJson = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
6377
|
+
const workspacePackages = readWorkspacePackages(input.context.repoRoot);
|
|
6378
|
+
const workspacePackageJsons = await readWorkspacePackageJsons(input.context.repoRoot, workspacePackages);
|
|
6379
|
+
properties["holocron_profile"] = deriveProfile({
|
|
6380
|
+
rootPackageJson,
|
|
6381
|
+
repoName: basename(input.context.repoRoot),
|
|
6382
|
+
isMonorepo,
|
|
6383
|
+
workspacePackageJsons
|
|
6384
|
+
});
|
|
6385
|
+
properties["holocron_stack"] = deriveStack(rootPackageJson);
|
|
6386
|
+
const capabilities = deriveCapabilities(config.providers);
|
|
6387
|
+
properties["holocron_capabilities"] = capabilities;
|
|
6388
|
+
properties["holocron_compliance"] = deriveCompliance(capabilities);
|
|
6389
|
+
}
|
|
6390
|
+
steps.push(await runSyncStep("source", "sync properties", dryRun, () => source.syncProperties(properties)));
|
|
6391
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6392
|
+
} else {
|
|
6393
|
+
steps.push({
|
|
6394
|
+
capability: "source",
|
|
6395
|
+
step: "sync properties",
|
|
6396
|
+
status: "skip",
|
|
6397
|
+
message: "provider does not implement syncProperties"
|
|
6398
|
+
});
|
|
6399
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6400
|
+
}
|
|
6075
6401
|
}
|
|
6076
6402
|
if (stepName === "teams") {
|
|
6077
6403
|
const teams = config.repo?.teams ?? [];
|
|
@@ -6283,36 +6609,42 @@ async function runSync(input) {
|
|
|
6283
6609
|
}
|
|
6284
6610
|
}
|
|
6285
6611
|
}
|
|
6286
|
-
if (stepName === "scripts")
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
const
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6612
|
+
if (stepName === "scripts") {
|
|
6613
|
+
if (config.syncScripts === false) {
|
|
6614
|
+
steps.push({
|
|
6615
|
+
capability: "local",
|
|
6616
|
+
step: "sync scripts",
|
|
6617
|
+
status: "skip",
|
|
6618
|
+
message: "syncScripts: false"
|
|
6619
|
+
});
|
|
6620
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6621
|
+
} else {
|
|
6622
|
+
const tasksConfig = { tasks: config.tasks };
|
|
6623
|
+
if (config.holocronScript !== void 0) tasksConfig.holocronScript = config.holocronScript;
|
|
6624
|
+
const astromechScripts = createAstromech({
|
|
6625
|
+
cwd: input.context.repoRoot,
|
|
6626
|
+
config: tasksConfig,
|
|
6627
|
+
logger
|
|
6628
|
+
});
|
|
6629
|
+
const desired = astromechScripts.packageScripts();
|
|
6630
|
+
const turboContent = astromechScripts.turboConfig();
|
|
6631
|
+
const turboEligible = new Set(turboContent ? Object.keys(JSON.parse(turboContent).tasks) : []);
|
|
6632
|
+
steps.push(await runSyncStep("local", "sync scripts", dryRun, async () => {
|
|
6633
|
+
const changed = await mergePackageJsonScripts(input.context.repoRoot, desired, turboEligible);
|
|
6634
|
+
if (changed === null) return "no package.json";
|
|
6635
|
+
if (changed.length === 0) return `${Object.keys(desired).length} scripts already current`;
|
|
6636
|
+
return `${changed.join(", ")} set`;
|
|
6637
|
+
}));
|
|
6638
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6639
|
+
}
|
|
6309
6640
|
}
|
|
6310
6641
|
if (stepName === "turbo") {
|
|
6311
|
-
const
|
|
6642
|
+
const astromechTurbo = createAstromech({
|
|
6312
6643
|
cwd: input.context.repoRoot,
|
|
6313
6644
|
config: { tasks: config.tasks },
|
|
6314
6645
|
logger
|
|
6315
|
-
})
|
|
6646
|
+
});
|
|
6647
|
+
const content = astromechTurbo.turboConfig();
|
|
6316
6648
|
if (content === null) {
|
|
6317
6649
|
steps.push({
|
|
6318
6650
|
capability: "local",
|
|
@@ -6326,6 +6658,26 @@ async function runSync(input) {
|
|
|
6326
6658
|
await writeFile(join(input.context.repoRoot, "turbo.json"), content, "utf8");
|
|
6327
6659
|
}));
|
|
6328
6660
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
6661
|
+
const rootPkgRaw = await readFile(join(input.context.repoRoot, "package.json"), "utf8").catch(() => null);
|
|
6662
|
+
const rootPkg = rootPkgRaw ? JSON.parse(rootPkgRaw) : null;
|
|
6663
|
+
const workspaceYaml = await readFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), "utf8").catch(() => "");
|
|
6664
|
+
const rootScripts = rootPkg?.scripts ?? {};
|
|
6665
|
+
const workspaceFix = astromechTurbo.ensureRootWorkspaceMember(workspaceYaml, rootScripts);
|
|
6666
|
+
if (workspaceFix.changed) {
|
|
6667
|
+
steps.push(await runSyncStep("local", "fix pnpm-workspace.yaml (root workspace member, #692)", dryRun, async () => {
|
|
6668
|
+
await writeFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), workspaceFix.content, "utf8");
|
|
6669
|
+
}));
|
|
6670
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6671
|
+
}
|
|
6672
|
+
if (rootPkgRaw) {
|
|
6673
|
+
const depFix = astromechTurbo.ensureTurboDependency(rootPkgRaw, workspaceFix.content);
|
|
6674
|
+
if (depFix.changed) {
|
|
6675
|
+
steps.push(await runSyncStep("local", "add turbo devDependency", dryRun, async () => {
|
|
6676
|
+
await writeFile(join(input.context.repoRoot, "package.json"), depFix.content, "utf8");
|
|
6677
|
+
}));
|
|
6678
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6679
|
+
}
|
|
6680
|
+
}
|
|
6329
6681
|
}
|
|
6330
6682
|
}
|
|
6331
6683
|
if (stepName === "wiki") {
|
|
@@ -6409,8 +6761,21 @@ async function writePackageJsonField(repoRoot, field, value) {
|
|
|
6409
6761
|
* Merge `desired` script entries into `package.json#scripts` — sets only the
|
|
6410
6762
|
* listed keys, never touches the rest. Returns the sorted list of keys that
|
|
6411
6763
|
* drifted (empty when all current), or `null` when there is no `package.json`.
|
|
6764
|
+
*
|
|
6765
|
+
* `turboEligible` (#747): task names `turbo.json` fans out — the same set
|
|
6766
|
+
* {@link EnsureRootWorkspaceMemberResult} above already keys off. For those
|
|
6767
|
+
* only, an *existing* script that isn't the generated `holocron run <task>
|
|
6768
|
+
* --` wrapper is left untouched: root may genuinely be a directly-buildable
|
|
6769
|
+
* package (not an orchestrator) with its own real command (`tsc --noEmit`,
|
|
6770
|
+
* `vitest run`, …) for that task. Overwriting it would silently discard the
|
|
6771
|
+
* real command and, once `turbo.json` exists and root is a real workspace
|
|
6772
|
+
* member (#692), reintroduce infinite recursion — `turbo run <task>` →
|
|
6773
|
+
* root's own `<task>` script → the wrapper → `holocron run <task>` sees
|
|
6774
|
+
* `turbo.json` still defines the task → `turbo run <task>` again, forever.
|
|
6775
|
+
* Every other key (non-turbo tasks, `holocron`, `prepare`, …) has no
|
|
6776
|
+
* legitimate direct-command alternative and stays fully generator-owned.
|
|
6412
6777
|
*/
|
|
6413
|
-
async function mergePackageJsonScripts(repoRoot, desired) {
|
|
6778
|
+
async function mergePackageJsonScripts(repoRoot, desired, turboEligible) {
|
|
6414
6779
|
const pkgPath = join(repoRoot, "package.json");
|
|
6415
6780
|
let content;
|
|
6416
6781
|
try {
|
|
@@ -6421,15 +6786,31 @@ async function mergePackageJsonScripts(repoRoot, desired) {
|
|
|
6421
6786
|
const pkg = JSON.parse(content);
|
|
6422
6787
|
const scripts = pkg.scripts ?? {};
|
|
6423
6788
|
const changed = [];
|
|
6424
|
-
for (const [name, command] of Object.entries(desired))
|
|
6425
|
-
scripts[name]
|
|
6426
|
-
|
|
6789
|
+
for (const [name, command] of Object.entries(desired)) {
|
|
6790
|
+
const existing = scripts[name];
|
|
6791
|
+
if (turboEligible.has(name) && existing !== void 0 && !isGeneratedRunWrapper(existing, name)) continue;
|
|
6792
|
+
if (existing !== command) {
|
|
6793
|
+
scripts[name] = command;
|
|
6794
|
+
changed.push(name);
|
|
6795
|
+
}
|
|
6427
6796
|
}
|
|
6428
6797
|
if (changed.length === 0) return [];
|
|
6429
6798
|
pkg.scripts = scripts;
|
|
6430
6799
|
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
6431
6800
|
return changed.sort();
|
|
6432
6801
|
}
|
|
6802
|
+
/**
|
|
6803
|
+
* Matches the exact shape `packageScripts()` generates for `taskName` —
|
|
6804
|
+
* `<bin> run <taskName> --`, `<bin>` normally `holocron` but substitutable
|
|
6805
|
+
* (`holocronScript`, itself allowed to be multi-word, e.g. `"node
|
|
6806
|
+
packages/cli/dist/cli.mjs"` — see its own test). Checking the *suffix*
|
|
6807
|
+
* rather than the whole string means an old wrapper survives a
|
|
6808
|
+
* `holocronScript` rename intact — it's still recognized as a wrapper (safe
|
|
6809
|
+
* to update to the new one), not mistaken for a real command.
|
|
6810
|
+
*/
|
|
6811
|
+
function isGeneratedRunWrapper(command, taskName) {
|
|
6812
|
+
return command.endsWith(` run ${taskName} --`);
|
|
6813
|
+
}
|
|
6433
6814
|
const README_DESC_START = "<!-- holocron:description -->";
|
|
6434
6815
|
const README_DESC_END = "<!-- /holocron:description -->";
|
|
6435
6816
|
async function updateReadmeDescription(repoRoot, description) {
|
|
@@ -7296,6 +7677,63 @@ async function runUpgradeNode(input) {
|
|
|
7296
7677
|
};
|
|
7297
7678
|
}
|
|
7298
7679
|
//#endregion
|
|
7680
|
+
//#region src/commit-lint/lint-message.ts
|
|
7681
|
+
/**
|
|
7682
|
+
* Lints one commit message string against an already-loaded commitlint
|
|
7683
|
+
* config. The genuinely environment-independent half of commit-message
|
|
7684
|
+
* linting (holocron#789) — *obtaining* a `QualifiedConfig` differs by
|
|
7685
|
+
* caller (Sentinel isolates itself from any ambient config since it runs
|
|
7686
|
+
* standalone, away from a real repo checkout; `lintCommitMsgFile` below
|
|
7687
|
+
* just lets cosmiconfig discover the invoking repo's own
|
|
7688
|
+
* `commitlint.config.*`, matching `commitlint --edit`'s own contract) —
|
|
7689
|
+
* but linting one message against a loaded config is identical either way.
|
|
7690
|
+
*
|
|
7691
|
+
* Real `@commitlint/lint` — the same programmatic API `@commitlint/cli`
|
|
7692
|
+
* itself uses internally — never a reimplementation of commitlint's rules.
|
|
7693
|
+
*/
|
|
7694
|
+
/** `parserPreset.parserOpts`, if the loaded config sets one — same lookup `@commitlint/cli`'s own `selectParserOpts()` does. */
|
|
7695
|
+
function selectParserOpts(parserPreset) {
|
|
7696
|
+
return parserPreset?.parserOpts;
|
|
7697
|
+
}
|
|
7698
|
+
async function lintCommitMessage(message, loaded) {
|
|
7699
|
+
const opts = {
|
|
7700
|
+
/* istanbul ignore next -- see comment above */
|
|
7701
|
+
parserOpts: selectParserOpts(loaded.parserPreset) ?? {},
|
|
7702
|
+
plugins: loaded.plugins,
|
|
7703
|
+
/* istanbul ignore next -- see comment above */
|
|
7704
|
+
ignores: loaded.ignores ?? [],
|
|
7705
|
+
defaultIgnores: loaded.defaultIgnores !== false
|
|
7706
|
+
};
|
|
7707
|
+
const outcome = await lint(message, loaded.rules, opts);
|
|
7708
|
+
if (outcome.valid) return [];
|
|
7709
|
+
return outcome.errors.map((error) => ({
|
|
7710
|
+
rule: error.name,
|
|
7711
|
+
message: error.message
|
|
7712
|
+
}));
|
|
7713
|
+
}
|
|
7714
|
+
//#endregion
|
|
7715
|
+
//#region src/commit-lint/lint-commit-msg-file.ts
|
|
7716
|
+
/**
|
|
7717
|
+
* `holocron lint commit-msg <file>` — a real programmatic replacement for
|
|
7718
|
+
* shelling out to `commitlint --edit "$1"` from `.husky/commit-msg`
|
|
7719
|
+
* (holocron#789). Matches that CLI's own contract exactly: `file` is a
|
|
7720
|
+
* path to the commit message being written (the arg git's `commit-msg`
|
|
7721
|
+
* hook passes as `$1`), no `--config` override needed — `@commitlint/load`
|
|
7722
|
+
* runs from `cwd` with no seed, so cosmiconfig's own normal upward search
|
|
7723
|
+
* discovers the invoking repo's real `commitlint.config.*` file, exactly
|
|
7724
|
+
* as `commitlint --edit` (no `--config` flag) already does today. Unlike
|
|
7725
|
+
* Sentinel's `loadIsolatedConfig()`, no isolation is needed here — this
|
|
7726
|
+
* runs inside a real repo checkout with a real `node_modules`, not
|
|
7727
|
+
* standalone on a server.
|
|
7728
|
+
*/
|
|
7729
|
+
async function lintCommitMsgFile(filePath, cwd = process.cwd()) {
|
|
7730
|
+
const violations = await lintCommitMessage(await readFile(filePath, "utf8"), await load({}, { cwd }));
|
|
7731
|
+
return {
|
|
7732
|
+
valid: violations.length === 0,
|
|
7733
|
+
violations
|
|
7734
|
+
};
|
|
7735
|
+
}
|
|
7736
|
+
//#endregion
|
|
7299
7737
|
//#region src/config/load-config.ts
|
|
7300
7738
|
/**
|
|
7301
7739
|
* `holocron.config.{ts,js,mjs,cjs,json}` loader.
|
|
@@ -7406,7 +7844,9 @@ const numeric = (value) => /^\d+$/.test(value) ? true : "enter a number";
|
|
|
7406
7844
|
* The full command surface, hand-maintained in parallel with `cli.ts`'s
|
|
7407
7845
|
* yargs registrations (this module does not introspect yargs at runtime).
|
|
7408
7846
|
* `run <task>` is deliberately excluded — it's CI/scripting-oriented, and an
|
|
7409
|
-
* interactive prompt in front of it would work against that.
|
|
7847
|
+
* interactive prompt in front of it would work against that. `lint
|
|
7848
|
+
* commit-msg <file>` is excluded for the same reason: it's invoked
|
|
7849
|
+
* exclusively from `.husky/commit-msg "$1"`, never typed by a human.
|
|
7410
7850
|
*/
|
|
7411
7851
|
const COMMAND_REGISTRY = [
|
|
7412
7852
|
{
|
|
@@ -7717,7 +8157,7 @@ async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessag
|
|
|
7717
8157
|
//#endregion
|
|
7718
8158
|
//#region src/update-notifier.ts
|
|
7719
8159
|
const PACKAGE_NAME = "@theholocron/cli";
|
|
7720
|
-
const CACHE_TTL_MS =
|
|
8160
|
+
const CACHE_TTL_MS = 864e5;
|
|
7721
8161
|
const FETCH_TIMEOUT_MS = 3e3;
|
|
7722
8162
|
function getCacheDir() {
|
|
7723
8163
|
return env.get("HOLOCRON_CACHE_DIR") ?? join(homedir(), ".cache", "holocron");
|
|
@@ -8083,9 +8523,17 @@ try {
|
|
|
8083
8523
|
type: "array",
|
|
8084
8524
|
default: ["production", "preview"],
|
|
8085
8525
|
describe: "Deployment targets to sync to. Defaults to production + preview."
|
|
8526
|
+
}).option("github-secret", {
|
|
8527
|
+
type: "array",
|
|
8528
|
+
describe: "Vault key(s) to push to GH Actions secrets (repeatable). Omit to push every key."
|
|
8529
|
+
}).option("github-secret-scope", {
|
|
8530
|
+
type: "string",
|
|
8531
|
+
default: "repo",
|
|
8532
|
+
describe: "Scope applied to every --github-secret key: \"repo\" (default), \"env=<name>\", or \"org=<name>\"."
|
|
8086
8533
|
}), async (argv) => {
|
|
8087
8534
|
const tokens = tokenContext(argv.token);
|
|
8088
8535
|
if (!tokens) return;
|
|
8536
|
+
const githubSecretScope = parseScope(argv.githubSecretScope);
|
|
8089
8537
|
const [environmentId] = await promptForPositionals(getEntry("secrets sync"), argv);
|
|
8090
8538
|
const loaded = await loadConfig(argv.cwd);
|
|
8091
8539
|
applyResolvedConfig(argv, loaded.resolved);
|
|
@@ -8099,11 +8547,13 @@ try {
|
|
|
8099
8547
|
},
|
|
8100
8548
|
environmentId,
|
|
8101
8549
|
...argv.projectId ? { projectId: argv.projectId } : {},
|
|
8550
|
+
...argv.githubSecret?.length ? { githubSecretKeys: argv.githubSecret } : {},
|
|
8551
|
+
githubSecretScope,
|
|
8102
8552
|
targets: argv.target
|
|
8103
8553
|
})).summary.fail > 0) process.exitCode = 1;
|
|
8104
8554
|
}).command("deploy [branch]", "Trigger a deployment via the configured `deployment` capability", (y) => y.positional("branch", {
|
|
8105
8555
|
type: "string",
|
|
8106
|
-
describe: "Git branch to deploy"
|
|
8556
|
+
describe: "Git branch to deploy (omit when using --files)"
|
|
8107
8557
|
}).option("project-id", {
|
|
8108
8558
|
type: "string",
|
|
8109
8559
|
demandOption: true,
|
|
@@ -8112,20 +8562,34 @@ try {
|
|
|
8112
8562
|
type: "string",
|
|
8113
8563
|
choices: ["production", "staging"],
|
|
8114
8564
|
describe: "Named environment to deploy into. Omit for a branch preview."
|
|
8115
|
-
}),
|
|
8565
|
+
}).option("files", {
|
|
8566
|
+
type: "string",
|
|
8567
|
+
describe: "Deploy from a local directory of source files instead of a Git branch — calls deployFunction(), no linked repo required. Conflicts with `branch`."
|
|
8568
|
+
}).conflicts("files", "branch"), async (argv) => {
|
|
8116
8569
|
const tokens = tokenContext(argv.token);
|
|
8117
8570
|
if (!tokens) return;
|
|
8118
|
-
const [branch] = await promptForPositionals(getEntry("deploy"), argv);
|
|
8119
8571
|
const loaded = await loadConfig(argv.cwd);
|
|
8120
8572
|
applyResolvedConfig(argv, loaded.resolved);
|
|
8573
|
+
const context = {
|
|
8574
|
+
repoRoot: argv.cwd,
|
|
8575
|
+
dryRun: argv.dryRun,
|
|
8576
|
+
...tokens,
|
|
8577
|
+
org: resolveOrg(argv, loaded.resolved)
|
|
8578
|
+
};
|
|
8579
|
+
if (argv.files) {
|
|
8580
|
+
if ((await runDeployFromFiles({
|
|
8581
|
+
loaded,
|
|
8582
|
+
context,
|
|
8583
|
+
projectId: argv.projectId,
|
|
8584
|
+
dir: argv.files,
|
|
8585
|
+
...argv.target ? { target: argv.target } : {}
|
|
8586
|
+
})).status === "fail") process.exitCode = 1;
|
|
8587
|
+
return;
|
|
8588
|
+
}
|
|
8589
|
+
const [branch] = await promptForPositionals(getEntry("deploy"), argv);
|
|
8121
8590
|
if ((await runDeploy({
|
|
8122
8591
|
loaded,
|
|
8123
|
-
context
|
|
8124
|
-
repoRoot: argv.cwd,
|
|
8125
|
-
dryRun: argv.dryRun,
|
|
8126
|
-
...tokens,
|
|
8127
|
-
org: resolveOrg(argv, loaded.resolved)
|
|
8128
|
-
},
|
|
8592
|
+
context,
|
|
8129
8593
|
projectId: argv.projectId,
|
|
8130
8594
|
branch,
|
|
8131
8595
|
...argv.target ? { target: argv.target } : {}
|
|
@@ -8301,7 +8765,17 @@ try {
|
|
|
8301
8765
|
scope: argv.all ? "all" : "required",
|
|
8302
8766
|
...argv.filter ? { filter: argv.filter } : {}
|
|
8303
8767
|
}).status === "fail") process.exitCode = 1;
|
|
8304
|
-
}).command("
|
|
8768
|
+
}).command("lint", "Lint content locally against the org's shared rules", (y) => y.command("commit-msg <file>", "Lint a commit message file — matches `commitlint --edit`'s own contract, for use from .husky/commit-msg", (yy) => yy.positional("file", {
|
|
8769
|
+
type: "string",
|
|
8770
|
+
demandOption: true,
|
|
8771
|
+
describe: "Path to the commit message file (git's commit-msg hook passes this as $1)."
|
|
8772
|
+
}), async (argv) => {
|
|
8773
|
+
const result = await lintCommitMsgFile(argv.file, argv.cwd);
|
|
8774
|
+
if (!result.valid) {
|
|
8775
|
+
for (const violation of result.violations) console.error(`✖ ${violation.rule}: ${violation.message}`);
|
|
8776
|
+
process.exitCode = 1;
|
|
8777
|
+
}
|
|
8778
|
+
}), () => {}).command("sync-github", "Sync workflow templates and composite actions to theholocron/.github", (y) => y.option("repo", {
|
|
8305
8779
|
type: "string",
|
|
8306
8780
|
default: "theholocron/.github",
|
|
8307
8781
|
describe: "Target org/repo (default: theholocron/.github)"
|