husteno-o feat: code review system, type safety, error handling, AI provider fallback · 9/9/2026
5ccaeae
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { githubClient } from "@devpulse/github";
import { SlashCommandBuilder } from "discord.js";
import { createBaseEmbed, createErrorEmbed, createReleaseNotesEmbed } from "../ui/embeds.js";
import { NF } from "../ui/icons.js";
import type { Command } from "./types.js";
export const releaseCommand: Command = {
data: new SlashCommandBuilder()
.setName("release")
.setDescription("Release Manager: latest releases, tag comparison, and automated release notes")
.addSubcommand((sub) =>
sub
.setName("latest")
.setDescription("View the latest published release and changelog")
.addStringOption((opt) =>
opt.setName("repo").setDescription("Repository format 'owner/repo'").setRequired(true),
),
)
.addSubcommand((sub) =>
sub
.setName("compare")
.setDescription("Compare changes between two releases or tags")
.addStringOption((opt) =>
opt.setName("repo").setDescription("Repository format 'owner/repo'").setRequired(true),
)
.addStringOption((opt) =>
opt.setName("base").setDescription("Base tag (e.g. v1.2.0)").setRequired(true),
)
.addStringOption((opt) =>
opt.setName("head").setDescription("Head tag (e.g. v1.3.0)").setRequired(true),
),
)
.addSubcommand((sub) =>
sub
.setName("notes")
.setDescription("Generate clean release notes automatically from merged PRs")
.addStringOption((opt) =>
opt.setName("repo").setDescription("Repository format 'owner/repo'").setRequired(true),
)
.addStringOption((opt) => opt.setName("from_tag").setDescription("Starting tag (optional)"))
.addStringOption((opt) =>
opt.setName("to_tag").setDescription("Target release version (default: vNext)"),
),
),
async execute(interaction) {
await interaction.deferReply();
const subcommand = interaction.options.getSubcommand();
const repoInput = interaction.options.getString("repo", true);
try {
if (subcommand === "latest") {
const releases = await githubClient.getReleases(repoInput, 1);
if (releases.length === 0) {
const embed = createBaseEmbed(`${NF.gitTag} Releases: ${repoInput}`).setDescription(
"No releases published in this repository yet.",
);
await interaction.editReply({ embeds: [embed] });
return;
}
const latest = releases[0];
const bodySnippet = latest.body
? `${latest.body.slice(0, 400)}...`
: "No release notes provided.";
const embed = createBaseEmbed(
`${NF.gitTag} Latest Release: ${latest.name} (${latest.tagName})`,
)
.setURL(latest.htmlUrl)
.setDescription(
`Published: <t:${Math.floor(new Date(latest.publishedAt).getTime() / 1000)}:R>\n\n` +
`**Changelog Highlights:**\n${bodySnippet}`,
);
await interaction.editReply({ embeds: [embed] });
return;
}
if (subcommand === "compare") {
const base = interaction.options.getString("base", true);
const head = interaction.options.getString("head", true);
const diff = await githubClient.compareReleases(repoInput, base, head);
const commitLines = diff.commits
.slice(0, 8)
.map(
(c) =>
`• [\`${c.sha}\`](${c.htmlUrl}) ${c.message.split("\n")[0]} — *${c.author.name}*`,
);
const embed = createBaseEmbed(
`${NF.gitTag} Compare: ${base}...${head} (${repoInput})`,
).setDescription(
`**Summary:** \`${diff.totalCommits}\` commits (${diff.aheadBy} ahead, ${diff.behindBy} behind)\n\n${commitLines.join("\n") || "No differences found between tags."}`,
);
await interaction.editReply({ embeds: [embed] });
return;
}
if (subcommand === "notes") {
const fromTag = interaction.options.getString("from_tag") || undefined;
const toTag = interaction.options.getString("to_tag") || "vNext";
const notes = await githubClient.generateReleaseNotes(repoInput, fromTag, toTag);
const embed = createReleaseNotesEmbed(repoInput, notes);
await interaction.editReply({ embeds: [embed] });
return;
}
} catch (err: unknown) {
await interaction.editReply({
embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
});
}
},
};