@theholocron/cli 4.19.0 → 5.0.0-alpha.10

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/dist/cli.mjs CHANGED
@@ -2154,12 +2154,11 @@ function render$10(inputs) {
2154
2154
  main: "./src/index.ts",
2155
2155
  exports: { ".": "./src/index.ts" },
2156
2156
  scripts: {
2157
- build: "tsdown",
2158
- lint: "eslint .",
2159
- typecheck: "tsc --noEmit",
2160
- test: "vitest run",
2157
+ "delivery.build": "tsdown",
2158
+ "sourceQuality.staticAnalysis": "holocron run sourceQuality.staticAnalysis --",
2159
+ "verification.typeSafety": "holocron run verification.typeSafety --",
2160
+ "verification.unitTests": "holocron run verification.unitTests --",
2161
2161
  "test:watch": "vitest",
2162
- "test:coverage": "vitest run --coverage",
2163
2162
  validate: "tsx scripts/validate.mjs"
2164
2163
  },
2165
2164
  peerDependencies: { "@theholocron/cli": "workspace:*" },
@@ -2879,7 +2878,7 @@ function runPluginCreate(input) {
2879
2878
  execFn("pnpm", [
2880
2879
  "--filter",
2881
2880
  pkg,
2882
- "typecheck"
2881
+ "verification.typeSafety"
2883
2882
  ], {
2884
2883
  cwd,
2885
2884
  stdio: "inherit"
@@ -2887,7 +2886,7 @@ function runPluginCreate(input) {
2887
2886
  execFn("pnpm", [
2888
2887
  "--filter",
2889
2888
  pkg,
2890
- "lint"
2889
+ "sourceQuality.staticAnalysis"
2891
2890
  ], {
2892
2891
  cwd,
2893
2892
  stdio: "inherit"
@@ -2895,7 +2894,7 @@ function runPluginCreate(input) {
2895
2894
  execFn("pnpm", [
2896
2895
  "--filter",
2897
2896
  pkg,
2898
- "test"
2897
+ "verification.unitTests"
2899
2898
  ], {
2900
2899
  cwd,
2901
2900
  stdio: "inherit"
@@ -3018,6 +3017,193 @@ function printNextSteps$1(print, inputs) {
3018
3017
  * "revoke this token at <url>" reminder — same pattern as `rando vc
3019
3018
  * setup` for the ephemeral GH admin PAT.
3020
3019
  */
3020
+ /**
3021
+ * `holocron publish --sync-trust <old-file> <new-file>` — bulk-migrates npm
3022
+ * Trusted Publisher (OIDC) config to a new workflow filename across every
3023
+ * public package. See issue #689 / `scripts/npm-trust-migrate.sh` (the
3024
+ * shell-script prototype this supersedes) for the full "why": npm ties
3025
+ * Trusted Publisher to a workflow FILENAME per package, so renaming the
3026
+ * release workflow breaks OIDC publish (`ENEEDAUTH`) until every package's
3027
+ * config is updated to match.
3028
+ *
3029
+ * Requires npm@11.15.0+ (`npm trust`) — always shells out through
3030
+ * `npx -y npm@latest` regardless of the locally-installed npm version.
3031
+ *
3032
+ * npm's `trust` mutations — and reads, once a short-lived OTP grant lapses —
3033
+ * require a fresh interactive browser 2FA approval (an `EOTP` error with an
3034
+ * `authUrl`). No token bypasses this (npm's own docs: Granular Access Tokens
3035
+ * with the bypass-2FA option are explicitly unsupported for trust commands).
3036
+ * This command can't drive that browser step itself, so on the first `EOTP`
3037
+ * it stops immediately, prints the `authUrl`, and returns — the operator
3038
+ * approves it and re-runs; already-migrated packages are skipped on the next
3039
+ * pass, so it's safe to resume from wherever it stopped.
3040
+ */
3041
+ async function runSyncTrust(input) {
3042
+ const print = input.print ?? ((line) => console.log(line));
3043
+ const logger = input.logger ?? getLogger();
3044
+ const cwd = input.cwd ?? process.cwd();
3045
+ const repo = input.repo ?? "theholocron/holocron";
3046
+ const dryRun = input.dryRun ?? false;
3047
+ const exec = input.exec ?? defaultExec$1;
3048
+ const packageNames = input.packages ?? discoverPublicPackages(cwd);
3049
+ const npm = "npx";
3050
+ const npmArgs = ["-y", "npm@latest"];
3051
+ print(`Holocron publish --sync-trust${dryRun ? " (dry-run)" : ""}`);
3052
+ print(` ${input.oldFile} -> ${input.newFile} (repo: ${repo})`);
3053
+ print(` ${packageNames.length} public packages`);
3054
+ print("");
3055
+ logger.info({
3056
+ oldFile: input.oldFile,
3057
+ newFile: input.newFile,
3058
+ packages: packageNames.length
3059
+ }, "sync-trust: start");
3060
+ if (packageNames.length === 0) {
3061
+ const message = "nothing to sync (no packages/* and root package.json is private or unnamed)";
3062
+ print(` ✗ ${message}`);
3063
+ return {
3064
+ status: "fail",
3065
+ message,
3066
+ packages: []
3067
+ };
3068
+ }
3069
+ if (dryRun) {
3070
+ for (const name of packageNames) print(` … would sync ${name}`);
3071
+ return {
3072
+ status: "dry-run",
3073
+ message: "dry-run — no npm calls made",
3074
+ packages: []
3075
+ };
3076
+ }
3077
+ const results = [];
3078
+ for (const name of packageNames) {
3079
+ print(`=== ${name} ===`);
3080
+ const parsed = parseJson((await exec(npm, [
3081
+ ...npmArgs,
3082
+ "trust",
3083
+ "list",
3084
+ name,
3085
+ "--json"
3086
+ ], { cwd })).stdout);
3087
+ if (isEotp(parsed)) {
3088
+ const authUrl = parsed.error?.authUrl ?? "";
3089
+ print(` needs fresh 2FA — open and approve: ${authUrl}`);
3090
+ print(" then re-run; already-migrated packages are skipped.");
3091
+ results.push({
3092
+ name,
3093
+ status: "needs-auth",
3094
+ message: authUrl
3095
+ });
3096
+ logger.warn({
3097
+ package: name,
3098
+ authUrl
3099
+ }, "sync-trust: needs auth, stopping");
3100
+ return {
3101
+ status: "fail",
3102
+ message: "npm 2FA grant needed — see printed authUrl",
3103
+ packages: results
3104
+ };
3105
+ }
3106
+ const entry = parsed;
3107
+ if (entry.file === input.newFile) {
3108
+ print(` already on ${input.newFile}, skipping`);
3109
+ results.push({
3110
+ name,
3111
+ status: "skipped"
3112
+ });
3113
+ continue;
3114
+ }
3115
+ if (entry.id) {
3116
+ print(` revoking existing trust (file=${entry.file ?? "?"}, id=${entry.id})`);
3117
+ const revoke = await exec(npm, [
3118
+ ...npmArgs,
3119
+ "trust",
3120
+ "revoke",
3121
+ name,
3122
+ "--id",
3123
+ entry.id,
3124
+ "-y"
3125
+ ], { cwd });
3126
+ if (revoke.exitCode !== 0 && isEotp(parseJson(revoke.stdout))) {
3127
+ const authUrl = parseJson(revoke.stdout).error?.authUrl ?? "";
3128
+ print(` needs fresh 2FA — open and approve: ${authUrl}`);
3129
+ results.push({
3130
+ name,
3131
+ status: "needs-auth",
3132
+ message: authUrl
3133
+ });
3134
+ return {
3135
+ status: "fail",
3136
+ message: "npm 2FA grant needed — see printed authUrl",
3137
+ packages: results
3138
+ };
3139
+ }
3140
+ } else print(" no existing trust config found");
3141
+ print(` creating trust: ${input.newFile}`);
3142
+ const create = await exec(npm, [
3143
+ ...npmArgs,
3144
+ "trust",
3145
+ "github",
3146
+ name,
3147
+ "--repo",
3148
+ repo,
3149
+ "--file",
3150
+ input.newFile,
3151
+ "--allow-publish",
3152
+ "--allow-stage-publish",
3153
+ "-y"
3154
+ ], { cwd });
3155
+ if (create.exitCode === 0) {
3156
+ print(" ✓ ok");
3157
+ results.push({
3158
+ name,
3159
+ status: "ok"
3160
+ });
3161
+ } else {
3162
+ const createParsed = parseJson(create.stdout);
3163
+ if (isEotp(createParsed)) {
3164
+ const authUrl = createParsed.error?.authUrl ?? "";
3165
+ print(` needs fresh 2FA — open and approve: ${authUrl}`);
3166
+ results.push({
3167
+ name,
3168
+ status: "needs-auth",
3169
+ message: authUrl
3170
+ });
3171
+ return {
3172
+ status: "fail",
3173
+ message: "npm 2FA grant needed — see printed authUrl",
3174
+ packages: results
3175
+ };
3176
+ }
3177
+ const message = create.stderr.trim() || create.stdout.trim() || `exit ${create.exitCode}`;
3178
+ print(` ✗ FAILED: ${message}`);
3179
+ results.push({
3180
+ name,
3181
+ status: "fail",
3182
+ message
3183
+ });
3184
+ }
3185
+ }
3186
+ const failed = results.filter((r) => r.status === "fail");
3187
+ logger.info({
3188
+ ok: results.filter((r) => r.status === "ok").length,
3189
+ failed: failed.length
3190
+ }, "sync-trust: done");
3191
+ return {
3192
+ status: failed.length > 0 ? "fail" : "ok",
3193
+ ...failed.length > 0 ? { message: `${failed.length} package(s) failed` } : {},
3194
+ packages: results
3195
+ };
3196
+ }
3197
+ function parseJson(raw) {
3198
+ try {
3199
+ return JSON.parse(raw);
3200
+ } catch {
3201
+ return;
3202
+ }
3203
+ }
3204
+ function isEotp(parsed) {
3205
+ return typeof parsed === "object" && parsed !== null && "error" in parsed && parsed.error?.code === "EOTP";
3206
+ }
3021
3207
  async function runPublish(input = {}) {
3022
3208
  const print = input.print ?? ((line) => console.log(line));
3023
3209
  const logger = input.logger ?? getLogger();
@@ -3138,6 +3324,170 @@ async function runPublish(input = {}) {
3138
3324
  packageNames
3139
3325
  };
3140
3326
  }
3327
+ /**
3328
+ * `holocron publish` (no `--initial`) — the steady-state publish
3329
+ * `exec.publishCmd` in `release.config.ts` should call, replacing each
3330
+ * repo's hand-typed `pnpm -r --filter=./packages/* publish ...` shell (which
3331
+ * differs, today, only in monorepo-vs-single-package, provenance on/off, and
3332
+ * whether a repo needs to skip already-published packages) with one uniform
3333
+ * invocation. Part of the config-resolution workstream, #676 — see
3334
+ * `.notes/tech-config-resolution.spec.md`.
3335
+ *
3336
+ * Deliberately does not do the `--initial` bootstrap dance (npm
3337
+ * login-if-needed, Trusted Publisher next-steps printout) — a steady-state
3338
+ * publish runs in CI, authenticated via OIDC automatically, no interactive
3339
+ * step involved. Reuses `hasPackagesDir()`'s monorepo-vs-single-package
3340
+ * detection rather than duplicating it.
3341
+ */
3342
+ async function runSteadyPublish(input = {}) {
3343
+ const print = input.print ?? ((line) => console.log(line));
3344
+ const logger = input.logger ?? getLogger();
3345
+ const cwd = input.cwd ?? process.cwd();
3346
+ const tag = input.tag ?? "latest";
3347
+ const dryRun = input.dryRun ?? false;
3348
+ const otp = input.otp;
3349
+ const exec = input.exec ?? defaultExec$1;
3350
+ const provenance = input.provenance ?? true;
3351
+ const skipAlreadyPublished = input.skipAlreadyPublished ?? false;
3352
+ const entries = input.packages ?? discoverPublicPackageEntries(cwd);
3353
+ const packageNames = entries.map((e) => e.name);
3354
+ print(`Holocron publish${dryRun ? " (dry-run)" : ""}`);
3355
+ print(` cwd: ${cwd}`);
3356
+ print(` tag: ${tag}`);
3357
+ print(` provenance: ${provenance}`);
3358
+ if (skipAlreadyPublished) print(" skip-already-published: true");
3359
+ logger.info({
3360
+ tag,
3361
+ provenance,
3362
+ skipAlreadyPublished,
3363
+ dryRun: dryRun || void 0
3364
+ }, "publish: start");
3365
+ if (otp) print(` otp: <${otp.length} chars>`);
3366
+ print("");
3367
+ if (entries.length === 0) {
3368
+ const message = "nothing to publish (no packages/* and root package.json is private or unnamed)";
3369
+ print(` ✗ ${message}`);
3370
+ logger.warn({ reason: message }, "publish: done");
3371
+ return {
3372
+ status: "fail",
3373
+ message,
3374
+ packageNames
3375
+ };
3376
+ }
3377
+ const baseFlags = [
3378
+ "--access",
3379
+ "public",
3380
+ "--no-git-checks",
3381
+ "--tag",
3382
+ tag,
3383
+ ...provenance ? ["--provenance"] : []
3384
+ ];
3385
+ const otpFlags = otp ? ["--otp", otp] : [];
3386
+ if (dryRun) {
3387
+ print("");
3388
+ if (skipAlreadyPublished) for (const entry of entries) print(` … would check ${entry.name}@${entry.version}, publish if not already on npm`);
3389
+ else {
3390
+ const args = [
3391
+ ...hasPackagesDir(cwd) ? ["-r", "--filter=./packages/*"] : [],
3392
+ "publish",
3393
+ ...baseFlags,
3394
+ ...otpFlags
3395
+ ];
3396
+ print(` … (dry-run) skipping actual publish`);
3397
+ print(` would run: pnpm ${args.join(" ")}`);
3398
+ }
3399
+ logger.info({
3400
+ tag,
3401
+ status: "dry-run",
3402
+ packages: entries.length
3403
+ }, "publish: done");
3404
+ return {
3405
+ status: "dry-run",
3406
+ message: "dry-run — no publish executed",
3407
+ packageNames
3408
+ };
3409
+ }
3410
+ if (skipAlreadyPublished) {
3411
+ print(" → publishing packages not already on npm…");
3412
+ for (const entry of entries) {
3413
+ const view = await exec("npm", [
3414
+ "view",
3415
+ `${entry.name}@${entry.version}`,
3416
+ "version"
3417
+ ], { cwd });
3418
+ if (view.exitCode === 0 && view.stdout.trim()) {
3419
+ print(` skip ${entry.name}@${entry.version} (already published)`);
3420
+ continue;
3421
+ }
3422
+ const publish = await exec("pnpm", [
3423
+ "--filter",
3424
+ entry.dir,
3425
+ "publish",
3426
+ ...baseFlags,
3427
+ ...otpFlags
3428
+ ], { cwd });
3429
+ if (publish.exitCode !== 0) {
3430
+ const message = `publish failed for ${entry.name}@${entry.version} (exit ${publish.exitCode}): ${publish.stderr.trim() || publish.stdout.trim() || "no output"}`;
3431
+ print(` ✗ ${message}`);
3432
+ logger.warn({
3433
+ tag,
3434
+ package: entry.name,
3435
+ reason: message
3436
+ }, "publish: done");
3437
+ return {
3438
+ status: "fail",
3439
+ message,
3440
+ packageNames
3441
+ };
3442
+ }
3443
+ print(` ✓ ${entry.name}@${entry.version}`);
3444
+ }
3445
+ logger.info({
3446
+ tag,
3447
+ status: "ok",
3448
+ packages: entries.length
3449
+ }, "publish: done");
3450
+ return {
3451
+ status: "ok",
3452
+ packageNames
3453
+ };
3454
+ }
3455
+ print(" → publishing all public @theholocron/* packages…");
3456
+ const publish = await exec("pnpm", [
3457
+ ...hasPackagesDir(cwd) ? ["-r", "--filter=./packages/*"] : [],
3458
+ "publish",
3459
+ ...baseFlags,
3460
+ ...otpFlags
3461
+ ], { cwd });
3462
+ if (publish.exitCode !== 0) {
3463
+ const message = `publish failed (exit ${publish.exitCode}): ${publish.stderr.trim() || publish.stdout.trim() || "no output"}`;
3464
+ print(` ✗ ${message}`);
3465
+ if (publish.stdout.includes("EOTP") || publish.stderr.includes("EOTP")) {
3466
+ print("");
3467
+ print(" → hint: your npm account requires 2FA for writes. Re-run with `--otp <code>`:");
3468
+ print(` pnpm exec holocron publish --otp <6-digit-code>`);
3469
+ }
3470
+ logger.warn({
3471
+ tag,
3472
+ reason: message
3473
+ }, "publish: done");
3474
+ return {
3475
+ status: "fail",
3476
+ message,
3477
+ packageNames
3478
+ };
3479
+ }
3480
+ print(" ✓ publish complete");
3481
+ logger.info({
3482
+ tag,
3483
+ status: "ok",
3484
+ packages: entries.length
3485
+ }, "publish: done");
3486
+ return {
3487
+ status: "ok",
3488
+ packageNames
3489
+ };
3490
+ }
3141
3491
  function hasPackagesDir(cwd) {
3142
3492
  return existsSync(join(cwd, "packages"));
3143
3493
  }
@@ -3151,6 +3501,37 @@ function publicPackageName(pkgPath) {
3151
3501
  return;
3152
3502
  }
3153
3503
  }
3504
+ /** Read a `package.json`'s `name` + `version`, when it's public (`!private && name`). */
3505
+ function publicPackageEntry(pkgPath, dir) {
3506
+ if (!existsSync(pkgPath)) return void 0;
3507
+ try {
3508
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
3509
+ return !pkg.private && pkg.name && pkg.version ? {
3510
+ name: pkg.name,
3511
+ version: pkg.version,
3512
+ dir
3513
+ } : void 0;
3514
+ } catch {
3515
+ return;
3516
+ }
3517
+ }
3518
+ /**
3519
+ * Same repo-layout detection as {@link discoverPublicPackages}, but carrying
3520
+ * version + a `pnpm --filter`-ready path — {@link runSteadyPublish}'s
3521
+ * `skipAlreadyPublished` mode needs both to check `npm view <pkg>@<version>`
3522
+ * and publish one workspace at a time.
3523
+ */
3524
+ function discoverPublicPackageEntries(cwd) {
3525
+ if (!hasPackagesDir(cwd)) {
3526
+ const entry = publicPackageEntry(join(cwd, "package.json"), ".");
3527
+ return entry ? [entry] : [];
3528
+ }
3529
+ const packagesDir = join(cwd, "packages");
3530
+ return readdirSync(packagesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).flatMap((e) => {
3531
+ const entry = publicPackageEntry(join(packagesDir, e.name, "package.json"), `./packages/${e.name}`);
3532
+ return entry ? [entry] : [];
3533
+ });
3534
+ }
3154
3535
  /**
3155
3536
  * Monorepo (`packages/` present): every public `packages/*` workspace.
3156
3537
  * Single package (no `packages/`): the root `package.json`, if it's public —
@@ -3180,7 +3561,7 @@ function printNextSteps(print, env, packageNames, repoName) {
3180
3561
  print("");
3181
3562
  print(" → next: configure Trusted Publisher for each package on npm:");
3182
3563
  for (const name of packageNames) print(` https://www.npmjs.com/package/${name}/access`);
3183
- print(` Publisher: GitHub Actions Org: theholocron Repo: ${repoName} Workflow: release.yml`);
3564
+ print(` Publisher: GitHub Actions Org: theholocron Repo: ${repoName} Workflow: delivery.publish.yml`);
3184
3565
  if (env.get("NPM_TOKEN")) {
3185
3566
  print("");
3186
3567
  print(" → cleanup: $NPM_TOKEN was used. Revoke it now (no API for self-revoke; UI-only):");
@@ -3563,12 +3944,21 @@ var alexrc_default = { allow: [
3563
3944
  ] };
3564
3945
  //#endregion
3565
3946
  //#region src/templates/configs/alexjs/create-config.ts
3566
- const { workflowHeader: workflowHeader$8 } = createHeader({ source: "packages/cli/src/templates/configs/alexjs/create-config.ts" });
3947
+ const { workflowHeader: workflowHeader$9 } = createHeader({ source: "packages/cli/src/templates/configs/alexjs/create-config.ts" });
3567
3948
  function createRcConfig() {
3568
3949
  return JSON.stringify(alexrc_default, null, 2) + "\n";
3569
3950
  }
3570
3951
  function createIgnoreConfig$1() {
3571
- return `${workflowHeader$8()}${alexignore_default}`;
3952
+ return `${workflowHeader$9()}${alexignore_default}`;
3953
+ }
3954
+ //#endregion
3955
+ //#region src/templates/configs/commit-msg/commit-msg
3956
+ 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";
3957
+ //#endregion
3958
+ //#region src/templates/configs/commit-msg/create-config.ts
3959
+ const { workflowHeader: workflowHeader$8 } = createHeader({ source: "packages/cli/src/templates/configs/commit-msg/create-config.ts" });
3960
+ function createConfig$7() {
3961
+ return `${workflowHeader$8("shebang")}${commit_msg_default}`;
3572
3962
  }
3573
3963
  //#endregion
3574
3964
  //#region src/templates/configs/devmoji/create-config.ts
@@ -3673,7 +4063,7 @@ function createConfig$2(holocronScript = "pnpm exec holocron") {
3673
4063
  }
3674
4064
  //#endregion
3675
4065
  //#region src/templates/configs/prepare-commit-msg/prepare-commit-msg
3676
- 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\nnpx devmoji -e\n";
4066
+ 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";
3677
4067
  //#endregion
3678
4068
  //#region src/templates/configs/prepare-commit-msg/create-config.ts
3679
4069
  const { workflowHeader: workflowHeader$3 } = createHeader({ source: "packages/cli/src/templates/configs/prepare-commit-msg/create-config.ts" });
@@ -4518,6 +4908,10 @@ const { workflowHeader: workflowHeader$1 } = createHeader({
4518
4908
  source: "packages/cli/src/commands/setup/run-setup.ts",
4519
4909
  tool: "holocron setup"
4520
4910
  });
4911
+ const { workflowHeader: thinCallerHeader } = createHeader({
4912
+ source: "packages/astromech/src/thin-callers.ts",
4913
+ tool: "holocron setup"
4914
+ });
4521
4915
  async function runSetup(input) {
4522
4916
  const print = input.print ?? ((line) => console.log(line));
4523
4917
  const logger = getLogger();
@@ -4552,7 +4946,7 @@ async function runSetup(input) {
4552
4946
  }, { skipCodes: SECURITY_SKIP_CODES[method] }));
4553
4947
  print(formatStep(steps[steps.length - 1]));
4554
4948
  }
4555
- const usesAdvancedCodeQL = (config.tasks ?? []).map((e) => typeof e === "string" ? e : e.name).includes("codeql");
4949
+ const usesAdvancedCodeQL = (config.tasks ?? []).map((e) => typeof e === "string" ? e : e.name).includes("security.codeScanning");
4556
4950
  steps.push(await runStep("source", usesAdvancedCodeQL ? "disableDefaultCodeScanning" : "enableCodeScanning", dryRun, async () => {
4557
4951
  if (usesAdvancedCodeQL) await source.disableDefaultCodeScanning();
4558
4952
  else return await source.enableCodeScanning();
@@ -4589,7 +4983,8 @@ async function runSetup(input) {
4589
4983
  }).thinCallers();
4590
4984
  for (const entry of tasks) {
4591
4985
  const name = typeof entry === "string" ? entry : entry.name;
4592
- if (!KNOWN_WORKFLOWS.has(name)) {
4986
+ const isCombinedContentOnly = name === "knowledge.docs" || name === "knowledge.components";
4987
+ if (!KNOWN_WORKFLOWS.has(name) && !isCombinedContentOnly) {
4593
4988
  steps.push({
4594
4989
  capability: "source",
4595
4990
  step: `write workflow ${name}`,
@@ -4599,12 +4994,12 @@ async function runSetup(input) {
4599
4994
  print(formatStep(steps[steps.length - 1]));
4600
4995
  continue;
4601
4996
  }
4602
- const filename = name === "deploy" ? "deploy.yml" : `${name}.yml`;
4997
+ const filename = `${name}.yml`;
4603
4998
  const content = files.get(filename);
4604
4999
  if (content === void 0) continue;
4605
- const step = name === "deploy" && content.includes("workflows/preview.yml@main") ? "write workflow deploy (with preview)" : `write workflow ${name}`;
5000
+ const step = content.includes("workflows/preview.yml@main") ? `write workflow ${name} (with preview)` : `write workflow ${name}`;
4606
5001
  steps.push(await runStep("source", step, dryRun, async () => {
4607
- await source.writeWorkflowFile(filename, `${workflowHeader$1()}${content}`);
5002
+ await source.writeWorkflowFile(filename, `${thinCallerHeader()}${content}`);
4608
5003
  }));
4609
5004
  print(formatStep(steps[steps.length - 1]));
4610
5005
  }
@@ -4665,6 +5060,10 @@ async function runSetup(input) {
4665
5060
  await source.writeRepoFile(".husky/prepare-commit-msg", createConfig$1());
4666
5061
  }));
4667
5062
  print(formatStep(steps[steps.length - 1]));
5063
+ steps.push(await runStep("source", "write .husky/commit-msg", dryRun, async () => {
5064
+ await source.writeRepoFile(".husky/commit-msg", createConfig$7());
5065
+ }));
5066
+ print(formatStep(steps[steps.length - 1]));
4668
5067
  if (hooksEnabled) {
4669
5068
  steps.push(await runStep("source", "write .husky/pre-push", dryRun, async () => {
4670
5069
  await source.writeRepoFile(".husky/pre-push", createConfig$2(config.holocronScript));
@@ -4699,7 +5098,7 @@ async function runSetup(input) {
4699
5098
  print(formatStep(steps[steps.length - 1]));
4700
5099
  }
4701
5100
  {
4702
- const hasTestWorkflow = (config.tasks ?? []).map((e) => typeof e === "string" ? e : e.name).includes("test");
5101
+ const hasTestWorkflow = (config.tasks ?? []).map((e) => typeof e === "string" ? e : e.name).includes("verification.unitTests");
4703
5102
  const existing = await readFile(join(input.context.repoRoot, "codecov.yml"), "utf8").catch(() => null);
4704
5103
  if (!hasTestWorkflow && existing == null) steps.push({
4705
5104
  capability: "source",
@@ -4718,6 +5117,23 @@ async function runSetup(input) {
4718
5117
  }));
4719
5118
  print(formatStep(steps[steps.length - 1]));
4720
5119
  }
5120
+ {
5121
+ const content = createAstromech({
5122
+ cwd: input.context.repoRoot,
5123
+ config: { tasks: config.tasks },
5124
+ logger
5125
+ }).turboConfig();
5126
+ if (content === null) steps.push({
5127
+ capability: "source",
5128
+ step: "write turbo.json",
5129
+ status: "skip",
5130
+ message: "no fan-out-eligible tasks configured"
5131
+ });
5132
+ else steps.push(await runStep("source", "write turbo.json", dryRun, async () => {
5133
+ await source.writeRepoFile("turbo.json", content);
5134
+ }));
5135
+ print(formatStep(steps[steps.length - 1]));
5136
+ }
4721
5137
  if (source.syncLabels) {
4722
5138
  steps.push(await runStep("source", "sync labels", dryRun, async () => {
4723
5139
  return source.syncLabels(CANONICAL_LABELS, STALE_LABELS);
@@ -4856,7 +5272,7 @@ async function runSetup(input) {
4856
5272
  if (loader.has("deployment")) {
4857
5273
  const deploy = loader.get("deployment");
4858
5274
  print(style.step("deployment"));
4859
- const deployEntry = (config.tasks ?? []).map((e) => typeof e === "string" ? { name: e } : e).find((e) => e.name === "deploy");
5275
+ const deployEntry = (config.tasks ?? []).map((e) => typeof e === "string" ? { name: e } : e).find((e) => e.name === "delivery.deploy" || e.name === "knowledge.docs" || e.name === "knowledge.components");
4860
5276
  const previewCfg = deployEntry?.with ? extractPreviewConfig(deployEntry.with, {
4861
5277
  org: config.org,
4862
5278
  domain: config.domain
@@ -5580,7 +5996,7 @@ async function runSyncWiki(input) {
5580
5996
  //#endregion
5581
5997
  //#region src/commands/sync.ts
5582
5998
  const { workflowHeader } = createHeader({
5583
- source: "packages/cli/src/commands/sync.ts",
5999
+ source: "packages/astromech/src/thin-callers.ts",
5584
6000
  tool: "holocron sync"
5585
6001
  });
5586
6002
  const SYNC_STEPS = [
@@ -5594,6 +6010,7 @@ const SYNC_STEPS = [
5594
6010
  "readme",
5595
6011
  "workflows",
5596
6012
  "scripts",
6013
+ "turbo",
5597
6014
  "wiki"
5598
6015
  ];
5599
6016
  const LOCAL_STEPS = /* @__PURE__ */ new Set([
@@ -5603,6 +6020,7 @@ const LOCAL_STEPS = /* @__PURE__ */ new Set([
5603
6020
  "readme",
5604
6021
  "workflows",
5605
6022
  "scripts",
6023
+ "turbo",
5606
6024
  "wiki"
5607
6025
  ]);
5608
6026
  async function runSync(input) {
@@ -5751,6 +6169,7 @@ async function runSync(input) {
5751
6169
  "readme",
5752
6170
  "workflows",
5753
6171
  "scripts",
6172
+ "turbo",
5754
6173
  "wiki"
5755
6174
  ]) {
5756
6175
  if (requestedSteps !== void 0 && !requestedSteps.includes(stepName)) continue;
@@ -5855,7 +6274,8 @@ async function runSync(input) {
5855
6274
  }).thinCallers();
5856
6275
  for (const entry of taskEntries) {
5857
6276
  const name = typeof entry === "string" ? entry : entry.name;
5858
- if (!KNOWN_WORKFLOWS.has(name)) {
6277
+ const isCombinedContentOnly = name === "knowledge.docs" || name === "knowledge.components";
6278
+ if (!KNOWN_WORKFLOWS.has(name) && !isCombinedContentOnly) {
5859
6279
  steps.push({
5860
6280
  capability: "local",
5861
6281
  step: `sync workflow ${name}`,
@@ -5865,10 +6285,10 @@ async function runSync(input) {
5865
6285
  print(formatSyncStep(steps[steps.length - 1]));
5866
6286
  continue;
5867
6287
  }
5868
- const filename = name === "deploy" ? "deploy.yml" : `${name}.yml`;
6288
+ const filename = `${name}.yml`;
5869
6289
  const content = files.get(filename);
5870
6290
  if (content === void 0) continue;
5871
- const step = `sync workflow ${name}${name === "deploy" && content.includes("workflows/preview.yml@main") ? " (with preview)" : ""}`;
6291
+ const step = `sync workflow ${name}${content.includes("workflows/preview.yml@main") ? " (with preview)" : ""}`;
5872
6292
  steps.push(await runSyncStep("local", step, dryRun, async () => {
5873
6293
  await writeWorkflowFile(input.context.repoRoot, filename, `${workflowHeader()}${content}`);
5874
6294
  }));
@@ -5900,6 +6320,27 @@ async function runSync(input) {
5900
6320
  }));
5901
6321
  print(formatSyncStep(steps[steps.length - 1]));
5902
6322
  }
6323
+ if (stepName === "turbo") {
6324
+ const content = createAstromech({
6325
+ cwd: input.context.repoRoot,
6326
+ config: { tasks: config.tasks },
6327
+ logger
6328
+ }).turboConfig();
6329
+ if (content === null) {
6330
+ steps.push({
6331
+ capability: "local",
6332
+ step: "sync turbo.json",
6333
+ status: "skip",
6334
+ message: "no fan-out-eligible tasks configured"
6335
+ });
6336
+ print(formatSyncStep(steps[steps.length - 1]));
6337
+ } else {
6338
+ steps.push(await runSyncStep("local", "sync turbo.json", dryRun, async () => {
6339
+ await writeFile(join(input.context.repoRoot, "turbo.json"), content, "utf8");
6340
+ }));
6341
+ print(formatSyncStep(steps[steps.length - 1]));
6342
+ }
6343
+ }
5903
6344
  if (stepName === "wiki") {
5904
6345
  const result = await runSyncWiki({
5905
6346
  loaded: input.loaded,
@@ -7743,24 +8184,59 @@ try {
7743
8184
  }).command("publish", "Publish @theholocron/* packages to npm", (y) => y.option("initial", {
7744
8185
  type: "boolean",
7745
8186
  default: false,
7746
- describe: "One-shot bootstrap publish for trusted-publishing-eligible packages (npm needs the package to exist before Trusted Publishing can be configured for it). Required today — a non-initial `publish` isn't implemented yet."
8187
+ 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."
7747
8188
  }).option("tag", {
7748
8189
  type: "string",
7749
- default: "alpha",
7750
- describe: "npm distribution tag (defaults to alpha)"
8190
+ describe: "npm distribution tag. Defaults to alpha for --initial, latest for a steady-state publish."
7751
8191
  }).option("otp", {
7752
8192
  type: "string",
7753
8193
  describe: "One-time password from your authenticator (required if npm needs 2FA for writes)"
8194
+ }).option("sync-trust", {
8195
+ type: "array",
8196
+ describe: "Migrate npm Trusted Publisher config to a new workflow filename across every public package: --sync-trust <old-file> <new-file>. See issue #689 — npm requires fresh interactive 2FA per package; re-run after approving the printed authUrl if it stops early."
8197
+ }).option("repo", {
8198
+ type: "string",
8199
+ describe: "owner/repo for Trusted Publisher config (--sync-trust). Defaults to theholocron/holocron."
8200
+ }).option("provenance", {
8201
+ type: "boolean",
8202
+ default: true,
8203
+ describe: "Publish with --provenance (steady-state only). Default on — free under CI/OIDC."
8204
+ }).option("skip-already-published", {
8205
+ type: "boolean",
8206
+ default: false,
8207
+ 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."
7754
8208
  }), async (argv) => {
7755
- if (!argv.initial) {
7756
- getLogger().error("publish: only `--initial` is supported today. Run `holocron publish --initial`.");
7757
- process.exitCode = 1;
8209
+ if (argv.syncTrust) {
8210
+ const [oldFile, newFile] = argv.syncTrust;
8211
+ if (!oldFile || !newFile) {
8212
+ getLogger().error("publish --sync-trust requires exactly two values: <old-file> <new-file>");
8213
+ process.exitCode = 1;
8214
+ return;
8215
+ }
8216
+ if ((await runSyncTrust({
8217
+ cwd: argv.cwd,
8218
+ oldFile,
8219
+ newFile,
8220
+ dryRun: argv.dryRun,
8221
+ ...argv.repo ? { repo: argv.repo } : {}
8222
+ })).status === "fail") process.exitCode = 1;
8223
+ return;
8224
+ }
8225
+ if (argv.initial) {
8226
+ if ((await runPublish({
8227
+ cwd: argv.cwd,
8228
+ dryRun: argv.dryRun,
8229
+ ...argv.tag ? { tag: argv.tag } : {},
8230
+ ...argv.otp ? { otp: argv.otp } : {}
8231
+ })).status === "fail") process.exitCode = 1;
7758
8232
  return;
7759
8233
  }
7760
- if ((await runPublish({
8234
+ if ((await runSteadyPublish({
7761
8235
  cwd: argv.cwd,
7762
- tag: argv.tag,
7763
8236
  dryRun: argv.dryRun,
8237
+ provenance: argv.provenance,
8238
+ skipAlreadyPublished: argv.skipAlreadyPublished,
8239
+ ...argv.tag ? { tag: argv.tag } : {},
7764
8240
  ...argv.otp ? { otp: argv.otp } : {}
7765
8241
  })).status === "fail") process.exitCode = 1;
7766
8242
  }).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", {