@sylphx/flow 2.1.4 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/package.json +1 -1
- package/src/commands/settings/checkbox-config.ts +128 -0
- package/src/commands/settings/index.ts +6 -0
- package/src/commands/settings-command.ts +76 -156
- package/src/config/servers.ts +67 -0
- package/src/core/attach/file-attacher.ts +170 -0
- package/src/core/attach/index.ts +5 -0
- package/src/core/attach-manager.ts +42 -101
- package/src/services/global-config.ts +24 -6
- package/src/services/target-installer.ts +2 -19
- package/src/targets/claude-code.ts +12 -70
- package/src/targets/opencode.ts +12 -63
- package/src/targets/shared/index.ts +7 -0
- package/src/targets/shared/mcp-transforms.ts +146 -0
- package/src/targets/shared/target-operations.ts +128 -0
- package/src/utils/target-selection.ts +1 -6
|
@@ -0,0 +1,170 @@
|
|
|
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> => fs.readFile(filePath, 'utf-8');
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Generic Attach Function
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Attach multiple items to a directory with conflict tracking
|
|
83
|
+
* Pure function that returns stats and manifest updates
|
|
84
|
+
*/
|
|
85
|
+
export const attachItemsToDir = async (
|
|
86
|
+
items: AttachItem[],
|
|
87
|
+
targetDir: string,
|
|
88
|
+
itemType: string
|
|
89
|
+
): Promise<{ stats: AttachStats; manifest: ManifestTracker }> => {
|
|
90
|
+
await ensureDir(targetDir);
|
|
91
|
+
|
|
92
|
+
const stats: AttachStats = {
|
|
93
|
+
added: [],
|
|
94
|
+
overridden: [],
|
|
95
|
+
conflicts: [],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const manifest: ManifestTracker = {
|
|
99
|
+
user: [],
|
|
100
|
+
flow: [],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
for (const item of items) {
|
|
104
|
+
const itemPath = path.join(targetDir, item.name);
|
|
105
|
+
const existed = fileExists(itemPath);
|
|
106
|
+
|
|
107
|
+
if (existed) {
|
|
108
|
+
stats.overridden.push(item.name);
|
|
109
|
+
stats.conflicts.push(createConflict(itemType, item.name, 'overridden'));
|
|
110
|
+
manifest.user.push(item.name);
|
|
111
|
+
} else {
|
|
112
|
+
stats.added.push(item.name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await writeFile(itemPath, item.content);
|
|
116
|
+
manifest.flow.push(item.name);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { stats, manifest };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Rules Attachment (Append Strategy)
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
const FLOW_RULES_START = '<!-- ========== Sylphx Flow Rules (Auto-injected) ========== -->';
|
|
127
|
+
const FLOW_RULES_END = '<!-- ========== End of Sylphx Flow Rules ========== -->';
|
|
128
|
+
const FLOW_RULES_MARKER = '<!-- Sylphx Flow Rules -->';
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if content already has Flow rules appended
|
|
132
|
+
*/
|
|
133
|
+
export const hasFlowRules = (content: string): boolean => content.includes(FLOW_RULES_MARKER);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Wrap rules content with markers
|
|
137
|
+
*/
|
|
138
|
+
export const wrapRulesContent = (rules: string): string =>
|
|
139
|
+
`\n\n${FLOW_RULES_START}\n\n${rules}\n\n${FLOW_RULES_END}\n`;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Append rules to existing content
|
|
143
|
+
*/
|
|
144
|
+
export const appendRules = (existingContent: string, rules: string): string =>
|
|
145
|
+
existingContent + wrapRulesContent(rules);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Attach rules file with append strategy
|
|
149
|
+
*/
|
|
150
|
+
export const attachRulesFile = async (
|
|
151
|
+
rulesPath: string,
|
|
152
|
+
rules: string
|
|
153
|
+
): Promise<{ originalSize: number; flowContentAdded: boolean }> => {
|
|
154
|
+
if (fileExists(rulesPath)) {
|
|
155
|
+
const existingContent = await readFile(rulesPath);
|
|
156
|
+
|
|
157
|
+
// Skip if already appended
|
|
158
|
+
if (hasFlowRules(existingContent)) {
|
|
159
|
+
return { originalSize: existingContent.length, flowContentAdded: false };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await writeFile(rulesPath, appendRules(existingContent, rules));
|
|
163
|
+
return { originalSize: existingContent.length, flowContentAdded: true };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create new file
|
|
167
|
+
await ensureDir(path.dirname(rulesPath));
|
|
168
|
+
await writeFile(rulesPath, rules);
|
|
169
|
+
return { originalSize: 0, flowContentAdded: true };
|
|
170
|
+
};
|
|
@@ -9,9 +9,10 @@ import { existsSync } from 'node:fs';
|
|
|
9
9
|
import fs from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
-
import { MCP_SERVER_REGISTRY } from '../config/servers.js';
|
|
12
|
+
import { MCP_SERVER_REGISTRY, type MCPServerID } from '../config/servers.js';
|
|
13
13
|
import { GlobalConfigService } from '../services/global-config.js';
|
|
14
14
|
import type { Target } from '../types/target.types.js';
|
|
15
|
+
import { attachItemsToDir, attachRulesFile } from './attach/index.js';
|
|
15
16
|
import type { BackupManifest } from './backup-manager.js';
|
|
16
17
|
import type { ProjectManager } from './project-manager.js';
|
|
17
18
|
import { targetManager } from './target-manager.js';
|
|
@@ -79,30 +80,33 @@ export class AttachManager {
|
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
82
|
* Load global MCP servers from ~/.sylphx-flow/mcp-config.json
|
|
83
|
+
* Uses SSOT: computeEffectiveServers for determining enabled servers
|
|
82
84
|
*/
|
|
83
85
|
private async loadGlobalMCPServers(
|
|
84
86
|
_target: Target
|
|
85
87
|
): Promise<Array<{ name: string; config: Record<string, unknown> }>> {
|
|
86
88
|
try {
|
|
87
|
-
const
|
|
89
|
+
const mcpConfig = await this.configService.loadMCPConfig();
|
|
90
|
+
const enabledServerIds = this.configService.getEnabledServerIds(mcpConfig.servers);
|
|
91
|
+
const effectiveServers = this.configService.getEffectiveMCPServers(mcpConfig.servers);
|
|
92
|
+
|
|
88
93
|
const servers: Array<{ name: string; config: Record<string, unknown> }> = [];
|
|
89
94
|
|
|
90
|
-
for (const
|
|
91
|
-
|
|
92
|
-
const
|
|
95
|
+
for (const serverId of enabledServerIds) {
|
|
96
|
+
const serverDef = MCP_SERVER_REGISTRY[serverId as MCPServerID];
|
|
97
|
+
const effective = effectiveServers[serverId as MCPServerID];
|
|
93
98
|
|
|
94
99
|
if (!serverDef) {
|
|
95
|
-
console.warn(`MCP server '${serverKey}' not found in registry, skipping`);
|
|
96
100
|
continue;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
// Clone the server config from registry
|
|
100
104
|
const config: Record<string, unknown> = { ...serverDef.config };
|
|
101
105
|
|
|
102
|
-
// Merge environment variables from
|
|
103
|
-
if (
|
|
106
|
+
// Merge environment variables from effective config (SSOT)
|
|
107
|
+
if (effective.env && Object.keys(effective.env).length > 0) {
|
|
104
108
|
if (config.type === 'stdio' || config.type === 'local') {
|
|
105
|
-
config.env = { ...config.env, ...
|
|
109
|
+
config.env = { ...config.env, ...effective.env };
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
|
|
@@ -188,6 +192,7 @@ export class AttachManager {
|
|
|
188
192
|
|
|
189
193
|
/**
|
|
190
194
|
* Attach agents (override strategy)
|
|
195
|
+
* Uses shared attachItemsToDir function
|
|
191
196
|
*/
|
|
192
197
|
private async attachAgents(
|
|
193
198
|
projectPath: string,
|
|
@@ -196,40 +201,22 @@ export class AttachManager {
|
|
|
196
201
|
result: AttachResult,
|
|
197
202
|
manifest: BackupManifest
|
|
198
203
|
): Promise<void> {
|
|
199
|
-
// Use full path from target config
|
|
200
204
|
const agentsDir = path.join(projectPath, target.config.agentDir);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
for (const agent of agents) {
|
|
204
|
-
const agentPath = path.join(agentsDir, agent.name);
|
|
205
|
-
const existed = existsSync(agentPath);
|
|
206
|
-
|
|
207
|
-
if (existed) {
|
|
208
|
-
// Conflict: user has same agent
|
|
209
|
-
result.agentsOverridden.push(agent.name);
|
|
210
|
-
result.conflicts.push({
|
|
211
|
-
type: 'agent',
|
|
212
|
-
name: agent.name,
|
|
213
|
-
action: 'overridden',
|
|
214
|
-
message: `Agent '${agent.name}' overridden (will be restored on exit)`,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Track in manifest
|
|
218
|
-
manifest.backup.agents.user.push(agent.name);
|
|
219
|
-
} else {
|
|
220
|
-
result.agentsAdded.push(agent.name);
|
|
221
|
-
}
|
|
205
|
+
const { stats, manifest: itemManifest } = await attachItemsToDir(agents, agentsDir, 'agent');
|
|
222
206
|
|
|
223
|
-
|
|
224
|
-
|
|
207
|
+
// Update result
|
|
208
|
+
result.agentsAdded.push(...stats.added);
|
|
209
|
+
result.agentsOverridden.push(...stats.overridden);
|
|
210
|
+
result.conflicts.push(...stats.conflicts.map((c) => ({ ...c, type: 'agent' as const })));
|
|
225
211
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
212
|
+
// Update manifest
|
|
213
|
+
manifest.backup.agents.user.push(...itemManifest.user);
|
|
214
|
+
manifest.backup.agents.flow.push(...itemManifest.flow);
|
|
229
215
|
}
|
|
230
216
|
|
|
231
217
|
/**
|
|
232
218
|
* Attach commands (override strategy)
|
|
219
|
+
* Uses shared attachItemsToDir function
|
|
233
220
|
*/
|
|
234
221
|
private async attachCommands(
|
|
235
222
|
projectPath: string,
|
|
@@ -238,40 +225,26 @@ export class AttachManager {
|
|
|
238
225
|
result: AttachResult,
|
|
239
226
|
manifest: BackupManifest
|
|
240
227
|
): Promise<void> {
|
|
241
|
-
// Use full path from target config
|
|
242
228
|
const commandsDir = path.join(projectPath, target.config.slashCommandsDir);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Track in manifest
|
|
260
|
-
manifest.backup.commands.user.push(command.name);
|
|
261
|
-
} else {
|
|
262
|
-
result.commandsAdded.push(command.name);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Write Flow command (override)
|
|
266
|
-
await fs.writeFile(commandPath, command.content);
|
|
267
|
-
|
|
268
|
-
// Track Flow command
|
|
269
|
-
manifest.backup.commands.flow.push(command.name);
|
|
270
|
-
}
|
|
229
|
+
const { stats, manifest: itemManifest } = await attachItemsToDir(
|
|
230
|
+
commands,
|
|
231
|
+
commandsDir,
|
|
232
|
+
'command'
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Update result
|
|
236
|
+
result.commandsAdded.push(...stats.added);
|
|
237
|
+
result.commandsOverridden.push(...stats.overridden);
|
|
238
|
+
result.conflicts.push(...stats.conflicts.map((c) => ({ ...c, type: 'command' as const })));
|
|
239
|
+
|
|
240
|
+
// Update manifest
|
|
241
|
+
manifest.backup.commands.user.push(...itemManifest.user);
|
|
242
|
+
manifest.backup.commands.flow.push(...itemManifest.flow);
|
|
271
243
|
}
|
|
272
244
|
|
|
273
245
|
/**
|
|
274
246
|
* Attach rules (append strategy for AGENTS.md)
|
|
247
|
+
* Uses shared attachRulesFile function
|
|
275
248
|
*/
|
|
276
249
|
private async attachRules(
|
|
277
250
|
projectPath: string,
|
|
@@ -280,52 +253,20 @@ export class AttachManager {
|
|
|
280
253
|
result: AttachResult,
|
|
281
254
|
manifest: BackupManifest
|
|
282
255
|
): Promise<void> {
|
|
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
256
|
const rulesPath = target.config.rulesFile
|
|
287
257
|
? path.join(projectPath, target.config.rulesFile)
|
|
288
258
|
: path.join(projectPath, target.config.agentDir, 'AGENTS.md');
|
|
289
259
|
|
|
290
|
-
|
|
291
|
-
// User has AGENTS.md, append Flow rules
|
|
292
|
-
const userRules = await fs.readFile(rulesPath, 'utf-8');
|
|
293
|
-
|
|
294
|
-
// Check if already appended (avoid duplicates)
|
|
295
|
-
if (userRules.includes('<!-- Sylphx Flow Rules -->')) {
|
|
296
|
-
// Already appended, skip
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const merged = `${userRules}
|
|
301
|
-
|
|
302
|
-
<!-- ========== Sylphx Flow Rules (Auto-injected) ========== -->
|
|
303
|
-
|
|
304
|
-
${rules}
|
|
305
|
-
|
|
306
|
-
<!-- ========== End of Sylphx Flow Rules ========== -->
|
|
307
|
-
`;
|
|
308
|
-
|
|
309
|
-
await fs.writeFile(rulesPath, merged);
|
|
260
|
+
const { originalSize, flowContentAdded } = await attachRulesFile(rulesPath, rules);
|
|
310
261
|
|
|
262
|
+
if (flowContentAdded) {
|
|
311
263
|
manifest.backup.rules = {
|
|
312
264
|
path: rulesPath,
|
|
313
|
-
originalSize
|
|
314
|
-
flowContentAdded: true,
|
|
315
|
-
};
|
|
316
|
-
} else {
|
|
317
|
-
// User doesn't have AGENTS.md, create new
|
|
318
|
-
await fs.mkdir(path.dirname(rulesPath), { recursive: true });
|
|
319
|
-
await fs.writeFile(rulesPath, rules);
|
|
320
|
-
|
|
321
|
-
manifest.backup.rules = {
|
|
322
|
-
path: rulesPath,
|
|
323
|
-
originalSize: 0,
|
|
265
|
+
originalSize,
|
|
324
266
|
flowContentAdded: true,
|
|
325
267
|
};
|
|
268
|
+
result.rulesAppended = true;
|
|
326
269
|
}
|
|
327
|
-
|
|
328
|
-
result.rulesAppended = true;
|
|
329
270
|
}
|
|
330
271
|
|
|
331
272
|
/**
|
|
@@ -7,10 +7,15 @@ import { existsSync } from 'node:fs';
|
|
|
7
7
|
import fs from 'node:fs/promises';
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
+
import {
|
|
11
|
+
computeEffectiveServers,
|
|
12
|
+
getEnabledServersFromEffective,
|
|
13
|
+
type MCPServerID,
|
|
14
|
+
} from '../config/servers.js';
|
|
10
15
|
|
|
11
16
|
export interface GlobalSettings {
|
|
12
17
|
version: string;
|
|
13
|
-
defaultTarget?: 'claude-code' | 'opencode' | '
|
|
18
|
+
defaultTarget?: 'claude-code' | 'opencode' | 'ask-every-time';
|
|
14
19
|
defaultAgent?: string; // Default agent to use (e.g., 'coder', 'writer', 'reviewer', 'orchestrator')
|
|
15
20
|
firstRun: boolean;
|
|
16
21
|
lastUpdated: string;
|
|
@@ -198,22 +203,35 @@ export class GlobalConfigService {
|
|
|
198
203
|
}
|
|
199
204
|
|
|
200
205
|
/**
|
|
201
|
-
* Load MCP config
|
|
206
|
+
* Load raw MCP config from file (may be empty if no file)
|
|
202
207
|
*/
|
|
203
208
|
async loadMCPConfig(): Promise<MCPConfig> {
|
|
204
209
|
const configPath = this.getMCPConfigPath();
|
|
205
210
|
|
|
206
211
|
if (!existsSync(configPath)) {
|
|
207
|
-
return {
|
|
208
|
-
version: '1.0.0',
|
|
209
|
-
servers: {},
|
|
210
|
-
};
|
|
212
|
+
return { version: '1.0.0', servers: {} };
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
const data = await fs.readFile(configPath, 'utf-8');
|
|
214
216
|
return JSON.parse(data);
|
|
215
217
|
}
|
|
216
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Get effective MCP servers using SSOT computation
|
|
221
|
+
* Merges saved config with defaults from registry
|
|
222
|
+
*/
|
|
223
|
+
getEffectiveMCPServers(savedServers: Record<string, MCPServerConfig> | undefined) {
|
|
224
|
+
return computeEffectiveServers(savedServers);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get enabled server IDs using SSOT
|
|
229
|
+
*/
|
|
230
|
+
getEnabledServerIds(savedServers: Record<string, MCPServerConfig> | undefined): MCPServerID[] {
|
|
231
|
+
const effective = computeEffectiveServers(savedServers);
|
|
232
|
+
return getEnabledServersFromEffective(effective);
|
|
233
|
+
}
|
|
234
|
+
|
|
217
235
|
/**
|
|
218
236
|
* Save MCP config
|
|
219
237
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Target Installation Service
|
|
3
|
-
* Auto-detects and installs AI CLI tools (Claude Code, OpenCode
|
|
3
|
+
* Auto-detects and installs AI CLI tools (Claude Code, OpenCode)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { exec } from 'node:child_process';
|
|
@@ -14,7 +14,7 @@ import { detectPackageManager, type PackageManager } from '../utils/package-mana
|
|
|
14
14
|
const execAsync = promisify(exec);
|
|
15
15
|
|
|
16
16
|
export interface TargetInstallation {
|
|
17
|
-
id: 'claude-code' | 'opencode'
|
|
17
|
+
id: 'claude-code' | 'opencode';
|
|
18
18
|
name: string;
|
|
19
19
|
package: string;
|
|
20
20
|
checkCommand: string;
|
|
@@ -61,16 +61,6 @@ const TARGET_INSTALLATIONS: TargetInstallation[] = [
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
},
|
|
64
|
-
{
|
|
65
|
-
id: 'cursor',
|
|
66
|
-
name: 'Cursor',
|
|
67
|
-
package: 'cursor',
|
|
68
|
-
checkCommand: 'cursor --version',
|
|
69
|
-
installCommand: () => {
|
|
70
|
-
// Cursor is typically installed via installer, not npm
|
|
71
|
-
return 'Visit https://cursor.sh to download and install';
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
64
|
];
|
|
75
65
|
|
|
76
66
|
export class TargetInstaller {
|
|
@@ -160,13 +150,6 @@ export class TargetInstaller {
|
|
|
160
150
|
return false;
|
|
161
151
|
}
|
|
162
152
|
|
|
163
|
-
// Special handling for Cursor (not npm-installable)
|
|
164
|
-
if (targetId === 'cursor') {
|
|
165
|
-
console.log(chalk.yellow('\n⚠️ Cursor requires manual installation'));
|
|
166
|
-
console.log(chalk.cyan(' Visit https://cursor.sh to download and install\n'));
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
153
|
// Confirm installation unless auto-confirm is enabled
|
|
171
154
|
if (!autoConfirm) {
|
|
172
155
|
try {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
2
|
import fsPromises from 'node:fs/promises';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import chalk from 'chalk';
|
|
@@ -7,10 +6,16 @@ import { installToDirectory } from '../core/installers/file-installer.js';
|
|
|
7
6
|
import { createMCPInstaller } from '../core/installers/mcp-installer.js';
|
|
8
7
|
import type { AgentMetadata } from '../types/target-config.types.js';
|
|
9
8
|
import type { CommonOptions, MCPServerConfigUnion, SetupResult, Target } from '../types.js';
|
|
10
|
-
import { getAgentsDir
|
|
9
|
+
import { getAgentsDir } from '../utils/config/paths.js';
|
|
11
10
|
import { fileUtils, generateHelpText, pathUtils, yamlUtils } from '../utils/config/target-utils.js';
|
|
12
11
|
import { CLIError } from '../utils/error-handler.js';
|
|
13
12
|
import { sanitize } from '../utils/security/security.js';
|
|
13
|
+
import {
|
|
14
|
+
detectTargetConfig,
|
|
15
|
+
setupSlashCommandsTo,
|
|
16
|
+
stripFrontMatter,
|
|
17
|
+
transformMCPConfig as transformMCP,
|
|
18
|
+
} from './shared/index.js';
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* Claude Code target - composition approach with all original functionality
|
|
@@ -68,50 +73,10 @@ export const claudeCodeTarget: Target = {
|
|
|
68
73
|
|
|
69
74
|
/**
|
|
70
75
|
* Transform MCP server configuration for Claude Code
|
|
71
|
-
*
|
|
76
|
+
* Uses shared pure function for bidirectional conversion
|
|
72
77
|
*/
|
|
73
78
|
transformMCPConfig(config: MCPServerConfigUnion, _serverId?: string): Record<string, unknown> {
|
|
74
|
-
|
|
75
|
-
if (config.type === 'local') {
|
|
76
|
-
// Convert OpenCode 'local' array command to Claude Code format
|
|
77
|
-
const [command, ...args] = config.command;
|
|
78
|
-
return {
|
|
79
|
-
type: 'stdio',
|
|
80
|
-
command,
|
|
81
|
-
...(args && args.length > 0 && { args }),
|
|
82
|
-
...(config.environment && { env: config.environment }),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Handle new stdio format (already optimized for Claude Code)
|
|
87
|
-
if (config.type === 'stdio') {
|
|
88
|
-
return {
|
|
89
|
-
type: 'stdio',
|
|
90
|
-
command: config.command,
|
|
91
|
-
...(config.args && config.args.length > 0 && { args: config.args }),
|
|
92
|
-
...(config.env && { env: config.env }),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Handle legacy OpenCode 'remote' type
|
|
97
|
-
if (config.type === 'remote') {
|
|
98
|
-
return {
|
|
99
|
-
type: 'http',
|
|
100
|
-
url: config.url,
|
|
101
|
-
...(config.headers && { headers: config.headers }),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Handle new http format (already optimized for Claude Code)
|
|
106
|
-
if (config.type === 'http') {
|
|
107
|
-
return {
|
|
108
|
-
type: 'http',
|
|
109
|
-
url: config.url,
|
|
110
|
-
...(config.headers && { headers: config.headers }),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return config;
|
|
79
|
+
return transformMCP(config, 'claude-code');
|
|
115
80
|
},
|
|
116
81
|
|
|
117
82
|
getConfigPath: (cwd: string) =>
|
|
@@ -321,12 +286,7 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
321
286
|
* Detect if this target is being used in the current environment
|
|
322
287
|
*/
|
|
323
288
|
detectFromEnvironment(): boolean {
|
|
324
|
-
|
|
325
|
-
const cwd = process.cwd();
|
|
326
|
-
return fs.existsSync(path.join(cwd, '.mcp.json'));
|
|
327
|
-
} catch {
|
|
328
|
-
return false;
|
|
329
|
-
}
|
|
289
|
+
return detectTargetConfig(process.cwd(), '.mcp.json');
|
|
330
290
|
},
|
|
331
291
|
|
|
332
292
|
/**
|
|
@@ -377,9 +337,7 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
377
337
|
* Transform rules content for Claude Code
|
|
378
338
|
* Claude Code doesn't need front matter in rules files (CLAUDE.md)
|
|
379
339
|
*/
|
|
380
|
-
|
|
381
|
-
return yamlUtils.stripFrontMatter(content);
|
|
382
|
-
},
|
|
340
|
+
transformRulesContent: stripFrontMatter,
|
|
383
341
|
|
|
384
342
|
/**
|
|
385
343
|
* Setup hooks for Claude Code
|
|
@@ -524,23 +482,7 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
524
482
|
if (!this.config.slashCommandsDir) {
|
|
525
483
|
return { count: 0 };
|
|
526
484
|
}
|
|
527
|
-
|
|
528
|
-
const slashCommandsDir = path.join(cwd, this.config.slashCommandsDir);
|
|
529
|
-
|
|
530
|
-
const results = await installToDirectory(
|
|
531
|
-
getSlashCommandsDir(),
|
|
532
|
-
slashCommandsDir,
|
|
533
|
-
async (content) => {
|
|
534
|
-
// Slash commands are plain markdown with front matter - no transformation needed
|
|
535
|
-
return content;
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
...options,
|
|
539
|
-
showProgress: false, // UI handled by init-command
|
|
540
|
-
}
|
|
541
|
-
);
|
|
542
|
-
|
|
543
|
-
return { count: results.length };
|
|
485
|
+
return setupSlashCommandsTo(path.join(cwd, this.config.slashCommandsDir), undefined, options);
|
|
544
486
|
},
|
|
545
487
|
};
|
|
546
488
|
|