chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,610 @@
|
||||
import { db } from '@sim/db'
|
||||
import { chat, workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { and, eq, inArray, isNull } from 'drizzle-orm'
|
||||
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types'
|
||||
import {
|
||||
performCreateWorkflowMcpServer,
|
||||
performDeleteWorkflowMcpServer,
|
||||
performUpdateWorkflowMcpServer,
|
||||
} from '@/lib/mcp/orchestration'
|
||||
import { generateWorkflowDiffSummary } from '@/lib/workflows/comparison'
|
||||
import { performActivateVersion, performRevertToVersion } from '@/lib/workflows/orchestration'
|
||||
import {
|
||||
listWorkflowVersions,
|
||||
updateDeploymentVersionMetadata,
|
||||
} from '@/lib/workflows/persistence/utils'
|
||||
import { checkNeedsRedeployment } from '@/app/api/workflows/utils'
|
||||
import { ensureWorkflowAccess, ensureWorkspaceAccess } from '../access'
|
||||
import type {
|
||||
CheckDeploymentStatusParams,
|
||||
CreateWorkspaceMcpServerParams,
|
||||
DeleteWorkspaceMcpServerParams,
|
||||
DiffWorkflowsParams,
|
||||
GetDeploymentLogParams,
|
||||
ListWorkspaceMcpServersParams,
|
||||
LoadDeploymentParams,
|
||||
PromoteToLiveParams,
|
||||
UpdateDeploymentVersionParams,
|
||||
UpdateWorkspaceMcpServerParams,
|
||||
} from '../param-types'
|
||||
import { resolveWorkflowStateRef } from './state-refs'
|
||||
|
||||
export async function executeCheckDeploymentStatus(
|
||||
params: CheckDeploymentStatusParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId || context.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId)
|
||||
const workspaceId = workflowRecord.workspaceId
|
||||
|
||||
const [apiDeploy, chatDeploy] = await Promise.all([
|
||||
db
|
||||
.select({ isDeployed: workflow.isDeployed, deployedAt: workflow.deployedAt })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1),
|
||||
db
|
||||
.select({
|
||||
id: chat.id,
|
||||
identifier: chat.identifier,
|
||||
title: chat.title,
|
||||
description: chat.description,
|
||||
authType: chat.authType,
|
||||
allowedEmails: chat.allowedEmails,
|
||||
outputConfigs: chat.outputConfigs,
|
||||
password: chat.password,
|
||||
customizations: chat.customizations,
|
||||
})
|
||||
.from(chat)
|
||||
.where(and(eq(chat.workflowId, workflowId), isNull(chat.archivedAt)))
|
||||
.limit(1),
|
||||
])
|
||||
|
||||
const isApiDeployed = apiDeploy[0]?.isDeployed || false
|
||||
const needsRedeployment = isApiDeployed ? await checkNeedsRedeployment(workflowId) : false
|
||||
const apiDetails = {
|
||||
isDeployed: isApiDeployed,
|
||||
deployedAt: apiDeploy[0]?.deployedAt || null,
|
||||
endpoint: isApiDeployed ? `/api/workflows/${workflowId}/execute` : null,
|
||||
apiKey: workflowRecord.workspaceId ? 'Workspace API keys' : 'Personal API keys',
|
||||
needsRedeployment,
|
||||
}
|
||||
|
||||
const isChatDeployed = !!chatDeploy[0]
|
||||
const chatCustomizations =
|
||||
(chatDeploy[0]?.customizations as
|
||||
| { welcomeMessage?: string; primaryColor?: string }
|
||||
| undefined) || {}
|
||||
const chatDetails = {
|
||||
isDeployed: isChatDeployed,
|
||||
chatId: chatDeploy[0]?.id || null,
|
||||
identifier: chatDeploy[0]?.identifier || null,
|
||||
chatUrl: isChatDeployed ? `/chat/${chatDeploy[0]?.identifier}` : null,
|
||||
title: chatDeploy[0]?.title || null,
|
||||
description: chatDeploy[0]?.description || null,
|
||||
authType: chatDeploy[0]?.authType || null,
|
||||
allowedEmails: chatDeploy[0]?.allowedEmails || null,
|
||||
outputConfigs: chatDeploy[0]?.outputConfigs || null,
|
||||
welcomeMessage: chatCustomizations.welcomeMessage || null,
|
||||
primaryColor: chatCustomizations.primaryColor || null,
|
||||
hasPassword: Boolean(chatDeploy[0]?.password),
|
||||
}
|
||||
|
||||
const mcpDetails: {
|
||||
isDeployed: boolean
|
||||
servers: Array<{
|
||||
serverId: string
|
||||
serverName: string
|
||||
toolName: string
|
||||
toolDescription: string | null
|
||||
parameterSchema: unknown
|
||||
toolId: string
|
||||
}>
|
||||
} = { isDeployed: false, servers: [] }
|
||||
if (workspaceId) {
|
||||
const servers = await db
|
||||
.select({
|
||||
serverId: workflowMcpServer.id,
|
||||
serverName: workflowMcpServer.name,
|
||||
toolName: workflowMcpTool.toolName,
|
||||
toolDescription: workflowMcpTool.toolDescription,
|
||||
parameterSchema: workflowMcpTool.parameterSchema,
|
||||
toolId: workflowMcpTool.id,
|
||||
})
|
||||
.from(workflowMcpTool)
|
||||
.innerJoin(workflowMcpServer, eq(workflowMcpTool.serverId, workflowMcpServer.id))
|
||||
.where(eq(workflowMcpTool.workflowId, workflowId))
|
||||
|
||||
if (servers.length > 0) {
|
||||
mcpDetails.isDeployed = true
|
||||
mcpDetails.servers = servers
|
||||
}
|
||||
}
|
||||
|
||||
const isDeployed = apiDetails.isDeployed || chatDetails.isDeployed || mcpDetails.isDeployed
|
||||
return {
|
||||
success: true,
|
||||
output: { isDeployed, api: apiDetails, chat: chatDetails, mcp: mcpDetails },
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeListWorkspaceMcpServers(
|
||||
params: ListWorkspaceMcpServersParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
let workspaceId = params.workspaceId || context.workspaceId
|
||||
const workflowId = context.workflowId
|
||||
|
||||
if (!workspaceId && workflowId) {
|
||||
const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId)
|
||||
workspaceId = workflowRecord.workspaceId ?? undefined
|
||||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
return { success: false, error: 'workspaceId is required' }
|
||||
}
|
||||
await ensureWorkspaceAccess(workspaceId, context.userId, 'read')
|
||||
|
||||
const servers = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
name: workflowMcpServer.name,
|
||||
description: workflowMcpServer.description,
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.workspaceId, workspaceId), isNull(workflowMcpServer.deletedAt))
|
||||
)
|
||||
|
||||
const serverIds = servers.map((server) => server.id)
|
||||
const tools =
|
||||
serverIds.length > 0
|
||||
? await db
|
||||
.select({
|
||||
serverId: workflowMcpTool.serverId,
|
||||
toolName: workflowMcpTool.toolName,
|
||||
})
|
||||
.from(workflowMcpTool)
|
||||
.where(
|
||||
and(inArray(workflowMcpTool.serverId, serverIds), isNull(workflowMcpTool.archivedAt))
|
||||
)
|
||||
: []
|
||||
|
||||
const toolNamesByServer: Record<string, string[]> = {}
|
||||
for (const tool of tools) {
|
||||
if (!toolNamesByServer[tool.serverId]) {
|
||||
toolNamesByServer[tool.serverId] = []
|
||||
}
|
||||
toolNamesByServer[tool.serverId].push(tool.toolName)
|
||||
}
|
||||
|
||||
const serversWithToolNames = servers.map((server) => ({
|
||||
...server,
|
||||
toolCount: toolNamesByServer[server.id]?.length || 0,
|
||||
toolNames: toolNamesByServer[server.id] || [],
|
||||
}))
|
||||
|
||||
return { success: true, output: { servers: serversWithToolNames, count: servers.length } }
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeCreateWorkspaceMcpServer(
|
||||
params: CreateWorkspaceMcpServerParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
let workspaceId = params.workspaceId || context.workspaceId
|
||||
const workflowId = context.workflowId
|
||||
|
||||
if (!workspaceId && workflowId) {
|
||||
const { workflow: workflowRecord } = await ensureWorkflowAccess(
|
||||
workflowId,
|
||||
context.userId,
|
||||
'write'
|
||||
)
|
||||
workspaceId = workflowRecord.workspaceId ?? undefined
|
||||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
return { success: false, error: 'workspaceId is required' }
|
||||
}
|
||||
await ensureWorkspaceAccess(workspaceId, context.userId, 'admin')
|
||||
|
||||
const name = params.name?.trim()
|
||||
if (!name) {
|
||||
return { success: false, error: 'name is required' }
|
||||
}
|
||||
|
||||
const result = await performCreateWorkflowMcpServer({
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
name,
|
||||
description: params.description,
|
||||
isPublic: params.isPublic,
|
||||
workflowIds: params.workflowIds,
|
||||
})
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to create MCP server' }
|
||||
}
|
||||
|
||||
return { success: true, output: { server: result.server, addedTools: result.addedTools || [] } }
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeUpdateWorkspaceMcpServer(
|
||||
params: UpdateWorkspaceMcpServerParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const serverId = params.serverId
|
||||
if (!serverId) {
|
||||
return { success: false, error: 'serverId is required' }
|
||||
}
|
||||
|
||||
const updates: { name?: string; description?: string | null; isPublic?: boolean } = {}
|
||||
if (typeof params.name === 'string') {
|
||||
const name = params.name.trim()
|
||||
if (!name) return { success: false, error: 'name cannot be empty' }
|
||||
updates.name = name
|
||||
}
|
||||
if (typeof params.description === 'string') {
|
||||
updates.description = params.description.trim() || null
|
||||
}
|
||||
if (typeof params.isPublic === 'boolean') {
|
||||
updates.isPublic = params.isPublic
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return { success: false, error: 'At least one of name, description, or isPublic is required' }
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
workspaceId: workflowMcpServer.workspaceId,
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.where(eq(workflowMcpServer.id, serverId))
|
||||
.limit(1)
|
||||
|
||||
if (!existing) {
|
||||
return { success: false, error: 'MCP server not found' }
|
||||
}
|
||||
|
||||
await ensureWorkspaceAccess(existing.workspaceId, context.userId, 'write')
|
||||
|
||||
const result = await performUpdateWorkflowMcpServer({
|
||||
serverId,
|
||||
workspaceId: existing.workspaceId,
|
||||
userId: context.userId,
|
||||
...updates,
|
||||
})
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to update MCP server' }
|
||||
}
|
||||
|
||||
return { success: true, output: { serverId, ...updates } }
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeDeleteWorkspaceMcpServer(
|
||||
params: DeleteWorkspaceMcpServerParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const serverId = params.serverId
|
||||
if (!serverId) {
|
||||
return { success: false, error: 'serverId is required' }
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
name: workflowMcpServer.name,
|
||||
workspaceId: workflowMcpServer.workspaceId,
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.where(and(eq(workflowMcpServer.id, serverId), isNull(workflowMcpServer.deletedAt)))
|
||||
.limit(1)
|
||||
|
||||
if (!existing) {
|
||||
return { success: false, error: 'MCP server not found' }
|
||||
}
|
||||
|
||||
await ensureWorkspaceAccess(existing.workspaceId, context.userId, 'admin')
|
||||
|
||||
const result = await performDeleteWorkflowMcpServer({
|
||||
serverId,
|
||||
workspaceId: existing.workspaceId,
|
||||
userId: context.userId,
|
||||
})
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to delete MCP server' }
|
||||
}
|
||||
|
||||
return { success: true, output: { serverId, name: existing.name, deleted: true } }
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeGetDeploymentLog(
|
||||
params: GetDeploymentLogParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId || context.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
await ensureWorkflowAccess(workflowId, context.userId)
|
||||
|
||||
const { versions: rows } = await listWorkflowVersions(workflowId)
|
||||
|
||||
const versions = rows.map((r) => ({
|
||||
id: r.id,
|
||||
version: r.version,
|
||||
name: r.name ?? undefined,
|
||||
description: r.description ?? undefined,
|
||||
isActive: r.isActive,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
createdBy: r.createdBy ?? undefined,
|
||||
}))
|
||||
|
||||
return { success: true, output: { workflowId, count: versions.length, versions } }
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
// Cap individual sub-block before/after values so a large diff can't blow the
|
||||
// tool-result budget. Oversized values are replaced with an elision marker.
|
||||
const MAX_DIFF_VALUE_BYTES = 2000
|
||||
|
||||
function guardDiffValue(value: unknown): unknown {
|
||||
try {
|
||||
const json = JSON.stringify(value)
|
||||
if (json && json.length > MAX_DIFF_VALUE_BYTES) {
|
||||
return { elided: true, bytes: json.length }
|
||||
}
|
||||
} catch {
|
||||
return { elided: true, reason: 'unserializable' }
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export async function executeDiffWorkflows(
|
||||
params: DiffWorkflowsParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId || context.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
if (params.ref1 === undefined || params.ref2 === undefined) {
|
||||
return { success: false, error: 'ref1 and ref2 are required' }
|
||||
}
|
||||
|
||||
// resolveWorkflowStateRef enforces read access on the workflow.
|
||||
const [side1, side2] = await Promise.all([
|
||||
resolveWorkflowStateRef(workflowId, params.ref1, context.userId),
|
||||
resolveWorkflowStateRef(workflowId, params.ref2, context.userId),
|
||||
])
|
||||
|
||||
// ref1 = base/previous, ref2 = target/current: added = present in ref2 only.
|
||||
const summary = generateWorkflowDiffSummary(side2.state, side1.state)
|
||||
const diff = {
|
||||
...summary,
|
||||
modifiedBlocks: summary.modifiedBlocks.map((block) => ({
|
||||
...block,
|
||||
changes: block.changes.map((change) => ({
|
||||
field: change.field,
|
||||
oldValue: guardDiffValue(change.oldValue),
|
||||
newValue: guardDiffValue(change.newValue),
|
||||
})),
|
||||
})),
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
workflowId,
|
||||
ref1: { ref: side1.ref, version: side1.version, isActive: side1.isActive },
|
||||
ref2: { ref: side2.ref, version: side2.version, isActive: side2.isActive },
|
||||
diff,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
function resolveLoadVersion(
|
||||
raw: number | string
|
||||
): { ok: true; version: number | 'active' } | { ok: false; error: string } {
|
||||
if (typeof raw === 'number' && Number.isFinite(raw)) return { ok: true, version: raw }
|
||||
if (typeof raw === 'string') {
|
||||
const t = raw.trim().toLowerCase()
|
||||
if (t === 'live' || t === 'active') return { ok: true, version: 'active' }
|
||||
if (t === 'draft' || t === 'current') {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'Cannot load "draft" — load_deployment restores a deployed version into the draft',
|
||||
}
|
||||
}
|
||||
if (/^\d+$/.test(t)) return { ok: true, version: Number.parseInt(t, 10) }
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: `Invalid version "${String(raw)}": expected a version number or "live"`,
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeLoadDeployment(
|
||||
params: LoadDeploymentParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId || context.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
if (params.version === undefined || params.version === null) {
|
||||
return { success: false, error: 'version is required' }
|
||||
}
|
||||
const target = resolveLoadVersion(params.version)
|
||||
if (!target.ok) {
|
||||
return { success: false, error: target.error }
|
||||
}
|
||||
|
||||
const { workflow: workflowRecord } = await ensureWorkflowAccess(
|
||||
workflowId,
|
||||
context.userId,
|
||||
'admin'
|
||||
)
|
||||
const result = await performRevertToVersion({
|
||||
workflowId,
|
||||
version: target.version,
|
||||
userId: context.userId,
|
||||
workflow: workflowRecord as Record<string, unknown>,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to load deployment' }
|
||||
}
|
||||
|
||||
const label = target.version === 'active' ? 'the live deployment' : `version ${target.version}`
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
workflowId,
|
||||
message: `Loaded ${label} into the workflow draft`,
|
||||
lastSaved: result.lastSaved,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePromoteVersion(raw: number | string): number | null {
|
||||
if (typeof raw === 'number' && Number.isFinite(raw)) return raw
|
||||
if (typeof raw === 'string' && /^\d+$/.test(raw.trim())) return Number.parseInt(raw.trim(), 10)
|
||||
return null
|
||||
}
|
||||
|
||||
export async function executePromoteToLive(
|
||||
params: PromoteToLiveParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId || context.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
if (params.version === undefined || params.version === null) {
|
||||
return { success: false, error: 'version is required' }
|
||||
}
|
||||
const version = normalizePromoteVersion(params.version)
|
||||
if (version === null) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'version must be a deployment version number (use load_deployment to change the draft; "live" is already live)',
|
||||
}
|
||||
}
|
||||
|
||||
const { workflow: workflowRecord } = await ensureWorkflowAccess(
|
||||
workflowId,
|
||||
context.userId,
|
||||
'admin'
|
||||
)
|
||||
const result = await performActivateVersion({
|
||||
workflowId,
|
||||
version,
|
||||
userId: context.userId,
|
||||
workflow: workflowRecord as Record<string, unknown>,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to promote version' }
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
workflowId,
|
||||
version,
|
||||
message: `Promoted version ${version} to live`,
|
||||
deployedAt: result.deployedAt ? new Date(result.deployedAt).toISOString() : undefined,
|
||||
warnings: result.warnings,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeUpdateDeploymentVersion(
|
||||
params: UpdateDeploymentVersionParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId || context.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
if (params.version === undefined || params.version === null) {
|
||||
return { success: false, error: 'version is required' }
|
||||
}
|
||||
const version = normalizePromoteVersion(params.version)
|
||||
if (version === null) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'version must be a deployment version number (use get_deployment_log to find it)',
|
||||
}
|
||||
}
|
||||
|
||||
const name = typeof params.name === 'string' ? params.name.trim() : undefined
|
||||
const description =
|
||||
typeof params.description === 'string' ? params.description.trim() : undefined
|
||||
if (name === undefined && description === undefined) {
|
||||
return { success: false, error: 'Provide a name and/or description to update' }
|
||||
}
|
||||
|
||||
await ensureWorkflowAccess(workflowId, context.userId, 'write')
|
||||
|
||||
const updated = await updateDeploymentVersionMetadata({
|
||||
workflowId,
|
||||
version,
|
||||
...(name !== undefined ? { name: name || null } : {}),
|
||||
...(description !== undefined ? { description: description || null } : {}),
|
||||
})
|
||||
if (!updated) {
|
||||
return { success: false, error: `Deployment version ${version} not found` }
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { workflowId, version, name: updated.name, description: updated.description },
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user