diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 89109b482ef..bd18d5686cd 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -587,6 +587,26 @@ function cleanStdout(stdout: string): string { return stdout } +/** + * Serializes a value for use as a shell environment variable. Strings pass through + * unchanged; primitives are coerced via `String`; objects, arrays, and other complex + * values are JSON-stringified so that referencing them via `$VAR` yields a useful + * representation instead of `[object Object]`. `null`/`undefined` become an empty + * string to match POSIX env semantics. + */ +function serializeForShellEnv(value: unknown, nullValue = ''): string { + if (value === null || value === undefined) return nullValue + if (typeof value === 'string') return value + if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') { + return String(value) + } + try { + return JSON.stringify(value) ?? '' + } catch { + return String(value) + } +} + async function maybeExportSandboxFileToWorkspace(args: { authUserId: string workflowId?: string @@ -722,6 +742,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { blockNameMapping = {}, blockOutputSchemas = {}, workflowVariables = {}, + contextVariables: preResolvedContextVariables = {}, workflowId, workspaceId, isCustomTool = false, @@ -746,6 +767,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => { // For shell, env vars are injected as OS env vars via shellEnvs. // Replace {{VAR}} placeholders with $VAR so the shell can access them natively. resolvedCode = code.replace(/\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}/g, '$$$1') + // Carry pre-resolved block output variables (e.g. __blockRef_N) so they can be + // injected as shell env vars below. The executor replaces block references in the + // code with these names, so the values must be present at runtime. + contextVariables = { ...preResolvedContextVariables } } else { const codeResolution = resolveCodeVariables( code, @@ -758,7 +783,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => { lang ) resolvedCode = codeResolution.resolvedCode - contextVariables = codeResolution.contextVariables + // Merge pre-resolved block output variables from the executor. These take precedence + // because they were produced by the resolver using full execution-state context + // (including loop/parallel scope) and should not be overwritten. + contextVariables = { ...codeResolution.contextVariables, ...preResolvedContextVariables } } let jsImports = '' @@ -783,10 +811,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => { const shellEnvs: Record = {} for (const [k, v] of Object.entries(envVars)) { - shellEnvs[k] = String(v) + shellEnvs[k] = serializeForShellEnv(v) } for (const [k, v] of Object.entries(contextVariables)) { - shellEnvs[k] = String(v) + shellEnvs[k] = serializeForShellEnv(v, 'null') } logger.info(`[${requestId}] E2B shell execution`, { @@ -893,7 +921,9 @@ export const POST = withRouteHandler(async (req: NextRequest) => { prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n` prologueLineCount++ for (const [k, v] of Object.entries(contextVariables)) { - prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n` + prologue += `globalThis[${JSON.stringify(k)}] = ${formatLiteralForCode(v, 'javascript')};\n` + prologue += `const ${k} = globalThis[${JSON.stringify(k)}];\n` + prologueLineCount++ prologueLineCount++ } diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 9a3c22e8529..73cdcb8d674 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -44,7 +44,10 @@ import { } from '@/executor/utils/iteration-context' import { isJSONString } from '@/executor/utils/json' import { filterOutputForLog } from '@/executor/utils/output-filter' -import type { VariableResolver } from '@/executor/variables/resolver' +import { + FUNCTION_BLOCK_CONTEXT_VARS_KEY, + type VariableResolver, +} from '@/executor/variables/resolver' import type { SerializedBlock } from '@/serializer/types' import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants' @@ -115,7 +118,13 @@ export class BlockExecutor { await validateBlockType(ctx.userId, ctx.workspaceId, blockType, ctx) } - resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block) + if (block.metadata?.id === BlockType.FUNCTION) { + const { resolvedInputs: fnInputs, contextVariables } = + this.resolver.resolveInputsForFunctionBlock(ctx, node.id, block.config.params, block) + resolvedInputs = { ...fnInputs, [FUNCTION_BLOCK_CONTEXT_VARS_KEY]: contextVariables } + } else { + resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block) + } if (blockLog) { blockLog.input = this.sanitizeInputsForLog(resolvedInputs) @@ -428,7 +437,11 @@ export class BlockExecutor { const result: Record = {} for (const [key, value] of Object.entries(inputs)) { - if (SYSTEM_SUBBLOCK_IDS.includes(key) || key === 'triggerMode') { + if ( + SYSTEM_SUBBLOCK_IDS.includes(key) || + key === 'triggerMode' || + key === FUNCTION_BLOCK_CONTEXT_VARS_KEY + ) { continue } diff --git a/apps/sim/executor/handlers/function/function-handler.test.ts b/apps/sim/executor/handlers/function/function-handler.test.ts index 0dcda1ce37d..384540cc7b2 100644 --- a/apps/sim/executor/handlers/function/function-handler.test.ts +++ b/apps/sim/executor/handlers/function/function-handler.test.ts @@ -3,6 +3,7 @@ import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants' import { BlockType } from '@/executor/constants' import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler' import type { ExecutionContext } from '@/executor/types' +import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' @@ -73,10 +74,13 @@ describe('FunctionBlockHandler', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, _context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId, + userId: mockContext.userId, isDeployedContext: mockContext.isDeployedContext, + enforceCredentialAccess: mockContext.enforceCredentialAccess, }, } const expectedOutput: any = { result: 'Success' } @@ -110,10 +114,13 @@ describe('FunctionBlockHandler', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, _context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId, + userId: mockContext.userId, isDeployedContext: mockContext.isDeployedContext, + enforceCredentialAccess: mockContext.enforceCredentialAccess, }, } const expectedOutput: any = { result: 'Success' } @@ -140,10 +147,13 @@ describe('FunctionBlockHandler', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, _context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId, + userId: mockContext.userId, isDeployedContext: mockContext.isDeployedContext, + enforceCredentialAccess: mockContext.enforceCredentialAccess, }, } @@ -168,6 +178,24 @@ describe('FunctionBlockHandler', () => { expect(mockExecuteTool).toHaveBeenCalled() }) + it('should pass runtime context variables to function_execute', async () => { + const contextVariables = { __blockRef_0: { result: 'from-block' } } + + await handler.execute(mockContext, mockBlock, { + code: 'return globalThis["__blockRef_0"]', + [FUNCTION_BLOCK_CONTEXT_VARS_KEY]: contextVariables, + }) + + expect(mockExecuteTool).toHaveBeenCalledWith( + 'function_execute', + expect.objectContaining({ + contextVariables, + }), + false, + mockContext + ) + }) + it('should handle tool error with no specific message', async () => { const inputs = { code: 'some code' } const errorResult = { success: false } diff --git a/apps/sim/executor/handlers/function/function-handler.ts b/apps/sim/executor/handlers/function/function-handler.ts index 68302412bcb..c008a8d07ea 100644 --- a/apps/sim/executor/handlers/function/function-handler.ts +++ b/apps/sim/executor/handlers/function/function-handler.ts @@ -3,6 +3,7 @@ import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages' import { BlockType } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { collectBlockData } from '@/executor/utils/block-data' +import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' @@ -25,6 +26,9 @@ export class FunctionBlockHandler implements BlockHandler { const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx) + const contextVariables = + (inputs[FUNCTION_BLOCK_CONTEXT_VARS_KEY] as Record | undefined) ?? {} + const result = await executeTool( 'function_execute', { @@ -36,6 +40,7 @@ export class FunctionBlockHandler implements BlockHandler { blockData, blockNameMapping, blockOutputSchemas, + contextVariables, _context: { workflowId: ctx.workflowId, workspaceId: ctx.workspaceId, diff --git a/apps/sim/executor/utils/reference-validation.ts b/apps/sim/executor/utils/reference-validation.ts index 60e57b69639..18b2b76d89f 100644 --- a/apps/sim/executor/utils/reference-validation.ts +++ b/apps/sim/executor/utils/reference-validation.ts @@ -143,14 +143,14 @@ export function createCombinedPattern(): RegExp { */ export function replaceValidReferences( template: string, - replacer: (match: string) => string + replacer: (match: string, index: number, template: string) => string ): string { const pattern = createReferencePattern() - return template.replace(pattern, (match) => { + return template.replace(pattern, (match, _content, index) => { if (!isLikelyReferenceSegment(match)) { return match } - return replacer(match) + return replacer(match, index, template) }) } diff --git a/apps/sim/executor/variables/resolver.test.ts b/apps/sim/executor/variables/resolver.test.ts new file mode 100644 index 00000000000..915afa54ae6 --- /dev/null +++ b/apps/sim/executor/variables/resolver.test.ts @@ -0,0 +1,153 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { BlockType } from '@/executor/constants' +import { ExecutionState } from '@/executor/execution/state' +import type { ExecutionContext } from '@/executor/types' +import { VariableResolver } from '@/executor/variables/resolver' +import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' + +function createBlock(id: string, name: string, type: string, params = {}): SerializedBlock { + return { + id, + metadata: { id: type, name }, + position: { x: 0, y: 0 }, + config: { tool: type, params }, + inputs: {}, + outputs: { + result: 'string', + items: 'json', + }, + enabled: true, + } +} + +function createResolver(language = 'javascript') { + const producer = createBlock('producer', 'Producer', BlockType.API) + const functionBlock = createBlock('function', 'Function', BlockType.FUNCTION, { + language, + }) + const workflow: SerializedWorkflow = { + version: '1', + blocks: [producer, functionBlock], + connections: [], + loops: {}, + parallels: {}, + } + const state = new ExecutionState() + state.setBlockOutput('producer', { + result: 'hello world', + items: ['a', 'b'], + }) + const ctx = { + blockStates: state.getBlockStates(), + blockLogs: [], + environmentVariables: {}, + workflowVariables: {}, + decisions: { router: new Map(), condition: new Map() }, + loopExecutions: new Map(), + executedBlocks: new Set(), + activeExecutionPath: new Set(), + completedLoops: new Set(), + metadata: {}, + } as ExecutionContext + + return { + block: functionBlock, + ctx, + resolver: new VariableResolver(workflow, {}, state), + } +} + +describe('VariableResolver function block inputs', () => { + it('returns empty inputs when params are missing', () => { + const { block, ctx, resolver } = createResolver() + + const result = resolver.resolveInputsForFunctionBlock(ctx, 'function', undefined, block) + + expect(result).toEqual({ resolvedInputs: {}, contextVariables: {} }) + }) + + it('resolves JavaScript block references through globalThis context variables', () => { + const { block, ctx, resolver } = createResolver('javascript') + + const result = resolver.resolveInputsForFunctionBlock( + ctx, + 'function', + { code: 'return ' }, + block + ) + + expect(result.resolvedInputs.code).toBe('return globalThis["__blockRef_0"]') + expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' }) + }) + + it('resolves Python block references through globals lookup', () => { + const { block, ctx, resolver } = createResolver('python') + + const result = resolver.resolveInputsForFunctionBlock( + ctx, + 'function', + { code: 'return ' }, + block + ) + + expect(result.resolvedInputs.code).toBe('return globals()["__blockRef_0"]') + expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' }) + }) + + it('uses separate Python context variables for repeated mutable references', () => { + const { block, ctx, resolver } = createResolver('python') + + const result = resolver.resolveInputsForFunctionBlock( + ctx, + 'function', + { code: 'a = \nb = \nreturn b' }, + block + ) + + expect(result.resolvedInputs.code).toBe( + 'a = globals()["__blockRef_0"]\nb = globals()["__blockRef_1"]\nreturn b' + ) + expect(result.contextVariables).toEqual({ + __blockRef_0: ['a', 'b'], + __blockRef_1: ['a', 'b'], + }) + }) + + it('uses shell-safe expansions for block references', () => { + const { block, ctx, resolver } = createResolver('shell') + + const result = resolver.resolveInputsForFunctionBlock( + ctx, + 'function', + { code: 'echo suffix && echo ""' }, + block + ) + + expect(result.resolvedInputs.code).toBe( + `echo "\${__blockRef_0}"suffix && echo "\${__blockRef_1}"` + ) + expect(result.contextVariables).toEqual({ + __blockRef_0: 'hello world', + __blockRef_1: 'hello world', + }) + }) + + it('ignores shell comment quotes when formatting later block references', () => { + const { block, ctx, resolver } = createResolver('shell') + + const result = resolver.resolveInputsForFunctionBlock( + ctx, + 'function', + { code: "# don't confuse quote tracking\necho " }, + block + ) + + expect(result.resolvedInputs.code).toBe( + `# don't confuse quote tracking\necho "\${__blockRef_0}"` + ) + expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' }) + }) +}) diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts index 412dd549d39..2cc9fd89e5b 100644 --- a/apps/sim/executor/variables/resolver.ts +++ b/apps/sim/executor/variables/resolver.ts @@ -16,8 +16,13 @@ import { import { WorkflowResolver } from '@/executor/variables/resolvers/workflow' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' +/** Key used to carry pre-resolved context variables through the inputs map. */ +export const FUNCTION_BLOCK_CONTEXT_VARS_KEY = '_runtimeContextVars' + const logger = createLogger('VariableResolver') +type ShellQuoteContext = 'single' | 'double' | null + export class VariableResolver { private resolvers: Resolver[] private blockResolver: BlockResolver @@ -37,6 +42,67 @@ export class VariableResolver { ] } + /** + * Resolves inputs for function blocks. Block output references in the `code` field + * are stored as named context variables instead of being embedded as JavaScript + * literals, preventing large values from bloating the code string. + * + * Returns the resolved inputs and a `contextVariables` map. Callers should inject + * contextVariables into the function execution request body so the isolated VM can + * access them as global variables. + */ + resolveInputsForFunctionBlock( + ctx: ExecutionContext, + currentNodeId: string, + params: Record | null | undefined, + block: SerializedBlock + ): { resolvedInputs: Record; contextVariables: Record } { + const contextVariables: Record = {} + const resolved: Record = {} + + if (!params) { + return { resolvedInputs: resolved, contextVariables } + } + + for (const [key, value] of Object.entries(params)) { + if (key === 'code') { + if (typeof value === 'string') { + resolved[key] = this.resolveCodeWithContextVars( + ctx, + currentNodeId, + value, + undefined, + block, + contextVariables + ) + } else if (Array.isArray(value)) { + resolved[key] = value.map((item: any) => { + if (item && typeof item === 'object' && typeof item.content === 'string') { + return { + ...item, + content: this.resolveCodeWithContextVars( + ctx, + currentNodeId, + item.content, + undefined, + block, + contextVariables + ), + } + } + return item + }) + } else { + resolved[key] = this.resolveValue(ctx, currentNodeId, value, undefined, block) + } + } else { + resolved[key] = this.resolveValue(ctx, currentNodeId, value, undefined, block) + } + } + + return { resolvedInputs: resolved, contextVariables } + } + resolveInputs( ctx: ExecutionContext, currentNodeId: string, @@ -150,6 +216,170 @@ export class VariableResolver { } return value } + /** + * Resolves a code template for a function block. Block output references are stored + * in `contextVarAccumulator` as named variables (e.g. `__blockRef_0`) and replaced + * with those variable names in the returned code string. Non-block references (loop + * items, workflow variables, env vars) are still inlined as literals so they remain + * available without any extra passing mechanism. + */ + private resolveCodeWithContextVars( + ctx: ExecutionContext, + currentNodeId: string, + template: string, + loopScope: LoopScope | undefined, + block: SerializedBlock, + contextVarAccumulator: Record + ): string { + const resolutionContext: ResolutionContext = { + executionContext: ctx, + executionState: this.state, + currentNodeId, + loopScope, + } + + const language = (block.config?.params as Record | undefined)?.language as + | string + | undefined + + let replacementError: Error | null = null + + let result = replaceValidReferences(template, (match, index) => { + if (replacementError) return match + + try { + if (this.blockResolver.canResolve(match)) { + const resolved = this.resolveReference(match, resolutionContext) + if (resolved === undefined) return match + + const effectiveValue = resolved === RESOLVED_EMPTY ? null : resolved + + // Block output: store in contextVarAccumulator and replace the reference + // with language-specific runtime access to that stored value. + const varName = `__blockRef_${Object.keys(contextVarAccumulator).length}` + contextVarAccumulator[varName] = effectiveValue + const replacement = this.formatContextVariableReference( + varName, + language, + template, + index, + effectiveValue + ) + return replacement + } + + const resolved = this.resolveReference(match, resolutionContext) + if (resolved === undefined) return match + + const effectiveValue = resolved === RESOLVED_EMPTY ? null : resolved + + // Non-block reference (loop, parallel, workflow, env): embed as literal + return this.blockResolver.formatValueForBlock(effectiveValue, BlockType.FUNCTION, language) + } catch (error) { + replacementError = error instanceof Error ? error : new Error(String(error)) + return match + } + }) + + if (replacementError !== null) { + throw replacementError + } + + result = result.replace(createEnvVarPattern(), (match) => { + const resolved = this.resolveReference(match, resolutionContext) + return typeof resolved === 'string' ? resolved : match + }) + + return result + } + + private formatContextVariableReference( + varName: string, + language: string | undefined, + template: string, + matchIndex: number, + value: unknown + ): string { + if (language === 'python') { + return `globals()[${JSON.stringify(varName)}]` + } + + if (language === 'shell') { + return this.formatShellContextVariableReference(varName, template, matchIndex, value) + } + + return `globalThis[${JSON.stringify(varName)}]` + } + + private formatShellContextVariableReference( + varName: string, + template: string, + matchIndex: number, + value: unknown + ): string { + const expansion = `\${${varName}}` + const quoteContext = this.getShellQuoteContext(template, matchIndex) + if (quoteContext === 'double') { + return expansion + } + + const shouldQuote = + quoteContext === 'single' || + typeof value === 'string' || + (typeof value === 'object' && value !== null) || + Array.isArray(value) + + if (!shouldQuote) { + return expansion + } + + const quotedExpansion = `"${expansion}"` + if (quoteContext === 'single') { + return `'${quotedExpansion}'` + } + + return quotedExpansion + } + + private getShellQuoteContext(template: string, index: number): ShellQuoteContext { + let quoteContext: ShellQuoteContext = null + + for (let i = 0; i < index; i++) { + const char = template[i] + + if (quoteContext === null && this.isShellCommentStart(template, i)) { + const nextNewline = template.indexOf('\n', i + 1) + if (nextNewline === -1 || nextNewline >= index) { + break + } + i = nextNewline + continue + } + + if (char === '\\' && quoteContext !== 'single') { + i++ + continue + } + + if (char === "'" && quoteContext !== 'double') { + quoteContext = quoteContext === 'single' ? null : 'single' + } else if (char === '"' && quoteContext !== 'single') { + quoteContext = quoteContext === 'double' ? null : 'double' + } + } + + return quoteContext + } + + private isShellCommentStart(template: string, index: number): boolean { + if (template[index] !== '#') { + return false + } + + const previous = template[index - 1] + return previous === undefined || /\s|[;&|()<>]/.test(previous) + } + private resolveTemplate( ctx: ExecutionContext, currentNodeId: string, diff --git a/apps/sim/lib/api/contracts/hotspots.ts b/apps/sim/lib/api/contracts/hotspots.ts index 08baa37b499..c4e7a277303 100644 --- a/apps/sim/lib/api/contracts/hotspots.ts +++ b/apps/sim/lib/api/contracts/hotspots.ts @@ -99,6 +99,7 @@ export const functionExecuteContract = defineRouteContract({ blockNameMapping: z.record(z.string(), z.string()).optional().default({}), blockOutputSchemas: z.record(z.string(), unknownRecordSchema).optional().default({}), workflowVariables: unknownRecordSchema.optional().default({}), + contextVariables: unknownRecordSchema.optional().default({}), workflowId: z.string().optional(), workspaceId: z.string().optional(), userId: z.string().optional(), diff --git a/apps/sim/tools/function/execute.test.ts b/apps/sim/tools/function/execute.test.ts index e1a966fe94d..73eb21de9e6 100644 --- a/apps/sim/tools/function/execute.test.ts +++ b/apps/sim/tools/function/execute.test.ts @@ -56,10 +56,17 @@ describe('Function Execute Tool', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, isCustomTool: false, language: 'javascript', + outputFormat: undefined, + outputMimeType: undefined, + outputPath: undefined, + outputSandboxPath: undefined, + outputTable: undefined, timeout: 5000, workflowId: undefined, + workspaceId: undefined, userId: undefined, }) }) @@ -85,9 +92,16 @@ describe('Function Execute Tool', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, isCustomTool: false, language: 'javascript', + outputFormat: undefined, + outputMimeType: undefined, + outputPath: undefined, + outputSandboxPath: undefined, + outputTable: undefined, workflowId: undefined, + workspaceId: undefined, userId: undefined, }) }) @@ -105,9 +119,16 @@ describe('Function Execute Tool', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, isCustomTool: false, language: 'javascript', + outputFormat: undefined, + outputMimeType: undefined, + outputPath: undefined, + outputSandboxPath: undefined, + outputTable: undefined, workflowId: undefined, + workspaceId: undefined, userId: undefined, }) }) diff --git a/apps/sim/tools/function/execute.ts b/apps/sim/tools/function/execute.ts index 844b1c6d515..59873843cbb 100644 --- a/apps/sim/tools/function/execute.ts +++ b/apps/sim/tools/function/execute.ts @@ -128,6 +128,7 @@ export const functionExecuteTool: ToolConfig blockNameMapping?: Record blockOutputSchemas?: Record> + /** Pre-resolved block output variables from the executor, injected as VM globals. */ + contextVariables?: Record _context?: { workflowId?: string userId?: string diff --git a/bun.lock b/bun.lock index cd73fd610fd..979cb1eb048 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio",