@sylphx/flow 2.1.4 ā 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 +16 -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 +63 -138
- package/src/core/attach/file-attacher.ts +172 -0
- package/src/core/attach/index.ts +5 -0
- package/src/core/attach-manager.ts +32 -94
- package/src/services/global-config.ts +1 -1
- package/src/services/target-installer.ts +2 -19
- package/src/targets/claude-code.ts +12 -70
- package/src/targets/opencode.ts +11 -62
- 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/utils/target-selection.ts +1 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @sylphx/flow
|
|
2
2
|
|
|
3
|
+
## 2.1.5 (2025-11-28)
|
|
4
|
+
|
|
5
|
+
### š Bug Fixes
|
|
6
|
+
|
|
7
|
+
- **settings:** use MCP_SERVER_REGISTRY instead of hardcoded list ([79fb625](https://github.com/SylphxAI/flow/commit/79fb625c27f58f7f62902314d92c205fdc84a06e))
|
|
8
|
+
|
|
9
|
+
### ā»ļø Refactoring
|
|
10
|
+
|
|
11
|
+
- **settings:** extract checkbox configuration handler ([66303bb](https://github.com/SylphxAI/flow/commit/66303bb21a5281e5f358c69b8a6c143f3866fa76))
|
|
12
|
+
- **attach:** extract file attachment pure functions ([5723be3](https://github.com/SylphxAI/flow/commit/5723be3817804228014ceec8de27f267c990fbe8))
|
|
13
|
+
- **targets:** extract shared pure functions for MCP transforms ([0bba2cb](https://github.com/SylphxAI/flow/commit/0bba2cbc4a4233e0d63a78875346a2e9c341d803))
|
|
14
|
+
|
|
15
|
+
### š§ Chores
|
|
16
|
+
|
|
17
|
+
- remove dead cursor target references ([bf16f75](https://github.com/SylphxAI/flow/commit/bf16f759ec4705ddf0a763ea0ef6c778c91ccbbe))
|
|
18
|
+
|
|
3
19
|
## 2.1.4 (2025-11-28)
|
|
4
20
|
|
|
5
21
|
### ā»ļø Refactoring
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/flow",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"description": "One CLI to rule them all. Unified orchestration layer for Claude Code, OpenCode, Cursor and all AI development tools. Auto-detection, auto-installation, auto-upgrade.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic checkbox configuration handler
|
|
3
|
+
* Pure functions for settings UI patterns
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface ConfigItem {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ConfigMap = Record<string, ConfigItem>;
|
|
18
|
+
|
|
19
|
+
export interface CheckboxConfigOptions<T extends string> {
|
|
20
|
+
/** Section title (e.g., "Agents Configuration") */
|
|
21
|
+
title: string;
|
|
22
|
+
/** Icon for the section (e.g., "š¤") */
|
|
23
|
+
icon: string;
|
|
24
|
+
/** Prompt message (e.g., "Select agents to enable:") */
|
|
25
|
+
message: string;
|
|
26
|
+
/** Available items with display names */
|
|
27
|
+
available: Record<T, string>;
|
|
28
|
+
/** Current config state */
|
|
29
|
+
current: ConfigMap;
|
|
30
|
+
/** Item type name for confirmation (e.g., "agents", "rules") */
|
|
31
|
+
itemType: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CheckboxConfigResult<T extends string> {
|
|
35
|
+
selected: T[];
|
|
36
|
+
updated: ConfigMap;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Pure Functions
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get currently enabled keys from config
|
|
45
|
+
*/
|
|
46
|
+
export const getEnabledKeys = (config: ConfigMap): string[] =>
|
|
47
|
+
Object.keys(config).filter((key) => config[key]?.enabled);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build checkbox choices from available items
|
|
51
|
+
*/
|
|
52
|
+
export const buildChoices = <T extends string>(
|
|
53
|
+
available: Record<T, string>,
|
|
54
|
+
enabledKeys: string[]
|
|
55
|
+
): Array<{ name: string; value: T; checked: boolean }> =>
|
|
56
|
+
Object.entries(available).map(([key, name]) => ({
|
|
57
|
+
name: name as string,
|
|
58
|
+
value: key as T,
|
|
59
|
+
checked: enabledKeys.includes(key),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Update config based on selection
|
|
64
|
+
* Returns new config object (immutable)
|
|
65
|
+
*/
|
|
66
|
+
export const updateConfig = <T extends string>(
|
|
67
|
+
available: Record<T, string>,
|
|
68
|
+
selected: T[]
|
|
69
|
+
): ConfigMap => {
|
|
70
|
+
const updated: ConfigMap = {};
|
|
71
|
+
for (const key of Object.keys(available)) {
|
|
72
|
+
updated[key] = { enabled: selected.includes(key as T) };
|
|
73
|
+
}
|
|
74
|
+
return updated;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Print section header
|
|
79
|
+
*/
|
|
80
|
+
export const printHeader = (icon: string, title: string): void => {
|
|
81
|
+
console.log(chalk.cyan.bold(`\nāāā ${icon} ${title}\n`));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Print confirmation message
|
|
86
|
+
*/
|
|
87
|
+
export const printConfirmation = (itemType: string, count: number): void => {
|
|
88
|
+
console.log(chalk.green(`\nā ${itemType} configuration saved`));
|
|
89
|
+
console.log(chalk.dim(` Enabled ${itemType.toLowerCase()}: ${count}`));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Main Handler
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generic checkbox configuration handler
|
|
98
|
+
* Handles the common pattern of select ā update ā save
|
|
99
|
+
*/
|
|
100
|
+
export const handleCheckboxConfig = async <T extends string>(
|
|
101
|
+
options: CheckboxConfigOptions<T>
|
|
102
|
+
): Promise<CheckboxConfigResult<T>> => {
|
|
103
|
+
const { title, icon, message, available, current, itemType } = options;
|
|
104
|
+
|
|
105
|
+
// Print header
|
|
106
|
+
printHeader(icon, title);
|
|
107
|
+
|
|
108
|
+
// Get current enabled items
|
|
109
|
+
const enabledKeys = getEnabledKeys(current);
|
|
110
|
+
|
|
111
|
+
// Show checkbox prompt
|
|
112
|
+
const { selected } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: 'checkbox',
|
|
115
|
+
name: 'selected',
|
|
116
|
+
message,
|
|
117
|
+
choices: buildChoices(available, enabledKeys),
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
// Update config
|
|
122
|
+
const updated = updateConfig(available, selected);
|
|
123
|
+
|
|
124
|
+
// Print confirmation
|
|
125
|
+
printConfirmation(itemType, selected.length);
|
|
126
|
+
|
|
127
|
+
return { selected, updated };
|
|
128
|
+
};
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import inquirer from 'inquirer';
|
|
9
|
+
import { getRequiredEnvVars, MCP_SERVER_REGISTRY, type MCPServerID } from '../config/servers.js';
|
|
9
10
|
import { GlobalConfigService } from '../services/global-config.js';
|
|
10
11
|
import { TargetInstaller } from '../services/target-installer.js';
|
|
11
12
|
import { UserCancelledError } from '../utils/errors.js';
|
|
12
13
|
import { buildAvailableTargets, promptForDefaultTarget } from '../utils/target-selection.js';
|
|
14
|
+
import { handleCheckboxConfig, printHeader, printConfirmation } from './settings/index.js';
|
|
13
15
|
|
|
14
16
|
export const settingsCommand = new Command('settings')
|
|
15
17
|
.description('Configure Sylphx Flow settings')
|
|
@@ -106,16 +108,12 @@ async function openSection(section: string, configService: GlobalConfigService):
|
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
/**
|
|
109
|
-
* Configure Agents
|
|
111
|
+
* Configure Agents - uses shared checkbox handler + default selection
|
|
110
112
|
*/
|
|
111
113
|
async function configureAgents(configService: GlobalConfigService): Promise<void> {
|
|
112
|
-
console.log(chalk.cyan.bold('\nāāā š¤ Agent Configuration\n'));
|
|
113
|
-
|
|
114
114
|
const flowConfig = await configService.loadFlowConfig();
|
|
115
115
|
const settings = await configService.loadSettings();
|
|
116
|
-
const currentAgents = flowConfig.agents || {};
|
|
117
116
|
|
|
118
|
-
// Available agents
|
|
119
117
|
const availableAgents = {
|
|
120
118
|
coder: 'Coder - Write and modify code',
|
|
121
119
|
writer: 'Writer - Documentation and explanation',
|
|
@@ -123,38 +121,22 @@ async function configureAgents(configService: GlobalConfigService): Promise<void
|
|
|
123
121
|
orchestrator: 'Orchestrator - Task coordination',
|
|
124
122
|
};
|
|
125
123
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
choices: Object.entries(availableAgents).map(([key, name]) => ({
|
|
135
|
-
name,
|
|
136
|
-
value: key,
|
|
137
|
-
checked: currentEnabled.includes(key),
|
|
138
|
-
})),
|
|
139
|
-
},
|
|
140
|
-
]);
|
|
141
|
-
|
|
142
|
-
// Update agents
|
|
143
|
-
for (const key of Object.keys(availableAgents)) {
|
|
144
|
-
if (selectedAgents.includes(key)) {
|
|
145
|
-
currentAgents[key] = { enabled: true };
|
|
146
|
-
} else {
|
|
147
|
-
currentAgents[key] = { enabled: false };
|
|
148
|
-
}
|
|
149
|
-
}
|
|
124
|
+
const { selected, updated } = await handleCheckboxConfig({
|
|
125
|
+
title: 'Agent Configuration',
|
|
126
|
+
icon: 'š¤',
|
|
127
|
+
message: 'Select agents to enable:',
|
|
128
|
+
available: availableAgents,
|
|
129
|
+
current: flowConfig.agents || {},
|
|
130
|
+
itemType: 'Agents',
|
|
131
|
+
});
|
|
150
132
|
|
|
151
|
-
//
|
|
133
|
+
// Additional step: select default agent from enabled ones
|
|
152
134
|
const { defaultAgent } = await inquirer.prompt([
|
|
153
135
|
{
|
|
154
136
|
type: 'list',
|
|
155
137
|
name: 'defaultAgent',
|
|
156
138
|
message: 'Select default agent:',
|
|
157
|
-
choices:
|
|
139
|
+
choices: selected.map((key) => ({
|
|
158
140
|
name: availableAgents[key as keyof typeof availableAgents],
|
|
159
141
|
value: key,
|
|
160
142
|
})),
|
|
@@ -162,109 +144,57 @@ async function configureAgents(configService: GlobalConfigService): Promise<void
|
|
|
162
144
|
},
|
|
163
145
|
]);
|
|
164
146
|
|
|
165
|
-
flowConfig.agents =
|
|
147
|
+
flowConfig.agents = updated;
|
|
166
148
|
await configService.saveFlowConfig(flowConfig);
|
|
167
149
|
|
|
168
150
|
settings.defaultAgent = defaultAgent;
|
|
169
151
|
await configService.saveSettings(settings);
|
|
170
152
|
|
|
171
|
-
console.log(chalk.green(`\nā Agent configuration saved`));
|
|
172
|
-
console.log(chalk.dim(` Enabled agents: ${selectedAgents.length}`));
|
|
173
153
|
console.log(chalk.dim(` Default agent: ${defaultAgent}`));
|
|
174
154
|
}
|
|
175
155
|
|
|
176
156
|
/**
|
|
177
|
-
* Configure Rules
|
|
157
|
+
* Configure Rules - uses shared checkbox handler
|
|
178
158
|
*/
|
|
179
159
|
async function configureRules(configService: GlobalConfigService): Promise<void> {
|
|
180
|
-
console.log(chalk.cyan.bold('\nāāā š Rules Configuration\n'));
|
|
181
|
-
|
|
182
160
|
const flowConfig = await configService.loadFlowConfig();
|
|
183
|
-
const currentRules = flowConfig.rules || {};
|
|
184
|
-
|
|
185
|
-
// Available rules
|
|
186
|
-
const availableRules = {
|
|
187
|
-
core: 'Core - Identity, personality, execution',
|
|
188
|
-
'code-standards': 'Code Standards - Quality, patterns, anti-patterns',
|
|
189
|
-
workspace: 'Workspace - Documentation management',
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
// Get current enabled rules
|
|
193
|
-
const currentEnabled = Object.keys(currentRules).filter((key) => currentRules[key].enabled);
|
|
194
161
|
|
|
195
|
-
const {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
checked: currentEnabled.includes(key),
|
|
204
|
-
})),
|
|
162
|
+
const { updated } = await handleCheckboxConfig({
|
|
163
|
+
title: 'Rules Configuration',
|
|
164
|
+
icon: 'š',
|
|
165
|
+
message: 'Select rules to enable:',
|
|
166
|
+
available: {
|
|
167
|
+
core: 'Core - Identity, personality, execution',
|
|
168
|
+
'code-standards': 'Code Standards - Quality, patterns, anti-patterns',
|
|
169
|
+
workspace: 'Workspace - Documentation management',
|
|
205
170
|
},
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
for (const key of Object.keys(availableRules)) {
|
|
210
|
-
if (selectedRules.includes(key)) {
|
|
211
|
-
currentRules[key] = { enabled: true };
|
|
212
|
-
} else {
|
|
213
|
-
currentRules[key] = { enabled: false };
|
|
214
|
-
}
|
|
215
|
-
}
|
|
171
|
+
current: flowConfig.rules || {},
|
|
172
|
+
itemType: 'Rules',
|
|
173
|
+
});
|
|
216
174
|
|
|
217
|
-
flowConfig.rules =
|
|
175
|
+
flowConfig.rules = updated;
|
|
218
176
|
await configService.saveFlowConfig(flowConfig);
|
|
219
|
-
|
|
220
|
-
console.log(chalk.green(`\nā Rules configuration saved`));
|
|
221
|
-
console.log(chalk.dim(` Enabled rules: ${selectedRules.length}`));
|
|
222
177
|
}
|
|
223
178
|
|
|
224
179
|
/**
|
|
225
|
-
* Configure Output Styles
|
|
180
|
+
* Configure Output Styles - uses shared checkbox handler
|
|
226
181
|
*/
|
|
227
182
|
async function configureOutputStyles(configService: GlobalConfigService): Promise<void> {
|
|
228
|
-
console.log(chalk.cyan.bold('\nāāā šØ Output Styles Configuration\n'));
|
|
229
|
-
|
|
230
183
|
const flowConfig = await configService.loadFlowConfig();
|
|
231
|
-
const currentStyles = flowConfig.outputStyles || {};
|
|
232
184
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const currentEnabled = Object.keys(currentStyles).filter((key) => currentStyles[key].enabled);
|
|
240
|
-
|
|
241
|
-
const { selectedStyles } = await inquirer.prompt([
|
|
242
|
-
{
|
|
243
|
-
type: 'checkbox',
|
|
244
|
-
name: 'selectedStyles',
|
|
245
|
-
message: 'Select output styles to enable:',
|
|
246
|
-
choices: Object.entries(availableStyles).map(([key, name]) => ({
|
|
247
|
-
name,
|
|
248
|
-
value: key,
|
|
249
|
-
checked: currentEnabled.includes(key),
|
|
250
|
-
})),
|
|
185
|
+
const { updated } = await handleCheckboxConfig({
|
|
186
|
+
title: 'Output Styles Configuration',
|
|
187
|
+
icon: 'šØ',
|
|
188
|
+
message: 'Select output styles to enable:',
|
|
189
|
+
available: {
|
|
190
|
+
silent: 'Silent - Execution without narration',
|
|
251
191
|
},
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
for (const key of Object.keys(availableStyles)) {
|
|
256
|
-
if (selectedStyles.includes(key)) {
|
|
257
|
-
currentStyles[key] = { enabled: true };
|
|
258
|
-
} else {
|
|
259
|
-
currentStyles[key] = { enabled: false };
|
|
260
|
-
}
|
|
261
|
-
}
|
|
192
|
+
current: flowConfig.outputStyles || {},
|
|
193
|
+
itemType: 'Output styles',
|
|
194
|
+
});
|
|
262
195
|
|
|
263
|
-
flowConfig.outputStyles =
|
|
196
|
+
flowConfig.outputStyles = updated;
|
|
264
197
|
await configService.saveFlowConfig(flowConfig);
|
|
265
|
-
|
|
266
|
-
console.log(chalk.green(`\nā Output styles configuration saved`));
|
|
267
|
-
console.log(chalk.dim(` Enabled styles: ${selectedStyles.length}`));
|
|
268
198
|
}
|
|
269
199
|
|
|
270
200
|
/**
|
|
@@ -276,14 +206,8 @@ async function configureMCP(configService: GlobalConfigService): Promise<void> {
|
|
|
276
206
|
const mcpConfig = await configService.loadMCPConfig();
|
|
277
207
|
const currentServers = mcpConfig.servers || {};
|
|
278
208
|
|
|
279
|
-
//
|
|
280
|
-
const
|
|
281
|
-
grep: { name: 'GitHub Code Search (grep.app)', requiresEnv: [] },
|
|
282
|
-
context7: { name: 'Context7 Docs', requiresEnv: [] },
|
|
283
|
-
playwright: { name: 'Playwright Browser Control', requiresEnv: [] },
|
|
284
|
-
github: { name: 'GitHub', requiresEnv: ['GITHUB_TOKEN'] },
|
|
285
|
-
notion: { name: 'Notion', requiresEnv: ['NOTION_API_KEY'] },
|
|
286
|
-
};
|
|
209
|
+
// Get all servers from registry
|
|
210
|
+
const allServerIds = Object.keys(MCP_SERVER_REGISTRY) as MCPServerID[];
|
|
287
211
|
|
|
288
212
|
// Get current enabled servers
|
|
289
213
|
const currentEnabled = Object.keys(currentServers).filter((key) => currentServers[key].enabled);
|
|
@@ -293,40 +217,42 @@ async function configureMCP(configService: GlobalConfigService): Promise<void> {
|
|
|
293
217
|
type: 'checkbox',
|
|
294
218
|
name: 'selectedServers',
|
|
295
219
|
message: 'Select MCP servers to enable:',
|
|
296
|
-
choices:
|
|
220
|
+
choices: allServerIds.map((id) => {
|
|
221
|
+
const server = MCP_SERVER_REGISTRY[id];
|
|
222
|
+
const requiredEnvVars = getRequiredEnvVars(id);
|
|
297
223
|
const requiresText =
|
|
298
|
-
|
|
299
|
-
? chalk.dim(` (requires ${info.requiresEnv.join(', ')})`)
|
|
300
|
-
: '';
|
|
224
|
+
requiredEnvVars.length > 0 ? chalk.dim(` (requires ${requiredEnvVars.join(', ')})`) : '';
|
|
301
225
|
return {
|
|
302
|
-
name: `${
|
|
303
|
-
value:
|
|
304
|
-
checked: currentEnabled.includes(
|
|
226
|
+
name: `${server.name} - ${server.description}${requiresText}`,
|
|
227
|
+
value: id,
|
|
228
|
+
checked: currentEnabled.includes(id) || server.defaultInInit,
|
|
305
229
|
};
|
|
306
230
|
}),
|
|
307
231
|
},
|
|
308
232
|
]);
|
|
309
233
|
|
|
310
234
|
// Update servers
|
|
311
|
-
for (const
|
|
312
|
-
if (selectedServers.includes(
|
|
313
|
-
if (currentServers[
|
|
314
|
-
currentServers[
|
|
235
|
+
for (const id of allServerIds) {
|
|
236
|
+
if (selectedServers.includes(id)) {
|
|
237
|
+
if (currentServers[id]) {
|
|
238
|
+
currentServers[id].enabled = true;
|
|
315
239
|
} else {
|
|
316
|
-
currentServers[
|
|
240
|
+
currentServers[id] = { enabled: true, env: {} };
|
|
317
241
|
}
|
|
318
|
-
} else if (currentServers[
|
|
319
|
-
currentServers[
|
|
242
|
+
} else if (currentServers[id]) {
|
|
243
|
+
currentServers[id].enabled = false;
|
|
320
244
|
}
|
|
321
245
|
}
|
|
322
246
|
|
|
323
247
|
// Ask for API keys for newly enabled servers
|
|
324
|
-
for (const
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
248
|
+
for (const serverId of selectedServers as MCPServerID[]) {
|
|
249
|
+
const serverDef = MCP_SERVER_REGISTRY[serverId];
|
|
250
|
+
const requiredEnvVars = getRequiredEnvVars(serverId);
|
|
251
|
+
|
|
252
|
+
if (requiredEnvVars.length > 0) {
|
|
253
|
+
const server = currentServers[serverId];
|
|
328
254
|
|
|
329
|
-
for (const envKey of
|
|
255
|
+
for (const envKey of requiredEnvVars) {
|
|
330
256
|
const hasKey = server.env?.[envKey];
|
|
331
257
|
|
|
332
258
|
const { shouldConfigure } = await inquirer.prompt([
|
|
@@ -334,8 +260,8 @@ async function configureMCP(configService: GlobalConfigService): Promise<void> {
|
|
|
334
260
|
type: 'confirm',
|
|
335
261
|
name: 'shouldConfigure',
|
|
336
262
|
message: hasKey
|
|
337
|
-
? `Update ${envKey} for ${
|
|
338
|
-
: `Configure ${envKey} for ${
|
|
263
|
+
? `Update ${envKey} for ${serverDef.name}?`
|
|
264
|
+
: `Configure ${envKey} for ${serverDef.name}?`,
|
|
339
265
|
default: !hasKey,
|
|
340
266
|
},
|
|
341
267
|
]);
|
|
@@ -452,7 +378,6 @@ async function configureTarget(configService: GlobalConfigService): Promise<void
|
|
|
452
378
|
settings.defaultTarget = defaultTarget as
|
|
453
379
|
| 'claude-code'
|
|
454
380
|
| 'opencode'
|
|
455
|
-
| 'cursor'
|
|
456
381
|
| 'ask-every-time';
|
|
457
382
|
await configService.saveSettings(settings);
|
|
458
383
|
|
|
@@ -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
|
+
};
|