@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 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.4",
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
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Settings utilities
3
+ * Shared handlers for settings UI patterns
4
+ */
5
+
6
+ export * from './checkbox-config.js';
@@ -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
- // Get current enabled agents
127
- const currentEnabled = Object.keys(currentAgents).filter((key) => currentAgents[key].enabled);
128
-
129
- const { selectedAgents } = await inquirer.prompt([
130
- {
131
- type: 'checkbox',
132
- name: 'selectedAgents',
133
- message: 'Select agents to enable:',
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
- // Select default agent
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: selectedAgents.map((key: string) => ({
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 = currentAgents;
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 { selectedRules } = await inquirer.prompt([
196
- {
197
- type: 'checkbox',
198
- name: 'selectedRules',
199
- message: 'Select rules to enable:',
200
- choices: Object.entries(availableRules).map(([key, name]) => ({
201
- name,
202
- value: key,
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
- // Update rules
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 = currentRules;
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
- // Available output styles
234
- const availableStyles = {
235
- silent: 'Silent - Execution without narration',
236
- };
237
-
238
- // Get current enabled styles
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
- // Update styles
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 = currentStyles;
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
- // Available MCP servers (from MCP_SERVER_REGISTRY)
280
- const availableServers = {
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: Object.entries(availableServers).map(([key, info]) => {
220
+ choices: allServerIds.map((id) => {
221
+ const server = MCP_SERVER_REGISTRY[id];
222
+ const requiredEnvVars = getRequiredEnvVars(id);
297
223
  const requiresText =
298
- info.requiresEnv.length > 0
299
- ? chalk.dim(` (requires ${info.requiresEnv.join(', ')})`)
300
- : '';
224
+ requiredEnvVars.length > 0 ? chalk.dim(` (requires ${requiredEnvVars.join(', ')})`) : '';
301
225
  return {
302
- name: `${info.name}${requiresText}`,
303
- value: key,
304
- checked: currentEnabled.includes(key),
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 key of Object.keys(availableServers)) {
312
- if (selectedServers.includes(key)) {
313
- if (currentServers[key]) {
314
- currentServers[key].enabled = true;
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[key] = { enabled: true, env: {} };
240
+ currentServers[id] = { enabled: true, env: {} };
317
241
  }
318
- } else if (currentServers[key]) {
319
- currentServers[key].enabled = false;
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 serverKey of selectedServers) {
325
- const serverInfo = availableServers[serverKey as keyof typeof availableServers];
326
- if (serverInfo.requiresEnv.length > 0) {
327
- const server = currentServers[serverKey];
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 serverInfo.requiresEnv) {
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 ${serverInfo.name}?`
338
- : `Configure ${envKey} for ${serverInfo.name}?`,
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
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Attach module - Pure functions for file attachment
3
+ */
4
+
5
+ export * from './file-attacher.js';