@sylphx/flow 3.19.0 → 3.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/assets/agents/builder.md +12 -3
- package/package.json +1 -3
- package/src/commands/flow/execute-v2.ts +126 -128
- package/src/commands/flow-command.ts +52 -42
- package/src/commands/hook-command.ts +161 -20
- package/src/config/index.ts +0 -20
- package/src/core/agent-loader.ts +2 -2
- package/src/core/attach-manager.ts +5 -1
- package/src/core/cleanup-handler.ts +20 -16
- package/src/core/flow-executor.ts +93 -62
- package/src/core/functional/index.ts +0 -11
- package/src/core/index.ts +1 -1
- package/src/core/project-manager.ts +14 -29
- package/src/core/secrets-manager.ts +15 -18
- package/src/core/session-manager.ts +4 -8
- package/src/core/target-manager.ts +6 -3
- package/src/core/upgrade-manager.ts +1 -1
- package/src/index.ts +1 -1
- package/src/services/auto-upgrade.ts +6 -14
- package/src/services/config-service.ts +7 -23
- package/src/services/index.ts +1 -1
- package/src/targets/claude-code.ts +14 -8
- package/src/targets/functional/claude-code-logic.ts +11 -7
- package/src/targets/opencode.ts +61 -39
- package/src/targets/shared/mcp-transforms.ts +20 -43
- package/src/types/agent.types.ts +5 -3
- package/src/types/mcp.types.ts +38 -1
- package/src/types.ts +4 -0
- package/src/utils/agent-enhancer.ts +1 -1
- package/src/utils/errors.ts +13 -0
- package/src/utils/files/file-operations.ts +16 -0
- package/src/utils/index.ts +1 -1
- package/src/core/error-handling.ts +0 -482
- package/src/core/functional/async.ts +0 -101
- package/src/core/functional/either.ts +0 -109
- package/src/core/functional/error-handler.ts +0 -135
- package/src/core/functional/pipe.ts +0 -189
- package/src/core/functional/validation.ts +0 -138
- package/src/types/mcp-config.types.ts +0 -448
- package/src/utils/error-handler.ts +0 -53
|
@@ -12,6 +12,7 @@ import os from 'node:os';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { promisify } from 'node:util';
|
|
14
14
|
import type { Target } from '../types/target.types.js';
|
|
15
|
+
import { readJsonFileSafe } from '../utils/files/file-operations.js';
|
|
15
16
|
import { targetManager } from './target-manager.js';
|
|
16
17
|
|
|
17
18
|
const execAsync = promisify(exec);
|
|
@@ -109,16 +110,11 @@ export class ProjectManager {
|
|
|
109
110
|
projectHash: string
|
|
110
111
|
): Promise<'claude-code' | 'opencode' | undefined> {
|
|
111
112
|
const prefsPath = path.join(this.flowHomeDir, 'project-preferences.json');
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const prefs = JSON.parse(await fs.readFile(prefsPath, 'utf-8'));
|
|
118
|
-
return prefs.projects?.[projectHash]?.target;
|
|
119
|
-
} catch {
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
113
|
+
const prefs = await readJsonFileSafe<{ projects?: Record<string, { target?: string }> }>(
|
|
114
|
+
prefsPath,
|
|
115
|
+
{}
|
|
116
|
+
);
|
|
117
|
+
return prefs.projects?.[projectHash]?.target as 'claude-code' | 'opencode' | undefined;
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
/**
|
|
@@ -126,15 +122,10 @@ export class ProjectManager {
|
|
|
126
122
|
*/
|
|
127
123
|
async saveProjectTargetPreference(projectHash: string, target: string): Promise<void> {
|
|
128
124
|
const prefsPath = path.join(this.flowHomeDir, 'project-preferences.json');
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
prefs = JSON.parse(await fs.readFile(prefsPath, 'utf-8'));
|
|
134
|
-
} catch {
|
|
135
|
-
// Use default
|
|
136
|
-
}
|
|
137
|
-
}
|
|
125
|
+
const prefs = await readJsonFileSafe<{ projects: Record<string, { target?: string }> }>(
|
|
126
|
+
prefsPath,
|
|
127
|
+
{ projects: {} }
|
|
128
|
+
);
|
|
138
129
|
|
|
139
130
|
if (!prefs.projects) {
|
|
140
131
|
prefs.projects = {};
|
|
@@ -187,16 +178,10 @@ export class ProjectManager {
|
|
|
187
178
|
// If both are installed, use global default
|
|
188
179
|
if (installed.claudeCode && installed.opencode) {
|
|
189
180
|
const globalSettingsPath = path.join(this.flowHomeDir, 'settings.json');
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
await this.saveProjectTargetPreference(projectHash, settings.defaultTarget);
|
|
195
|
-
return settings.defaultTarget;
|
|
196
|
-
}
|
|
197
|
-
} catch {
|
|
198
|
-
// Fall through
|
|
199
|
-
}
|
|
181
|
+
const settings = await readJsonFileSafe<{ defaultTarget?: string }>(globalSettingsPath, {});
|
|
182
|
+
if (settings.defaultTarget) {
|
|
183
|
+
await this.saveProjectTargetPreference(projectHash, settings.defaultTarget);
|
|
184
|
+
return settings.defaultTarget;
|
|
200
185
|
}
|
|
201
186
|
|
|
202
187
|
// Both installed, no global default, use claude-code
|
|
@@ -8,6 +8,7 @@ import { existsSync } from 'node:fs';
|
|
|
8
8
|
import fs from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import type { Target } from '../types/target.types.js';
|
|
11
|
+
import { readJsonFileSafe } from '../utils/files/file-operations.js';
|
|
11
12
|
import type { ProjectManager } from './project-manager.js';
|
|
12
13
|
import { resolveTargetOrId } from './target-resolver.js';
|
|
13
14
|
|
|
@@ -60,20 +61,26 @@ export class SecretsManager {
|
|
|
60
61
|
const mcpServers = config[mcpPath] as Record<string, unknown> | undefined;
|
|
61
62
|
if (mcpServers) {
|
|
62
63
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
63
|
-
|
|
64
|
+
if (typeof serverConfig !== 'object' || serverConfig === null) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const server = serverConfig as Record<string, unknown>;
|
|
64
68
|
|
|
65
69
|
// Extract env vars (sensitive) - handle both 'env' and 'environment' keys
|
|
66
|
-
const envVars = server.env || server.environment;
|
|
67
|
-
if (envVars && Object.keys(envVars).length > 0) {
|
|
70
|
+
const envVars = (server.env || server.environment) as Record<string, string> | undefined;
|
|
71
|
+
if (envVars && typeof envVars === 'object' && Object.keys(envVars).length > 0) {
|
|
68
72
|
secrets.servers[serverName] = {
|
|
69
73
|
env: envVars,
|
|
70
74
|
};
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
// Extract args (may contain secrets) - handle both 'args' and 'command' array
|
|
74
|
-
const args =
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
const args = Array.isArray(server.args)
|
|
79
|
+
? server.args
|
|
80
|
+
: Array.isArray(server.command)
|
|
81
|
+
? (server.command as string[]).slice(1)
|
|
82
|
+
: undefined;
|
|
83
|
+
if (args && args.length > 0) {
|
|
77
84
|
if (!secrets.servers[serverName]) {
|
|
78
85
|
secrets.servers[serverName] = {};
|
|
79
86
|
}
|
|
@@ -105,20 +112,10 @@ export class SecretsManager {
|
|
|
105
112
|
/**
|
|
106
113
|
* Load secrets from storage
|
|
107
114
|
*/
|
|
108
|
-
|
|
115
|
+
loadSecrets(projectHash: string): Promise<MCPSecrets | null> {
|
|
109
116
|
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
110
117
|
const secretsPath = path.join(paths.secretsDir, 'mcp-env.json');
|
|
111
|
-
|
|
112
|
-
if (!existsSync(secretsPath)) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
const data = await fs.readFile(secretsPath, 'utf-8');
|
|
118
|
-
return JSON.parse(data);
|
|
119
|
-
} catch {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
118
|
+
return readJsonFileSafe<MCPSecrets | null>(secretsPath, null);
|
|
122
119
|
}
|
|
123
120
|
|
|
124
121
|
/**
|
|
@@ -8,6 +8,7 @@ import { existsSync } from 'node:fs';
|
|
|
8
8
|
import fs from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import type { Target } from '../types/target.types.js';
|
|
11
|
+
import { readJsonFileSafe } from '../utils/files/file-operations.js';
|
|
11
12
|
import type { ProjectManager } from './project-manager.js';
|
|
12
13
|
|
|
13
14
|
export interface Session {
|
|
@@ -143,14 +144,9 @@ export class SessionManager {
|
|
|
143
144
|
/**
|
|
144
145
|
* Get active session for a project
|
|
145
146
|
*/
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const data = await fs.readFile(paths.sessionFile, 'utf-8');
|
|
150
|
-
return JSON.parse(data);
|
|
151
|
-
} catch {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
147
|
+
getActiveSession(projectHash: string): Promise<Session | null> {
|
|
148
|
+
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
149
|
+
return readJsonFileSafe<Session | null>(paths.sessionFile, null);
|
|
154
150
|
}
|
|
155
151
|
|
|
156
152
|
/**
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
getTargetsWithMCPSupport,
|
|
9
9
|
isTargetImplemented,
|
|
10
10
|
} from '../config/targets.js';
|
|
11
|
-
import {
|
|
11
|
+
import { isSome, match } from '../core/functional/option.js';
|
|
12
12
|
import { projectSettings } from '../utils/config/settings.js';
|
|
13
13
|
import { promptSelect } from '../utils/prompts/index.js';
|
|
14
14
|
|
|
@@ -85,9 +85,12 @@ export function createTargetManager(): TargetManager {
|
|
|
85
85
|
message: 'Select target platform:',
|
|
86
86
|
options: availableTargets.map((id) => {
|
|
87
87
|
const targetOption = getTarget(id);
|
|
88
|
-
const
|
|
88
|
+
const label = match(
|
|
89
|
+
(t: ReturnType<typeof getAllTargets>[number]) => t.name || id,
|
|
90
|
+
() => id
|
|
91
|
+
)(targetOption);
|
|
89
92
|
return {
|
|
90
|
-
label
|
|
93
|
+
label,
|
|
91
94
|
value: id,
|
|
92
95
|
};
|
|
93
96
|
}),
|
|
@@ -5,7 +5,7 @@ import { promisify } from 'node:util';
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { getProjectSettingsFile } from '../config/constants.js';
|
|
7
7
|
import type { Target } from '../types/target.types.js';
|
|
8
|
-
import { CLIError } from '../utils/
|
|
8
|
+
import { CLIError } from '../utils/errors.js';
|
|
9
9
|
import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
|
|
10
10
|
import { createSpinner, log } from '../utils/prompts/index.js';
|
|
11
11
|
import type { ProjectState } from './state-detector.js';
|
package/src/index.ts
CHANGED
|
@@ -126,7 +126,7 @@ function handleCommandError(error: unknown): void {
|
|
|
126
126
|
|
|
127
127
|
// Handle Commander.js specific errors
|
|
128
128
|
if (error.name === 'CommanderError') {
|
|
129
|
-
const commanderError = error as
|
|
129
|
+
const commanderError = error as Error & { code?: string; exitCode?: number };
|
|
130
130
|
|
|
131
131
|
// Don't exit for help or version commands - they should already be handled
|
|
132
132
|
if (commanderError.code === 'commander.help' || commanderError.code === 'commander.version') {
|
|
@@ -10,6 +10,7 @@ import os from 'node:os';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { promisify } from 'node:util';
|
|
13
|
+
import { readJsonFileSafe } from '../utils/files/file-operations.js';
|
|
13
14
|
import { getUpgradeCommand } from '../utils/package-manager-detector.js';
|
|
14
15
|
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -91,13 +92,8 @@ export class AutoUpgrade {
|
|
|
91
92
|
/**
|
|
92
93
|
* Read version info from disk
|
|
93
94
|
*/
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
const data = await fs.readFile(VERSION_FILE, 'utf-8');
|
|
97
|
-
return JSON.parse(data);
|
|
98
|
-
} catch {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
95
|
+
private readVersionInfo(): Promise<VersionInfo | null> {
|
|
96
|
+
return readJsonFileSafe<VersionInfo | null>(VERSION_FILE, null);
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
/**
|
|
@@ -127,13 +123,9 @@ export class AutoUpgrade {
|
|
|
127
123
|
* Get current Flow version from package.json
|
|
128
124
|
*/
|
|
129
125
|
private async getCurrentVersion(): Promise<string> {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return packageJson.version;
|
|
134
|
-
} catch {
|
|
135
|
-
return 'unknown';
|
|
136
|
-
}
|
|
126
|
+
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
|
|
127
|
+
const pkg = await readJsonFileSafe<{ version?: string }>(packageJsonPath, {});
|
|
128
|
+
return pkg.version ?? 'unknown';
|
|
137
129
|
}
|
|
138
130
|
|
|
139
131
|
/**
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getProjectSettingsFile,
|
|
12
12
|
USER_SETTINGS_FILE,
|
|
13
13
|
} from '../config/constants.js';
|
|
14
|
+
import { readJsonFileSafe } from '../utils/files/file-operations.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* User configuration (sensitive data, saved to home directory)
|
|
@@ -102,13 +103,8 @@ export class ConfigService {
|
|
|
102
103
|
/**
|
|
103
104
|
* Load user global settings (mainly for API keys)
|
|
104
105
|
*/
|
|
105
|
-
static
|
|
106
|
-
|
|
107
|
-
const content = await fs.readFile(USER_SETTINGS_FILE, 'utf-8');
|
|
108
|
-
return JSON.parse(content);
|
|
109
|
-
} catch {
|
|
110
|
-
return {};
|
|
111
|
-
}
|
|
106
|
+
static loadHomeSettings(): Promise<UserSettings> {
|
|
107
|
+
return readJsonFileSafe<UserSettings>(USER_SETTINGS_FILE, {});
|
|
112
108
|
}
|
|
113
109
|
|
|
114
110
|
/**
|
|
@@ -154,14 +150,8 @@ export class ConfigService {
|
|
|
154
150
|
/**
|
|
155
151
|
* Load project-level settings
|
|
156
152
|
*/
|
|
157
|
-
static
|
|
158
|
-
|
|
159
|
-
const configPath = getProjectSettingsFile(cwd);
|
|
160
|
-
const content = await fs.readFile(configPath, 'utf-8');
|
|
161
|
-
return JSON.parse(content);
|
|
162
|
-
} catch {
|
|
163
|
-
return {};
|
|
164
|
-
}
|
|
153
|
+
static loadProjectSettings(cwd: string = process.cwd()): Promise<ProjectSettings> {
|
|
154
|
+
return readJsonFileSafe<ProjectSettings>(getProjectSettingsFile(cwd), {});
|
|
165
155
|
}
|
|
166
156
|
|
|
167
157
|
/**
|
|
@@ -186,14 +176,8 @@ export class ConfigService {
|
|
|
186
176
|
/**
|
|
187
177
|
* Load project-local settings (overrides everything)
|
|
188
178
|
*/
|
|
189
|
-
static
|
|
190
|
-
|
|
191
|
-
const configPath = getProjectLocalSettingsFile(cwd);
|
|
192
|
-
const content = await fs.readFile(configPath, 'utf-8');
|
|
193
|
-
return JSON.parse(content);
|
|
194
|
-
} catch {
|
|
195
|
-
return {};
|
|
196
|
-
}
|
|
179
|
+
static loadLocalSettings(cwd: string = process.cwd()): Promise<RuntimeChoices> {
|
|
180
|
+
return readJsonFileSafe<RuntimeChoices>(getProjectLocalSettingsFile(cwd), {});
|
|
197
181
|
}
|
|
198
182
|
|
|
199
183
|
/**
|
package/src/services/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
pathUtils,
|
|
12
12
|
yamlUtils,
|
|
13
13
|
} from '../utils/config/target-utils.js';
|
|
14
|
-
import { CLIError } from '../utils/
|
|
14
|
+
import { CLIError } from '../utils/errors.js';
|
|
15
15
|
import { sanitize } from '../utils/security/security.js';
|
|
16
16
|
import { DEFAULT_CLAUDE_CODE_ENV } from './functional/claude-code-logic.js';
|
|
17
17
|
import {
|
|
@@ -37,6 +37,11 @@ interface ProcessExitError extends Error {
|
|
|
37
37
|
code: number | null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Type guard for Node.js errors with errno/code properties */
|
|
41
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
42
|
+
return error instanceof Error && 'code' in error;
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
/**
|
|
41
46
|
* Claude Code target - composition approach with all original functionality
|
|
42
47
|
*/
|
|
@@ -296,16 +301,18 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
296
301
|
});
|
|
297
302
|
});
|
|
298
303
|
} catch (error: unknown) {
|
|
299
|
-
if (error
|
|
300
|
-
|
|
301
|
-
if (errWithCode.code === 'ENOENT') {
|
|
304
|
+
if (isNodeError(error)) {
|
|
305
|
+
if (error.code === 'ENOENT') {
|
|
302
306
|
throw new CLIError('Claude Code not found. Please install it first.', 'CLAUDE_NOT_FOUND');
|
|
303
307
|
}
|
|
304
|
-
if (
|
|
305
|
-
throw new CLIError(`Claude Code exited with code ${
|
|
308
|
+
if (error.code !== undefined) {
|
|
309
|
+
throw new CLIError(`Claude Code exited with code ${error.code}`, 'CLAUDE_ERROR');
|
|
306
310
|
}
|
|
307
311
|
throw new CLIError(`Failed to execute Claude Code: ${error.message}`, 'CLAUDE_ERROR');
|
|
308
312
|
}
|
|
313
|
+
if (error instanceof Error) {
|
|
314
|
+
throw new CLIError(`Failed to execute Claude Code: ${error.message}`, 'CLAUDE_ERROR');
|
|
315
|
+
}
|
|
309
316
|
throw new CLIError(`Failed to execute Claude Code: ${String(error)}`, 'CLAUDE_ERROR');
|
|
310
317
|
}
|
|
311
318
|
},
|
|
@@ -331,8 +338,7 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
331
338
|
const content = await fsPromises.readFile(settingsPath, 'utf8');
|
|
332
339
|
settings = JSON.parse(content);
|
|
333
340
|
} catch (error: unknown) {
|
|
334
|
-
|
|
335
|
-
if (err.code !== 'ENOENT') {
|
|
341
|
+
if (!isNodeError(error) || error.code !== 'ENOENT') {
|
|
336
342
|
throw error;
|
|
337
343
|
}
|
|
338
344
|
// File doesn't exist, will create new
|
|
@@ -66,6 +66,7 @@ const DEFAULT_CLAUDE_CODE_SETTINGS: Partial<ClaudeCodeSettings> = {
|
|
|
66
66
|
|
|
67
67
|
export interface HookConfig {
|
|
68
68
|
notificationCommand?: string;
|
|
69
|
+
sessionStartCommand?: string;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/**
|
|
@@ -75,15 +76,16 @@ export interface HookConfig {
|
|
|
75
76
|
export const generateHookCommands = async (targetId: string): Promise<HookConfig> => {
|
|
76
77
|
return {
|
|
77
78
|
notificationCommand: `sylphx-flow hook --type notification --target ${targetId}`,
|
|
79
|
+
sessionStartCommand: `sylphx-flow hook --type session-start --target ${targetId}`,
|
|
78
80
|
};
|
|
79
81
|
};
|
|
80
82
|
|
|
81
83
|
/**
|
|
82
84
|
* Default hook commands (fallback)
|
|
83
|
-
* Simplified to only include notification hook
|
|
84
85
|
*/
|
|
85
86
|
const DEFAULT_HOOKS: HookConfig = {
|
|
86
87
|
notificationCommand: 'sylphx-flow hook --type notification --target claude-code',
|
|
88
|
+
sessionStartCommand: 'sylphx-flow hook --type session-start --target claude-code',
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
/**
|
|
@@ -94,17 +96,19 @@ export const processSettings = (
|
|
|
94
96
|
hookConfig: HookConfig = DEFAULT_HOOKS
|
|
95
97
|
): Result<string, ConfigError> => {
|
|
96
98
|
const notificationCommand = hookConfig.notificationCommand || DEFAULT_HOOKS.notificationCommand!;
|
|
99
|
+
const sessionStartCommand = hookConfig.sessionStartCommand || DEFAULT_HOOKS.sessionStartCommand!;
|
|
97
100
|
|
|
98
101
|
const hookConfiguration: ClaudeCodeSettings['hooks'] = {
|
|
99
102
|
Notification: [
|
|
100
103
|
{
|
|
101
104
|
matcher: '',
|
|
102
|
-
hooks: [
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
hooks: [{ type: 'command', command: notificationCommand }],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
SessionStart: [
|
|
109
|
+
{
|
|
110
|
+
matcher: '',
|
|
111
|
+
hooks: [{ type: 'command', command: sessionStartCommand }],
|
|
108
112
|
},
|
|
109
113
|
],
|
|
110
114
|
};
|
package/src/targets/opencode.ts
CHANGED
|
@@ -2,8 +2,13 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { MCP_SERVER_REGISTRY } from '../config/servers.js';
|
|
3
3
|
import type { AgentMetadata } from '../types/target-config.types.js';
|
|
4
4
|
import type { MCPServerConfigUnion, Target } from '../types.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
type ConfigData,
|
|
7
|
+
fileUtils,
|
|
8
|
+
generateHelpText,
|
|
9
|
+
yamlUtils,
|
|
10
|
+
} from '../utils/config/target-utils.js';
|
|
11
|
+
import { CLIError } from '../utils/errors.js';
|
|
7
12
|
import { secretUtils } from '../utils/security/secret-utils.js';
|
|
8
13
|
import {
|
|
9
14
|
detectTargetConfig,
|
|
@@ -11,6 +16,49 @@ import {
|
|
|
11
16
|
transformMCPConfig as transformMCP,
|
|
12
17
|
} from './shared/index.js';
|
|
13
18
|
|
|
19
|
+
/** OpenCode configuration data with MCP config */
|
|
20
|
+
interface OpenCodeConfigData extends ConfigData {
|
|
21
|
+
mcp?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert secret environment variables in a single MCP server config to file references
|
|
26
|
+
*/
|
|
27
|
+
async function convertServerSecrets(
|
|
28
|
+
cwd: string,
|
|
29
|
+
serverId: string,
|
|
30
|
+
serverConfig: Record<string, unknown>
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
if (!serverConfig || typeof serverConfig !== 'object' || !('environment' in serverConfig)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const envVars = serverConfig.environment as Record<string, string>;
|
|
37
|
+
if (!envVars || typeof envVars !== 'object') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const serverDef = Object.values(MCP_SERVER_REGISTRY).find((s) => s.name === serverId);
|
|
42
|
+
if (!serverDef?.envVars) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const secretEnvVars: Record<string, string> = {};
|
|
47
|
+
const nonSecretEnvVars: Record<string, string> = {};
|
|
48
|
+
|
|
49
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
50
|
+
const envConfig = serverDef.envVars[key];
|
|
51
|
+
if (envConfig?.secret && value && !secretUtils.isFileReference(value)) {
|
|
52
|
+
secretEnvVars[key] = value;
|
|
53
|
+
} else {
|
|
54
|
+
nonSecretEnvVars[key] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const convertedSecrets = await secretUtils.convertSecretsToFileReferences(cwd, secretEnvVars);
|
|
59
|
+
serverConfig.environment = { ...nonSecretEnvVars, ...convertedSecrets };
|
|
60
|
+
}
|
|
61
|
+
|
|
14
62
|
/**
|
|
15
63
|
* OpenCode target - composition approach with all original functionality
|
|
16
64
|
*/
|
|
@@ -92,11 +140,14 @@ export const opencodeTarget: Target = {
|
|
|
92
140
|
/**
|
|
93
141
|
* Read OpenCode configuration with structure normalization
|
|
94
142
|
*/
|
|
95
|
-
async readConfig(cwd: string): Promise<
|
|
143
|
+
async readConfig(cwd: string): Promise<OpenCodeConfigData> {
|
|
96
144
|
const config = await fileUtils.readConfig(opencodeTarget.config, cwd);
|
|
97
145
|
|
|
98
146
|
// Resolve any file references in the configuration
|
|
99
|
-
const resolvedConfig = await secretUtils.resolveFileReferences(
|
|
147
|
+
const resolvedConfig = (await secretUtils.resolveFileReferences(
|
|
148
|
+
cwd,
|
|
149
|
+
config
|
|
150
|
+
)) as OpenCodeConfigData;
|
|
100
151
|
|
|
101
152
|
// Ensure the config has the expected structure
|
|
102
153
|
if (!resolvedConfig.mcp) {
|
|
@@ -110,46 +161,17 @@ export const opencodeTarget: Target = {
|
|
|
110
161
|
* Write OpenCode configuration with structure normalization
|
|
111
162
|
*/
|
|
112
163
|
async writeConfig(cwd: string, config: Record<string, unknown>): Promise<void> {
|
|
113
|
-
// Ensure the config has the expected structure for OpenCode
|
|
114
164
|
if (!config.mcp) {
|
|
115
165
|
config.mcp = {};
|
|
116
166
|
}
|
|
117
167
|
|
|
118
|
-
// Convert secrets to file references if secret files are enabled
|
|
119
168
|
if (opencodeTarget.config.installation?.useSecretFiles) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const serverDef = Object.values(MCP_SERVER_REGISTRY).find((s) => s.name === serverId);
|
|
127
|
-
if (serverDef?.envVars) {
|
|
128
|
-
// Separate secret and non-secret variables
|
|
129
|
-
const secretEnvVars: Record<string, string> = {};
|
|
130
|
-
const nonSecretEnvVars: Record<string, string> = {};
|
|
131
|
-
|
|
132
|
-
for (const [key, value] of Object.entries(envVars)) {
|
|
133
|
-
const envConfig = serverDef.envVars[key];
|
|
134
|
-
if (envConfig?.secret && value && !secretUtils.isFileReference(value)) {
|
|
135
|
-
secretEnvVars[key] = value;
|
|
136
|
-
} else {
|
|
137
|
-
nonSecretEnvVars[key] = value;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Convert only secret variables
|
|
142
|
-
const convertedSecrets = await secretUtils.convertSecretsToFileReferences(
|
|
143
|
-
cwd,
|
|
144
|
-
secretEnvVars
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// Merge back
|
|
148
|
-
serverConfig.environment = { ...nonSecretEnvVars, ...convertedSecrets };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
169
|
+
const mcpServers = config.mcp as Record<string, Record<string, unknown>>;
|
|
170
|
+
await Promise.all(
|
|
171
|
+
Object.entries(mcpServers).map(([serverId, serverConfig]) =>
|
|
172
|
+
convertServerSecrets(cwd, serverId, serverConfig)
|
|
173
|
+
)
|
|
174
|
+
);
|
|
153
175
|
}
|
|
154
176
|
|
|
155
177
|
await fileUtils.writeConfig(opencodeTarget.config, cwd, config);
|