@theholocron/cli 5.0.0-alpha.10 → 5.0.0-alpha.101
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 +110 -2
- package/dist/cli.mjs +683 -142
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +187 -2
- package/dist/index.mjs +235 -3
- package/dist/plugin/capabilities.d.mts +91 -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";
|
|
@@ -295,7 +297,8 @@ function resolveConfig(raw) {
|
|
|
295
297
|
docs,
|
|
296
298
|
env: raw.env,
|
|
297
299
|
log: raw.log,
|
|
298
|
-
telemetry: raw.telemetry
|
|
300
|
+
telemetry: raw.telemetry,
|
|
301
|
+
eslint: raw.eslint
|
|
299
302
|
};
|
|
300
303
|
}
|
|
301
304
|
//#endregion
|
|
@@ -532,7 +535,8 @@ var PluginLoader = class {
|
|
|
532
535
|
/** Default importer — resolves from cwd first so the global CLI finds project plugins. */
|
|
533
536
|
const defaultImporter$1 = async (pkg) => {
|
|
534
537
|
try {
|
|
535
|
-
|
|
538
|
+
const resolved = createRequire(process.cwd() + "/").resolve(pkg);
|
|
539
|
+
return import(pathToFileURL(resolved).href);
|
|
536
540
|
} catch {
|
|
537
541
|
return import(pkg);
|
|
538
542
|
}
|
|
@@ -664,7 +668,8 @@ const style = {
|
|
|
664
668
|
*/
|
|
665
669
|
const defaultImporter = async (pkg) => {
|
|
666
670
|
try {
|
|
667
|
-
|
|
671
|
+
const resolved = createRequire(process.cwd() + "/").resolve(pkg);
|
|
672
|
+
return await import(pathToFileURL(resolved).href);
|
|
668
673
|
} catch {
|
|
669
674
|
return await import(pkg);
|
|
670
675
|
}
|
|
@@ -1106,8 +1111,10 @@ async function runClone(input) {
|
|
|
1106
1111
|
}).status };
|
|
1107
1112
|
});
|
|
1108
1113
|
print(style.header(`Holocron clone — org=${input.org} → ${targetDir}${dryRun ? " (dry-run)" : ""}`));
|
|
1109
|
-
if (!existsSync(targetDir))
|
|
1110
|
-
|
|
1114
|
+
if (!existsSync(targetDir)) {
|
|
1115
|
+
if (dryRun) print(style.dim(` would create ${targetDir}`));
|
|
1116
|
+
else mkdirSync(targetDir, { recursive: true });
|
|
1117
|
+
}
|
|
1111
1118
|
let repos;
|
|
1112
1119
|
try {
|
|
1113
1120
|
repos = await listOrgRepos(input.org, input.token, fetchFn);
|
|
@@ -1129,7 +1136,8 @@ async function runClone(input) {
|
|
|
1129
1136
|
let skipped = 0;
|
|
1130
1137
|
let failed = 0;
|
|
1131
1138
|
for (const repo of repos) {
|
|
1132
|
-
const
|
|
1139
|
+
const dirName = repo.name.startsWith(".") ? repo.name.slice(1) : repo.name;
|
|
1140
|
+
const dest = join(targetDir, dirName);
|
|
1133
1141
|
if (existsSync(dest)) {
|
|
1134
1142
|
print(style.dim(` skip ${repo.full_name}`));
|
|
1135
1143
|
skipped++;
|
|
@@ -1214,6 +1222,7 @@ const COMMAND_CONTEXTS = {
|
|
|
1214
1222
|
"skills install": "global",
|
|
1215
1223
|
"skills remove": "global",
|
|
1216
1224
|
"skills update": "global",
|
|
1225
|
+
"lint commit-msg": "global",
|
|
1217
1226
|
run: "repo-aware",
|
|
1218
1227
|
ci: "repo-aware",
|
|
1219
1228
|
"config show": "repo-aware",
|
|
@@ -1233,6 +1242,80 @@ function commandsInContext(context) {
|
|
|
1233
1242
|
}
|
|
1234
1243
|
//#endregion
|
|
1235
1244
|
//#region src/commands/deploy.ts
|
|
1245
|
+
/**
|
|
1246
|
+
* `holocron deploy` — trigger a deployment via the configured
|
|
1247
|
+
* `deployment` capability.
|
|
1248
|
+
*
|
|
1249
|
+
* Two modes, one command: `runDeploy` (the default — a Git branch,
|
|
1250
|
+
* `triggerDeployment()`) and `runDeployFromFiles` (`--files <dir>` —
|
|
1251
|
+
* a local directory of source files, `deployFunction()`, no linked
|
|
1252
|
+
* repo required — for a consumer with no Git history to deploy from,
|
|
1253
|
+
* e.g. Sentinel, shipped as an npm package). Separate functions, not
|
|
1254
|
+
* one branching `runDeploy`, because their inputs genuinely differ
|
|
1255
|
+
* (`branch` vs `dir`) and their result shapes differ too
|
|
1256
|
+
* (`DeploymentRecord` carries `status`/`branch`; `DeployFunctionResult`
|
|
1257
|
+
* is deliberately minimal, same as `DeployScriptResult` for Workers).
|
|
1258
|
+
*
|
|
1259
|
+
* Errors clearly when `deployment` isn't loaded (no provider declared
|
|
1260
|
+
* in `holocron.config.json`), or when the configured provider doesn't
|
|
1261
|
+
* implement `deployFunction` (it's optional on `Deployment` — not
|
|
1262
|
+
* every provider has a files-based deploy API).
|
|
1263
|
+
*
|
|
1264
|
+
* Dry-run skips the actual deploy call but still verifies the
|
|
1265
|
+
* deployment capability (and, for `--files`, `deployFunction`) is
|
|
1266
|
+
* present, so operators can sanity-check the wiring without spinning
|
|
1267
|
+
* up a build.
|
|
1268
|
+
*
|
|
1269
|
+
* **Both modes wait for the deployment to finish** (holocron#911): a
|
|
1270
|
+
* provider accepting the upload/trigger is not the deployment succeeding.
|
|
1271
|
+
* Vercel builds asynchronously — an `npm install` failure there leaves
|
|
1272
|
+
* the deployment in `ERROR` while production keeps serving the previous
|
|
1273
|
+
* one, and this command used to report `ok` regardless (found live:
|
|
1274
|
+
* Sentinel's holocron#910 deploy "succeeded" in CI, never went live). So
|
|
1275
|
+
* after triggering, {@link waitForDeployment} polls
|
|
1276
|
+
* `Deployment.getDeployment()` until a terminal state; only `ready` is
|
|
1277
|
+
* `ok`, while `error` / `cancelled` / a timeout are `fail` (non-zero exit
|
|
1278
|
+
* via `cli.ts`).
|
|
1279
|
+
*/
|
|
1280
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 6e5;
|
|
1281
|
+
const DEFAULT_WAIT_INTERVAL_MS = 5e3;
|
|
1282
|
+
const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
1283
|
+
"ready",
|
|
1284
|
+
"error",
|
|
1285
|
+
"cancelled"
|
|
1286
|
+
]);
|
|
1287
|
+
/**
|
|
1288
|
+
* Polls `getDeployment(id)` until the deployment reaches a terminal status
|
|
1289
|
+
* (`ready` / `error` / `cancelled`). Throws on timeout — the caller turns
|
|
1290
|
+
* that into a `fail` report, same as a provider throwing mid-trigger.
|
|
1291
|
+
*/
|
|
1292
|
+
async function waitForDeployment(deploy, deploymentId, options = {}) {
|
|
1293
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1294
|
+
const intervalMs = options.intervalMs ?? DEFAULT_WAIT_INTERVAL_MS;
|
|
1295
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1296
|
+
const now = options.now ?? Date.now;
|
|
1297
|
+
const deadline = now() + timeoutMs;
|
|
1298
|
+
for (;;) {
|
|
1299
|
+
const record = await deploy.getDeployment(deploymentId);
|
|
1300
|
+
if (TERMINAL_STATUSES.has(record.status)) return record;
|
|
1301
|
+
if (now() >= deadline) throw new Error(`deployment ${deploymentId} still ${record.status} after ${Math.round(timeoutMs / 1e3)}s — check the provider's dashboard`);
|
|
1302
|
+
await sleep(intervalMs);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
/** One terminal, non-ready record as a single human-readable failure line. */
|
|
1306
|
+
function terminalFailure(record) {
|
|
1307
|
+
return `deployment ${record.id} ended ${record.status}${record.errorMessage ? `: ${record.errorMessage}` : ""}`;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Provider `url` fields (Vercel's REST API, at least) come back as bare
|
|
1311
|
+
* hostnames — `"my-app.vercel.app"`, no scheme — which terminals don't
|
|
1312
|
+
* linkify and browsers won't navigate to unmodified. Printed output
|
|
1313
|
+
* should be a URL a human can click/paste; structured log fields keep
|
|
1314
|
+
* the raw provider value as-is.
|
|
1315
|
+
*/
|
|
1316
|
+
function withScheme(url) {
|
|
1317
|
+
return /^https?:\/\//.test(url) ? url : `https://${url}`;
|
|
1318
|
+
}
|
|
1236
1319
|
async function runDeploy(input) {
|
|
1237
1320
|
const print = input.print ?? ((line) => console.log(line));
|
|
1238
1321
|
const logger = input.logger ?? getLogger();
|
|
@@ -1259,17 +1342,37 @@ async function runDeploy(input) {
|
|
|
1259
1342
|
};
|
|
1260
1343
|
}
|
|
1261
1344
|
try {
|
|
1262
|
-
const
|
|
1345
|
+
const triggered = await withSpinner(`Deploying ${input.branch}${input.target ? ` → ${input.target}` : ""}…`, () => deploy.triggerDeployment({
|
|
1263
1346
|
projectId: input.projectId,
|
|
1264
1347
|
branch: input.branch,
|
|
1265
1348
|
...input.target ? { target: input.target } : {}
|
|
1266
1349
|
}));
|
|
1267
|
-
|
|
1350
|
+
logger.info({
|
|
1351
|
+
status: triggered.status,
|
|
1352
|
+
url: triggered.url,
|
|
1353
|
+
id: triggered.id
|
|
1354
|
+
}, "deploy: triggered");
|
|
1355
|
+
const record = TERMINAL_STATUSES.has(triggered.status) ? triggered : await withSpinner(`Waiting for ${withScheme(triggered.url)} to finish…`, () => waitForDeployment(deploy, triggered.id, input.wait));
|
|
1356
|
+
if (record.status !== "ready") {
|
|
1357
|
+
const message = terminalFailure(record);
|
|
1358
|
+
print(` ${style.fail(`${message} — ${withScheme(triggered.url)}`)}`);
|
|
1359
|
+
logger.warn({
|
|
1360
|
+
id: record.id,
|
|
1361
|
+
status: record.status,
|
|
1362
|
+
reason: record.errorMessage
|
|
1363
|
+
}, "deploy: failed");
|
|
1364
|
+
return {
|
|
1365
|
+
deployment: record,
|
|
1366
|
+
status: "fail",
|
|
1367
|
+
message
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
print(` ${style.success(`${record.status} — ${withScheme(triggered.url)}`)}`);
|
|
1268
1371
|
logger.info({
|
|
1269
1372
|
status: record.status,
|
|
1270
|
-
url:
|
|
1373
|
+
url: triggered.url,
|
|
1271
1374
|
id: record.id
|
|
1272
|
-
}, "deploy:
|
|
1375
|
+
}, "deploy: ready");
|
|
1273
1376
|
return {
|
|
1274
1377
|
deployment: record,
|
|
1275
1378
|
status: "ok"
|
|
@@ -1288,6 +1391,106 @@ async function runDeploy(input) {
|
|
|
1288
1391
|
};
|
|
1289
1392
|
}
|
|
1290
1393
|
}
|
|
1394
|
+
const SKIP_DIRS$2 = /* @__PURE__ */ new Set([
|
|
1395
|
+
".git",
|
|
1396
|
+
"node_modules",
|
|
1397
|
+
".turbo"
|
|
1398
|
+
]);
|
|
1399
|
+
/** Recursively lists absolute file paths under `dir`, skipping VCS/tooling noise. */
|
|
1400
|
+
function defaultWalkFiles$2(dir) {
|
|
1401
|
+
const results = [];
|
|
1402
|
+
for (const entry of readdirSync(dir)) {
|
|
1403
|
+
if (SKIP_DIRS$2.has(entry)) continue;
|
|
1404
|
+
const full = join(dir, entry);
|
|
1405
|
+
const stat = statSync(full);
|
|
1406
|
+
if (stat.isDirectory()) results.push(...defaultWalkFiles$2(full));
|
|
1407
|
+
else if (stat.isFile()) results.push(full);
|
|
1408
|
+
}
|
|
1409
|
+
return results;
|
|
1410
|
+
}
|
|
1411
|
+
/** `holocron deploy --files <dir>` — the non-git counterpart to `runDeploy`. */
|
|
1412
|
+
async function runDeployFromFiles(input) {
|
|
1413
|
+
const print = input.print ?? ((line) => console.log(line));
|
|
1414
|
+
const logger = input.logger ?? getLogger();
|
|
1415
|
+
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
1416
|
+
await loader.load();
|
|
1417
|
+
assertPluginsResolvable(loader, "deploy");
|
|
1418
|
+
const dryRun = input.context.dryRun ?? false;
|
|
1419
|
+
const walkFiles = input.walkFiles ?? defaultWalkFiles$2;
|
|
1420
|
+
const readFile = input.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
1421
|
+
const files = {};
|
|
1422
|
+
for (const abs of walkFiles(input.dir)) {
|
|
1423
|
+
const rel = relative(input.dir, abs).split(sep).join("/");
|
|
1424
|
+
files[rel] = readFile(abs);
|
|
1425
|
+
}
|
|
1426
|
+
const fileCount = Object.keys(files).length;
|
|
1427
|
+
logger.info({
|
|
1428
|
+
dir: input.dir,
|
|
1429
|
+
fileCount,
|
|
1430
|
+
target: input.target ?? "preview",
|
|
1431
|
+
projectId: input.projectId,
|
|
1432
|
+
dryRun: dryRun || void 0
|
|
1433
|
+
}, "deploy: start (files)");
|
|
1434
|
+
print(style.header(`Holocron deploy — files=${input.dir} (${fileCount} file${fileCount === 1 ? "" : "s"})${input.target ? `, target=${input.target}` : " (preview)"}${dryRun ? " (dry-run)" : ""}`));
|
|
1435
|
+
if (!loader.has("deployment")) throw new Error("deployment capability is not configured — add a `deployment` provider to holocron.config.json");
|
|
1436
|
+
const deploy = loader.get("deployment");
|
|
1437
|
+
if (!deploy.deployFunction) throw new Error(`deployment provider "${deploy.providerName}" does not support deploying from files — deployFunction() is not implemented`);
|
|
1438
|
+
if (dryRun) {
|
|
1439
|
+
const message = `would: ${deploy.providerName}.deployFunction(projectId=${input.projectId}, files=${fileCount}${input.target ? `, target=${input.target}` : ""})`;
|
|
1440
|
+
print(` ${style.dim(`… ${message}`)}`);
|
|
1441
|
+
return {
|
|
1442
|
+
deployment: null,
|
|
1443
|
+
status: "dry-run",
|
|
1444
|
+
message
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
try {
|
|
1448
|
+
const result = await withSpinner(`Deploying ${fileCount} file${fileCount === 1 ? "" : "s"} from ${input.dir}…`, () => deploy.deployFunction(input.projectId, {
|
|
1449
|
+
files,
|
|
1450
|
+
...input.target ? { target: input.target } : {}
|
|
1451
|
+
}));
|
|
1452
|
+
logger.info({
|
|
1453
|
+
url: result.url,
|
|
1454
|
+
id: result.deploymentId
|
|
1455
|
+
}, "deploy: triggered (files)");
|
|
1456
|
+
const record = await withSpinner(`Waiting for ${withScheme(result.url)} to finish…`, () => waitForDeployment(deploy, result.deploymentId, input.wait));
|
|
1457
|
+
if (record.status !== "ready") {
|
|
1458
|
+
const message = terminalFailure(record);
|
|
1459
|
+
print(` ${style.fail(`${message} — ${withScheme(result.url)}`)}`);
|
|
1460
|
+
logger.warn({
|
|
1461
|
+
id: record.id,
|
|
1462
|
+
status: record.status,
|
|
1463
|
+
reason: record.errorMessage
|
|
1464
|
+
}, "deploy: failed (files)");
|
|
1465
|
+
return {
|
|
1466
|
+
deployment: result,
|
|
1467
|
+
status: "fail",
|
|
1468
|
+
message
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
print(` ${style.success(withScheme(result.url))}`);
|
|
1472
|
+
logger.info({
|
|
1473
|
+
url: result.url,
|
|
1474
|
+
id: result.deploymentId
|
|
1475
|
+
}, "deploy: ready (files)");
|
|
1476
|
+
return {
|
|
1477
|
+
deployment: result,
|
|
1478
|
+
status: "ok"
|
|
1479
|
+
};
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1482
|
+
print(` ${style.fail(message)}`);
|
|
1483
|
+
logger.warn({
|
|
1484
|
+
dir: input.dir,
|
|
1485
|
+
reason: message
|
|
1486
|
+
}, "deploy: failed (files)");
|
|
1487
|
+
return {
|
|
1488
|
+
deployment: null,
|
|
1489
|
+
status: "fail",
|
|
1490
|
+
message
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1291
1494
|
//#endregion
|
|
1292
1495
|
//#region src/commands/doctor.ts
|
|
1293
1496
|
async function runDoctor(input) {
|
|
@@ -3696,10 +3899,13 @@ async function runSecretsSync(input) {
|
|
|
3696
3899
|
const rows = [];
|
|
3697
3900
|
if (loader.has("secrets")) {
|
|
3698
3901
|
const secrets = loader.get("secrets");
|
|
3699
|
-
|
|
3902
|
+
const scope = input.githubSecretScope ?? { kind: "repo" };
|
|
3903
|
+
const allowedKeys = input.githubSecretKeys ? new Set(input.githubSecretKeys) : void 0;
|
|
3904
|
+
print(style.step("secrets"));
|
|
3700
3905
|
for (const key of keys) {
|
|
3701
|
-
|
|
3702
|
-
|
|
3906
|
+
if (allowedKeys && !allowedKeys.has(key)) continue;
|
|
3907
|
+
rows.push(await runRow(`secrets:${secrets.providerName}`, scopeLabel(scope), key, dryRun, async () => {
|
|
3908
|
+
await secrets.setSecret(scope, key, envVars[key]);
|
|
3703
3909
|
}));
|
|
3704
3910
|
print(formatRow(rows[rows.length - 1]));
|
|
3705
3911
|
}
|
|
@@ -3791,11 +3997,114 @@ function formatRow(row) {
|
|
|
3791
3997
|
if (row.status === "dry-run") return ` ${style.dim(`… ${label}`)}`;
|
|
3792
3998
|
return ` ${style.dim(`· ${label}`)}`;
|
|
3793
3999
|
}
|
|
4000
|
+
function scopeLabel(scope) {
|
|
4001
|
+
if (scope.kind === "repo") return "scope=repo";
|
|
4002
|
+
if (scope.kind === "organization") return `scope=organization,org=${scope.name}`;
|
|
4003
|
+
return `scope=environment,env=${scope.name}`;
|
|
4004
|
+
}
|
|
3794
4005
|
function vaultProviderName(loader) {
|
|
3795
4006
|
if (!loader.has("vault")) return "<missing>";
|
|
3796
4007
|
return loader.get("vault").providerName;
|
|
3797
4008
|
}
|
|
3798
4009
|
//#endregion
|
|
4010
|
+
//#region src/commands/setup/derived-properties.ts
|
|
4011
|
+
/**
|
|
4012
|
+
* Repo archetype, in priority order:
|
|
4013
|
+
* 1. `*-template` repo name → `template` (independent of package shape).
|
|
4014
|
+
* 2. Monorepo whose primary published artifact is a CLI (some workspace
|
|
4015
|
+
* package has `bin`) → `platform` — matches `holocron` itself: the CLI is
|
|
4016
|
+
* the deliverable, the plugin packages are supporting infrastructure for
|
|
4017
|
+
* it, not separate products.
|
|
4018
|
+
* 3. Monorepo with no CLI but at least one non-private workspace package →
|
|
4019
|
+
* `library` — matches `clients`/`configs`/`utils`/`themes`/
|
|
4020
|
+
* `observability`: a family of published packages, none of them a CLI.
|
|
4021
|
+
* Checked *before* falling into the root-package branch below, because
|
|
4022
|
+
* these repos' own root `package.json` is private and often carries
|
|
4023
|
+
* `@theholocron/astro-config` for a docs site built from the monorepo
|
|
4024
|
+
* root — without this check they'd wrongly resolve to `docs` off that
|
|
4025
|
+
* root-level signal alone, when the docs site is a secondary concern of
|
|
4026
|
+
* a library collection, not what the repo fundamentally is.
|
|
4027
|
+
* 4. Single-package repo with `package.json#bin` → `cli`.
|
|
4028
|
+
* 5. Docs-only site (astro/starlight present, root package private, no
|
|
4029
|
+
* publishable exports) → `docs`.
|
|
4030
|
+
* 6. Private, non-publishable, non-docs → `app`.
|
|
4031
|
+
* 7. Otherwise, a single publishable package → `library`.
|
|
4032
|
+
*/
|
|
4033
|
+
function deriveProfile(input) {
|
|
4034
|
+
if (/-template$/.test(input.repoName)) return "template";
|
|
4035
|
+
if (input.isMonorepo) {
|
|
4036
|
+
if (input.workspacePackageJsons.some((pkg) => Boolean(pkg.bin))) return "platform";
|
|
4037
|
+
if (input.workspacePackageJsons.some((pkg) => pkg.private !== true)) return "library";
|
|
4038
|
+
}
|
|
4039
|
+
const pkg = input.rootPackageJson;
|
|
4040
|
+
if (pkg?.bin) return "cli";
|
|
4041
|
+
const deps = {
|
|
4042
|
+
...pkg?.dependencies,
|
|
4043
|
+
...pkg?.devDependencies
|
|
4044
|
+
};
|
|
4045
|
+
const isDocsSite = Boolean(deps["@theholocron/astro-config"] || deps["astro"]);
|
|
4046
|
+
if (pkg?.private && isDocsSite) return "docs";
|
|
4047
|
+
if (pkg?.private) return "app";
|
|
4048
|
+
return "library";
|
|
4049
|
+
}
|
|
4050
|
+
const KNOWN_STACK_DEPS = [
|
|
4051
|
+
"next",
|
|
4052
|
+
"vite",
|
|
4053
|
+
"astro",
|
|
4054
|
+
"tsdown",
|
|
4055
|
+
"rollup",
|
|
4056
|
+
"webpack",
|
|
4057
|
+
"storybook",
|
|
4058
|
+
"vitest",
|
|
4059
|
+
"playwright",
|
|
4060
|
+
"turbo"
|
|
4061
|
+
];
|
|
4062
|
+
/**
|
|
4063
|
+
* Detected build/framework tooling — the literal "does this repo need
|
|
4064
|
+
* next/vite/tsdown" signal `holocron.config.ts` has no field for today
|
|
4065
|
+
* (framework choice isn't a config concept, unlike `providers`/`tasks`).
|
|
4066
|
+
* Curated table, not exhaustive by design — add to `KNOWN_STACK_DEPS` as new
|
|
4067
|
+
* frameworks show up rather than trying to detect anything on npm.
|
|
4068
|
+
*/
|
|
4069
|
+
function deriveStack(pkg) {
|
|
4070
|
+
const deps = {
|
|
4071
|
+
...pkg?.dependencies,
|
|
4072
|
+
...pkg?.devDependencies
|
|
4073
|
+
};
|
|
4074
|
+
return KNOWN_STACK_DEPS.filter((name) => name in deps);
|
|
4075
|
+
}
|
|
4076
|
+
/** The provider capability keys actually wired in `providers: {}` — zero heuristics. */
|
|
4077
|
+
function deriveCapabilities(providers) {
|
|
4078
|
+
return Object.keys(providers ?? {}).sort();
|
|
4079
|
+
}
|
|
4080
|
+
/**
|
|
4081
|
+
* `source` + `ci` are the minimal baseline every repo `holocron setup`
|
|
4082
|
+
* touches wires — every `holocron.config.ts` in this org already satisfies
|
|
4083
|
+
* this, so `non-compliant` here means drift (a provider manually removed
|
|
4084
|
+
* after setup), not an unmet aspirational policy. Deliberately not a richer
|
|
4085
|
+
* per-profile policy yet (e.g. "a `library` must have `deployment`") — that's
|
|
4086
|
+
* a Phase B (GitHub App) refinement once it can post *why* on a check run.
|
|
4087
|
+
*/
|
|
4088
|
+
const REQUIRED_BASELINE = ["source", "ci"];
|
|
4089
|
+
function deriveCompliance(capabilities) {
|
|
4090
|
+
return REQUIRED_BASELINE.every((key) => capabilities.includes(key)) ? "compliant" : "non-compliant";
|
|
4091
|
+
}
|
|
4092
|
+
/**
|
|
4093
|
+
* Reads each workspace package's actual `package.json` content (not just
|
|
4094
|
+
* `readWorkspacePackages()`'s `{ slug, name, dir }` — `deriveProfile()`'s
|
|
4095
|
+
* `platform` detection needs `bin`, which that helper doesn't carry).
|
|
4096
|
+
* Missing/invalid files are skipped, matching `readWorkspacePackages()`'s
|
|
4097
|
+
* own soft-fail behavior.
|
|
4098
|
+
*/
|
|
4099
|
+
async function readWorkspacePackageJsons(repoRoot, workspacePackages) {
|
|
4100
|
+
const results = [];
|
|
4101
|
+
for (const { slug, dir = "packages" } of workspacePackages) try {
|
|
4102
|
+
const raw = await readFile(join(repoRoot, dir, slug, "package.json"), "utf8");
|
|
4103
|
+
results.push(JSON.parse(raw));
|
|
4104
|
+
} catch {}
|
|
4105
|
+
return results;
|
|
4106
|
+
}
|
|
4107
|
+
//#endregion
|
|
3799
4108
|
//#region src/commands/setup/labels.ts
|
|
3800
4109
|
const CANONICAL_LABELS = [
|
|
3801
4110
|
{
|
|
@@ -3935,6 +4244,9 @@ var alexignore_default = ".github/*\nCHANGELOG.md\nLICENSE\n";
|
|
|
3935
4244
|
//#region src/templates/configs/alexjs/alexrc.json
|
|
3936
4245
|
var alexrc_default = { allow: [
|
|
3937
4246
|
"dead",
|
|
4247
|
+
"execute",
|
|
4248
|
+
"execution",
|
|
4249
|
+
"executes",
|
|
3938
4250
|
"failure",
|
|
3939
4251
|
"failures",
|
|
3940
4252
|
"hook",
|
|
@@ -3953,12 +4265,21 @@ function createIgnoreConfig$1() {
|
|
|
3953
4265
|
}
|
|
3954
4266
|
//#endregion
|
|
3955
4267
|
//#region src/templates/configs/commit-msg/commit-msg
|
|
3956
|
-
var commit_msg_default = "
|
|
4268
|
+
var commit_msg_default = "__HOLOCRON_SCRIPT__ lint commit-msg \"$1\"\n";
|
|
3957
4269
|
//#endregion
|
|
3958
4270
|
//#region src/templates/configs/commit-msg/create-config.ts
|
|
3959
4271
|
const { workflowHeader: workflowHeader$8 } = createHeader({ source: "packages/cli/src/templates/configs/commit-msg/create-config.ts" });
|
|
3960
|
-
|
|
3961
|
-
|
|
4272
|
+
/**
|
|
4273
|
+
* `.husky/commit-msg` — lints the commit message via `holocron lint
|
|
4274
|
+
* commit-msg` (holocron#789), the real programmatic replacement for
|
|
4275
|
+
* shelling out to `commitlint --edit`. Installed by `holocron setup` when
|
|
4276
|
+
* hooks are enabled. Same `__HOLOCRON_SCRIPT__` substitution as `pre-push`.
|
|
4277
|
+
*
|
|
4278
|
+
* @param holocronScript - the command that invokes the CLI. Defaults to
|
|
4279
|
+
* `pnpm exec holocron`; the source repo passes `node packages/cli/dist/cli.mjs`.
|
|
4280
|
+
*/
|
|
4281
|
+
function createConfig$7(holocronScript = "pnpm exec holocron") {
|
|
4282
|
+
return `${workflowHeader$8("shebang")}${commit_msg_default.replace("__HOLOCRON_SCRIPT__", holocronScript)}`;
|
|
3962
4283
|
}
|
|
3963
4284
|
//#endregion
|
|
3964
4285
|
//#region src/templates/configs/devmoji/create-config.ts
|
|
@@ -3974,7 +4295,7 @@ function createConfig$6() {
|
|
|
3974
4295
|
}
|
|
3975
4296
|
//#endregion
|
|
3976
4297
|
//#region src/templates/configs/editorconfig/editorconfig
|
|
3977
|
-
var editorconfig_default = "root = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = tab\nindent_size = 4\n\n[.gitattributes]\nindent_style = space\nindent_size = 2\n\n[*.{json,yml,yaml}]\nindent_style = space\nindent_size = 2\n\n[*.{md,mdx}]\ntrim_trailing_whitespace = false\n\n[.*{rc,ignore}]\nindent_style = space\nindent_size = 2\n";
|
|
4298
|
+
var editorconfig_default = "root = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = tab\nindent_size = 4\n\n[.gitattributes]\nindent_style = space\nindent_size = 2\n\n[*.{json,yml,yaml}]\nindent_style = space\nindent_size = 2\n\n[*.{md,mdx}]\ntrim_trailing_whitespace = false\nindent_style = unset\n\n[.*{rc,ignore}]\nindent_style = space\nindent_size = 2\n";
|
|
3978
4299
|
//#endregion
|
|
3979
4300
|
//#region src/templates/configs/editorconfig/create-config.ts
|
|
3980
4301
|
const { workflowHeader: workflowHeader$6 } = createHeader({ source: "packages/cli/src/templates/configs/editorconfig/create-config.ts" });
|
|
@@ -4954,7 +5275,10 @@ async function runSetup(input) {
|
|
|
4954
5275
|
print(formatStep(steps[steps.length - 1]));
|
|
4955
5276
|
if (effectivePreset && effectivePreset !== "none") {
|
|
4956
5277
|
steps.push(await runStep("source", "updateRepoSettings", dryRun, async () => {
|
|
4957
|
-
await source.updateRepoSettings(
|
|
5278
|
+
await source.updateRepoSettings({
|
|
5279
|
+
...BALANCED_REPO_SETTINGS,
|
|
5280
|
+
...repo?.defaultBranch ? { default_branch: repo.defaultBranch } : {}
|
|
5281
|
+
});
|
|
4958
5282
|
}));
|
|
4959
5283
|
print(formatStep(steps[steps.length - 1]));
|
|
4960
5284
|
const tasksConfig = { tasks: config.tasks };
|
|
@@ -5061,7 +5385,7 @@ async function runSetup(input) {
|
|
|
5061
5385
|
}));
|
|
5062
5386
|
print(formatStep(steps[steps.length - 1]));
|
|
5063
5387
|
steps.push(await runStep("source", "write .husky/commit-msg", dryRun, async () => {
|
|
5064
|
-
await source.writeRepoFile(".husky/commit-msg", createConfig$7());
|
|
5388
|
+
await source.writeRepoFile(".husky/commit-msg", createConfig$7(config.holocronScript));
|
|
5065
5389
|
}));
|
|
5066
5390
|
print(formatStep(steps[steps.length - 1]));
|
|
5067
5391
|
if (hooksEnabled) {
|
|
@@ -5118,20 +5442,38 @@ async function runSetup(input) {
|
|
|
5118
5442
|
print(formatStep(steps[steps.length - 1]));
|
|
5119
5443
|
}
|
|
5120
5444
|
{
|
|
5121
|
-
const
|
|
5445
|
+
const astromechTurbo = createAstromech({
|
|
5122
5446
|
cwd: input.context.repoRoot,
|
|
5123
5447
|
config: { tasks: config.tasks },
|
|
5124
5448
|
logger
|
|
5125
|
-
})
|
|
5449
|
+
});
|
|
5450
|
+
const content = astromechTurbo.turboConfig();
|
|
5126
5451
|
if (content === null) steps.push({
|
|
5127
5452
|
capability: "source",
|
|
5128
5453
|
step: "write turbo.json",
|
|
5129
5454
|
status: "skip",
|
|
5130
5455
|
message: "no fan-out-eligible tasks configured"
|
|
5131
5456
|
});
|
|
5132
|
-
else
|
|
5133
|
-
await
|
|
5134
|
-
|
|
5457
|
+
else {
|
|
5458
|
+
steps.push(await runStep("source", "write turbo.json", dryRun, async () => {
|
|
5459
|
+
await source.writeRepoFile("turbo.json", content);
|
|
5460
|
+
}));
|
|
5461
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5462
|
+
const rootPkgRaw = await readFile(join(input.context.repoRoot, "package.json"), "utf8").catch(() => null);
|
|
5463
|
+
const rootPkg = rootPkgRaw ? JSON.parse(rootPkgRaw) : null;
|
|
5464
|
+
const workspaceYaml = await readFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), "utf8").catch(() => "");
|
|
5465
|
+
const rootScripts = rootPkg?.scripts ?? {};
|
|
5466
|
+
const workspaceFix = astromechTurbo.ensureRootWorkspaceMember(workspaceYaml, rootScripts);
|
|
5467
|
+
if (workspaceFix.changed) steps.push(await runStep("source", "fix pnpm-workspace.yaml (root workspace member, #692)", dryRun, async () => {
|
|
5468
|
+
await source.writeRepoFile("pnpm-workspace.yaml", workspaceFix.content);
|
|
5469
|
+
}));
|
|
5470
|
+
if (rootPkgRaw) {
|
|
5471
|
+
const depFix = astromechTurbo.ensureTurboDependency(rootPkgRaw, workspaceFix.content);
|
|
5472
|
+
if (depFix.changed) steps.push(await runStep("source", "add turbo devDependency", dryRun, async () => {
|
|
5473
|
+
await source.writeRepoFile("package.json", depFix.content);
|
|
5474
|
+
}));
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5135
5477
|
print(formatStep(steps[steps.length - 1]));
|
|
5136
5478
|
}
|
|
5137
5479
|
if (source.syncLabels) {
|
|
@@ -5141,7 +5483,7 @@ async function runSetup(input) {
|
|
|
5141
5483
|
print(formatStep(steps[steps.length - 1]));
|
|
5142
5484
|
}
|
|
5143
5485
|
const properties = {};
|
|
5144
|
-
if (effectivePreset && effectivePreset !== "none") properties["
|
|
5486
|
+
if (effectivePreset && effectivePreset !== "none") properties["holocron_branch_protection_level"] = effectivePreset;
|
|
5145
5487
|
const isMonorepo = await access(join(input.context.repoRoot, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
|
|
5146
5488
|
properties["monorepo"] = String(isMonorepo);
|
|
5147
5489
|
const manual = repo?.properties ?? {};
|
|
@@ -5149,6 +5491,21 @@ async function runSetup(input) {
|
|
|
5149
5491
|
if (manual.open_source !== void 0) properties["open_source"] = String(manual.open_source);
|
|
5150
5492
|
if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
|
|
5151
5493
|
if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
|
|
5494
|
+
{
|
|
5495
|
+
const rootPackageJson = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
5496
|
+
const workspacePackages = readWorkspacePackages(input.context.repoRoot);
|
|
5497
|
+
const workspacePackageJsons = await readWorkspacePackageJsons(input.context.repoRoot, workspacePackages);
|
|
5498
|
+
properties["holocron_profile"] = deriveProfile({
|
|
5499
|
+
rootPackageJson,
|
|
5500
|
+
repoName: basename(input.context.repoRoot),
|
|
5501
|
+
isMonorepo,
|
|
5502
|
+
workspacePackageJsons
|
|
5503
|
+
});
|
|
5504
|
+
properties["holocron_stack"] = deriveStack(rootPackageJson);
|
|
5505
|
+
const capabilities = deriveCapabilities(config.providers);
|
|
5506
|
+
properties["holocron_capabilities"] = capabilities;
|
|
5507
|
+
properties["holocron_compliance"] = deriveCompliance(capabilities);
|
|
5508
|
+
}
|
|
5152
5509
|
if (source.syncProperties) {
|
|
5153
5510
|
steps.push(await runStep("source", "sync properties", dryRun, () => source.syncProperties(properties)));
|
|
5154
5511
|
print(formatStep(steps[steps.length - 1]));
|
|
@@ -5167,34 +5524,36 @@ async function runSetup(input) {
|
|
|
5167
5524
|
print(formatStep(steps[steps.length - 1]));
|
|
5168
5525
|
}
|
|
5169
5526
|
const teams = repo?.teams ?? [];
|
|
5170
|
-
if (teams.length > 0)
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5527
|
+
if (teams.length > 0) {
|
|
5528
|
+
if (source.syncTeams) {
|
|
5529
|
+
steps.push(await runStep("source", "sync teams", dryRun, () => source.syncTeams(teams), { skipCodes: [422] }));
|
|
5530
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5531
|
+
const repoCoord = input.context.repo ?? repo?.name ?? "";
|
|
5532
|
+
const org = repoCoord.includes("/") ? repoCoord.split("/")[0] : "";
|
|
5533
|
+
const writeableTeams = teams.map((t) => typeof t === "string" ? {
|
|
5534
|
+
slug: t,
|
|
5535
|
+
permission: "push"
|
|
5536
|
+
} : t).filter((t) => [
|
|
5537
|
+
"push",
|
|
5538
|
+
"maintain",
|
|
5539
|
+
"admin"
|
|
5540
|
+
].includes(t.permission));
|
|
5541
|
+
if (org && writeableTeams.length > 0) {
|
|
5542
|
+
steps.push(await runStep("source", "write .github/CODEOWNERS", dryRun, async () => {
|
|
5543
|
+
const content = writeableTeams.map((t) => `* @${org}/${t.slug}`).join("\n") + "\n";
|
|
5544
|
+
await source.writeRepoFile(".github/CODEOWNERS", content);
|
|
5545
|
+
}));
|
|
5546
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5547
|
+
}
|
|
5548
|
+
} else {
|
|
5549
|
+
steps.push({
|
|
5550
|
+
capability: "source",
|
|
5551
|
+
step: "sync teams",
|
|
5552
|
+
status: "skip",
|
|
5553
|
+
message: "provider does not implement syncTeams"
|
|
5554
|
+
});
|
|
5188
5555
|
print(formatStep(steps[steps.length - 1]));
|
|
5189
5556
|
}
|
|
5190
|
-
} else {
|
|
5191
|
-
steps.push({
|
|
5192
|
-
capability: "source",
|
|
5193
|
-
step: "sync teams",
|
|
5194
|
-
status: "skip",
|
|
5195
|
-
message: "provider does not implement syncTeams"
|
|
5196
|
-
});
|
|
5197
|
-
print(formatStep(steps[steps.length - 1]));
|
|
5198
5557
|
}
|
|
5199
5558
|
}
|
|
5200
5559
|
if (loader.has("environments")) {
|
|
@@ -5250,11 +5609,9 @@ async function runSetup(input) {
|
|
|
5250
5609
|
const wikiProxy = wiki.proxyConfig?.();
|
|
5251
5610
|
if (wikiDns && loader.has("dns")) {
|
|
5252
5611
|
const dns = loader.get("dns");
|
|
5253
|
-
steps.push(await runStep("dns", `upsertRecord ${wikiDns.
|
|
5612
|
+
steps.push(await runStep("dns", `upsertRecord ${wikiDns.record.name}`, dryRun, async () => {
|
|
5254
5613
|
await dns.upsertRecord(wikiDns.zone, {
|
|
5255
|
-
|
|
5256
|
-
name: wikiDns.cname,
|
|
5257
|
-
content: wikiDns.target,
|
|
5614
|
+
...wikiDns.record,
|
|
5258
5615
|
ttl: 1,
|
|
5259
5616
|
...wikiProxy ? { proxied: true } : {}
|
|
5260
5617
|
});
|
|
@@ -5263,8 +5620,8 @@ async function runSetup(input) {
|
|
|
5263
5620
|
}
|
|
5264
5621
|
if (wikiProxy && wikiDns && loader.has("workers")) {
|
|
5265
5622
|
const workers = loader.get("workers");
|
|
5266
|
-
steps.push(await runStep("workers", `upsertProxy ${wikiDns.
|
|
5267
|
-
await workers.upsertProxy(wikiDns.
|
|
5623
|
+
steps.push(await runStep("workers", `upsertProxy ${wikiDns.record.name}`, dryRun, async () => {
|
|
5624
|
+
await workers.upsertProxy(wikiDns.record.name, wikiProxy);
|
|
5268
5625
|
}));
|
|
5269
5626
|
print(formatStep(steps[steps.length - 1]));
|
|
5270
5627
|
}
|
|
@@ -5277,11 +5634,31 @@ async function runSetup(input) {
|
|
|
5277
5634
|
org: config.org,
|
|
5278
5635
|
domain: config.domain
|
|
5279
5636
|
}) : null;
|
|
5637
|
+
const ensureCustomDomain = async (projectId, hostname) => {
|
|
5638
|
+
if (!deploy.ensureCustomDomain) return;
|
|
5639
|
+
const customDomainDns = { value: null };
|
|
5640
|
+
steps.push(await runStep("deployment", `ensureCustomDomain ${hostname}`, dryRun, async () => {
|
|
5641
|
+
customDomainDns.value = await deploy.ensureCustomDomain(projectId, hostname);
|
|
5642
|
+
}));
|
|
5643
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5644
|
+
if (customDomainDns.value && loader.has("dns")) {
|
|
5645
|
+
const dns = loader.get("dns");
|
|
5646
|
+
const dnsRecord = customDomainDns.value;
|
|
5647
|
+
steps.push(await runStep("dns", `upsertRecord ${dnsRecord.record.name}`, dryRun, async () => {
|
|
5648
|
+
await dns.upsertRecord(dnsRecord.zone, {
|
|
5649
|
+
...dnsRecord.record,
|
|
5650
|
+
ttl: 1
|
|
5651
|
+
});
|
|
5652
|
+
}));
|
|
5653
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5654
|
+
}
|
|
5655
|
+
};
|
|
5280
5656
|
if (!previewCfg) {
|
|
5281
5657
|
steps.push(await runStep("deployment", `ensureProject ${config.name}`, dryRun, async () => {
|
|
5282
5658
|
await deploy.ensureProject({ name: config.name });
|
|
5283
5659
|
}));
|
|
5284
5660
|
print(formatStep(steps[steps.length - 1]));
|
|
5661
|
+
if (deploy.domain) await ensureCustomDomain(config.name, deploy.domain);
|
|
5285
5662
|
}
|
|
5286
5663
|
if (previewCfg) {
|
|
5287
5664
|
steps.push(await runStep("deployment", `ensureProject ${previewCfg.project}`, dryRun, async () => {
|
|
@@ -5289,13 +5666,8 @@ async function runSetup(input) {
|
|
|
5289
5666
|
}));
|
|
5290
5667
|
print(formatStep(steps[steps.length - 1]));
|
|
5291
5668
|
}
|
|
5292
|
-
if (previewCfg?.domain
|
|
5293
|
-
|
|
5294
|
-
await deploy.ensureCustomDomain(previewCfg.project, previewCfg.domain);
|
|
5295
|
-
}));
|
|
5296
|
-
print(formatStep(steps[steps.length - 1]));
|
|
5297
|
-
}
|
|
5298
|
-
if (previewCfg?.domain && loader.has("dns")) {
|
|
5669
|
+
if (previewCfg?.domain) await ensureCustomDomain(previewCfg.project, previewCfg.domain);
|
|
5670
|
+
if (previewCfg?.domain && deploy.providerName === "cloudflare" && loader.has("dns")) {
|
|
5299
5671
|
const dns = loader.get("dns");
|
|
5300
5672
|
const wildcardDomain = `*.${previewCfg.domain}`;
|
|
5301
5673
|
steps.push(await runStep("dns", `upsertRecord ${previewCfg.domain}`, dryRun, async () => {
|
|
@@ -5743,11 +6115,13 @@ async function runSyncReadme(input) {
|
|
|
5743
6115
|
}
|
|
5744
6116
|
const description = loaded.resolved.description ?? "";
|
|
5745
6117
|
const homepage = loaded.resolved.homepage ?? pkg.homepage ?? "";
|
|
6118
|
+
const scripts = pkg.scripts ?? {};
|
|
6119
|
+
const packages = pkg.name ? REGISTRY_MAP[pkg.name]?.() : void 0;
|
|
5746
6120
|
const sections = generateReadme({
|
|
5747
6121
|
description,
|
|
5748
6122
|
homepage,
|
|
5749
|
-
scripts
|
|
5750
|
-
packages
|
|
6123
|
+
scripts,
|
|
6124
|
+
packages
|
|
5751
6125
|
});
|
|
5752
6126
|
const markerSections = {
|
|
5753
6127
|
...sections.packages !== void 0 && { packages: sections.packages },
|
|
@@ -5989,7 +6363,7 @@ async function runSyncWiki(input) {
|
|
|
5989
6363
|
capability: "local",
|
|
5990
6364
|
step: "sync wiki",
|
|
5991
6365
|
status: "fail",
|
|
5992
|
-
message: err instanceof Error ? err.message :
|
|
6366
|
+
message: err instanceof Error ? err.message : /* c8 ignore next */ String(err)
|
|
5993
6367
|
};
|
|
5994
6368
|
}
|
|
5995
6369
|
}
|
|
@@ -6051,40 +6425,59 @@ async function runSync(input) {
|
|
|
6051
6425
|
for (const stepName of SYNC_STEPS) {
|
|
6052
6426
|
if (requestedSteps !== void 0 && !requestedSteps.includes(stepName)) continue;
|
|
6053
6427
|
if (LOCAL_STEPS.has(stepName)) continue;
|
|
6054
|
-
if (stepName === "labels")
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6428
|
+
if (stepName === "labels") {
|
|
6429
|
+
if (source.syncLabels) {
|
|
6430
|
+
steps.push(await runSyncStep("source", "sync labels", dryRun, () => source.syncLabels(CANONICAL_LABELS, STALE_LABELS)));
|
|
6431
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6432
|
+
} else {
|
|
6433
|
+
steps.push({
|
|
6434
|
+
capability: "source",
|
|
6435
|
+
step: "sync labels",
|
|
6436
|
+
status: "skip",
|
|
6437
|
+
message: "provider does not implement syncLabels"
|
|
6438
|
+
});
|
|
6439
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6440
|
+
}
|
|
6065
6441
|
}
|
|
6066
|
-
if (stepName === "properties")
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6442
|
+
if (stepName === "properties") {
|
|
6443
|
+
if (source.syncProperties) {
|
|
6444
|
+
const repo = config.repo;
|
|
6445
|
+
const properties = {};
|
|
6446
|
+
const effectivePreset = repo?.protection;
|
|
6447
|
+
if (effectivePreset && effectivePreset !== "none") properties["holocron_branch_protection_level"] = effectivePreset;
|
|
6448
|
+
const isMonorepo = await access(join(input.context.repoRoot, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
|
|
6449
|
+
properties["monorepo"] = String(isMonorepo);
|
|
6450
|
+
const manual = repo?.properties ?? {};
|
|
6451
|
+
if (manual.lifecycle) properties["lifecycle"] = manual.lifecycle;
|
|
6452
|
+
if (manual.open_source !== void 0) properties["open_source"] = String(manual.open_source);
|
|
6453
|
+
if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
|
|
6454
|
+
if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
|
|
6455
|
+
{
|
|
6456
|
+
const rootPackageJson = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
6457
|
+
const workspacePackages = readWorkspacePackages(input.context.repoRoot);
|
|
6458
|
+
const workspacePackageJsons = await readWorkspacePackageJsons(input.context.repoRoot, workspacePackages);
|
|
6459
|
+
properties["holocron_profile"] = deriveProfile({
|
|
6460
|
+
rootPackageJson,
|
|
6461
|
+
repoName: basename(input.context.repoRoot),
|
|
6462
|
+
isMonorepo,
|
|
6463
|
+
workspacePackageJsons
|
|
6464
|
+
});
|
|
6465
|
+
properties["holocron_stack"] = deriveStack(rootPackageJson);
|
|
6466
|
+
const capabilities = deriveCapabilities(config.providers);
|
|
6467
|
+
properties["holocron_capabilities"] = capabilities;
|
|
6468
|
+
properties["holocron_compliance"] = deriveCompliance(capabilities);
|
|
6469
|
+
}
|
|
6470
|
+
steps.push(await runSyncStep("source", "sync properties", dryRun, () => source.syncProperties(properties)));
|
|
6471
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6472
|
+
} else {
|
|
6473
|
+
steps.push({
|
|
6474
|
+
capability: "source",
|
|
6475
|
+
step: "sync properties",
|
|
6476
|
+
status: "skip",
|
|
6477
|
+
message: "provider does not implement syncProperties"
|
|
6478
|
+
});
|
|
6479
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6480
|
+
}
|
|
6088
6481
|
}
|
|
6089
6482
|
if (stepName === "teams") {
|
|
6090
6483
|
const teams = config.repo?.teams ?? [];
|
|
@@ -6296,36 +6689,42 @@ async function runSync(input) {
|
|
|
6296
6689
|
}
|
|
6297
6690
|
}
|
|
6298
6691
|
}
|
|
6299
|
-
if (stepName === "scripts")
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
const
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6692
|
+
if (stepName === "scripts") {
|
|
6693
|
+
if (config.syncScripts === false) {
|
|
6694
|
+
steps.push({
|
|
6695
|
+
capability: "local",
|
|
6696
|
+
step: "sync scripts",
|
|
6697
|
+
status: "skip",
|
|
6698
|
+
message: "syncScripts: false"
|
|
6699
|
+
});
|
|
6700
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6701
|
+
} else {
|
|
6702
|
+
const tasksConfig = { tasks: config.tasks };
|
|
6703
|
+
if (config.holocronScript !== void 0) tasksConfig.holocronScript = config.holocronScript;
|
|
6704
|
+
const astromechScripts = createAstromech({
|
|
6705
|
+
cwd: input.context.repoRoot,
|
|
6706
|
+
config: tasksConfig,
|
|
6707
|
+
logger
|
|
6708
|
+
});
|
|
6709
|
+
const desired = astromechScripts.packageScripts();
|
|
6710
|
+
const turboContent = astromechScripts.turboConfig();
|
|
6711
|
+
const turboEligible = new Set(turboContent ? Object.keys(JSON.parse(turboContent).tasks) : []);
|
|
6712
|
+
steps.push(await runSyncStep("local", "sync scripts", dryRun, async () => {
|
|
6713
|
+
const changed = await mergePackageJsonScripts(input.context.repoRoot, desired, turboEligible);
|
|
6714
|
+
if (changed === null) return "no package.json";
|
|
6715
|
+
if (changed.length === 0) return `${Object.keys(desired).length} scripts already current`;
|
|
6716
|
+
return `${changed.join(", ")} set`;
|
|
6717
|
+
}));
|
|
6718
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6719
|
+
}
|
|
6322
6720
|
}
|
|
6323
6721
|
if (stepName === "turbo") {
|
|
6324
|
-
const
|
|
6722
|
+
const astromechTurbo = createAstromech({
|
|
6325
6723
|
cwd: input.context.repoRoot,
|
|
6326
6724
|
config: { tasks: config.tasks },
|
|
6327
6725
|
logger
|
|
6328
|
-
})
|
|
6726
|
+
});
|
|
6727
|
+
const content = astromechTurbo.turboConfig();
|
|
6329
6728
|
if (content === null) {
|
|
6330
6729
|
steps.push({
|
|
6331
6730
|
capability: "local",
|
|
@@ -6339,6 +6738,26 @@ async function runSync(input) {
|
|
|
6339
6738
|
await writeFile(join(input.context.repoRoot, "turbo.json"), content, "utf8");
|
|
6340
6739
|
}));
|
|
6341
6740
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
6741
|
+
const rootPkgRaw = await readFile(join(input.context.repoRoot, "package.json"), "utf8").catch(() => null);
|
|
6742
|
+
const rootPkg = rootPkgRaw ? JSON.parse(rootPkgRaw) : null;
|
|
6743
|
+
const workspaceYaml = await readFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), "utf8").catch(() => "");
|
|
6744
|
+
const rootScripts = rootPkg?.scripts ?? {};
|
|
6745
|
+
const workspaceFix = astromechTurbo.ensureRootWorkspaceMember(workspaceYaml, rootScripts);
|
|
6746
|
+
if (workspaceFix.changed) {
|
|
6747
|
+
steps.push(await runSyncStep("local", "fix pnpm-workspace.yaml (root workspace member, #692)", dryRun, async () => {
|
|
6748
|
+
await writeFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), workspaceFix.content, "utf8");
|
|
6749
|
+
}));
|
|
6750
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6751
|
+
}
|
|
6752
|
+
if (rootPkgRaw) {
|
|
6753
|
+
const depFix = astromechTurbo.ensureTurboDependency(rootPkgRaw, workspaceFix.content);
|
|
6754
|
+
if (depFix.changed) {
|
|
6755
|
+
steps.push(await runSyncStep("local", "add turbo devDependency", dryRun, async () => {
|
|
6756
|
+
await writeFile(join(input.context.repoRoot, "package.json"), depFix.content, "utf8");
|
|
6757
|
+
}));
|
|
6758
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6759
|
+
}
|
|
6760
|
+
}
|
|
6342
6761
|
}
|
|
6343
6762
|
}
|
|
6344
6763
|
if (stepName === "wiki") {
|
|
@@ -6422,8 +6841,21 @@ async function writePackageJsonField(repoRoot, field, value) {
|
|
|
6422
6841
|
* Merge `desired` script entries into `package.json#scripts` — sets only the
|
|
6423
6842
|
* listed keys, never touches the rest. Returns the sorted list of keys that
|
|
6424
6843
|
* drifted (empty when all current), or `null` when there is no `package.json`.
|
|
6844
|
+
*
|
|
6845
|
+
* `turboEligible` (#747): task names `turbo.json` fans out — the same set
|
|
6846
|
+
* {@link EnsureRootWorkspaceMemberResult} above already keys off. For those
|
|
6847
|
+
* only, an *existing* script that isn't the generated `holocron run <task>
|
|
6848
|
+
* --` wrapper is left untouched: root may genuinely be a directly-buildable
|
|
6849
|
+
* package (not an orchestrator) with its own real command (`tsc --noEmit`,
|
|
6850
|
+
* `vitest run`, …) for that task. Overwriting it would silently discard the
|
|
6851
|
+
* real command and, once `turbo.json` exists and root is a real workspace
|
|
6852
|
+
* member (#692), reintroduce infinite recursion — `turbo run <task>` →
|
|
6853
|
+
* root's own `<task>` script → the wrapper → `holocron run <task>` sees
|
|
6854
|
+
* `turbo.json` still defines the task → `turbo run <task>` again, forever.
|
|
6855
|
+
* Every other key (non-turbo tasks, `holocron`, `prepare`, …) has no
|
|
6856
|
+
* legitimate direct-command alternative and stays fully generator-owned.
|
|
6425
6857
|
*/
|
|
6426
|
-
async function mergePackageJsonScripts(repoRoot, desired) {
|
|
6858
|
+
async function mergePackageJsonScripts(repoRoot, desired, turboEligible) {
|
|
6427
6859
|
const pkgPath = join(repoRoot, "package.json");
|
|
6428
6860
|
let content;
|
|
6429
6861
|
try {
|
|
@@ -6434,15 +6866,31 @@ async function mergePackageJsonScripts(repoRoot, desired) {
|
|
|
6434
6866
|
const pkg = JSON.parse(content);
|
|
6435
6867
|
const scripts = pkg.scripts ?? {};
|
|
6436
6868
|
const changed = [];
|
|
6437
|
-
for (const [name, command] of Object.entries(desired))
|
|
6438
|
-
scripts[name]
|
|
6439
|
-
|
|
6869
|
+
for (const [name, command] of Object.entries(desired)) {
|
|
6870
|
+
const existing = scripts[name];
|
|
6871
|
+
if (turboEligible.has(name) && existing !== void 0 && !isGeneratedRunWrapper(existing, name)) continue;
|
|
6872
|
+
if (existing !== command) {
|
|
6873
|
+
scripts[name] = command;
|
|
6874
|
+
changed.push(name);
|
|
6875
|
+
}
|
|
6440
6876
|
}
|
|
6441
6877
|
if (changed.length === 0) return [];
|
|
6442
6878
|
pkg.scripts = scripts;
|
|
6443
6879
|
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
6444
6880
|
return changed.sort();
|
|
6445
6881
|
}
|
|
6882
|
+
/**
|
|
6883
|
+
* Matches the exact shape `packageScripts()` generates for `taskName` —
|
|
6884
|
+
* `<bin> run <taskName> --`, `<bin>` normally `holocron` but substitutable
|
|
6885
|
+
* (`holocronScript`, itself allowed to be multi-word, e.g. `"node
|
|
6886
|
+
packages/cli/dist/cli.mjs"` — see its own test). Checking the *suffix*
|
|
6887
|
+
* rather than the whole string means an old wrapper survives a
|
|
6888
|
+
* `holocronScript` rename intact — it's still recognized as a wrapper (safe
|
|
6889
|
+
* to update to the new one), not mistaken for a real command.
|
|
6890
|
+
*/
|
|
6891
|
+
function isGeneratedRunWrapper(command, taskName) {
|
|
6892
|
+
return command.endsWith(` run ${taskName} --`);
|
|
6893
|
+
}
|
|
6446
6894
|
const README_DESC_START = "<!-- holocron:description -->";
|
|
6447
6895
|
const README_DESC_END = "<!-- /holocron:description -->";
|
|
6448
6896
|
async function updateReadmeDescription(repoRoot, description) {
|
|
@@ -7309,6 +7757,63 @@ async function runUpgradeNode(input) {
|
|
|
7309
7757
|
};
|
|
7310
7758
|
}
|
|
7311
7759
|
//#endregion
|
|
7760
|
+
//#region src/commit-lint/lint-message.ts
|
|
7761
|
+
/**
|
|
7762
|
+
* Lints one commit message string against an already-loaded commitlint
|
|
7763
|
+
* config. The genuinely environment-independent half of commit-message
|
|
7764
|
+
* linting (holocron#789) — *obtaining* a `QualifiedConfig` differs by
|
|
7765
|
+
* caller (Sentinel isolates itself from any ambient config since it runs
|
|
7766
|
+
* standalone, away from a real repo checkout; `lintCommitMsgFile` below
|
|
7767
|
+
* just lets cosmiconfig discover the invoking repo's own
|
|
7768
|
+
* `commitlint.config.*`, matching `commitlint --edit`'s own contract) —
|
|
7769
|
+
* but linting one message against a loaded config is identical either way.
|
|
7770
|
+
*
|
|
7771
|
+
* Real `@commitlint/lint` — the same programmatic API `@commitlint/cli`
|
|
7772
|
+
* itself uses internally — never a reimplementation of commitlint's rules.
|
|
7773
|
+
*/
|
|
7774
|
+
/** `parserPreset.parserOpts`, if the loaded config sets one — same lookup `@commitlint/cli`'s own `selectParserOpts()` does. */
|
|
7775
|
+
function selectParserOpts(parserPreset) {
|
|
7776
|
+
return parserPreset?.parserOpts;
|
|
7777
|
+
}
|
|
7778
|
+
async function lintCommitMessage(message, loaded) {
|
|
7779
|
+
const opts = {
|
|
7780
|
+
/* istanbul ignore next -- see comment above */
|
|
7781
|
+
parserOpts: selectParserOpts(loaded.parserPreset) ?? {},
|
|
7782
|
+
plugins: loaded.plugins,
|
|
7783
|
+
/* istanbul ignore next -- see comment above */
|
|
7784
|
+
ignores: loaded.ignores ?? [],
|
|
7785
|
+
defaultIgnores: loaded.defaultIgnores !== false
|
|
7786
|
+
};
|
|
7787
|
+
const outcome = await lint(message, loaded.rules, opts);
|
|
7788
|
+
if (outcome.valid) return [];
|
|
7789
|
+
return outcome.errors.map((error) => ({
|
|
7790
|
+
rule: error.name,
|
|
7791
|
+
message: error.message
|
|
7792
|
+
}));
|
|
7793
|
+
}
|
|
7794
|
+
//#endregion
|
|
7795
|
+
//#region src/commit-lint/lint-commit-msg-file.ts
|
|
7796
|
+
/**
|
|
7797
|
+
* `holocron lint commit-msg <file>` — a real programmatic replacement for
|
|
7798
|
+
* shelling out to `commitlint --edit "$1"` from `.husky/commit-msg`
|
|
7799
|
+
* (holocron#789). Matches that CLI's own contract exactly: `file` is a
|
|
7800
|
+
* path to the commit message being written (the arg git's `commit-msg`
|
|
7801
|
+
* hook passes as `$1`), no `--config` override needed — `@commitlint/load`
|
|
7802
|
+
* runs from `cwd` with no seed, so cosmiconfig's own normal upward search
|
|
7803
|
+
* discovers the invoking repo's real `commitlint.config.*` file, exactly
|
|
7804
|
+
* as `commitlint --edit` (no `--config` flag) already does today. Unlike
|
|
7805
|
+
* Sentinel's `loadIsolatedConfig()`, no isolation is needed here — this
|
|
7806
|
+
* runs inside a real repo checkout with a real `node_modules`, not
|
|
7807
|
+
* standalone on a server.
|
|
7808
|
+
*/
|
|
7809
|
+
async function lintCommitMsgFile(filePath, cwd = process.cwd()) {
|
|
7810
|
+
const violations = await lintCommitMessage(await readFile(filePath, "utf8"), await load({}, { cwd }));
|
|
7811
|
+
return {
|
|
7812
|
+
valid: violations.length === 0,
|
|
7813
|
+
violations
|
|
7814
|
+
};
|
|
7815
|
+
}
|
|
7816
|
+
//#endregion
|
|
7312
7817
|
//#region src/config/load-config.ts
|
|
7313
7818
|
/**
|
|
7314
7819
|
* `holocron.config.{ts,js,mjs,cjs,json}` loader.
|
|
@@ -7419,7 +7924,9 @@ const numeric = (value) => /^\d+$/.test(value) ? true : "enter a number";
|
|
|
7419
7924
|
* The full command surface, hand-maintained in parallel with `cli.ts`'s
|
|
7420
7925
|
* yargs registrations (this module does not introspect yargs at runtime).
|
|
7421
7926
|
* `run <task>` is deliberately excluded — it's CI/scripting-oriented, and an
|
|
7422
|
-
* interactive prompt in front of it would work against that.
|
|
7927
|
+
* interactive prompt in front of it would work against that. `lint
|
|
7928
|
+
* commit-msg <file>` is excluded for the same reason: it's invoked
|
|
7929
|
+
* exclusively from `.husky/commit-msg "$1"`, never typed by a human.
|
|
7423
7930
|
*/
|
|
7424
7931
|
const COMMAND_REGISTRY = [
|
|
7425
7932
|
{
|
|
@@ -7730,7 +8237,7 @@ async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessag
|
|
|
7730
8237
|
//#endregion
|
|
7731
8238
|
//#region src/update-notifier.ts
|
|
7732
8239
|
const PACKAGE_NAME = "@theholocron/cli";
|
|
7733
|
-
const CACHE_TTL_MS =
|
|
8240
|
+
const CACHE_TTL_MS = 864e5;
|
|
7734
8241
|
const FETCH_TIMEOUT_MS = 3e3;
|
|
7735
8242
|
function getCacheDir() {
|
|
7736
8243
|
return env.get("HOLOCRON_CACHE_DIR") ?? join(homedir(), ".cache", "holocron");
|
|
@@ -8096,9 +8603,17 @@ try {
|
|
|
8096
8603
|
type: "array",
|
|
8097
8604
|
default: ["production", "preview"],
|
|
8098
8605
|
describe: "Deployment targets to sync to. Defaults to production + preview."
|
|
8606
|
+
}).option("github-secret", {
|
|
8607
|
+
type: "array",
|
|
8608
|
+
describe: "Vault key(s) to push to GH Actions secrets (repeatable). Omit to push every key."
|
|
8609
|
+
}).option("github-secret-scope", {
|
|
8610
|
+
type: "string",
|
|
8611
|
+
default: "repo",
|
|
8612
|
+
describe: "Scope applied to every --github-secret key: \"repo\" (default), \"env=<name>\", or \"org=<name>\"."
|
|
8099
8613
|
}), async (argv) => {
|
|
8100
8614
|
const tokens = tokenContext(argv.token);
|
|
8101
8615
|
if (!tokens) return;
|
|
8616
|
+
const githubSecretScope = parseScope(argv.githubSecretScope);
|
|
8102
8617
|
const [environmentId] = await promptForPositionals(getEntry("secrets sync"), argv);
|
|
8103
8618
|
const loaded = await loadConfig(argv.cwd);
|
|
8104
8619
|
applyResolvedConfig(argv, loaded.resolved);
|
|
@@ -8112,11 +8627,13 @@ try {
|
|
|
8112
8627
|
},
|
|
8113
8628
|
environmentId,
|
|
8114
8629
|
...argv.projectId ? { projectId: argv.projectId } : {},
|
|
8630
|
+
...argv.githubSecret?.length ? { githubSecretKeys: argv.githubSecret } : {},
|
|
8631
|
+
githubSecretScope,
|
|
8115
8632
|
targets: argv.target
|
|
8116
8633
|
})).summary.fail > 0) process.exitCode = 1;
|
|
8117
8634
|
}).command("deploy [branch]", "Trigger a deployment via the configured `deployment` capability", (y) => y.positional("branch", {
|
|
8118
8635
|
type: "string",
|
|
8119
|
-
describe: "Git branch to deploy"
|
|
8636
|
+
describe: "Git branch to deploy (omit when using --files)"
|
|
8120
8637
|
}).option("project-id", {
|
|
8121
8638
|
type: "string",
|
|
8122
8639
|
demandOption: true,
|
|
@@ -8125,20 +8642,34 @@ try {
|
|
|
8125
8642
|
type: "string",
|
|
8126
8643
|
choices: ["production", "staging"],
|
|
8127
8644
|
describe: "Named environment to deploy into. Omit for a branch preview."
|
|
8128
|
-
}),
|
|
8645
|
+
}).option("files", {
|
|
8646
|
+
type: "string",
|
|
8647
|
+
describe: "Deploy from a local directory of source files instead of a Git branch — calls deployFunction(), no linked repo required. Conflicts with `branch`."
|
|
8648
|
+
}).conflicts("files", "branch"), async (argv) => {
|
|
8129
8649
|
const tokens = tokenContext(argv.token);
|
|
8130
8650
|
if (!tokens) return;
|
|
8131
|
-
const [branch] = await promptForPositionals(getEntry("deploy"), argv);
|
|
8132
8651
|
const loaded = await loadConfig(argv.cwd);
|
|
8133
8652
|
applyResolvedConfig(argv, loaded.resolved);
|
|
8653
|
+
const context = {
|
|
8654
|
+
repoRoot: argv.cwd,
|
|
8655
|
+
dryRun: argv.dryRun,
|
|
8656
|
+
...tokens,
|
|
8657
|
+
org: resolveOrg(argv, loaded.resolved)
|
|
8658
|
+
};
|
|
8659
|
+
if (argv.files) {
|
|
8660
|
+
if ((await runDeployFromFiles({
|
|
8661
|
+
loaded,
|
|
8662
|
+
context,
|
|
8663
|
+
projectId: argv.projectId,
|
|
8664
|
+
dir: argv.files,
|
|
8665
|
+
...argv.target ? { target: argv.target } : {}
|
|
8666
|
+
})).status === "fail") process.exitCode = 1;
|
|
8667
|
+
return;
|
|
8668
|
+
}
|
|
8669
|
+
const [branch] = await promptForPositionals(getEntry("deploy"), argv);
|
|
8134
8670
|
if ((await runDeploy({
|
|
8135
8671
|
loaded,
|
|
8136
|
-
context
|
|
8137
|
-
repoRoot: argv.cwd,
|
|
8138
|
-
dryRun: argv.dryRun,
|
|
8139
|
-
...tokens,
|
|
8140
|
-
org: resolveOrg(argv, loaded.resolved)
|
|
8141
|
-
},
|
|
8672
|
+
context,
|
|
8142
8673
|
projectId: argv.projectId,
|
|
8143
8674
|
branch,
|
|
8144
8675
|
...argv.target ? { target: argv.target } : {}
|
|
@@ -8314,7 +8845,17 @@ try {
|
|
|
8314
8845
|
scope: argv.all ? "all" : "required",
|
|
8315
8846
|
...argv.filter ? { filter: argv.filter } : {}
|
|
8316
8847
|
}).status === "fail") process.exitCode = 1;
|
|
8317
|
-
}).command("
|
|
8848
|
+
}).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", {
|
|
8849
|
+
type: "string",
|
|
8850
|
+
demandOption: true,
|
|
8851
|
+
describe: "Path to the commit message file (git's commit-msg hook passes this as $1)."
|
|
8852
|
+
}), async (argv) => {
|
|
8853
|
+
const result = await lintCommitMsgFile(argv.file, argv.cwd);
|
|
8854
|
+
if (!result.valid) {
|
|
8855
|
+
for (const violation of result.violations) console.error(`✖ ${violation.rule}: ${violation.message}`);
|
|
8856
|
+
process.exitCode = 1;
|
|
8857
|
+
}
|
|
8858
|
+
}), () => {}).command("sync-github", "Sync workflow templates and composite actions to theholocron/.github", (y) => y.option("repo", {
|
|
8318
8859
|
type: "string",
|
|
8319
8860
|
default: "theholocron/.github",
|
|
8320
8861
|
describe: "Target org/repo (default: theholocron/.github)"
|