@sylphx/flow 1.8.1 → 2.0.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 +79 -0
- package/UPGRADE.md +140 -0
- package/assets/output-styles/silent.md +4 -0
- package/package.json +2 -1
- package/src/commands/flow/execute-v2.ts +278 -0
- package/src/commands/flow/execute.ts +1 -18
- package/src/commands/flow/types.ts +3 -2
- package/src/commands/flow-command.ts +32 -69
- package/src/commands/flow-orchestrator.ts +18 -55
- package/src/commands/run-command.ts +12 -6
- package/src/commands/settings-command.ts +529 -0
- package/src/core/attach-manager.ts +482 -0
- package/src/core/backup-manager.ts +308 -0
- package/src/core/cleanup-handler.ts +166 -0
- package/src/core/flow-executor.ts +323 -0
- package/src/core/git-stash-manager.ts +133 -0
- package/src/core/project-manager.ts +274 -0
- package/src/core/secrets-manager.ts +229 -0
- package/src/core/session-manager.ts +268 -0
- package/src/core/template-loader.ts +189 -0
- package/src/core/upgrade-manager.ts +79 -47
- package/src/index.ts +13 -27
- package/src/services/first-run-setup.ts +220 -0
- package/src/services/global-config.ts +337 -0
- package/src/utils/__tests__/package-manager-detector.test.ts +163 -0
- package/src/utils/agent-enhancer.ts +40 -22
- package/src/utils/errors.ts +9 -0
- package/src/utils/package-manager-detector.ts +139 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Manager
|
|
3
|
+
* Manages project identification and paths for multi-project support
|
|
4
|
+
* All projects store data in ~/.sylphx-flow/ isolated by project hash
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
|
|
13
|
+
export interface ProjectPaths {
|
|
14
|
+
sessionFile: string;
|
|
15
|
+
backupsDir: string;
|
|
16
|
+
secretsDir: string;
|
|
17
|
+
latestBackup: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ProjectManager {
|
|
21
|
+
private readonly flowHomeDir: string;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
// All Flow data stored in ~/.sylphx-flow/
|
|
25
|
+
this.flowHomeDir = path.join(os.homedir(), '.sylphx-flow');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get unique hash for project path
|
|
30
|
+
* Uses first 16 chars of SHA256 hash of absolute path
|
|
31
|
+
*/
|
|
32
|
+
getProjectHash(projectPath: string): string {
|
|
33
|
+
const absolutePath = path.resolve(projectPath);
|
|
34
|
+
return crypto
|
|
35
|
+
.createHash('sha256')
|
|
36
|
+
.update(absolutePath)
|
|
37
|
+
.digest('hex')
|
|
38
|
+
.substring(0, 16);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get all paths for a project
|
|
43
|
+
*/
|
|
44
|
+
getProjectPaths(projectHash: string): ProjectPaths {
|
|
45
|
+
const sessionsDir = path.join(this.flowHomeDir, 'sessions');
|
|
46
|
+
const backupsDir = path.join(this.flowHomeDir, 'backups', projectHash);
|
|
47
|
+
const secretsDir = path.join(this.flowHomeDir, 'secrets', projectHash);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
sessionFile: path.join(sessionsDir, `${projectHash}.json`),
|
|
51
|
+
backupsDir,
|
|
52
|
+
secretsDir,
|
|
53
|
+
latestBackup: path.join(backupsDir, 'latest'),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get Flow home directory
|
|
59
|
+
*/
|
|
60
|
+
getFlowHomeDir(): string {
|
|
61
|
+
return this.flowHomeDir;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Initialize Flow directories
|
|
66
|
+
*/
|
|
67
|
+
async initialize(): Promise<void> {
|
|
68
|
+
const dirs = [
|
|
69
|
+
this.flowHomeDir,
|
|
70
|
+
path.join(this.flowHomeDir, 'sessions'),
|
|
71
|
+
path.join(this.flowHomeDir, 'backups'),
|
|
72
|
+
path.join(this.flowHomeDir, 'secrets'),
|
|
73
|
+
path.join(this.flowHomeDir, 'templates'),
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
for (const dir of dirs) {
|
|
77
|
+
await fs.mkdir(dir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if a command is available on the system
|
|
83
|
+
*/
|
|
84
|
+
private async isCommandAvailable(command: string): Promise<boolean> {
|
|
85
|
+
try {
|
|
86
|
+
const { execSync } = await import('node:child_process');
|
|
87
|
+
execSync(`which ${command}`, { stdio: 'ignore' });
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Detect which commands are installed on this machine
|
|
96
|
+
*/
|
|
97
|
+
private async detectInstalledCommands(): Promise<{
|
|
98
|
+
claudeCode: boolean;
|
|
99
|
+
opencode: boolean;
|
|
100
|
+
}> {
|
|
101
|
+
const [claudeCode, opencode] = await Promise.all([
|
|
102
|
+
this.isCommandAvailable('claude'),
|
|
103
|
+
this.isCommandAvailable('opencode'),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
return { claudeCode, opencode };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get project-specific target preference from global config
|
|
111
|
+
*/
|
|
112
|
+
private async getProjectTargetPreference(
|
|
113
|
+
projectHash: string
|
|
114
|
+
): Promise<'claude-code' | 'opencode' | undefined> {
|
|
115
|
+
const prefsPath = path.join(this.flowHomeDir, 'project-preferences.json');
|
|
116
|
+
if (!existsSync(prefsPath)) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const prefs = JSON.parse(await fs.readFile(prefsPath, 'utf-8'));
|
|
122
|
+
return prefs.projects?.[projectHash]?.target;
|
|
123
|
+
} catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Save project-specific target preference to global config
|
|
130
|
+
*/
|
|
131
|
+
async saveProjectTargetPreference(
|
|
132
|
+
projectHash: string,
|
|
133
|
+
target: 'claude-code' | 'opencode'
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const prefsPath = path.join(this.flowHomeDir, 'project-preferences.json');
|
|
136
|
+
let prefs: any = { projects: {} };
|
|
137
|
+
|
|
138
|
+
if (existsSync(prefsPath)) {
|
|
139
|
+
try {
|
|
140
|
+
prefs = JSON.parse(await fs.readFile(prefsPath, 'utf-8'));
|
|
141
|
+
} catch {
|
|
142
|
+
// Use default
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!prefs.projects) {
|
|
147
|
+
prefs.projects = {};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
prefs.projects[projectHash] = {
|
|
151
|
+
target,
|
|
152
|
+
lastUsed: new Date().toISOString(),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await fs.writeFile(prefsPath, JSON.stringify(prefs, null, 2));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Detect target platform (claude-code or opencode)
|
|
160
|
+
* New strategy: Detect based on installed commands, not folders
|
|
161
|
+
* Priority: saved preference > installed commands > global default
|
|
162
|
+
*/
|
|
163
|
+
async detectTarget(projectPath: string): Promise<'claude-code' | 'opencode'> {
|
|
164
|
+
const projectHash = this.getProjectHash(projectPath);
|
|
165
|
+
|
|
166
|
+
// 1. Check if we already have a saved preference for this project
|
|
167
|
+
const savedPreference = await this.getProjectTargetPreference(projectHash);
|
|
168
|
+
if (savedPreference) {
|
|
169
|
+
// Verify the command is still available
|
|
170
|
+
const isAvailable =
|
|
171
|
+
savedPreference === 'claude-code'
|
|
172
|
+
? await this.isCommandAvailable('claude')
|
|
173
|
+
: await this.isCommandAvailable('opencode');
|
|
174
|
+
|
|
175
|
+
if (isAvailable) {
|
|
176
|
+
return savedPreference;
|
|
177
|
+
}
|
|
178
|
+
// Command no longer available, fall through to re-detect
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 2. Detect which commands are installed
|
|
182
|
+
const installed = await this.detectInstalledCommands();
|
|
183
|
+
|
|
184
|
+
// If only one is installed, use that
|
|
185
|
+
if (installed.claudeCode && !installed.opencode) {
|
|
186
|
+
await this.saveProjectTargetPreference(projectHash, 'claude-code');
|
|
187
|
+
return 'claude-code';
|
|
188
|
+
}
|
|
189
|
+
if (installed.opencode && !installed.claudeCode) {
|
|
190
|
+
await this.saveProjectTargetPreference(projectHash, 'opencode');
|
|
191
|
+
return 'opencode';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// If both are installed, use global default
|
|
195
|
+
if (installed.claudeCode && installed.opencode) {
|
|
196
|
+
const globalSettingsPath = path.join(this.flowHomeDir, 'settings.json');
|
|
197
|
+
if (existsSync(globalSettingsPath)) {
|
|
198
|
+
try {
|
|
199
|
+
const settings = JSON.parse(await fs.readFile(globalSettingsPath, 'utf-8'));
|
|
200
|
+
if (settings.defaultTarget) {
|
|
201
|
+
await this.saveProjectTargetPreference(projectHash, settings.defaultTarget);
|
|
202
|
+
return settings.defaultTarget;
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// Fall through
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Both installed, no global default, use claude-code
|
|
210
|
+
await this.saveProjectTargetPreference(projectHash, 'claude-code');
|
|
211
|
+
return 'claude-code';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Neither installed - this will fail later, but return default
|
|
215
|
+
return 'claude-code';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get target config directory for project
|
|
220
|
+
*/
|
|
221
|
+
getTargetConfigDir(projectPath: string, target: 'claude-code' | 'opencode'): string {
|
|
222
|
+
return target === 'claude-code'
|
|
223
|
+
? path.join(projectPath, '.claude')
|
|
224
|
+
: path.join(projectPath, '.opencode');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get all active projects
|
|
229
|
+
*/
|
|
230
|
+
async getActiveProjects(): Promise<Array<{ hash: string; sessionFile: string }>> {
|
|
231
|
+
const sessionsDir = path.join(this.flowHomeDir, 'sessions');
|
|
232
|
+
|
|
233
|
+
if (!existsSync(sessionsDir)) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const files = await fs.readdir(sessionsDir);
|
|
238
|
+
return files
|
|
239
|
+
.filter((file) => file.endsWith('.json'))
|
|
240
|
+
.map((file) => ({
|
|
241
|
+
hash: file.replace('.json', ''),
|
|
242
|
+
sessionFile: path.join(sessionsDir, file),
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get project info from hash
|
|
248
|
+
*/
|
|
249
|
+
async getProjectInfo(projectHash: string): Promise<{
|
|
250
|
+
hash: string;
|
|
251
|
+
hasActiveSession: boolean;
|
|
252
|
+
backupsCount: number;
|
|
253
|
+
hasSecrets: boolean;
|
|
254
|
+
} | null> {
|
|
255
|
+
const paths = this.getProjectPaths(projectHash);
|
|
256
|
+
|
|
257
|
+
const hasActiveSession = existsSync(paths.sessionFile);
|
|
258
|
+
|
|
259
|
+
let backupsCount = 0;
|
|
260
|
+
if (existsSync(paths.backupsDir)) {
|
|
261
|
+
const backups = await fs.readdir(paths.backupsDir);
|
|
262
|
+
backupsCount = backups.filter((b) => b.startsWith('session-')).length;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const hasSecrets = existsSync(paths.secretsDir);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
hash: projectHash,
|
|
269
|
+
hasActiveSession,
|
|
270
|
+
backupsCount,
|
|
271
|
+
hasSecrets,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets Manager
|
|
3
|
+
* Handles extraction, storage, and restoration of MCP secrets
|
|
4
|
+
* Stores secrets in ~/.sylphx-flow/secrets/{project-hash}/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import { ProjectManager } from './project-manager.js';
|
|
13
|
+
|
|
14
|
+
export interface MCPSecrets {
|
|
15
|
+
version: string;
|
|
16
|
+
extractedAt: string;
|
|
17
|
+
servers: Record<
|
|
18
|
+
string,
|
|
19
|
+
{
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
args?: string[];
|
|
22
|
+
}
|
|
23
|
+
>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SecretsManager {
|
|
27
|
+
private projectManager: ProjectManager;
|
|
28
|
+
|
|
29
|
+
constructor(projectManager: ProjectManager) {
|
|
30
|
+
this.projectManager = projectManager;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract MCP secrets from project config
|
|
35
|
+
*/
|
|
36
|
+
async extractMCPSecrets(
|
|
37
|
+
projectPath: string,
|
|
38
|
+
projectHash: string,
|
|
39
|
+
target: 'claude-code' | 'opencode'
|
|
40
|
+
): Promise<MCPSecrets> {
|
|
41
|
+
const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
|
|
42
|
+
const configPath =
|
|
43
|
+
target === 'claude-code'
|
|
44
|
+
? path.join(targetDir, 'settings.json')
|
|
45
|
+
: path.join(targetDir, '.mcp.json');
|
|
46
|
+
|
|
47
|
+
const secrets: MCPSecrets = {
|
|
48
|
+
version: '1.0.0',
|
|
49
|
+
extractedAt: new Date().toISOString(),
|
|
50
|
+
servers: {},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (!existsSync(configPath)) {
|
|
54
|
+
return secrets;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
59
|
+
|
|
60
|
+
// Extract MCP server secrets
|
|
61
|
+
if (config.mcp && config.mcp.servers) {
|
|
62
|
+
for (const [serverName, serverConfig] of Object.entries(config.mcp.servers)) {
|
|
63
|
+
const server = serverConfig as any;
|
|
64
|
+
|
|
65
|
+
// Extract env vars (sensitive)
|
|
66
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
67
|
+
secrets.servers[serverName] = {
|
|
68
|
+
env: server.env,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract args (may contain secrets)
|
|
73
|
+
if (server.args && Array.isArray(server.args)) {
|
|
74
|
+
if (!secrets.servers[serverName]) {
|
|
75
|
+
secrets.servers[serverName] = {};
|
|
76
|
+
}
|
|
77
|
+
secrets.servers[serverName].args = server.args;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
// Config file exists but cannot be parsed, skip
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return secrets;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Save secrets to ~/.sylphx-flow/secrets/{project-hash}/
|
|
90
|
+
*/
|
|
91
|
+
async saveSecrets(projectHash: string, secrets: MCPSecrets): Promise<void> {
|
|
92
|
+
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
93
|
+
|
|
94
|
+
// Ensure secrets directory exists
|
|
95
|
+
await fs.mkdir(paths.secretsDir, { recursive: true });
|
|
96
|
+
|
|
97
|
+
// Save unencrypted (for now - can add encryption later)
|
|
98
|
+
const secretsPath = path.join(paths.secretsDir, 'mcp-env.json');
|
|
99
|
+
await fs.writeFile(secretsPath, JSON.stringify(secrets, null, 2));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Load secrets from storage
|
|
104
|
+
*/
|
|
105
|
+
async loadSecrets(projectHash: string): Promise<MCPSecrets | null> {
|
|
106
|
+
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
107
|
+
const secretsPath = path.join(paths.secretsDir, 'mcp-env.json');
|
|
108
|
+
|
|
109
|
+
if (!existsSync(secretsPath)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const data = await fs.readFile(secretsPath, 'utf-8');
|
|
115
|
+
return JSON.parse(data);
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Restore secrets to project config
|
|
123
|
+
*/
|
|
124
|
+
async restoreSecrets(
|
|
125
|
+
projectPath: string,
|
|
126
|
+
projectHash: string,
|
|
127
|
+
target: 'claude-code' | 'opencode',
|
|
128
|
+
secrets: MCPSecrets
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
if (Object.keys(secrets.servers).length === 0) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
|
|
135
|
+
const configPath =
|
|
136
|
+
target === 'claude-code'
|
|
137
|
+
? path.join(targetDir, 'settings.json')
|
|
138
|
+
: path.join(targetDir, '.mcp.json');
|
|
139
|
+
|
|
140
|
+
if (!existsSync(configPath)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
146
|
+
|
|
147
|
+
// Restore secrets to MCP servers
|
|
148
|
+
if (config.mcp && config.mcp.servers) {
|
|
149
|
+
for (const [serverName, serverSecrets] of Object.entries(secrets.servers)) {
|
|
150
|
+
if (config.mcp.servers[serverName]) {
|
|
151
|
+
// Restore env vars
|
|
152
|
+
if (serverSecrets.env) {
|
|
153
|
+
config.mcp.servers[serverName].env = serverSecrets.env;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Restore args
|
|
157
|
+
if (serverSecrets.args) {
|
|
158
|
+
config.mcp.servers[serverName].args = serverSecrets.args;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Write updated config
|
|
165
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// Config restore failed, skip
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Encrypt secrets (optional - for enhanced security)
|
|
173
|
+
*/
|
|
174
|
+
private async encrypt(data: string): Promise<string> {
|
|
175
|
+
// Use machine ID + user HOME as stable key source
|
|
176
|
+
const keySource = `${os.homedir()}-${os.hostname()}`;
|
|
177
|
+
const key = crypto.scryptSync(keySource, 'sylphx-flow-salt', 32);
|
|
178
|
+
const iv = crypto.randomBytes(16);
|
|
179
|
+
|
|
180
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
181
|
+
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
182
|
+
encrypted += cipher.final('hex');
|
|
183
|
+
|
|
184
|
+
const authTag = cipher.getAuthTag();
|
|
185
|
+
|
|
186
|
+
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Decrypt secrets (optional)
|
|
191
|
+
*/
|
|
192
|
+
private async decrypt(encrypted: string): Promise<string> {
|
|
193
|
+
const [ivHex, authTagHex, encryptedData] = encrypted.split(':');
|
|
194
|
+
|
|
195
|
+
const keySource = `${os.homedir()}-${os.hostname()}`;
|
|
196
|
+
const key = crypto.scryptSync(keySource, 'sylphx-flow-salt', 32);
|
|
197
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
198
|
+
const authTag = Buffer.from(authTagHex, 'hex');
|
|
199
|
+
|
|
200
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
201
|
+
decipher.setAuthTag(authTag);
|
|
202
|
+
|
|
203
|
+
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
|
204
|
+
decrypted += decipher.final('utf8');
|
|
205
|
+
|
|
206
|
+
return decrypted;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Clear secrets for a project
|
|
211
|
+
*/
|
|
212
|
+
async clearSecrets(projectHash: string): Promise<void> {
|
|
213
|
+
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
214
|
+
const secretsPath = path.join(paths.secretsDir, 'mcp-env.json');
|
|
215
|
+
|
|
216
|
+
if (existsSync(secretsPath)) {
|
|
217
|
+
await fs.unlink(secretsPath);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if project has stored secrets
|
|
223
|
+
*/
|
|
224
|
+
async hasSecrets(projectHash: string): Promise<boolean> {
|
|
225
|
+
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
226
|
+
const secretsPath = path.join(paths.secretsDir, 'mcp-env.json');
|
|
227
|
+
return existsSync(secretsPath);
|
|
228
|
+
}
|
|
229
|
+
}
|