@proofofwork-agency/toolpin 0.2.3

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.
Files changed (61) hide show
  1. package/CONTRIBUTING.md +117 -0
  2. package/LICENSE +183 -0
  3. package/README.md +323 -0
  4. package/SECURITY.md +61 -0
  5. package/action.yml +134 -0
  6. package/dist/canonicalJson.js +38 -0
  7. package/dist/capabilities.js +139 -0
  8. package/dist/ci.js +26 -0
  9. package/dist/cli.js +1843 -0
  10. package/dist/clientSupport.js +76 -0
  11. package/dist/codexToml.js +213 -0
  12. package/dist/config.js +337 -0
  13. package/dist/constants.js +3 -0
  14. package/dist/continueYaml.js +76 -0
  15. package/dist/doctor.js +163 -0
  16. package/dist/install.js +191 -0
  17. package/dist/installed.js +405 -0
  18. package/dist/integrity.js +14 -0
  19. package/dist/inventory.js +169 -0
  20. package/dist/packageIntegrity.js +153 -0
  21. package/dist/plan.js +595 -0
  22. package/dist/policy.js +310 -0
  23. package/dist/registry.js +1610 -0
  24. package/dist/runtimeAdvisory.js +80 -0
  25. package/dist/safeFetch.js +157 -0
  26. package/dist/sarif.js +162 -0
  27. package/dist/scan.js +113 -0
  28. package/dist/search.js +44 -0
  29. package/dist/secrets.js +165 -0
  30. package/dist/signing.js +146 -0
  31. package/dist/tester.js +240 -0
  32. package/dist/trust.js +528 -0
  33. package/dist/tui/app.js +1731 -0
  34. package/dist/tui/command.js +50 -0
  35. package/dist/tui/configSnippet.js +11 -0
  36. package/dist/tui/constants.js +37 -0
  37. package/dist/tui/format.js +31 -0
  38. package/dist/tui/installedState.js +23 -0
  39. package/dist/tui/layout.js +65 -0
  40. package/dist/tui/selectors.js +282 -0
  41. package/dist/tui/types.js +1 -0
  42. package/dist/tui/ui/trust.js +77 -0
  43. package/dist/tui/views/installed.js +82 -0
  44. package/dist/tui/views/panels.js +637 -0
  45. package/dist/tui.js +12 -0
  46. package/dist/types.js +1 -0
  47. package/dist/verificationTrust.js +103 -0
  48. package/dist/verify.js +537 -0
  49. package/dist/version.js +1 -0
  50. package/dist/versions.js +127 -0
  51. package/docs/assets/readme/terminal-demo.svg +174 -0
  52. package/docs/assets/readme/tui-browse-overview.jpg +0 -0
  53. package/docs/assets/readme/tui-config-preview.jpg +0 -0
  54. package/docs/assets/readme/tui-help.jpg +0 -0
  55. package/docs/assets/readme/tui-installed-inventory.jpg +0 -0
  56. package/docs/how-to/catch-drift-in-ci.md +189 -0
  57. package/docs/how-to/custom-registries.md +156 -0
  58. package/docs/how-to/toolpin-curated-registry.md +153 -0
  59. package/package.json +76 -0
  60. package/registry/README.md +92 -0
  61. package/registry/v0/servers +115 -0
@@ -0,0 +1,637 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import { exportClientConfig } from "../../config.js";
5
+ import { buildInstallPlan } from "../../plan.js";
6
+ import { compareRegistrySources, REGISTRY_SOURCES } from "../../registry.js";
7
+ import { evidenceStatus, evidenceSummary, scoreServer, trustCapExplanation, trustProfileScore, trustTier } from "../../trust.js";
8
+ import { TOOLPIN_VERSION } from "../../version.js";
9
+ import { commandLineFor } from "../command.js";
10
+ import { ACCENT, BLUE, CHROME, ERR, MUTED, OK, SURFACE, WARN } from "../constants.js";
11
+ import { formatClientConfigSnippet } from "../configSnippet.js";
12
+ import { asObject, safeJson, shortPath, truncate } from "../format.js";
13
+ import { computeMenuLayout, listWindowStart } from "../layout.js";
14
+ import { browseSortLabel, clientSupportSummary, commandLogForView, configTargetLabel, formatVersionChoices, installClientChoicesForServerScope, installClientLabel, scopeLabel, selectedInstallClientsForServerScope, } from "../selectors.js";
15
+ import { trustBarCells, trustDimensions, trustRiskTone, trustTierScore } from "../ui/trust.js";
16
+ export function ChromeHeader({ state, resultCount, totalMatches, selectedServer, width }) {
17
+ const status = state.installing ? "install" : state.testing ? "test" : state.loading ? "sync" : state.error ? "err" : "ready";
18
+ const statusColor = state.installing || state.testing || state.loading ? WARN : state.error ? ERR : OK;
19
+ const right = `${status} | client:${state.client} | source:${state.sourceMode} | sort:${browseSortLabel(state.browseSortMode)} | shown:${resultCount}/${totalMatches} matches`;
20
+ const leftWidth = Math.max(18, width - right.length - 7);
21
+ return (_jsxs(Box, { paddingX: 2, marginTop: 1, marginBottom: 1, justifyContent: "space-between", children: [_jsx(Box, { width: leftWidth, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { bold: true, color: "white", children: "ToolPin" }), _jsxs(Text, { color: MUTED, children: [" v", TOOLPIN_VERSION] }), _jsx(Text, { color: CHROME, children: " " }), _jsx(Text, { color: CHROME, children: shortPath(process.cwd()) })] }) }), _jsxs(Text, { children: [_jsx(Text, { color: statusColor, children: status }), _jsx(Text, { color: CHROME, children: " | " }), _jsx(Text, { color: CHROME, children: "client:" }), _jsx(Text, { color: "white", children: state.client }), _jsx(Text, { color: CHROME, children: " | " }), _jsx(Text, { color: CHROME, children: "source:" }), _jsx(Text, { color: state.dataMode === "live" ? WARN : OK, children: state.sourceMode }), _jsx(Text, { color: CHROME, children: " | " }), _jsx(Text, { color: CHROME, children: "sort:" }), _jsx(Text, { color: MUTED, children: browseSortLabel(state.browseSortMode) }), _jsx(Text, { color: CHROME, children: " | " }), _jsx(Text, { color: CHROME, children: "shown:" }), _jsxs(Text, { color: MUTED, children: [resultCount, " / ", totalMatches, " matches"] })] })] }));
22
+ }
23
+ export function PromptBar({ state, width }) {
24
+ const active = state.inputMode === "search";
25
+ const commandActive = state.inputMode === "command";
26
+ const editing = active || commandActive;
27
+ const [cursorVisible, setCursorVisible] = React.useState(true);
28
+ React.useEffect(() => {
29
+ if (!editing) {
30
+ setCursorVisible(false);
31
+ return undefined;
32
+ }
33
+ setCursorVisible(true);
34
+ const interval = setInterval(() => setCursorVisible((visible) => !visible), 520);
35
+ return () => clearInterval(interval);
36
+ }, [editing]);
37
+ const cursor = editing ? (_jsx(Text, { color: cursorVisible ? BLUE : SURFACE, children: "\u258C" })) : null;
38
+ return (_jsx(Box, { marginX: 2, marginBottom: 1, backgroundColor: SURFACE, paddingX: 1, paddingY: 1, children: _jsxs(Box, { justifyContent: "space-between", width: Math.max(1, width - 6), children: [_jsx(Text, { wrap: "truncate", children: commandActive ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: "Command " }), _jsx(Text, { color: CHROME, children: "toolpin " }), _jsx(Text, { color: "white", children: state.commandQuery || "command" }), cursor] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: "Search: " }), _jsx(Text, { color: "white", children: state.query || "Search MCP servers" }), cursor] })) }), _jsx(Text, { color: active || commandActive ? BLUE : MUTED, children: commandActive ? "Enter runs, Esc closes" : active ? "Enter applies, Esc cancels" : "/ edit search : commands" })] }) }));
39
+ }
40
+ export function ModeLine({ active, selectedServer, width }) {
41
+ const hasSelection = Boolean(selectedServer);
42
+ const layout = computeMenuLayout({ width, hasSelection, selectedLabel: selectedServer?.title || selectedServer?.name });
43
+ const segment = (view) => layout.segments.find((entry) => entry.view === view);
44
+ return (_jsxs(Box, { paddingX: 2, marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { bold: active === "discover", color: active === "discover" ? BLUE : MUTED, children: segment("discover")?.label }), _jsx(Text, { color: CHROME, children: " " }), _jsx(Text, { bold: active === "installed", color: active === "installed" ? BLUE : MUTED, children: segment("installed")?.label }), _jsx(Text, { color: CHROME, children: " | " }), _jsx(Text, { color: hasSelection ? MUTED : CHROME, children: "Selected: " }), _jsx(Text, { color: hasSelection ? "white" : CHROME, children: layout.selectedLabel }), hasSelection ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: CHROME, children: " | " }), _jsx(Text, { bold: active === "details", color: active === "details" ? BLUE : MUTED, children: segment("details")?.label }), _jsx(Text, { color: CHROME, children: " " }), _jsx(Text, { bold: active === "plan", color: active === "plan" ? BLUE : MUTED, children: segment("plan")?.label }), _jsx(Text, { color: CHROME, children: " " }), _jsx(Text, { bold: active === "config", color: active === "config" ? BLUE : MUTED, children: segment("config")?.label })] })) : null] }), _jsx(Text, { bold: active === "help", color: active === "help" ? BLUE : MUTED, children: segment("help")?.label })] }));
45
+ }
46
+ export function OptionList({ results, totalMatches, totalServers, totalVersions, selected, height, width, query, loading, browseLayout, browseSortMode = "source-first", sourceMode = "all", dimmed, }) {
47
+ const grouped = browseLayout !== "flat";
48
+ const visibleCount = grouped ? Math.max(1, Math.floor((height - 2) / 2)) : Math.max(2, height - 1);
49
+ const start = listWindowStart(selected, visibleCount, results.length);
50
+ const visible = results.slice(start, start + visibleCount);
51
+ let previousCategory = "";
52
+ let usedLines = 0;
53
+ const maxLines = Math.max(1, height - 1);
54
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 3, height: height, children: [results.length === 0 && loading ? (_jsx(RegistryLoadingPanel, { height: height })) : results.length === 0 ? (_jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsx(Text, { bold: true, color: "white", children: "No servers found" }), _jsx(Text, { color: MUTED, wrap: "wrap", children: query.trim()
55
+ ? `No MCP servers match "${query.trim()}". Try a different search, press r to refresh cached registries, or press l to search live sources.`
56
+ : "No MCP servers are available for the current filters. Type / to search, press r to refresh cached registries, or press l to search live sources." })] })) : null, visible.map((result, index) => {
57
+ if (!grouped) {
58
+ return _jsx(OptionRow, { result: result, selected: start + index === selected, dimmed: dimmed, width: width }, `${result.server.name}:${result.server.version}`);
59
+ }
60
+ const category = browseCategory(result.server);
61
+ const showCategory = browseLayout === "category" && category !== previousCategory;
62
+ const rowLines = showCategory ? 3 : 2;
63
+ previousCategory = category;
64
+ if (usedLines + rowLines > maxLines)
65
+ return null;
66
+ usedLines += rowLines;
67
+ return (_jsx(GroupedOptionRow, { result: result, selected: start + index === selected, dimmed: dimmed, width: width, category: showCategory ? category : undefined }, `${result.server.name}:${result.server.version}`));
68
+ }), results.length > 0 ? (_jsxs(Text, { color: CHROME, wrap: "truncate", children: [" ", "selected ", selected + 1, " of ", results.length, " shown / ", totalMatches, " matches / ", totalServers, " latest servers / ", totalVersions, " cached versions", _jsxs(Text, { color: MUTED, children: [" source:", sourceMode, " sort:", browseSortLabel(browseSortMode), " layout:", browseLayout] }), results.length < totalMatches ? _jsx(Text, { color: MUTED, children: " press m for more" }) : null] })) : null] }));
69
+ }
70
+ export function RegistryLoadingPanel({ height }) {
71
+ const frames = ["[-]", "[\\]", "[|]", "[/]"];
72
+ const [frame, setFrame] = React.useState(0);
73
+ React.useEffect(() => {
74
+ const interval = setInterval(() => setFrame((current) => (current + 1) % frames.length), 140);
75
+ return () => clearInterval(interval);
76
+ }, []);
77
+ return (_jsx(Box, { height: height, alignItems: "center", justifyContent: "center", flexDirection: "column", children: _jsxs(Box, { flexDirection: "column", backgroundColor: SURFACE, paddingX: 1, children: [_jsx(Text, { color: CHROME, children: "+---------------+" }), _jsxs(Text, { children: [_jsx(Text, { color: CHROME, children: "| " }), _jsx(Text, { bold: true, color: ACCENT, children: "ToolPin sync" }), _jsx(Text, { color: CHROME, children: " |" })] }), _jsxs(Text, { children: [_jsx(Text, { color: CHROME, children: "| " }), _jsx(Text, { color: OK, children: frames[frame] }), _jsx(Text, { color: MUTED, children: " registry" }), _jsx(Text, { color: CHROME, children: " |" })] }), _jsx(Text, { color: CHROME, children: "+---------------+" })] }) }));
78
+ }
79
+ function GroupedOptionRow({ result, selected = false, dimmed, width, category }) {
80
+ const server = result.server;
81
+ const contentWidth = Math.max(24, width - 6);
82
+ const titleWidth = Math.max(14, contentWidth - 24);
83
+ const detailWidth = Math.max(10, contentWidth - 5);
84
+ const project = browseProject(server);
85
+ const detail = `${project} ${server.registrySource} ${(server.packageTypes[0] ?? server.remoteTypes[0] ?? "unknown")}`;
86
+ const meterWidth = Math.max(6, Math.min(18, contentWidth - titleWidth - TRUST_TIER_LABEL_WIDTH - 4));
87
+ return (_jsxs(_Fragment, { children: [category ? _jsxs(Text, { color: MUTED, wrap: "truncate", children: [" category ", truncate(category, Math.max(8, width - 15))] }) : null, _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: selected ? BLUE : dimmed ? CHROME : BLUE, children: selected ? "> " : ": " }), _jsx(Text, { bold: selected, color: dimmed ? MUTED : "white", children: truncate(server.title || server.name, titleWidth).padEnd(titleWidth) }), _jsx(Text, { color: CHROME, children: " " }), _jsx(TrustTierMeter, { tier: result.trust.tier, score: trustProfileScore(result.trust), issues: result.trust.issues, cells: meterWidth })] }), _jsxs(Text, { color: dimmed ? CHROME : MUTED, wrap: "truncate", children: [_jsx(Text, { color: CHROME, children: " project " }), truncate(detail, detailWidth)] })] }));
88
+ }
89
+ function OptionRow({ result, selected = false, dimmed, width }) {
90
+ const server = result.server;
91
+ const contentWidth = Math.max(24, width - 6);
92
+ const titleWidth = Math.max(14, Math.min(36, Math.floor(contentWidth * 0.52)));
93
+ const meterWidth = Math.max(6, contentWidth - 2 - 8 - 1 - titleWidth - 1 - 1 - TRUST_TIER_LABEL_WIDTH);
94
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: selected ? BLUE : dimmed ? CHROME : BLUE, children: selected ? "> " : ": " }), _jsxs(Text, { color: selected ? BLUE : dimmed ? CHROME : BLUE, children: [truncate(server.registrySource, 8).padEnd(8), " "] }), _jsx(Text, { bold: selected, color: dimmed ? MUTED : "white", children: truncate(server.title || server.name, titleWidth).padEnd(titleWidth) }), _jsx(Text, { color: CHROME, children: " " }), _jsx(TrustTierMeter, { tier: result.trust.tier, score: trustProfileScore(result.trust), issues: result.trust.issues, cells: meterWidth })] }));
95
+ }
96
+ function browseCategory(server) {
97
+ const sourceMeta = server.raw._meta?.["dev.toolpin/source"];
98
+ const category = sourceMeta && typeof sourceMeta === "object" && !Array.isArray(sourceMeta)
99
+ ? sourceMeta.category
100
+ : undefined;
101
+ return typeof category === "string" && category.trim() ? category.trim() : "uncategorized";
102
+ }
103
+ function browseProject(server) {
104
+ if (!server.repositoryUrl)
105
+ return "no project declared";
106
+ try {
107
+ const url = new URL(server.repositoryUrl);
108
+ const parts = url.pathname.replace(/\.git$/, "").split("/").filter(Boolean);
109
+ return parts.length >= 2 ? parts.slice(-2).join("/") : server.repositoryUrl;
110
+ }
111
+ catch {
112
+ return server.repositoryUrl;
113
+ }
114
+ }
115
+ export function SourcesView({ sources, entries, activeSource, selectedSource, dataMode, width, height, }) {
116
+ const contentWidth = Math.max(24, width - 4);
117
+ const counts = sourceEntryCounts(entries);
118
+ const sorted = [...sources].sort(compareRegistrySources);
119
+ const connected = sorted.filter((source) => source.enabled);
120
+ const legendRows = height >= 18 ? 5 : 3;
121
+ const visibleRows = Math.max(1, height - 9 - legendRows);
122
+ const visible = sorted.slice(0, visibleRows);
123
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: SURFACE, paddingX: 2, paddingY: 1, height: height, flexGrow: 1, children: [_jsx(ModalTitle, { title: "sources", file: "registry list" }), _jsxs(Text, { color: MUTED, wrap: "truncate", children: ["Active registry sources feed Browse/search. Known adapters can stay disabled and are not server sources. ", _jsxs(Text, { color: activeSource === "all" ? OK : BLUE, children: ["active filter:", activeSource] }), " ", _jsxs(Text, { color: CHROME, children: ["mode:", dataMode] })] }), _jsx(Spacer, {}), _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: OK, children: "trusted first" }), _jsx(Text, { color: CHROME, children: " " }), _jsxs(Text, { color: MUTED, children: [connected.length, " active / ", sources.length, " known registry sources"] }), _jsx(Text, { color: CHROME, children: " " }), _jsx(Text, { color: MUTED, children: "j/k select, Enter/space toggle, r refresh enabled; pinned sources stay on" })] }), _jsx(Divider, { width: contentWidth }), visible.map((source, index) => (_jsx(SourceRow, { source: source, count: counts.get(source.id) ?? 0, dataMode: dataMode, active: activeSource === source.id || (activeSource === "all" && source.enabled), selected: index === selectedSource, width: contentWidth }, source.id))), visible.length < sorted.length ? _jsxs(Text, { color: CHROME, children: [" ".repeat(2), sorted.length - visible.length, " more source(s) hidden on this terminal height."] }) : null, _jsx(Box, { flexGrow: 1 }), _jsx(Divider, { width: contentWidth }), _jsx(SourceLegend, { width: contentWidth, compact: legendRows < 5 })] }));
124
+ }
125
+ function SourceRow({ source, count, dataMode, active, selected, width }) {
126
+ const trustColorValue = source.trust === "canonical" ? OK : source.trust === "curated" ? BLUE : source.trust === "directory" ? WARN : MUTED;
127
+ const status = source.status ?? (source.enabled ? source.mode === "discovery" ? "discovery-only" : "ready" : "disabled");
128
+ const statusColor = status === "ready" ? OK : status === "auth-missing" || status === "fetch-error" || status === "stale" ? ERR : status === "discovery-only" ? WARN : CHROME;
129
+ const auth = source.authRequired ? "auth required" : "no auth";
130
+ const modeColor = source.mode === "installable" ? OK : WARN;
131
+ const titleWidth = Math.max(16, Math.min(34, Math.floor(width * 0.26)));
132
+ const meta = `${source.trust} / ${source.mode} / ${source.type ?? "custom"} / ${auth}`;
133
+ const pin = source.pinned ? " / pinned" : "";
134
+ const rowColor = selected ? BLUE : active ? OK : CHROME;
135
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: rowColor, children: selected ? "> " : active ? "* " : ": " }), _jsx(Text, { bold: selected || active, color: selected ? BLUE : active ? OK : "white", children: truncate(source.label, titleWidth).padEnd(titleWidth) }), _jsx(Text, { color: CHROME, children: " " }), _jsx(Text, { color: trustColorValue, children: source.trust.padEnd(9) }), _jsx(Text, { color: CHROME, children: " " }), _jsx(Text, { color: statusColor, children: status.padEnd(14) }), _jsx(Text, { color: CHROME, children: " " }), _jsx(Text, { color: modeColor, children: source.mode }), _jsx(Text, { color: CHROME, children: " " }), _jsx(Text, { color: MUTED, children: sourceCountLabel(source, count, dataMode) }), source.pinned ? _jsx(Text, { color: BLUE, children: " pinned" }) : null] }), _jsxs(Text, { color: MUTED, wrap: "truncate", children: [_jsxs(Text, { color: CHROME, children: [" ", source.id.padEnd(12)] }), truncate(source.setupHint && status === "auth-missing" ? source.setupHint : `${source.description || meta}${pin}`, Math.max(8, width - 18))] })] }));
136
+ }
137
+ export function sourceCountLabel(source, count, dataMode) {
138
+ const label = dataMode === "live" ? "loaded" : "cached";
139
+ return `${count}${source.cachePageInfo?.hasMore ? "+" : ""} ${label}`;
140
+ }
141
+ function SourceLegend({ width, compact }) {
142
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: MUTED, wrap: "truncate", children: [_jsx(Text, { color: OK, children: "installable" }), " means ToolPin has package/remote metadata it can review, lock, and write."] }), _jsxs(Text, { color: MUTED, wrap: "truncate", children: [_jsx(Text, { color: WARN, children: "discovery-only" }), " means browse/search only; no install or lock until metadata is normalized."] }), _jsxs(Text, { color: MUTED, wrap: "truncate", children: [_jsx(Text, { color: CHROME, children: "cached/loaded" }), " is the number of entries currently stored or fetched for that source."] }), !compact ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: MUTED, wrap: "truncate", children: [_jsx(Text, { color: OK, children: "canonical" }), "/", _jsx(Text, { color: BLUE, children: "curated" }), " can carry install metadata; ", _jsx(Text, { color: WARN, children: "directory" }), " is broad discovery such as Glama."] }), _jsx(Text, { color: MUTED, wrap: "truncate", children: "Verified metadata comes from official/Docker or an official-compatible curated registry with pinned package/remotes." })] })) : null, compact ? _jsx(Text, { color: CHROME, wrap: "truncate", children: truncate("Verified metadata requires an installable source with pinned package/remotes.", width) }) : null] }));
143
+ }
144
+ function sourceEntryCounts(entries) {
145
+ const counts = new Map();
146
+ for (const entry of entries) {
147
+ const meta = entry._meta?.["dev.toolpin/source"];
148
+ const source = entry.source ?? (meta && typeof meta === "object" && !Array.isArray(meta) ? meta.source : undefined);
149
+ if (typeof source === "string")
150
+ counts.set(source, (counts.get(source) ?? 0) + 1);
151
+ }
152
+ return counts;
153
+ }
154
+ function TrustMeter({ score, showScore = true, cells: cellCount = 9 }) {
155
+ const cells = cellCount === 9
156
+ ? trustBarCells(score)
157
+ : {
158
+ filled: Math.max(0, Math.min(cellCount, Math.round((score / 100) * cellCount))),
159
+ empty: Math.max(0, cellCount - Math.max(0, Math.min(cellCount, Math.round((score / 100) * cellCount)))),
160
+ };
161
+ const color = trustColor(score);
162
+ return (_jsxs(Text, { children: [_jsx(Text, { color: color, children: "▓".repeat(cells.filled) }), _jsx(Text, { color: CHROME, children: "░".repeat(cells.empty) }), showScore ? _jsx(Text, { color: color, children: " " + `${score}%`.padStart(4) }) : null] }));
163
+ }
164
+ const TRUST_TIER_LABEL_WIDTH = 10;
165
+ function TrustTierMeter({ tier, score, issues, cells: cellCount = 9, showLabel = true }) {
166
+ const tone = trustRiskTone({ score, issues, tier });
167
+ const filled = tierFill(tone.tier, cellCount);
168
+ const label = tone.tier === "verified" ? "EVIDENCE" : tone.label;
169
+ return (_jsxs(Text, { children: [_jsx(Text, { color: tierColor(tone.tier), children: "▓".repeat(filled) }), _jsx(Text, { color: CHROME, children: "░".repeat(Math.max(0, cellCount - filled)) }), showLabel ? _jsxs(Text, { color: tierColor(tone.tier), children: [" ", truncate(label, TRUST_TIER_LABEL_WIDTH).padStart(TRUST_TIER_LABEL_WIDTH)] }) : null] }));
170
+ }
171
+ export function Centered({ width, children }) {
172
+ const margin = Math.max(0, Math.floor((width - Math.min(width - 4, 104)) / 2));
173
+ return (_jsx(Box, { marginLeft: margin, marginRight: margin, children: children }));
174
+ }
175
+ export function SelectedServerPanel({ view, result, server, client, installScope, width, testResult, testing, versionInfo, }) {
176
+ switch (view) {
177
+ case "plan":
178
+ return _jsx(PlanView, { server: server, client: client, installScope: installScope, width: width, versionInfo: versionInfo });
179
+ case "config":
180
+ return _jsx(ConfigView, { server: server, client: client, installScope: installScope, width: width });
181
+ case "details":
182
+ case "discover":
183
+ default:
184
+ return _jsx(DetailsView, { result: result, server: server, width: width, testResult: testResult, testing: testing, versionInfo: versionInfo });
185
+ }
186
+ }
187
+ function DetailsView({ result, server: selectedServer, width, testResult, testing, versionInfo }) {
188
+ if (!result)
189
+ return _jsx(EmptyPanel, { title: "Overview" });
190
+ const server = selectedServer ?? result.server;
191
+ const trust = scoreServer(server);
192
+ const risk = trustRiskTone(trust);
193
+ const dimensions = trustDimensions(trust);
194
+ const trustRowWidth = Math.max(28, width - 6);
195
+ const trustBarCells = Math.max(9, Math.min(16, trustRowWidth - 34));
196
+ const profileScore = trustProfileScore(trust);
197
+ const capScore = trust.overallScore ?? trust.score;
198
+ const capExplanation = trustCapExplanation(trust);
199
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: SURFACE, paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(ModalTitle, { title: "overview", file: "server.json" }), _jsxs(Text, { bold: true, color: "white", wrap: "truncate", children: [server.name, "@", server.version] }), _jsx(Text, { color: MUTED, wrap: "wrap", children: server.description || "No description declared." }), _jsx(Spacer, {}), _jsx(Metric, { label: "title", value: server.title }), _jsx(Metric, { label: "registry", value: server.registrySource, valueColor: server.registrySource === "docker" ? WARN : OK }), _jsx(Metric, { label: "runtime", value: server.packageTypes.join(", ") || server.remoteTypes.join(", ") || "none" }), _jsx(Metric, { label: "transport", value: server.transports.join(", ") || "none" }), _jsx(Metric, { label: "clients", value: clientSupportSummary(server, "project") }), _jsx(Metric, { label: "secrets", value: server.requiresSecrets ? "declared" : "none declared", valueColor: server.requiresSecrets ? WARN : OK }), versionInfo ? (_jsxs(_Fragment, { children: [_jsx(Metric, { label: "selected", value: versionInfo.selectedVersion, valueColor: versionInfo.selectedVersion === versionInfo.latestVersion ? OK : WARN }), _jsx(Metric, { label: "latest", value: versionInfo.latestVersion, valueColor: versionInfo.status === "update available" ? WARN : OK }), _jsx(Metric, { label: "locked", value: `${versionInfo.lockedLabel} (${versionInfo.status})`, valueColor: lockStatusColor(versionInfo.status) }), versionInfo.versions.length > 1 ? _jsx(Metric, { label: "versions", value: formatVersionChoices(versionInfo, 6) }) : null] })) : null, _jsx(Metric, { label: "badges", value: trust.badges.join(", ") || "no badges" }), _jsx(Metric, { label: "evidence", value: evidenceSummary(trust) }), _jsx(Spacer, {}), _jsx(CompactDivider, { width: width }), _jsx(Spacer, {}), _jsxs(Box, { flexDirection: "column", children: [_jsx(TrustTierRow, { label: "evidence", value: risk.label, tier: risk.tier, score: trustTierScore(trust), issues: trust.issues, cells: trustBarCells, suffix: `${trustTier(trust)} tier`, width: trustRowWidth }), _jsx(TrustScoreRow, { label: "profile", score: profileScore, cells: trustBarCells, suffix: "metadata profile", width: trustRowWidth }), dimensions.map((dimension) => (_jsx(TrustScoreRow, { label: dimension.label, score: dimension.score, cells: trustBarCells, suffix: "pillar", width: trustRowWidth }, dimension.label))), capExplanation ? _jsxs(Text, { color: WARN, wrap: "wrap", children: ["cap ", truncate(capLabel(trust.capReason, capScore), width - 12)] }) : null, trust.gatedBy?.length ? _jsxs(Text, { color: WARN, wrap: "truncate", children: ["gated by ", truncate(trust.gatedBy.join(", "), width - 12)] }) : null, trust.issues.length > 0 ? _jsx(IssueRows, { issues: trust.issues, width: width, rows: Math.min(4, trust.issues.length) }) : null] }), _jsx(Spacer, {}), _jsx(CompactDivider, { width: width }), _jsx(Spacer, {}), _jsxs(Text, { children: [_jsx(Text, { color: MUTED, children: "test " }), testing ? _jsx(Text, { color: WARN, children: "running MCP handshake and tools/list..." }) : testResult ? (_jsxs(Text, { color: testResult.ok ? OK : ERR, children: [testResult.ok ? "passed" : "failed", ": ", truncate(testResult.message, width - 20)] })) : _jsx(Text, { color: MUTED, children: "press t to connect and list tools" })] }), testResult?.tools.slice(0, 4).map((tool) => (_jsxs(Text, { color: "white", wrap: "truncate", children: ["tool ", tool.name, tool.description ? _jsxs(Text, { color: MUTED, children: [" - ", truncate(tool.description, width - tool.name.length - 20)] }) : null] }, tool.name)))] }));
200
+ }
201
+ const TRUST_ROW_LABEL_WIDTH = 12;
202
+ const TRUST_ROW_VALUE_WIDTH = 12;
203
+ function TrustTierRow({ label, value, tier, score, issues, cells, suffix, width, }) {
204
+ const suffixWidth = Math.max(0, width - TRUST_ROW_LABEL_WIDTH - TRUST_ROW_VALUE_WIDTH - cells - 4);
205
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: MUTED, children: label.padEnd(TRUST_ROW_LABEL_WIDTH) }), _jsx(Text, { color: tierColor(tier), children: truncate(value, TRUST_ROW_VALUE_WIDTH).padEnd(TRUST_ROW_VALUE_WIDTH) }), _jsx(Text, { color: CHROME, children: " " }), _jsx(TrustTierMeter, { tier: tier, score: score, issues: issues, cells: cells, showLabel: false }), _jsx(Text, { color: CHROME, children: " " }), _jsx(Text, { color: MUTED, children: truncate(suffix, suffixWidth) })] }));
206
+ }
207
+ function TrustScoreRow({ label, score, cells, suffix, width }) {
208
+ const suffixWidth = Math.max(0, width - TRUST_ROW_LABEL_WIDTH - TRUST_ROW_VALUE_WIDTH - cells - 4);
209
+ const value = `${Math.round(score)}%`;
210
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: MUTED, children: label.padEnd(TRUST_ROW_LABEL_WIDTH) }), _jsx(Text, { color: trustColor(score), children: value.padStart(4).padEnd(TRUST_ROW_VALUE_WIDTH) }), _jsx(Text, { color: CHROME, children: " " }), _jsx(TrustMeter, { score: score, showScore: false, cells: cells }), _jsx(Text, { color: CHROME, children: " " }), _jsx(Text, { color: MUTED, children: truncate(suffix, suffixWidth) })] }));
211
+ }
212
+ function capLabel(capReason, cappedScore) {
213
+ if (capReason === "automated evidence incomplete")
214
+ return "evidence gate max 69%: automated evidence incomplete";
215
+ if (capReason === "no verified provenance")
216
+ return "evidence gate max 59%: no verified provenance";
217
+ if (capReason?.startsWith("veto: "))
218
+ return `evidence gate max 20%: ${capReason}`;
219
+ if (capReason)
220
+ return `evidence gate max ${Math.round(cappedScore)}%: ${capReason}`;
221
+ return `evidence gate max ${Math.round(cappedScore)}%`;
222
+ }
223
+ function PlanView({ server, client, installScope, width, versionInfo }) {
224
+ if (!server)
225
+ return _jsx(EmptyPanel, { title: "Install" });
226
+ const targetClients = selectedInstallClientsForServerScope(client, installScope, server);
227
+ const planClient = targetClients[0] ?? (client === "all" ? "codex" : client);
228
+ const content = safeJson(() => buildInstallPlan(server, planClient));
229
+ if ("error" in asObject(content)) {
230
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: SURFACE, paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(ModalTitle, { title: "install", file: "plan" }), _jsxs(Text, { color: MUTED, children: ["support ", _jsx(Text, { color: "white", children: truncate(clientSupportSummary(server, installScope), width - 17) })] }), _jsx(Spacer, {}), _jsx(Text, { color: ERR, children: String(asObject(content).error) })] }));
231
+ }
232
+ const plan = content;
233
+ const target = asObject(plan.selectedTarget);
234
+ const targetKind = String(target.kind ?? "unknown");
235
+ const targetRuntime = targetKind === "remote"
236
+ ? String(target.type ?? "remote")
237
+ : String(target.registryType ?? "package");
238
+ const targetLocator = targetKind === "remote"
239
+ ? String(target.url ?? "")
240
+ : String(target.identifier ?? "");
241
+ const clientLabel = installClientLabel(client, targetClients);
242
+ const trustTone = trustRiskTone(plan.trust);
243
+ const writeSummary = client === "all"
244
+ ? `${scopeLabel(installScope)} configs for ${targetClients.join(", ")} plus mcp-lock.json`
245
+ : `${scopeLabel(installScope)} ${client} config plus mcp-lock.json`;
246
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: SURFACE, paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(ModalTitle, { title: "install", file: "plan" }), _jsxs(Text, { color: MUTED, children: ["client ", _jsx(Text, { color: "white", children: clientLabel })] }), _jsxs(Text, { color: MUTED, children: ["support ", _jsx(Text, { color: "white", children: truncate(clientSupportSummary(server, installScope), width - 17) })] }), _jsxs(Text, { color: MUTED, children: ["scope ", _jsx(Text, { color: "white", children: scopeLabel(installScope) })] }), _jsxs(Text, { color: MUTED, children: ["actions ", _jsx(Text, { color: "white", children: "i install" }), " ", _jsx(Text, { color: "white", children: "w lock" })] }), _jsx(Spacer, {}), _jsx(PlanMetric, { label: "target", value: targetRuntime, width: width, valueColor: targetKind === "remote" ? OK : WARN }), _jsx(PlanMetric, { label: "locator", value: targetLocator, width: width, valueColor: targetKind === "remote" ? OK : WARN }), versionInfo ? (_jsxs(_Fragment, { children: [_jsx(Spacer, {}), _jsx(PlanMetric, { label: "selected", value: versionInfo.selectedVersion, width: width, valueColor: versionInfo.selectedVersion === versionInfo.latestVersion ? OK : WARN }), _jsx(PlanMetric, { label: "locked", value: versionInfo.lockedLabel, width: width, valueColor: versionInfo.lockedLabel === "none" ? MUTED : OK }), _jsx(PlanMetric, { label: "latest", value: versionInfo.latestVersion, width: width, valueColor: versionInfo.status === "update available" ? WARN : OK }), _jsx(PlanMetric, { label: "status", value: versionInfo.status, width: width, valueColor: versionInfo.selectedVersion === versionInfo.latestVersion ? OK : WARN })] })) : null, versionInfo && versionInfo.versions.length > 1 ? _jsx(PlanMetric, { label: "versions", value: `${formatVersionChoices(versionInfo, 8)} (v/V cycle)`, width: width }) : null, _jsx(Spacer, {}), _jsx(PlanMetric, { label: "tier", value: `${trustTone.label} / ${trustTier(plan.trust)}`, width: width, valueColor: trustColor(trustTierScore(plan.trust)) }), _jsx(PlanMetric, { label: "profile", value: `${trustProfileScore(plan.trust)}% metadata / ${evidenceStatus(plan.trust)}`, width: width, valueColor: trustColor(trustProfileScore(plan.trust)) }), _jsx(PlanMetric, { label: "evidence", value: evidenceSummary(plan.trust), width: width }), _jsx(Spacer, {}), _jsx(PlanMetric, { label: "writes", value: writeSummary, width: width }), targetClients.map((targetClient) => (_jsx(PlanMetric, { label: targetClient, value: configTargetLabel(targetClient, installScope), width: width }, targetClient))), server.requiresSecrets ? _jsx(PlanMetric, { label: "secrets", value: "required before runtime/test can succeed", width: width, valueColor: WARN }) : null, _jsx(IssueRows, { issues: plan.trust.issues, width: width })] }));
247
+ }
248
+ function ConfigView({ server, client, installScope, width }) {
249
+ if (!server)
250
+ return _jsx(EmptyPanel, { title: "Config" });
251
+ if (client === "all") {
252
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: SURFACE, paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(ModalTitle, { title: "config", file: "targets" }), _jsxs(Text, { color: MUTED, children: ["client ", _jsx(Text, { color: "white", children: "all" }), " scope ", _jsx(Text, { color: "white", children: installScope })] }), _jsx(Spacer, {}), selectedInstallClientsForServerScope("all", installScope, server).map((targetClient) => (_jsx(PlanMetric, { label: targetClient, value: configTargetLabel(targetClient, installScope), width: width }, targetClient)))] }));
253
+ }
254
+ const formatted = safeJson(() => formatClientConfigSnippet(client, exportClientConfig(server, client).config));
255
+ const formattedError = asObject(formatted).error;
256
+ const content = typeof formattedError === "string"
257
+ ? JSON.stringify(formatted, null, 2)
258
+ : formatted.content.trimEnd();
259
+ const extension = typeof formattedError === "string"
260
+ ? "json"
261
+ : formatted.extension;
262
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: SURFACE, paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(ModalTitle, { title: "config", file: `${client}.${extension}` }), _jsxs(Text, { color: MUTED, children: ["client ", _jsx(Text, { color: "white", children: client }), " scope ", _jsx(Text, { color: "white", children: installScope }), " target ", _jsx(Text, { color: "white", children: configTargetLabel(client, installScope) })] }), _jsx(Text, { color: MUTED, children: "i install s save" }), _jsx(CodeBlock, { content: content, width: width, maxLines: 16 })] }));
263
+ }
264
+ export function HelpView({ width, height }) {
265
+ const lines = [
266
+ ["Browse", "/", "Edit the search text. Press Esc while searching to clear it."],
267
+ ["Browse", "j/k", "Move through the current server list."],
268
+ ["Browse", "f", "Change list layout: flat, grouped by project, or grouped by category when categories exist."],
269
+ ["Browse", "a", "Change sort: source-first, alpha A-Z, alpha Z-A, source-last, or relevance."],
270
+ ["Browse", "m / +", "Show 50 more matches until every loaded or cached match is visible."],
271
+ ["Browse", "r", "Refresh enabled registry sources into .toolpin/registry-cache.json."],
272
+ ["Browse", "l", "Toggle live session loading without writing the registry cache."],
273
+ ["Browse", "g", `Change exact registry source filter. Enabled sources: ${REGISTRY_SOURCES.filter((source) => source.enabled).map((source) => source.id).join(", ")}.`],
274
+ ["Installed", "I", "Show installed MCP servers and refresh the installed inventory."],
275
+ ["Sources", "S", "Show installable vs discovery-only sources, auth status, cache/live counts, and the source legend."],
276
+ ["Review", "Enter", "Open the install plan for the selected server."],
277
+ ["Review", "t", "Test the selected server with initialize and tools/list."],
278
+ ["Review", "v / V", "Cycle selected server version."],
279
+ ["Install", "i", "Open the install wizard; choose version when available, then folder/global and client."],
280
+ ["Install", "w", "Write only the lockfile entry for the selected server."],
281
+ ["Config", "s", "Save the shown client config snippet under .toolpin/ for manual review."],
282
+ ["Installed", "u", "Resolve the installed entry in the registry, make it registry-backed, and write/update mcp-lock.json."],
283
+ ["Installed", "v / V", "Cycle the selected locked install target version; press u to rewrite config and lockfile for that explicit version."],
284
+ ["Installed", "U", "Update all locked installed entries; unlocked adoptable entries are reported separately."],
285
+ ["Installed", "x", "Open a Yes/No confirmation before removing the selected config and lock entry."],
286
+ ["Installed", "d", "Run doctor against installed config and mcp-lock.json."],
287
+ ["Global", "c / G", "Cycle target client and project/global install scope."],
288
+ ["Global", ":", "Open the command palette."],
289
+ ["Global", "R", "Reset search, source, result count, client, and scope."],
290
+ ["Global", "q", "Quit ToolPin."],
291
+ ];
292
+ const lineWidth = Math.max(24, width - 8);
293
+ const showExplanations = height >= 16;
294
+ const shortcutRows = showExplanations
295
+ ? Math.max(2, Math.min(lines.length, height - 25))
296
+ : Math.max(1, height - 4);
297
+ const visibleShortcuts = lines.slice(0, shortcutRows);
298
+ const hiddenShortcutCount = Math.max(0, lines.length - visibleShortcuts.length);
299
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: SURFACE, paddingX: 2, paddingY: 1, height: height, flexGrow: 1, children: [_jsx(ModalTitle, { title: "help", file: "shortcuts + trust" }), _jsx(Text, { color: MUTED, wrap: "truncate", children: "Keyboard shortcuts and what each action changes." }), _jsx(Spacer, {}), _jsx(Text, { bold: true, color: BLUE, children: "shortcuts" }), visibleShortcuts.map(([section, keys, description]) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: MUTED, children: section.padEnd(10) }), _jsx(Text, { bold: true, color: OK, children: keys.padEnd(10) }), _jsx(Text, { color: "white", children: truncate(description, lineWidth - 20) })] }, `${section}:${keys}`))), hiddenShortcutCount > 0 ? _jsxs(Text, { color: CHROME, children: [" ".repeat(10), hiddenShortcutCount, " more shortcut(s) hidden on this terminal height."] }) : null, showExplanations ? (_jsxs(_Fragment, { children: [_jsx(Divider, { width: width }), _jsx(Text, { bold: true, color: BLUE, children: "about toolpin" }), _jsx(HelpNote, { width: lineWidth, label: "created", text: "ToolPin is the review gate between MCP registries and AI clients that run servers with your credentials." }), _jsx(HelpNote, { width: lineWidth, label: "goal", text: "Make MCP installs reviewable and repeatable: inspect the plan, write client config, commit mcp-lock.json, fail CI on drift." }), _jsx(HelpNote, { width: lineWidth, label: "needed", text: "MCP servers can add tools, local process access, network access, and secrets; copied JSON alone leaves no reviewed artifact." }), _jsx(HelpNote, { width: lineWidth, label: "not", text: "ToolPin is not a catalog, runtime sandbox, gateway, or secret vault; it is the repo-owned install and governance layer." }), _jsx(Spacer, {}), _jsx(Text, { bold: true, color: BLUE, children: "scoring" }), _jsx(HelpNote, { width: lineWidth, label: "score", text: "0-100 metadata completeness score for review priority, not a security guarantee or install blocker." }), _jsx(HelpNote, { width: lineWidth, label: "inputs", text: "Source trust, repository metadata, namespace, transport, package pinning, secrets, and description-scan findings." }), _jsx(HelpNote, { width: lineWidth, label: "tiers", text: "Verified requires a pinned install target plus artifact proof; conditional means useful metadata exists but proof is incomplete." }), _jsx(HelpNote, { width: lineWidth, label: "69% cap", text: "69% until proof verified; proof=npm/OCI/MCPB." }), _jsx(HelpNote, { width: lineWidth, label: "cap notes", text: "Cap notes appear below the score bars and explain why the evidence gate capped verification." }), _jsx(HelpNote, { width: lineWidth, label: "colors", text: "Green is verified evidence, yellow needs review, red means blocked or unverified evidence." }), _jsx(Spacer, {}), _jsx(Text, { bold: true, color: BLUE, children: "locking" }), _jsx(HelpNote, { width: lineWidth, label: "lockfile", text: "mcp-lock.json records the selected server, version, client, resolved launch target, trust data, and integrity digest." }), _jsx(HelpNote, { width: lineWidth, label: "install", text: "Install writes client config and the matching lock entry after policy and drift checks pass." }), _jsx(HelpNote, { width: lineWidth, label: "doctor/ci", text: "Doctor and CI compare client config with mcp-lock.json so config drift, digest drift, and signature failures are visible." }), _jsx(HelpNote, { width: lineWidth, label: "adopt / update", text: "Installed u resolves an existing config entry in the registry and locks it; U updates already locked entries." }), _jsx(Spacer, {}), _jsx(Text, { bold: true, color: BLUE, children: "sources" }), _jsx(HelpNote, { width: lineWidth, label: "installable", text: "Official/Docker or official-compatible entries with package/remote metadata can be reviewed, installed, and locked." }), _jsx(HelpNote, { width: lineWidth, label: "discovery", text: "Directory entries such as Glama are browse/search only until normalized into installable metadata." }), _jsx(HelpNote, { width: lineWidth, label: "cached", text: "Cached/loaded is the number of entries currently stored or fetched for that source." }), _jsx(Spacer, {}), _jsx(Text, { bold: true, color: BLUE, children: "installed actions" }), _jsx(HelpNote, { width: lineWidth, label: "action:update", text: "The row is locked and a newer registry version is loaded; u updates config and mcp-lock.json." }), _jsx(HelpNote, { width: lineWidth, label: "action:adopt", text: "The row is installed but not locked; u finds the registry match and writes the reviewed lock entry." }), _jsx(HelpNote, { width: lineWidth, label: "action:none", text: "No safe registry lifecycle action is loaded; refresh, switch source/live, or inspect the row first." })] })) : null] }));
300
+ }
301
+ function HelpNote({ label, text, width }) {
302
+ const labelWidth = 15;
303
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: MUTED, children: label.padEnd(labelWidth) }), _jsx(Text, { color: "white", children: truncate(text, width - labelWidth) })] }));
304
+ }
305
+ export function CommandPalette({ commands, selected, state, selectedServer, width, }) {
306
+ const selectedCommand = commands[selected];
307
+ const commandRows = commands.map((command, index) => {
308
+ const disabled = command.requiresServer && !selectedServer;
309
+ const marker = index === selected ? ">" : ":";
310
+ const suffix = disabled ? " select a server first" : "";
311
+ return truncate(`${marker} ${command.id.padEnd(13)} ${command.label}${suffix}`, width - 4);
312
+ }).join("\n");
313
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: SURFACE, paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(ModalTitle, { title: "commands", file: "toolpin" }), _jsx(Text, { color: MUTED, wrap: "truncate", children: "Enter runs the selected command; install opens the scope/client wizard." }), commands.length === 0 ? _jsx(Text, { color: MUTED, children: "No command matched." }) : null, commands.length > 0 ? _jsx(Text, { color: "white", children: commandRows }) : null, selectedCommand ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: ACCENT, wrap: "truncate", children: truncate(commandLineFor(selectedCommand.id, state, selectedServer), width - 4) }), _jsx(Text, { color: MUTED, wrap: "truncate", children: truncate(selectedCommand.description, width - 4) })] })) : null, state.commandLog ? (_jsxs(_Fragment, { children: [_jsx(Spacer, {}), _jsx(Text, { color: state.commandLog.ok ? OK : ERR, wrap: "truncate", children: state.commandLog.command }), state.commandLog.lines.slice(0, 4).map((line, index) => (_jsxs(Text, { color: MUTED, wrap: "truncate", children: [" ", truncate(line, width - 4)] }, `${index}:${line}`)))] })) : null] }));
314
+ }
315
+ export function InstallWizard({ flow, width, height }) {
316
+ const contentWidth = Math.max(24, width - 6);
317
+ const versionStepEnabled = flow.versions.length > 1;
318
+ const totalSteps = versionStepEnabled ? 3 : 2;
319
+ const stepIndex = installStepIndex(flow, versionStepEnabled);
320
+ const versionOptions = flow.versions.map((server, index) => ({
321
+ id: server.version,
322
+ label: server.version,
323
+ hint: index === 0 ? "latest version" : `older version from ${server.registrySource}`,
324
+ }));
325
+ const scopeOptions = [
326
+ { id: "project", label: "folder (project)", hint: "config in this folder" },
327
+ { id: "global", label: "global (user)", hint: "current-user config" },
328
+ ];
329
+ const clientOptions = installClientChoicesForServerScope(flow.scope ?? "project", flow.preferredClient, flow.server).map((client) => ({
330
+ id: client,
331
+ label: client,
332
+ hint: client === "all" ? "every supported client for the chosen scope" : "",
333
+ }));
334
+ const options = flow.step === "version" ? versionOptions : flow.step === "scope" ? scopeOptions : clientOptions;
335
+ const scopeText = flow.scope === "global" ? "global/user" : "folder/project";
336
+ const stepLabel = flow.step === "version"
337
+ ? "Choose version"
338
+ : flow.step === "scope"
339
+ ? "Choose where to install"
340
+ : flow.step === "client"
341
+ ? `Choose client (${scopeText})`
342
+ : flow.step === "complete"
343
+ ? "Install complete"
344
+ : flow.step === "failed"
345
+ ? "Install failed"
346
+ : "Installing selected MCP server";
347
+ const visibleCount = Math.max(3, height - 10);
348
+ const start = listWindowStart(flow.selected, visibleCount, options.length);
349
+ const visible = options.slice(start, start + visibleCount);
350
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: SURFACE, paddingX: 2, paddingY: 1, height: height, children: [_jsx(ModalTitle, { title: "install", file: flow.server.name }), _jsx(Text, { color: MUTED, wrap: "truncate", children: stepLabel }), stepIndex ? _jsxs(Text, { color: CHROME, wrap: "truncate", children: ["Step ", stepIndex, " of ", totalSteps] }) : null, flow.step === "version" || flow.step === "scope" || flow.step === "client" ? (_jsx(Text, { color: OK, wrap: "truncate", children: "Select an option, then press Enter to continue." })) : null, _jsx(Spacer, {}), flow.step === "installing" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: MUTED, children: "Writing config and mcp-lock.json..." }), _jsx(Box, { marginTop: 1, children: _jsx(InstallActivityBar, { width: contentWidth }) })] })) : flow.step === "complete" ? (_jsx(Text, { color: MUTED, children: "Press Enter or Esc to close." })) : flow.step === "failed" ? (_jsx(Text, { color: ERR, children: "Review the status message, then press Enter or Esc." })) : visible.map((option, index) => {
351
+ const isSelected = start + index === flow.selected;
352
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: isSelected ? OK : CHROME, children: isSelected ? ">" : ":" }), _jsxs(Text, { bold: isSelected, color: isSelected ? OK : "white", children: [" ", option.label.padEnd(18)] }), _jsx(Text, { color: isSelected ? OK : MUTED, children: truncate(option.hint, Math.max(0, contentWidth - 22)) })] }, option.id));
353
+ }), _jsx(Box, { flexGrow: 1 }), flow.step === "version" || flow.step === "scope" || flow.step === "client" ? (_jsx(Text, { color: CHROME, wrap: "truncate", children: " j/k or arrows move Enter continue Esc cancel" })) : flow.step === "installing" ? (_jsx(Text, { color: CHROME, wrap: "truncate", children: " Installing. Keep this session open." })) : (_jsx(Text, { color: CHROME, wrap: "truncate", children: " Enter close Esc close" }))] }));
354
+ }
355
+ function installStepIndex(flow, hasVersionStep) {
356
+ if (flow.step === "version")
357
+ return 1;
358
+ if (flow.step === "scope")
359
+ return hasVersionStep ? 2 : 1;
360
+ if (flow.step === "client")
361
+ return hasVersionStep ? 3 : 2;
362
+ return undefined;
363
+ }
364
+ function InstallActivityBar({ width }) {
365
+ const barWidth = Math.max(10, Math.min(36, width - 2));
366
+ const filled = Math.max(3, Math.floor(barWidth * 0.55));
367
+ const empty = Math.max(0, barWidth - filled);
368
+ return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: ACCENT, children: "▓".repeat(filled) }), _jsx(Text, { color: CHROME, children: "░".repeat(empty) })] }), _jsx(Text, { color: ACCENT, wrap: "truncate", children: "installing" })] }));
369
+ }
370
+ export function ActivityStrip({ state, width }) {
371
+ const active = state.installing || state.testing || state.loading || state.checking;
372
+ const log = commandLogForView(state);
373
+ const color = state.error || log?.ok === false ? ERR : active ? WARN : log ? OK : MUTED;
374
+ const label = active ? "working" : log ? log.title : "status";
375
+ const activeMessage = state.loading
376
+ ? "loading registry data..."
377
+ : state.installing
378
+ ? "installing reviewed server..."
379
+ : state.testing
380
+ ? "testing selected MCP server..."
381
+ : state.checking
382
+ ? "checking installed config drift..."
383
+ : undefined;
384
+ const primary = activeMessage ?? log?.lines[0] ?? state.lastAction ?? "ready";
385
+ const secondary = active ? log?.lines.slice(0, 2) ?? [] : log?.lines.slice(1, 3) ?? [];
386
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, flexShrink: 0, children: [_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { bold: true, color: color, children: label.padEnd(8) }), _jsx(Text, { color: "white", children: truncate(primary, width - 14) })] }), secondary.map((line, index) => (_jsxs(Text, { color: MUTED, wrap: "truncate", children: [_jsx(Text, { color: CHROME, children: " ".repeat(8) }), truncate(line, width - 11)] }, `${index}:${line}`)))] }));
387
+ }
388
+ export function OperationModal({ state, width, height }) {
389
+ const active = state.installing || state.testing || state.checking;
390
+ const log = commandLogForView(state);
391
+ const candidate = buildOperationSnapshot({ active, log, state });
392
+ const [snapshot, setSnapshot] = React.useState(candidate);
393
+ const candidateKey = candidate?.key;
394
+ React.useEffect(() => {
395
+ if (state.installFlow || !candidate) {
396
+ setSnapshot(undefined);
397
+ return undefined;
398
+ }
399
+ setSnapshot(candidate);
400
+ if (candidate.active)
401
+ return undefined;
402
+ const timer = setTimeout(() => {
403
+ setSnapshot((current) => current?.key === candidate.key ? undefined : current);
404
+ }, 3000);
405
+ return () => clearTimeout(timer);
406
+ }, [candidateKey, state.installFlow]);
407
+ if (!snapshot || state.installFlow)
408
+ return null;
409
+ const modalWidth = Math.min(Math.max(44, Math.floor(width * 0.44)), 86);
410
+ const lines = snapshot.lines.slice(0, 4);
411
+ const modalHeight = lines.length + 6;
412
+ const left = Math.max(0, Math.floor((width - modalWidth) / 2));
413
+ const top = Math.max(0, Math.floor((height - modalHeight) / 2));
414
+ const lineWidth = Math.max(8, modalWidth - 4);
415
+ return (_jsxs(Box, { position: "absolute", left: left, top: top, width: modalWidth, backgroundColor: SURFACE, paddingX: 2, paddingY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: BLUE, wrap: "truncate", children: truncate(snapshot.title, modalWidth - 4) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: CHROME, children: "─".repeat(lineWidth) }) }), _jsx(Spacer, {}), lines.map((line, index) => (_jsx(Text, { color: MUTED, wrap: "truncate", children: truncate(line, modalWidth - 4) }, `${index}:${line}`)))] }));
416
+ }
417
+ export function DeleteConfirmModal({ state, width, height }) {
418
+ const confirm = state.deleteConfirm;
419
+ if (!confirm)
420
+ return null;
421
+ const modalWidth = Math.min(Math.max(54, Math.floor(width * 0.42)), 84);
422
+ const modalHeight = 12;
423
+ const left = Math.max(0, Math.floor((width - modalWidth) / 2));
424
+ const top = Math.max(0, Math.floor((height - modalHeight) / 2));
425
+ const lineWidth = Math.max(8, modalWidth - 4);
426
+ const selectedNo = confirm.selected === "no";
427
+ const selectedYes = confirm.selected === "yes";
428
+ return (_jsxs(Box, { position: "absolute", left: left, top: top, width: modalWidth, backgroundColor: SURFACE, paddingX: 2, paddingY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: BLUE, children: "delete installed server?" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: CHROME, children: "─".repeat(lineWidth) }) }), _jsx(Spacer, {}), _jsx(Text, { color: MUTED, wrap: "wrap", children: "This will remove the client config entry and matching mcp-lock.json entry." }), confirm.runtimeAdvisory ? (_jsx(Text, { color: WARN, wrap: "wrap", children: truncate(confirm.runtimeAdvisory, modalWidth * 2) })) : null, _jsx(Text, { color: "white", wrap: "truncate", children: truncate(confirm.serverName, modalWidth - 4) }), _jsxs(Text, { color: MUTED, wrap: "truncate", children: [confirm.client, " / ", confirm.scope] }), _jsx(Spacer, {}), _jsxs(Text, { children: [_jsxs(Text, { bold: selectedNo, color: selectedNo ? OK : MUTED, children: [selectedNo ? "> " : " ", "No, keep it"] }), _jsx(Text, { color: CHROME, children: " " }), _jsxs(Text, { bold: selectedYes, color: selectedYes ? ERR : MUTED, children: [selectedYes ? "> " : " ", "Yes, delete"] })] }), _jsx(Spacer, {}), _jsx(Text, { color: CHROME, children: "Left/right or j/k choose Enter confirm y yes n/Esc no" })] }));
429
+ }
430
+ export function buildOperationSnapshot({ active, log, state }) {
431
+ if (!active && !log)
432
+ return undefined;
433
+ if (!active && log && !isOperationLog(log.title))
434
+ return undefined;
435
+ const activeTitle = state.testing
436
+ ? "testing"
437
+ : state.checking
438
+ ? "checking"
439
+ : activeOperationTitle(log?.title ?? "install");
440
+ const title = active ? activeTitle : settledOperationTitle(log?.title ?? "operation", log?.ok ?? true);
441
+ const activeMessage = state.testing
442
+ ? "testing selected MCP server..."
443
+ : state.checking
444
+ ? "checking config and lock drift..."
445
+ : "installing or updating MCP server...";
446
+ const outcome = !active && log ? (log.ok ? "complete" : "failed") : undefined;
447
+ const lines = [
448
+ active ? activeMessage : outcome,
449
+ ...(log?.lines ?? []),
450
+ ].filter((line) => Boolean(line));
451
+ const key = [
452
+ active ? "active" : "settled",
453
+ title,
454
+ log?.ok === false ? "error" : "ok",
455
+ log?.command ?? "",
456
+ lines.join("\u0000"),
457
+ ].join("\u0001");
458
+ return { key, title, lines, active };
459
+ }
460
+ function activeOperationTitle(title) {
461
+ if (title === "test")
462
+ return "testing";
463
+ if (title === "install")
464
+ return "installing";
465
+ if (title === "update")
466
+ return "updating";
467
+ if (title === "adopt")
468
+ return "adopting";
469
+ if (title === "doctor")
470
+ return "checking";
471
+ return title;
472
+ }
473
+ function settledOperationTitle(title, ok) {
474
+ const suffix = ok ? "complete" : "failed";
475
+ if (title === "test")
476
+ return `test ${suffix}`;
477
+ if (title === "install")
478
+ return `install ${suffix}`;
479
+ if (title === "update")
480
+ return `update ${suffix}`;
481
+ if (title === "adopt")
482
+ return `adopt ${suffix}`;
483
+ if (title === "doctor")
484
+ return `check ${suffix}`;
485
+ if (title === "remove")
486
+ return `remove ${suffix}`;
487
+ return `${title} ${suffix}`;
488
+ }
489
+ function isOperationLog(title) {
490
+ return ["install", "update", "adopt", "test", "doctor", "remove"].includes(title);
491
+ }
492
+ export function Footer({ state, width }) {
493
+ const { inputMode, view } = state;
494
+ const hints = inputMode === "search"
495
+ ? [["Enter", "apply"], ["Esc", "cancel"], ["Backspace", "edit"]]
496
+ : inputMode === "command"
497
+ ? [["Enter", "run"], ["Esc", "close"], ["Type", "filter"], ["j/k", "select"]]
498
+ : view === "discover"
499
+ ? [["/", "search"], ["a", `sort ${browseSortLabel(state.browseSortMode)}`], ["f", "layout"], ["g", `source ${state.sourceMode}`], ["S", "sources"], ["m", "more"], ["i", "install"], ["I", "installed"], ["r", "cache-refresh"], ["l", "live/cache"], ["R", "reset"], ["j/k", "move"], ["q", "quit"]]
500
+ : view === "installed"
501
+ ? [["j/k", "move"], ["I", "refresh list"], ["S", "sources"], ["u", "registry+lock"], ["v/V", "version"], ["U", "update locked"], ["x", "delete"], ["t", "test-installed"], ["d", "doctor"], ["q", "quit"]]
502
+ : view === "sources"
503
+ ? [["Esc", "browse"], ["g", "source"], ["l", "cache/live"], ["r", "refresh"], ["1", "browse"], ["2", "installed"], ["q", "quit"]]
504
+ : view === "details"
505
+ ? [["Enter", "plan"], ["Esc", "browse"], ["c", "client"], ["G", "scope"], ["v/V", "version"], ["t", "test"], ["i", "install"], ["q", "quit"]]
506
+ : view === "plan"
507
+ ? [["Enter", "install"], ["Esc", "browse"], ["c", "client"], ["G", "scope"], ["v/V", "version"], ["i", "install"], ["q", "quit"]]
508
+ : [["Esc", "browse"], ["c", "client"], ["G", "scope"], ["v/V", "version"], ["i", "install"], ["q", "quit"]];
509
+ const copyright = "© 2026 Proofofwork Agency · https://github.com/proofofwork-agency/toolpin";
510
+ const copyrightWidth = Math.min(copyright.length, Math.max(0, width - 8));
511
+ const hintWidth = Math.max(10, width - copyrightWidth - 8);
512
+ return (_jsxs(Box, { paddingX: 2, marginTop: 1, flexShrink: 0, flexDirection: "column", children: [_jsx(TrustStateLegend, { width: Math.max(1, width - 4) }), _jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { width: hintWidth, children: _jsx(Text, { wrap: "truncate", children: hints.map(([keyName, label], index) => (_jsxs(React.Fragment, { children: [index > 0 ? _jsx(Text, { color: CHROME, children: " | " }) : null, _jsx(Text, { bold: true, color: "white", children: keyName }), _jsxs(Text, { color: MUTED, children: [":", label] })] }, keyName))) }) }), _jsx(Text, { color: CHROME, wrap: "truncate", children: truncate(copyright, copyrightWidth) })] })] }));
513
+ }
514
+ export function TrustStateLegend({ width }) {
515
+ const compact = width < 118;
516
+ const items = compact
517
+ ? [
518
+ { label: "OK", color: OK, text: "verified" },
519
+ { label: "REVIEW", color: WARN, text: "needs-proof" },
520
+ { label: "UNVERIFIED", color: ERR, text: "weak" },
521
+ { label: "BLOCKED", color: ERR, text: "stop" },
522
+ ]
523
+ : [
524
+ { label: "OK", color: OK, text: "verified" },
525
+ { label: "REVIEW", color: WARN, text: "needs-proof" },
526
+ { label: "UNVERIFIED", color: ERR, text: "weak evidence" },
527
+ { label: "BLOCKED", color: ERR, text: "stop" },
528
+ ];
529
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: CHROME, children: "trust " }), items.map((item, index) => (_jsxs(React.Fragment, { children: [index > 0 ? _jsx(Text, { color: CHROME, children: " " }) : null, _jsx(Text, { color: item.color, children: "\u2593" }), _jsx(Text, { bold: true, color: item.color, children: item.label }), _jsxs(Text, { color: MUTED, children: [" ", item.text] })] }, item.label)))] }));
530
+ }
531
+ function EmptyPanel({ title }) {
532
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: SURFACE, paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(ModalTitle, { title: title.toLowerCase(), file: "empty" }), _jsx(Text, { color: MUTED, children: "No server selected. Search and select a server first." })] }));
533
+ }
534
+ function ModalTitle({ title, file }) {
535
+ return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: ACCENT, children: title }), _jsx(Text, { color: CHROME, children: file })] }));
536
+ }
537
+ function Metric({ label, value, valueColor }) {
538
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: MUTED, children: label.padEnd(12) }), _jsx(Text, { color: valueColor ?? "white", children: value })] }));
539
+ }
540
+ function PlanMetric({ label, value, width, valueColor }) {
541
+ const valueWidth = Math.max(8, width - 18);
542
+ const lines = wrapValue(value, valueWidth);
543
+ return (_jsx(_Fragment, { children: lines.map((line, index) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: MUTED, children: index === 0 ? label.padEnd(12) : "".padEnd(12) }), _jsx(Text, { color: valueColor ?? "white", children: line })] }, `${label}:${index}:${line}`))) }));
544
+ }
545
+ function wrapValue(value, width) {
546
+ if (width <= 0)
547
+ return [""];
548
+ const words = value.split(/(\s+)/).filter((part) => part.length > 0);
549
+ const lines = [];
550
+ let line = "";
551
+ for (const word of words) {
552
+ if (/^\s+$/.test(word)) {
553
+ if (line && !line.endsWith(" "))
554
+ line += " ";
555
+ continue;
556
+ }
557
+ if (word.length > width) {
558
+ if (line.trimEnd())
559
+ lines.push(line.trimEnd());
560
+ for (let index = 0; index < word.length; index += width) {
561
+ lines.push(word.slice(index, index + width));
562
+ }
563
+ line = "";
564
+ continue;
565
+ }
566
+ const candidate = line ? `${line}${word}` : word;
567
+ if (candidate.length > width) {
568
+ if (line.trimEnd())
569
+ lines.push(line.trimEnd());
570
+ line = word;
571
+ }
572
+ else {
573
+ line = candidate;
574
+ }
575
+ }
576
+ if (line.trimEnd())
577
+ lines.push(line.trimEnd());
578
+ return lines.length ? lines : [""];
579
+ }
580
+ function CodeBlock({ content, width, maxLines }) {
581
+ const lines = content.split("\n");
582
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [lines.slice(0, maxLines).map((line, index) => (_jsx(Text, { color: line.trim().startsWith('"') || line.includes("=") ? "white" : MUTED, wrap: "truncate", children: truncate(line, width - 6) }, `${index}:${line}`))), lines.length > maxLines ? _jsxs(Text, { color: MUTED, children: ["... ", lines.length - maxLines, " more lines"] }) : null] }));
583
+ }
584
+ function Spacer() {
585
+ return _jsx(Text, { children: " " });
586
+ }
587
+ function Divider({ width, marginBottom = 0 }) {
588
+ const lineWidth = Math.max(8, width - 6);
589
+ return (_jsx(Box, { marginTop: 1, marginBottom: marginBottom, children: _jsx(Text, { color: CHROME, children: "─".repeat(lineWidth) }) }));
590
+ }
591
+ function CompactDivider({ width }) {
592
+ const lineWidth = Math.max(8, width - 6);
593
+ return _jsx(Text, { color: CHROME, children: "─".repeat(lineWidth) });
594
+ }
595
+ function IssueRows({ issues, width, rows = 4 }) {
596
+ const visible = issues.slice(0, rows);
597
+ const blank = " ".repeat(Math.max(1, width - 4));
598
+ return (_jsx(_Fragment, { children: Array.from({ length: rows }, (_, index) => {
599
+ const issue = visible[index];
600
+ if (!issue) {
601
+ return _jsx(Text, { children: blank }, `issue-empty-${index}`);
602
+ }
603
+ return (_jsxs(Text, { color: issue.severity === "critical" ? ERR : issue.severity === "warning" ? WARN : MUTED, wrap: "truncate", children: [issue.severity, ": ", truncate(issue.message, width - 12)] }, `${issue.code}-${index}`));
604
+ }) }));
605
+ }
606
+ function lockStatusColor(status) {
607
+ if (status === "current")
608
+ return OK;
609
+ if (status === "not locked")
610
+ return ERR;
611
+ if (status === "update available" || status === "ahead of registry")
612
+ return WARN;
613
+ return MUTED;
614
+ }
615
+ function trustColor(score) {
616
+ if (score >= 80)
617
+ return OK;
618
+ if (score >= 60)
619
+ return WARN;
620
+ return ERR;
621
+ }
622
+ function tierColor(tier) {
623
+ if (tier === "verified")
624
+ return OK;
625
+ if (tier === "conditional")
626
+ return WARN;
627
+ return ERR;
628
+ }
629
+ function tierFill(tier, cells) {
630
+ if (tier === "verified")
631
+ return cells;
632
+ if (tier === "conditional")
633
+ return Math.max(1, Math.round(cells * 0.66));
634
+ if (tier === "unverified")
635
+ return Math.max(1, Math.round(cells * 0.33));
636
+ return 0;
637
+ }