mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 22:15:33 +00:00
feat: add API profile lifecycle discover/copy/export/import parity
This commit is contained in:
1 parent
8403284cd2
commit
02c81743a0
13 files changed
+1732
-31
No files matched your search
@@ -16,6 +16,15 @@ export {
|
||||
type CreateApiProfileResult,
|
||||
type RemoveApiProfileResult,
|
||||
type UpdateApiProfileTargetResult,
|
||||
type ProfileValidationIssue,
|
||||
type ProfileValidationSummary,
|
||||
type ApiProfileOrphanCandidate,
|
||||
type DiscoverApiProfileOrphansResult,
|
||||
type RegisterApiProfileOrphansResult,
|
||||
type CopyApiProfileResult,
|
||||
type ApiProfileExportBundle,
|
||||
type ExportApiProfileResult,
|
||||
type ImportApiProfileResult,
|
||||
} from './profile-types';
|
||||
|
||||
// Profile read operations
|
||||
@@ -30,6 +39,16 @@ export {
|
||||
// Profile write operations
|
||||
export { createApiProfile, removeApiProfile, updateApiProfileTarget } from './profile-writer';
|
||||
|
||||
// Lifecycle validation and operations
|
||||
export { validateApiProfileSettingsPayload } from './profile-lifecycle-validation';
|
||||
export {
|
||||
discoverApiProfileOrphans,
|
||||
registerApiProfileOrphans,
|
||||
copyApiProfile,
|
||||
exportApiProfile,
|
||||
importApiProfileBundle,
|
||||
} from './profile-lifecycle-service';
|
||||
|
||||
// OpenRouter catalog and picker
|
||||
export { isOpenRouterUrl, fetchOpenRouterModels, type OpenRouterModel } from './openrouter-catalog';
|
||||
export { pickOpenRouterModel, type OpenRouterSelection } from './openrouter-picker';
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* API profile lifecycle service.
|
||||
*
|
||||
* Discovery, registration, copy, export, and import for API profiles.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { Config, Settings } from '../../types';
|
||||
import type { TargetType } from '../../targets/target-adapter';
|
||||
import { getCcsDir, getConfigPath, loadConfigSafe } from '../../utils/config-manager';
|
||||
import { ensureProfileHooks } from '../../utils/websearch/profile-hook-injector';
|
||||
import { isSensitiveKey } from '../../utils/sensitive-keys';
|
||||
import { isReservedName } from '../../config/reserved-names';
|
||||
import {
|
||||
isUnifiedMode,
|
||||
loadOrCreateUnifiedConfig,
|
||||
saveUnifiedConfig,
|
||||
} from '../../config/unified-config-loader';
|
||||
import { validateApiName } from './validation-service';
|
||||
import { listApiProfiles } from './profile-reader';
|
||||
import { validateApiProfileSettingsPayload } from './profile-lifecycle-validation';
|
||||
import type {
|
||||
ApiProfileExportBundle,
|
||||
CopyApiProfileResult,
|
||||
DiscoverApiProfileOrphansResult,
|
||||
ExportApiProfileResult,
|
||||
ImportApiProfileResult,
|
||||
RegisterApiProfileOrphansResult,
|
||||
} from './profile-types';
|
||||
|
||||
const SETTINGS_FILE_SUFFIX = '.settings.json';
|
||||
const REDACTED_TOKEN_SENTINEL = '__CCS_REDACTED__';
|
||||
|
||||
function validateProfileNameForPath(name: string, label: string): string | null {
|
||||
const validationError = validateApiName(name);
|
||||
if (validationError) {
|
||||
return `Invalid ${label} profile name "${name}": ${validationError}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getProfileSettingsPath(name: string): string {
|
||||
return path.join(getCcsDir(), `${name}${SETTINGS_FILE_SUFFIX}`);
|
||||
}
|
||||
|
||||
function writeJsonObjectAtomically(filePath: string, value: unknown): void {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const tempPath = `${filePath}.tmp`;
|
||||
fs.writeFileSync(tempPath, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tempPath, filePath);
|
||||
}
|
||||
|
||||
function registerApiProfileInConfig(name: string, target: TargetType, force = false): void {
|
||||
if (isUnifiedMode()) {
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
if (config.profiles[name] && !force) {
|
||||
throw new Error(`API profile already exists: ${name}`);
|
||||
}
|
||||
|
||||
config.profiles[name] = {
|
||||
type: 'api',
|
||||
settings: `~/.ccs/${name}${SETTINGS_FILE_SUFFIX}`,
|
||||
...(target !== 'claude' && { target }),
|
||||
};
|
||||
saveUnifiedConfig(config);
|
||||
return;
|
||||
}
|
||||
|
||||
const configPath = getConfigPath();
|
||||
const config = loadConfigSafe() as Config;
|
||||
if (config.profiles[name] && !force) {
|
||||
throw new Error(`API profile already exists: ${name}`);
|
||||
}
|
||||
|
||||
config.profiles[name] = `~/.ccs/${name}${SETTINGS_FILE_SUFFIX}`;
|
||||
config.profile_targets = config.profile_targets || {};
|
||||
if (target === 'claude') {
|
||||
delete config.profile_targets[name];
|
||||
} else {
|
||||
config.profile_targets[name] = target;
|
||||
}
|
||||
|
||||
writeJsonObjectAtomically(configPath, config);
|
||||
}
|
||||
|
||||
function getRegisteredSettingsFileNames(): Set<string> {
|
||||
const { profiles, variants } = listApiProfiles();
|
||||
const names = new Set<string>();
|
||||
|
||||
for (const profile of profiles) {
|
||||
names.add(`${profile.name}${SETTINGS_FILE_SUFFIX}`);
|
||||
}
|
||||
|
||||
for (const variant of variants) {
|
||||
if (!variant.settings || variant.settings === '-') continue;
|
||||
names.add(path.basename(variant.settings.replace(/^~\/\.ccs\//, '')));
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
function getProfileTarget(name: string): TargetType {
|
||||
const { profiles } = listApiProfiles();
|
||||
return profiles.find((profile) => profile.name === name)?.target || 'claude';
|
||||
}
|
||||
|
||||
function readJsonObject(filePath: string): Record<string, unknown> {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
throw new Error('Settings file must contain a JSON object.');
|
||||
}
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function rollbackSettingsFile(
|
||||
filePath: string,
|
||||
previousContent: string | null,
|
||||
existedBefore: boolean
|
||||
): void {
|
||||
if (existedBefore && previousContent !== null) {
|
||||
fs.writeFileSync(filePath, previousContent, 'utf8');
|
||||
return;
|
||||
}
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
export function discoverApiProfileOrphans(): DiscoverApiProfileOrphansResult {
|
||||
const ccsDir = getCcsDir();
|
||||
if (!fs.existsSync(ccsDir)) {
|
||||
return { orphans: [] };
|
||||
}
|
||||
|
||||
const registeredSettings = getRegisteredSettingsFileNames();
|
||||
const files = fs.readdirSync(ccsDir).filter((file) => file.endsWith(SETTINGS_FILE_SUFFIX));
|
||||
const ignoredNames = new Set(['cursor.settings.json']);
|
||||
|
||||
const orphans = files
|
||||
.filter((file) => !registeredSettings.has(file))
|
||||
.filter((file) => !file.startsWith('base-'))
|
||||
.filter((file) => !ignoredNames.has(file))
|
||||
.filter((file) => !isReservedName(file.slice(0, -SETTINGS_FILE_SUFFIX.length)))
|
||||
.map((file) => {
|
||||
const name = file.slice(0, -SETTINGS_FILE_SUFFIX.length);
|
||||
const settingsPath = path.join(ccsDir, file);
|
||||
|
||||
try {
|
||||
const settings = readJsonObject(settingsPath);
|
||||
return {
|
||||
name,
|
||||
settingsPath,
|
||||
validation: validateApiProfileSettingsPayload(settings),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name,
|
||||
settingsPath,
|
||||
validation: {
|
||||
valid: false,
|
||||
issues: [
|
||||
{
|
||||
level: 'error' as const,
|
||||
code: 'invalid_json',
|
||||
message: (error as Error).message,
|
||||
field: 'settings',
|
||||
hint: 'Fix JSON syntax before registration.',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return { orphans };
|
||||
}
|
||||
|
||||
export function registerApiProfileOrphans(options?: {
|
||||
names?: string[];
|
||||
target?: TargetType;
|
||||
force?: boolean;
|
||||
}): RegisterApiProfileOrphansResult {
|
||||
const discovered = discoverApiProfileOrphans();
|
||||
const selected =
|
||||
options?.names === undefined
|
||||
? discovered.orphans
|
||||
: discovered.orphans.filter((orphan) => options.names?.includes(orphan.name));
|
||||
|
||||
const result: RegisterApiProfileOrphansResult = { registered: [], skipped: [] };
|
||||
for (const orphan of selected) {
|
||||
if (!options?.force && !orphan.validation.valid) {
|
||||
result.skipped.push({
|
||||
name: orphan.name,
|
||||
reason: 'Validation failed. Use --force to register.',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
registerApiProfileInConfig(orphan.name, options?.target || 'claude', options?.force || false);
|
||||
ensureProfileHooks(orphan.name);
|
||||
result.registered.push(orphan.name);
|
||||
} catch (error) {
|
||||
result.skipped.push({ name: orphan.name, reason: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function copyApiProfile(
|
||||
source: string,
|
||||
destination: string,
|
||||
options?: { target?: TargetType; force?: boolean }
|
||||
): CopyApiProfileResult {
|
||||
const sourceError = validateProfileNameForPath(source, 'source');
|
||||
if (sourceError) return { success: false, error: sourceError };
|
||||
|
||||
const destinationError = validateApiName(destination);
|
||||
if (destinationError) return { success: false, error: destinationError };
|
||||
|
||||
const sourceSettingsPath = getProfileSettingsPath(source);
|
||||
if (!fs.existsSync(sourceSettingsPath)) {
|
||||
return { success: false, error: `Source profile settings not found: ${source}` };
|
||||
}
|
||||
|
||||
const destinationSettingsPath = getProfileSettingsPath(destination);
|
||||
if (fs.existsSync(destinationSettingsPath) && !options?.force) {
|
||||
return { success: false, error: `Destination settings already exist: ${destination}` };
|
||||
}
|
||||
|
||||
try {
|
||||
const sourceSettings = readJsonObject(sourceSettingsPath) as Settings;
|
||||
const validation = validateApiProfileSettingsPayload(sourceSettings);
|
||||
if (!validation.valid && !options?.force) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Source profile has validation errors. Use --force to copy.',
|
||||
};
|
||||
}
|
||||
|
||||
const destinationExisted = fs.existsSync(destinationSettingsPath);
|
||||
const previousDestinationContent = destinationExisted
|
||||
? fs.readFileSync(destinationSettingsPath, 'utf8')
|
||||
: null;
|
||||
|
||||
writeJsonObjectAtomically(destinationSettingsPath, sourceSettings);
|
||||
ensureProfileHooks(destination);
|
||||
try {
|
||||
registerApiProfileInConfig(
|
||||
destination,
|
||||
options?.target || getProfileTarget(source),
|
||||
options?.force
|
||||
);
|
||||
} catch (registrationError) {
|
||||
rollbackSettingsFile(destinationSettingsPath, previousDestinationContent, destinationExisted);
|
||||
throw registrationError;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
name: destination,
|
||||
settingsPath: destinationSettingsPath,
|
||||
warnings: validation.issues
|
||||
.filter((issue) => issue.level === 'warning')
|
||||
.map((issue) => issue.message),
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export function exportApiProfile(name: string, includeSecrets = false): ExportApiProfileResult {
|
||||
const nameError = validateProfileNameForPath(name, 'profile');
|
||||
if (nameError) return { success: false, error: nameError };
|
||||
|
||||
const settingsPath = getProfileSettingsPath(name);
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
return { success: false, error: `Profile settings not found: ${name}` };
|
||||
}
|
||||
|
||||
try {
|
||||
const settings = readJsonObject(settingsPath);
|
||||
let redacted = false;
|
||||
if (!includeSecrets) {
|
||||
const env = settings.env;
|
||||
if (typeof env === 'object' && env !== null) {
|
||||
for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
|
||||
if (!isSensitiveKey(key) || typeof value !== 'string') continue;
|
||||
(env as Record<string, unknown>)[key] = REDACTED_TOKEN_SENTINEL;
|
||||
redacted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bundle: ApiProfileExportBundle = {
|
||||
schemaVersion: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
profile: {
|
||||
name,
|
||||
target: getProfileTarget(name),
|
||||
},
|
||||
settings,
|
||||
};
|
||||
|
||||
return { success: true, bundle, redacted };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export function importApiProfileBundle(
|
||||
bundle: unknown,
|
||||
options?: { name?: string; target?: TargetType; force?: boolean }
|
||||
): ImportApiProfileResult {
|
||||
if (typeof bundle !== 'object' || bundle === null || Array.isArray(bundle)) {
|
||||
return { success: false, error: 'Import bundle must be a JSON object.' };
|
||||
}
|
||||
|
||||
const input = bundle as Partial<ApiProfileExportBundle>;
|
||||
if (input.schemaVersion !== 1 || !input.profile || !input.settings) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid bundle schema. Expected schemaVersion=1 with profile and settings.',
|
||||
};
|
||||
}
|
||||
|
||||
const name = options?.name || input.profile.name;
|
||||
const nameError = validateApiName(name);
|
||||
if (nameError) return { success: false, error: nameError };
|
||||
|
||||
const settings = JSON.parse(JSON.stringify(input.settings)) as Record<string, unknown>;
|
||||
const env = settings.env as Record<string, unknown> | undefined;
|
||||
const warnings: string[] = [];
|
||||
if (env?.ANTHROPIC_AUTH_TOKEN === REDACTED_TOKEN_SENTINEL) {
|
||||
env.ANTHROPIC_AUTH_TOKEN = '';
|
||||
warnings.push('Imported bundle had redacted token. Set ANTHROPIC_AUTH_TOKEN before use.');
|
||||
}
|
||||
|
||||
const validation = validateApiProfileSettingsPayload(settings);
|
||||
if (!validation.valid && !options?.force) {
|
||||
return { success: false, error: 'Import validation failed.', validation };
|
||||
}
|
||||
|
||||
const settingsPath = getProfileSettingsPath(name);
|
||||
try {
|
||||
const settingsExisted = fs.existsSync(settingsPath);
|
||||
const previousSettingsContent = settingsExisted ? fs.readFileSync(settingsPath, 'utf8') : null;
|
||||
|
||||
writeJsonObjectAtomically(settingsPath, settings);
|
||||
ensureProfileHooks(name);
|
||||
try {
|
||||
registerApiProfileInConfig(
|
||||
name,
|
||||
options?.target || input.profile.target || 'claude',
|
||||
options?.force
|
||||
);
|
||||
} catch (registrationError) {
|
||||
rollbackSettingsFile(settingsPath, previousSettingsContent, settingsExisted);
|
||||
throw registrationError;
|
||||
}
|
||||
|
||||
warnings.push(
|
||||
...validation.issues
|
||||
.filter((issue) => issue.level === 'warning')
|
||||
.map((issue) => issue.message)
|
||||
);
|
||||
|
||||
return { success: true, name, warnings, validation };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message, validation };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Profile lifecycle validation helpers.
|
||||
*
|
||||
* Shared by orphan discovery, copy, export/import, and dashboard routes.
|
||||
*/
|
||||
|
||||
import {
|
||||
extractProviderFromPathname,
|
||||
getDeniedModelIdReasonForProvider,
|
||||
} from '../../cliproxy/model-id-normalizer';
|
||||
import { mapExternalProviderName } from '../../cliproxy/provider-capabilities';
|
||||
import type { CLIProxyProvider } from '../../cliproxy/types';
|
||||
import type { ProfileValidationIssue, ProfileValidationSummary } from './profile-types';
|
||||
|
||||
const MODEL_ENV_KEYS = [
|
||||
'ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||
] as const;
|
||||
|
||||
const ALLOWED_ANTHROPIC_ENV_KEYS = new Set<string>([
|
||||
'ANTHROPIC_BASE_URL',
|
||||
'ANTHROPIC_AUTH_TOKEN',
|
||||
'ANTHROPIC_API_KEY',
|
||||
...MODEL_ENV_KEYS,
|
||||
]);
|
||||
|
||||
function resolveProviderFromBaseUrl(baseUrl: string): CLIProxyProvider | null {
|
||||
if (!baseUrl.trim()) return null;
|
||||
|
||||
try {
|
||||
const parsed = new URL(baseUrl);
|
||||
const extracted = extractProviderFromPathname(parsed.pathname);
|
||||
return extracted ? mapExternalProviderName(extracted) : null;
|
||||
} catch {
|
||||
const extracted = extractProviderFromPathname(baseUrl);
|
||||
return extracted ? mapExternalProviderName(extracted) : null;
|
||||
}
|
||||
}
|
||||
|
||||
function pushIssue(
|
||||
issues: ProfileValidationIssue[],
|
||||
level: ProfileValidationIssue['level'],
|
||||
code: string,
|
||||
message: string,
|
||||
field?: string,
|
||||
hint?: string
|
||||
): void {
|
||||
issues.push({ level, code, message, field, hint });
|
||||
}
|
||||
|
||||
function asObject(value: unknown): Record<string, unknown> | null {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an API profile settings payload and return actionable diagnostics.
|
||||
*/
|
||||
export function validateApiProfileSettingsPayload(settings: unknown): ProfileValidationSummary {
|
||||
const issues: ProfileValidationIssue[] = [];
|
||||
const settingsObj = asObject(settings);
|
||||
|
||||
if (!settingsObj) {
|
||||
pushIssue(
|
||||
issues,
|
||||
'error',
|
||||
'invalid_settings_type',
|
||||
'Settings payload must be a JSON object.',
|
||||
'settings',
|
||||
'Expected object like { "env": { ... } }'
|
||||
);
|
||||
return { valid: false, issues };
|
||||
}
|
||||
|
||||
const envObj = asObject(settingsObj.env);
|
||||
if (!envObj) {
|
||||
pushIssue(
|
||||
issues,
|
||||
'error',
|
||||
'missing_env_object',
|
||||
'settings.env must be a JSON object.',
|
||||
'settings.env',
|
||||
'Add env keys such as ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN.'
|
||||
);
|
||||
return { valid: false, issues };
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
typeof envObj.ANTHROPIC_BASE_URL === 'string' ? envObj.ANTHROPIC_BASE_URL.trim() : '';
|
||||
if (!baseUrl) {
|
||||
pushIssue(
|
||||
issues,
|
||||
'error',
|
||||
'missing_base_url',
|
||||
'ANTHROPIC_BASE_URL is required.',
|
||||
'env.ANTHROPIC_BASE_URL',
|
||||
'Example: https://api.openai.com/v1 or provider-specific endpoint.'
|
||||
);
|
||||
}
|
||||
|
||||
const authToken =
|
||||
typeof envObj.ANTHROPIC_AUTH_TOKEN === 'string' ? envObj.ANTHROPIC_AUTH_TOKEN.trim() : '';
|
||||
if (!authToken) {
|
||||
pushIssue(
|
||||
issues,
|
||||
'warning',
|
||||
'missing_auth_token',
|
||||
'ANTHROPIC_AUTH_TOKEN is empty; profile may not run until token is configured.',
|
||||
'env.ANTHROPIC_AUTH_TOKEN',
|
||||
'Set token after import if exported in redacted mode.'
|
||||
);
|
||||
}
|
||||
|
||||
const provider = resolveProviderFromBaseUrl(baseUrl);
|
||||
for (const modelKey of MODEL_ENV_KEYS) {
|
||||
const value = envObj[modelKey];
|
||||
if (typeof value !== 'string' || value.trim().length === 0) continue;
|
||||
const denyReason = getDeniedModelIdReasonForProvider(value, provider);
|
||||
if (denyReason) {
|
||||
pushIssue(
|
||||
issues,
|
||||
'error',
|
||||
'model_denylisted',
|
||||
`${modelKey}: ${denyReason}`,
|
||||
`env.${modelKey}`,
|
||||
'Choose a supported model for the provider endpoint.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(envObj)) {
|
||||
if (key.startsWith('ANTHROPIC_') && !ALLOWED_ANTHROPIC_ENV_KEYS.has(key)) {
|
||||
pushIssue(
|
||||
issues,
|
||||
'warning',
|
||||
'unknown_anthropic_env_key',
|
||||
`Unknown ANTHROPIC env key: ${key}`,
|
||||
`env.${key}`,
|
||||
'Check for typos or provider-unsupported settings.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const hasErrors = issues.some((issue) => issue.level === 'error');
|
||||
return { valid: !hasErrors, issues };
|
||||
}
|
||||
@@ -56,3 +56,76 @@ export interface UpdateApiProfileTargetResult {
|
||||
target?: TargetType;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Validation severity for profile lifecycle checks */
|
||||
export type ProfileValidationLevel = 'error' | 'warning';
|
||||
|
||||
/** Field-level validation issue emitted by lifecycle operations */
|
||||
export interface ProfileValidationIssue {
|
||||
level: ProfileValidationLevel;
|
||||
code: string;
|
||||
message: string;
|
||||
field?: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
/** Validation summary for settings payload */
|
||||
export interface ProfileValidationSummary {
|
||||
valid: boolean;
|
||||
issues: ProfileValidationIssue[];
|
||||
}
|
||||
|
||||
/** Orphan settings file candidate discovered on disk */
|
||||
export interface ApiProfileOrphanCandidate {
|
||||
name: string;
|
||||
settingsPath: string;
|
||||
validation: ProfileValidationSummary;
|
||||
}
|
||||
|
||||
/** Discovery result for orphan settings files */
|
||||
export interface DiscoverApiProfileOrphansResult {
|
||||
orphans: ApiProfileOrphanCandidate[];
|
||||
}
|
||||
|
||||
/** Registration result for orphan settings files */
|
||||
export interface RegisterApiProfileOrphansResult {
|
||||
registered: string[];
|
||||
skipped: Array<{ name: string; reason: string }>;
|
||||
}
|
||||
|
||||
/** Copy result for API profile duplication */
|
||||
export interface CopyApiProfileResult {
|
||||
success: boolean;
|
||||
name?: string;
|
||||
settingsPath?: string;
|
||||
warnings?: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Portable export bundle schema */
|
||||
export interface ApiProfileExportBundle {
|
||||
schemaVersion: 1;
|
||||
exportedAt: string;
|
||||
profile: {
|
||||
name: string;
|
||||
target: TargetType;
|
||||
};
|
||||
settings: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Export operation result */
|
||||
export interface ExportApiProfileResult {
|
||||
success: boolean;
|
||||
bundle?: ApiProfileExportBundle;
|
||||
redacted?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Import operation result */
|
||||
export interface ImportApiProfileResult {
|
||||
success: boolean;
|
||||
name?: string;
|
||||
warnings?: string[];
|
||||
validation?: ProfileValidationSummary;
|
||||
error?: string;
|
||||
}
|
||||
Reference in new issue
Block a user