@sylphx/flow 2.1.3 → 2.1.5
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 +28 -0
- package/README.md +44 -0
- package/package.json +79 -73
- package/src/commands/flow/execute-v2.ts +37 -29
- package/src/commands/flow/prompt.ts +5 -3
- package/src/commands/flow/types.ts +0 -2
- package/src/commands/flow-command.ts +20 -13
- package/src/commands/hook-command.ts +1 -3
- package/src/commands/settings/checkbox-config.ts +128 -0
- package/src/commands/settings/index.ts +6 -0
- package/src/commands/settings-command.ts +84 -156
- package/src/config/ai-config.ts +60 -41
- package/src/core/agent-loader.ts +11 -6
- package/src/core/attach/file-attacher.ts +172 -0
- package/src/core/attach/index.ts +5 -0
- package/src/core/attach-manager.ts +117 -171
- package/src/core/backup-manager.ts +35 -29
- package/src/core/cleanup-handler.ts +11 -8
- package/src/core/error-handling.ts +23 -30
- package/src/core/flow-executor.ts +58 -76
- package/src/core/formatting/bytes.ts +2 -4
- package/src/core/functional/async.ts +5 -4
- package/src/core/functional/error-handler.ts +2 -2
- package/src/core/git-stash-manager.ts +21 -10
- package/src/core/installers/file-installer.ts +0 -1
- package/src/core/installers/mcp-installer.ts +0 -1
- package/src/core/project-manager.ts +24 -18
- package/src/core/secrets-manager.ts +54 -73
- package/src/core/session-manager.ts +20 -22
- package/src/core/state-detector.ts +139 -80
- package/src/core/template-loader.ts +13 -31
- package/src/core/upgrade-manager.ts +122 -69
- package/src/index.ts +8 -5
- package/src/services/auto-upgrade.ts +1 -1
- package/src/services/config-service.ts +41 -29
- package/src/services/global-config.ts +3 -3
- package/src/services/target-installer.ts +11 -26
- package/src/targets/claude-code.ts +35 -81
- package/src/targets/opencode.ts +28 -68
- package/src/targets/shared/index.ts +7 -0
- package/src/targets/shared/mcp-transforms.ts +132 -0
- package/src/targets/shared/target-operations.ts +135 -0
- package/src/types/cli.types.ts +2 -2
- package/src/types/provider.types.ts +1 -7
- package/src/types/session.types.ts +11 -11
- package/src/types/target.types.ts +3 -1
- package/src/types/todo.types.ts +1 -1
- package/src/types.ts +1 -1
- package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
- package/src/utils/agent-enhancer.ts +4 -4
- package/src/utils/config/paths.ts +3 -1
- package/src/utils/config/target-utils.ts +2 -2
- package/src/utils/display/banner.ts +2 -2
- package/src/utils/display/notifications.ts +58 -45
- package/src/utils/display/status.ts +29 -12
- package/src/utils/files/file-operations.ts +1 -1
- package/src/utils/files/sync-utils.ts +38 -41
- package/src/utils/index.ts +19 -27
- package/src/utils/package-manager-detector.ts +15 -5
- package/src/utils/security/security.ts +8 -4
- package/src/utils/target-selection.ts +6 -8
- package/src/utils/version.ts +4 -2
- package/src/commands/flow-orchestrator.ts +0 -328
- package/src/commands/init-command.ts +0 -92
- package/src/commands/init-core.ts +0 -331
- package/src/core/agent-manager.ts +0 -174
- package/src/core/loop-controller.ts +0 -200
- package/src/core/rule-loader.ts +0 -147
- package/src/core/rule-manager.ts +0 -240
- package/src/services/claude-config-service.ts +0 -252
- package/src/services/first-run-setup.ts +0 -220
- package/src/services/smart-config-service.ts +0 -269
- package/src/types/api.types.ts +0 -9
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure functions for file attachment operations
|
|
3
|
+
* Generic utilities for attaching files with conflict tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export interface AttachItem {
|
|
15
|
+
name: string;
|
|
16
|
+
content: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ConflictInfo {
|
|
20
|
+
type: string;
|
|
21
|
+
name: string;
|
|
22
|
+
action: 'overridden' | 'added' | 'skipped';
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AttachStats {
|
|
27
|
+
added: string[];
|
|
28
|
+
overridden: string[];
|
|
29
|
+
conflicts: ConflictInfo[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ManifestTracker {
|
|
33
|
+
user: string[];
|
|
34
|
+
flow: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Pure Functions
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create conflict info object
|
|
43
|
+
*/
|
|
44
|
+
export const createConflict = (
|
|
45
|
+
type: string,
|
|
46
|
+
name: string,
|
|
47
|
+
action: 'overridden' | 'added' | 'skipped' = 'overridden'
|
|
48
|
+
): ConflictInfo => ({
|
|
49
|
+
type,
|
|
50
|
+
name,
|
|
51
|
+
action,
|
|
52
|
+
message: `${type.charAt(0).toUpperCase() + type.slice(1)} '${name}' ${action} (will be restored on exit)`,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if file exists at path
|
|
57
|
+
*/
|
|
58
|
+
export const fileExists = (filePath: string): boolean => existsSync(filePath);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Ensure directory exists
|
|
62
|
+
*/
|
|
63
|
+
export const ensureDir = (dirPath: string): Promise<void> =>
|
|
64
|
+
fs.mkdir(dirPath, { recursive: true }).then(() => {});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Write file content
|
|
68
|
+
*/
|
|
69
|
+
export const writeFile = (filePath: string, content: string): Promise<void> =>
|
|
70
|
+
fs.writeFile(filePath, content);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read file content
|
|
74
|
+
*/
|
|
75
|
+
export const readFile = (filePath: string): Promise<string> =>
|
|
76
|
+
fs.readFile(filePath, 'utf-8');
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Generic Attach Function
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Attach multiple items to a directory with conflict tracking
|
|
84
|
+
* Pure function that returns stats and manifest updates
|
|
85
|
+
*/
|
|
86
|
+
export const attachItemsToDir = async (
|
|
87
|
+
items: AttachItem[],
|
|
88
|
+
targetDir: string,
|
|
89
|
+
itemType: string
|
|
90
|
+
): Promise<{ stats: AttachStats; manifest: ManifestTracker }> => {
|
|
91
|
+
await ensureDir(targetDir);
|
|
92
|
+
|
|
93
|
+
const stats: AttachStats = {
|
|
94
|
+
added: [],
|
|
95
|
+
overridden: [],
|
|
96
|
+
conflicts: [],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const manifest: ManifestTracker = {
|
|
100
|
+
user: [],
|
|
101
|
+
flow: [],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
const itemPath = path.join(targetDir, item.name);
|
|
106
|
+
const existed = fileExists(itemPath);
|
|
107
|
+
|
|
108
|
+
if (existed) {
|
|
109
|
+
stats.overridden.push(item.name);
|
|
110
|
+
stats.conflicts.push(createConflict(itemType, item.name, 'overridden'));
|
|
111
|
+
manifest.user.push(item.name);
|
|
112
|
+
} else {
|
|
113
|
+
stats.added.push(item.name);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await writeFile(itemPath, item.content);
|
|
117
|
+
manifest.flow.push(item.name);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { stats, manifest };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Rules Attachment (Append Strategy)
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
const FLOW_RULES_START = '<!-- ========== Sylphx Flow Rules (Auto-injected) ========== -->';
|
|
128
|
+
const FLOW_RULES_END = '<!-- ========== End of Sylphx Flow Rules ========== -->';
|
|
129
|
+
const FLOW_RULES_MARKER = '<!-- Sylphx Flow Rules -->';
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if content already has Flow rules appended
|
|
133
|
+
*/
|
|
134
|
+
export const hasFlowRules = (content: string): boolean =>
|
|
135
|
+
content.includes(FLOW_RULES_MARKER);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Wrap rules content with markers
|
|
139
|
+
*/
|
|
140
|
+
export const wrapRulesContent = (rules: string): string =>
|
|
141
|
+
`\n\n${FLOW_RULES_START}\n\n${rules}\n\n${FLOW_RULES_END}\n`;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Append rules to existing content
|
|
145
|
+
*/
|
|
146
|
+
export const appendRules = (existingContent: string, rules: string): string =>
|
|
147
|
+
existingContent + wrapRulesContent(rules);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Attach rules file with append strategy
|
|
151
|
+
*/
|
|
152
|
+
export const attachRulesFile = async (
|
|
153
|
+
rulesPath: string,
|
|
154
|
+
rules: string
|
|
155
|
+
): Promise<{ originalSize: number; flowContentAdded: boolean }> => {
|
|
156
|
+
if (fileExists(rulesPath)) {
|
|
157
|
+
const existingContent = await readFile(rulesPath);
|
|
158
|
+
|
|
159
|
+
// Skip if already appended
|
|
160
|
+
if (hasFlowRules(existingContent)) {
|
|
161
|
+
return { originalSize: existingContent.length, flowContentAdded: false };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await writeFile(rulesPath, appendRules(existingContent, rules));
|
|
165
|
+
return { originalSize: existingContent.length, flowContentAdded: true };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Create new file
|
|
169
|
+
await ensureDir(path.dirname(rulesPath));
|
|
170
|
+
await writeFile(rulesPath, rules);
|
|
171
|
+
return { originalSize: 0, flowContentAdded: true };
|
|
172
|
+
};
|
|
@@ -4,15 +4,18 @@
|
|
|
4
4
|
* Strategy: Direct override with backup, restore on cleanup
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
7
9
|
import fs from 'node:fs/promises';
|
|
8
10
|
import path from 'node:path';
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
10
|
-
import { createHash } from 'node:crypto';
|
|
11
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
12
|
import { MCP_SERVER_REGISTRY } from '../config/servers.js';
|
|
13
|
+
import { GlobalConfigService } from '../services/global-config.js';
|
|
14
|
+
import type { Target } from '../types/target.types.js';
|
|
15
|
+
import { attachItemsToDir, attachRulesFile } from './attach/index.js';
|
|
16
|
+
import type { BackupManifest } from './backup-manager.js';
|
|
17
|
+
import type { ProjectManager } from './project-manager.js';
|
|
18
|
+
import { targetManager } from './target-manager.js';
|
|
16
19
|
|
|
17
20
|
export interface AttachResult {
|
|
18
21
|
agentsAdded: string[];
|
|
@@ -39,13 +42,12 @@ export interface FlowTemplates {
|
|
|
39
42
|
agents: Array<{ name: string; content: string }>;
|
|
40
43
|
commands: Array<{ name: string; content: string }>;
|
|
41
44
|
rules?: string;
|
|
42
|
-
mcpServers: Array<{ name: string; config:
|
|
45
|
+
mcpServers: Array<{ name: string; config: Record<string, unknown> }>;
|
|
43
46
|
hooks: Array<{ name: string; content: string }>;
|
|
44
47
|
singleFiles: Array<{ path: string; content: string }>;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export class AttachManager {
|
|
48
|
-
private projectManager: ProjectManager;
|
|
49
51
|
private configService: GlobalConfigService;
|
|
50
52
|
|
|
51
53
|
constructor(projectManager: ProjectManager) {
|
|
@@ -66,26 +68,25 @@ export class AttachManager {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
|
-
*
|
|
71
|
+
* Resolve target from ID string to Target object
|
|
70
72
|
*/
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
: { agents: 'agent', commands: 'command' };
|
|
73
|
+
private resolveTarget(targetId: string): Target {
|
|
74
|
+
const targetOption = targetManager.getTarget(targetId);
|
|
75
|
+
if (targetOption._tag === 'None') {
|
|
76
|
+
throw new Error(`Unknown target: ${targetId}`);
|
|
77
|
+
}
|
|
78
|
+
return targetOption.value;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
82
|
* Load global MCP servers from ~/.sylphx-flow/mcp-config.json
|
|
82
83
|
*/
|
|
83
84
|
private async loadGlobalMCPServers(
|
|
84
|
-
|
|
85
|
-
): Promise<Array<{ name: string; config:
|
|
85
|
+
_target: Target
|
|
86
|
+
): Promise<Array<{ name: string; config: Record<string, unknown> }>> {
|
|
86
87
|
try {
|
|
87
88
|
const enabledServers = await this.configService.getEnabledMCPServers();
|
|
88
|
-
const servers: Array<{ name: string; config:
|
|
89
|
+
const servers: Array<{ name: string; config: Record<string, unknown> }> = [];
|
|
89
90
|
|
|
90
91
|
for (const [serverKey, serverConfig] of Object.entries(enabledServers)) {
|
|
91
92
|
// Lookup server definition in registry
|
|
@@ -97,7 +98,7 @@ export class AttachManager {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
// Clone the server config from registry
|
|
100
|
-
|
|
101
|
+
const config: Record<string, unknown> = { ...serverDef.config };
|
|
101
102
|
|
|
102
103
|
// Merge environment variables from global config
|
|
103
104
|
if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
|
|
@@ -110,7 +111,7 @@ export class AttachManager {
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
return servers;
|
|
113
|
-
} catch (
|
|
114
|
+
} catch (_error) {
|
|
114
115
|
// If global config doesn't exist or fails to load, return empty array
|
|
115
116
|
return [];
|
|
116
117
|
}
|
|
@@ -119,15 +120,21 @@ export class AttachManager {
|
|
|
119
120
|
/**
|
|
120
121
|
* Attach Flow templates to project
|
|
121
122
|
* Strategy: Override with warning, backup handles restoration
|
|
123
|
+
* @param projectPath - Project root path
|
|
124
|
+
* @param _projectHash - Project hash (unused but kept for API compatibility)
|
|
125
|
+
* @param targetOrId - Target object or target ID string
|
|
126
|
+
* @param templates - Flow templates to attach
|
|
127
|
+
* @param manifest - Backup manifest to track changes
|
|
122
128
|
*/
|
|
123
129
|
async attach(
|
|
124
130
|
projectPath: string,
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
_projectHash: string,
|
|
132
|
+
targetOrId: Target | string,
|
|
127
133
|
templates: FlowTemplates,
|
|
128
134
|
manifest: BackupManifest
|
|
129
135
|
): Promise<AttachResult> {
|
|
130
|
-
|
|
136
|
+
// Resolve target from ID if needed
|
|
137
|
+
const target = typeof targetOrId === 'string' ? this.resolveTarget(targetOrId) : targetOrId;
|
|
131
138
|
|
|
132
139
|
const result: AttachResult = {
|
|
133
140
|
agentsAdded: [],
|
|
@@ -143,18 +150,17 @@ export class AttachManager {
|
|
|
143
150
|
conflicts: [],
|
|
144
151
|
};
|
|
145
152
|
|
|
146
|
-
//
|
|
147
|
-
await fs.mkdir(targetDir, { recursive: true });
|
|
153
|
+
// All paths are relative to projectPath, using target.config.* directly
|
|
148
154
|
|
|
149
155
|
// 1. Attach agents
|
|
150
|
-
await this.attachAgents(
|
|
156
|
+
await this.attachAgents(projectPath, target, templates.agents, result, manifest);
|
|
151
157
|
|
|
152
158
|
// 2. Attach commands
|
|
153
|
-
await this.attachCommands(
|
|
159
|
+
await this.attachCommands(projectPath, target, templates.commands, result, manifest);
|
|
154
160
|
|
|
155
161
|
// 3. Attach rules (if applicable)
|
|
156
162
|
if (templates.rules) {
|
|
157
|
-
await this.attachRules(
|
|
163
|
+
await this.attachRules(projectPath, target, templates.rules, result, manifest);
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
// 4. Attach MCP servers (merge global + template servers)
|
|
@@ -162,18 +168,12 @@ export class AttachManager {
|
|
|
162
168
|
const allMCPServers = [...globalMCPServers, ...templates.mcpServers];
|
|
163
169
|
|
|
164
170
|
if (allMCPServers.length > 0) {
|
|
165
|
-
await this.attachMCPServers(
|
|
166
|
-
targetDir,
|
|
167
|
-
target,
|
|
168
|
-
allMCPServers,
|
|
169
|
-
result,
|
|
170
|
-
manifest
|
|
171
|
-
);
|
|
171
|
+
await this.attachMCPServers(projectPath, target, allMCPServers, result, manifest);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
// 5. Attach hooks
|
|
175
175
|
if (templates.hooks.length > 0) {
|
|
176
|
-
await this.attachHooks(
|
|
176
|
+
await this.attachHooks(projectPath, target, templates.hooks, result, manifest);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// 6. Attach single files
|
|
@@ -189,177 +189,122 @@ export class AttachManager {
|
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
191
|
* Attach agents (override strategy)
|
|
192
|
+
* Uses shared attachItemsToDir function
|
|
192
193
|
*/
|
|
193
194
|
private async attachAgents(
|
|
194
|
-
|
|
195
|
-
target:
|
|
195
|
+
projectPath: string,
|
|
196
|
+
target: Target,
|
|
196
197
|
agents: Array<{ name: string; content: string }>,
|
|
197
198
|
result: AttachResult,
|
|
198
199
|
manifest: BackupManifest
|
|
199
200
|
): Promise<void> {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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);
|
|
201
|
+
const agentsDir = path.join(projectPath, target.config.agentDir);
|
|
202
|
+
const { stats, manifest: itemManifest } = await attachItemsToDir(agents, agentsDir, 'agent');
|
|
203
|
+
|
|
204
|
+
// Update result
|
|
205
|
+
result.agentsAdded.push(...stats.added);
|
|
206
|
+
result.agentsOverridden.push(...stats.overridden);
|
|
207
|
+
result.conflicts.push(
|
|
208
|
+
...stats.conflicts.map((c) => ({ ...c, type: 'agent' as const }))
|
|
209
|
+
);
|
|
226
210
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
211
|
+
// Update manifest
|
|
212
|
+
manifest.backup.agents.user.push(...itemManifest.user);
|
|
213
|
+
manifest.backup.agents.flow.push(...itemManifest.flow);
|
|
230
214
|
}
|
|
231
215
|
|
|
232
216
|
/**
|
|
233
217
|
* Attach commands (override strategy)
|
|
218
|
+
* Uses shared attachItemsToDir function
|
|
234
219
|
*/
|
|
235
220
|
private async attachCommands(
|
|
236
|
-
|
|
237
|
-
target:
|
|
221
|
+
projectPath: string,
|
|
222
|
+
target: Target,
|
|
238
223
|
commands: Array<{ name: string; content: string }>,
|
|
239
224
|
result: AttachResult,
|
|
240
225
|
manifest: BackupManifest
|
|
241
226
|
): Promise<void> {
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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);
|
|
227
|
+
const commandsDir = path.join(projectPath, target.config.slashCommandsDir);
|
|
228
|
+
const { stats, manifest: itemManifest } = await attachItemsToDir(commands, commandsDir, 'command');
|
|
229
|
+
|
|
230
|
+
// Update result
|
|
231
|
+
result.commandsAdded.push(...stats.added);
|
|
232
|
+
result.commandsOverridden.push(...stats.overridden);
|
|
233
|
+
result.conflicts.push(
|
|
234
|
+
...stats.conflicts.map((c) => ({ ...c, type: 'command' as const }))
|
|
235
|
+
);
|
|
268
236
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
237
|
+
// Update manifest
|
|
238
|
+
manifest.backup.commands.user.push(...itemManifest.user);
|
|
239
|
+
manifest.backup.commands.flow.push(...itemManifest.flow);
|
|
272
240
|
}
|
|
273
241
|
|
|
274
242
|
/**
|
|
275
243
|
* Attach rules (append strategy for AGENTS.md)
|
|
244
|
+
* Uses shared attachRulesFile function
|
|
276
245
|
*/
|
|
277
246
|
private async attachRules(
|
|
278
|
-
|
|
279
|
-
target:
|
|
247
|
+
projectPath: string,
|
|
248
|
+
target: Target,
|
|
280
249
|
rules: string,
|
|
281
250
|
result: AttachResult,
|
|
282
251
|
manifest: BackupManifest
|
|
283
252
|
): Promise<void> {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
`;
|
|
253
|
+
const rulesPath = target.config.rulesFile
|
|
254
|
+
? path.join(projectPath, target.config.rulesFile)
|
|
255
|
+
: path.join(projectPath, target.config.agentDir, 'AGENTS.md');
|
|
310
256
|
|
|
311
|
-
|
|
257
|
+
const { originalSize, flowContentAdded } = await attachRulesFile(rulesPath, rules);
|
|
312
258
|
|
|
259
|
+
if (flowContentAdded) {
|
|
313
260
|
manifest.backup.rules = {
|
|
314
261
|
path: rulesPath,
|
|
315
|
-
originalSize
|
|
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,
|
|
262
|
+
originalSize,
|
|
326
263
|
flowContentAdded: true,
|
|
327
264
|
};
|
|
265
|
+
result.rulesAppended = true;
|
|
328
266
|
}
|
|
329
|
-
|
|
330
|
-
result.rulesAppended = true;
|
|
331
267
|
}
|
|
332
268
|
|
|
333
269
|
/**
|
|
334
270
|
* Attach MCP servers (merge strategy)
|
|
271
|
+
* Uses target.config.configFile and target.config.mcpConfigPath
|
|
272
|
+
* Note: configFile is relative to project root, not targetDir
|
|
335
273
|
*/
|
|
336
274
|
private async attachMCPServers(
|
|
337
|
-
|
|
338
|
-
target:
|
|
339
|
-
mcpServers: Array<{ name: string; config:
|
|
275
|
+
projectPath: string,
|
|
276
|
+
target: Target,
|
|
277
|
+
mcpServers: Array<{ name: string; config: Record<string, unknown> }>,
|
|
340
278
|
result: AttachResult,
|
|
341
279
|
manifest: BackupManifest
|
|
342
280
|
): Promise<void> {
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
: path.join(targetDir, '.mcp.json');
|
|
281
|
+
// Use target config for file path and MCP config structure
|
|
282
|
+
// Claude Code: .mcp.json at project root with mcpServers key
|
|
283
|
+
// OpenCode: opencode.jsonc at project root with mcp key
|
|
284
|
+
const configPath = path.join(projectPath, target.config.configFile);
|
|
285
|
+
const mcpPath = target.config.mcpConfigPath;
|
|
349
286
|
|
|
350
|
-
let config:
|
|
287
|
+
let config: Record<string, unknown> = {};
|
|
351
288
|
|
|
352
289
|
if (existsSync(configPath)) {
|
|
353
290
|
config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
354
291
|
}
|
|
355
292
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
293
|
+
// Get or create the MCP servers object at the correct path
|
|
294
|
+
// Claude Code: config.mcpServers = {}
|
|
295
|
+
// OpenCode: config.mcp = {}
|
|
296
|
+
let mcpContainer = config[mcpPath] as Record<string, unknown> | undefined;
|
|
297
|
+
if (!mcpContainer) {
|
|
298
|
+
mcpContainer = {};
|
|
299
|
+
config[mcpPath] = mcpContainer;
|
|
300
|
+
}
|
|
359
301
|
|
|
360
302
|
// Add Flow MCP servers
|
|
361
303
|
for (const server of mcpServers) {
|
|
362
|
-
|
|
304
|
+
// Transform the server config for this target
|
|
305
|
+
const transformedConfig = target.transformMCPConfig(server.config as any, server.name);
|
|
306
|
+
|
|
307
|
+
if (mcpContainer[server.name]) {
|
|
363
308
|
// Conflict: user has same MCP server
|
|
364
309
|
result.mcpServersOverridden.push(server.name);
|
|
365
310
|
result.conflicts.push({
|
|
@@ -372,8 +317,8 @@ ${rules}
|
|
|
372
317
|
result.mcpServersAdded.push(server.name);
|
|
373
318
|
}
|
|
374
319
|
|
|
375
|
-
// Override with Flow config
|
|
376
|
-
|
|
320
|
+
// Override with Flow config (transformed for target)
|
|
321
|
+
mcpContainer[server.name] = transformedConfig;
|
|
377
322
|
}
|
|
378
323
|
|
|
379
324
|
// Write updated config
|
|
@@ -383,7 +328,7 @@ ${rules}
|
|
|
383
328
|
manifest.backup.config = {
|
|
384
329
|
path: configPath,
|
|
385
330
|
hash: await this.calculateFileHash(configPath),
|
|
386
|
-
mcpServersCount: Object.keys(
|
|
331
|
+
mcpServersCount: Object.keys(mcpContainer).length,
|
|
387
332
|
};
|
|
388
333
|
}
|
|
389
334
|
|
|
@@ -391,12 +336,14 @@ ${rules}
|
|
|
391
336
|
* Attach hooks (override strategy)
|
|
392
337
|
*/
|
|
393
338
|
private async attachHooks(
|
|
394
|
-
|
|
339
|
+
projectPath: string,
|
|
340
|
+
target: Target,
|
|
395
341
|
hooks: Array<{ name: string; content: string }>,
|
|
396
342
|
result: AttachResult,
|
|
397
|
-
|
|
343
|
+
_manifest: BackupManifest
|
|
398
344
|
): Promise<void> {
|
|
399
|
-
|
|
345
|
+
// Hooks are in configDir/hooks
|
|
346
|
+
const hooksDir = path.join(projectPath, target.config.configDir, 'hooks');
|
|
400
347
|
await fs.mkdir(hooksDir, { recursive: true });
|
|
401
348
|
|
|
402
349
|
for (const hook of hooks) {
|
|
@@ -430,13 +377,16 @@ ${rules}
|
|
|
430
377
|
result: AttachResult,
|
|
431
378
|
manifest: BackupManifest
|
|
432
379
|
): Promise<void> {
|
|
433
|
-
// Get target from manifest to determine
|
|
434
|
-
const
|
|
435
|
-
|
|
380
|
+
// Get target from manifest to determine config directory
|
|
381
|
+
const targetOption = targetManager.getTarget(manifest.target);
|
|
382
|
+
if (targetOption._tag === 'None') {
|
|
383
|
+
return; // Unknown target, skip
|
|
384
|
+
}
|
|
385
|
+
const target = targetOption.value;
|
|
436
386
|
|
|
437
387
|
for (const file of singleFiles) {
|
|
438
|
-
// Write to target config directory
|
|
439
|
-
const filePath = path.join(
|
|
388
|
+
// Write to target config directory (e.g., .claude/ or .opencode/)
|
|
389
|
+
const filePath = path.join(projectPath, target.config.configDir, file.path);
|
|
440
390
|
const existed = existsSync(filePath);
|
|
441
391
|
|
|
442
392
|
if (existed) {
|
|
@@ -474,13 +424,9 @@ ${rules}
|
|
|
474
424
|
console.log(chalk.yellow('\n⚠️ Conflicts detected:\n'));
|
|
475
425
|
|
|
476
426
|
for (const conflict of result.conflicts) {
|
|
477
|
-
console.log(
|
|
478
|
-
chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`)
|
|
479
|
-
);
|
|
427
|
+
console.log(chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`));
|
|
480
428
|
}
|
|
481
429
|
|
|
482
|
-
console.log(
|
|
483
|
-
chalk.dim('\n Don\'t worry! All overridden content will be restored on exit.\n')
|
|
484
|
-
);
|
|
430
|
+
console.log(chalk.dim("\n Don't worry! All overridden content will be restored on exit.\n"));
|
|
485
431
|
}
|
|
486
432
|
}
|