feat: provider config management (#193)
* feat: support scoped removal of provider config (auth, user, project, custom) * feat: implement UI session token management with cookies for window visibility control
This commit is contained in:
committed by
GitHub
parent
d4f1d8abbf
commit
05caf4cc58
@@ -3,8 +3,8 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import { type OpenCodeManager } from './opencode';
|
||||
import { createAgent, createCommand, deleteAgent, deleteCommand, getAgentSources, getCommandSources, updateAgent, updateCommand, type AgentScope, type CommandScope, AGENT_SCOPE, COMMAND_SCOPE, discoverSkills, getSkillSources, createSkill, updateSkill, deleteSkill, readSkillSupportingFile, writeSkillSupportingFile, deleteSkillSupportingFile, type SkillScope, SKILL_SCOPE } from './opencodeConfig';
|
||||
import { removeProviderAuth } from './opencodeAuth';
|
||||
import { createAgent, createCommand, deleteAgent, deleteCommand, getAgentSources, getCommandSources, updateAgent, updateCommand, type AgentScope, type CommandScope, AGENT_SCOPE, COMMAND_SCOPE, discoverSkills, getSkillSources, createSkill, updateSkill, deleteSkill, readSkillSupportingFile, writeSkillSupportingFile, deleteSkillSupportingFile, type SkillScope, SKILL_SCOPE, getProviderSources, removeProviderConfig } from './opencodeConfig';
|
||||
import { getProviderAuth, removeProviderAuth } from './opencodeAuth';
|
||||
import * as gitService from './gitService';
|
||||
import {
|
||||
getSkillsCatalog,
|
||||
@@ -1293,13 +1293,31 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:provider/auth:delete': {
|
||||
const { providerId } = (payload || {}) as { providerId?: string };
|
||||
case 'api:provider/auth:delete': {
|
||||
const { providerId, scope } = (payload || {}) as { providerId?: string; scope?: string };
|
||||
if (!providerId) {
|
||||
return { id, type, success: false, error: 'Provider ID is required' };
|
||||
}
|
||||
const normalizedScope = typeof scope === 'string' ? scope : 'auth';
|
||||
try {
|
||||
const removed = removeProviderAuth(providerId);
|
||||
let removed = false;
|
||||
if (normalizedScope === 'auth') {
|
||||
removed = removeProviderAuth(providerId);
|
||||
} else if (normalizedScope === 'user' || normalizedScope === 'project' || normalizedScope === 'custom') {
|
||||
removed = removeProviderConfig(providerId, ctx?.manager?.getWorkingDirectory(), normalizedScope);
|
||||
} else if (normalizedScope === 'all') {
|
||||
const workingDirectory = ctx?.manager?.getWorkingDirectory();
|
||||
const authRemoved = removeProviderAuth(providerId);
|
||||
const userRemoved = removeProviderConfig(providerId, workingDirectory, 'user');
|
||||
const projectRemoved = workingDirectory
|
||||
? removeProviderConfig(providerId, workingDirectory, 'project')
|
||||
: false;
|
||||
const customRemoved = removeProviderConfig(providerId, workingDirectory, 'custom');
|
||||
removed = authRemoved || userRemoved || projectRemoved || customRemoved;
|
||||
} else {
|
||||
return { id, type, success: false, error: 'Invalid scope' };
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
await ctx?.manager?.restart();
|
||||
}
|
||||
@@ -1323,6 +1341,23 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:provider/source:get': {
|
||||
const { providerId } = (payload || {}) as { providerId?: string };
|
||||
if (!providerId) {
|
||||
return { id, type, success: false, error: 'Provider ID is required' };
|
||||
}
|
||||
try {
|
||||
const sources = getProviderSources(providerId, ctx?.manager?.getWorkingDirectory());
|
||||
const auth = getProviderAuth(providerId);
|
||||
sources.auth.exists = Boolean(auth);
|
||||
return { id, type, success: true, data: { providerId, sources } };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return { id, type, success: false, error: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case 'vscode:command': {
|
||||
const { command, args } = (payload || {}) as { command?: string; args?: unknown[] };
|
||||
if (!command) {
|
||||
|
||||
@@ -259,6 +259,13 @@ const readConfigLayers = (workingDirectory?: string) => {
|
||||
const readConfig = (workingDirectory?: string): Record<string, unknown> =>
|
||||
readConfigLayers(workingDirectory).mergedConfig;
|
||||
|
||||
const getConfigForPath = (layers: ReturnType<typeof readConfigLayers>, targetPath?: string | null) => {
|
||||
if (!targetPath) return layers.userConfig;
|
||||
if (layers.paths.customPath && targetPath === layers.paths.customPath) return layers.customConfig;
|
||||
if (layers.paths.projectPath && targetPath === layers.paths.projectPath) return layers.projectConfig;
|
||||
return layers.userConfig;
|
||||
};
|
||||
|
||||
const writeConfig = (config: Record<string, unknown>, filePath: string = CONFIG_FILE) => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const backupFile = `${filePath}.openchamber.backup`;
|
||||
@@ -732,6 +739,99 @@ export const updateCommand = (commandName: string, updates: Record<string, unkno
|
||||
}
|
||||
};
|
||||
|
||||
export const getProviderSources = (providerId: string, workingDirectory?: string) => {
|
||||
const layers = readConfigLayers(workingDirectory);
|
||||
const customProviders = isPlainObject((layers.customConfig as Record<string, unknown>)?.provider)
|
||||
? (layers.customConfig as Record<string, unknown>).provider as Record<string, unknown>
|
||||
: {};
|
||||
const customProvidersAlias = isPlainObject((layers.customConfig as Record<string, unknown>)?.providers)
|
||||
? (layers.customConfig as Record<string, unknown>).providers as Record<string, unknown>
|
||||
: {};
|
||||
const projectProviders = isPlainObject((layers.projectConfig as Record<string, unknown>)?.provider)
|
||||
? (layers.projectConfig as Record<string, unknown>).provider as Record<string, unknown>
|
||||
: {};
|
||||
const projectProvidersAlias = isPlainObject((layers.projectConfig as Record<string, unknown>)?.providers)
|
||||
? (layers.projectConfig as Record<string, unknown>).providers as Record<string, unknown>
|
||||
: {};
|
||||
const userProviders = isPlainObject((layers.userConfig as Record<string, unknown>)?.provider)
|
||||
? (layers.userConfig as Record<string, unknown>).provider as Record<string, unknown>
|
||||
: {};
|
||||
const userProvidersAlias = isPlainObject((layers.userConfig as Record<string, unknown>)?.providers)
|
||||
? (layers.userConfig as Record<string, unknown>).providers as Record<string, unknown>
|
||||
: {};
|
||||
|
||||
const customExists = Object.prototype.hasOwnProperty.call(customProviders, providerId)
|
||||
|| Object.prototype.hasOwnProperty.call(customProvidersAlias, providerId);
|
||||
const projectExists = Object.prototype.hasOwnProperty.call(projectProviders, providerId)
|
||||
|| Object.prototype.hasOwnProperty.call(projectProvidersAlias, providerId);
|
||||
const userExists = Object.prototype.hasOwnProperty.call(userProviders, providerId)
|
||||
|| Object.prototype.hasOwnProperty.call(userProvidersAlias, providerId);
|
||||
|
||||
return {
|
||||
auth: { exists: false },
|
||||
user: { exists: userExists, path: layers.paths.userPath },
|
||||
project: { exists: projectExists, path: layers.paths.projectPath ?? null },
|
||||
custom: { exists: customExists, path: layers.paths.customPath },
|
||||
};
|
||||
};
|
||||
|
||||
export const removeProviderConfig = (providerId: string, workingDirectory?: string, scope: 'user' | 'project' | 'custom' = 'user') => {
|
||||
if (!providerId) throw new Error('Provider ID is required');
|
||||
|
||||
const layers = readConfigLayers(workingDirectory);
|
||||
let targetPath: string | null | undefined = layers.paths.userPath;
|
||||
|
||||
if (scope === 'project') {
|
||||
if (!workingDirectory) {
|
||||
throw new Error('Working directory is required for project scope');
|
||||
}
|
||||
targetPath = layers.paths.projectPath ?? targetPath;
|
||||
}
|
||||
|
||||
if (scope === 'custom') {
|
||||
if (!layers.paths.customPath) {
|
||||
return false;
|
||||
}
|
||||
targetPath = layers.paths.customPath;
|
||||
}
|
||||
|
||||
const targetConfig = getConfigForPath(layers, targetPath);
|
||||
const providerConfig = isPlainObject((targetConfig as Record<string, unknown>).provider)
|
||||
? (targetConfig as Record<string, unknown>).provider as Record<string, unknown>
|
||||
: {};
|
||||
const providersConfig = isPlainObject((targetConfig as Record<string, unknown>).providers)
|
||||
? (targetConfig as Record<string, unknown>).providers as Record<string, unknown>
|
||||
: {};
|
||||
|
||||
const removedProvider = Object.prototype.hasOwnProperty.call(providerConfig, providerId);
|
||||
const removedProviders = Object.prototype.hasOwnProperty.call(providersConfig, providerId);
|
||||
|
||||
if (!removedProvider && !removedProviders) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (removedProvider) {
|
||||
delete providerConfig[providerId];
|
||||
if (Object.keys(providerConfig).length === 0) {
|
||||
delete (targetConfig as Record<string, unknown>).provider;
|
||||
} else {
|
||||
(targetConfig as Record<string, unknown>).provider = providerConfig;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedProviders) {
|
||||
delete providersConfig[providerId];
|
||||
if (Object.keys(providersConfig).length === 0) {
|
||||
delete (targetConfig as Record<string, unknown>).providers;
|
||||
} else {
|
||||
(targetConfig as Record<string, unknown>).providers = providersConfig;
|
||||
}
|
||||
}
|
||||
|
||||
writeConfig(targetConfig as Record<string, unknown>, targetPath || CONFIG_FILE);
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deleteCommand = (commandName: string, workingDirectory?: string) => {
|
||||
let deleted = false;
|
||||
|
||||
|
||||
@@ -613,8 +613,22 @@ const handleLocalApiRequest = async (url: URL, init?: RequestInit) => {
|
||||
const providerAuthMatch = pathname.match(/^\/api\/provider\/([^/]+)\/auth$/);
|
||||
if (providerAuthMatch && (init?.method || 'GET').toUpperCase() === 'DELETE') {
|
||||
const providerId = decodeURIComponent(providerAuthMatch[1]);
|
||||
const scope = url.searchParams.get('scope') || 'auth';
|
||||
try {
|
||||
const data = await sendBridgeMessage('api:provider/auth:delete', { providerId });
|
||||
const data = await sendBridgeMessage('api:provider/auth:delete', { providerId, scope });
|
||||
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return new Response(JSON.stringify({ error: message }), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
}
|
||||
|
||||
// Handle provider source lookup: GET /api/provider/:providerId/source
|
||||
const providerSourceMatch = pathname.match(/^\/api\/provider\/([^/]+)\/source$/);
|
||||
if (providerSourceMatch && (init?.method || 'GET').toUpperCase() === 'GET') {
|
||||
const providerId = decodeURIComponent(providerSourceMatch[1]);
|
||||
try {
|
||||
const data = await sendBridgeMessage('api:provider/source:get', { providerId });
|
||||
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
Reference in New Issue
Block a user