Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
dist/
.worktrees/

# Temp files from runInlineTs (tests/e2e). Cleaned up by a finally block
# on success; listing them here keeps a SIGKILL'd test runner from leaving
Expand Down
187 changes: 187 additions & 0 deletions docs/cvd-palette-preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>asm CVD palette preview</title>
<style>
:root {
--fg: #E8E8E8;
--dim: #9A9A9A;
--border: #6B6B6B;
--accent: #56B4E9;
--success: #2BC48A;
--warning: #E69F00;
--danger: #D55E00;
--special: #CC79A7;
--bg: #111111;
}
* { box-sizing: border-box; }
body {
margin: 0;
font: 14px/1.4 ui-monospace, "JetBrains Mono", Menlo, monospace;
background: #0b0b0b;
color: var(--fg);
}
header {
padding: 16px 24px;
border-bottom: 1px solid var(--border);
display: flex;
gap: 16px;
align-items: center;
flex-wrap: wrap;
}
h1 { font-size: 16px; margin: 0; color: var(--accent); }
.filters button {
background: #1a1a1a;
color: var(--fg);
border: 1px solid var(--border);
padding: 6px 10px;
margin-right: 6px;
cursor: pointer;
}
.filters button.active { border-color: var(--accent); color: var(--accent); }
main { padding: 24px; display: grid; gap: 24px; }
.sim { transition: filter 0.15s; }
.swatches { display: flex; gap: 8px; flex-wrap: wrap; }
.swatch { width: 120px; }
.chip { height: 36px; border: 1px solid var(--border); }
.screen {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 12px 14px;
white-space: pre;
}
.title { color: var(--dim); margin-bottom: 8px; }
.accent { color: var(--accent); }
.success { color: var(--success); }
.warning { color: var(--warning); }
.danger { color: var(--danger); }
.special { color: var(--special); }
.dim { color: var(--dim); }
.sel { background: var(--accent); color: #111; }
.box { border: 1px solid var(--border); padding: 8px; }
.box.danger-b { border-color: var(--danger); }
.box.accent-b { border-color: var(--accent); }
</style>
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="position:absolute">
<filter id="protanopia">
<feColorMatrix type="matrix" values="
0.567 0.433 0 0 0
0.558 0.442 0 0 0
0.000 0.242 0.758 0 0
0 0 0 1 0"/>
</filter>
<filter id="deuteranopia">
<feColorMatrix type="matrix" values="
0.625 0.375 0 0 0
0.700 0.300 0 0 0
0.000 0.300 0.700 0 0
0 0 0 1 0"/>
</filter>
<filter id="gray">
<feColorMatrix type="matrix" values="
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0 0 0 1 0"/>
</filter>
</svg>
</head>
<body>
<header>
<h1>asm CVD palette — Okabe–Ito default</h1>
<div class="filters">
<button class="active" data-f="none">Original</button>
<button data-f="url(#protanopia)">Protanopia</button>
<button data-f="url(#deuteranopia)">Deuteranopia</button>
<button data-f="url(#gray)">Grayscale</button>
</div>
<span class="dim">Must-have screens. Color is never the only signal.</span>
</header>
<main class="sim" id="sim">
<section>
<div class="title">Roles</div>
<div class="swatches">
<div class="swatch"><div class="chip" style="background:#56B4E9"></div>accent #56B4E9</div>
<div class="swatch"><div class="chip" style="background:#2BC48A"></div>success #2BC48A</div>
<div class="swatch"><div class="chip" style="background:#E69F00"></div>warning #E69F00</div>
<div class="swatch"><div class="chip" style="background:#D55E00"></div>danger #D55E00</div>
<div class="swatch"><div class="chip" style="background:#CC79A7"></div>special #CC79A7</div>
<div class="swatch"><div class="chip" style="background:#E8E8E8"></div>fg #E8E8E8</div>
<div class="swatch"><div class="chip" style="background:#9A9A9A"></div>dim #9A9A9A</div>
</div>
</section>

<section>
<div class="title">TUI dashboard (rainbow stripped; dupes dual-encoded)</div>
<div class="screen">
<span class="accent"><b>agent-skill-manager</b></span>
<div class="box">Total: 42 (38 unique) Global: 30 Project: 12 Symlinks: 4 Tools: 6 <span class="warning">Dupes: ! 3</span></div>
<span class="accent sel"> Both </span> <span class="dim">Global</span> <span class="dim">Project</span> <span class="dim">— Claude, Codex +4</span>
<div class="box dim">press / to search...</div>
<span class="sel">❯ skill-auto-improver 0.3.1 medium Claude Code</span>
skill-creator 1.2.0 <span class="success">low</span> Codex
beads 1.4.0 <span class="danger">high</span> Grok CLI
</div>
</section>

<section>
<div class="title">TUI config (ASCII on/off)</div>
<div class="screen box accent-b">
<div class="accent"> Configuration </div>
<span class="dim">Config: ~/.config/agent-skill-manager/config.json</span>
<span class="warning">Tools (Enter to toggle, e to edit config file):</span>
<span class="sel">❯ on Claude Code ~/.claude/skills</span>
<span class="success">on </span> Codex ~/.codex/skills
<span class="danger">off</span> Grok CLI ~/.grok/skills
</div>
</section>

<section>
<div class="title">TUI uninstall confirm</div>
<div class="screen box danger-b">
<div class="danger"> Uninstall: old-skill </div>
<span class="warning">The following will be removed:</span>
<span class="danger"> ~/.claude/skills/old-skill</span>
<span class="dim"> Yes, uninstall Cancel</span>
</div>
</section>

<section>
<div class="title">CLI asm list (effort words + hue; no provider hues)</div>
<div class="screen">
Name Version Effort Tool
skill-auto-improver 0.3.1 <span class="warning">medium</span> [Claude Code]
skill-creator 1.2.0 <span class="success">low</span> [Codex]
beads 1.4.0 <span class="danger">high</span> [Grok CLI]
eval-max 0.1.0 <span class="special">max</span> [Oh My Pi]
</div>
</section>

<section>
<div class="title">CLI tools + security badges (no bg fills)</div>
<div class="screen">
Allowed tools: <span class="danger">Bash !</span> Read <span class="warning">WebFetch</span>

+-- <b>Security Audit</b> --------------------------+
| clean-skill <span class="success">[SAFE]</span>|
| risky-skill <span class="danger">[!! DANGEROUS]</span>|
| noisy-skill <span class="warning">[WARNING]</span>|
| odd-skill <span class="accent">[CAUTION]</span>|
</div>
</section>
</main>
<script>
const sim = document.getElementById("sim");
document.querySelectorAll(".filters button").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".filters button").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
const f = btn.getAttribute("data-f");
sim.style.filter = f === "none" ? "none" : f;
});
});
</script>
</body>
</html>
69 changes: 8 additions & 61 deletions src/formatter-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,9 @@ import type { SkillInfo } from "./utils/types";

import { formatTokenCount } from "./utils/token-count";
import { formatInvocability } from "./utils/frontmatter";
import { ansi, useColor } from "./utils/colors";

// ─── Color helpers ──────────────────────────────────────────────────────────

export const useColor = (): boolean => {
if (process.env.NO_COLOR !== undefined) return false;
if (globalThis.__CLI_NO_COLOR) return false;
if (!process.stdout.isTTY) return false;
return true;
};

const ansi = {
bold: (s: string) => (useColor() ? `\x1b[1m${s}\x1b[0m` : s),
cyan: (s: string) => (useColor() ? `\x1b[36m${s}\x1b[0m` : s),
green: (s: string) => (useColor() ? `\x1b[32m${s}\x1b[0m` : s),
yellow: (s: string) => (useColor() ? `\x1b[33m${s}\x1b[0m` : s),
dim: (s: string) => (useColor() ? `\x1b[2m${s}\x1b[0m` : s),
white: (s: string) => (useColor() ? `\x1b[37m${s}\x1b[0m` : s),
red: (s: string) => (useColor() ? `\x1b[31m${s}\x1b[0m` : s),
blue: (s: string) => (useColor() ? `\x1b[34m${s}\x1b[0m` : s),
blueBold: (s: string) => (useColor() ? `\x1b[34;1m${s}\x1b[0m` : s),
magenta: (s: string) => (useColor() ? `\x1b[35m${s}\x1b[0m` : s),
bgDim: (s: string) => (useColor() ? `\x1b[48;5;236m${s}\x1b[0m` : s),
bgRed: (s: string) => (useColor() ? `\x1b[41m\x1b[37m\x1b[1m${s}\x1b[0m` : s),
bgYellow: (s: string) =>
useColor() ? `\x1b[43m\x1b[30m\x1b[1m${s}\x1b[0m` : s,
bgGreen: (s: string) =>
useColor() ? `\x1b[42m\x1b[30m\x1b[1m${s}\x1b[0m` : s,
bgCyan: (s: string) =>
useColor() ? `\x1b[46m\x1b[30m\x1b[1m${s}\x1b[0m` : s,
};

export { ansi };
export { ansi, useColor };

// ─── Effort colors ─────────────────────────────────────────────────────────

Expand All @@ -58,37 +29,13 @@ export function colorEffort(effort: string | undefined): string {
}
}

// ─── Provider colors ───────────────────────────────────────────────────────

const PROVIDER_COLORS: Record<string, (s: string) => string> = {
claude: ansi.blueBold,
codex: ansi.cyan,
"codex-plugin": ansi.cyan,
openclaw: ansi.yellow,
agents: ansi.green,
custom: ansi.magenta,
cursor: ansi.blue,
windsurf: ansi.cyan,
cline: ansi.green,
roocode: ansi.magenta,
continue: ansi.yellow,
copilot: ansi.white,
aider: ansi.red,
opencode: ansi.cyan,
zed: ansi.blue,
augment: ansi.green,
amp: ansi.yellow,
};

export function colorProvider(provider: string, label: string): string {
const colorFn = PROVIDER_COLORS[provider] || ansi.dim;
return colorFn(label);
// Provider names are the signal; hue is not used.
export function colorProvider(_provider: string, label: string): string {
return label;
}

export function providerBadge(provider: string, label: string): string {
if (!useColor()) return `[${label}]`;
const colorFn = PROVIDER_COLORS[provider] || ansi.dim;
return colorFn(`[${label}]`);
export function providerBadge(_provider: string, label: string): string {
return `[${label}]`;
}

// ─── Path shortening ───────────────────────────────────────────────────────
Expand Down Expand Up @@ -728,7 +675,7 @@ export const HIGH_RISK_TOOLS = new Set([
export const MEDIUM_RISK_TOOLS = new Set(["WebFetch", "WebSearch"]);

export function colorTool(tool: string): string {
if (HIGH_RISK_TOOLS.has(tool)) return ansi.red(tool);
if (HIGH_RISK_TOOLS.has(tool)) return ansi.red(`${tool} !`);
if (MEDIUM_RISK_TOOLS.has(tool)) return ansi.yellow(tool);
return ansi.green(tool);
}
Expand Down
32 changes: 27 additions & 5 deletions src/formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,28 @@ describe("colorProvider", () => {
});
});

describe("colorProvider drops per-provider hues", () => {
test("does not wrap labels in ANSI when color is on", () => {
delete (globalThis as { __CLI_NO_COLOR?: boolean }).__CLI_NO_COLOR;
const originalIsTTY = process.stdout.isTTY;
Object.defineProperty(process.stdout, "isTTY", {
value: true,
configurable: true,
});
process.env.COLORTERM = "truecolor";
try {
expect(colorProvider("claude", "Claude Code")).toBe("Claude Code");
expect(colorProvider("codex", "Codex")).toBe("Codex");
} finally {
Object.defineProperty(process.stdout, "isTTY", {
value: originalIsTTY,
configurable: true,
});
delete process.env.COLORTERM;
}
});
});

// ─── formatGroupedTable ───────────────────────────────────────────────────

describe("formatGroupedTable", () => {
Expand Down Expand Up @@ -851,10 +873,10 @@ describe("colorTool", () => {
});

test("returns tool name for high-risk tools (no ANSI in no-color mode)", () => {
expect(colorTool("Bash")).toBe("Bash");
expect(colorTool("Write")).toBe("Write");
expect(colorTool("Edit")).toBe("Edit");
expect(colorTool("NotebookEdit")).toBe("NotebookEdit");
expect(colorTool("Bash")).toBe("Bash !");
expect(colorTool("Write")).toBe("Write !");
expect(colorTool("Edit")).toBe("Edit !");
expect(colorTool("NotebookEdit")).toBe("NotebookEdit !");
});

test("returns tool name for medium-risk tools", () => {
Expand Down Expand Up @@ -897,7 +919,7 @@ describe("formatAllowedTools", () => {

test("joins tools with double-space separator", () => {
const result = formatAllowedTools(["Bash", "Read", "Grep"]);
expect(result).toBe("Bash Read Grep");
expect(result).toBe("Bash ! Read Grep");
});

test("handles single tool", () => {
Expand Down
7 changes: 6 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,12 @@ export function App({ initialConfig }: AppProps) {
const visibleListRows = Math.max(5, termHeight - chromeRows - 4);

return (
<Box flexDirection="column" width={termWidth} height={termHeight}>
<Box
flexDirection="column"
width={termWidth}
height={termHeight}
backgroundColor={theme.bg}
>
{view === "dashboard" && (
<>
{scanning && !hasScanned && (
Expand Down
3 changes: 2 additions & 1 deletion src/security-auditor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,8 @@ describe("formatSecurityReport", () => {

expect(output).toContain("Security Audit");
expect(output).toContain("clean");
expect(output).toContain("SAFE");
expect(output).toContain("[SAFE]");
expect(output).not.toContain(" SAFE ");
expect(output).toContain("No suspicious patterns");
});

Expand Down
8 changes: 4 additions & 4 deletions src/security-auditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,13 +651,13 @@ const SEVERITY_ORDER: Record<string, number> = {
function verdictBadge(verdict: SecurityVerdict): string {
switch (verdict) {
case "safe":
return color.bgGreen(" SAFE ");
return color.green("[SAFE]");
case "caution":
return color.bgCyan(" CAUTION ");
return color.cyan("[CAUTION]");
case "warning":
return color.bgYellow(" WARNING ");
return color.yellow("[WARNING]");
case "dangerous":
return color.bgRed(" DANGEROUS ");
return color.red("[!! DANGEROUS]");
Comment on lines 659 to +660

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the dangerous badge within the audit header width

For valid installed skill names of 36–38 characters, the longer [!! DANGEROUS] badge makes this row exceed the formatter's fixed width even though the previous badge still fit, so its closing border no longer aligns with the rest of the security report. Installed names can be much longer under src/installer-core.ts:329-336; truncate the displayed name, widen the box, or account for the badge before enforcing the one-space minimum gap.

Useful? React with 👍 / 👎.

}
}

Expand Down
Loading
Loading