dify/cli/src/store/manager.ts
2026-06-15 02:24:51 +00:00

85 lines
2.8 KiB
TypeScript

import type { StorageMode, Store } from './store'
import type { TokenStore } from './token-store'
import { join } from 'node:path'
import { resolveCacheDir, resolveConfigDir } from './dir'
import { YamlStore } from './store'
import { FileTokenStore, KeychainTokenStore } from './token-store'
export const CACHE_APP_INFO = 'app-info'
export const CACHE_NUDGE = 'nudge'
const HOSTS_FILE = 'hosts.yml'
const TOKENS_FILE = 'tokens.yml'
export const CONFIG_FILE_NAME = 'config.yml'
const KEYRING_SERVICE = 'difyctl'
function getStore(filePath: string): YamlStore {
return new YamlStore(filePath)
}
export function cachePath(cacheDir: string, name: string): string {
return join(cacheDir, `${name}.yml`)
}
export function getConfigurationStore(): YamlStore {
return getStore(join(resolveConfigDir(), CONFIG_FILE_NAME))
}
export function getCache(cacheName: string): Store {
return getStore(cachePath(resolveCacheDir(), cacheName))
}
export function getHostStore(): YamlStore {
return getStore(join(resolveConfigDir(), HOSTS_FILE))
}
const PROBE_HOST = '__difyctl_probe__'
const PROBE_EMAIL = '__difyctl_probe__'
const PROBE_VALUE = 'probe-v1'
export type GetTokenStoreOptions = {
readonly factory?: {
readonly keyring?: () => TokenStore
readonly file?: () => TokenStore
}
}
const TOKEN_STORE_OPENERS: Record<StorageMode, (opts: GetTokenStoreOptions) => TokenStore> = {
file: opts => opts.factory?.file?.() ?? new FileTokenStore(join(resolveConfigDir(), TOKENS_FILE)),
keychain: opts => opts.factory?.keyring?.() ?? new KeychainTokenStore(KEYRING_SERVICE),
}
/**
* Decide which credential backend to use by probing the OS keyring with a
* write/read/remove round-trip. The probe MUTATES the keyring, so call this
* only where a credential is about to be written anyway (login).
*/
export async function detectTokenStore(opts: GetTokenStoreOptions = {}): Promise<{ store: TokenStore, mode: StorageMode }> {
// DIFY_E2E_NO_KEYRING=1 forces file-based storage in E2E tests to avoid
// macOS keychain UI prompts blocking child processes spawned by vitest.
if (process.env.DIFY_E2E_NO_KEYRING === '1')
return { store: TOKEN_STORE_OPENERS.file(opts), mode: 'file' }
try {
const k = TOKEN_STORE_OPENERS.keychain(opts)
await k.write(PROBE_HOST, PROBE_EMAIL, PROBE_VALUE)
let got = ''
try {
got = await k.read(PROBE_HOST, PROBE_EMAIL)
}
finally {
await k.remove(PROBE_HOST, PROBE_EMAIL)
}
if (got === PROBE_VALUE)
return { store: k, mode: 'keychain' }
}
catch { /* keyring unavailable → fall through to file */ }
return { store: TOKEN_STORE_OPENERS.file(opts), mode: 'file' }
}
/**
* Construct the credential backend the registry already recorded at login.
*/
export function getTokenStore(mode: StorageMode, opts: GetTokenStoreOptions = {}): TokenStore {
return TOKEN_STORE_OPENERS[mode](opts)
}