@theholocron/cli 5.0.0-alpha.4 → 5.0.0-alpha.40
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 +46 -0
- package/dist/cli.mjs +613 -51
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +79 -2
- package/dist/index.mjs +109 -2
- package/dist/plugin/capabilities.d.mts +89 -23
- package/package.json +6 -6
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";
|
|
@@ -1233,6 +1233,40 @@ function commandsInContext(context) {
|
|
|
1233
1233
|
}
|
|
1234
1234
|
//#endregion
|
|
1235
1235
|
//#region src/commands/deploy.ts
|
|
1236
|
+
/**
|
|
1237
|
+
* `holocron deploy` — trigger a deployment via the configured
|
|
1238
|
+
* `deployment` capability.
|
|
1239
|
+
*
|
|
1240
|
+
* Two modes, one command: `runDeploy` (the default — a Git branch,
|
|
1241
|
+
* `triggerDeployment()`) and `runDeployFromFiles` (`--files <dir>` —
|
|
1242
|
+
* a local directory of source files, `deployFunction()`, no linked
|
|
1243
|
+
* repo required — for a consumer with no Git history to deploy from,
|
|
1244
|
+
* e.g. Sentinel, shipped as an npm package). Separate functions, not
|
|
1245
|
+
* one branching `runDeploy`, because their inputs genuinely differ
|
|
1246
|
+
* (`branch` vs `dir`) and their result shapes differ too
|
|
1247
|
+
* (`DeploymentRecord` carries `status`/`branch`; `DeployFunctionResult`
|
|
1248
|
+
* is deliberately minimal, same as `DeployScriptResult` for Workers).
|
|
1249
|
+
*
|
|
1250
|
+
* Errors clearly when `deployment` isn't loaded (no provider declared
|
|
1251
|
+
* in `holocron.config.json`), or when the configured provider doesn't
|
|
1252
|
+
* implement `deployFunction` (it's optional on `Deployment` — not
|
|
1253
|
+
* every provider has a files-based deploy API).
|
|
1254
|
+
*
|
|
1255
|
+
* Dry-run skips the actual deploy call but still verifies the
|
|
1256
|
+
* deployment capability (and, for `--files`, `deployFunction`) is
|
|
1257
|
+
* present, so operators can sanity-check the wiring without spinning
|
|
1258
|
+
* up a build.
|
|
1259
|
+
*/
|
|
1260
|
+
/**
|
|
1261
|
+
* Provider `url` fields (Vercel's REST API, at least) come back as bare
|
|
1262
|
+
* hostnames — `"my-app.vercel.app"`, no scheme — which terminals don't
|
|
1263
|
+
* linkify and browsers won't navigate to unmodified. Printed output
|
|
1264
|
+
* should be a URL a human can click/paste; structured log fields keep
|
|
1265
|
+
* the raw provider value as-is.
|
|
1266
|
+
*/
|
|
1267
|
+
function withScheme(url) {
|
|
1268
|
+
return /^https?:\/\//.test(url) ? url : `https://${url}`;
|
|
1269
|
+
}
|
|
1236
1270
|
async function runDeploy(input) {
|
|
1237
1271
|
const print = input.print ?? ((line) => console.log(line));
|
|
1238
1272
|
const logger = input.logger ?? getLogger();
|
|
@@ -1264,7 +1298,7 @@ async function runDeploy(input) {
|
|
|
1264
1298
|
branch: input.branch,
|
|
1265
1299
|
...input.target ? { target: input.target } : {}
|
|
1266
1300
|
}));
|
|
1267
|
-
print(` ${style.success(`${record.status} — ${record.url}`)}`);
|
|
1301
|
+
print(` ${style.success(`${record.status} — ${withScheme(record.url)}`)}`);
|
|
1268
1302
|
logger.info({
|
|
1269
1303
|
status: record.status,
|
|
1270
1304
|
url: record.url,
|
|
@@ -1288,6 +1322,87 @@ async function runDeploy(input) {
|
|
|
1288
1322
|
};
|
|
1289
1323
|
}
|
|
1290
1324
|
}
|
|
1325
|
+
const SKIP_DIRS$2 = /* @__PURE__ */ new Set([
|
|
1326
|
+
".git",
|
|
1327
|
+
"node_modules",
|
|
1328
|
+
".turbo"
|
|
1329
|
+
]);
|
|
1330
|
+
/** Recursively lists absolute file paths under `dir`, skipping VCS/tooling noise. */
|
|
1331
|
+
function defaultWalkFiles$2(dir) {
|
|
1332
|
+
const results = [];
|
|
1333
|
+
for (const entry of readdirSync(dir)) {
|
|
1334
|
+
if (SKIP_DIRS$2.has(entry)) continue;
|
|
1335
|
+
const full = join(dir, entry);
|
|
1336
|
+
const stat = statSync(full);
|
|
1337
|
+
if (stat.isDirectory()) results.push(...defaultWalkFiles$2(full));
|
|
1338
|
+
else if (stat.isFile()) results.push(full);
|
|
1339
|
+
}
|
|
1340
|
+
return results;
|
|
1341
|
+
}
|
|
1342
|
+
/** `holocron deploy --files <dir>` — the non-git counterpart to `runDeploy`. */
|
|
1343
|
+
async function runDeployFromFiles(input) {
|
|
1344
|
+
const print = input.print ?? ((line) => console.log(line));
|
|
1345
|
+
const logger = input.logger ?? getLogger();
|
|
1346
|
+
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
1347
|
+
await loader.load();
|
|
1348
|
+
assertPluginsResolvable(loader, "deploy");
|
|
1349
|
+
const dryRun = input.context.dryRun ?? false;
|
|
1350
|
+
const walkFiles = input.walkFiles ?? defaultWalkFiles$2;
|
|
1351
|
+
const readFile = input.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
1352
|
+
const files = {};
|
|
1353
|
+
for (const abs of walkFiles(input.dir)) {
|
|
1354
|
+
const rel = relative(input.dir, abs).split(sep).join("/");
|
|
1355
|
+
files[rel] = readFile(abs);
|
|
1356
|
+
}
|
|
1357
|
+
const fileCount = Object.keys(files).length;
|
|
1358
|
+
logger.info({
|
|
1359
|
+
dir: input.dir,
|
|
1360
|
+
fileCount,
|
|
1361
|
+
target: input.target ?? "preview",
|
|
1362
|
+
projectId: input.projectId,
|
|
1363
|
+
dryRun: dryRun || void 0
|
|
1364
|
+
}, "deploy: start (files)");
|
|
1365
|
+
print(style.header(`Holocron deploy — files=${input.dir} (${fileCount} file${fileCount === 1 ? "" : "s"})${input.target ? `, target=${input.target}` : " (preview)"}${dryRun ? " (dry-run)" : ""}`));
|
|
1366
|
+
if (!loader.has("deployment")) throw new Error("deployment capability is not configured — add a `deployment` provider to holocron.config.json");
|
|
1367
|
+
const deploy = loader.get("deployment");
|
|
1368
|
+
if (!deploy.deployFunction) throw new Error(`deployment provider "${deploy.providerName}" does not support deploying from files — deployFunction() is not implemented`);
|
|
1369
|
+
if (dryRun) {
|
|
1370
|
+
const message = `would: ${deploy.providerName}.deployFunction(projectId=${input.projectId}, files=${fileCount}${input.target ? `, target=${input.target}` : ""})`;
|
|
1371
|
+
print(` ${style.dim(`… ${message}`)}`);
|
|
1372
|
+
return {
|
|
1373
|
+
deployment: null,
|
|
1374
|
+
status: "dry-run",
|
|
1375
|
+
message
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
try {
|
|
1379
|
+
const result = await withSpinner(`Deploying ${fileCount} file${fileCount === 1 ? "" : "s"} from ${input.dir}…`, () => deploy.deployFunction(input.projectId, {
|
|
1380
|
+
files,
|
|
1381
|
+
...input.target ? { target: input.target } : {}
|
|
1382
|
+
}));
|
|
1383
|
+
print(` ${style.success(withScheme(result.url))}`);
|
|
1384
|
+
logger.info({
|
|
1385
|
+
url: result.url,
|
|
1386
|
+
id: result.deploymentId
|
|
1387
|
+
}, "deploy: triggered (files)");
|
|
1388
|
+
return {
|
|
1389
|
+
deployment: result,
|
|
1390
|
+
status: "ok"
|
|
1391
|
+
};
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1394
|
+
print(` ${style.fail(message)}`);
|
|
1395
|
+
logger.warn({
|
|
1396
|
+
dir: input.dir,
|
|
1397
|
+
reason: message
|
|
1398
|
+
}, "deploy: failed (files)");
|
|
1399
|
+
return {
|
|
1400
|
+
deployment: null,
|
|
1401
|
+
status: "fail",
|
|
1402
|
+
message
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1291
1406
|
//#endregion
|
|
1292
1407
|
//#region src/commands/doctor.ts
|
|
1293
1408
|
async function runDoctor(input) {
|
|
@@ -3324,6 +3439,170 @@ async function runPublish(input = {}) {
|
|
|
3324
3439
|
packageNames
|
|
3325
3440
|
};
|
|
3326
3441
|
}
|
|
3442
|
+
/**
|
|
3443
|
+
* `holocron publish` (no `--initial`) — the steady-state publish
|
|
3444
|
+
* `exec.publishCmd` in `release.config.ts` should call, replacing each
|
|
3445
|
+
* repo's hand-typed `pnpm -r --filter=./packages/* publish ...` shell (which
|
|
3446
|
+
* differs, today, only in monorepo-vs-single-package, provenance on/off, and
|
|
3447
|
+
* whether a repo needs to skip already-published packages) with one uniform
|
|
3448
|
+
* invocation. Part of the config-resolution workstream, #676 — see
|
|
3449
|
+
* `.notes/tech-config-resolution.spec.md`.
|
|
3450
|
+
*
|
|
3451
|
+
* Deliberately does not do the `--initial` bootstrap dance (npm
|
|
3452
|
+
* login-if-needed, Trusted Publisher next-steps printout) — a steady-state
|
|
3453
|
+
* publish runs in CI, authenticated via OIDC automatically, no interactive
|
|
3454
|
+
* step involved. Reuses `hasPackagesDir()`'s monorepo-vs-single-package
|
|
3455
|
+
* detection rather than duplicating it.
|
|
3456
|
+
*/
|
|
3457
|
+
async function runSteadyPublish(input = {}) {
|
|
3458
|
+
const print = input.print ?? ((line) => console.log(line));
|
|
3459
|
+
const logger = input.logger ?? getLogger();
|
|
3460
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3461
|
+
const tag = input.tag ?? "latest";
|
|
3462
|
+
const dryRun = input.dryRun ?? false;
|
|
3463
|
+
const otp = input.otp;
|
|
3464
|
+
const exec = input.exec ?? defaultExec$1;
|
|
3465
|
+
const provenance = input.provenance ?? true;
|
|
3466
|
+
const skipAlreadyPublished = input.skipAlreadyPublished ?? false;
|
|
3467
|
+
const entries = input.packages ?? discoverPublicPackageEntries(cwd);
|
|
3468
|
+
const packageNames = entries.map((e) => e.name);
|
|
3469
|
+
print(`Holocron publish${dryRun ? " (dry-run)" : ""}`);
|
|
3470
|
+
print(` cwd: ${cwd}`);
|
|
3471
|
+
print(` tag: ${tag}`);
|
|
3472
|
+
print(` provenance: ${provenance}`);
|
|
3473
|
+
if (skipAlreadyPublished) print(" skip-already-published: true");
|
|
3474
|
+
logger.info({
|
|
3475
|
+
tag,
|
|
3476
|
+
provenance,
|
|
3477
|
+
skipAlreadyPublished,
|
|
3478
|
+
dryRun: dryRun || void 0
|
|
3479
|
+
}, "publish: start");
|
|
3480
|
+
if (otp) print(` otp: <${otp.length} chars>`);
|
|
3481
|
+
print("");
|
|
3482
|
+
if (entries.length === 0) {
|
|
3483
|
+
const message = "nothing to publish (no packages/* and root package.json is private or unnamed)";
|
|
3484
|
+
print(` ✗ ${message}`);
|
|
3485
|
+
logger.warn({ reason: message }, "publish: done");
|
|
3486
|
+
return {
|
|
3487
|
+
status: "fail",
|
|
3488
|
+
message,
|
|
3489
|
+
packageNames
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
const baseFlags = [
|
|
3493
|
+
"--access",
|
|
3494
|
+
"public",
|
|
3495
|
+
"--no-git-checks",
|
|
3496
|
+
"--tag",
|
|
3497
|
+
tag,
|
|
3498
|
+
...provenance ? ["--provenance"] : []
|
|
3499
|
+
];
|
|
3500
|
+
const otpFlags = otp ? ["--otp", otp] : [];
|
|
3501
|
+
if (dryRun) {
|
|
3502
|
+
print("");
|
|
3503
|
+
if (skipAlreadyPublished) for (const entry of entries) print(` … would check ${entry.name}@${entry.version}, publish if not already on npm`);
|
|
3504
|
+
else {
|
|
3505
|
+
const args = [
|
|
3506
|
+
...hasPackagesDir(cwd) ? ["-r", "--filter=./packages/*"] : [],
|
|
3507
|
+
"publish",
|
|
3508
|
+
...baseFlags,
|
|
3509
|
+
...otpFlags
|
|
3510
|
+
];
|
|
3511
|
+
print(` … (dry-run) skipping actual publish`);
|
|
3512
|
+
print(` would run: pnpm ${args.join(" ")}`);
|
|
3513
|
+
}
|
|
3514
|
+
logger.info({
|
|
3515
|
+
tag,
|
|
3516
|
+
status: "dry-run",
|
|
3517
|
+
packages: entries.length
|
|
3518
|
+
}, "publish: done");
|
|
3519
|
+
return {
|
|
3520
|
+
status: "dry-run",
|
|
3521
|
+
message: "dry-run — no publish executed",
|
|
3522
|
+
packageNames
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
if (skipAlreadyPublished) {
|
|
3526
|
+
print(" → publishing packages not already on npm…");
|
|
3527
|
+
for (const entry of entries) {
|
|
3528
|
+
const view = await exec("npm", [
|
|
3529
|
+
"view",
|
|
3530
|
+
`${entry.name}@${entry.version}`,
|
|
3531
|
+
"version"
|
|
3532
|
+
], { cwd });
|
|
3533
|
+
if (view.exitCode === 0 && view.stdout.trim()) {
|
|
3534
|
+
print(` skip ${entry.name}@${entry.version} (already published)`);
|
|
3535
|
+
continue;
|
|
3536
|
+
}
|
|
3537
|
+
const publish = await exec("pnpm", [
|
|
3538
|
+
"--filter",
|
|
3539
|
+
entry.dir,
|
|
3540
|
+
"publish",
|
|
3541
|
+
...baseFlags,
|
|
3542
|
+
...otpFlags
|
|
3543
|
+
], { cwd });
|
|
3544
|
+
if (publish.exitCode !== 0) {
|
|
3545
|
+
const message = `publish failed for ${entry.name}@${entry.version} (exit ${publish.exitCode}): ${publish.stderr.trim() || publish.stdout.trim() || "no output"}`;
|
|
3546
|
+
print(` ✗ ${message}`);
|
|
3547
|
+
logger.warn({
|
|
3548
|
+
tag,
|
|
3549
|
+
package: entry.name,
|
|
3550
|
+
reason: message
|
|
3551
|
+
}, "publish: done");
|
|
3552
|
+
return {
|
|
3553
|
+
status: "fail",
|
|
3554
|
+
message,
|
|
3555
|
+
packageNames
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
print(` ✓ ${entry.name}@${entry.version}`);
|
|
3559
|
+
}
|
|
3560
|
+
logger.info({
|
|
3561
|
+
tag,
|
|
3562
|
+
status: "ok",
|
|
3563
|
+
packages: entries.length
|
|
3564
|
+
}, "publish: done");
|
|
3565
|
+
return {
|
|
3566
|
+
status: "ok",
|
|
3567
|
+
packageNames
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
print(" → publishing all public @theholocron/* packages…");
|
|
3571
|
+
const publish = await exec("pnpm", [
|
|
3572
|
+
...hasPackagesDir(cwd) ? ["-r", "--filter=./packages/*"] : [],
|
|
3573
|
+
"publish",
|
|
3574
|
+
...baseFlags,
|
|
3575
|
+
...otpFlags
|
|
3576
|
+
], { cwd });
|
|
3577
|
+
if (publish.exitCode !== 0) {
|
|
3578
|
+
const message = `publish failed (exit ${publish.exitCode}): ${publish.stderr.trim() || publish.stdout.trim() || "no output"}`;
|
|
3579
|
+
print(` ✗ ${message}`);
|
|
3580
|
+
if (publish.stdout.includes("EOTP") || publish.stderr.includes("EOTP")) {
|
|
3581
|
+
print("");
|
|
3582
|
+
print(" → hint: your npm account requires 2FA for writes. Re-run with `--otp <code>`:");
|
|
3583
|
+
print(` pnpm exec holocron publish --otp <6-digit-code>`);
|
|
3584
|
+
}
|
|
3585
|
+
logger.warn({
|
|
3586
|
+
tag,
|
|
3587
|
+
reason: message
|
|
3588
|
+
}, "publish: done");
|
|
3589
|
+
return {
|
|
3590
|
+
status: "fail",
|
|
3591
|
+
message,
|
|
3592
|
+
packageNames
|
|
3593
|
+
};
|
|
3594
|
+
}
|
|
3595
|
+
print(" ✓ publish complete");
|
|
3596
|
+
logger.info({
|
|
3597
|
+
tag,
|
|
3598
|
+
status: "ok",
|
|
3599
|
+
packages: entries.length
|
|
3600
|
+
}, "publish: done");
|
|
3601
|
+
return {
|
|
3602
|
+
status: "ok",
|
|
3603
|
+
packageNames
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3327
3606
|
function hasPackagesDir(cwd) {
|
|
3328
3607
|
return existsSync(join(cwd, "packages"));
|
|
3329
3608
|
}
|
|
@@ -3337,6 +3616,37 @@ function publicPackageName(pkgPath) {
|
|
|
3337
3616
|
return;
|
|
3338
3617
|
}
|
|
3339
3618
|
}
|
|
3619
|
+
/** Read a `package.json`'s `name` + `version`, when it's public (`!private && name`). */
|
|
3620
|
+
function publicPackageEntry(pkgPath, dir) {
|
|
3621
|
+
if (!existsSync(pkgPath)) return void 0;
|
|
3622
|
+
try {
|
|
3623
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
3624
|
+
return !pkg.private && pkg.name && pkg.version ? {
|
|
3625
|
+
name: pkg.name,
|
|
3626
|
+
version: pkg.version,
|
|
3627
|
+
dir
|
|
3628
|
+
} : void 0;
|
|
3629
|
+
} catch {
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
/**
|
|
3634
|
+
* Same repo-layout detection as {@link discoverPublicPackages}, but carrying
|
|
3635
|
+
* version + a `pnpm --filter`-ready path — {@link runSteadyPublish}'s
|
|
3636
|
+
* `skipAlreadyPublished` mode needs both to check `npm view <pkg>@<version>`
|
|
3637
|
+
* and publish one workspace at a time.
|
|
3638
|
+
*/
|
|
3639
|
+
function discoverPublicPackageEntries(cwd) {
|
|
3640
|
+
if (!hasPackagesDir(cwd)) {
|
|
3641
|
+
const entry = publicPackageEntry(join(cwd, "package.json"), ".");
|
|
3642
|
+
return entry ? [entry] : [];
|
|
3643
|
+
}
|
|
3644
|
+
const packagesDir = join(cwd, "packages");
|
|
3645
|
+
return readdirSync(packagesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).flatMap((e) => {
|
|
3646
|
+
const entry = publicPackageEntry(join(packagesDir, e.name, "package.json"), `./packages/${e.name}`);
|
|
3647
|
+
return entry ? [entry] : [];
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3340
3650
|
/**
|
|
3341
3651
|
* Monorepo (`packages/` present): every public `packages/*` workspace.
|
|
3342
3652
|
* Single package (no `packages/`): the root `package.json`, if it's public —
|
|
@@ -3601,6 +3911,104 @@ function vaultProviderName(loader) {
|
|
|
3601
3911
|
return loader.get("vault").providerName;
|
|
3602
3912
|
}
|
|
3603
3913
|
//#endregion
|
|
3914
|
+
//#region src/commands/setup/derived-properties.ts
|
|
3915
|
+
/**
|
|
3916
|
+
* Repo archetype, in priority order:
|
|
3917
|
+
* 1. `*-template` repo name → `template` (independent of package shape).
|
|
3918
|
+
* 2. Monorepo whose primary published artifact is a CLI (some workspace
|
|
3919
|
+
* package has `bin`) → `platform` — matches `holocron` itself: the CLI is
|
|
3920
|
+
* the deliverable, the plugin packages are supporting infrastructure for
|
|
3921
|
+
* it, not separate products.
|
|
3922
|
+
* 3. Monorepo with no CLI but at least one non-private workspace package →
|
|
3923
|
+
* `library` — matches `clients`/`configs`/`utils`/`themes`/
|
|
3924
|
+
* `observability`: a family of published packages, none of them a CLI.
|
|
3925
|
+
* Checked *before* falling into the root-package branch below, because
|
|
3926
|
+
* these repos' own root `package.json` is private and often carries
|
|
3927
|
+
* `@theholocron/astro-config` for a docs site built from the monorepo
|
|
3928
|
+
* root — without this check they'd wrongly resolve to `docs` off that
|
|
3929
|
+
* root-level signal alone, when the docs site is a secondary concern of
|
|
3930
|
+
* a library collection, not what the repo fundamentally is.
|
|
3931
|
+
* 4. Single-package repo with `package.json#bin` → `cli`.
|
|
3932
|
+
* 5. Docs-only site (astro/starlight present, root package private, no
|
|
3933
|
+
* publishable exports) → `docs`.
|
|
3934
|
+
* 6. Private, non-publishable, non-docs → `app`.
|
|
3935
|
+
* 7. Otherwise, a single publishable package → `library`.
|
|
3936
|
+
*/
|
|
3937
|
+
function deriveProfile(input) {
|
|
3938
|
+
if (/-template$/.test(input.repoName)) return "template";
|
|
3939
|
+
if (input.isMonorepo) {
|
|
3940
|
+
if (input.workspacePackageJsons.some((pkg) => Boolean(pkg.bin))) return "platform";
|
|
3941
|
+
if (input.workspacePackageJsons.some((pkg) => pkg.private !== true)) return "library";
|
|
3942
|
+
}
|
|
3943
|
+
const pkg = input.rootPackageJson;
|
|
3944
|
+
if (pkg?.bin) return "cli";
|
|
3945
|
+
const deps = {
|
|
3946
|
+
...pkg?.dependencies,
|
|
3947
|
+
...pkg?.devDependencies
|
|
3948
|
+
};
|
|
3949
|
+
const isDocsSite = Boolean(deps["@theholocron/astro-config"] || deps["astro"]);
|
|
3950
|
+
if (pkg?.private && isDocsSite) return "docs";
|
|
3951
|
+
if (pkg?.private) return "app";
|
|
3952
|
+
return "library";
|
|
3953
|
+
}
|
|
3954
|
+
const KNOWN_STACK_DEPS = [
|
|
3955
|
+
"next",
|
|
3956
|
+
"vite",
|
|
3957
|
+
"astro",
|
|
3958
|
+
"tsdown",
|
|
3959
|
+
"rollup",
|
|
3960
|
+
"webpack",
|
|
3961
|
+
"storybook",
|
|
3962
|
+
"vitest",
|
|
3963
|
+
"playwright",
|
|
3964
|
+
"turbo"
|
|
3965
|
+
];
|
|
3966
|
+
/**
|
|
3967
|
+
* Detected build/framework tooling — the literal "does this repo need
|
|
3968
|
+
* next/vite/tsdown" signal `holocron.config.ts` has no field for today
|
|
3969
|
+
* (framework choice isn't a config concept, unlike `providers`/`tasks`).
|
|
3970
|
+
* Curated table, not exhaustive by design — add to `KNOWN_STACK_DEPS` as new
|
|
3971
|
+
* frameworks show up rather than trying to detect anything on npm.
|
|
3972
|
+
*/
|
|
3973
|
+
function deriveStack(pkg) {
|
|
3974
|
+
const deps = {
|
|
3975
|
+
...pkg?.dependencies,
|
|
3976
|
+
...pkg?.devDependencies
|
|
3977
|
+
};
|
|
3978
|
+
return KNOWN_STACK_DEPS.filter((name) => name in deps);
|
|
3979
|
+
}
|
|
3980
|
+
/** The provider capability keys actually wired in `providers: {}` — zero heuristics. */
|
|
3981
|
+
function deriveCapabilities(providers) {
|
|
3982
|
+
return Object.keys(providers ?? {}).sort();
|
|
3983
|
+
}
|
|
3984
|
+
/**
|
|
3985
|
+
* `source` + `ci` are the minimal baseline every repo `holocron setup`
|
|
3986
|
+
* touches wires — every `holocron.config.ts` in this org already satisfies
|
|
3987
|
+
* this, so `non-compliant` here means drift (a provider manually removed
|
|
3988
|
+
* after setup), not an unmet aspirational policy. Deliberately not a richer
|
|
3989
|
+
* per-profile policy yet (e.g. "a `library` must have `deployment`") — that's
|
|
3990
|
+
* a Phase B (GitHub App) refinement once it can post *why* on a check run.
|
|
3991
|
+
*/
|
|
3992
|
+
const REQUIRED_BASELINE = ["source", "ci"];
|
|
3993
|
+
function deriveCompliance(capabilities) {
|
|
3994
|
+
return REQUIRED_BASELINE.every((key) => capabilities.includes(key)) ? "compliant" : "non-compliant";
|
|
3995
|
+
}
|
|
3996
|
+
/**
|
|
3997
|
+
* Reads each workspace package's actual `package.json` content (not just
|
|
3998
|
+
* `readWorkspacePackages()`'s `{ slug, name, dir }` — `deriveProfile()`'s
|
|
3999
|
+
* `platform` detection needs `bin`, which that helper doesn't carry).
|
|
4000
|
+
* Missing/invalid files are skipped, matching `readWorkspacePackages()`'s
|
|
4001
|
+
* own soft-fail behavior.
|
|
4002
|
+
*/
|
|
4003
|
+
async function readWorkspacePackageJsons(repoRoot, workspacePackages) {
|
|
4004
|
+
const results = [];
|
|
4005
|
+
for (const { slug, dir = "packages" } of workspacePackages) try {
|
|
4006
|
+
const raw = await readFile(join(repoRoot, dir, slug, "package.json"), "utf8");
|
|
4007
|
+
results.push(JSON.parse(raw));
|
|
4008
|
+
} catch {}
|
|
4009
|
+
return results;
|
|
4010
|
+
}
|
|
4011
|
+
//#endregion
|
|
3604
4012
|
//#region src/commands/setup/labels.ts
|
|
3605
4013
|
const CANONICAL_LABELS = [
|
|
3606
4014
|
{
|
|
@@ -3749,12 +4157,21 @@ var alexrc_default = { allow: [
|
|
|
3749
4157
|
] };
|
|
3750
4158
|
//#endregion
|
|
3751
4159
|
//#region src/templates/configs/alexjs/create-config.ts
|
|
3752
|
-
const { workflowHeader: workflowHeader$
|
|
4160
|
+
const { workflowHeader: workflowHeader$9 } = createHeader({ source: "packages/cli/src/templates/configs/alexjs/create-config.ts" });
|
|
3753
4161
|
function createRcConfig() {
|
|
3754
4162
|
return JSON.stringify(alexrc_default, null, 2) + "\n";
|
|
3755
4163
|
}
|
|
3756
4164
|
function createIgnoreConfig$1() {
|
|
3757
|
-
return `${workflowHeader$
|
|
4165
|
+
return `${workflowHeader$9()}${alexignore_default}`;
|
|
4166
|
+
}
|
|
4167
|
+
//#endregion
|
|
4168
|
+
//#region src/templates/configs/commit-msg/commit-msg
|
|
4169
|
+
var commit_msg_default = "# --config <resolved shared path> (config-resolution workstream, #676) — falls\n# back to commitlint's own auto-discovery of a local commitlint.config.ts when\n# the shared package's built dist/ isn't present (not installed, or a stale\n# install predating this file).\nCOMMITLINT_CONFIG=\"node_modules/@theholocron/commitlint-config/dist/index.js\"\nif [ -f \"$COMMITLINT_CONFIG\" ]; then\n npx --no -- commitlint --config \"$COMMITLINT_CONFIG\" --edit \"$1\"\nelse\n npx --no -- commitlint --edit \"$1\"\nfi\n";
|
|
4170
|
+
//#endregion
|
|
4171
|
+
//#region src/templates/configs/commit-msg/create-config.ts
|
|
4172
|
+
const { workflowHeader: workflowHeader$8 } = createHeader({ source: "packages/cli/src/templates/configs/commit-msg/create-config.ts" });
|
|
4173
|
+
function createConfig$7() {
|
|
4174
|
+
return `${workflowHeader$8("shebang")}${commit_msg_default}`;
|
|
3758
4175
|
}
|
|
3759
4176
|
//#endregion
|
|
3760
4177
|
//#region src/templates/configs/devmoji/create-config.ts
|
|
@@ -3859,7 +4276,7 @@ function createConfig$2(holocronScript = "pnpm exec holocron") {
|
|
|
3859
4276
|
}
|
|
3860
4277
|
//#endregion
|
|
3861
4278
|
//#region src/templates/configs/prepare-commit-msg/prepare-commit-msg
|
|
3862
|
-
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\
|
|
4279
|
+
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";
|
|
3863
4280
|
//#endregion
|
|
3864
4281
|
//#region src/templates/configs/prepare-commit-msg/create-config.ts
|
|
3865
4282
|
const { workflowHeader: workflowHeader$3 } = createHeader({ source: "packages/cli/src/templates/configs/prepare-commit-msg/create-config.ts" });
|
|
@@ -4856,6 +5273,10 @@ async function runSetup(input) {
|
|
|
4856
5273
|
await source.writeRepoFile(".husky/prepare-commit-msg", createConfig$1());
|
|
4857
5274
|
}));
|
|
4858
5275
|
print(formatStep(steps[steps.length - 1]));
|
|
5276
|
+
steps.push(await runStep("source", "write .husky/commit-msg", dryRun, async () => {
|
|
5277
|
+
await source.writeRepoFile(".husky/commit-msg", createConfig$7());
|
|
5278
|
+
}));
|
|
5279
|
+
print(formatStep(steps[steps.length - 1]));
|
|
4859
5280
|
if (hooksEnabled) {
|
|
4860
5281
|
steps.push(await runStep("source", "write .husky/pre-push", dryRun, async () => {
|
|
4861
5282
|
await source.writeRepoFile(".husky/pre-push", createConfig$2(config.holocronScript));
|
|
@@ -4910,20 +5331,38 @@ async function runSetup(input) {
|
|
|
4910
5331
|
print(formatStep(steps[steps.length - 1]));
|
|
4911
5332
|
}
|
|
4912
5333
|
{
|
|
4913
|
-
const
|
|
5334
|
+
const astromechTurbo = createAstromech({
|
|
4914
5335
|
cwd: input.context.repoRoot,
|
|
4915
5336
|
config: { tasks: config.tasks },
|
|
4916
5337
|
logger
|
|
4917
|
-
})
|
|
5338
|
+
});
|
|
5339
|
+
const content = astromechTurbo.turboConfig();
|
|
4918
5340
|
if (content === null) steps.push({
|
|
4919
5341
|
capability: "source",
|
|
4920
5342
|
step: "write turbo.json",
|
|
4921
5343
|
status: "skip",
|
|
4922
5344
|
message: "no fan-out-eligible tasks configured"
|
|
4923
5345
|
});
|
|
4924
|
-
else
|
|
4925
|
-
await
|
|
4926
|
-
|
|
5346
|
+
else {
|
|
5347
|
+
steps.push(await runStep("source", "write turbo.json", dryRun, async () => {
|
|
5348
|
+
await source.writeRepoFile("turbo.json", content);
|
|
5349
|
+
}));
|
|
5350
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5351
|
+
const rootPkgRaw = await readFile(join(input.context.repoRoot, "package.json"), "utf8").catch(() => null);
|
|
5352
|
+
const rootPkg = rootPkgRaw ? JSON.parse(rootPkgRaw) : null;
|
|
5353
|
+
const workspaceYaml = await readFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), "utf8").catch(() => "");
|
|
5354
|
+
const rootScripts = Object.keys(rootPkg?.scripts ?? {});
|
|
5355
|
+
const workspaceFix = astromechTurbo.ensureRootWorkspaceMember(workspaceYaml, rootScripts);
|
|
5356
|
+
if (workspaceFix.changed) steps.push(await runStep("source", "fix pnpm-workspace.yaml (root workspace member, #692)", dryRun, async () => {
|
|
5357
|
+
await source.writeRepoFile("pnpm-workspace.yaml", workspaceFix.content);
|
|
5358
|
+
}));
|
|
5359
|
+
if (rootPkgRaw) {
|
|
5360
|
+
const depFix = astromechTurbo.ensureTurboDependency(rootPkgRaw, workspaceFix.content);
|
|
5361
|
+
if (depFix.changed) steps.push(await runStep("source", "add turbo devDependency", dryRun, async () => {
|
|
5362
|
+
await source.writeRepoFile("package.json", depFix.content);
|
|
5363
|
+
}));
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
4927
5366
|
print(formatStep(steps[steps.length - 1]));
|
|
4928
5367
|
}
|
|
4929
5368
|
if (source.syncLabels) {
|
|
@@ -4933,7 +5372,7 @@ async function runSetup(input) {
|
|
|
4933
5372
|
print(formatStep(steps[steps.length - 1]));
|
|
4934
5373
|
}
|
|
4935
5374
|
const properties = {};
|
|
4936
|
-
if (effectivePreset && effectivePreset !== "none") properties["
|
|
5375
|
+
if (effectivePreset && effectivePreset !== "none") properties["holocron_branch_protection_level"] = effectivePreset;
|
|
4937
5376
|
const isMonorepo = await access(join(input.context.repoRoot, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
|
|
4938
5377
|
properties["monorepo"] = String(isMonorepo);
|
|
4939
5378
|
const manual = repo?.properties ?? {};
|
|
@@ -4941,6 +5380,21 @@ async function runSetup(input) {
|
|
|
4941
5380
|
if (manual.open_source !== void 0) properties["open_source"] = String(manual.open_source);
|
|
4942
5381
|
if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
|
|
4943
5382
|
if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
|
|
5383
|
+
{
|
|
5384
|
+
const rootPackageJson = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
5385
|
+
const workspacePackages = readWorkspacePackages(input.context.repoRoot);
|
|
5386
|
+
const workspacePackageJsons = await readWorkspacePackageJsons(input.context.repoRoot, workspacePackages);
|
|
5387
|
+
properties["holocron_profile"] = deriveProfile({
|
|
5388
|
+
rootPackageJson,
|
|
5389
|
+
repoName: basename(input.context.repoRoot),
|
|
5390
|
+
isMonorepo,
|
|
5391
|
+
workspacePackageJsons
|
|
5392
|
+
});
|
|
5393
|
+
properties["holocron_stack"] = deriveStack(rootPackageJson);
|
|
5394
|
+
const capabilities = deriveCapabilities(config.providers);
|
|
5395
|
+
properties["holocron_capabilities"] = capabilities;
|
|
5396
|
+
properties["holocron_compliance"] = deriveCompliance(capabilities);
|
|
5397
|
+
}
|
|
4944
5398
|
if (source.syncProperties) {
|
|
4945
5399
|
steps.push(await runStep("source", "sync properties", dryRun, () => source.syncProperties(properties)));
|
|
4946
5400
|
print(formatStep(steps[steps.length - 1]));
|
|
@@ -5042,11 +5496,9 @@ async function runSetup(input) {
|
|
|
5042
5496
|
const wikiProxy = wiki.proxyConfig?.();
|
|
5043
5497
|
if (wikiDns && loader.has("dns")) {
|
|
5044
5498
|
const dns = loader.get("dns");
|
|
5045
|
-
steps.push(await runStep("dns", `upsertRecord ${wikiDns.
|
|
5499
|
+
steps.push(await runStep("dns", `upsertRecord ${wikiDns.record.name}`, dryRun, async () => {
|
|
5046
5500
|
await dns.upsertRecord(wikiDns.zone, {
|
|
5047
|
-
|
|
5048
|
-
name: wikiDns.cname,
|
|
5049
|
-
content: wikiDns.target,
|
|
5501
|
+
...wikiDns.record,
|
|
5050
5502
|
ttl: 1,
|
|
5051
5503
|
...wikiProxy ? { proxied: true } : {}
|
|
5052
5504
|
});
|
|
@@ -5055,8 +5507,8 @@ async function runSetup(input) {
|
|
|
5055
5507
|
}
|
|
5056
5508
|
if (wikiProxy && wikiDns && loader.has("workers")) {
|
|
5057
5509
|
const workers = loader.get("workers");
|
|
5058
|
-
steps.push(await runStep("workers", `upsertProxy ${wikiDns.
|
|
5059
|
-
await workers.upsertProxy(wikiDns.
|
|
5510
|
+
steps.push(await runStep("workers", `upsertProxy ${wikiDns.record.name}`, dryRun, async () => {
|
|
5511
|
+
await workers.upsertProxy(wikiDns.record.name, wikiProxy);
|
|
5060
5512
|
}));
|
|
5061
5513
|
print(formatStep(steps[steps.length - 1]));
|
|
5062
5514
|
}
|
|
@@ -5069,11 +5521,31 @@ async function runSetup(input) {
|
|
|
5069
5521
|
org: config.org,
|
|
5070
5522
|
domain: config.domain
|
|
5071
5523
|
}) : null;
|
|
5524
|
+
const ensureCustomDomain = async (projectId, hostname) => {
|
|
5525
|
+
if (!deploy.ensureCustomDomain) return;
|
|
5526
|
+
const customDomainDns = { value: null };
|
|
5527
|
+
steps.push(await runStep("deployment", `ensureCustomDomain ${hostname}`, dryRun, async () => {
|
|
5528
|
+
customDomainDns.value = await deploy.ensureCustomDomain(projectId, hostname);
|
|
5529
|
+
}));
|
|
5530
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5531
|
+
if (customDomainDns.value && loader.has("dns")) {
|
|
5532
|
+
const dns = loader.get("dns");
|
|
5533
|
+
const dnsRecord = customDomainDns.value;
|
|
5534
|
+
steps.push(await runStep("dns", `upsertRecord ${dnsRecord.record.name}`, dryRun, async () => {
|
|
5535
|
+
await dns.upsertRecord(dnsRecord.zone, {
|
|
5536
|
+
...dnsRecord.record,
|
|
5537
|
+
ttl: 1
|
|
5538
|
+
});
|
|
5539
|
+
}));
|
|
5540
|
+
print(formatStep(steps[steps.length - 1]));
|
|
5541
|
+
}
|
|
5542
|
+
};
|
|
5072
5543
|
if (!previewCfg) {
|
|
5073
5544
|
steps.push(await runStep("deployment", `ensureProject ${config.name}`, dryRun, async () => {
|
|
5074
5545
|
await deploy.ensureProject({ name: config.name });
|
|
5075
5546
|
}));
|
|
5076
5547
|
print(formatStep(steps[steps.length - 1]));
|
|
5548
|
+
if (deploy.domain) await ensureCustomDomain(config.name, deploy.domain);
|
|
5077
5549
|
}
|
|
5078
5550
|
if (previewCfg) {
|
|
5079
5551
|
steps.push(await runStep("deployment", `ensureProject ${previewCfg.project}`, dryRun, async () => {
|
|
@@ -5081,13 +5553,8 @@ async function runSetup(input) {
|
|
|
5081
5553
|
}));
|
|
5082
5554
|
print(formatStep(steps[steps.length - 1]));
|
|
5083
5555
|
}
|
|
5084
|
-
if (previewCfg?.domain
|
|
5085
|
-
|
|
5086
|
-
await deploy.ensureCustomDomain(previewCfg.project, previewCfg.domain);
|
|
5087
|
-
}));
|
|
5088
|
-
print(formatStep(steps[steps.length - 1]));
|
|
5089
|
-
}
|
|
5090
|
-
if (previewCfg?.domain && loader.has("dns")) {
|
|
5556
|
+
if (previewCfg?.domain) await ensureCustomDomain(previewCfg.project, previewCfg.domain);
|
|
5557
|
+
if (previewCfg?.domain && deploy.providerName === "cloudflare" && loader.has("dns")) {
|
|
5091
5558
|
const dns = loader.get("dns");
|
|
5092
5559
|
const wildcardDomain = `*.${previewCfg.domain}`;
|
|
5093
5560
|
steps.push(await runStep("dns", `upsertRecord ${previewCfg.domain}`, dryRun, async () => {
|
|
@@ -5859,7 +6326,7 @@ async function runSync(input) {
|
|
|
5859
6326
|
const repo = config.repo;
|
|
5860
6327
|
const properties = {};
|
|
5861
6328
|
const effectivePreset = repo?.protection;
|
|
5862
|
-
if (effectivePreset && effectivePreset !== "none") properties["
|
|
6329
|
+
if (effectivePreset && effectivePreset !== "none") properties["holocron_branch_protection_level"] = effectivePreset;
|
|
5863
6330
|
const isMonorepo = await access(join(input.context.repoRoot, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
|
|
5864
6331
|
properties["monorepo"] = String(isMonorepo);
|
|
5865
6332
|
const manual = repo?.properties ?? {};
|
|
@@ -5867,6 +6334,21 @@ async function runSync(input) {
|
|
|
5867
6334
|
if (manual.open_source !== void 0) properties["open_source"] = String(manual.open_source);
|
|
5868
6335
|
if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
|
|
5869
6336
|
if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
|
|
6337
|
+
{
|
|
6338
|
+
const rootPackageJson = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
6339
|
+
const workspacePackages = readWorkspacePackages(input.context.repoRoot);
|
|
6340
|
+
const workspacePackageJsons = await readWorkspacePackageJsons(input.context.repoRoot, workspacePackages);
|
|
6341
|
+
properties["holocron_profile"] = deriveProfile({
|
|
6342
|
+
rootPackageJson,
|
|
6343
|
+
repoName: basename(input.context.repoRoot),
|
|
6344
|
+
isMonorepo,
|
|
6345
|
+
workspacePackageJsons
|
|
6346
|
+
});
|
|
6347
|
+
properties["holocron_stack"] = deriveStack(rootPackageJson);
|
|
6348
|
+
const capabilities = deriveCapabilities(config.providers);
|
|
6349
|
+
properties["holocron_capabilities"] = capabilities;
|
|
6350
|
+
properties["holocron_compliance"] = deriveCompliance(capabilities);
|
|
6351
|
+
}
|
|
5870
6352
|
steps.push(await runSyncStep("source", "sync properties", dryRun, () => source.syncProperties(properties)));
|
|
5871
6353
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
5872
6354
|
} else {
|
|
@@ -6099,13 +6581,16 @@ async function runSync(input) {
|
|
|
6099
6581
|
} else {
|
|
6100
6582
|
const tasksConfig = { tasks: config.tasks };
|
|
6101
6583
|
if (config.holocronScript !== void 0) tasksConfig.holocronScript = config.holocronScript;
|
|
6102
|
-
const
|
|
6584
|
+
const astromechScripts = createAstromech({
|
|
6103
6585
|
cwd: input.context.repoRoot,
|
|
6104
6586
|
config: tasksConfig,
|
|
6105
6587
|
logger
|
|
6106
|
-
})
|
|
6588
|
+
});
|
|
6589
|
+
const desired = astromechScripts.packageScripts();
|
|
6590
|
+
const turboContent = astromechScripts.turboConfig();
|
|
6591
|
+
const turboEligible = new Set(turboContent ? Object.keys(JSON.parse(turboContent).tasks) : []);
|
|
6107
6592
|
steps.push(await runSyncStep("local", "sync scripts", dryRun, async () => {
|
|
6108
|
-
const changed = await mergePackageJsonScripts(input.context.repoRoot, desired);
|
|
6593
|
+
const changed = await mergePackageJsonScripts(input.context.repoRoot, desired, turboEligible);
|
|
6109
6594
|
if (changed === null) return "no package.json";
|
|
6110
6595
|
if (changed.length === 0) return `${Object.keys(desired).length} scripts already current`;
|
|
6111
6596
|
return `${changed.join(", ")} set`;
|
|
@@ -6113,11 +6598,12 @@ async function runSync(input) {
|
|
|
6113
6598
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
6114
6599
|
}
|
|
6115
6600
|
if (stepName === "turbo") {
|
|
6116
|
-
const
|
|
6601
|
+
const astromechTurbo = createAstromech({
|
|
6117
6602
|
cwd: input.context.repoRoot,
|
|
6118
6603
|
config: { tasks: config.tasks },
|
|
6119
6604
|
logger
|
|
6120
|
-
})
|
|
6605
|
+
});
|
|
6606
|
+
const content = astromechTurbo.turboConfig();
|
|
6121
6607
|
if (content === null) {
|
|
6122
6608
|
steps.push({
|
|
6123
6609
|
capability: "local",
|
|
@@ -6131,6 +6617,26 @@ async function runSync(input) {
|
|
|
6131
6617
|
await writeFile(join(input.context.repoRoot, "turbo.json"), content, "utf8");
|
|
6132
6618
|
}));
|
|
6133
6619
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
6620
|
+
const rootPkgRaw = await readFile(join(input.context.repoRoot, "package.json"), "utf8").catch(() => null);
|
|
6621
|
+
const rootPkg = rootPkgRaw ? JSON.parse(rootPkgRaw) : null;
|
|
6622
|
+
const workspaceYaml = await readFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), "utf8").catch(() => "");
|
|
6623
|
+
const rootScripts = Object.keys(rootPkg?.scripts ?? {});
|
|
6624
|
+
const workspaceFix = astromechTurbo.ensureRootWorkspaceMember(workspaceYaml, rootScripts);
|
|
6625
|
+
if (workspaceFix.changed) {
|
|
6626
|
+
steps.push(await runSyncStep("local", "fix pnpm-workspace.yaml (root workspace member, #692)", dryRun, async () => {
|
|
6627
|
+
await writeFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), workspaceFix.content, "utf8");
|
|
6628
|
+
}));
|
|
6629
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6630
|
+
}
|
|
6631
|
+
if (rootPkgRaw) {
|
|
6632
|
+
const depFix = astromechTurbo.ensureTurboDependency(rootPkgRaw, workspaceFix.content);
|
|
6633
|
+
if (depFix.changed) {
|
|
6634
|
+
steps.push(await runSyncStep("local", "add turbo devDependency", dryRun, async () => {
|
|
6635
|
+
await writeFile(join(input.context.repoRoot, "package.json"), depFix.content, "utf8");
|
|
6636
|
+
}));
|
|
6637
|
+
print(formatSyncStep(steps[steps.length - 1]));
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6134
6640
|
}
|
|
6135
6641
|
}
|
|
6136
6642
|
if (stepName === "wiki") {
|
|
@@ -6214,8 +6720,21 @@ async function writePackageJsonField(repoRoot, field, value) {
|
|
|
6214
6720
|
* Merge `desired` script entries into `package.json#scripts` — sets only the
|
|
6215
6721
|
* listed keys, never touches the rest. Returns the sorted list of keys that
|
|
6216
6722
|
* drifted (empty when all current), or `null` when there is no `package.json`.
|
|
6723
|
+
*
|
|
6724
|
+
* `turboEligible` (#747): task names `turbo.json` fans out — the same set
|
|
6725
|
+
* {@link EnsureRootWorkspaceMemberResult} above already keys off. For those
|
|
6726
|
+
* only, an *existing* script that isn't the generated `holocron run <task>
|
|
6727
|
+
* --` wrapper is left untouched: root may genuinely be a directly-buildable
|
|
6728
|
+
* package (not an orchestrator) with its own real command (`tsc --noEmit`,
|
|
6729
|
+
* `vitest run`, …) for that task. Overwriting it would silently discard the
|
|
6730
|
+
* real command and, once `turbo.json` exists and root is a real workspace
|
|
6731
|
+
* member (#692), reintroduce infinite recursion — `turbo run <task>` →
|
|
6732
|
+
* root's own `<task>` script → the wrapper → `holocron run <task>` sees
|
|
6733
|
+
* `turbo.json` still defines the task → `turbo run <task>` again, forever.
|
|
6734
|
+
* Every other key (non-turbo tasks, `holocron`, `prepare`, …) has no
|
|
6735
|
+
* legitimate direct-command alternative and stays fully generator-owned.
|
|
6217
6736
|
*/
|
|
6218
|
-
async function mergePackageJsonScripts(repoRoot, desired) {
|
|
6737
|
+
async function mergePackageJsonScripts(repoRoot, desired, turboEligible) {
|
|
6219
6738
|
const pkgPath = join(repoRoot, "package.json");
|
|
6220
6739
|
let content;
|
|
6221
6740
|
try {
|
|
@@ -6226,15 +6745,31 @@ async function mergePackageJsonScripts(repoRoot, desired) {
|
|
|
6226
6745
|
const pkg = JSON.parse(content);
|
|
6227
6746
|
const scripts = pkg.scripts ?? {};
|
|
6228
6747
|
const changed = [];
|
|
6229
|
-
for (const [name, command] of Object.entries(desired))
|
|
6230
|
-
scripts[name]
|
|
6231
|
-
|
|
6748
|
+
for (const [name, command] of Object.entries(desired)) {
|
|
6749
|
+
const existing = scripts[name];
|
|
6750
|
+
if (turboEligible.has(name) && existing !== void 0 && !isGeneratedRunWrapper(existing, name)) continue;
|
|
6751
|
+
if (existing !== command) {
|
|
6752
|
+
scripts[name] = command;
|
|
6753
|
+
changed.push(name);
|
|
6754
|
+
}
|
|
6232
6755
|
}
|
|
6233
6756
|
if (changed.length === 0) return [];
|
|
6234
6757
|
pkg.scripts = scripts;
|
|
6235
6758
|
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
6236
6759
|
return changed.sort();
|
|
6237
6760
|
}
|
|
6761
|
+
/**
|
|
6762
|
+
* Matches the exact shape `packageScripts()` generates for `taskName` —
|
|
6763
|
+
* `<bin> run <taskName> --`, `<bin>` normally `holocron` but substitutable
|
|
6764
|
+
* (`holocronScript`, itself allowed to be multi-word, e.g. `"node
|
|
6765
|
+
packages/cli/dist/cli.mjs"` — see its own test). Checking the *suffix*
|
|
6766
|
+
* rather than the whole string means an old wrapper survives a
|
|
6767
|
+
* `holocronScript` rename intact — it's still recognized as a wrapper (safe
|
|
6768
|
+
* to update to the new one), not mistaken for a real command.
|
|
6769
|
+
*/
|
|
6770
|
+
function isGeneratedRunWrapper(command, taskName) {
|
|
6771
|
+
return command.endsWith(` run ${taskName} --`);
|
|
6772
|
+
}
|
|
6238
6773
|
const README_DESC_START = "<!-- holocron:description -->";
|
|
6239
6774
|
const README_DESC_END = "<!-- /holocron:description -->";
|
|
6240
6775
|
async function updateReadmeDescription(repoRoot, description) {
|
|
@@ -7908,7 +8443,7 @@ try {
|
|
|
7908
8443
|
})).summary.fail > 0) process.exitCode = 1;
|
|
7909
8444
|
}).command("deploy [branch]", "Trigger a deployment via the configured `deployment` capability", (y) => y.positional("branch", {
|
|
7910
8445
|
type: "string",
|
|
7911
|
-
describe: "Git branch to deploy"
|
|
8446
|
+
describe: "Git branch to deploy (omit when using --files)"
|
|
7912
8447
|
}).option("project-id", {
|
|
7913
8448
|
type: "string",
|
|
7914
8449
|
demandOption: true,
|
|
@@ -7917,20 +8452,34 @@ try {
|
|
|
7917
8452
|
type: "string",
|
|
7918
8453
|
choices: ["production", "staging"],
|
|
7919
8454
|
describe: "Named environment to deploy into. Omit for a branch preview."
|
|
7920
|
-
}),
|
|
8455
|
+
}).option("files", {
|
|
8456
|
+
type: "string",
|
|
8457
|
+
describe: "Deploy from a local directory of source files instead of a Git branch — calls deployFunction(), no linked repo required. Conflicts with `branch`."
|
|
8458
|
+
}).conflicts("files", "branch"), async (argv) => {
|
|
7921
8459
|
const tokens = tokenContext(argv.token);
|
|
7922
8460
|
if (!tokens) return;
|
|
7923
|
-
const [branch] = await promptForPositionals(getEntry("deploy"), argv);
|
|
7924
8461
|
const loaded = await loadConfig(argv.cwd);
|
|
7925
8462
|
applyResolvedConfig(argv, loaded.resolved);
|
|
8463
|
+
const context = {
|
|
8464
|
+
repoRoot: argv.cwd,
|
|
8465
|
+
dryRun: argv.dryRun,
|
|
8466
|
+
...tokens,
|
|
8467
|
+
org: resolveOrg(argv, loaded.resolved)
|
|
8468
|
+
};
|
|
8469
|
+
if (argv.files) {
|
|
8470
|
+
if ((await runDeployFromFiles({
|
|
8471
|
+
loaded,
|
|
8472
|
+
context,
|
|
8473
|
+
projectId: argv.projectId,
|
|
8474
|
+
dir: argv.files,
|
|
8475
|
+
...argv.target ? { target: argv.target } : {}
|
|
8476
|
+
})).status === "fail") process.exitCode = 1;
|
|
8477
|
+
return;
|
|
8478
|
+
}
|
|
8479
|
+
const [branch] = await promptForPositionals(getEntry("deploy"), argv);
|
|
7926
8480
|
if ((await runDeploy({
|
|
7927
8481
|
loaded,
|
|
7928
|
-
context
|
|
7929
|
-
repoRoot: argv.cwd,
|
|
7930
|
-
dryRun: argv.dryRun,
|
|
7931
|
-
...tokens,
|
|
7932
|
-
org: resolveOrg(argv, loaded.resolved)
|
|
7933
|
-
},
|
|
8482
|
+
context,
|
|
7934
8483
|
projectId: argv.projectId,
|
|
7935
8484
|
branch,
|
|
7936
8485
|
...argv.target ? { target: argv.target } : {}
|
|
@@ -7976,11 +8525,10 @@ try {
|
|
|
7976
8525
|
}).command("publish", "Publish @theholocron/* packages to npm", (y) => y.option("initial", {
|
|
7977
8526
|
type: "boolean",
|
|
7978
8527
|
default: false,
|
|
7979
|
-
describe: "One-shot bootstrap publish for trusted-publishing-eligible packages (npm needs the package to exist before Trusted Publishing can be configured for it).
|
|
8528
|
+
describe: "One-shot bootstrap publish for trusted-publishing-eligible packages (npm needs the package to exist before Trusted Publishing can be configured for it). Skips the steady-state OIDC path below — use only for a package's very first publish."
|
|
7980
8529
|
}).option("tag", {
|
|
7981
8530
|
type: "string",
|
|
7982
|
-
|
|
7983
|
-
describe: "npm distribution tag (defaults to alpha)"
|
|
8531
|
+
describe: "npm distribution tag. Defaults to alpha for --initial, latest for a steady-state publish."
|
|
7984
8532
|
}).option("otp", {
|
|
7985
8533
|
type: "string",
|
|
7986
8534
|
describe: "One-time password from your authenticator (required if npm needs 2FA for writes)"
|
|
@@ -7990,6 +8538,14 @@ try {
|
|
|
7990
8538
|
}).option("repo", {
|
|
7991
8539
|
type: "string",
|
|
7992
8540
|
describe: "owner/repo for Trusted Publisher config (--sync-trust). Defaults to theholocron/holocron."
|
|
8541
|
+
}).option("provenance", {
|
|
8542
|
+
type: "boolean",
|
|
8543
|
+
default: true,
|
|
8544
|
+
describe: "Publish with --provenance (steady-state only). Default on — free under CI/OIDC."
|
|
8545
|
+
}).option("skip-already-published", {
|
|
8546
|
+
type: "boolean",
|
|
8547
|
+
default: false,
|
|
8548
|
+
describe: "Steady-state only: check `npm view <pkg>@<version>` before publishing each package and skip it if that exact version already exists, instead of one bulk publish. For repos that ship many independently-versioned packages where a release doesn't bump every one."
|
|
7993
8549
|
}), async (argv) => {
|
|
7994
8550
|
if (argv.syncTrust) {
|
|
7995
8551
|
const [oldFile, newFile] = argv.syncTrust;
|
|
@@ -8007,15 +8563,21 @@ try {
|
|
|
8007
8563
|
})).status === "fail") process.exitCode = 1;
|
|
8008
8564
|
return;
|
|
8009
8565
|
}
|
|
8010
|
-
if (
|
|
8011
|
-
|
|
8012
|
-
|
|
8566
|
+
if (argv.initial) {
|
|
8567
|
+
if ((await runPublish({
|
|
8568
|
+
cwd: argv.cwd,
|
|
8569
|
+
dryRun: argv.dryRun,
|
|
8570
|
+
...argv.tag ? { tag: argv.tag } : {},
|
|
8571
|
+
...argv.otp ? { otp: argv.otp } : {}
|
|
8572
|
+
})).status === "fail") process.exitCode = 1;
|
|
8013
8573
|
return;
|
|
8014
8574
|
}
|
|
8015
|
-
if ((await
|
|
8575
|
+
if ((await runSteadyPublish({
|
|
8016
8576
|
cwd: argv.cwd,
|
|
8017
|
-
tag: argv.tag,
|
|
8018
8577
|
dryRun: argv.dryRun,
|
|
8578
|
+
provenance: argv.provenance,
|
|
8579
|
+
skipAlreadyPublished: argv.skipAlreadyPublished,
|
|
8580
|
+
...argv.tag ? { tag: argv.tag } : {},
|
|
8019
8581
|
...argv.otp ? { otp: argv.otp } : {}
|
|
8020
8582
|
})).status === "fail") process.exitCode = 1;
|
|
8021
8583
|
}).command("sync [steps..]", "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)", (y) => y.positional("steps", {
|