@sylphx/flow 1.8.2 → 2.1.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 +159 -0
- package/UPGRADE.md +151 -0
- package/package.json +11 -6
- package/src/commands/flow/execute-v2.ts +372 -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 +536 -0
- package/src/config/ai-config.ts +2 -69
- package/src/config/targets.ts +0 -11
- package/src/core/attach-manager.ts +495 -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/installers/file-installer.ts +0 -57
- package/src/core/installers/mcp-installer.ts +0 -33
- 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 +15 -29
- package/src/services/auto-upgrade.ts +248 -0
- package/src/services/first-run-setup.ts +220 -0
- package/src/services/global-config.ts +337 -0
- package/src/services/target-installer.ts +254 -0
- package/src/targets/claude-code.ts +5 -7
- package/src/targets/opencode.ts +6 -26
- 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
- package/src/utils/prompt-helpers.ts +48 -0
- package/src/utils/target-selection.ts +169 -0
package/src/config/ai-config.ts
CHANGED
|
@@ -58,11 +58,6 @@ const GLOBAL_CONFIG_FILE = path.join(os.homedir(), '.sylphx-flow', 'settings.jso
|
|
|
58
58
|
const PROJECT_CONFIG_FILE = '.sylphx-flow/settings.json';
|
|
59
59
|
const LOCAL_CONFIG_FILE = '.sylphx-flow/settings.local.json';
|
|
60
60
|
|
|
61
|
-
/**
|
|
62
|
-
* Deprecated config file (for migration)
|
|
63
|
-
*/
|
|
64
|
-
const LEGACY_CONFIG_FILE = '.sylphx-flow/ai-config.json';
|
|
65
|
-
|
|
66
61
|
/**
|
|
67
62
|
* Get AI config file paths in priority order
|
|
68
63
|
*/
|
|
@@ -70,12 +65,10 @@ export const getAIConfigPaths = (cwd: string = process.cwd()): {
|
|
|
70
65
|
global: string;
|
|
71
66
|
project: string;
|
|
72
67
|
local: string;
|
|
73
|
-
legacy: string;
|
|
74
68
|
} => ({
|
|
75
69
|
global: GLOBAL_CONFIG_FILE,
|
|
76
70
|
project: path.join(cwd, PROJECT_CONFIG_FILE),
|
|
77
71
|
local: path.join(cwd, LOCAL_CONFIG_FILE),
|
|
78
|
-
legacy: path.join(cwd, LEGACY_CONFIG_FILE),
|
|
79
72
|
});
|
|
80
73
|
|
|
81
74
|
/**
|
|
@@ -140,18 +133,12 @@ export const aiConfigExists = async (cwd: string = process.cwd()): Promise<boole
|
|
|
140
133
|
return true;
|
|
141
134
|
} catch {}
|
|
142
135
|
|
|
143
|
-
try {
|
|
144
|
-
await fs.access(paths.legacy);
|
|
145
|
-
return true;
|
|
146
|
-
} catch {}
|
|
147
|
-
|
|
148
136
|
return false;
|
|
149
137
|
};
|
|
150
138
|
|
|
151
139
|
/**
|
|
152
140
|
* Load AI configuration
|
|
153
141
|
* Merges global, project, and local configs with priority: local > project > global
|
|
154
|
-
* Automatically migrates legacy config on first load
|
|
155
142
|
*/
|
|
156
143
|
export const loadAIConfig = async (cwd: string = process.cwd()): Promise<Result<AIConfig, Error>> => {
|
|
157
144
|
return tryCatchAsync(
|
|
@@ -159,39 +146,19 @@ export const loadAIConfig = async (cwd: string = process.cwd()): Promise<Result<
|
|
|
159
146
|
const paths = getAIConfigPaths(cwd);
|
|
160
147
|
|
|
161
148
|
// Load all config files
|
|
162
|
-
const [globalConfig, projectConfig, localConfig
|
|
149
|
+
const [globalConfig, projectConfig, localConfig] = await Promise.all([
|
|
163
150
|
loadConfigFile(paths.global),
|
|
164
151
|
loadConfigFile(paths.project),
|
|
165
152
|
loadConfigFile(paths.local),
|
|
166
|
-
loadConfigFile(paths.legacy),
|
|
167
153
|
]);
|
|
168
154
|
|
|
169
|
-
// Auto-migrate legacy config if it exists and global doesn't
|
|
170
|
-
if (legacyConfig && !globalConfig) {
|
|
171
|
-
await migrateLegacyConfig(cwd);
|
|
172
|
-
// Reload global config after migration
|
|
173
|
-
const migratedGlobal = await loadConfigFile(paths.global);
|
|
174
|
-
if (migratedGlobal) {
|
|
175
|
-
// Start with empty config
|
|
176
|
-
let merged: AIConfig = {};
|
|
177
|
-
|
|
178
|
-
// Merge in priority order: global < project < local
|
|
179
|
-
merged = mergeConfigs(merged, migratedGlobal);
|
|
180
|
-
if (projectConfig) merged = mergeConfigs(merged, projectConfig);
|
|
181
|
-
if (localConfig) merged = mergeConfigs(merged, localConfig);
|
|
182
|
-
|
|
183
|
-
return merged;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
155
|
// Start with empty config
|
|
188
156
|
let merged: AIConfig = {};
|
|
189
157
|
|
|
190
|
-
// Merge in priority order: global < project < local
|
|
158
|
+
// Merge in priority order: global < project < local
|
|
191
159
|
if (globalConfig) merged = mergeConfigs(merged, globalConfig);
|
|
192
160
|
if (projectConfig) merged = mergeConfigs(merged, projectConfig);
|
|
193
161
|
if (localConfig) merged = mergeConfigs(merged, localConfig);
|
|
194
|
-
if (legacyConfig) merged = mergeConfigs(merged, legacyConfig);
|
|
195
162
|
|
|
196
163
|
return merged;
|
|
197
164
|
},
|
|
@@ -340,37 +307,3 @@ export const getConfiguredProviders = async (
|
|
|
340
307
|
return providers;
|
|
341
308
|
};
|
|
342
309
|
|
|
343
|
-
/**
|
|
344
|
-
* Migrate legacy ai-config.json to new settings system
|
|
345
|
-
* Automatically called on first load if legacy config exists
|
|
346
|
-
*/
|
|
347
|
-
export const migrateLegacyConfig = async (cwd: string = process.cwd()): Promise<Result<void, Error>> => {
|
|
348
|
-
return tryCatchAsync(
|
|
349
|
-
async () => {
|
|
350
|
-
const paths = getAIConfigPaths(cwd);
|
|
351
|
-
|
|
352
|
-
// Check if legacy config exists
|
|
353
|
-
const legacyConfig = await loadConfigFile(paths.legacy);
|
|
354
|
-
if (!legacyConfig) {
|
|
355
|
-
return; // No legacy config to migrate
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Check if global config already exists
|
|
359
|
-
const globalConfig = await loadConfigFile(paths.global);
|
|
360
|
-
if (globalConfig) {
|
|
361
|
-
// Global config exists, don't overwrite it
|
|
362
|
-
console.log('Legacy config found but global config already exists. Skipping migration.');
|
|
363
|
-
console.log(`You can manually delete ${paths.legacy} if migration is complete.`);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Migrate to global config
|
|
368
|
-
await fs.mkdir(path.dirname(paths.global), { recursive: true });
|
|
369
|
-
await fs.writeFile(paths.global, JSON.stringify(legacyConfig, null, 2) + '\n', 'utf8');
|
|
370
|
-
|
|
371
|
-
console.log(`✓ Migrated configuration from ${paths.legacy} to ${paths.global}`);
|
|
372
|
-
console.log(` You can now safely delete the legacy file: ${paths.legacy}`);
|
|
373
|
-
},
|
|
374
|
-
(error: any) => new Error(`Failed to migrate legacy config: ${error.message}`)
|
|
375
|
-
);
|
|
376
|
-
};
|
package/src/config/targets.ts
CHANGED
|
@@ -113,14 +113,3 @@ export const isTargetImplemented = (id: string): boolean => {
|
|
|
113
113
|
* Utility type for target IDs
|
|
114
114
|
*/
|
|
115
115
|
export type TargetID = ReturnType<typeof getAllTargetIDs>[number];
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Legacy aliases for backward compatibility
|
|
119
|
-
* @deprecated Use getAllTargets() instead
|
|
120
|
-
*/
|
|
121
|
-
export const ALL_TARGETS = getAllTargets;
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @deprecated Use getImplementedTargets() instead
|
|
125
|
-
*/
|
|
126
|
-
export const IMPLEMENTED_TARGETS = getImplementedTargets;
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach Manager
|
|
3
|
+
* Handles merging Flow templates into user's project environment
|
|
4
|
+
* Strategy: Direct override with backup, restore on cleanup
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { ProjectManager } from './project-manager.js';
|
|
13
|
+
import type { BackupManifest } from './backup-manager.js';
|
|
14
|
+
import { GlobalConfigService } from '../services/global-config.js';
|
|
15
|
+
import { MCP_SERVER_REGISTRY } from '../config/servers.js';
|
|
16
|
+
|
|
17
|
+
export interface AttachResult {
|
|
18
|
+
agentsAdded: string[];
|
|
19
|
+
agentsOverridden: string[];
|
|
20
|
+
commandsAdded: string[];
|
|
21
|
+
commandsOverridden: string[];
|
|
22
|
+
rulesAppended: boolean;
|
|
23
|
+
mcpServersAdded: string[];
|
|
24
|
+
mcpServersOverridden: string[];
|
|
25
|
+
singleFilesMerged: string[];
|
|
26
|
+
hooksAdded: string[];
|
|
27
|
+
hooksOverridden: string[];
|
|
28
|
+
conflicts: ConflictInfo[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ConflictInfo {
|
|
32
|
+
type: 'agent' | 'command' | 'mcp' | 'hook';
|
|
33
|
+
name: string;
|
|
34
|
+
action: 'overridden' | 'merged';
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface FlowTemplates {
|
|
39
|
+
agents: Array<{ name: string; content: string }>;
|
|
40
|
+
commands: Array<{ name: string; content: string }>;
|
|
41
|
+
rules?: string;
|
|
42
|
+
mcpServers: Array<{ name: string; config: any }>;
|
|
43
|
+
hooks: Array<{ name: string; content: string }>;
|
|
44
|
+
singleFiles: Array<{ path: string; content: string }>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class AttachManager {
|
|
48
|
+
private projectManager: ProjectManager;
|
|
49
|
+
private configService: GlobalConfigService;
|
|
50
|
+
|
|
51
|
+
constructor(projectManager: ProjectManager) {
|
|
52
|
+
this.projectManager = projectManager;
|
|
53
|
+
this.configService = new GlobalConfigService();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Calculate SHA256 hash of file content
|
|
58
|
+
*/
|
|
59
|
+
private async calculateFileHash(filePath: string): Promise<string> {
|
|
60
|
+
try {
|
|
61
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
62
|
+
return createHash('sha256').update(content).digest('hex');
|
|
63
|
+
} catch {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get target-specific directory names
|
|
70
|
+
*/
|
|
71
|
+
private getTargetDirs(target: 'claude-code' | 'opencode'): {
|
|
72
|
+
agents: string;
|
|
73
|
+
commands: string;
|
|
74
|
+
} {
|
|
75
|
+
return target === 'claude-code'
|
|
76
|
+
? { agents: 'agents', commands: 'commands' }
|
|
77
|
+
: { agents: 'agent', commands: 'command' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Load global MCP servers from ~/.sylphx-flow/mcp-config.json
|
|
82
|
+
*/
|
|
83
|
+
private async loadGlobalMCPServers(
|
|
84
|
+
target: 'claude-code' | 'opencode'
|
|
85
|
+
): Promise<Array<{ name: string; config: any }>> {
|
|
86
|
+
try {
|
|
87
|
+
const enabledServers = await this.configService.getEnabledMCPServers();
|
|
88
|
+
const servers: Array<{ name: string; config: any }> = [];
|
|
89
|
+
|
|
90
|
+
for (const [serverKey, serverConfig] of Object.entries(enabledServers)) {
|
|
91
|
+
// Lookup server definition in registry
|
|
92
|
+
const serverDef = MCP_SERVER_REGISTRY[serverKey];
|
|
93
|
+
|
|
94
|
+
if (!serverDef) {
|
|
95
|
+
console.warn(`MCP server '${serverKey}' not found in registry, skipping`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Clone the server config from registry
|
|
100
|
+
let config: any = { ...serverDef.config };
|
|
101
|
+
|
|
102
|
+
// Merge environment variables from global config
|
|
103
|
+
if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
|
|
104
|
+
if (config.type === 'stdio' || config.type === 'local') {
|
|
105
|
+
config.env = { ...config.env, ...serverConfig.env };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
servers.push({ name: serverDef.name, config });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return servers;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// If global config doesn't exist or fails to load, return empty array
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Attach Flow templates to project
|
|
121
|
+
* Strategy: Override with warning, backup handles restoration
|
|
122
|
+
*/
|
|
123
|
+
async attach(
|
|
124
|
+
projectPath: string,
|
|
125
|
+
projectHash: string,
|
|
126
|
+
target: 'claude-code' | 'opencode',
|
|
127
|
+
templates: FlowTemplates,
|
|
128
|
+
manifest: BackupManifest
|
|
129
|
+
): Promise<AttachResult> {
|
|
130
|
+
const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
|
|
131
|
+
|
|
132
|
+
const result: AttachResult = {
|
|
133
|
+
agentsAdded: [],
|
|
134
|
+
agentsOverridden: [],
|
|
135
|
+
commandsAdded: [],
|
|
136
|
+
commandsOverridden: [],
|
|
137
|
+
rulesAppended: false,
|
|
138
|
+
mcpServersAdded: [],
|
|
139
|
+
mcpServersOverridden: [],
|
|
140
|
+
singleFilesMerged: [],
|
|
141
|
+
hooksAdded: [],
|
|
142
|
+
hooksOverridden: [],
|
|
143
|
+
conflicts: [],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Ensure target directory exists
|
|
147
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
148
|
+
|
|
149
|
+
// 1. Attach agents
|
|
150
|
+
await this.attachAgents(targetDir, target, templates.agents, result, manifest);
|
|
151
|
+
|
|
152
|
+
// 2. Attach commands
|
|
153
|
+
await this.attachCommands(targetDir, target, templates.commands, result, manifest);
|
|
154
|
+
|
|
155
|
+
// 3. Attach rules (if applicable)
|
|
156
|
+
if (templates.rules) {
|
|
157
|
+
await this.attachRules(targetDir, target, templates.rules, result, manifest);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 4. Attach MCP servers (merge global + template servers)
|
|
161
|
+
const globalMCPServers = await this.loadGlobalMCPServers(target);
|
|
162
|
+
const allMCPServers = [...globalMCPServers, ...templates.mcpServers];
|
|
163
|
+
|
|
164
|
+
if (allMCPServers.length > 0) {
|
|
165
|
+
await this.attachMCPServers(
|
|
166
|
+
targetDir,
|
|
167
|
+
target,
|
|
168
|
+
allMCPServers,
|
|
169
|
+
result,
|
|
170
|
+
manifest
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 5. Attach hooks
|
|
175
|
+
if (templates.hooks.length > 0) {
|
|
176
|
+
await this.attachHooks(targetDir, templates.hooks, result, manifest);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 6. Attach single files
|
|
180
|
+
if (templates.singleFiles.length > 0) {
|
|
181
|
+
await this.attachSingleFiles(projectPath, templates.singleFiles, result, manifest);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Show conflict warnings
|
|
185
|
+
this.showConflictWarnings(result);
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Attach agents (override strategy)
|
|
192
|
+
*/
|
|
193
|
+
private async attachAgents(
|
|
194
|
+
targetDir: string,
|
|
195
|
+
target: 'claude-code' | 'opencode',
|
|
196
|
+
agents: Array<{ name: string; content: string }>,
|
|
197
|
+
result: AttachResult,
|
|
198
|
+
manifest: BackupManifest
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
const dirs = this.getTargetDirs(target);
|
|
201
|
+
const agentsDir = path.join(targetDir, dirs.agents);
|
|
202
|
+
await fs.mkdir(agentsDir, { recursive: true });
|
|
203
|
+
|
|
204
|
+
for (const agent of agents) {
|
|
205
|
+
const agentPath = path.join(agentsDir, agent.name);
|
|
206
|
+
const existed = existsSync(agentPath);
|
|
207
|
+
|
|
208
|
+
if (existed) {
|
|
209
|
+
// Conflict: user has same agent
|
|
210
|
+
result.agentsOverridden.push(agent.name);
|
|
211
|
+
result.conflicts.push({
|
|
212
|
+
type: 'agent',
|
|
213
|
+
name: agent.name,
|
|
214
|
+
action: 'overridden',
|
|
215
|
+
message: `Agent '${agent.name}' overridden (will be restored on exit)`,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Track in manifest
|
|
219
|
+
manifest.backup.agents.user.push(agent.name);
|
|
220
|
+
} else {
|
|
221
|
+
result.agentsAdded.push(agent.name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Write Flow agent (override)
|
|
225
|
+
await fs.writeFile(agentPath, agent.content);
|
|
226
|
+
|
|
227
|
+
// Track Flow agent
|
|
228
|
+
manifest.backup.agents.flow.push(agent.name);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Attach commands (override strategy)
|
|
234
|
+
*/
|
|
235
|
+
private async attachCommands(
|
|
236
|
+
targetDir: string,
|
|
237
|
+
target: 'claude-code' | 'opencode',
|
|
238
|
+
commands: Array<{ name: string; content: string }>,
|
|
239
|
+
result: AttachResult,
|
|
240
|
+
manifest: BackupManifest
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
const dirs = this.getTargetDirs(target);
|
|
243
|
+
const commandsDir = path.join(targetDir, dirs.commands);
|
|
244
|
+
await fs.mkdir(commandsDir, { recursive: true });
|
|
245
|
+
|
|
246
|
+
for (const command of commands) {
|
|
247
|
+
const commandPath = path.join(commandsDir, command.name);
|
|
248
|
+
const existed = existsSync(commandPath);
|
|
249
|
+
|
|
250
|
+
if (existed) {
|
|
251
|
+
// Conflict: user has same command
|
|
252
|
+
result.commandsOverridden.push(command.name);
|
|
253
|
+
result.conflicts.push({
|
|
254
|
+
type: 'command',
|
|
255
|
+
name: command.name,
|
|
256
|
+
action: 'overridden',
|
|
257
|
+
message: `Command '${command.name}' overridden (will be restored on exit)`,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Track in manifest
|
|
261
|
+
manifest.backup.commands.user.push(command.name);
|
|
262
|
+
} else {
|
|
263
|
+
result.commandsAdded.push(command.name);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Write Flow command (override)
|
|
267
|
+
await fs.writeFile(commandPath, command.content);
|
|
268
|
+
|
|
269
|
+
// Track Flow command
|
|
270
|
+
manifest.backup.commands.flow.push(command.name);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Attach rules (append strategy for AGENTS.md)
|
|
276
|
+
*/
|
|
277
|
+
private async attachRules(
|
|
278
|
+
targetDir: string,
|
|
279
|
+
target: 'claude-code' | 'opencode',
|
|
280
|
+
rules: string,
|
|
281
|
+
result: AttachResult,
|
|
282
|
+
manifest: BackupManifest
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
// Claude Code: .claude/agents/AGENTS.md
|
|
285
|
+
// OpenCode: .opencode/AGENTS.md
|
|
286
|
+
const dirs = this.getTargetDirs(target);
|
|
287
|
+
const rulesPath =
|
|
288
|
+
target === 'claude-code'
|
|
289
|
+
? path.join(targetDir, dirs.agents, 'AGENTS.md')
|
|
290
|
+
: path.join(targetDir, 'AGENTS.md');
|
|
291
|
+
|
|
292
|
+
if (existsSync(rulesPath)) {
|
|
293
|
+
// User has AGENTS.md, append Flow rules
|
|
294
|
+
const userRules = await fs.readFile(rulesPath, 'utf-8');
|
|
295
|
+
|
|
296
|
+
// Check if already appended (avoid duplicates)
|
|
297
|
+
if (userRules.includes('<!-- Sylphx Flow Rules -->')) {
|
|
298
|
+
// Already appended, skip
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const merged = `${userRules}
|
|
303
|
+
|
|
304
|
+
<!-- ========== Sylphx Flow Rules (Auto-injected) ========== -->
|
|
305
|
+
|
|
306
|
+
${rules}
|
|
307
|
+
|
|
308
|
+
<!-- ========== End of Sylphx Flow Rules ========== -->
|
|
309
|
+
`;
|
|
310
|
+
|
|
311
|
+
await fs.writeFile(rulesPath, merged);
|
|
312
|
+
|
|
313
|
+
manifest.backup.rules = {
|
|
314
|
+
path: rulesPath,
|
|
315
|
+
originalSize: userRules.length,
|
|
316
|
+
flowContentAdded: true,
|
|
317
|
+
};
|
|
318
|
+
} else {
|
|
319
|
+
// User doesn't have AGENTS.md, create new
|
|
320
|
+
await fs.mkdir(path.dirname(rulesPath), { recursive: true });
|
|
321
|
+
await fs.writeFile(rulesPath, rules);
|
|
322
|
+
|
|
323
|
+
manifest.backup.rules = {
|
|
324
|
+
path: rulesPath,
|
|
325
|
+
originalSize: 0,
|
|
326
|
+
flowContentAdded: true,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
result.rulesAppended = true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Attach MCP servers (merge strategy)
|
|
335
|
+
*/
|
|
336
|
+
private async attachMCPServers(
|
|
337
|
+
targetDir: string,
|
|
338
|
+
target: 'claude-code' | 'opencode',
|
|
339
|
+
mcpServers: Array<{ name: string; config: any }>,
|
|
340
|
+
result: AttachResult,
|
|
341
|
+
manifest: BackupManifest
|
|
342
|
+
): Promise<void> {
|
|
343
|
+
// Claude Code: .claude/settings.json (mcp.servers)
|
|
344
|
+
// OpenCode: .opencode/.mcp.json
|
|
345
|
+
const configPath =
|
|
346
|
+
target === 'claude-code'
|
|
347
|
+
? path.join(targetDir, 'settings.json')
|
|
348
|
+
: path.join(targetDir, '.mcp.json');
|
|
349
|
+
|
|
350
|
+
let config: any = {};
|
|
351
|
+
|
|
352
|
+
if (existsSync(configPath)) {
|
|
353
|
+
config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Ensure mcp.servers exists
|
|
357
|
+
if (!config.mcp) config.mcp = {};
|
|
358
|
+
if (!config.mcp.servers) config.mcp.servers = {};
|
|
359
|
+
|
|
360
|
+
// Add Flow MCP servers
|
|
361
|
+
for (const server of mcpServers) {
|
|
362
|
+
if (config.mcp.servers[server.name]) {
|
|
363
|
+
// Conflict: user has same MCP server
|
|
364
|
+
result.mcpServersOverridden.push(server.name);
|
|
365
|
+
result.conflicts.push({
|
|
366
|
+
type: 'mcp',
|
|
367
|
+
name: server.name,
|
|
368
|
+
action: 'overridden',
|
|
369
|
+
message: `MCP server '${server.name}' overridden (will be restored on exit)`,
|
|
370
|
+
});
|
|
371
|
+
} else {
|
|
372
|
+
result.mcpServersAdded.push(server.name);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Override with Flow config
|
|
376
|
+
config.mcp.servers[server.name] = server.config;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Write updated config
|
|
380
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
381
|
+
|
|
382
|
+
// Track in manifest
|
|
383
|
+
manifest.backup.config = {
|
|
384
|
+
path: configPath,
|
|
385
|
+
hash: await this.calculateFileHash(configPath),
|
|
386
|
+
mcpServersCount: Object.keys(config.mcp.servers).length,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Attach hooks (override strategy)
|
|
392
|
+
*/
|
|
393
|
+
private async attachHooks(
|
|
394
|
+
targetDir: string,
|
|
395
|
+
hooks: Array<{ name: string; content: string }>,
|
|
396
|
+
result: AttachResult,
|
|
397
|
+
manifest: BackupManifest
|
|
398
|
+
): Promise<void> {
|
|
399
|
+
const hooksDir = path.join(targetDir, 'hooks');
|
|
400
|
+
await fs.mkdir(hooksDir, { recursive: true });
|
|
401
|
+
|
|
402
|
+
for (const hook of hooks) {
|
|
403
|
+
const hookPath = path.join(hooksDir, hook.name);
|
|
404
|
+
const existed = existsSync(hookPath);
|
|
405
|
+
|
|
406
|
+
if (existed) {
|
|
407
|
+
result.hooksOverridden.push(hook.name);
|
|
408
|
+
result.conflicts.push({
|
|
409
|
+
type: 'hook',
|
|
410
|
+
name: hook.name,
|
|
411
|
+
action: 'overridden',
|
|
412
|
+
message: `Hook '${hook.name}' overridden (will be restored on exit)`,
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
result.hooksAdded.push(hook.name);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
await fs.writeFile(hookPath, hook.content);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Attach single files (CLAUDE.md, .cursorrules, etc.)
|
|
424
|
+
*/
|
|
425
|
+
private async attachSingleFiles(
|
|
426
|
+
projectPath: string,
|
|
427
|
+
singleFiles: Array<{ path: string; content: string }>,
|
|
428
|
+
result: AttachResult,
|
|
429
|
+
manifest: BackupManifest
|
|
430
|
+
): Promise<void> {
|
|
431
|
+
for (const file of singleFiles) {
|
|
432
|
+
const filePath = path.join(projectPath, file.path);
|
|
433
|
+
const existed = existsSync(filePath);
|
|
434
|
+
|
|
435
|
+
if (existed) {
|
|
436
|
+
// User has file, append Flow content
|
|
437
|
+
const userContent = await fs.readFile(filePath, 'utf-8');
|
|
438
|
+
|
|
439
|
+
// Check if already appended
|
|
440
|
+
if (userContent.includes('<!-- Sylphx Flow Enhancement -->')) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const merged = `${userContent}
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
**Sylphx Flow Enhancement:**
|
|
449
|
+
|
|
450
|
+
${file.content}
|
|
451
|
+
`;
|
|
452
|
+
|
|
453
|
+
await fs.writeFile(filePath, merged);
|
|
454
|
+
|
|
455
|
+
manifest.backup.singleFiles[file.path] = {
|
|
456
|
+
existed: true,
|
|
457
|
+
originalSize: userContent.length,
|
|
458
|
+
flowContentAdded: true,
|
|
459
|
+
};
|
|
460
|
+
} else {
|
|
461
|
+
// Create new file
|
|
462
|
+
await fs.writeFile(filePath, file.content);
|
|
463
|
+
|
|
464
|
+
manifest.backup.singleFiles[file.path] = {
|
|
465
|
+
existed: false,
|
|
466
|
+
originalSize: 0,
|
|
467
|
+
flowContentAdded: true,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
result.singleFilesMerged.push(file.path);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Show conflict warnings to user
|
|
477
|
+
*/
|
|
478
|
+
private showConflictWarnings(result: AttachResult): void {
|
|
479
|
+
if (result.conflicts.length === 0) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
console.log(chalk.yellow('\n⚠️ Conflicts detected:\n'));
|
|
484
|
+
|
|
485
|
+
for (const conflict of result.conflicts) {
|
|
486
|
+
console.log(
|
|
487
|
+
chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`)
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
console.log(
|
|
492
|
+
chalk.dim('\n Don\'t worry! All overridden content will be restored on exit.\n')
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|