@theholocron/cli 5.0.0-alpha.185 → 5.0.0-alpha.186
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 +43 -16
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/plugin/capabilities.d.mts +7 -0
- package/package.json +3 -3
package/dist/cli.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import ora from "ora";
|
|
|
26
26
|
import { findRootSync, isWorkspaceRoot, walkFilesSync } from "@theholocron/fs-utils";
|
|
27
27
|
import { access, copyFile, mkdir, readFile, readdir, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
28
28
|
import { stringifyJson } from "@theholocron/json-utils";
|
|
29
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
29
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
30
30
|
import { generateReadme } from "@theholocron/components-doc/markdown";
|
|
31
31
|
import { getClients, getConfigs, getDocs, getPlugins, getSkills, getThemes, getUtils } from "@theholocron/registry-doc";
|
|
32
32
|
import { createGitHubClient } from "@theholocron/github-client";
|
|
@@ -3249,8 +3249,16 @@ async function runNew(input) {
|
|
|
3249
3249
|
function defaultSecretName(channel) {
|
|
3250
3250
|
return `HOLOCRON_NOTIFY_WEBHOOK_${channel.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
|
|
3251
3251
|
}
|
|
3252
|
-
/**
|
|
3253
|
-
|
|
3252
|
+
/**
|
|
3253
|
+
* Prefix of the throwaway secret the preflight writes and removes. Every run adds a
|
|
3254
|
+
* random suffix, so it can never overwrite (and then delete) a secret someone already
|
|
3255
|
+
* has; layouts may not use any name under this prefix for their own secrets.
|
|
3256
|
+
*/
|
|
3257
|
+
const PREFLIGHT_PREFIX = "HOLOCRON_NOTIFY_PREFLIGHT";
|
|
3258
|
+
/** Whether a secret name is under the preflight's reserved prefix. GitHub secret names are case-insensitive. */
|
|
3259
|
+
function isReservedSecret(name) {
|
|
3260
|
+
return name.toUpperCase().startsWith(PREFLIGHT_PREFIX);
|
|
3261
|
+
}
|
|
3254
3262
|
/**
|
|
3255
3263
|
* Prove the secrets store can be WRITTEN before creating anything that needs it.
|
|
3256
3264
|
* A webhook's URL is shown once; creating the webhook and then failing to store
|
|
@@ -3259,12 +3267,13 @@ const PREFLIGHT_SECRET = "HOLOCRON_NOTIFY_PREFLIGHT";
|
|
|
3259
3267
|
* remove it.
|
|
3260
3268
|
*/
|
|
3261
3269
|
async function assertSecretsReachable(secrets, scope) {
|
|
3270
|
+
const probe = `${PREFLIGHT_PREFIX}_${randomBytes(4).toString("hex").toUpperCase()}`;
|
|
3262
3271
|
try {
|
|
3263
|
-
await secrets.setSecret(scope,
|
|
3264
|
-
await secrets.deleteSecret(scope,
|
|
3272
|
+
await secrets.setSecret(scope, probe, "ok");
|
|
3273
|
+
await secrets.deleteSecret(scope, probe);
|
|
3265
3274
|
} catch (err) {
|
|
3266
3275
|
const where = scope.kind === "organization" ? `organization ${scope.name}` : scope.kind;
|
|
3267
|
-
throw new Error(`cannot write to the secrets store for ${where} (${describeProviderError(err)}) — fix that token before creating webhooks`, { cause: err });
|
|
3276
|
+
throw new Error(`cannot write to the secrets store for ${where} (${describeProviderError(err)}) — fix that token before creating webhooks. If a secret named ${probe} was left behind, delete it`, { cause: err });
|
|
3268
3277
|
}
|
|
3269
3278
|
}
|
|
3270
3279
|
/** Why a capability is not loaded: the plugin's own load error when there is one (e.g. a missing token). */
|
|
@@ -3294,6 +3303,7 @@ async function provisionChannel(deps, spec) {
|
|
|
3294
3303
|
const where = spec.category ? ` in ${spec.category}` : "";
|
|
3295
3304
|
if (!notifications.ensureChannel) throw new Error(`\`${notifications.providerName}\` cannot create channels`);
|
|
3296
3305
|
if (webhookCfg && !notifications.ensureWebhook) throw new Error(`\`${notifications.providerName}\` cannot create webhooks`);
|
|
3306
|
+
if (webhookCfg && isReservedSecret(secretName)) throw new Error(`secret name ${secretName} is reserved for the setup preflight -- choose another`);
|
|
3297
3307
|
if (webhookCfg && !dryRun && !secrets) throw new Error("storing a webhook URL needs a `secrets` provider — add one to holocron.config.ts");
|
|
3298
3308
|
if (dryRun) {
|
|
3299
3309
|
print(` … would ensure channel ${spec.name}${where}`);
|
|
@@ -3463,10 +3473,10 @@ async function runNotificationsSetup(input) {
|
|
|
3463
3473
|
}
|
|
3464
3474
|
const layout = notifications.layout;
|
|
3465
3475
|
if (!layout) return refuse(`the \`${notifications.providerName}\` provider declares no layout — add \`layout\` to its options in holocron.config.ts`);
|
|
3466
|
-
const
|
|
3476
|
+
const wantsSecrets = (layout.categories ?? []).some((c) => (c.channels ?? []).some((ch) => ch.webhook)) || (layout.roles ?? []).some((r) => r.secret);
|
|
3467
3477
|
let secrets;
|
|
3468
|
-
if (
|
|
3469
|
-
if (!loader.has("secrets")) return refuse(whyMissing(loader, "secrets", "the layout
|
|
3478
|
+
if (wantsSecrets && !dryRun) {
|
|
3479
|
+
if (!loader.has("secrets")) return refuse(whyMissing(loader, "secrets", "the layout stores webhook URLs or role ids, which need a `secrets` capability — add a `secrets` provider"));
|
|
3470
3480
|
secrets = loader.get("secrets");
|
|
3471
3481
|
}
|
|
3472
3482
|
const org = input.context.org;
|
|
@@ -3474,19 +3484,29 @@ async function runNotificationsSetup(input) {
|
|
|
3474
3484
|
kind: "organization",
|
|
3475
3485
|
name: org
|
|
3476
3486
|
} : void 0);
|
|
3477
|
-
if (
|
|
3487
|
+
if (wantsSecrets && !scope && !dryRun) return refuse("no org to store secrets under — pass --scope or set `org` in holocron.config.ts");
|
|
3478
3488
|
if (secrets && scope && !dryRun) try {
|
|
3479
3489
|
await assertSecretsReachable(secrets, scope);
|
|
3480
3490
|
} catch (err) {
|
|
3481
3491
|
return refuse(errorMessage(err), logReason(err));
|
|
3482
3492
|
}
|
|
3483
3493
|
const taken = /* @__PURE__ */ new Map();
|
|
3494
|
+
const claim = (secret, owner, hint) => {
|
|
3495
|
+
if (isReservedSecret(secret)) return `${owner} would store into ${secret}, a name reserved for the setup preflight — choose another`;
|
|
3496
|
+
const key = secret.toUpperCase();
|
|
3497
|
+
const other = taken.get(key);
|
|
3498
|
+
if (other) return `${other} and ${owner} would both store into ${secret} — ${hint}`;
|
|
3499
|
+
taken.set(key, owner);
|
|
3500
|
+
};
|
|
3484
3501
|
for (const category of layout.categories ?? []) for (const channel of category.channels ?? []) {
|
|
3485
3502
|
if (!channel.webhook) continue;
|
|
3486
|
-
const
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3503
|
+
const clash = claim(channel.webhook !== true && channel.webhook.secret || defaultSecretName(channel.name), `channel \`${channel.name}\``, "give one a distinct `webhook.secret`");
|
|
3504
|
+
if (clash) return refuse(clash);
|
|
3505
|
+
}
|
|
3506
|
+
for (const role of layout.roles ?? []) {
|
|
3507
|
+
if (!role.secret) continue;
|
|
3508
|
+
const clash = claim(role.secret, `role \`${role.name}\``, "give one a distinct secret");
|
|
3509
|
+
if (clash) return refuse(clash);
|
|
3490
3510
|
}
|
|
3491
3511
|
print(`Holocron notifications setup — ${notifications.providerName}${dryRun ? " (dry-run)" : ""}`);
|
|
3492
3512
|
const attempt = async (step, fn) => {
|
|
@@ -3537,10 +3557,11 @@ async function runNotificationsSetup(input) {
|
|
|
3537
3557
|
for (const role of layout.roles ?? []) await attempt(`role ${role.name}`, async () => {
|
|
3538
3558
|
if (!notifications.ensureRole) throw new Error(`\`${notifications.providerName}\` cannot manage roles`);
|
|
3539
3559
|
if (dryRun) {
|
|
3540
|
-
print(` … would ensure role ${role.name}`);
|
|
3560
|
+
print(` … would ensure role ${role.name}${role.secret ? ` and store its id in ${role.secret}` : ""}`);
|
|
3541
3561
|
report.roles.push({
|
|
3542
3562
|
name: role.name,
|
|
3543
|
-
created: false
|
|
3563
|
+
created: false,
|
|
3564
|
+
...role.secret ? { secret: role.secret } : {}
|
|
3544
3565
|
});
|
|
3545
3566
|
return;
|
|
3546
3567
|
}
|
|
@@ -3555,6 +3576,12 @@ async function runNotificationsSetup(input) {
|
|
|
3555
3576
|
};
|
|
3556
3577
|
report.roles.push(outcome);
|
|
3557
3578
|
print(` ${made.created ? "✓ created" : "· exists "} role ${role.name} (id ${made.id})`);
|
|
3579
|
+
if (role.secret) {
|
|
3580
|
+
await secrets.setSecret(scope, role.secret, made.id);
|
|
3581
|
+
outcome.secret = role.secret;
|
|
3582
|
+
outcome.stored = true;
|
|
3583
|
+
print(` ✓ stored the id of ${role.name} in ${role.secret}`);
|
|
3584
|
+
}
|
|
3558
3585
|
});
|
|
3559
3586
|
if (report.failures.length > 0) {
|
|
3560
3587
|
report.status = "fail";
|