c8eca38
commit c8eca38
6 files changed
+342 -193
Showing 6 changed files+342-193
βΌapps/bot/src/commands/help.ts
+44-18
| @@ -4,6 +4,7 @@ import { createHelpSelect } from "../ui/components.js"; | ||
4 | 4 | import { createBaseEmbed } from "../ui/embeds.js"; |
5 | 5 | import { NF } from "../ui/icons.js"; |
6 | 6 | import { |
7 | + ANSI, | |
7 | 8 | renderTuiCard, |
8 | 9 | tuiBottomBar, |
9 | 10 | tuiDivider, |
| @@ -17,7 +18,7 @@ export function getModuleHelpEmbed(moduleName: string) { | ||
17 | 18 | const map: Record<string, { title: string; synopsis: string; commands: [string, string][] }> = { |
18 | 19 | repo: { |
19 | 20 | title: "REPO INTELLIGENCE", |
20 | - synopsis: "Deep repository metrics, health score & dependencies", | |
21 | + synopsis: "Deep repository metrics, health scores, and dependencies", | |
21 | 22 | commands: [ |
22 | 23 | ["/repo <repo>", "Master telemetry dashboard"], |
23 | 24 | ["/repo <repo> view:health", "Health breakdown & audit"], |
| @@ -98,21 +99,25 @@ export function getModuleHelpEmbed(moduleName: string) { | ||
98 | 99 | tuiTopBar(`MAN: ${info.title}`), |
99 | 100 | tuiPrompt(`gitbot man ${moduleName}`), |
100 | 101 | tuiDivider("SYNOPSIS"), |
101 | - tuiLine(info.synopsis), | |
102 | + tuiLine(`${ANSI.green}${info.synopsis}${ANSI.reset}`), | |
102 | 103 | tuiDivider("COMMAND SUITE"), |
103 | 104 | ]; |
104 | 105 | |
105 | 106 | for (const [cmd, desc] of info.commands) { |
106 | - tuiLines.push(tuiLine(cmd)); | |
107 | - tuiLines.push(tuiLine(` βΈ ${desc}`)); | |
107 | + tuiLines.push(tuiLine(`${ANSI.white}${cmd}${ANSI.reset}`)); | |
108 | + tuiLines.push(tuiLine(` ${ANSI.cyan}βΈ${ANSI.reset} ${ANSI.dim}${desc}${ANSI.reset}`)); | |
108 | 109 | } |
109 | 110 | |
110 | 111 | tuiLines.push(tuiBottomBar()); |
111 | 112 | const tui = renderTuiCard(tuiLines); |
112 | 113 | |
114 | + const header = | |
115 | + `### π» [**Manual: ${info.title}**](https://github.com/husteno-o/git-discord-bot)\n` + | |
116 | + `> *${info.synopsis}*\n\n`; | |
117 | + | |
113 | 118 | return createBaseEmbed(`${NF.terminal} Manual: ${info.title}`) |
114 | 119 | .setColor(BrandColors.primary) |
115 | - .setDescription(tui); | |
120 | + .setDescription(`${header}${tui}`); | |
116 | 121 | } |
117 | 122 | |
118 | 123 | export const helpCommand: Command = { |
| @@ -122,27 +127,48 @@ export const helpCommand: Command = { | ||
122 | 127 | |
123 | 128 | async execute(interaction) { |
124 | 129 | const tui = renderTuiCard([ |
125 | - tuiTopBar("GITBOT TERMINAL v2.4.0"), | |
130 | + tuiTopBar("GITBOT CORE ENGINE v2.4"), | |
126 | 131 | tuiPrompt("gitbot --help"), |
127 | 132 | tuiDivider("ACTIVE SUBSYSTEMS"), |
128 | - tuiLine("[1] REPO : /repo <target> [view]"), | |
129 | - tuiLine("[2] PR : /pr <view|list|review>"), | |
130 | - tuiLine("[3] SEARCH : /search <code|issues|prs>"), | |
131 | - tuiLine("[4] CODE : /code view, /code blame"), | |
132 | - tuiLine("[5] TRACE : /why, /investigate"), | |
133 | - tuiLine("[6] METRICS : /activity, /team"), | |
134 | - tuiLine("[7] RADAR : /trending, /watch"), | |
135 | - tuiLine("[8] CI/CD : /actions status, /release"), | |
136 | - tuiLine("[9] SECURE : /security audit, /connect"), | |
133 | + tuiLine( | |
134 | + `${ANSI.yellow}[1] REPO ${ANSI.reset}: ${ANSI.white}/repo <target> [view]${ANSI.reset}`, | |
135 | + ), | |
136 | + tuiLine( | |
137 | + `${ANSI.yellow}[2] PR ${ANSI.reset}: ${ANSI.white}/pr <view|list|review>${ANSI.reset}`, | |
138 | + ), | |
139 | + tuiLine( | |
140 | + `${ANSI.yellow}[3] SEARCH ${ANSI.reset}: ${ANSI.white}/search <code|issues|prs>${ANSI.reset}`, | |
141 | + ), | |
142 | + tuiLine( | |
143 | + `${ANSI.yellow}[4] CODE ${ANSI.reset}: ${ANSI.white}/code view, /code blame${ANSI.reset}`, | |
144 | + ), | |
145 | + tuiLine( | |
146 | + `${ANSI.yellow}[5] TRACE ${ANSI.reset}: ${ANSI.white}/why, /investigate${ANSI.reset}`, | |
147 | + ), | |
148 | + tuiLine(`${ANSI.yellow}[6] STATS ${ANSI.reset}: ${ANSI.white}/activity, /team${ANSI.reset}`), | |
149 | + tuiLine( | |
150 | + `${ANSI.yellow}[7] RADAR ${ANSI.reset}: ${ANSI.white}/trending, /watch${ANSI.reset}`, | |
151 | + ), | |
152 | + tuiLine( | |
153 | + `${ANSI.yellow}[8] CI/CD ${ANSI.reset}: ${ANSI.white}/actions status, /release${ANSI.reset}`, | |
154 | + ), | |
155 | + tuiLine( | |
156 | + `${ANSI.yellow}[9] SECURE ${ANSI.reset}: ${ANSI.white}/security audit, /connect${ANSI.reset}`, | |
157 | + ), | |
137 | 158 | tuiDivider("COMMAND CENTER"), |
138 | - tuiLine("βΈ Terminal CLI engineered for Discord"), | |
139 | - tuiLine("βΈ Select a manual below or run command"), | |
159 | + tuiLine(`${ANSI.green}βΈ Interactive Developer Console for Discord${ANSI.reset}`), | |
160 | + tuiLine(`${ANSI.cyan}βΈ Pick a manual below or type a command${ANSI.reset}`), | |
140 | 161 | tuiBottomBar(), |
141 | 162 | ]); |
142 | 163 | |
164 | + const header = | |
165 | + `### π [**GITBOT β GitHub Developer Operating System**](https://github.com/husteno-o/git-discord-bot)\n` + | |
166 | + `> *The ultimate Discord command center engineered so developers never have to leave chat for GitHub workflows.*\n\n` + | |
167 | + `π **Instant Telemetry** β’ πΏ **PR Power Tools** β’ π‘οΈ **Zero-Config Security**\n\n`; | |
168 | + | |
143 | 169 | const embed = createBaseEmbed(`${NF.github} GITBOT Developer Center`) |
144 | 170 | .setColor(BrandColors.primary) |
145 | - .setDescription(tui); | |
171 | + .setDescription(`${header}${tui}`); | |
146 | 172 | |
147 | 173 | const components = [createHelpSelect()]; |
148 | 174 | await interaction.reply({ embeds: [embed], components }); |
149 | 175 | |
βΌapps/bot/src/ui/components.ts
+12-12
| @@ -15,23 +15,23 @@ export function createRepoNavButtons( | ||
15 | 15 | const row1 = new ActionRowBuilder<ButtonBuilder>().addComponents( |
16 | 16 | new ButtonBuilder() |
17 | 17 | .setCustomId(`repo:overview:${owner}:${repo}`) |
18 | - .setLabel("Overview") | |
18 | + .setLabel("π¦ Overview") | |
19 | 19 | .setStyle(activeTab === "overview" ? ButtonStyle.Primary : ButtonStyle.Secondary), |
20 | 20 | new ButtonBuilder() |
21 | 21 | .setCustomId(`repo:prs:${owner}:${repo}`) |
22 | - .setLabel("PRs") | |
22 | + .setLabel("π Pull Requests") | |
23 | 23 | .setStyle(activeTab === "prs" ? ButtonStyle.Primary : ButtonStyle.Secondary), |
24 | 24 | new ButtonBuilder() |
25 | 25 | .setCustomId(`repo:issues:${owner}:${repo}`) |
26 | - .setLabel("Issues") | |
26 | + .setLabel("π Issues") | |
27 | 27 | .setStyle(activeTab === "issues" ? ButtonStyle.Primary : ButtonStyle.Secondary), |
28 | 28 | new ButtonBuilder() |
29 | 29 | .setCustomId(`repo:commits:${owner}:${repo}`) |
30 | - .setLabel("Commits") | |
30 | + .setLabel("π Commits") | |
31 | 31 | .setStyle(activeTab === "commits" ? ButtonStyle.Primary : ButtonStyle.Secondary), |
32 | 32 | new ButtonBuilder() |
33 | 33 | .setCustomId(`repo:releases:${owner}:${repo}`) |
34 | - .setLabel("Releases") | |
34 | + .setLabel("π·οΈ Releases") | |
35 | 35 | .setStyle(activeTab === "releases" ? ButtonStyle.Primary : ButtonStyle.Secondary), |
36 | 36 | ); |
37 | 37 | |
| @@ -57,7 +57,7 @@ export function createPrActionButtons( | ||
57 | 57 | .setCustomId(`pr:merge:${owner}:${repo}:${prNumber}`) |
58 | 58 | .setLabel(`${NF.gitMerge} Merge`) |
59 | 59 | .setStyle(ButtonStyle.Primary), |
60 | - new ButtonBuilder().setLabel("Open GitHub β").setStyle(ButtonStyle.Link).setURL(prUrl), | |
60 | + new ButtonBuilder().setLabel("π Open GitHub β").setStyle(ButtonStyle.Link).setURL(prUrl), | |
61 | 61 | ); |
62 | 62 | return [row]; |
63 | 63 | } |
| @@ -66,19 +66,19 @@ export function createHomeNavButtons(): ActionRowBuilder<ButtonBuilder>[] { | ||
66 | 66 | const row = new ActionRowBuilder<ButtonBuilder>().addComponents( |
67 | 67 | new ButtonBuilder() |
68 | 68 | .setCustomId("home:nav:repos") |
69 | - .setLabel("Repositories") | |
69 | + .setLabel("π¦ Repositories") | |
70 | 70 | .setStyle(ButtonStyle.Secondary), |
71 | 71 | new ButtonBuilder() |
72 | 72 | .setCustomId("home:nav:prs") |
73 | - .setLabel("Pull Requests") | |
73 | + .setLabel("π Pull Requests") | |
74 | 74 | .setStyle(ButtonStyle.Secondary), |
75 | 75 | new ButtonBuilder() |
76 | 76 | .setCustomId("home:nav:issues") |
77 | - .setLabel("Issues") | |
77 | + .setLabel("π Issues") | |
78 | 78 | .setStyle(ButtonStyle.Secondary), |
79 | 79 | new ButtonBuilder() |
80 | 80 | .setCustomId("home:nav:activity") |
81 | - .setLabel("Activity") | |
81 | + .setLabel("π Activity") | |
82 | 82 | .setStyle(ButtonStyle.Secondary), |
83 | 83 | ); |
84 | 84 | return [row]; |
| @@ -88,10 +88,10 @@ export function createDeleteSecretButton(messageId: string): ActionRowBuilder<Bu | ||
88 | 88 | return new ActionRowBuilder<ButtonBuilder>().addComponents( |
89 | 89 | new ButtonBuilder() |
90 | 90 | .setCustomId(`secret:delete:${messageId}`) |
91 | - .setLabel(`${NF.cross} Delete Leaked Message`) | |
91 | + .setLabel("ποΈ Delete Leaked Secret") | |
92 | 92 | .setStyle(ButtonStyle.Danger), |
93 | 93 | new ButtonBuilder() |
94 | - .setLabel("Security Guide β") | |
94 | + .setLabel("π‘οΈ Security Guide β") | |
95 | 95 | .setStyle(ButtonStyle.Link) |
96 | 96 | .setURL( |
97 | 97 | "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/token-expiration-and-revocation", |
98 | 98 | |
βΌapps/bot/src/ui/embeds.ts
+169-92
| @@ -21,7 +21,9 @@ import { EmbedBuilder } from "discord.js"; | ||
21 | 21 | import { BrandColors } from "./colors.js"; |
22 | 22 | import { NF } from "./icons.js"; |
23 | 23 | import { |
24 | - padText, | |
24 | + ANSI, | |
25 | + clipAnsi, | |
26 | + padAnsi, | |
25 | 27 | renderMeter, |
26 | 28 | renderTuiCard, |
27 | 29 | tuiBottomBar, |
| @@ -37,7 +39,7 @@ export function createBaseEmbed(title: string, description?: string): EmbedBuild | ||
37 | 39 | .setColor(BrandColors.primary) |
38 | 40 | .setTitle(title) |
39 | 41 | .setFooter({ |
40 | - text: "GITBOT TUI β’ Developer Operating System", | |
42 | + text: "GITBOT v2.4.0 β’ Interactive Terminal Console", | |
41 | 43 | iconURL: "https://github.githubassets.com/favicons/favicon.png", |
42 | 44 | }) |
43 | 45 | .setTimestamp(); |
| @@ -54,10 +56,10 @@ export function createErrorEmbed(error: Error | string): EmbedBuilder { | ||
54 | 56 | tuiTopBar("GITBOT ERROR"), |
55 | 57 | tuiPrompt("gitbot --last-error"), |
56 | 58 | tuiDivider("DIAGNOSTIC TRACE"), |
57 | - tuiLine(`Status : ${NF.cross} Execution Failed`), | |
58 | - tuiLine(`Error : ${message}`), | |
59 | + tuiLine(`${ANSI.red}Status : β Execution Failed${ANSI.reset}`), | |
60 | + tuiLine(`${ANSI.yellow}Error : ${clipAnsi(message, 30)}${ANSI.reset}`), | |
59 | 61 | tuiDivider("RECOVERY"), |
60 | - tuiLine("Check parameters or repo access."), | |
62 | + tuiLine(`${ANSI.dim}Check parameters or repository access.${ANSI.reset}`), | |
61 | 63 | tuiBottomBar(), |
62 | 64 | ]); |
63 | 65 | |
| @@ -75,7 +77,6 @@ export function createRepoDashboardEmbed( | ||
75 | 77 | metrics: RepoDashboardMetrics, |
76 | 78 | _activeTab = "overview", |
77 | 79 | ): EmbedBuilder { |
78 | - const descQuote = repo.description ? `> ${repo.description.replace(/\n/g, " ")}\n\n` : ""; | |
79 | 80 | const healthScore = Math.round( |
80 | 81 | Math.min( |
81 | 82 | 100, |
| @@ -87,58 +88,68 @@ export function createRepoDashboardEmbed( | ||
87 | 88 | ), |
88 | 89 | ), |
89 | 90 | ); |
91 | + | |
90 | 92 | const tui = renderTuiCard([ |
91 | 93 | tuiTopBar("GITBOT TUI v2.4.0"), |
92 | 94 | tuiPrompt(`gitbot repo ${repo.fullName}`), |
93 | 95 | tuiDivider("METADATA"), |
94 | - tuiLine(`Target : ${repo.fullName}`), | |
95 | 96 | tuiLine( |
96 | - `Stack : ${repo.language || "Plain Text"} β’ ${repo.isPrivate ? "Private" : "Public"}`, | |
97 | + `${ANSI.dim}Target :${ANSI.reset} ${ANSI.bold}${clipAnsi(repo.fullName, 29)}${ANSI.reset}`, | |
98 | + ), | |
99 | + tuiLine( | |
100 | + `${ANSI.dim}Stack :${ANSI.reset} ${ANSI.magenta}${repo.language || "Plain Text"}${ANSI.reset} β’ ${repo.isPrivate ? `${ANSI.red}Private` : `${ANSI.green}Public`}${ANSI.reset}`, | |
101 | + ), | |
102 | + tuiLine( | |
103 | + `${ANSI.dim}Branch :${ANSI.reset} ${ANSI.blue}${repo.defaultBranch}${ANSI.reset} β’ ${ANSI.yellow}${repo.license?.spdxId || "No License"}${ANSI.reset}`, | |
97 | 104 | ), |
98 | - tuiLine(`Branch : ${repo.defaultBranch} β’ ${repo.license?.spdxId || "No License"}`), | |
99 | 105 | tuiDivider("TELEMETRY"), |
100 | 106 | tuiRow2( |
101 | 107 | "Stars", |
102 | - `β
${repo.stars.toLocaleString()}`, | |
108 | + `${ANSI.yellow}${repo.stars.toLocaleString()}${ANSI.reset}`, | |
103 | 109 | "Forks", |
104 | - `β ${repo.forks.toLocaleString()}`, | |
110 | + `${ANSI.magenta}${repo.forks.toLocaleString()}${ANSI.reset}`, | |
105 | 111 | ), |
106 | 112 | tuiRow2( |
107 | 113 | "Issues", |
108 | - `β ${repo.openIssuesCount.toLocaleString()} Open`, | |
114 | + `${ANSI.green}${repo.openIssuesCount.toLocaleString()} Open${ANSI.reset}`, | |
109 | 115 | "PRs", |
110 | - `β ${metrics.activity.prsMerged} Merged`, | |
116 | + `${ANSI.cyan}${metrics.activity.prsMerged} Merged${ANSI.reset}`, | |
111 | 117 | ), |
112 | 118 | tuiDivider("14-DAY ACTIVITY"), |
113 | 119 | tuiRow2( |
114 | 120 | "Commits", |
115 | - `${metrics.activity.commitsCount} pushed`, | |
116 | - "PR Cycle", | |
117 | - formatDuration(metrics.cycleTime.avgPrCycleTimeMs), | |
121 | + `${ANSI.cyan}${metrics.activity.commitsCount} pushed${ANSI.reset}`, | |
122 | + "PR Time", | |
123 | + `${ANSI.white}${formatDuration(metrics.cycleTime.avgPrCycleTimeMs)}${ANSI.reset}`, | |
118 | 124 | ), |
119 | 125 | tuiRow2( |
120 | 126 | "Through", |
121 | - `${metrics.codingMetrics.prThroughputPerWeek.toFixed(1)} pr/wk`, | |
127 | + `${ANSI.white}${metrics.codingMetrics.prThroughputPerWeek.toFixed(1)} pr/wk${ANSI.reset}`, | |
122 | 128 | "Resolved", |
123 | - `${metrics.codingMetrics.issueResolutionRatePercent.toFixed(0)}%`, | |
129 | + `${ANSI.green}${metrics.codingMetrics.issueResolutionRatePercent.toFixed(0)}%${ANSI.reset}`, | |
124 | 130 | ), |
125 | 131 | tuiDivider("HEALTH INDEX"), |
126 | - tuiLine(`Health : ${renderMeter(healthScore)} ${healthScore}/100`), | |
132 | + tuiLine( | |
133 | + `${ANSI.dim}Health :${ANSI.reset} ${renderMeter(healthScore)} ${ANSI.green}${healthScore}/100${ANSI.reset}`, | |
134 | + ), | |
127 | 135 | tuiBottomBar(), |
128 | 136 | ]); |
129 | 137 | |
138 | + const descQuote = repo.description ? `> *${repo.description.replace(/\n/g, " ")}*\n\n` : ""; | |
139 | + const badges = `β **${repo.stars.toLocaleString()}** Stars β’ π΄ **${repo.forks.toLocaleString()}** Forks β’ ${repo.isPrivate ? "π΄ **Private**" : "π’ **Public**"} β’ π·οΈ **${repo.license?.spdxId || "No License"}**\n\n`; | |
140 | + | |
130 | 141 | return new EmbedBuilder() |
131 | - .setColor(BrandColors.github) | |
142 | + .setColor(healthScore >= 80 ? BrandColors.success : BrandColors.warning) | |
132 | 143 | .setTitle(`${NF.github} ${repo.fullName}`) |
133 | 144 | .setAuthor({ |
134 | - name: `GITBOT TUI β’ ${repo.fullName}`, | |
145 | + name: `GITBOT Terminal β’ ${repo.fullName}`, | |
135 | 146 | iconURL: repo.owner.avatarUrl, |
136 | 147 | url: repo.htmlUrl, |
137 | 148 | }) |
138 | 149 | .setURL(repo.htmlUrl) |
139 | - .setDescription(descQuote + tui) | |
150 | + .setDescription(`${descQuote}${badges}${tui}`) | |
140 | 151 | .setFooter({ |
141 | - text: "GITBOT TUI β’ Use buttons below to switch tabs", | |
152 | + text: "GITBOT v2.4.0 β’ Interactive Terminal Console β’ Press buttons below to navigate", | |
142 | 153 | iconURL: "https://github.githubassets.com/favicons/favicon.png", |
143 | 154 | }) |
144 | 155 | .setTimestamp(); |
| @@ -149,36 +160,50 @@ export function createRepoHealthEmbed(repo: GitHubRepo, health: RepoHealthScore) | ||
149 | 160 | tuiTopBar(`HEALTH SCORE: ${health.overallScore}/100`), |
150 | 161 | tuiPrompt(`gitbot repo ${repo.fullName} --health`), |
151 | 162 | tuiDivider("METRIC GAUGES"), |
152 | - tuiLine(`Activity ${renderMeter(health.activityScore)} ${health.activityScore}/100`), | |
153 | - tuiLine(`Maintenance ${renderMeter(health.maintenanceScore)} ${health.maintenanceScore}/100`), | |
154 | - tuiLine(`CI / CD ${renderMeter(health.ciScore)} ${health.ciScore}/100`), | |
155 | - tuiLine(`Community ${renderMeter(health.communityScore)} ${health.communityScore}/100`), | |
156 | - tuiLine(`Releases ${renderMeter(health.releasesScore)} ${health.releasesScore}/100`), | |
163 | + tuiLine( | |
164 | + `${ANSI.dim}Activity ${ANSI.reset}${renderMeter(health.activityScore)} ${ANSI.green}${health.activityScore}/100${ANSI.reset}`, | |
165 | + ), | |
166 | + tuiLine( | |
167 | + `${ANSI.dim}Maintenance ${ANSI.reset}${renderMeter(health.maintenanceScore)} ${ANSI.green}${health.maintenanceScore}/100${ANSI.reset}`, | |
168 | + ), | |
169 | + tuiLine( | |
170 | + `${ANSI.dim}CI / CD ${ANSI.reset}${renderMeter(health.ciScore)} ${ANSI.green}${health.ciScore}/100${ANSI.reset}`, | |
171 | + ), | |
172 | + tuiLine( | |
173 | + `${ANSI.dim}Community ${ANSI.reset}${renderMeter(health.communityScore)} ${ANSI.green}${health.communityScore}/100${ANSI.reset}`, | |
174 | + ), | |
175 | + tuiLine( | |
176 | + `${ANSI.dim}Releases ${ANSI.reset}${renderMeter(health.releasesScore)} ${ANSI.green}${health.releasesScore}/100${ANSI.reset}`, | |
177 | + ), | |
157 | 178 | tuiDivider("AUDIT METRICS"), |
158 | 179 | tuiRow2( |
159 | 180 | "Commits(30d)", |
160 | - `${health.details.commits30d}`, | |
181 | + `${ANSI.cyan}${health.details.commits30d}${ANSI.reset}`, | |
161 | 182 | "Open Ratio", |
162 | - `${health.details.openIssuesRatio}`, | |
183 | + `${ANSI.yellow}${health.details.openIssuesRatio}${ANSI.reset}`, | |
163 | 184 | ), |
164 | 185 | tuiRow2( |
165 | 186 | "Releases", |
166 | - `${health.details.releasesCount} tags`, | |
187 | + `${ANSI.magenta}${health.details.releasesCount} tags${ANSI.reset}`, | |
167 | 188 | "Status", |
168 | - health.overallScore >= 80 ? "HEALTHY" : "NEEDS WORK", | |
189 | + health.overallScore >= 80 | |
190 | + ? `${ANSI.green}HEALTHY${ANSI.reset}` | |
191 | + : `${ANSI.yellow}NEEDS WORK${ANSI.reset}`, | |
169 | 192 | ), |
170 | 193 | tuiBottomBar(), |
171 | 194 | ]); |
172 | 195 | |
196 | + const badges = `β€οΈ **Score:** \`${health.overallScore}/100\` β’ ${health.overallScore >= 80 ? "π’ **Healthy**" : "π‘ **Needs Maintenance**"} β’ π **${health.details.commits30d}** Commits (30d)\n\n`; | |
197 | + | |
173 | 198 | return createBaseEmbed(`${NF.heart} Health Score: ${repo.fullName}`) |
174 | 199 | .setAuthor({ |
175 | - name: `GITBOT TUI β’ ${repo.fullName}`, | |
200 | + name: `GITBOT Terminal β’ ${repo.fullName}`, | |
176 | 201 | iconURL: repo.owner.avatarUrl, |
177 | 202 | url: repo.htmlUrl, |
178 | 203 | }) |
179 | 204 | .setURL(repo.htmlUrl) |
180 | 205 | .setColor(health.overallScore > 80 ? BrandColors.success : BrandColors.warning) |
181 | - .setDescription(tui); | |
206 | + .setDescription(`${badges}${tui}`); | |
182 | 207 | } |
183 | 208 | |
184 | 209 | export function createRepoGrowthEmbed(repo: GitHubRepo, growth: RepoGrowthMetrics): EmbedBuilder { |
| @@ -187,53 +212,70 @@ export function createRepoGrowthEmbed(repo: GitHubRepo, growth: RepoGrowthMetric | ||
187 | 212 | tuiPrompt(`gitbot repo ${repo.fullName} --growth`), |
188 | 213 | tuiDivider("TELEMETRY ACCELERATION"), |
189 | 214 | tuiLine( |
190 | - `Stars Total : ${growth.starsTotal.toLocaleString()} (+${growth.starsDelta30d.toLocaleString()} in 30d)`, | |
215 | + `${ANSI.dim}Stars Total :${ANSI.reset} ${ANSI.yellow}${growth.starsTotal.toLocaleString()} (+${growth.starsDelta30d.toLocaleString()} in 30d)${ANSI.reset}`, | |
216 | + ), | |
217 | + tuiLine( | |
218 | + `${ANSI.dim}Forks Total :${ANSI.reset} ${ANSI.magenta}${growth.forksTotal.toLocaleString()} (+${growth.forksDelta30d.toLocaleString()} in 30d)${ANSI.reset}`, | |
191 | 219 | ), |
192 | 220 | tuiLine( |
193 | - `Forks Total : ${growth.forksTotal.toLocaleString()} (+${growth.forksDelta30d.toLocaleString()} in 30d)`, | |
221 | + `${ANSI.dim}Contributors :${ANSI.reset} ${ANSI.cyan}${growth.contributorsTotal} authors${ANSI.reset}`, | |
194 | 222 | ), |
195 | - tuiLine(`Contributors : ${growth.contributorsTotal} authors`), | |
196 | 223 | tuiLine( |
197 | - `PRs Merged : ${growth.prsMerged30d} (${(growth.prsMerged30d / 4.2).toFixed(1)} / week)`, | |
224 | + `${ANSI.dim}PRs Merged :${ANSI.reset} ${ANSI.green}${growth.prsMerged30d} (${(growth.prsMerged30d / 4.2).toFixed(1)}/wk)${ANSI.reset}`, | |
198 | 225 | ), |
199 | 226 | tuiBottomBar(), |
200 | 227 | ]); |
201 | 228 | |
229 | + const badges = `π **Velocity:** \`${(growth.prsMerged30d / 4.2).toFixed(1)} PRs/wk\` β’ β **+${growth.starsDelta30d}** Stars β’ π₯ **${growth.contributorsTotal}** Authors\n\n`; | |
230 | + | |
202 | 231 | return createBaseEmbed(`${NF.speedometer} 30-Day Growth: ${repo.fullName}`) |
203 | 232 | .setAuthor({ |
204 | - name: `GITBOT TUI β’ ${repo.fullName}`, | |
233 | + name: `GITBOT Terminal β’ ${repo.fullName}`, | |
205 | 234 | iconURL: repo.owner.avatarUrl, |
206 | 235 | url: repo.htmlUrl, |
207 | 236 | }) |
208 | 237 | .setColor(BrandColors.primary) |
209 | 238 | .setURL(repo.htmlUrl) |
210 | - .setDescription(tui); | |
239 | + .setDescription(`${badges}${tui}`); | |
211 | 240 | } |
212 | 241 | |
213 | 242 | export function createDependenciesEmbed(repo: GitHubRepo, deps: RepoDependencies): EmbedBuilder { |
214 | 243 | const topDepsLines = deps.dependencies.slice(0, 8).map((d) => { |
215 | - return tuiLine(`βΈ ${padText(d.name, 22)} ${d.version}`); | |
244 | + return tuiLine( | |
245 | + `${ANSI.cyan}βΈ${ANSI.reset} ${ANSI.bold}${padAnsi(clipAnsi(d.name, 22), 22)}${ANSI.reset} ${ANSI.green}${clipAnsi(d.version, 13)}${ANSI.reset}`, | |
246 | + ); | |
216 | 247 | }); |
217 | 248 | |
218 | 249 | const tui = renderTuiCard([ |
219 | 250 | tuiTopBar("DEPENDENCY MANIFEST"), |
220 | 251 | tuiPrompt(`gitbot repo ${repo.fullName} --deps`), |
221 | 252 | tuiDivider("ECOSYSTEM & PACKAGES"), |
222 | - tuiLine(`Manifest : ${deps.manifestFile} (${deps.ecosystem})`), | |
223 | - tuiRow2("Total", `${deps.totalCount}`, "Outdated", `${deps.outdatedCount}`), | |
253 | + tuiLine( | |
254 | + `${ANSI.dim}Manifest :${ANSI.reset} ${ANSI.yellow}${clipAnsi(deps.manifestFile, 18)}${ANSI.reset} (${deps.ecosystem})`, | |
255 | + ), | |
256 | + tuiRow2( | |
257 | + "Total", | |
258 | + `${ANSI.cyan}${deps.totalCount}${ANSI.reset}`, | |
259 | + "Outdated", | |
260 | + deps.outdatedCount > 0 | |
261 | + ? `${ANSI.red}${deps.outdatedCount}${ANSI.reset}` | |
262 | + : `${ANSI.green}0${ANSI.reset}`, | |
263 | + ), | |
224 | 264 | tuiDivider("TOP PACKAGES"), |
225 | 265 | ...(topDepsLines.length > 0 ? topDepsLines : [tuiLine("No dependencies detected.")]), |
226 | 266 | tuiBottomBar(), |
227 | 267 | ]); |
228 | 268 | |
269 | + const badges = `π¦ **Packages:** \`${deps.totalCount}\` β’ ${deps.outdatedCount > 0 ? `β οΈ **${deps.outdatedCount}** Outdated` : "π’ **All Up to Date**"} β’ π **${deps.manifestFile}**\n\n`; | |
270 | + | |
229 | 271 | return createBaseEmbed(`${NF.network} Dependencies: ${repo.fullName}`) |
230 | 272 | .setAuthor({ |
231 | - name: `GITBOT TUI β’ ${repo.fullName}`, | |
273 | + name: `GITBOT Terminal β’ ${repo.fullName}`, | |
232 | 274 | iconURL: repo.owner.avatarUrl, |
233 | 275 | url: repo.htmlUrl, |
234 | 276 | }) |
235 | 277 | .setURL(repo.htmlUrl) |
236 | - .setDescription(tui); | |
278 | + .setDescription(`${badges}${tui}`); | |
237 | 279 | } |
238 | 280 | |
239 | 281 | // 2. PR POWER TOOLS |
| @@ -252,44 +294,55 @@ export function createDetailedPrEmbed( | ||
252 | 294 | : "In Review"; |
253 | 295 | |
254 | 296 | const approvedCount = pr.reviews.filter((r) => r.state === "APPROVED").length; |
255 | - const ciStatusText = | |
256 | - pr.ciStatus === "success" | |
257 | - ? "[β] Passing" | |
258 | - : pr.ciStatus === "failure" | |
259 | - ? "[β] Failed" | |
260 | - : "[β ] Running"; | |
261 | - const conflictText = pr.mergeable === false ? "[β] Conflicts" : "[β] Clean"; | |
297 | + const ciColor = | |
298 | + pr.ciStatus === "success" ? ANSI.green : pr.ciStatus === "failure" ? ANSI.red : ANSI.yellow; | |
299 | + const ciStatusText = `${ciColor}${pr.ciStatus === "success" ? "β Passing" : pr.ciStatus === "failure" ? "β Failed" : "β Running"}${ANSI.reset}`; | |
300 | + const conflictText = | |
301 | + pr.mergeable === false | |
302 | + ? `${ANSI.red}β Conflicts${ANSI.reset}` | |
303 | + : `${ANSI.green}β Clean${ANSI.reset}`; | |
262 | 304 | |
263 | 305 | const tui = renderTuiCard([ |
264 | 306 | tuiTopBar(`PR #${pr.number} INSPECTION`), |
265 | 307 | tuiPrompt(`gh pr view ${pr.number}`), |
266 | 308 | tuiDivider("BRANCH & STATUS"), |
267 | - tuiLine(`Title : ${pr.title}`), | |
268 | - tuiLine(`Branch : ${pr.headBranch} -> ${pr.baseBranch}`), | |
309 | + tuiLine(`${ANSI.dim}Title :${ANSI.reset} ${clipAnsi(pr.title, 28)}`), | |
310 | + tuiLine( | |
311 | + `${ANSI.dim}Branch :${ANSI.reset} ${ANSI.cyan}${clipAnsi(pr.headBranch, 12)}${ANSI.reset} β ${ANSI.blue}${clipAnsi(pr.baseBranch, 12)}${ANSI.reset}`, | |
312 | + ), | |
269 | 313 | tuiRow2("Author", `@${pr.author.login}`, "Status", statusBadge), |
270 | 314 | tuiDivider("DIFF & CHECKS"), |
271 | - tuiLine(`Changes : +${pr.additions} -${pr.deletions} (${pr.changedFiles} files)`), | |
315 | + tuiLine( | |
316 | + `${ANSI.dim}Changes :${ANSI.reset} ${ANSI.green}+${pr.additions}${ANSI.reset} ${ANSI.red}-${pr.deletions}${ANSI.reset} (${pr.changedFiles} files)`, | |
317 | + ), | |
272 | 318 | tuiRow2("CI Check", ciStatusText, "Conflicts", conflictText), |
273 | - tuiRow2("Reviews", `${approvedCount} approved`, "Cycle Time", formatDuration(pr.cycleTimeMs)), | |
319 | + tuiRow2( | |
320 | + "Reviews", | |
321 | + `${ANSI.green}${approvedCount} approved${ANSI.reset}`, | |
322 | + "Cycle Time", | |
323 | + formatDuration(pr.cycleTimeMs), | |
324 | + ), | |
274 | 325 | tuiBottomBar(), |
275 | 326 | ]); |
276 | 327 | |
328 | + const badges = `${isReady ? "π’ **Ready to Merge**" : pr.draft ? "βͺ **Draft**" : "π‘ **In Review**"} β’ πΏ \`${pr.headBranch}\` β \`${pr.baseBranch}\` β’ π€ **@${pr.author.login}**\n\n`; | |
329 | + | |
277 | 330 | const embed = createBaseEmbed(`${NF.gitPullRequest} PR #${pr.number}: ${pr.title}`) |
278 | 331 | .setURL(pr.htmlUrl) |
279 | 332 | .setColor(isReady ? BrandColors.success : BrandColors.warning) |
280 | - .setDescription(tui); | |
333 | + .setDescription(`${badges}${tui}`); | |
281 | 334 | |
282 | 335 | const warnings: string[] = []; |
283 | 336 | if (pr.changedFiles > 15 || pr.additions + pr.deletions > 500) { |
284 | - warnings.push(`${NF.warning} High change complexity (> 500 lines or > 15 files)`); | |
337 | + warnings.push(`${NF.warning} **High change complexity** (> 500 lines or > 15 files)`); | |
285 | 338 | } |
286 | 339 | if (pr.waitingOn && pr.waitingOn.length > 0) { |
287 | 340 | warnings.push( |
288 | - `${NF.clock} Waiting on review: ${pr.waitingOn.map((u) => `@${u}`).join(", ")} (${pr.waitingHours}h)`, | |
341 | + `${NF.clock} **Waiting on review:** ${pr.waitingOn.map((u) => `@${u}`).join(", ")} (${pr.waitingHours}h)`, | |
289 | 342 | ); |
290 | 343 | } |
291 | 344 | if (pr.isStale) { |
292 | - warnings.push(`${NF.warning} Pull request has been inactive for over 14 days`); | |
345 | + warnings.push(`${NF.warning} **Stale PR:** Inactive for over 14 days`); | |
293 | 346 | } |
294 | 347 | |
295 | 348 | if (warnings.length > 0) { |
| @@ -358,7 +411,7 @@ export function createActionsTreeEmbed(repo: string, runs: any[]): EmbedBuilder | ||
358 | 411 | const prefix = isLast ? "βββ" : "βββ"; |
359 | 412 | const statusIcon = |
360 | 413 | r.conclusion === "success" ? NF.check : r.conclusion === "failure" ? NF.cross : NF.spinner; |
361 | - return `${prefix} ${padText(r.name, 16)} ${statusIcon} (${r.headBranch})`; | |
414 | + return `${prefix} ${padAnsi(clipAnsi(r.name, 16), 16)} ${statusIcon} (${r.headBranch})`; | |
362 | 415 | }); |
363 | 416 | |
364 | 417 | return createBaseEmbed(`${NF.robot} GitHub Actions: ${repo}`).setDescription( |
| @@ -445,10 +498,20 @@ export function createHomeDashboardEmbed( | ||
445 | 498 | ): EmbedBuilder { |
446 | 499 | const tui = renderTuiCard([ |
447 | 500 | tuiTopBar("DEVELOPER CENTER"), |
448 | - tuiPrompt(`gitbot whoami --stats`), | |
501 | + tuiPrompt("gitbot whoami --stats"), | |
449 | 502 | tuiDivider(`USER: @${username.toUpperCase()}`), |
450 | - tuiRow2("Commits", `${userStats.commits}`, "PRs", `${userStats.prs}`), | |
451 | - tuiRow2("Reviews", `${userStats.reviews}`, "Issues", `${userStats.issues}`), | |
503 | + tuiRow2( | |
504 | + "Commits", | |
505 | + `${ANSI.cyan}${userStats.commits}${ANSI.reset}`, | |
506 | + "PRs", | |
507 | + `${ANSI.green}${userStats.prs}${ANSI.reset}`, | |
508 | + ), | |
509 | + tuiRow2( | |
510 | + "Reviews", | |
511 | + `${ANSI.yellow}${userStats.reviews}${ANSI.reset}`, | |
512 | + "Issues", | |
513 | + `${ANSI.magenta}${userStats.issues}${ANSI.reset}`, | |
514 | + ), | |
452 | 515 | tuiBottomBar(), |
453 | 516 | ]); |
454 | 517 | |
| @@ -470,13 +533,17 @@ export function createConnectStatusEmbed( | ||
470 | 533 | tuiTopBar("AUTH SUBSYSTEM"), |
471 | 534 | tuiPrompt("gitbot auth status"), |
472 | 535 | tuiDivider("SESSION METADATA"), |
473 | - tuiLine(`Identity : @${account}`), | |
474 | - tuiLine(`Status : ${isConn ? "[β] Connected via App" : "[β] Disconnected"}`), | |
536 | + tuiLine(`${ANSI.dim}Identity :${ANSI.reset} ${ANSI.bold}@${account}${ANSI.reset}`), | |
537 | + tuiLine( | |
538 | + `${ANSI.dim}Status :${ANSI.reset} ${isConn ? `${ANSI.green}β Connected via App` : `${ANSI.red}β Disconnected`}${ANSI.reset}`, | |
539 | + ), | |
475 | 540 | tuiDivider("CAPABILITIES"), |
476 | - tuiLine(`Repos Read/Watch : [β] Enabled`), | |
477 | - tuiLine(`Issues & PRs : [β] Enabled`), | |
478 | - tuiLine(`Actions CI : [β] Enabled`), | |
479 | - tuiLine(`Write & Merge : ${scopes.includes("write") ? "[β] Enabled" : "[β] Read Mode"}`), | |
541 | + tuiLine(`${ANSI.dim}Repos Read/Watch :${ANSI.reset} ${ANSI.green}β Enabled${ANSI.reset}`), | |
542 | + tuiLine(`${ANSI.dim}Issues & PRs :${ANSI.reset} ${ANSI.green}β Enabled${ANSI.reset}`), | |
543 | + tuiLine(`${ANSI.dim}Actions CI :${ANSI.reset} ${ANSI.green}β Enabled${ANSI.reset}`), | |
544 | + tuiLine( | |
545 | + `${ANSI.dim}Write & Merge :${ANSI.reset} ${scopes.includes("write") ? `${ANSI.green}β Enabled` : `${ANSI.yellow}β Read Mode`}${ANSI.reset}`, | |
546 | + ), | |
480 | 547 | tuiBottomBar(), |
481 | 548 | ]); |
482 | 549 | |
| @@ -497,101 +564,111 @@ export function createRepoCommitsEmbed(repo: GitHubRepo, commits: GitHubCommit[] | ||
497 | 564 | const commitLines = commits.slice(0, 7).map((c) => { |
498 | 565 | const sha = c.sha.slice(0, 7); |
499 | 566 | const msg = c.message.split("\n")[0]; |
500 | - return tuiLine(`βΈ ${sha} ${msg}`); | |
567 | + return tuiLine( | |
568 | + `${ANSI.cyan}βΈ${ANSI.reset} ${ANSI.yellow}${sha}${ANSI.reset} ${clipAnsi(msg, 28)}`, | |
569 | + ); | |
501 | 570 | }); |
502 | 571 | |
503 | 572 | const tui = renderTuiCard([ |
504 | 573 | tuiTopBar("GIT LOG --ONELINE"), |
505 | - tuiPrompt(`git log -n 7 --oneline`), | |
574 | + tuiPrompt("git log -n 7 --oneline"), | |
506 | 575 | tuiDivider("RECENT COMMITS"), |
507 | 576 | ...(commitLines.length > 0 ? commitLines : [tuiLine("No commits recorded.")]), |
508 | 577 | tuiBottomBar(), |
509 | 578 | ]); |
510 | 579 | |
511 | 580 | const markdownLinks = commits |
512 | - .slice(0, 6) | |
581 | + .slice(0, 5) | |
513 | 582 | .map( |
514 | 583 | (c) => |
515 | - `β’ [\`${c.sha.slice(0, 7)}\`](${c.htmlUrl}) ${c.message.split("\n")[0]} β *@${c.author.name}*`, | |
584 | + `β’ [\`${c.sha.slice(0, 7)}\`](${c.htmlUrl}) **${c.message.split("\n")[0].slice(0, 45)}** β *@${c.author.name}*`, | |
516 | 585 | ) |
517 | 586 | .join("\n"); |
518 | 587 | |
519 | 588 | return createBaseEmbed(`${NF.gitCommit} Commits: ${repo.fullName}`) |
520 | 589 | .setAuthor({ |
521 | - name: `GITBOT TUI β’ ${repo.fullName}`, | |
590 | + name: `GITBOT Terminal β’ ${repo.fullName}`, | |
522 | 591 | iconURL: repo.owner.avatarUrl, |
523 | 592 | url: repo.htmlUrl, |
524 | 593 | }) |
525 | 594 | .setURL(repo.htmlUrl) |
526 | - .setDescription(`${tui}\n\n**Commit History:**\n${markdownLinks || "None"}`); | |
595 | + .setDescription(`${tui}\n\n**π Direct Commit Links:**\n${markdownLinks || "None"}`); | |
527 | 596 | } |
528 | 597 | |
529 | 598 | export function createRepoPrsEmbed(repo: GitHubRepo, prs: GitHubPullRequest[]): EmbedBuilder { |
530 | 599 | const prLines = prs.slice(0, 7).map((p) => { |
531 | 600 | const status = p.state.toUpperCase(); |
532 | - return tuiLine(`[#${p.number}] ${padText(p.title, 24)} (${status})`); | |
601 | + const color = p.state === "open" ? ANSI.green : ANSI.magenta; | |
602 | + return tuiLine( | |
603 | + `${ANSI.cyan}[#${p.number}]${ANSI.reset} ${padAnsi(clipAnsi(p.title, 22), 22)} (${color}${status}${ANSI.reset})`, | |
604 | + ); | |
533 | 605 | }); |
534 | 606 | |
535 | 607 | const tui = renderTuiCard([ |
536 | 608 | tuiTopBar("PULL REQUEST QUEUE"), |
537 | - tuiPrompt(`gh pr list --state all --limit 7`), | |
609 | + tuiPrompt("gh pr list --state all --limit 7"), | |
538 | 610 | tuiDivider("RECENT PR RECORDS"), |
539 | 611 | ...(prLines.length > 0 ? prLines : [tuiLine("No pull requests found.")]), |
540 | 612 | tuiBottomBar(), |
541 | 613 | ]); |
542 | 614 | |
543 | 615 | const markdownLinks = prs |
544 | - .slice(0, 6) | |
545 | - .map((p) => `β’ [#${p.number}](${p.htmlUrl}) **${p.title}** (\`${p.state}\`)`) | |
616 | + .slice(0, 5) | |
617 | + .map((p) => `β’ [#${p.number}](${p.htmlUrl}) **${p.title.slice(0, 45)}** (\`${p.state}\`)`) | |
546 | 618 | .join("\n"); |
547 | 619 | |
548 | 620 | return createBaseEmbed(`${NF.gitPullRequest} Pull Requests: ${repo.fullName}`) |
549 | 621 | .setAuthor({ |
550 | - name: `GITBOT TUI β’ ${repo.fullName}`, | |
622 | + name: `GITBOT Terminal β’ ${repo.fullName}`, | |
551 | 623 | iconURL: repo.owner.avatarUrl, |
552 | 624 | url: repo.htmlUrl, |
553 | 625 | }) |
554 | 626 | .setURL(repo.htmlUrl) |
555 | - .setDescription(`${tui}\n\n**Quick PR Links:**\n${markdownLinks || "None"}`); | |
627 | + .setDescription(`${tui}\n\n**π Direct PR Links:**\n${markdownLinks || "None"}`); | |
556 | 628 | } |
557 | 629 | |
558 | 630 | export function createRepoIssuesEmbed(repo: GitHubRepo, issues: GitHubIssue[]): EmbedBuilder { |
559 | 631 | const issueLines = issues.slice(0, 7).map((i) => { |
560 | 632 | const status = i.state.toUpperCase(); |
561 | - return tuiLine(`[#${i.number}] ${padText(i.title, 24)} (${status})`); | |
633 | + const color = i.state === "open" ? ANSI.green : ANSI.magenta; | |
634 | + return tuiLine( | |
635 | + `${ANSI.cyan}[#${i.number}]${ANSI.reset} ${padAnsi(clipAnsi(i.title, 22), 22)} (${color}${status}${ANSI.reset})`, | |
636 | + ); | |
562 | 637 | }); |
563 | 638 | |
564 | 639 | const tui = renderTuiCard([ |
565 | 640 | tuiTopBar("ISSUE TRACKER"), |
566 | - tuiPrompt(`gh issue list --limit 7`), | |
641 | + tuiPrompt("gh issue list --limit 7"), | |
567 | 642 | tuiDivider("ACTIVE ISSUES"), |
568 | 643 | ...(issueLines.length > 0 ? issueLines : [tuiLine("No issues found.")]), |
569 | 644 | tuiBottomBar(), |
570 | 645 | ]); |
571 | 646 | |
572 | 647 | const markdownLinks = issues |
573 | - .slice(0, 6) | |
574 | - .map((i) => `β’ [#${i.number}](${i.htmlUrl}) **${i.title}** (\`${i.state}\`)`) | |
648 | + .slice(0, 5) | |
649 | + .map((i) => `β’ [#${i.number}](${i.htmlUrl}) **${i.title.slice(0, 45)}** (\`${i.state}\`)`) | |
575 | 650 | .join("\n"); |
576 | 651 | |
577 | 652 | return createBaseEmbed(`${NF.issue} Issues: ${repo.fullName}`) |
578 | 653 | .setAuthor({ |
579 | - name: `GITBOT TUI β’ ${repo.fullName}`, | |
654 | + name: `GITBOT Terminal β’ ${repo.fullName}`, | |
580 | 655 | iconURL: repo.owner.avatarUrl, |
581 | 656 | url: repo.htmlUrl, |
582 | 657 | }) |
583 | 658 | .setURL(repo.htmlUrl) |
584 | - .setDescription(`${tui}\n\n**Quick Issue Links:**\n${markdownLinks || "None"}`); | |
659 | + .setDescription(`${tui}\n\n**π Direct Issue Links:**\n${markdownLinks || "None"}`); | |
585 | 660 | } |
586 | 661 | |
587 | 662 | export function createRepoReleasesEmbed(repo: GitHubRepo, releases: GitHubRelease[]): EmbedBuilder { |
588 | 663 | const releaseLines = releases.slice(0, 5).map((r) => { |
589 | - return tuiLine(`βΈ ${padText(r.tagName, 12)} ${r.name}`); | |
664 | + return tuiLine( | |
665 | + `${ANSI.cyan}βΈ${ANSI.reset} ${ANSI.yellow}${padAnsi(clipAnsi(r.tagName, 10), 10)}${ANSI.reset} ${clipAnsi(r.name, 22)}`, | |
666 | + ); | |
590 | 667 | }); |
591 | 668 | |
592 | 669 | const tui = renderTuiCard([ |
593 | 670 | tuiTopBar("RELEASE TAGS"), |
594 | - tuiPrompt(`gh release list --limit 5`), | |
671 | + tuiPrompt("gh release list --limit 5"), | |
595 | 672 | tuiDivider("PUBLISHED RELEASES"), |
596 | 673 | ...(releaseLines.length > 0 ? releaseLines : [tuiLine("No releases found.")]), |
597 | 674 | tuiBottomBar(), |
| @@ -607,12 +684,12 @@ export function createRepoReleasesEmbed(repo: GitHubRepo, releases: GitHubReleas | ||
607 | 684 | |
608 | 685 | return createBaseEmbed(`${NF.gitTag} Releases: ${repo.fullName}`) |
609 | 686 | .setAuthor({ |
610 | - name: `GITBOT TUI β’ ${repo.fullName}`, | |
687 | + name: `GITBOT Terminal β’ ${repo.fullName}`, | |
611 | 688 | iconURL: repo.owner.avatarUrl, |
612 | 689 | url: repo.htmlUrl, |
613 | 690 | }) |
614 | 691 | .setURL(repo.htmlUrl) |
615 | - .setDescription(`${tui}\n\n**Release Details:**\n${markdownLinks || "None"}`); | |
692 | + .setDescription(`${tui}\n\n**π·οΈ Direct Release Links:**\n${markdownLinks || "None"}`); | |
616 | 693 | } |
617 | 694 | |
618 | 695 | export function createSecurityAlertEmbed(secret: DetectedSecret): EmbedBuilder { |
619 | 696 | |
βΌapps/bot/src/ui/icons.ts
+40-33
| @@ -1,40 +1,47 @@ | ||
1 | -// Universal Developer Glyph Suite for GITBOT | |
2 | -// 100% supported across all Discord clients (Desktop, Web, iOS, Android) | |
3 | -// Uses standard Unicode BMP glyphs (zero cartoon emojis, zero unrendered PUA codepoints) | |
1 | +// Expressive Developer Emoji & Glyph Suite for GITBOT | |
2 | +// Vibrant, colorful, and supported across Discord Desktop, Web, iOS, and Android. | |
4 | 3 | |
5 | 4 | export const NF = { |
6 | 5 | // Brand & Git |
7 | - github: "β", | |
8 | - gitBranch: "β", | |
9 | - gitCommit: "βͺ", | |
10 | - gitPullRequest: "β", | |
11 | - gitMerge: "β", | |
12 | - gitTag: "β", | |
13 | - gitFork: "β", | |
6 | + github: "π", | |
7 | + gitBranch: "πΏ", | |
8 | + gitCommit: "π", | |
9 | + gitPullRequest: "π", | |
10 | + gitMerge: "π", | |
11 | + gitTag: "π·οΈ", | |
12 | + gitFork: "π΄", | |
13 | + issue: "π", | |
14 | + bug: "π", | |
14 | 15 | |
15 | 16 | // Status & Telemetry |
16 | - check: "β", | |
17 | - cross: "β", | |
18 | - warning: "!", | |
19 | - spinner: "β ", | |
20 | - star: "β
", | |
21 | - flame: "β²", | |
22 | - clock: "β·", | |
23 | - speedometer: "β", | |
24 | - package: "βͺ", | |
25 | - terminal: "βΈ", | |
26 | - document: "β‘", | |
27 | - search: "β", | |
28 | - shield: "β", | |
29 | - network: "β", | |
30 | - heart: "β₯", | |
31 | - user: "@", | |
32 | - users: "@@", | |
17 | + check: "β
", | |
18 | + cross: "β", | |
19 | + warning: "β οΈ", | |
20 | + spinner: "β³", | |
21 | + star: "β", | |
22 | + flame: "π₯", | |
23 | + clock: "β±οΈ", | |
24 | + speedometer: "π", | |
25 | + package: "π¦", | |
26 | + terminal: "π»", | |
27 | + document: "π", | |
28 | + search: "π", | |
29 | + shield: "π‘οΈ", | |
30 | + network: "π", | |
31 | + heart: "β€οΈ", | |
32 | + user: "π€", | |
33 | + users: "π₯", | |
33 | 34 | bullet: "β’", |
34 | - arrowRight: "β", | |
35 | - robot: "β", | |
36 | - sparkle: "β¦", | |
37 | - chat: "βΈ", | |
38 | - issue: "β", | |
39 | - bug: "βͺ", | |
35 | + arrowRight: "β", | |
36 | + robot: "π€", | |
37 | + sparkle: "β¨", | |
38 | + chat: "π¬", | |
39 | + rocket: "π", | |
40 | + tools: "π οΈ", | |
41 | + bell: "π", | |
42 | + lock: "π", | |
43 | + unlock: "π", | |
44 | + greenDot: "π’", | |
45 | + yellowDot: "π‘", | |
46 | + redDot: "π΄", | |
40 | 47 | } as const; |
41 | 48 | |
βΌapps/bot/src/ui/tui.test.ts
+20-17
| @@ -3,51 +3,54 @@ import { | ||
3 | 3 | TUI_WIDTH, |
4 | 4 | renderMeter, |
5 | 5 | renderTuiCard, |
6 | + stripAnsi, | |
6 | 7 | tuiBottomBar, |
7 | 8 | tuiDivider, |
8 | 9 | tuiLine, |
9 | 10 | tuiPrompt, |
10 | 11 | tuiRow2, |
11 | 12 | tuiTopBar, |
13 | + visibleLength, | |
12 | 14 | } from "./tui.js"; |
13 | 15 | |
14 | 16 | describe("TUI Rendering Engine", () => { |
15 | - it("enforces exact fixed line width of 43 characters across all components", () => { | |
17 | + it("enforces exact fixed visible width of 43 characters across all components", () => { | |
16 | 18 | const top = tuiTopBar("GITBOT TUI"); |
17 | 19 | const prompt = tuiPrompt("gitbot test"); |
18 | 20 | const div = tuiDivider("SECTION"); |
19 | 21 | const plainLine = tuiLine("Sample text here"); |
20 | - const row = tuiRow2("Stars", "β
10", "Forks", "β 2"); | |
22 | + const row = tuiRow2("Stars", "β 10", "Forks", "π΄ 2"); | |
21 | 23 | const singleRow = tuiRow2("License", "MIT License"); |
22 | 24 | const bottom = tuiBottomBar(); |
23 | 25 | |
24 | - expect(top.length).toBe(TUI_WIDTH); | |
25 | - expect(prompt.length).toBe(TUI_WIDTH); | |
26 | - expect(div.length).toBe(TUI_WIDTH); | |
27 | - expect(plainLine.length).toBe(TUI_WIDTH); | |
28 | - expect(row.length).toBe(TUI_WIDTH); | |
29 | - expect(singleRow.length).toBe(TUI_WIDTH); | |
30 | - expect(bottom.length).toBe(TUI_WIDTH); | |
26 | + expect(visibleLength(top)).toBe(TUI_WIDTH); | |
27 | + expect(visibleLength(prompt)).toBe(TUI_WIDTH); | |
28 | + expect(visibleLength(div)).toBe(TUI_WIDTH); | |
29 | + expect(visibleLength(plainLine)).toBe(TUI_WIDTH); | |
30 | + expect(visibleLength(row)).toBe(TUI_WIDTH); | |
31 | + expect(visibleLength(singleRow)).toBe(TUI_WIDTH); | |
32 | + expect(visibleLength(bottom)).toBe(TUI_WIDTH); | |
31 | 33 | }); |
32 | 34 | |
33 | 35 | it("handles long text by gracefully clipping to prevent Discord line breaks", () => { |
34 | 36 | const overflowText = |
35 | 37 | "This is a very long line that exceeds the standard terminal viewport and would otherwise wrap awkwardly on mobile Discord screens."; |
36 | 38 | const line = tuiLine(overflowText); |
37 | - expect(line.length).toBe(TUI_WIDTH); | |
38 | - expect(line.startsWith("β ")).toBe(true); | |
39 | - expect(line.endsWith(" β")).toBe(true); | |
39 | + expect(visibleLength(line)).toBe(TUI_WIDTH); | |
40 | + const stripped = stripAnsi(line); | |
41 | + expect(stripped.startsWith("β ")).toBe(true); | |
42 | + expect(stripped.endsWith(" β")).toBe(true); | |
40 | 43 | }); |
41 | 44 | |
42 | 45 | it("renders meter gauges correctly across boundary percentages", () => { |
43 | - expect(renderMeter(0, 10)).toBe("[β‘β‘β‘β‘β‘β‘β‘β‘β‘β‘]"); | |
44 | - expect(renderMeter(50, 10)).toBe("[β β β β β β‘β‘β‘β‘β‘]"); | |
45 | - expect(renderMeter(100, 10)).toBe("[β β β β β β β β β β ]"); | |
46 | + expect(stripAnsi(renderMeter(0, 10))).toBe("[β‘β‘β‘β‘β‘β‘β‘β‘β‘β‘]"); | |
47 | + expect(stripAnsi(renderMeter(50, 10))).toBe("[β β β β β β‘β‘β‘β‘β‘]"); | |
48 | + expect(stripAnsi(renderMeter(100, 10))).toBe("[β β β β β β β β β β ]"); | |
46 | 49 | }); |
47 | 50 | |
48 | - it("wraps TUI lines in a monospace code block", () => { | |
51 | + it("wraps TUI lines in an ANSI monospace code block", () => { | |
49 | 52 | const card = renderTuiCard([tuiTopBar("TITLE"), tuiPrompt("cmd"), tuiBottomBar()]); |
50 | - expect(card.startsWith("```text\n")).toBe(true); | |
53 | + expect(card.startsWith("```ansi\n")).toBe(true); | |
51 | 54 | expect(card.endsWith("\n```")).toBe(true); |
52 | 55 | }); |
53 | 56 | }); |
54 | 57 | |
βΌapps/bot/src/ui/tui.ts
+57-21
| @@ -1,41 +1,75 @@ | ||
1 | -// Terminal User Interface (TUI) Rendering Engine for GITBOT | |
2 | -// Strict fixed-width monospace containers guaranteed to render without line-wrapping across Discord Desktop, Web, iOS, and Android. | |
1 | +// Terminal User Interface (TUI) Colorful ANSI Rendering Engine for GITBOT | |
2 | +// Strict fixed-width (43-column) monospace containers with rich Discord ANSI colors. | |
3 | 3 | |
4 | 4 | export const TUI_WIDTH = 43; |
5 | 5 | |
6 | -export function padText(str: string, len: number): string { | |
7 | - if (str.length > len) { | |
8 | - return str.slice(0, len); | |
6 | +export const ANSI = { | |
7 | + reset: "\x1b[0m", | |
8 | + bold: "\x1b[1m", | |
9 | + dim: "\x1b[2;37m", | |
10 | + red: "\x1b[1;31m", | |
11 | + green: "\x1b[1;32m", | |
12 | + yellow: "\x1b[1;33m", | |
13 | + blue: "\x1b[1;34m", | |
14 | + magenta: "\x1b[1;35m", | |
15 | + cyan: "\x1b[1;36m", | |
16 | + white: "\x1b[1;37m", | |
17 | + gray: "\x1b[0;30m", | |
18 | +} as const; | |
19 | + | |
20 | +const ANSI_REGEX = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"); | |
21 | + | |
22 | +export function stripAnsi(str: string): string { | |
23 | + return str.replace(ANSI_REGEX, ""); | |
24 | +} | |
25 | + | |
26 | +export function visibleLength(str: string): number { | |
27 | + return stripAnsi(str).length; | |
28 | +} | |
29 | + | |
30 | +export function padAnsi(str: string, targetLen: number): string { | |
31 | + const visLen = visibleLength(str); | |
32 | + if (visLen >= targetLen) { | |
33 | + return str; | |
34 | + } | |
35 | + return `${str}${" ".repeat(targetLen - visLen)}`; | |
36 | +} | |
37 | + | |
38 | +export function clipAnsi(str: string, maxLen: number): string { | |
39 | + if (visibleLength(str) <= maxLen) { | |
40 | + return str; | |
9 | 41 | } |
10 | - return str + " ".repeat(len - str.length); | |
42 | + const stripped = stripAnsi(str); | |
43 | + return `${stripped.slice(0, maxLen - 1)}β¦`; | |
11 | 44 | } |
12 | 45 | |
13 | 46 | export function tuiTopBar(title: string, width = TUI_WIDTH): string { |
14 | - const prefix = `ββ [ ${title} ] `; | |
15 | - const rem = Math.max(0, width - prefix.length - 1); | |
16 | - return `${prefix}${"β".repeat(rem)}β`; | |
47 | + const innerTitle = `${ANSI.cyan}ββ [ ${ANSI.green}${title}${ANSI.cyan} ] ${ANSI.reset}`; | |
48 | + const rem = width - visibleLength(innerTitle) - 1; | |
49 | + return `${innerTitle}${ANSI.cyan}${"β".repeat(Math.max(0, rem))}β${ANSI.reset}`; | |
17 | 50 | } |
18 | 51 | |
19 | 52 | export function tuiBottomBar(width = TUI_WIDTH): string { |
20 | - return `β${"β".repeat(width - 2)}β`; | |
53 | + return `${ANSI.cyan}β${"β".repeat(width - 2)}β${ANSI.reset}`; | |
21 | 54 | } |
22 | 55 | |
23 | 56 | export function tuiDivider(title?: string, width = TUI_WIDTH): string { |
24 | 57 | if (!title) { |
25 | - return `β${"β".repeat(width - 2)}β€`; | |
58 | + return `${ANSI.cyan}β${"β".repeat(width - 2)}β€${ANSI.reset}`; | |
26 | 59 | } |
27 | - const prefix = `ββ ${title} `; | |
28 | - const rem = Math.max(0, width - prefix.length - 1); | |
29 | - return `${prefix}${"β".repeat(rem)}β€`; | |
60 | + const prefix = `${ANSI.cyan}ββ ${ANSI.yellow}${title} ${ANSI.cyan}`; | |
61 | + const rem = width - visibleLength(prefix) - 1; | |
62 | + return `${prefix}${"β".repeat(Math.max(0, rem))}β€${ANSI.reset}`; | |
30 | 63 | } |
31 | 64 | |
32 | 65 | export function tuiLine(text = "", width = TUI_WIDTH): string { |
33 | 66 | const contentWidth = width - 4; |
34 | - return `β ${padText(text, contentWidth)} β`; | |
67 | + const clipped = clipAnsi(text, contentWidth); | |
68 | + return `${ANSI.cyan}β${ANSI.reset} ${padAnsi(clipped, contentWidth)} ${ANSI.cyan}β${ANSI.reset}`; | |
35 | 69 | } |
36 | 70 | |
37 | 71 | export function tuiPrompt(cmd: string, width = TUI_WIDTH): string { |
38 | - return tuiLine(`$ ${cmd}`, width); | |
72 | + return tuiLine(`${ANSI.white}$ ${cmd}${ANSI.reset}`, width); | |
39 | 73 | } |
40 | 74 | |
41 | 75 | export function tuiRow2( |
| @@ -47,19 +81,21 @@ export function tuiRow2( | ||
47 | 81 | ): string { |
48 | 82 | const contentWidth = width - 4; |
49 | 83 | if (!l2) { |
50 | - return tuiLine(`${l1.padEnd(10)}: ${v1}`, width); | |
84 | + const text = `${ANSI.dim}${l1.padEnd(9)}:${ANSI.reset} ${v1}`; | |
85 | + return tuiLine(text, width); | |
51 | 86 | } |
52 | 87 | const half = Math.floor((contentWidth - 2) / 2); |
53 | - const col1 = padText(`${l1.padEnd(7)}: ${v1}`, half); | |
54 | - const col2 = padText(`${l2.padEnd(7)}: ${v2}`, contentWidth - half - 2); | |
88 | + const col1 = padAnsi(`${ANSI.dim}${l1.padEnd(7)}:${ANSI.reset} ${v1}`, half); | |
89 | + const col2 = padAnsi(`${ANSI.dim}${l2.padEnd(7)}:${ANSI.reset} ${v2}`, contentWidth - half - 2); | |
55 | 90 | return tuiLine(`${col1} ${col2}`, width); |
56 | 91 | } |
57 | 92 | |
58 | 93 | export function renderMeter(pct: number, length = 10): string { |
59 | 94 | const filled = Math.min(length, Math.max(0, Math.round((pct / 100) * length))); |
60 | - return `[${"β ".repeat(filled)}${"β‘".repeat(length - filled)}]`; | |
95 | + const color = pct >= 80 ? ANSI.green : pct >= 50 ? ANSI.yellow : ANSI.red; | |
96 | + return `[${color}${"β ".repeat(filled)}${ANSI.dim}${"β‘".repeat(length - filled)}${ANSI.reset}]`; | |
61 | 97 | } |
62 | 98 | |
63 | 99 | export function renderTuiCard(lines: string[]): string { |
64 | - return `\`\`\`text\n${lines.join("\n")}\n\`\`\``; | |
100 | + return `\`\`\`ansi\n${lines.join("\n")}\n\`\`\``; | |
65 | 101 | } |
66 | 102 | |