mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 16:06:16 +00:00
fix(terminal): sanitize skills JSON and fallback on legacy Windows (#43520)
* Terminal: use ASCII borders on legacy Windows consoles * Skills: sanitize JSON output for control bytes * Changelog: credit terminal follow-up fixes * Update CHANGELOG.md * Update CHANGELOG.md Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Skills: strip remaining escape sequences from JSON output --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { SkillStatusEntry, SkillStatusReport } from "../agents/skills-status.js";
|
||||
import { stripAnsi } from "../terminal/ansi.js";
|
||||
import { getTerminalTableWidth, renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
@@ -42,6 +43,33 @@ function normalizeSkillEmoji(emoji?: string): string {
|
||||
return (emoji ?? "📦").replaceAll("\uFE0E", "\uFE0F");
|
||||
}
|
||||
|
||||
const REMAINING_ESC_SEQUENCE_REGEX = new RegExp(
|
||||
String.raw`\u001b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`,
|
||||
"g",
|
||||
);
|
||||
const JSON_CONTROL_CHAR_REGEX = new RegExp(String.raw`[\u0000-\u001f\u007f-\u009f]`, "g");
|
||||
|
||||
function sanitizeJsonString(value: string): string {
|
||||
return stripAnsi(value)
|
||||
.replace(REMAINING_ESC_SEQUENCE_REGEX, "")
|
||||
.replace(JSON_CONTROL_CHAR_REGEX, "");
|
||||
}
|
||||
|
||||
function sanitizeJsonValue(value: unknown): unknown {
|
||||
if (typeof value === "string") {
|
||||
return sanitizeJsonString(value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => sanitizeJsonValue(item));
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entryValue]) => [key, sanitizeJsonValue(entryValue)]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function formatSkillName(skill: SkillStatusEntry): string {
|
||||
const emoji = normalizeSkillEmoji(skill.emoji);
|
||||
return `${emoji} ${theme.command(skill.name)}`;
|
||||
@@ -71,7 +99,7 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
|
||||
const skills = opts.eligible ? report.skills.filter((s) => s.eligible) : report.skills;
|
||||
|
||||
if (opts.json) {
|
||||
const jsonReport = {
|
||||
const jsonReport = sanitizeJsonValue({
|
||||
workspaceDir: report.workspaceDir,
|
||||
managedSkillsDir: report.managedSkillsDir,
|
||||
skills: skills.map((s) => ({
|
||||
@@ -87,7 +115,7 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
|
||||
homepage: s.homepage,
|
||||
missing: s.missing,
|
||||
})),
|
||||
};
|
||||
});
|
||||
return JSON.stringify(jsonReport, null, 2);
|
||||
}
|
||||
|
||||
@@ -154,7 +182,7 @@ export function formatSkillInfo(
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
return JSON.stringify(skill, null, 2);
|
||||
return JSON.stringify(sanitizeJsonValue(skill), null, 2);
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
@@ -251,7 +279,7 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp
|
||||
|
||||
if (opts.json) {
|
||||
return JSON.stringify(
|
||||
{
|
||||
sanitizeJsonValue({
|
||||
summary: {
|
||||
total: report.skills.length,
|
||||
eligible: eligible.length,
|
||||
@@ -267,7 +295,7 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp
|
||||
missing: s.missing,
|
||||
install: s.install,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
@@ -243,5 +243,46 @@ describe("skills-cli", () => {
|
||||
const parsed = JSON.parse(output) as Record<string, unknown>;
|
||||
assert(parsed);
|
||||
});
|
||||
|
||||
it("sanitizes ANSI and C1 controls in skills list JSON output", () => {
|
||||
const report = createMockReport([
|
||||
createMockSkill({
|
||||
name: "json-skill",
|
||||
emoji: "\u001b[31m📧\u001b[0m\u009f",
|
||||
description: "desc\u0093\u001b[2J\u001b[33m colored\u001b[0m",
|
||||
}),
|
||||
]);
|
||||
|
||||
const output = formatSkillsList(report, { json: true });
|
||||
const parsed = JSON.parse(output) as {
|
||||
skills: Array<{ emoji: string; description: string }>;
|
||||
};
|
||||
|
||||
expect(parsed.skills[0]?.emoji).toBe("📧");
|
||||
expect(parsed.skills[0]?.description).toBe("desc colored");
|
||||
expect(output).not.toContain("\\u001b");
|
||||
});
|
||||
|
||||
it("sanitizes skills info JSON output", () => {
|
||||
const report = createMockReport([
|
||||
createMockSkill({
|
||||
name: "info-json",
|
||||
emoji: "\u001b[31m🎙\u001b[0m\u009f",
|
||||
description: "hi\u0091",
|
||||
homepage: "https://example.com/\u0092docs",
|
||||
}),
|
||||
]);
|
||||
|
||||
const output = formatSkillInfo(report, "info-json", { json: true });
|
||||
const parsed = JSON.parse(output) as {
|
||||
emoji: string;
|
||||
description: string;
|
||||
homepage: string;
|
||||
};
|
||||
|
||||
expect(parsed.emoji).toBe("🎙");
|
||||
expect(parsed.description).toBe("hi");
|
||||
expect(parsed.homepage).toBe("https://example.com/docs");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user