dify/cli/src/commands/import/app/run.ts
Yunlu Wen 0a051b598f
feat: support import / export dsl in CLI (#37232)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: cheatofrom <85830867+cheatofrom@users.noreply.github.com>
Co-authored-by: Escape0707 <tothesong@gmail.com>
Co-authored-by: Rohit Gahlawat <personal.rg56@gmail.com>
Co-authored-by: L1nSn0w <l1nsn0w@qq.com>
Co-authored-by: 盐粒 Yanli <yanli@dify.ai>
2026-06-10 09:51:40 +00:00

139 lines
4.7 KiB
TypeScript

import type { Import, PluginDependency } from '@dify/contracts/api/openapi/types.gen'
import type { ActiveContext } from '@/auth/hosts'
import type { HttpClient } from '@/http/types'
import type { IOStreams } from '@/sys/io/streams'
import fs from 'node:fs'
import { AppDslClient } from '@/api/app-dsl'
import { newError } from '@/errors/base'
import { ErrorCode } from '@/errors/codes'
import { getEnv } from '@/sys/index'
import { runWithSpinner } from '@/sys/io/spinner'
import { nullStreams } from '@/sys/io/streams'
import { resolveWorkspaceId } from '@/workspace/resolver'
export type ImportAppOptions = {
readonly fromFile?: string
readonly fromUrl?: string
readonly workspace?: string
readonly name?: string
readonly description?: string
readonly appId?: string
readonly iconType?: string
readonly icon?: string
readonly iconBackground?: string
}
export type ImportAppDeps = {
readonly active: ActiveContext
readonly http: HttpClient
readonly io?: IOStreams
readonly envLookup?: (k: string) => string | undefined
readonly dslFactory?: (http: HttpClient) => AppDslClient
}
export type ImportAppResult = {
readonly result: Import
readonly leakedDependencies: readonly PluginDependency[]
}
export async function runImportApp(opts: ImportAppOptions, deps: ImportAppDeps): Promise<ImportAppResult> {
const env = deps.envLookup ?? getEnv
const io = deps.io ?? nullStreams()
const dslFactory = deps.dslFactory ?? ((h: HttpClient) => new AppDslClient(h))
const workspaceId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active })
const client = dslFactory(deps.http)
if (opts.fromFile !== undefined && opts.fromUrl !== undefined)
throw newError(ErrorCode.UsageInvalidFlag, '--from-file and --from-url are mutually exclusive')
let mode: 'yaml-content' | 'yaml-url'
let yamlContent: string | undefined
let yamlUrl: string | undefined
if (opts.fromFile !== undefined) {
mode = 'yaml-content'
try {
yamlContent = fs.readFileSync(opts.fromFile, 'utf8')
}
catch (err) {
const code = (err as NodeJS.ErrnoException).code
if (code === 'ENOENT')
throw newError(ErrorCode.UsageInvalidFlag, `--from-file: file not found: ${opts.fromFile}`)
throw err
}
}
else if (opts.fromUrl !== undefined) {
mode = 'yaml-url'
yamlUrl = opts.fromUrl
}
else {
throw newError(ErrorCode.UsageInvalidFlag, 'one of --from-file or --from-url is required')
}
let result = await runWithSpinner(
{ io, label: 'Importing app DSL' },
() => client.importApp(workspaceId, {
mode,
yaml_content: yamlContent,
yaml_url: yamlUrl,
name: opts.name,
description: opts.description,
app_id: opts.appId,
icon_type: opts.iconType,
icon: opts.icon,
icon_background: opts.iconBackground,
}),
)
if (result.status === 'failed') {
throw newError(
ErrorCode.Server4xxOther,
`Import failed: ${result.error !== '' ? result.error : 'unknown error'}`,
)
}
// DSL version mismatch: the server needs an explicit acknowledgement before
// finalising. Auto-confirm here so the user does not need a second command.
if (result.status === 'pending') {
io.err.write(`note: DSL version mismatch (imported ${result.imported_dsl_version ?? '?'}, current ${result.current_dsl_version ?? '?'}); confirming automatically\n`)
result = await runWithSpinner(
{ io, label: 'Confirming import' },
() => client.confirmImport(workspaceId, result.id),
)
}
if (result.status === 'failed') {
throw newError(
ErrorCode.Server4xxOther,
`Import failed after confirmation: ${result.error !== '' ? result.error : 'unknown error'}`,
)
}
const appId = result.app_id
if (appId === undefined || appId === null)
return { result, leakedDependencies: [] }
const { leaked_dependencies } = await runWithSpinner(
{ io, label: 'Checking plugin dependencies' },
() => client.checkDependencies(appId),
)
return { result, leakedDependencies: leaked_dependencies ?? [] }
}
// `value` is a loosely-typed wire object (Github | Marketplace | Package); narrow it here to
// surface a human-readable identifier without depending on which variant the server returned.
export function pluginDependencyLabel(dep: PluginDependency): string {
const value = dep.value
if (typeof value === 'object' && value !== null) {
const fields = value as Record<string, unknown>
const id = fields.marketplace_plugin_unique_identifier
?? fields.github_plugin_unique_identifier
?? fields.plugin_unique_identifier
if (typeof id === 'string' && id !== '')
return id
}
return dep.current_identifier ?? '<unknown>'
}