#!/usr/bin/env node /** * @termix-ssh/plugin-sdk/ui is public API, implemented by * src/ui/plugin-host/sdk-ui.ts. Adding to it is a contract change and * removing from it is a breaking one, so neither should happen by accident. * * Resolves every name the module exports (through its `export *` lines too) * and fails when that differs from ALLOWED. A new export goes on the list in * the same change, which is what makes it a deliberate one. Only generic * shell pieces belong here: anything one feature needs lives in its plugin. * * --list prints what the module exports now. */ const path = require("node:path"); const ROOT = path.resolve(__dirname, ".."); const SDK_UI = path.join(ROOT, "src", "ui", "plugin-host", "sdk-ui.ts"); const TSCONFIG = path.join(ROOT, "tsconfig.app.json"); const ALLOWED = [ "ACCENT_PRESET_COLORS", "APP_VERSION", "AccessRecord", "ActionSlot", "ActiveSessionInfo", "AddButton", "AdminSection", "Alert", "AlertDescription", "AlertDialog", "AlertDialogAction", "AlertDialogCancel", "AlertDialogContent", "AlertDialogDescription", "AlertDialogFooter", "AlertDialogHeader", "AlertDialogOverlay", "AlertDialogPortal", "AlertDialogTitle", "AlertDialogTrigger", "AlertTitle", "ApiKey", "AuditLog", "AuditLogFilters", "AuthEnrollmentSections", "AuthOverrideProtocol", "BackButton", "Badge", "BarSeries", "BarSeriesItem", "BarSeriesProps", "BrandingSettings", "BrowserSignInDialog", "Button", "ButtonProps", "CORE_HOST_DEFAULTS", "CORE_NAMESPACE", "CREDENTIAL_ROW_FIELD_KEYS", "Card", "CardAction", "CardContent", "CardDescription", "CardFooter", "CardGridCanvas", "CardHeader", "CardMasonry", "CardTitle", "Checkbox", "ColumnCountStepper", "Command", "CommandEmpty", "CommandGroup", "CommandItem", "CommandList", "CommandPrimitive", "ComponentSlot", "ConfirmOptions", "ConnectionGate", "ConnectionGateController", "ConnectionGateProps", "ConnectionLogPanel", "ConnectionLogProvider", "ConnectionOrigin", "ConnectionScreen", "ConnectionStage", "ConnectionStatus", "ConnectionUnavailable", "CreatedApiKey", "Credential", "CredentialDensity", "CredentialPermissionLevel", "CredentialRowFields", "CredentialSidebarFilterState", "CredentialSidebarPreferences", "CredentialSortKey", "CredentialTrayTrigger", "CustomKeybinding", "DASHBOARD_CARDS", "DEFAULT_FOLDER_COLOR", "DEFAULT_FOLDER_ICON", "DashboardCardId", "DataColumn", "DataView", "DefaultOverrides", "DefaultsOnly", "DesktopUpdateMode", "DesktopUpdateState", "Dialog", "DialogClose", "DialogContent", "DialogDescription", "DialogFooter", "DialogHeader", "DialogOverlay", "DialogPortal", "DialogTitle", "DialogTrigger", "DocsLink", "DocsLinkProps", "DocsPage", "DropdownMenu", "DropdownMenuCheckboxItem", "DropdownMenuContent", "DropdownMenuGroup", "DropdownMenuItem", "DropdownMenuLabel", "DropdownMenuPortal", "DropdownMenuRadioGroup", "DropdownMenuRadioItem", "DropdownMenuSeparator", "DropdownMenuShortcut", "DropdownMenuSub", "DropdownMenuSubContent", "DropdownMenuSubTrigger", "DropdownMenuTrigger", "EditingWidth", "EmptyState", "FOLDER_COLORS", "FONT_SIZES", "Facts", "FakeSwitch", "Field", "FieldPair", "FolderAccessRule", "FolderIconEl", "FontSizeId", "FormFooter", "FullScreenAppPhase", "FullScreenAppWrapper", "GridCardCatalogEntry", "GridColSpan", "GridColumns", "GridLayout", "GridSlot", "GroupHeading", "HexColorPicker", "Host", "HostActionDef", "HostClickBehavior", "HostData", "HostDefaultBadge", "HostDefaultField", "HostDefaultsChange", "HostDefaultsContext", "HostDefaultsContextValue", "HostDefaultsLevel", "HostDefaultsTarget", "HostDensity", "HostFeatureFields", "HostFeatureFieldsProps", "HostFolder", "HostGroupKey", "HostKeyVerificationDialog", "HostOnly", "HostProtocolAuthInput", "HostProtocolDef", "HostProtocols", "HostRowFields", "HostSidebarFilterState", "HostSidebarPreferences", "HostTrayTrigger", "IconPicker", "InlineView", "Input", "Kbd", "KbdKey", "KbdSeparator", "KeyCombo", "KeybindingAction", "KeybindingActionDef", "KeybindingActionType", "LANGUAGES", "Label", "LineChart", "LineChartSeries", "LinkServerDialog", "LinkedAccountInfo", "ListBadge", "ListRow", "ListRowAction", "ListRowFolder", "ListRowTone", "LogContext", "LogEntry", "LookSettings", "MANAGE_REQUEST_EVENT", "MAX_PANES", "MFAPromptMode", "ManageMode", "ManageRequest", "ManagerCardError", "ManagerCardShell", "ManagerSearch", "Meter", "MetricCard", "MiniStat", "NumberField", "OPEN_PLUGINS_EVENT", "OpenTabRecord", "PANEL", "PROMPT_BUTTON", "PROMPT_DESTRUCTIVE_BUTTON", "PROMPT_PRIMARY_BUTTON", "PaletteItemDef", "PanePrompt", "PanelDensity", "PanelList", "PanelSearch", "PanelShell", "PanelViewMode", "PassphraseDialog", "PasswordInput", "PermissionCatalogEntry", "PluginComponent", "PRESETS", "PluginIcon", "PluginSettingsErrors", "PluginSettingsField", "PluginSettingsValidationError", "PluginSummary", "PluginViewPlaceholder", "Popover", "PopoverAnchor", "PopoverContent", "PopoverTrigger", "ProxyNode", "ProxySettingsFields", "RadialGauge", "RadialGaugeProps", "RailGroup", "RailItemDef", "RecentActivityItem", "ReleaseInfo", "Repeater", "ResolvedHostDefault", "RobustClipboardProvider", "Role", "SSHAuthDialog", "SSHHost", "SSHHostData", "SSHHostWithStatus", "SSH_AUTH_PROTOCOL", "ScrollArea", "ScrollBar", "SecretField", "SectionCard", "Segmented", "Select", "Select2", "SelectContent", "SelectField", "SelectGroup", "SelectItem", "SelectLabel", "SelectScrollDownButton", "SelectScrollUpButton", "SelectSeparator", "SelectTrigger", "SelectValue", "Separator", "SettingRow", "SettingsFieldRow", "SharePermissionLevel", "ShareTarget", "Sheet", "SheetClose", "SheetContent", "SheetDescription", "SheetFooter", "SheetHeader", "SheetTitle", "SheetTrigger", "ShellConfirmCapture", "Skeleton", "Slider", "SortKey", "Sparkline", "SparklineProps", "SplitLayoutMenu", "SplitOpenTarget", "SplitSummary", "StatRow", "StatusValue", "SurfaceKind", "SurfaceScope", "Switch", "SwitchRow", "SyncBasicAuth", "SyncConflictItem", "SyncEntityInfo", "SyncErrorItem", "SyncProxyHeader", "SyncStatus", "TOTPDialog", "Tab", "TabShellCallbacks", "TabSplitAction", "TabStage", "TabStrip", "TabStripItem", "TabType", "TagInput", "TextAreaField", "TextField", "Textarea", "TextareaProps", "ThemeId", "TlsStatus", "ToolsTab", "Tooltip", "TooltipContent", "TooltipProvider", "TooltipTrigger", "UI_AREA_KEYS", "UI_FONTS", "UiAreaKey", "UiFontId", "UiPreset", "UpdateChannelState", "UptimeInfo", "UsagePair", "UserPreferences", "UserRole", "UserSecondFactorSummary", "VersionBadge", "VersionInfo", "ViewToggle", "WebSocketConnectionTarget", "WidgetTitle", "actionOrder", "activityTarget", "adminCreateUser", "adminCreateUserCredential", "adminCreateUserHost", "adminDeleteUserCredential", "adminDeleteUserHost", "adminExportUserData", "adminGetHostPassword", "adminGetUserCredentials", "adminGetUserHosts", "adminResetUserPassword", "adminUpdateUserCredential", "adminUpdateUserHost", "applyAccentColor", "applyFontSize", "applyPresetSideEffects", "applyUiFont", "assignRoleToUser", "beginSplitDrag", "buildOriginWsUrl", "bulkUpdateSSHHosts", "canConfigureUpdates", "canJoinSplit", "changeAppLanguage", "changePassword", "clearLocalAdaptivePreferences", "cn", "copyToClipboard", "createApiKey", "createCredential", "createFrontendLogger", "createQuickConnectHost", "createRole", "createSSHHost", "defaultConnectAction", "defaultCredentialRowFields", "defaultCredentialSidebarPreferences", "defaultHostRowFields", "defaultHostSidebarPreferences", "defaultValuesEqual", "deleteAccount", "deleteAllHostsInFolder", "deleteApiKey", "deleteCredential", "deleteOpenTab", "deleteRole", "deleteSSHHost", "deleteUser", "desktopUpdates", "docsUrl", "duplicateCredential", "enabledHostProtocols", "endSplitDrag", "filterPaletteItems", "findCoreHostDefault", "findMatchingKeybinding", "getActiveSessions", "getAdaptiveResourceBudget", "getApiKeys", "getAuditLogActions", "getAuditLogs", "getBasePath", "getBranding", "getChannelReleases", "getConnectedRemoteApi", "getCredentialAccess", "getCredentialDetails", "getCredentialSidebarPreferences", "getCredentials", "getDatabaseHealth", "getDatabaseTransferUrl", "getDefaultConnectionTab", "getDeveloperMode", "getDeviceId", "getErrorMessage", "getExternalAutoProvision", "getFolderAccess", "getFolderDefaultsId", "getHostAccess", "getHostAuthOverride", "getHostDefaultsLevel", "getHostEditorSection", "getHostPassword", "getHostSidebarPreferences", "getHostTags", "getLiveHostStatus", "getLogLevel", "getNotificationPrivateEndpoints", "getPanel", "getPasswordLoginAllowed", "getPasswordResetAllowed", "getPermissionsCatalog", "getPluginAdminSettings", "getPluginHostSettings", "getPluginUserSettings", "getPlugins", "getPollingEnvironmentMultiplier", "getPreferredHostAction", "getRecentActivity", "getRegisteredDashboardCard", "getRegistrationAllowed", "getRoles", "getSSHFolders", "getSSHHosts", "getSecondFactorAfterExternalLogin", "getSessionTimeout", "getSessionTimeoutMinutes", "getSessions", "getStatusCheckSettings", "getStatusClasses", "getSyncConflicts", "getSyncErrors", "getTabType", "getTlsStatus", "getUpdateChannel", "getUptime", "getUserInfo", "getUserList", "getUserPreferences", "getUserRoles", "getUserSecondFactors", "getVersionInfo", "globalShortcutHandler", "groupRailItems", "hasVisibleFields", "hostActionsFor", "hostBadgesFor", "hostEditorToolbarItemsFor", "hostMenuItemsFor", "hostProtocolFlags", "hostStatusIndicatorsFor", "hydrateLocalSharedHostAuth", "invalidateHostsAndStatusCaches", "invalidateServerStatusCache", "isCoreRailView", "isElectron", "isFieldShown", "isMacPlatform", "isPersistentTabType", "isPrerelease", "isSessionTabType", "isSplitDragging", "isSplitTab", "isTabJumpHotkey", "linkExternalToPasswordAccount", "linkedServerUrl", "listHostActions", "listHostProtocols", "loadPaletteGroup", "logoutUser", "makeHostSshSubTabs", "makeHostTabs", "makeUserAdmin", "mapCredentials", "markAdaptiveResourceUsed", "markTabSurfaceUsed", "moveRailItem", "moveSplitDrag", "normalizeLanguageCode", "notifySyncChanged", "openInSplit", "paletteEntriesFor", "parseCustomKeybindings", "parseServerTime", "pluginKey", "pluginName", "pluginWsUrl", "preloadTabSurface", "previewHostDefaultsLevel", "promotableIds", "protocolEnabled", "protocolPort", "quickConnectTargets", "railItemLabel", "readFromClipboard", "recordHostActionPreference", "refreshSyncStatus", "registerLayout", "registerTabCloseGuard", "releaseUrlFrom", "rem", "removeAdminStatus", "removeRoleFromUser", "renameCredentialFolder", "renameFolder", "reorderCredentials", "reorderFolders", "reorderSSHHosts", "reportBetaFeedbackUrl", "reportCoreIssueUrl", "requestFromLegacyEvent", "requestManage", "resetHostToDefaults", "resetRecentActivity", "resetUserSecondFactors", "resolveConnectionOrigin", "resolveHostDefaults", "resolveHostTabType", "resolveRemoteHostId", "retrySyncErrors", "revokeAllUserSessions", "revokeCredentialAccess", "revokeFolderAccess", "revokeHostAccess", "revokeSession", "rightDockableIds", "roleLabel", "runAdaptiveBackgroundTask", "runAdaptivePolling", "runHostAction", "runVisibleInterval", "sanitizeCredentialSidebarPreferences", "sanitizeHostSidebarPreferences", "saveCredentialSidebarPreferences", "saveHostDefaultsLevel", "saveHostSidebarPreferences", "saveHostTags", "saveUserPreferences", "setAllPluginChannels", "setDeveloperMode", "setHostAuthOverride", "setNotificationPrivateEndpoints", "setPluginOptions", "setSyncStatus", "setUpdateChannel", "settleSyncConflict", "shareCredential", "shareFolder", "shareHost", "shell", "splitDefaultKey", "sshHostToHost", "syncNow", "tabIcon", "tabJumpHotkeyKeys", "takePendingManageRequest", "unlinkExternalFromPasswordAccount", "unlinkServer", "updateCredential", "updateExternalAutoProvision", "updateFolderMetadata", "updateHostAccess", "updateLogLevel", "updatePasswordLoginAllowed", "updatePasswordResetAllowed", "updatePluginAdminSettings", "updatePluginHostSettings", "updatePluginUserSettings", "updateRegistrationAllowed", "updateRole", "updateSSHHost", "updateSecondFactorAfterExternalLogin", "updateSessionTimeout", "updateStatusCheckSettings", "updateSyncSettings", "uploadTlsCertificate", "usageBarColor", "usageColor", "useActionSlot", "useAdaptivePolling", "useAppTheme", "useAreaPreferences", "useBranding", "useChooseTheme", "useConfirm", "useConnectionGate", "useConnectionLog", "useDesktopUpdate", "useEditorDefaultKeys", "useHostActions", "useHostBadges", "useHostContextMenuItems", "useHostEditorToolbarItems", "useHostStatusIndicators", "useHostEditorSections", "useHostProtocols", "useHostStatus", "useInstalledPlugins", "useIsDefaultsEditor", "useIsMobile", "useKeybindingActions", "useKeybindingDefaults", "useOptionalConnectionLog", "usePageVisibleInterval", "usePaletteEntries", "usePaletteGroups", "usePanels", "usePermissions", "usePluginDocsUrl", "usePluginHostSections", "usePluginStore", "useRailItems", "useRegisteredDashboardCards", "useServerStatus", "useServerStatusMeta", "useShellController", "useSplitTargets", "useSshAuthEditors", "useSshAuthProviders", "useStatusColorScheme", "useSurfaceKind", "useSyncStatus", "useTabHandlesVersion", "useTabs", "useTabsSafe", "useTheme", "useThemeOptions", "useUiPreference", "useUiPreferencesContext", "waitForDefaultsJob", "withProtocolSettings", ]; /** Every name a module exports, resolved the way the compiler sees it. */ function exportedNames(file = SDK_UI, tsconfig = TSCONFIG) { const ts = require("typescript"); const parsed = ts.getParsedCommandLineOfConfigFile( tsconfig, {}, { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, ); const program = ts.createProgram([file], { ...(parsed?.options ?? {}), noEmit: true, }); const checker = program.getTypeChecker(); const source = program.getSourceFile(file); const symbol = source && checker.getSymbolAtLocation(source); if (!symbol) throw new Error(`Cannot read the exports of ${file}`); return checker .getExportsOfModule(symbol) .map((entry) => entry.getName()) .sort(); } const SNAPSHOT = path.join(ROOT, "packages", "plugin-sdk", "ui.api.txt"); /** * One line per export with its type as the compiler prints it. Committed as * packages/plugin-sdk/ui.api.txt, so a changed prop or signature shows up as * a diff instead of reaching plugins unnoticed. */ function exportSignatures(file = SDK_UI, tsconfig = TSCONFIG) { const ts = require("typescript"); const parsed = ts.getParsedCommandLineOfConfigFile( tsconfig, {}, { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, ); const program = ts.createProgram([file], { ...(parsed?.options ?? {}), noEmit: true, }); const checker = program.getTypeChecker(); const source = program.getSourceFile(file); const symbol = source && checker.getSymbolAtLocation(source); const flags = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.WriteArrowStyleSignature; return checker .getExportsOfModule(symbol) .map((entry) => { const target = entry.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(entry) : entry; const isValue = !!(target.flags & ts.SymbolFlags.Value); const declaration = target.declarations?.[0] ?? source; const type = isValue ? checker.getTypeOfSymbolAtLocation(target, declaration) : checker.getDeclaredTypeOfSymbol(target); const text = checker .typeToString(type, undefined, flags) .replace(/import\("[^"]*"\)\./g, "") .replace(/\s+/g, " "); return `${entry.getName()} (${isValue ? "value" : "type"}): ${text}`; }) .sort() .join("\n") .concat("\n"); } /** What the module adds to, and drops from, the allowlist. */ function compare(names, allowed = ALLOWED) { const allowedSet = new Set(allowed); const nameSet = new Set(names); return { added: names.filter((name) => !allowedSet.has(name)), removed: allowed.filter((name) => !nameSet.has(name)), }; } function main() { const fs = require("node:fs"); if (/^export (type )?\*/m.test(fs.readFileSync(SDK_UI, "utf8"))) { console.error( "sdk-ui.ts uses export *; name each export so nothing becomes public API by accident.", ); process.exit(1); } if (process.argv.includes("--update-snapshot")) { fs.writeFileSync(SNAPSHOT, exportSignatures()); console.log(`Wrote ${path.relative(ROOT, SNAPSHOT)}`); return; } const saved = fs.existsSync(SNAPSHOT) ? fs.readFileSync(SNAPSHOT, "utf8") : ""; const current = exportSignatures(); if (current !== saved) { const before = new Set(saved.split("\n")); const after = new Set(current.split("\n")); console.error( "@termix-ssh/plugin-sdk/ui changed shape. If that is deliberate, run: node scripts/check-sdk-ui-exports.cjs --update-snapshot", ); for (const line of saved.split("\n")) { if (line && !after.has(line)) console.error(` - ${line.slice(0, 200)}`); } for (const line of current.split("\n")) { if (line && !before.has(line)) console.error(` + ${line.slice(0, 200)}`); } process.exitCode = 1; } const names = exportedNames(); if (process.argv.includes("--list")) { console.log(names.join("\n")); return; } const { added, removed } = compare(names); if (added.length === 0 && removed.length === 0) return; if (added.length > 0) { console.error( "@termix-ssh/plugin-sdk/ui exports names that are not on the allowlist in scripts/check-sdk-ui-exports.cjs:", ); for (const name of added) console.error(` + ${name}`); } if (removed.length > 0) { console.error( "@termix-ssh/plugin-sdk/ui no longer exports these allowlisted names (a breaking change for plugins):", ); for (const name of removed) console.error(` - ${name}`); } process.exit(1); } if (require.main === module) main(); module.exports = { ALLOWED, compare, exportedNames, exportSignatures };