From c7b77bd303ef88f7165a8d782c575458d02febb2 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 8 Jul 2025 20:42:40 -0700 Subject: [PATCH] Yaml language basics --- .../export-controls/export-controls.tsx | 165 +++++++++++ .../components/control-bar/control-bar.tsx | 2 + apps/sim/package.json | 2 + apps/sim/stores/workflows/yaml/README.md | 143 ++++++++++ apps/sim/stores/workflows/yaml/store.ts | 268 ++++++++++++++++++ bun.lock | 12 +- 6 files changed, 589 insertions(+), 3 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/export-controls/export-controls.tsx create mode 100644 apps/sim/stores/workflows/yaml/README.md create mode 100644 apps/sim/stores/workflows/yaml/store.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/export-controls/export-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/export-controls/export-controls.tsx new file mode 100644 index 000000000..e116cb4a1 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/export-controls/export-controls.tsx @@ -0,0 +1,165 @@ +'use client' + +import { useState } from 'react' +import { Download, FileText } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { createLogger } from '@/lib/logs/console-logger' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store' + +const logger = createLogger('ExportControls') + +interface ExportControlsProps { + disabled?: boolean +} + +export function ExportControls({ disabled = false }: ExportControlsProps) { + const [isExporting, setIsExporting] = useState(false) + const workflowState = useWorkflowStore() + const { workflows, activeWorkflowId } = useWorkflowRegistry() + const getYaml = useWorkflowYamlStore(state => state.getYaml) + + const currentWorkflow = activeWorkflowId ? workflows[activeWorkflowId] : null + + const downloadFile = (content: string, filename: string, mimeType: string) => { + try { + const blob = new Blob([content], { type: mimeType }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } catch (error) { + logger.error('Failed to download file:', error) + } + } + + const handleExportJson = async () => { + if (!currentWorkflow || !activeWorkflowId) { + logger.warn('No active workflow to export') + return + } + + setIsExporting(true) + try { + const exportData = { + workflow: { + id: activeWorkflowId, + name: currentWorkflow.name, + description: currentWorkflow.description, + color: currentWorkflow.color, + }, + state: { + blocks: workflowState.blocks, + edges: workflowState.edges, + loops: workflowState.loops, + parallels: workflowState.parallels, + }, + exportedAt: new Date().toISOString(), + version: '1.0' + } + + const jsonContent = JSON.stringify(exportData, null, 2) + const filename = `${currentWorkflow.name.replace(/[^a-z0-9]/gi, '_')}_workflow.json` + + downloadFile(jsonContent, filename, 'application/json') + logger.info('Workflow exported as JSON') + } catch (error) { + logger.error('Failed to export workflow as JSON:', error) + } finally { + setIsExporting(false) + } + } + + const handleExportYaml = async () => { + if (!currentWorkflow || !activeWorkflowId) { + logger.warn('No active workflow to export') + return + } + + setIsExporting(true) + try { + const yamlContent = getYaml() + const filename = `${currentWorkflow.name.replace(/[^a-z0-9]/gi, '_')}_workflow.yaml` + + downloadFile(yamlContent, filename, 'text/yaml') + logger.info('Workflow exported as YAML') + } catch (error) { + logger.error('Failed to export workflow as YAML:', error) + } finally { + setIsExporting(false) + } + } + + return ( + + + + + + + + + {disabled + ? 'Export not available' + : !currentWorkflow + ? 'No workflow to export' + : 'Export Workflow' + } + + + + + + +
+ Export as JSON + + Full workflow data + +
+
+ + + + + +
+ Export as YAML + + Condensed workflow language + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx index 855f41d30..a149be00f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx @@ -56,6 +56,7 @@ import { } from '../../../hooks/use-keyboard-shortcuts' import { useWorkflowExecution } from '../../hooks/use-workflow-execution' import { DeploymentControls } from './components/deployment-controls/deployment-controls' +import { ExportControls } from './components/export-controls/export-controls' import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item' import { MarketplaceModal } from './components/marketplace-modal/marketplace-modal' import { NotificationDropdownItem } from './components/notification-dropdown-item/notification-dropdown-item' @@ -1287,6 +1288,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { {renderDuplicateButton()} {renderAutoLayoutButton()} {renderDebugModeToggle()} + {/* {renderPublishButton()} */} {renderDeployButton()} {renderRunButton()} diff --git a/apps/sim/package.json b/apps/sim/package.json index 7d112d7ac..7c00c8e4a 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -65,6 +65,7 @@ "@radix-ui/react-tooltip": "^1.1.6", "@react-email/components": "^0.0.34", "@sentry/nextjs": "^9.15.0", + "@types/js-yaml": "4.0.9", "@types/three": "0.177.0", "@vercel/og": "^0.6.5", "@vercel/speed-insights": "^1.2.0", @@ -86,6 +87,7 @@ "input-otp": "^1.4.2", "ioredis": "^5.6.0", "jose": "6.0.11", + "js-yaml": "4.1.0", "jwt-decode": "^4.0.0", "lenis": "^1.2.3", "lucide-react": "^0.479.0", diff --git a/apps/sim/stores/workflows/yaml/README.md b/apps/sim/stores/workflows/yaml/README.md new file mode 100644 index 000000000..b7c111976 --- /dev/null +++ b/apps/sim/stores/workflows/yaml/README.md @@ -0,0 +1,143 @@ +# Workflow YAML Store + +This store dynamically generates a condensed YAML representation of workflows from the JSON workflow state. It extracts input values, connections, and block relationships to create a clean, readable format. + +## Features + +- **Dynamic Input Extraction**: Automatically reads input values from block configurations and subblock stores +- **Connection Mapping**: Determines preceding and following blocks from workflow edges +- **Type-Aware Processing**: Handles different input types (text, numbers, booleans, objects) appropriately +- **Auto-Refresh**: Automatically updates when workflow state or input values change +- **Clean Format**: Generates well-formatted YAML with proper indentation + +## YAML Structure + +```yaml +version: "1.0" +blocks: + block-id-1: + type: "starter" + name: "Start" + inputs: + startWorkflow: "manual" + following: + - "block-id-2" + + block-id-2: + type: "agent" + name: "AI Agent" + inputs: + systemPrompt: "You are a helpful assistant" + userPrompt: "Process the input data" + model: "gpt-4" + temperature: 0.7 + preceding: + - "block-id-1" + following: + - "block-id-3" +``` + +## Usage + +### Basic Usage + +```typescript +import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store' + +function WorkflowYamlViewer() { + const yaml = useWorkflowYamlStore(state => state.getYaml()) + + return ( +
+      {yaml}
+    
+ ) +} +``` + +### Manual Refresh + +```typescript +import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store' + +function WorkflowControls() { + const refreshYaml = useWorkflowYamlStore(state => state.refreshYaml) + + return ( + + ) +} +``` + +### Advanced Usage + +```typescript +import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store' + +function WorkflowExporter() { + const { yaml, lastGenerated, generateYaml } = useWorkflowYamlStore() + + const exportToFile = () => { + const blob = new Blob([yaml], { type: 'text/yaml' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'workflow.yaml' + a.click() + URL.revokeObjectURL(url) + } + + return ( +
+

Last generated: {lastGenerated ? new Date(lastGenerated).toLocaleString() : 'Never'}

+ + +
+ ) +} +``` + +## Input Types Handled + +The store intelligently processes different subblock input types: + +- **Text Inputs** (`short-input`, `long-input`): Trimmed strings +- **Dropdowns/Combobox** (`dropdown`, `combobox`): Selected values +- **Tables** (`table`): Arrays of objects (only if non-empty) +- **Code Blocks** (`code`): Preserves formatting for strings and objects +- **Switches** (`switch`): Boolean values +- **Sliders** (`slider`): Numeric values +- **Checkbox Lists** (`checkbox-list`): Arrays of selected items + +## Auto-Refresh Behavior + +The store automatically refreshes in these scenarios: + +1. **Workflow Structure Changes**: When blocks are added, removed, or connections change +2. **Input Value Changes**: When any subblock input values are modified +3. **Debounced Updates**: Changes are debounced to prevent excessive regeneration + +## Performance + +- **Lazy Generation**: YAML is only generated when requested +- **Caching**: Results are cached and only regenerated when data changes +- **Debouncing**: Rapid changes are debounced to improve performance +- **Selective Updates**: Only regenerates when meaningful changes occur + +## Error Handling + +If YAML generation fails, the store returns an error message in YAML comment format: + +```yaml +# Error generating YAML: [error message] +``` + +## Dependencies + +- `js-yaml`: For YAML serialization +- `zustand`: For state management +- `@/blocks`: For block configuration access +- `@/stores/workflows/workflow/store`: For workflow state +- `@/stores/workflows/subblock/store`: For input values \ No newline at end of file diff --git a/apps/sim/stores/workflows/yaml/store.ts b/apps/sim/stores/workflows/yaml/store.ts new file mode 100644 index 000000000..37f17fe89 --- /dev/null +++ b/apps/sim/stores/workflows/yaml/store.ts @@ -0,0 +1,268 @@ +import { create } from 'zustand' +import { devtools } from 'zustand/middleware' +import { dump as yamlDump } from 'js-yaml' +import { getBlock } from '@/blocks' +import { createLogger } from '@/lib/logs/console-logger' +import { useWorkflowStore } from '../workflow/store' +import { useSubBlockStore } from '../subblock/store' +import type { BlockState, WorkflowState } from '../workflow/types' +import type { SubBlockConfig } from '@/blocks/types' + +const logger = createLogger('WorkflowYamlStore') + +interface YamlBlock { + type: string + name: string + inputs?: Record + preceding?: string[] + following?: string[] +} + +interface YamlWorkflow { + version: string + blocks: Record +} + +interface WorkflowYamlState { + yaml: string + lastGenerated?: number +} + +interface WorkflowYamlActions { + generateYaml: () => void + getYaml: () => string + refreshYaml: () => void +} + +type WorkflowYamlStore = WorkflowYamlState & WorkflowYamlActions + +/** + * Extract input values from a block's subBlocks based on its configuration + */ +function extractBlockInputs(blockState: BlockState, blockId: string): Record { + const blockConfig = getBlock(blockState.type) + const subBlockStore = useSubBlockStore.getState() + const inputs: Record = {} + + if (!blockConfig) { + // For custom blocks like loops/parallels, extract available subBlock values + Object.entries(blockState.subBlocks || {}).forEach(([subBlockId, subBlockState]) => { + const value = subBlockStore.getValue(blockId, subBlockId) ?? subBlockState.value + if (value !== undefined && value !== null && value !== '') { + inputs[subBlockId] = value + } + }) + return inputs + } + + // Process each subBlock configuration + blockConfig.subBlocks.forEach((subBlockConfig: SubBlockConfig) => { + const subBlockId = subBlockConfig.id + + // Skip hidden or conditional fields that aren't active + if (subBlockConfig.hidden) return + + // Get value from subblock store or fallback to block state + const value = subBlockStore.getValue(blockId, subBlockId) ?? + blockState.subBlocks[subBlockId]?.value + + // Include value if it exists and isn't empty + if (value !== undefined && value !== null && value !== '') { + // Handle different input types appropriately + switch (subBlockConfig.type) { + case 'table': + // Tables are arrays of objects + if (Array.isArray(value) && value.length > 0) { + inputs[subBlockId] = value + } + break + + case 'checkbox-list': + // Checkbox lists return arrays + if (Array.isArray(value) && value.length > 0) { + inputs[subBlockId] = value + } + break + + case 'code': + // Code blocks should preserve formatting + if (typeof value === 'string' && value.trim()) { + inputs[subBlockId] = value + } else if (typeof value === 'object') { + inputs[subBlockId] = value + } + break + + case 'switch': + // Boolean values + inputs[subBlockId] = Boolean(value) + break + + case 'slider': + // Numeric values + if (typeof value === 'number' || (typeof value === 'string' && !isNaN(Number(value)))) { + inputs[subBlockId] = Number(value) + } + break + + default: + // Text inputs, dropdowns, etc. + if (typeof value === 'string' && value.trim()) { + inputs[subBlockId] = value.trim() + } else if (typeof value === 'object' || typeof value === 'number' || typeof value === 'boolean') { + inputs[subBlockId] = value + } + break + } + } + }) + + return inputs +} + +/** + * Find preceding blocks for a given block ID + */ +function findPrecedingBlocks(blockId: string, edges: any[]): string[] { + return edges + .filter(edge => edge.target === blockId) + .map(edge => edge.source) + .filter((source, index, arr) => arr.indexOf(source) === index) // Remove duplicates +} + +/** + * Find following blocks for a given block ID + */ +function findFollowingBlocks(blockId: string, edges: any[]): string[] { + return edges + .filter(edge => edge.source === blockId) + .map(edge => edge.target) + .filter((target, index, arr) => arr.indexOf(target) === index) // Remove duplicates +} + +/** + * Generate YAML representation of the workflow + */ +function generateWorkflowYaml(workflowState: WorkflowState): string { + try { + const yamlWorkflow: YamlWorkflow = { + version: '1.0', + blocks: {} + } + + // Process each block + Object.entries(workflowState.blocks).forEach(([blockId, blockState]) => { + const inputs = extractBlockInputs(blockState, blockId) + const preceding = findPrecedingBlocks(blockId, workflowState.edges) + const following = findFollowingBlocks(blockId, workflowState.edges) + + const yamlBlock: YamlBlock = { + type: blockState.type, + name: blockState.name + } + + // Only include inputs if they exist + if (Object.keys(inputs).length > 0) { + yamlBlock.inputs = inputs + } + + // Only include connections if they exist + if (preceding.length > 0) { + yamlBlock.preceding = preceding + } + + if (following.length > 0) { + yamlBlock.following = following + } + + yamlWorkflow.blocks[blockId] = yamlBlock + }) + + // Convert to YAML with clean formatting + return yamlDump(yamlWorkflow, { + indent: 2, + lineWidth: -1, // Disable line wrapping + noRefs: true, + sortKeys: false + }) + } catch (error) { + logger.error('Failed to generate workflow YAML:', error) + return `# Error generating YAML: ${error instanceof Error ? error.message : 'Unknown error'}` + } +} + +export const useWorkflowYamlStore = create()( + devtools( + (set, get) => ({ + yaml: '', + lastGenerated: undefined, + + generateYaml: () => { + const workflowState = useWorkflowStore.getState() + const yaml = generateWorkflowYaml(workflowState) + + set({ + yaml, + lastGenerated: Date.now() + }) + }, + + getYaml: () => { + const currentTime = Date.now() + const { yaml, lastGenerated } = get() + + // Auto-refresh if data is stale (older than 1 second) or never generated + if (!lastGenerated || currentTime - lastGenerated > 1000) { + get().generateYaml() + return get().yaml + } + + return yaml + }, + + refreshYaml: () => { + get().generateYaml() + } + }), + { + name: 'workflow-yaml-store' + } + ) +) + +// Auto-refresh YAML when workflow state changes +let lastWorkflowState: { blockCount: number; edgeCount: number } | null = null + +useWorkflowStore.subscribe((state) => { + const currentState = { + blockCount: Object.keys(state.blocks).length, + edgeCount: state.edges.length + } + + // Only refresh if the structure has changed + if (!lastWorkflowState || + lastWorkflowState.blockCount !== currentState.blockCount || + lastWorkflowState.edgeCount !== currentState.edgeCount) { + + lastWorkflowState = currentState + + // Debounce the refresh to avoid excessive updates + const refreshYaml = useWorkflowYamlStore.getState().refreshYaml + setTimeout(refreshYaml, 100) + } +}) + +// Subscribe to subblock store changes +let lastSubBlockChangeTime = 0 + +useSubBlockStore.subscribe((state) => { + const currentTime = Date.now() + + // Debounce rapid changes + if (currentTime - lastSubBlockChangeTime > 100) { + lastSubBlockChangeTime = currentTime + + const refreshYaml = useWorkflowYamlStore.getState().refreshYaml + setTimeout(refreshYaml, 100) + } +}) \ No newline at end of file diff --git a/bun.lock b/bun.lock index 01d0336af..8e086568b 100644 --- a/bun.lock +++ b/bun.lock @@ -95,6 +95,7 @@ "@radix-ui/react-tooltip": "^1.1.6", "@react-email/components": "^0.0.34", "@sentry/nextjs": "^9.15.0", + "@types/js-yaml": "4.0.9", "@types/three": "0.177.0", "@vercel/og": "^0.6.5", "@vercel/speed-insights": "^1.2.0", @@ -116,6 +117,7 @@ "input-otp": "^1.4.2", "ioredis": "^5.6.0", "jose": "6.0.11", + "js-yaml": "4.1.0", "jwt-decode": "^4.0.0", "lenis": "^1.2.3", "lucide-react": "^0.479.0", @@ -1324,6 +1326,8 @@ "@types/jest": ["@types/jest@26.0.24", "", { "dependencies": { "jest-diff": "^26.0.0", "pretty-format": "^26.0.0" } }, "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w=="], + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + "@types/jsdom": ["@types/jsdom@21.1.7", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -1482,7 +1486,7 @@ "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], @@ -3472,8 +3476,6 @@ "jest-diff/pretty-format": ["pretty-format@26.6.2", "", { "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^17.0.1" } }, "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg=="], - "js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "jsondiffpatch/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "linebreak/base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], @@ -3498,6 +3500,8 @@ "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "mammoth/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -3844,6 +3848,8 @@ "openai/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "openapi/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "openapi/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],