@theholocron/cli 5.0.0-alpha.196 → 5.0.0-alpha.197
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 +103 -11
- package/dist/cli.mjs.map +1 -1
- package/dist/plugin/capabilities.d.mts +15 -0
- package/package.json +3 -3
package/dist/cli.mjs
CHANGED
|
@@ -24,8 +24,8 @@ import search from "@inquirer/search";
|
|
|
24
24
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
25
25
|
import ora from "ora";
|
|
26
26
|
import { findRootSync, isWorkspaceRoot, walkFilesSync } from "@theholocron/fs-utils";
|
|
27
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
28
27
|
import { access, copyFile, mkdir, readFile, readdir, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
28
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
29
29
|
import { stringifyJson } from "@theholocron/json-utils";
|
|
30
30
|
import { generateReadme } from "@theholocron/components-doc/markdown";
|
|
31
31
|
import { getClients, getConfigs, getDocs, getPlugins, getSkills, getThemes, getUtils } from "@theholocron/registry-doc";
|
|
@@ -2634,6 +2634,59 @@ function mk(capability, provider, status, message, logMessage) {
|
|
|
2634
2634
|
};
|
|
2635
2635
|
}
|
|
2636
2636
|
//#endregion
|
|
2637
|
+
//#region src/commands/notifications-channel-add/avatar.ts
|
|
2638
|
+
/**
|
|
2639
|
+
* Read a webhook avatar named in a layout: a file path (from the directory the CLI runs in) or an
|
|
2640
|
+
* `https://` URL. Returns it as a `data:` URI, which is how providers take an image.
|
|
2641
|
+
*
|
|
2642
|
+
* The type comes from the first bytes, not from the name or the response header, so what is sent is
|
|
2643
|
+
* the image it claims to be.
|
|
2644
|
+
*/
|
|
2645
|
+
/** Providers cap avatars well below this; it only keeps a wrong file from filling memory. */
|
|
2646
|
+
const MAX_AVATAR_BYTES = 8388608;
|
|
2647
|
+
const FETCH_TIMEOUT_MS$2 = 15e3;
|
|
2648
|
+
const SIGNATURES = [
|
|
2649
|
+
["image/png", [
|
|
2650
|
+
137,
|
|
2651
|
+
80,
|
|
2652
|
+
78,
|
|
2653
|
+
71,
|
|
2654
|
+
13,
|
|
2655
|
+
10,
|
|
2656
|
+
26,
|
|
2657
|
+
10
|
|
2658
|
+
]],
|
|
2659
|
+
["image/jpeg", [
|
|
2660
|
+
255,
|
|
2661
|
+
216,
|
|
2662
|
+
255
|
|
2663
|
+
]],
|
|
2664
|
+
["image/gif", [
|
|
2665
|
+
71,
|
|
2666
|
+
73,
|
|
2667
|
+
70,
|
|
2668
|
+
56
|
|
2669
|
+
]]
|
|
2670
|
+
];
|
|
2671
|
+
const tooBig = () => /* @__PURE__ */ new Error(`it is larger than ${MAX_AVATAR_BYTES / 1024 / 1024} MB`);
|
|
2672
|
+
async function loadAvatar(source, baseDir = process.cwd(), fetchImpl = fetch) {
|
|
2673
|
+
let bytes;
|
|
2674
|
+
if (/^https:\/\//i.test(source)) {
|
|
2675
|
+
const res = await fetchImpl(source, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS$2) });
|
|
2676
|
+
if (!res.ok) throw new Error(`fetching it answered ${res.status}`);
|
|
2677
|
+
if (Number(res.headers.get("content-length")) > 8388608) throw tooBig();
|
|
2678
|
+
bytes = Buffer.from(await res.arrayBuffer());
|
|
2679
|
+
} else {
|
|
2680
|
+
const path = resolve(baseDir, source);
|
|
2681
|
+
if ((await stat(path)).size > 8388608) throw tooBig();
|
|
2682
|
+
bytes = await readFile(path);
|
|
2683
|
+
}
|
|
2684
|
+
if (bytes.length > 8388608) throw tooBig();
|
|
2685
|
+
const type = SIGNATURES.find(([, sig]) => sig.every((b, i) => bytes[i] === b))?.[0];
|
|
2686
|
+
if (!type) throw new Error("it is not a PNG, JPEG or GIF image, which are the formats providers accept");
|
|
2687
|
+
return `data:${type};base64,${bytes.toString("base64")}`;
|
|
2688
|
+
}
|
|
2689
|
+
//#endregion
|
|
2637
2690
|
//#region src/commands/notifications-channel-add/notifications-channel-add.ts
|
|
2638
2691
|
/**
|
|
2639
2692
|
* `holocron notifications channel add` — create one channel (and, with
|
|
@@ -2784,8 +2837,10 @@ async function provisionChannel(deps, spec) {
|
|
|
2784
2837
|
const hook = await notifications.ensureWebhook({
|
|
2785
2838
|
channel: spec.name,
|
|
2786
2839
|
name: webhookName,
|
|
2787
|
-
...spec.category ? { category: spec.category } : {}
|
|
2840
|
+
...spec.category ? { category: spec.category } : {},
|
|
2841
|
+
...webhookCfg.avatar ? { avatar: webhookCfg.avatar } : {}
|
|
2788
2842
|
});
|
|
2843
|
+
const avatarSet = hook.updated?.includes("avatar") ?? false;
|
|
2789
2844
|
if (hook.url) {
|
|
2790
2845
|
try {
|
|
2791
2846
|
await secrets.setSecret(scope, secretName, hook.url);
|
|
@@ -2798,7 +2853,8 @@ async function provisionChannel(deps, spec) {
|
|
|
2798
2853
|
name: webhookName,
|
|
2799
2854
|
created: hook.created,
|
|
2800
2855
|
secret: secretName,
|
|
2801
|
-
stored: true
|
|
2856
|
+
stored: true,
|
|
2857
|
+
...avatarSet ? { updated: ["avatar"] } : {}
|
|
2802
2858
|
};
|
|
2803
2859
|
} else {
|
|
2804
2860
|
const note = `webhook ${webhookName} already exists and its URL cannot be read back — delete it in the provider and re-run, or set ${secretName} yourself`;
|
|
@@ -2808,9 +2864,15 @@ async function provisionChannel(deps, spec) {
|
|
|
2808
2864
|
created: hook.created,
|
|
2809
2865
|
secret: secretName,
|
|
2810
2866
|
stored: false,
|
|
2811
|
-
note
|
|
2867
|
+
note,
|
|
2868
|
+
...avatarSet ? { updated: ["avatar"] } : {}
|
|
2812
2869
|
};
|
|
2813
2870
|
}
|
|
2871
|
+
if (avatarSet) {
|
|
2872
|
+
print(` ${style.success(`set avatar on webhook ${webhookName}`)}`);
|
|
2873
|
+
if (tally) tally.ok++;
|
|
2874
|
+
}
|
|
2875
|
+
if (hook.avatarError) throw new Error(`webhook ${webhookName} is ready but setting its avatar failed (${hook.avatarError})`);
|
|
2814
2876
|
return outcome;
|
|
2815
2877
|
}
|
|
2816
2878
|
async function runNotificationsChannelAdd(input) {
|
|
@@ -2898,11 +2960,6 @@ async function runNotificationsChannelAdd(input) {
|
|
|
2898
2960
|
*/
|
|
2899
2961
|
/** Features not built yet, in the order of the table in holocron#1098. Move an entry into a real check when it lands. */
|
|
2900
2962
|
const PLANNED_CHECKS = [
|
|
2901
|
-
{
|
|
2902
|
-
step: 12,
|
|
2903
|
-
id: "webhook-appearance",
|
|
2904
|
-
title: "webhook names and avatars set"
|
|
2905
|
-
},
|
|
2906
2963
|
{
|
|
2907
2964
|
step: 13,
|
|
2908
2965
|
id: "app-settings",
|
|
@@ -2942,6 +2999,7 @@ const promptKey = (p) => JSON.stringify([
|
|
|
2942
2999
|
const accessText = (v) => v === "everyone" ? "everyone" : v.length === 0 ? "nobody (bots and webhooks only)" : v.join(", ");
|
|
2943
3000
|
const shown = (s) => sanitizeDetail(s);
|
|
2944
3001
|
const WELCOME_TITLE = "welcome screen and onboarding set";
|
|
3002
|
+
const AVATAR_TITLE = "webhook names and avatars set";
|
|
2945
3003
|
const SERVER_TITLE = "server verification, notifications and community set";
|
|
2946
3004
|
const ACCESS_TITLE = "channel and category access set";
|
|
2947
3005
|
const ROLE_APPEARANCE_TITLE = "role colour, hoist and permissions set";
|
|
@@ -3064,7 +3122,8 @@ async function runDoctorNotifications(input) {
|
|
|
3064
3122
|
["role-appearance", ROLE_APPEARANCE_TITLE],
|
|
3065
3123
|
["access", ACCESS_TITLE],
|
|
3066
3124
|
["server-settings", SERVER_TITLE],
|
|
3067
|
-
["welcome-onboarding", WELCOME_TITLE]
|
|
3125
|
+
["welcome-onboarding", WELCOME_TITLE],
|
|
3126
|
+
["webhook-appearance", AVATAR_TITLE]
|
|
3068
3127
|
]) failed(id, title, "not checked: could not read the server");
|
|
3069
3128
|
} else {
|
|
3070
3129
|
const inspected = found;
|
|
@@ -3247,6 +3306,19 @@ async function runDoctorNotifications(input) {
|
|
|
3247
3306
|
const welcomeParts = (wantWelcome ? 1 : 0) + (wantOnboarding ? 1 : 0);
|
|
3248
3307
|
add("welcome-onboarding", WELCOME_TITLE, [...welcomeProblems, ...onboardingProblems], welcomeParts === 0 ? "none declared" : `${welcomeParts} of ${welcomeParts} as declared`);
|
|
3249
3308
|
}
|
|
3309
|
+
if (found) {
|
|
3310
|
+
const avatarOf = (webhook) => (typeof webhook === "object" && webhook ? webhook.avatar : void 0) ?? layout.webhookAvatar;
|
|
3311
|
+
const wantAvatar = declaredChannels.filter(({ channel }) => channel.webhook && avatarOf(channel.webhook));
|
|
3312
|
+
const avatarProblems = [];
|
|
3313
|
+
for (const { cat, channel } of wantAvatar) {
|
|
3314
|
+
const name = (channel.webhook === true ? {} : channel.webhook).name ?? channel.name;
|
|
3315
|
+
const { match } = pick(channel.name, cat);
|
|
3316
|
+
if (!match) avatarProblems.push(`#${shown(channel.name)}: no channel to hold the webhook`);
|
|
3317
|
+
else if (match.webhooksError) avatarProblems.push(`#${shown(channel.name)}: could not list its webhooks (${shown(match.webhooksError)})`);
|
|
3318
|
+
else if (match.webhookAvatars && !match.webhookAvatars.some((w) => same(w, name))) avatarProblems.push(`#${shown(channel.name)}: webhook ${shown(name)} has no avatar`);
|
|
3319
|
+
}
|
|
3320
|
+
add("webhook-appearance", AVATAR_TITLE, avatarProblems, wantAvatar.length === 0 ? "none declared" : `${wantAvatar.length} of ${wantAvatar.length} set`);
|
|
3321
|
+
}
|
|
3250
3322
|
const wanted = [...declaredChannels.filter(({ channel }) => channel.webhook).map(({ channel }) => channel.webhook !== true && channel.webhook && channel.webhook.secret ? channel.webhook.secret : defaultSecretName(channel.name)), ...(layout.roles ?? []).flatMap((r) => r.secret ? [r.secret] : [])];
|
|
3251
3323
|
if (wanted.length === 0) checks.push({
|
|
3252
3324
|
id: "secrets",
|
|
@@ -3907,6 +3979,10 @@ async function runNew(input) {
|
|
|
3907
3979
|
* shared preset would apply it from every repo.
|
|
3908
3980
|
*/
|
|
3909
3981
|
/** A channel's access: its category's, with any field the channel states itself laid over it. */
|
|
3982
|
+
/** The picture a channel's webhook should have: its own, else the layout's, else none. */
|
|
3983
|
+
function webhookAvatarOf(layout, webhook) {
|
|
3984
|
+
return (typeof webhook === "object" ? webhook.avatar : void 0) ?? layout.webhookAvatar;
|
|
3985
|
+
}
|
|
3910
3986
|
function effectiveAccess(category, channel) {
|
|
3911
3987
|
if (!category && !channel) return void 0;
|
|
3912
3988
|
return {
|
|
@@ -4034,6 +4110,17 @@ async function runNotificationsSetup(input) {
|
|
|
4034
4110
|
name: org
|
|
4035
4111
|
} : void 0);
|
|
4036
4112
|
if (wantsSecrets && !scope && !dryRun) return refuse("no org to store secrets under — pass --scope or set `org` in holocron.config.ts");
|
|
4113
|
+
const avatars = /* @__PURE__ */ new Map();
|
|
4114
|
+
const read = input.loadAvatar ?? loadAvatar;
|
|
4115
|
+
for (const channel of (layout.categories ?? []).flatMap((c) => c.channels ?? [])) {
|
|
4116
|
+
const source = channel.webhook ? webhookAvatarOf(layout, channel.webhook) : void 0;
|
|
4117
|
+
if (source === void 0 || avatars.has(source)) continue;
|
|
4118
|
+
try {
|
|
4119
|
+
avatars.set(source, await read(source));
|
|
4120
|
+
} catch (err) {
|
|
4121
|
+
return refuse(`webhook avatar \`${source}\`: ${errorMessage(err)}`);
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4037
4124
|
if (secrets && scope && !dryRun) try {
|
|
4038
4125
|
await assertSecretsReachable(secrets, scope);
|
|
4039
4126
|
} catch (err) {
|
|
@@ -4139,6 +4226,11 @@ async function runNotificationsSetup(input) {
|
|
|
4139
4226
|
continue;
|
|
4140
4227
|
}
|
|
4141
4228
|
for (const channel of channels) await attempt(`channel ${channel.name}`, async () => {
|
|
4229
|
+
const source = channel.webhook ? webhookAvatarOf(layout, channel.webhook) : void 0;
|
|
4230
|
+
const webhook = channel.webhook ? {
|
|
4231
|
+
...channel.webhook === true ? {} : channel.webhook,
|
|
4232
|
+
...source ? { avatar: avatars.get(source) } : {}
|
|
4233
|
+
} : void 0;
|
|
4142
4234
|
const outcome = await provisionChannel({
|
|
4143
4235
|
notifications,
|
|
4144
4236
|
...secrets ? { secrets } : {},
|
|
@@ -4151,7 +4243,7 @@ async function runNotificationsSetup(input) {
|
|
|
4151
4243
|
category: category.name,
|
|
4152
4244
|
...channel.topic ? { topic: channel.topic } : {},
|
|
4153
4245
|
...effectiveAccess(category.access, channel.access) ? { access: effectiveAccess(category.access, channel.access) } : {},
|
|
4154
|
-
...
|
|
4246
|
+
...webhook ? { webhook } : {}
|
|
4155
4247
|
});
|
|
4156
4248
|
report.channels.push(outcome);
|
|
4157
4249
|
if (outcome.webhook && !outcome.webhook.stored) report.failures.push({
|