@sylphx/flow 2.1.3 → 2.1.4
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 +12 -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-command.ts +36 -33
- package/src/config/ai-config.ts +60 -41
- package/src/core/agent-loader.ts +11 -6
- package/src/core/attach-manager.ts +92 -84
- 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 +2 -2
- package/src/services/target-installer.ts +9 -7
- package/src/targets/claude-code.ts +24 -12
- package/src/targets/opencode.ts +17 -6
- 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 +5 -2
- 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
package/src/config/ai-config.ts
CHANGED
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from 'node:fs/promises';
|
|
13
|
-
import path from 'node:path';
|
|
14
13
|
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
-
import { type Result,
|
|
16
|
+
import { type Result, tryCatchAsync } from '../core/functional/result.js';
|
|
17
17
|
import { getAllProviders } from '../providers/index.js';
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
ProviderConfigValue as ProviderConfigValueType,
|
|
20
|
+
ProviderId,
|
|
21
|
+
} from '../types/provider.types.js';
|
|
19
22
|
|
|
20
23
|
// Re-export types for backward compatibility
|
|
21
24
|
export type { ProviderId } from '../types/provider.types.js';
|
|
@@ -39,14 +42,20 @@ export type ProviderConfigValue = ProviderConfigValueType;
|
|
|
39
42
|
* Uses generic Record for provider configs - validation happens at provider level
|
|
40
43
|
*/
|
|
41
44
|
const aiConfigSchema = z.object({
|
|
42
|
-
defaultProvider: z
|
|
45
|
+
defaultProvider: z
|
|
46
|
+
.enum(['anthropic', 'openai', 'google', 'openrouter', 'claude-code', 'zai'])
|
|
47
|
+
.optional(),
|
|
43
48
|
defaultModel: z.string().optional(),
|
|
44
|
-
providers: z
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
providers: z
|
|
50
|
+
.record(
|
|
51
|
+
z.string(),
|
|
52
|
+
z
|
|
53
|
+
.object({
|
|
54
|
+
defaultModel: z.string().optional(),
|
|
55
|
+
})
|
|
56
|
+
.passthrough() // Allow additional fields defined by provider
|
|
57
|
+
)
|
|
58
|
+
.optional(),
|
|
50
59
|
});
|
|
51
60
|
|
|
52
61
|
export type AIConfig = z.infer<typeof aiConfigSchema>;
|
|
@@ -61,7 +70,9 @@ const LOCAL_CONFIG_FILE = '.sylphx-flow/settings.local.json';
|
|
|
61
70
|
/**
|
|
62
71
|
* Get AI config file paths in priority order
|
|
63
72
|
*/
|
|
64
|
-
export const getAIConfigPaths = (
|
|
73
|
+
export const getAIConfigPaths = (
|
|
74
|
+
cwd: string = process.cwd()
|
|
75
|
+
): {
|
|
65
76
|
global: string;
|
|
66
77
|
project: string;
|
|
67
78
|
local: string;
|
|
@@ -79,8 +90,9 @@ const loadConfigFile = async (filePath: string): Promise<AIConfig | null> => {
|
|
|
79
90
|
const content = await fs.readFile(filePath, 'utf8');
|
|
80
91
|
const parsed = JSON.parse(content);
|
|
81
92
|
return aiConfigSchema.parse(parsed);
|
|
82
|
-
} catch (error:
|
|
83
|
-
|
|
93
|
+
} catch (error: unknown) {
|
|
94
|
+
const err = error as NodeJS.ErrnoException;
|
|
95
|
+
if (err.code === 'ENOENT') {
|
|
84
96
|
return null; // File doesn't exist
|
|
85
97
|
}
|
|
86
98
|
throw error; // Re-throw other errors
|
|
@@ -97,7 +109,7 @@ const mergeConfigs = (a: AIConfig, b: AIConfig): AIConfig => {
|
|
|
97
109
|
...Object.keys(b.providers || {}),
|
|
98
110
|
]);
|
|
99
111
|
|
|
100
|
-
const mergedProviders: Record<string,
|
|
112
|
+
const mergedProviders: Record<string, Record<string, unknown>> = {};
|
|
101
113
|
for (const providerId of allProviderIds) {
|
|
102
114
|
mergedProviders[providerId] = {
|
|
103
115
|
...a.providers?.[providerId],
|
|
@@ -117,30 +129,31 @@ const mergeConfigs = (a: AIConfig, b: AIConfig): AIConfig => {
|
|
|
117
129
|
*/
|
|
118
130
|
export const aiConfigExists = async (cwd: string = process.cwd()): Promise<boolean> => {
|
|
119
131
|
const paths = getAIConfigPaths(cwd);
|
|
120
|
-
try {
|
|
121
|
-
// Check any of the config files
|
|
122
|
-
await fs.access(paths.global).catch(() => {});
|
|
123
|
-
return true;
|
|
124
|
-
} catch {}
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
await fs.access(paths.project);
|
|
128
|
-
return true;
|
|
129
|
-
} catch {}
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
// Check if any config file exists
|
|
134
|
+
const checks = await Promise.all([
|
|
135
|
+
fs
|
|
136
|
+
.access(paths.global)
|
|
137
|
+
.then(() => true)
|
|
138
|
+
.catch(() => false),
|
|
139
|
+
fs
|
|
140
|
+
.access(paths.project)
|
|
141
|
+
.then(() => true)
|
|
142
|
+
.catch(() => false),
|
|
143
|
+
fs
|
|
144
|
+
.access(paths.local)
|
|
145
|
+
.then(() => true)
|
|
146
|
+
.catch(() => false),
|
|
147
|
+
]);
|
|
135
148
|
|
|
136
|
-
return
|
|
149
|
+
return checks.some(Boolean);
|
|
137
150
|
};
|
|
138
151
|
|
|
139
152
|
/**
|
|
140
153
|
* Load AI configuration
|
|
141
154
|
* Merges global, project, and local configs with priority: local > project > global
|
|
142
155
|
*/
|
|
143
|
-
export const loadAIConfig =
|
|
156
|
+
export const loadAIConfig = (cwd: string = process.cwd()): Promise<Result<AIConfig, Error>> => {
|
|
144
157
|
return tryCatchAsync(
|
|
145
158
|
async () => {
|
|
146
159
|
const paths = getAIConfigPaths(cwd);
|
|
@@ -156,13 +169,19 @@ export const loadAIConfig = async (cwd: string = process.cwd()): Promise<Result<
|
|
|
156
169
|
let merged: AIConfig = {};
|
|
157
170
|
|
|
158
171
|
// Merge in priority order: global < project < local
|
|
159
|
-
if (globalConfig)
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
if (globalConfig) {
|
|
173
|
+
merged = mergeConfigs(merged, globalConfig);
|
|
174
|
+
}
|
|
175
|
+
if (projectConfig) {
|
|
176
|
+
merged = mergeConfigs(merged, projectConfig);
|
|
177
|
+
}
|
|
178
|
+
if (localConfig) {
|
|
179
|
+
merged = mergeConfigs(merged, localConfig);
|
|
180
|
+
}
|
|
162
181
|
|
|
163
182
|
return merged;
|
|
164
183
|
},
|
|
165
|
-
(error:
|
|
184
|
+
(error: unknown) => new Error(`Failed to load AI config: ${(error as Error).message}`)
|
|
166
185
|
);
|
|
167
186
|
};
|
|
168
187
|
|
|
@@ -171,7 +190,7 @@ export const loadAIConfig = async (cwd: string = process.cwd()): Promise<Result<
|
|
|
171
190
|
* By default, all configuration (including API keys) goes to ~/.sylphx-flow/settings.json
|
|
172
191
|
* Automatically sets default provider if not set
|
|
173
192
|
*/
|
|
174
|
-
export const saveAIConfig =
|
|
193
|
+
export const saveAIConfig = (
|
|
175
194
|
config: AIConfig,
|
|
176
195
|
cwd: string = process.cwd()
|
|
177
196
|
): Promise<Result<void, Error>> => {
|
|
@@ -211,16 +230,16 @@ export const saveAIConfig = async (
|
|
|
211
230
|
const validated = aiConfigSchema.parse(configToSave);
|
|
212
231
|
|
|
213
232
|
// Write config
|
|
214
|
-
await fs.writeFile(configPath, JSON.stringify(validated, null, 2)
|
|
233
|
+
await fs.writeFile(configPath, `${JSON.stringify(validated, null, 2)}\n`, 'utf8');
|
|
215
234
|
},
|
|
216
|
-
(error:
|
|
235
|
+
(error: unknown) => new Error(`Failed to save AI config: ${(error as Error).message}`)
|
|
217
236
|
);
|
|
218
237
|
};
|
|
219
238
|
|
|
220
239
|
/**
|
|
221
240
|
* Save AI configuration to a specific location
|
|
222
241
|
*/
|
|
223
|
-
export const saveAIConfigTo =
|
|
242
|
+
export const saveAIConfigTo = (
|
|
224
243
|
config: AIConfig,
|
|
225
244
|
location: 'global' | 'project' | 'local',
|
|
226
245
|
cwd: string = process.cwd()
|
|
@@ -237,9 +256,10 @@ export const saveAIConfigTo = async (
|
|
|
237
256
|
const validated = aiConfigSchema.parse(config);
|
|
238
257
|
|
|
239
258
|
// Write config
|
|
240
|
-
await fs.writeFile(configPath, JSON.stringify(validated, null, 2)
|
|
259
|
+
await fs.writeFile(configPath, `${JSON.stringify(validated, null, 2)}\n`, 'utf8');
|
|
241
260
|
},
|
|
242
|
-
(error:
|
|
261
|
+
(error: unknown) =>
|
|
262
|
+
new Error(`Failed to save AI config to ${location}: ${(error as Error).message}`)
|
|
243
263
|
);
|
|
244
264
|
};
|
|
245
265
|
|
|
@@ -306,4 +326,3 @@ export const getConfiguredProviders = async (
|
|
|
306
326
|
|
|
307
327
|
return providers;
|
|
308
328
|
};
|
|
309
|
-
|
package/src/core/agent-loader.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Loads agent definitions from markdown files with front matter
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { join, parse, relative, dirname } from 'node:path';
|
|
6
|
+
import { access, readdir, readFile } from 'node:fs/promises';
|
|
8
7
|
import { homedir } from 'node:os';
|
|
8
|
+
import { dirname, join, parse, relative } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import matter from 'gray-matter';
|
|
11
11
|
import type { Agent, AgentMetadata } from '../types/agent.types.js';
|
|
@@ -52,7 +52,10 @@ export async function loadAgentFromFile(
|
|
|
52
52
|
/**
|
|
53
53
|
* Load all agents from a directory (recursively)
|
|
54
54
|
*/
|
|
55
|
-
export async function loadAgentsFromDirectory(
|
|
55
|
+
export async function loadAgentsFromDirectory(
|
|
56
|
+
dirPath: string,
|
|
57
|
+
isBuiltin: boolean = false
|
|
58
|
+
): Promise<Agent[]> {
|
|
56
59
|
try {
|
|
57
60
|
// Read directory recursively to support subdirectories
|
|
58
61
|
const files = await readdir(dirPath, { recursive: true, withFileTypes: true });
|
|
@@ -72,7 +75,7 @@ export async function loadAgentsFromDirectory(dirPath: string, isBuiltin: boolea
|
|
|
72
75
|
);
|
|
73
76
|
|
|
74
77
|
return agents.filter((agent): agent is Agent => agent !== null);
|
|
75
|
-
} catch (
|
|
78
|
+
} catch (_error) {
|
|
76
79
|
// Directory doesn't exist or can't be read
|
|
77
80
|
return [];
|
|
78
81
|
}
|
|
@@ -117,7 +120,7 @@ export async function loadAllAgents(cwd: string, targetAgentDir?: string): Promi
|
|
|
117
120
|
const systemPath = await getSystemAgentsPath();
|
|
118
121
|
const [globalPath, projectPath] = getAgentSearchPaths(cwd);
|
|
119
122
|
|
|
120
|
-
|
|
123
|
+
const allAgentPaths = [systemPath, globalPath, projectPath];
|
|
121
124
|
|
|
122
125
|
// If a target-specific agent directory is provided, add it with highest priority
|
|
123
126
|
if (targetAgentDir) {
|
|
@@ -126,7 +129,9 @@ export async function loadAllAgents(cwd: string, targetAgentDir?: string): Promi
|
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
// Load agents from all paths
|
|
129
|
-
const loadedAgentsPromises = allAgentPaths.map(path =>
|
|
132
|
+
const loadedAgentsPromises = allAgentPaths.map((path) =>
|
|
133
|
+
loadAgentsFromDirectory(path, path === systemPath)
|
|
134
|
+
);
|
|
130
135
|
const loadedAgentsArrays = await Promise.all(loadedAgentsPromises);
|
|
131
136
|
|
|
132
137
|
// Flatten and deduplicate
|
|
@@ -4,15 +4,17 @@
|
|
|
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 type { BackupManifest } from './backup-manager.js';
|
|
16
|
+
import type { ProjectManager } from './project-manager.js';
|
|
17
|
+
import { targetManager } from './target-manager.js';
|
|
16
18
|
|
|
17
19
|
export interface AttachResult {
|
|
18
20
|
agentsAdded: string[];
|
|
@@ -39,13 +41,12 @@ export interface FlowTemplates {
|
|
|
39
41
|
agents: Array<{ name: string; content: string }>;
|
|
40
42
|
commands: Array<{ name: string; content: string }>;
|
|
41
43
|
rules?: string;
|
|
42
|
-
mcpServers: Array<{ name: string; config:
|
|
44
|
+
mcpServers: Array<{ name: string; config: Record<string, unknown> }>;
|
|
43
45
|
hooks: Array<{ name: string; content: string }>;
|
|
44
46
|
singleFiles: Array<{ path: string; content: string }>;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export class AttachManager {
|
|
48
|
-
private projectManager: ProjectManager;
|
|
49
50
|
private configService: GlobalConfigService;
|
|
50
51
|
|
|
51
52
|
constructor(projectManager: ProjectManager) {
|
|
@@ -66,26 +67,25 @@ export class AttachManager {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
|
-
*
|
|
70
|
+
* Resolve target from ID string to Target object
|
|
70
71
|
*/
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
: { agents: 'agent', commands: 'command' };
|
|
72
|
+
private resolveTarget(targetId: string): Target {
|
|
73
|
+
const targetOption = targetManager.getTarget(targetId);
|
|
74
|
+
if (targetOption._tag === 'None') {
|
|
75
|
+
throw new Error(`Unknown target: ${targetId}`);
|
|
76
|
+
}
|
|
77
|
+
return targetOption.value;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Load global MCP servers from ~/.sylphx-flow/mcp-config.json
|
|
82
82
|
*/
|
|
83
83
|
private async loadGlobalMCPServers(
|
|
84
|
-
|
|
85
|
-
): Promise<Array<{ name: string; config:
|
|
84
|
+
_target: Target
|
|
85
|
+
): Promise<Array<{ name: string; config: Record<string, unknown> }>> {
|
|
86
86
|
try {
|
|
87
87
|
const enabledServers = await this.configService.getEnabledMCPServers();
|
|
88
|
-
const servers: Array<{ name: string; config:
|
|
88
|
+
const servers: Array<{ name: string; config: Record<string, unknown> }> = [];
|
|
89
89
|
|
|
90
90
|
for (const [serverKey, serverConfig] of Object.entries(enabledServers)) {
|
|
91
91
|
// Lookup server definition in registry
|
|
@@ -97,7 +97,7 @@ export class AttachManager {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// Clone the server config from registry
|
|
100
|
-
|
|
100
|
+
const config: Record<string, unknown> = { ...serverDef.config };
|
|
101
101
|
|
|
102
102
|
// Merge environment variables from global config
|
|
103
103
|
if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
|
|
@@ -110,7 +110,7 @@ export class AttachManager {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
return servers;
|
|
113
|
-
} catch (
|
|
113
|
+
} catch (_error) {
|
|
114
114
|
// If global config doesn't exist or fails to load, return empty array
|
|
115
115
|
return [];
|
|
116
116
|
}
|
|
@@ -119,15 +119,21 @@ export class AttachManager {
|
|
|
119
119
|
/**
|
|
120
120
|
* Attach Flow templates to project
|
|
121
121
|
* Strategy: Override with warning, backup handles restoration
|
|
122
|
+
* @param projectPath - Project root path
|
|
123
|
+
* @param _projectHash - Project hash (unused but kept for API compatibility)
|
|
124
|
+
* @param targetOrId - Target object or target ID string
|
|
125
|
+
* @param templates - Flow templates to attach
|
|
126
|
+
* @param manifest - Backup manifest to track changes
|
|
122
127
|
*/
|
|
123
128
|
async attach(
|
|
124
129
|
projectPath: string,
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
_projectHash: string,
|
|
131
|
+
targetOrId: Target | string,
|
|
127
132
|
templates: FlowTemplates,
|
|
128
133
|
manifest: BackupManifest
|
|
129
134
|
): Promise<AttachResult> {
|
|
130
|
-
|
|
135
|
+
// Resolve target from ID if needed
|
|
136
|
+
const target = typeof targetOrId === 'string' ? this.resolveTarget(targetOrId) : targetOrId;
|
|
131
137
|
|
|
132
138
|
const result: AttachResult = {
|
|
133
139
|
agentsAdded: [],
|
|
@@ -143,18 +149,17 @@ export class AttachManager {
|
|
|
143
149
|
conflicts: [],
|
|
144
150
|
};
|
|
145
151
|
|
|
146
|
-
//
|
|
147
|
-
await fs.mkdir(targetDir, { recursive: true });
|
|
152
|
+
// All paths are relative to projectPath, using target.config.* directly
|
|
148
153
|
|
|
149
154
|
// 1. Attach agents
|
|
150
|
-
await this.attachAgents(
|
|
155
|
+
await this.attachAgents(projectPath, target, templates.agents, result, manifest);
|
|
151
156
|
|
|
152
157
|
// 2. Attach commands
|
|
153
|
-
await this.attachCommands(
|
|
158
|
+
await this.attachCommands(projectPath, target, templates.commands, result, manifest);
|
|
154
159
|
|
|
155
160
|
// 3. Attach rules (if applicable)
|
|
156
161
|
if (templates.rules) {
|
|
157
|
-
await this.attachRules(
|
|
162
|
+
await this.attachRules(projectPath, target, templates.rules, result, manifest);
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
// 4. Attach MCP servers (merge global + template servers)
|
|
@@ -162,18 +167,12 @@ export class AttachManager {
|
|
|
162
167
|
const allMCPServers = [...globalMCPServers, ...templates.mcpServers];
|
|
163
168
|
|
|
164
169
|
if (allMCPServers.length > 0) {
|
|
165
|
-
await this.attachMCPServers(
|
|
166
|
-
targetDir,
|
|
167
|
-
target,
|
|
168
|
-
allMCPServers,
|
|
169
|
-
result,
|
|
170
|
-
manifest
|
|
171
|
-
);
|
|
170
|
+
await this.attachMCPServers(projectPath, target, allMCPServers, result, manifest);
|
|
172
171
|
}
|
|
173
172
|
|
|
174
173
|
// 5. Attach hooks
|
|
175
174
|
if (templates.hooks.length > 0) {
|
|
176
|
-
await this.attachHooks(
|
|
175
|
+
await this.attachHooks(projectPath, target, templates.hooks, result, manifest);
|
|
177
176
|
}
|
|
178
177
|
|
|
179
178
|
// 6. Attach single files
|
|
@@ -191,14 +190,14 @@ export class AttachManager {
|
|
|
191
190
|
* Attach agents (override strategy)
|
|
192
191
|
*/
|
|
193
192
|
private async attachAgents(
|
|
194
|
-
|
|
195
|
-
target:
|
|
193
|
+
projectPath: string,
|
|
194
|
+
target: Target,
|
|
196
195
|
agents: Array<{ name: string; content: string }>,
|
|
197
196
|
result: AttachResult,
|
|
198
197
|
manifest: BackupManifest
|
|
199
198
|
): Promise<void> {
|
|
200
|
-
|
|
201
|
-
const agentsDir = path.join(
|
|
199
|
+
// Use full path from target config
|
|
200
|
+
const agentsDir = path.join(projectPath, target.config.agentDir);
|
|
202
201
|
await fs.mkdir(agentsDir, { recursive: true });
|
|
203
202
|
|
|
204
203
|
for (const agent of agents) {
|
|
@@ -233,14 +232,14 @@ export class AttachManager {
|
|
|
233
232
|
* Attach commands (override strategy)
|
|
234
233
|
*/
|
|
235
234
|
private async attachCommands(
|
|
236
|
-
|
|
237
|
-
target:
|
|
235
|
+
projectPath: string,
|
|
236
|
+
target: Target,
|
|
238
237
|
commands: Array<{ name: string; content: string }>,
|
|
239
238
|
result: AttachResult,
|
|
240
239
|
manifest: BackupManifest
|
|
241
240
|
): Promise<void> {
|
|
242
|
-
|
|
243
|
-
const commandsDir = path.join(
|
|
241
|
+
// Use full path from target config
|
|
242
|
+
const commandsDir = path.join(projectPath, target.config.slashCommandsDir);
|
|
244
243
|
await fs.mkdir(commandsDir, { recursive: true });
|
|
245
244
|
|
|
246
245
|
for (const command of commands) {
|
|
@@ -275,19 +274,18 @@ export class AttachManager {
|
|
|
275
274
|
* Attach rules (append strategy for AGENTS.md)
|
|
276
275
|
*/
|
|
277
276
|
private async attachRules(
|
|
278
|
-
|
|
279
|
-
target:
|
|
277
|
+
projectPath: string,
|
|
278
|
+
target: Target,
|
|
280
279
|
rules: string,
|
|
281
280
|
result: AttachResult,
|
|
282
281
|
manifest: BackupManifest
|
|
283
282
|
): Promise<void> {
|
|
284
|
-
//
|
|
285
|
-
// OpenCode:
|
|
286
|
-
|
|
287
|
-
const rulesPath =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
: path.join(targetDir, 'AGENTS.md');
|
|
283
|
+
// Use full paths from target config:
|
|
284
|
+
// - rulesFile defined (e.g., OpenCode): projectPath/rulesFile
|
|
285
|
+
// - rulesFile undefined (e.g., Claude Code): projectPath/agentDir/AGENTS.md
|
|
286
|
+
const rulesPath = target.config.rulesFile
|
|
287
|
+
? path.join(projectPath, target.config.rulesFile)
|
|
288
|
+
: path.join(projectPath, target.config.agentDir, 'AGENTS.md');
|
|
291
289
|
|
|
292
290
|
if (existsSync(rulesPath)) {
|
|
293
291
|
// User has AGENTS.md, append Flow rules
|
|
@@ -332,34 +330,43 @@ ${rules}
|
|
|
332
330
|
|
|
333
331
|
/**
|
|
334
332
|
* Attach MCP servers (merge strategy)
|
|
333
|
+
* Uses target.config.configFile and target.config.mcpConfigPath
|
|
334
|
+
* Note: configFile is relative to project root, not targetDir
|
|
335
335
|
*/
|
|
336
336
|
private async attachMCPServers(
|
|
337
|
-
|
|
338
|
-
target:
|
|
339
|
-
mcpServers: Array<{ name: string; config:
|
|
337
|
+
projectPath: string,
|
|
338
|
+
target: Target,
|
|
339
|
+
mcpServers: Array<{ name: string; config: Record<string, unknown> }>,
|
|
340
340
|
result: AttachResult,
|
|
341
341
|
manifest: BackupManifest
|
|
342
342
|
): Promise<void> {
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
: path.join(targetDir, '.mcp.json');
|
|
343
|
+
// Use target config for file path and MCP config structure
|
|
344
|
+
// Claude Code: .mcp.json at project root with mcpServers key
|
|
345
|
+
// OpenCode: opencode.jsonc at project root with mcp key
|
|
346
|
+
const configPath = path.join(projectPath, target.config.configFile);
|
|
347
|
+
const mcpPath = target.config.mcpConfigPath;
|
|
349
348
|
|
|
350
|
-
let config:
|
|
349
|
+
let config: Record<string, unknown> = {};
|
|
351
350
|
|
|
352
351
|
if (existsSync(configPath)) {
|
|
353
352
|
config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
354
353
|
}
|
|
355
354
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
355
|
+
// Get or create the MCP servers object at the correct path
|
|
356
|
+
// Claude Code: config.mcpServers = {}
|
|
357
|
+
// OpenCode: config.mcp = {}
|
|
358
|
+
let mcpContainer = config[mcpPath] as Record<string, unknown> | undefined;
|
|
359
|
+
if (!mcpContainer) {
|
|
360
|
+
mcpContainer = {};
|
|
361
|
+
config[mcpPath] = mcpContainer;
|
|
362
|
+
}
|
|
359
363
|
|
|
360
364
|
// Add Flow MCP servers
|
|
361
365
|
for (const server of mcpServers) {
|
|
362
|
-
|
|
366
|
+
// Transform the server config for this target
|
|
367
|
+
const transformedConfig = target.transformMCPConfig(server.config as any, server.name);
|
|
368
|
+
|
|
369
|
+
if (mcpContainer[server.name]) {
|
|
363
370
|
// Conflict: user has same MCP server
|
|
364
371
|
result.mcpServersOverridden.push(server.name);
|
|
365
372
|
result.conflicts.push({
|
|
@@ -372,8 +379,8 @@ ${rules}
|
|
|
372
379
|
result.mcpServersAdded.push(server.name);
|
|
373
380
|
}
|
|
374
381
|
|
|
375
|
-
// Override with Flow config
|
|
376
|
-
|
|
382
|
+
// Override with Flow config (transformed for target)
|
|
383
|
+
mcpContainer[server.name] = transformedConfig;
|
|
377
384
|
}
|
|
378
385
|
|
|
379
386
|
// Write updated config
|
|
@@ -383,7 +390,7 @@ ${rules}
|
|
|
383
390
|
manifest.backup.config = {
|
|
384
391
|
path: configPath,
|
|
385
392
|
hash: await this.calculateFileHash(configPath),
|
|
386
|
-
mcpServersCount: Object.keys(
|
|
393
|
+
mcpServersCount: Object.keys(mcpContainer).length,
|
|
387
394
|
};
|
|
388
395
|
}
|
|
389
396
|
|
|
@@ -391,12 +398,14 @@ ${rules}
|
|
|
391
398
|
* Attach hooks (override strategy)
|
|
392
399
|
*/
|
|
393
400
|
private async attachHooks(
|
|
394
|
-
|
|
401
|
+
projectPath: string,
|
|
402
|
+
target: Target,
|
|
395
403
|
hooks: Array<{ name: string; content: string }>,
|
|
396
404
|
result: AttachResult,
|
|
397
|
-
|
|
405
|
+
_manifest: BackupManifest
|
|
398
406
|
): Promise<void> {
|
|
399
|
-
|
|
407
|
+
// Hooks are in configDir/hooks
|
|
408
|
+
const hooksDir = path.join(projectPath, target.config.configDir, 'hooks');
|
|
400
409
|
await fs.mkdir(hooksDir, { recursive: true });
|
|
401
410
|
|
|
402
411
|
for (const hook of hooks) {
|
|
@@ -430,13 +439,16 @@ ${rules}
|
|
|
430
439
|
result: AttachResult,
|
|
431
440
|
manifest: BackupManifest
|
|
432
441
|
): Promise<void> {
|
|
433
|
-
// Get target from manifest to determine
|
|
434
|
-
const
|
|
435
|
-
|
|
442
|
+
// Get target from manifest to determine config directory
|
|
443
|
+
const targetOption = targetManager.getTarget(manifest.target);
|
|
444
|
+
if (targetOption._tag === 'None') {
|
|
445
|
+
return; // Unknown target, skip
|
|
446
|
+
}
|
|
447
|
+
const target = targetOption.value;
|
|
436
448
|
|
|
437
449
|
for (const file of singleFiles) {
|
|
438
|
-
// Write to target config directory
|
|
439
|
-
const filePath = path.join(
|
|
450
|
+
// Write to target config directory (e.g., .claude/ or .opencode/)
|
|
451
|
+
const filePath = path.join(projectPath, target.config.configDir, file.path);
|
|
440
452
|
const existed = existsSync(filePath);
|
|
441
453
|
|
|
442
454
|
if (existed) {
|
|
@@ -474,13 +486,9 @@ ${rules}
|
|
|
474
486
|
console.log(chalk.yellow('\n⚠️ Conflicts detected:\n'));
|
|
475
487
|
|
|
476
488
|
for (const conflict of result.conflicts) {
|
|
477
|
-
console.log(
|
|
478
|
-
chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`)
|
|
479
|
-
);
|
|
489
|
+
console.log(chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`));
|
|
480
490
|
}
|
|
481
491
|
|
|
482
|
-
console.log(
|
|
483
|
-
chalk.dim('\n Don\'t worry! All overridden content will be restored on exit.\n')
|
|
484
|
-
);
|
|
492
|
+
console.log(chalk.dim("\n Don't worry! All overridden content will be restored on exit.\n"));
|
|
485
493
|
}
|
|
486
494
|
}
|