mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 04:17:11 +00:00
refactor(config): remove secrets.yaml architecture
- delete secrets-manager.ts entirely - remove secrets API routes and client methods - simplify unified-config-types (remove vault/secrets) - update profile-reader to remove secrets loading - clean up unused hooks and API client methods
This commit is contained in:
@@ -9,7 +9,6 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getCcsDir, loadConfig } from '../../utils/config-manager';
|
||||
import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
|
||||
import { getProfileSecrets } from '../../config/secrets-manager';
|
||||
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
|
||||
|
||||
/**
|
||||
@@ -36,29 +35,7 @@ export function isApiProfileConfigured(apiName: string): boolean {
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${apiName}.settings.json`);
|
||||
|
||||
if (isUnifiedMode()) {
|
||||
// Check secrets.yaml first
|
||||
const secrets = getProfileSecrets(apiName);
|
||||
const secretToken = secrets?.ANTHROPIC_AUTH_TOKEN || '';
|
||||
if (
|
||||
secretToken.length > 0 &&
|
||||
!secretToken.includes('YOUR_') &&
|
||||
!secretToken.includes('your-')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: check settings.json file (profiles created via UI store keys here)
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
const token = settings?.env?.ANTHROPIC_AUTH_TOKEN || '';
|
||||
return token.length > 0 && !token.includes('YOUR_') && !token.includes('your-');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Legacy: check settings.json file
|
||||
// Check settings.json file for API key
|
||||
if (!fs.existsSync(settingsPath)) return false;
|
||||
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
saveUnifiedConfig,
|
||||
isUnifiedMode,
|
||||
} from '../../config/unified-config-loader';
|
||||
import { deleteAllProfileSecrets } from '../../config/secrets-manager';
|
||||
import type { ModelMapping, CreateApiProfileResult, RemoveApiProfileResult } from './profile-types';
|
||||
|
||||
/** Create settings.json file for API profile (legacy format) */
|
||||
@@ -152,9 +151,6 @@ function removeApiProfileUnified(name: string): void {
|
||||
}
|
||||
|
||||
saveUnifiedConfig(config);
|
||||
|
||||
// Remove any legacy secrets
|
||||
deleteAllProfileSecrets(name);
|
||||
}
|
||||
|
||||
/** Remove API profile from legacy config */
|
||||
|
||||
@@ -16,7 +16,6 @@ import { findSimilarStrings } from '../utils/helpers';
|
||||
import { Config, Settings, ProfileMetadata } from '../types';
|
||||
import { UnifiedConfig, CopilotConfig } from '../config/unified-config-types';
|
||||
import { loadUnifiedConfig, isUnifiedMode } from '../config/unified-config-loader';
|
||||
import { getProfileSecrets } from '../config/secrets-manager';
|
||||
|
||||
export type ProfileType = 'settings' | 'account' | 'cliproxy' | 'copilot' | 'default';
|
||||
|
||||
@@ -110,12 +109,10 @@ class ProfileDetector {
|
||||
const profile = config.profiles[profileName];
|
||||
// Load env from settings file
|
||||
const settingsEnv = loadSettingsFromFile(profile.settings);
|
||||
// Merge with secrets (for backward compat with any extracted secrets)
|
||||
const secrets = getProfileSecrets(profileName);
|
||||
return {
|
||||
type: 'settings',
|
||||
name: profileName,
|
||||
env: { ...settingsEnv, ...secrets },
|
||||
env: settingsEnv,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -384,11 +384,9 @@ async function handleRemove(args: string[]): Promise<void> {
|
||||
// Confirm deletion
|
||||
console.log('');
|
||||
console.log(`API '${color(name, 'command')}' will be removed.`);
|
||||
console.log(` Settings: ~/.ccs/${name}.settings.json`);
|
||||
if (isUsingUnifiedConfig()) {
|
||||
console.log(' Config: ~/.ccs/config.yaml');
|
||||
console.log(' Secrets: ~/.ccs/secrets.yaml');
|
||||
} else {
|
||||
console.log(` Settings: ~/.ccs/${name}.settings.json`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ export * from './reserved-names';
|
||||
|
||||
// Loaders
|
||||
export * from './unified-config-loader';
|
||||
export * from './secrets-manager';
|
||||
|
||||
// Migration
|
||||
export * from './migration-manager';
|
||||
|
||||
@@ -224,11 +224,9 @@ export async function rollback(backupPath: string): Promise<boolean> {
|
||||
try {
|
||||
// Remove new config files
|
||||
const configYaml = path.join(ccsDir, 'config.yaml');
|
||||
const secretsYaml = path.join(ccsDir, 'secrets.yaml');
|
||||
const cacheDir = path.join(ccsDir, 'cache');
|
||||
|
||||
if (fs.existsSync(configYaml)) fs.unlinkSync(configYaml);
|
||||
if (fs.existsSync(secretsYaml)) fs.unlinkSync(secretsYaml);
|
||||
|
||||
// Restore cache files to original locations
|
||||
if (fs.existsSync(cacheDir)) {
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
/**
|
||||
* Secrets Manager
|
||||
*
|
||||
* Handles loading and saving secrets (API keys, tokens) in a separate file
|
||||
* with restricted permissions (chmod 600).
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { getCcsDir } from '../utils/config-manager';
|
||||
import { SecretsConfig, isSecretsConfig, createEmptySecretsConfig } from './unified-config-types';
|
||||
|
||||
// Re-export from shared utility for backward compatibility
|
||||
export { isSensitiveKey as isSecretKey } from '../utils/sensitive-keys';
|
||||
|
||||
const SECRETS_FILE = 'secrets.yaml';
|
||||
const SECRETS_FILE_MODE = 0o600; // Owner read/write only
|
||||
|
||||
/**
|
||||
* Get path to secrets.yaml
|
||||
*/
|
||||
export function getSecretsPath(): string {
|
||||
return path.join(getCcsDir(), SECRETS_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if secrets.yaml exists
|
||||
*/
|
||||
export function hasSecrets(): boolean {
|
||||
return fs.existsSync(getSecretsPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load secrets from YAML file.
|
||||
* Returns empty secrets config if file doesn't exist.
|
||||
*/
|
||||
export function loadSecrets(): SecretsConfig {
|
||||
const secretsPath = getSecretsPath();
|
||||
|
||||
if (!fs.existsSync(secretsPath)) {
|
||||
return createEmptySecretsConfig();
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(secretsPath, 'utf8');
|
||||
const parsed = yaml.load(content);
|
||||
|
||||
if (!isSecretsConfig(parsed)) {
|
||||
console.error(`[!] Invalid secrets format in ${secretsPath}`);
|
||||
return createEmptySecretsConfig();
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`[X] Failed to load secrets: ${error}`);
|
||||
return createEmptySecretsConfig();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save secrets to YAML file with restricted permissions.
|
||||
* Uses atomic write (temp file + rename) to prevent corruption.
|
||||
*/
|
||||
export function saveSecrets(secrets: SecretsConfig): void {
|
||||
const secretsPath = getSecretsPath();
|
||||
const dir = path.dirname(secretsPath);
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
// Convert to YAML
|
||||
const content = yaml.dump(secrets, {
|
||||
indent: 2,
|
||||
lineWidth: -1,
|
||||
quotingType: '"',
|
||||
noRefs: true,
|
||||
});
|
||||
|
||||
// Atomic write: write to temp file, then rename
|
||||
const tempPath = `${secretsPath}.tmp.${process.pid}`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(tempPath, content, { mode: SECRETS_FILE_MODE });
|
||||
fs.renameSync(tempPath, secretsPath);
|
||||
|
||||
// Ensure correct permissions after rename (some systems may not preserve)
|
||||
fs.chmodSync(secretsPath, SECRETS_FILE_MODE);
|
||||
} catch (err) {
|
||||
// Clean up temp file on error
|
||||
if (fs.existsSync(tempPath)) {
|
||||
try {
|
||||
fs.unlinkSync(tempPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a secret value for a specific profile.
|
||||
*/
|
||||
export function getProfileSecret(profileName: string, key: string): string | undefined {
|
||||
const secrets = loadSecrets();
|
||||
return secrets.profiles[profileName]?.[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a secret value for a specific profile.
|
||||
*/
|
||||
export function setProfileSecret(profileName: string, key: string, value: string): void {
|
||||
const secrets = loadSecrets();
|
||||
|
||||
if (!secrets.profiles[profileName]) {
|
||||
secrets.profiles[profileName] = {};
|
||||
}
|
||||
|
||||
secrets.profiles[profileName][key] = value;
|
||||
saveSecrets(secrets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a secret value for a specific profile.
|
||||
*/
|
||||
export function deleteProfileSecret(profileName: string, key: string): boolean {
|
||||
const secrets = loadSecrets();
|
||||
|
||||
if (!secrets.profiles[profileName]?.[key]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete secrets.profiles[profileName][key];
|
||||
|
||||
// Clean up empty profile object
|
||||
if (Object.keys(secrets.profiles[profileName]).length === 0) {
|
||||
delete secrets.profiles[profileName];
|
||||
}
|
||||
|
||||
saveSecrets(secrets);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all secrets for a profile.
|
||||
*/
|
||||
export function getProfileSecrets(profileName: string): Record<string, string> {
|
||||
const secrets = loadSecrets();
|
||||
return secrets.profiles[profileName] || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all secrets for a profile (replaces existing).
|
||||
*/
|
||||
export function setProfileSecrets(
|
||||
profileName: string,
|
||||
profileSecrets: Record<string, string>
|
||||
): void {
|
||||
const secrets = loadSecrets();
|
||||
|
||||
if (Object.keys(profileSecrets).length === 0) {
|
||||
delete secrets.profiles[profileName];
|
||||
} else {
|
||||
secrets.profiles[profileName] = profileSecrets;
|
||||
}
|
||||
|
||||
saveSecrets(secrets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all secrets for a profile.
|
||||
*/
|
||||
export function deleteAllProfileSecrets(profileName: string): boolean {
|
||||
const secrets = loadSecrets();
|
||||
|
||||
if (!secrets.profiles[profileName]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete secrets.profiles[profileName];
|
||||
saveSecrets(secrets);
|
||||
return true;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* - profiles.json (account metadata)
|
||||
* - *.settings.json (env vars)
|
||||
*
|
||||
* Into a single config.yaml + secrets.yaml structure.
|
||||
* Into a single config.yaml structure.
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -321,18 +321,6 @@ export interface UnifiedConfig {
|
||||
cliproxy_server?: CliproxyServerConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Secrets configuration structure.
|
||||
* Stored in ~/.ccs/secrets.yaml with chmod 600.
|
||||
* Contains sensitive values like API keys.
|
||||
*/
|
||||
export interface SecretsConfig {
|
||||
/** Secrets version */
|
||||
version: number;
|
||||
/** Profile secrets mapping: profile_name -> { key: value } */
|
||||
profiles: Record<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default Copilot configuration.
|
||||
* Strictly opt-in - disabled by default.
|
||||
@@ -422,16 +410,6 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty secrets config.
|
||||
*/
|
||||
export function createEmptySecretsConfig(): SecretsConfig {
|
||||
return {
|
||||
version: 1,
|
||||
profiles: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for UnifiedConfig.
|
||||
* Relaxed validation: accepts configs with version >= 1 and any subset of sections.
|
||||
@@ -444,12 +422,3 @@ export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig {
|
||||
// Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig
|
||||
return typeof config.version === 'number' && config.version >= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for SecretsConfig.
|
||||
*/
|
||||
export function isSecretsConfig(obj: unknown): obj is SecretsConfig {
|
||||
if (typeof obj !== 'object' || obj === null) return false;
|
||||
const config = obj as Record<string, unknown>;
|
||||
return typeof config.version === 'number' && typeof config.profiles === 'object';
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as os from 'os';
|
||||
import { Config, isConfig, Settings, isSettings } from '../types';
|
||||
import { expandPath, error } from './helpers';
|
||||
import { info } from './ui';
|
||||
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
||||
|
||||
// TODO: Replace with proper imports after converting these files
|
||||
// const { ErrorManager } = require('./error-manager');
|
||||
@@ -82,16 +83,52 @@ export function readConfig(): Config {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings path for profile
|
||||
* Get settings path for profile.
|
||||
* In unified mode (config.yaml exists), reads from config.yaml first,
|
||||
* then falls back to config.json for backward compatibility.
|
||||
*/
|
||||
export function getSettingsPath(profile: string): string {
|
||||
const config = readConfig();
|
||||
let settingsPath: string | undefined;
|
||||
let availableProfiles: string[] = [];
|
||||
|
||||
// Get settings path
|
||||
const settingsPath = config.profiles[profile];
|
||||
// Check unified config first (config.yaml)
|
||||
if (isUnifiedMode()) {
|
||||
const unifiedConfig = loadOrCreateUnifiedConfig();
|
||||
|
||||
// Check if profile exists in unified config
|
||||
const profileConfig = unifiedConfig.profiles[profile];
|
||||
if (profileConfig?.settings) {
|
||||
settingsPath = profileConfig.settings;
|
||||
}
|
||||
|
||||
// Collect available profiles from unified config
|
||||
availableProfiles = Object.keys(unifiedConfig.profiles);
|
||||
|
||||
// If not found in unified config, try legacy config.json as fallback
|
||||
if (!settingsPath) {
|
||||
try {
|
||||
const legacyConfig = loadConfig();
|
||||
if (legacyConfig.profiles[profile]) {
|
||||
settingsPath = legacyConfig.profiles[profile];
|
||||
// Merge legacy profiles into available list (avoid duplicates)
|
||||
for (const p of Object.keys(legacyConfig.profiles)) {
|
||||
if (!availableProfiles.includes(p)) {
|
||||
availableProfiles.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Legacy config doesn't exist or is invalid - that's OK in unified mode
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy mode - read from config.json only
|
||||
const config = readConfig();
|
||||
settingsPath = config.profiles[profile];
|
||||
availableProfiles = Object.keys(config.profiles);
|
||||
}
|
||||
|
||||
if (!settingsPath) {
|
||||
const availableProfiles = Object.keys(config.profiles);
|
||||
const profileList = availableProfiles.map((p) => ` - ${p}`);
|
||||
error(`Profile '${profile}' not found. Available profiles:\n${profileList.join('\n')}`);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
rollback,
|
||||
getBackupDirectories,
|
||||
} from '../../config/migration-manager';
|
||||
import { getProfileSecrets, setProfileSecrets } from '../../config/secrets-manager';
|
||||
import { isUnifiedConfig } from '../../config/unified-config-types';
|
||||
|
||||
const router = Router();
|
||||
@@ -111,36 +110,4 @@ router.post('/rollback', async (req: Request, res: Response): Promise<void> => {
|
||||
res.json({ success });
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/secrets/:profile - Update profile secrets (write-only)
|
||||
*/
|
||||
router.put('/secrets/:profile', (req: Request, res: Response): void => {
|
||||
const { profile } = req.params;
|
||||
const secrets = req.body;
|
||||
|
||||
if (!secrets || typeof secrets !== 'object') {
|
||||
res.status(400).json({ error: 'Invalid secrets format' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setProfileSecrets(profile, secrets as Record<string, string>);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/secrets/:profile/exists - Check if secrets exist (no values returned)
|
||||
*/
|
||||
router.get('/secrets/:profile/exists', (req: Request, res: Response) => {
|
||||
const { profile } = req.params;
|
||||
const secrets = getProfileSecrets(profile);
|
||||
res.json({
|
||||
exists: Object.keys(secrets).length > 0,
|
||||
keys: Object.keys(secrets), // Only key names, not values
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -31,9 +31,8 @@ apiRoutes.use('/settings', settingsRoutes);
|
||||
apiRoutes.use('/accounts', profileRoutes);
|
||||
|
||||
// ==================== Unified Config ====================
|
||||
// Config format, migration, secrets
|
||||
// Config format, migration
|
||||
apiRoutes.use('/config', configRoutes);
|
||||
apiRoutes.use('/secrets', configRoutes);
|
||||
|
||||
// ==================== Health Checks ====================
|
||||
apiRoutes.use('/health', healthRoutes);
|
||||
|
||||
@@ -11,14 +11,12 @@ import {
|
||||
} from '../../src/config/reserved-names';
|
||||
import {
|
||||
createEmptyUnifiedConfig,
|
||||
createEmptySecretsConfig,
|
||||
isUnifiedConfig,
|
||||
isSecretsConfig,
|
||||
UNIFIED_CONFIG_VERSION,
|
||||
} from '../../src/config/unified-config-types';
|
||||
import { isUnifiedConfigEnabled } from '../../src/config/feature-flags';
|
||||
|
||||
// Inline helper to test secret key detection (copied from secrets-manager to avoid import chain)
|
||||
// Inline helper to test secret key detection (utility kept for potential reuse)
|
||||
function isSecretKey(key: string): boolean {
|
||||
const upper = key.toUpperCase();
|
||||
const secretPatterns = ['TOKEN', 'SECRET', 'API_KEY', 'APIKEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'PRIVATE'];
|
||||
@@ -102,18 +100,6 @@ describe('unified-config-types', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createEmptySecretsConfig', () => {
|
||||
it('should create secrets with version 1', () => {
|
||||
const secrets = createEmptySecretsConfig();
|
||||
expect(secrets.version).toBe(1);
|
||||
});
|
||||
|
||||
it('should have empty profiles', () => {
|
||||
const secrets = createEmptySecretsConfig();
|
||||
expect(Object.keys(secrets.profiles)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUnifiedConfig', () => {
|
||||
it('should return true for valid config', () => {
|
||||
const config = createEmptyUnifiedConfig();
|
||||
@@ -143,24 +129,9 @@ describe('unified-config-types', () => {
|
||||
expect(isUnifiedConfig({ version: -1 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSecretsConfig', () => {
|
||||
it('should return true for valid secrets', () => {
|
||||
const secrets = createEmptySecretsConfig();
|
||||
expect(isSecretsConfig(secrets)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for null', () => {
|
||||
expect(isSecretsConfig(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for missing fields', () => {
|
||||
expect(isSecretsConfig({ version: 1 })).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('secrets-manager', () => {
|
||||
describe('sensitive-keys', () => {
|
||||
describe('isSecretKey', () => {
|
||||
it('should identify token keys as secrets', () => {
|
||||
expect(isSecretKey('ANTHROPIC_AUTH_TOKEN')).toBe(true);
|
||||
|
||||
@@ -98,30 +98,3 @@ export function useRollback() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update profile secrets
|
||||
*/
|
||||
export function useUpdateSecrets() {
|
||||
return useMutation({
|
||||
mutationFn: ({ profile, secrets }: { profile: string; secrets: Record<string, string> }) =>
|
||||
api.secrets.update(profile, secrets),
|
||||
onSuccess: () => {
|
||||
toast.success('Secrets updated successfully');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if profile has secrets (doesn't return values)
|
||||
*/
|
||||
export function useSecretsExists(profile: string) {
|
||||
return useQuery({
|
||||
queryKey: ['secrets-exists', profile],
|
||||
queryFn: () => api.secrets.exists(profile),
|
||||
enabled: !!profile,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,11 +132,6 @@ export interface MigrationResult {
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface SecretsExists {
|
||||
exists: boolean;
|
||||
keys: string[];
|
||||
}
|
||||
|
||||
/** Model preset for quick model switching */
|
||||
export interface ModelPreset {
|
||||
name: string;
|
||||
@@ -369,14 +364,6 @@ export const api = {
|
||||
body: JSON.stringify({ backupPath }),
|
||||
}),
|
||||
},
|
||||
secrets: {
|
||||
update: (profile: string, secrets: Record<string, string>) =>
|
||||
request<{ success: boolean }>(`/secrets/${profile}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(secrets),
|
||||
}),
|
||||
exists: (profile: string) => request<SecretsExists>(`/secrets/${profile}/exists`),
|
||||
},
|
||||
/** Model presets for quick model switching */
|
||||
presets: {
|
||||
list: (profile: string) => request<{ presets: ModelPreset[] }>(`/settings/${profile}/presets`),
|
||||
|
||||
Reference in New Issue
Block a user