@sylphx/flow 2.1.3 → 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +44 -0
  3. package/package.json +79 -73
  4. package/src/commands/flow/execute-v2.ts +37 -29
  5. package/src/commands/flow/prompt.ts +5 -3
  6. package/src/commands/flow/types.ts +0 -2
  7. package/src/commands/flow-command.ts +20 -13
  8. package/src/commands/hook-command.ts +1 -3
  9. package/src/commands/settings/checkbox-config.ts +128 -0
  10. package/src/commands/settings/index.ts +6 -0
  11. package/src/commands/settings-command.ts +84 -156
  12. package/src/config/ai-config.ts +60 -41
  13. package/src/core/agent-loader.ts +11 -6
  14. package/src/core/attach/file-attacher.ts +172 -0
  15. package/src/core/attach/index.ts +5 -0
  16. package/src/core/attach-manager.ts +117 -171
  17. package/src/core/backup-manager.ts +35 -29
  18. package/src/core/cleanup-handler.ts +11 -8
  19. package/src/core/error-handling.ts +23 -30
  20. package/src/core/flow-executor.ts +58 -76
  21. package/src/core/formatting/bytes.ts +2 -4
  22. package/src/core/functional/async.ts +5 -4
  23. package/src/core/functional/error-handler.ts +2 -2
  24. package/src/core/git-stash-manager.ts +21 -10
  25. package/src/core/installers/file-installer.ts +0 -1
  26. package/src/core/installers/mcp-installer.ts +0 -1
  27. package/src/core/project-manager.ts +24 -18
  28. package/src/core/secrets-manager.ts +54 -73
  29. package/src/core/session-manager.ts +20 -22
  30. package/src/core/state-detector.ts +139 -80
  31. package/src/core/template-loader.ts +13 -31
  32. package/src/core/upgrade-manager.ts +122 -69
  33. package/src/index.ts +8 -5
  34. package/src/services/auto-upgrade.ts +1 -1
  35. package/src/services/config-service.ts +41 -29
  36. package/src/services/global-config.ts +3 -3
  37. package/src/services/target-installer.ts +11 -26
  38. package/src/targets/claude-code.ts +35 -81
  39. package/src/targets/opencode.ts +28 -68
  40. package/src/targets/shared/index.ts +7 -0
  41. package/src/targets/shared/mcp-transforms.ts +132 -0
  42. package/src/targets/shared/target-operations.ts +135 -0
  43. package/src/types/cli.types.ts +2 -2
  44. package/src/types/provider.types.ts +1 -7
  45. package/src/types/session.types.ts +11 -11
  46. package/src/types/target.types.ts +3 -1
  47. package/src/types/todo.types.ts +1 -1
  48. package/src/types.ts +1 -1
  49. package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
  50. package/src/utils/agent-enhancer.ts +4 -4
  51. package/src/utils/config/paths.ts +3 -1
  52. package/src/utils/config/target-utils.ts +2 -2
  53. package/src/utils/display/banner.ts +2 -2
  54. package/src/utils/display/notifications.ts +58 -45
  55. package/src/utils/display/status.ts +29 -12
  56. package/src/utils/files/file-operations.ts +1 -1
  57. package/src/utils/files/sync-utils.ts +38 -41
  58. package/src/utils/index.ts +19 -27
  59. package/src/utils/package-manager-detector.ts +15 -5
  60. package/src/utils/security/security.ts +8 -4
  61. package/src/utils/target-selection.ts +6 -8
  62. package/src/utils/version.ts +4 -2
  63. package/src/commands/flow-orchestrator.ts +0 -328
  64. package/src/commands/init-command.ts +0 -92
  65. package/src/commands/init-core.ts +0 -331
  66. package/src/core/agent-manager.ts +0 -174
  67. package/src/core/loop-controller.ts +0 -200
  68. package/src/core/rule-loader.ts +0 -147
  69. package/src/core/rule-manager.ts +0 -240
  70. package/src/services/claude-config-service.ts +0 -252
  71. package/src/services/first-run-setup.ts +0 -220
  72. package/src/services/smart-config-service.ts +0 -269
  73. package/src/types/api.types.ts +0 -9
@@ -1,14 +1,15 @@
1
+ import { exec } from 'node:child_process';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
- import { exec } from 'node:child_process';
4
4
  import { promisify } from 'node:util';
5
5
  import chalk from 'chalk';
6
6
  import ora from 'ora';
7
- import type { ProjectState } from './state-detector.js';
8
- import { CLIError } from '../utils/error-handler.js';
9
- import { ConfigService } from '../services/config-service.js';
10
7
  import { getProjectSettingsFile } from '../config/constants.js';
11
- import { detectPackageManager, getUpgradeCommand, type PackageManager } from '../utils/package-manager-detector.js';
8
+ import type { Target } from '../types/target.types.js';
9
+ import { CLIError } from '../utils/error-handler.js';
10
+ import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
11
+ import type { ProjectState } from './state-detector.js';
12
+ import { targetManager } from './target-manager.js';
12
13
 
13
14
  const execAsync = promisify(exec);
14
15
 
@@ -150,67 +151,83 @@ export class UpgradeManager {
150
151
  }
151
152
  }
152
153
 
154
+ /**
155
+ * Resolve target from ID string to Target object
156
+ */
157
+ private resolveTarget(targetId: string): Target | null {
158
+ const targetOption = targetManager.getTarget(targetId);
159
+ if (targetOption._tag === 'None') {
160
+ return null;
161
+ }
162
+ return targetOption.value;
163
+ }
164
+
153
165
  async upgradeTarget(state: ProjectState, autoInstall: boolean = false): Promise<boolean> {
154
166
  if (!state.target || !state.targetLatestVersion) {
155
167
  return false;
156
168
  }
157
169
 
158
- const spinner = ora(`Upgrading ${state.target}...`).start();
170
+ const target = this.resolveTarget(state.target);
171
+ if (!target) {
172
+ return false;
173
+ }
174
+
175
+ const spinner = ora(`Upgrading ${target.name}...`).start();
159
176
 
160
177
  try {
161
- if (state.target === 'claude-code') {
162
- await this.upgradeClaudeCode(autoInstall);
163
- } else if (state.target === 'opencode') {
164
- await this.upgradeOpenCode();
165
- }
178
+ // Use target-specific upgrade logic based on target ID
179
+ // This is necessary because each CLI has different upgrade commands
180
+ await this.upgradeTargetCLI(target, autoInstall);
166
181
 
167
- spinner.succeed(`${state.target} upgraded to latest version`);
182
+ spinner.succeed(`${target.name} upgraded to latest version`);
168
183
  return true;
169
184
  } catch (error) {
170
- spinner.fail(`${state.target} upgrade failed`);
185
+ spinner.fail(`${target.name} upgrade failed`);
171
186
  throw new CLIError(
172
- `Failed to upgrade ${state.target}: ${error instanceof Error ? error.message : String(error)}`,
187
+ `Failed to upgrade ${target.name}: ${error instanceof Error ? error.message : String(error)}`,
173
188
  'TARGET_UPGRADE_FAILED'
174
189
  );
175
190
  }
176
191
  }
177
192
 
178
- private async upgradeClaudeCode(autoInstall: boolean = false): Promise<void> {
193
+ /**
194
+ * Upgrade target CLI - handles target-specific upgrade commands
195
+ */
196
+ private async upgradeTargetCLI(target: Target, autoInstall: boolean = false): Promise<void> {
179
197
  if (this.options.dryRun) {
180
- console.log('Dry run: claude update');
198
+ console.log(`Dry run: upgrade ${target.id}`);
181
199
  return;
182
200
  }
183
201
 
184
- if (autoInstall) {
185
- // Use detected package manager to install latest version
186
- const packageManager = detectPackageManager(this.projectPath);
187
- const installCmd = getUpgradeCommand('@anthropic-ai/claude-code', packageManager);
188
- const { stdout } = await execAsync(installCmd);
189
-
190
- if (this.options.verbose) {
191
- console.log(stdout);
192
- }
193
- } else {
194
- // Claude Code has built-in update command
195
- const { stdout } = await execAsync('claude update');
202
+ // Each CLI target has specific upgrade commands
203
+ // This is inherently target-specific and can't be fully abstracted
204
+ switch (target.id) {
205
+ case 'claude-code':
206
+ if (autoInstall) {
207
+ const packageManager = detectPackageManager(this.projectPath);
208
+ const installCmd = getUpgradeCommand('@anthropic-ai/claude-code', packageManager);
209
+ const { stdout } = await execAsync(installCmd);
210
+ if (this.options.verbose) {
211
+ console.log(stdout);
212
+ }
213
+ } else {
214
+ const { stdout } = await execAsync('claude update');
215
+ if (this.options.verbose) {
216
+ console.log(stdout);
217
+ }
218
+ }
219
+ break;
196
220
 
197
- if (this.options.verbose) {
198
- console.log(stdout);
221
+ case 'opencode': {
222
+ const { stdout: ocStdout } = await execAsync('opencode upgrade');
223
+ if (this.options.verbose) {
224
+ console.log(ocStdout);
225
+ }
226
+ break;
199
227
  }
200
- }
201
- }
202
-
203
- private async upgradeOpenCode(): Promise<void> {
204
- if (this.options.dryRun) {
205
- console.log('模拟: opencode upgrade');
206
- return;
207
- }
208
-
209
- // OpenCode has built-in upgrade command
210
- const { stdout } = await execAsync('opencode upgrade');
211
228
 
212
- if (this.options.verbose) {
213
- console.log(stdout);
229
+ default:
230
+ console.log(chalk.yellow(`No upgrade command available for ${target.name}`));
214
231
  }
215
232
  }
216
233
 
@@ -235,40 +252,63 @@ export class UpgradeManager {
235
252
  return upgraded;
236
253
  }
237
254
 
255
+ /**
256
+ * Get the current target from project settings
257
+ */
258
+ private async getCurrentTarget(): Promise<Target | null> {
259
+ try {
260
+ const configPath = path.join(this.projectPath, getProjectSettingsFile());
261
+ const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
262
+ if (config.target) {
263
+ return this.resolveTarget(config.target);
264
+ }
265
+ } catch {
266
+ // Cannot read config
267
+ }
268
+ return null;
269
+ }
270
+
238
271
  private async upgradeComponent(component: string): Promise<void> {
239
- // 删除旧版本
240
- const componentPath = path.join(this.projectPath, '.claude', component);
272
+ // Get target config for correct directory
273
+ const target = await this.getCurrentTarget();
274
+ const configDir = target?.config.configDir || '.claude';
275
+
276
+ // Delete old version
277
+ const componentPath = path.join(this.projectPath, configDir, component);
241
278
  await fs.rm(componentPath, { recursive: true, force: true });
242
279
 
243
- // 重新安装最新版本
244
- // 实际实现会调用相应的 installer
245
- // 这里用 dry-run 模式模拟
280
+ // Reinstall latest version
281
+ // Actual implementation would call the appropriate installer
246
282
  if (this.options.dryRun) {
247
- console.log(`模拟: 重新安装 ${component}`);
283
+ console.log(`Dry run: reinstall ${component}`);
248
284
  }
249
285
  }
250
286
 
251
287
  private async backupConfig(): Promise<string> {
252
- const backupDir = this.options.backupPath || path.join(this.projectPath, '.claude-backup');
288
+ // Get target config for correct directories
289
+ const target = await this.getCurrentTarget();
290
+ const configDir = target?.config.configDir || '.claude';
291
+
292
+ const backupDir = this.options.backupPath || path.join(this.projectPath, `${configDir}-backup`);
253
293
  await fs.mkdir(backupDir, { recursive: true });
254
294
 
255
295
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
256
296
  const backupPath = path.join(backupDir, `backup-${timestamp}`);
257
297
 
258
- // 备份 .claude 目录
259
- const claudePath = path.join(this.projectPath, '.claude');
298
+ // Backup target config directory
299
+ const targetConfigPath = path.join(this.projectPath, configDir);
260
300
  try {
261
- await fs.cp(claudePath, path.join(backupPath, '.claude'), { recursive: true });
301
+ await fs.cp(targetConfigPath, path.join(backupPath, configDir), { recursive: true });
262
302
  } catch {
263
- // .claude 目录可能不存在
303
+ // Config directory may not exist
264
304
  }
265
305
 
266
- // 备份配置文件
267
- const configPath = path.join(this.projectPath, getProjectSettingsFile());
306
+ // Backup project settings file
307
+ const settingsPath = path.join(this.projectPath, getProjectSettingsFile());
268
308
  try {
269
- await fs.cp(configPath, path.join(backupPath, getProjectSettingsFile()));
309
+ await fs.cp(settingsPath, path.join(backupPath, getProjectSettingsFile()));
270
310
  } catch {
271
- // 配置文件可能不存在
311
+ // Settings file may not exist
272
312
  }
273
313
 
274
314
  return backupPath;
@@ -281,7 +321,7 @@ export class UpgradeManager {
281
321
  const packagePath = path.join(__dirname, '..', '..', 'package.json');
282
322
  const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf-8'));
283
323
  return packageJson.version || null;
284
- } catch (error) {
324
+ } catch (_error) {
285
325
  // Fallback: try to get version from globally installed package
286
326
  try {
287
327
  const { stdout } = await execAsync('npm list -g @sylphx/flow --depth=0 --json');
@@ -311,18 +351,31 @@ export class UpgradeManager {
311
351
  }
312
352
  }
313
353
 
314
- private async getCurrentTargetVersion(target: string): Promise<string | null> {
315
- if (target === 'claude-code') {
316
- try {
317
- const { stdout } = await execAsync('claude --version');
318
- const match = stdout.match(/v?(\d+\.\d+\.\d+)/);
319
- return match ? match[1] : null;
320
- } catch {
321
- return null;
322
- }
354
+ private async getCurrentTargetVersion(targetId: string): Promise<string | null> {
355
+ const target = this.resolveTarget(targetId);
356
+ if (!target) {
357
+ return null;
323
358
  }
324
359
 
325
- return null;
360
+ // Each CLI target has specific version commands
361
+ try {
362
+ switch (target.id) {
363
+ case 'claude-code': {
364
+ const { stdout } = await execAsync('claude --version');
365
+ const match = stdout.match(/v?(\d+\.\d+\.\d+)/);
366
+ return match ? match[1] : null;
367
+ }
368
+ case 'opencode': {
369
+ const { stdout } = await execAsync('opencode --version');
370
+ const match = stdout.match(/v?(\d+\.\d+\.\d+)/);
371
+ return match ? match[1] : null;
372
+ }
373
+ default:
374
+ return null;
375
+ }
376
+ } catch {
377
+ return null;
378
+ }
326
379
  }
327
380
 
328
381
  private async getLatestTargetVersion(): Promise<string | null> {
package/src/index.ts CHANGED
@@ -8,16 +8,16 @@ import { readFileSync } from 'node:fs';
8
8
  import { dirname, join } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { Command } from 'commander';
11
- import { hookCommand } from './commands/hook-command.js';
11
+ import { executeFlow } from './commands/flow/execute-v2.js';
12
12
  import {
13
+ doctorCommand,
13
14
  flowCommand,
14
- statusCommand,
15
15
  setupCommand,
16
- doctorCommand,
16
+ statusCommand,
17
17
  upgradeCommand,
18
18
  } from './commands/flow-command.js';
19
+ import { hookCommand } from './commands/hook-command.js';
19
20
  import { settingsCommand } from './commands/settings-command.js';
20
- import { executeFlow } from './commands/flow/execute-v2.js';
21
21
  import { UserCancelledError } from './utils/errors.js';
22
22
 
23
23
  // Read version from package.json
@@ -53,7 +53,10 @@ export function createCLI(): Command {
53
53
  // Default action: delegate to flow command for convenience
54
54
  // This allows `sylphx-flow "prompt"` instead of requiring `sylphx-flow flow "prompt"`
55
55
  program
56
- .argument('[prompt]', 'Prompt to execute with agent (optional, supports @file.txt for file input)')
56
+ .argument(
57
+ '[prompt]',
58
+ 'Prompt to execute with agent (optional, supports @file.txt for file input)'
59
+ )
57
60
  .option('--agent <name>', 'Agent to use (default: coder)', 'coder')
58
61
  .option('--agent-file <path>', 'Load agent from specific file')
59
62
  .option('--verbose', 'Show detailed output')
@@ -4,9 +4,9 @@
4
4
  */
5
5
 
6
6
  import { exec } from 'node:child_process';
7
- import { promisify } from 'node:util';
8
7
  import fs from 'node:fs/promises';
9
8
  import path from 'node:path';
9
+ import { promisify } from 'node:util';
10
10
  import chalk from 'chalk';
11
11
  import ora from 'ora';
12
12
  import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
@@ -5,12 +5,11 @@
5
5
 
6
6
  import fs from 'node:fs/promises';
7
7
  import path from 'node:path';
8
- import os from 'node:os';
9
8
  import {
10
9
  CONFIG_DIR,
11
- USER_SETTINGS_FILE,
10
+ getProjectLocalSettingsFile,
12
11
  getProjectSettingsFile,
13
- getProjectLocalSettingsFile
12
+ USER_SETTINGS_FILE,
14
13
  } from '../config/constants.js';
15
14
 
16
15
  /**
@@ -27,8 +26,8 @@ export interface UserSettings {
27
26
 
28
27
  // API keys for providers
29
28
  apiKeys?: {
30
- kimi?: string; // Kimi provider
31
- 'z.ai'?: string; // Z.ai proxy
29
+ kimi?: string; // Kimi provider
30
+ 'z.ai'?: string; // Z.ai proxy
32
31
  };
33
32
 
34
33
  // User preferences (can be changed anytime)
@@ -45,7 +44,7 @@ export interface UserSettings {
45
44
  export interface ProjectSettings {
46
45
  target?: string;
47
46
  version?: string;
48
- defaultAgent?: string; // Can override user default per project
47
+ defaultAgent?: string; // Can override user default per project
49
48
 
50
49
  [key: string]: unknown;
51
50
  }
@@ -55,9 +54,9 @@ export interface ProjectSettings {
55
54
  * These are selected each run but can be overridden by CLI flags
56
55
  */
57
56
  export interface RuntimeChoices {
58
- provider?: string; // Selected for this run
59
- agent?: string; // Selected for this run
60
- prompt?: string; // User prompt for this run
57
+ provider?: string; // Selected for this run
58
+ agent?: string; // Selected for this run
59
+ prompt?: string; // User prompt for this run
61
60
 
62
61
  [key: string]: unknown;
63
62
  }
@@ -71,9 +70,9 @@ export class ConfigService {
71
70
  project: ProjectSettings;
72
71
  choices: RuntimeChoices;
73
72
  }> {
74
- const userSettings = await this.loadHomeSettings();
75
- const projectSettings = await this.loadProjectSettings(cwd);
76
- const localSettings = await this.loadLocalSettings(cwd);
73
+ const userSettings = await ConfigService.loadHomeSettings();
74
+ const projectSettings = await ConfigService.loadProjectSettings(cwd);
75
+ const localSettings = await ConfigService.loadLocalSettings(cwd);
77
76
 
78
77
  // Runtime choices merge: local > project > user defaults
79
78
  const choices: RuntimeChoices = {
@@ -92,7 +91,7 @@ export class ConfigService {
92
91
  * Legacy method for backward compatibility
93
92
  */
94
93
  static async loadSettings(cwd: string = process.cwd()): Promise<any> {
95
- const config = await this.loadConfiguration(cwd);
94
+ const config = await ConfigService.loadConfiguration(cwd);
96
95
  return {
97
96
  ...config.user,
98
97
  ...config.project,
@@ -120,18 +119,21 @@ export class ConfigService {
120
119
  await fs.mkdir(USER_SETTINGS_FILE.replace('/settings.json', ''), { recursive: true });
121
120
 
122
121
  // Merge with existing settings and save
123
- const existing = await this.loadHomeSettings();
122
+ const existing = await ConfigService.loadHomeSettings();
124
123
  const merged = { ...existing, ...settings };
125
- await fs.writeFile(USER_SETTINGS_FILE, JSON.stringify(merged, null, 2) + '\n');
124
+ await fs.writeFile(USER_SETTINGS_FILE, `${JSON.stringify(merged, null, 2)}\n`);
126
125
  }
127
126
 
128
127
  /**
129
128
  * Check if user has completed initial setup (API keys configured)
130
129
  */
131
130
  static async hasInitialSetup(): Promise<boolean> {
132
- const userSettings = await this.loadHomeSettings();
131
+ const userSettings = await ConfigService.loadHomeSettings();
133
132
  // Check if user has completed setup (either has API keys OR has explicitly chosen default)
134
- return !!(userSettings.hasCompletedSetup || (userSettings.apiKeys && Object.keys(userSettings.apiKeys).length > 0));
133
+ return !!(
134
+ userSettings.hasCompletedSetup ||
135
+ (userSettings.apiKeys && Object.keys(userSettings.apiKeys).length > 0)
136
+ );
135
137
  }
136
138
 
137
139
  /**
@@ -140,8 +142,12 @@ export class ConfigService {
140
142
  */
141
143
  static getAvailableProviders(userSettings: UserSettings): string[] {
142
144
  const providers: string[] = ['default']; // Always available
143
- if (userSettings.apiKeys?.kimi) providers.push('kimi');
144
- if (userSettings.apiKeys?.['z.ai']) providers.push('z.ai');
145
+ if (userSettings.apiKeys?.kimi) {
146
+ providers.push('kimi');
147
+ }
148
+ if (userSettings.apiKeys?.['z.ai']) {
149
+ providers.push('z.ai');
150
+ }
145
151
  return providers;
146
152
  }
147
153
 
@@ -161,17 +167,20 @@ export class ConfigService {
161
167
  /**
162
168
  * Save project-level settings
163
169
  */
164
- static async saveProjectSettings(settings: ProjectSettings, cwd: string = process.cwd()): Promise<void> {
170
+ static async saveProjectSettings(
171
+ settings: ProjectSettings,
172
+ cwd: string = process.cwd()
173
+ ): Promise<void> {
165
174
  // Ensure directory exists
166
175
  const configDir = path.join(cwd, CONFIG_DIR);
167
176
  await fs.mkdir(configDir, { recursive: true });
168
177
 
169
178
  // Merge with existing settings and save
170
- const existing = await this.loadProjectSettings(cwd);
179
+ const existing = await ConfigService.loadProjectSettings(cwd);
171
180
  const merged = { ...existing, ...settings };
172
181
 
173
182
  const configPath = getProjectSettingsFile(cwd);
174
- await fs.writeFile(configPath, JSON.stringify(merged, null, 2) + '\n');
183
+ await fs.writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`);
175
184
  }
176
185
 
177
186
  /**
@@ -190,13 +199,16 @@ export class ConfigService {
190
199
  /**
191
200
  * Save project-local settings
192
201
  */
193
- static async saveLocalSettings(settings: RuntimeChoices, cwd: string = process.cwd()): Promise<void> {
202
+ static async saveLocalSettings(
203
+ settings: RuntimeChoices,
204
+ cwd: string = process.cwd()
205
+ ): Promise<void> {
194
206
  // Ensure directory exists
195
207
  const configDir = path.join(cwd, CONFIG_DIR);
196
208
  await fs.mkdir(configDir, { recursive: true });
197
209
 
198
210
  const configPath = getProjectLocalSettingsFile(cwd);
199
- await fs.writeFile(configPath, JSON.stringify(settings, null, 2) + '\n');
211
+ await fs.writeFile(configPath, `${JSON.stringify(settings, null, 2)}\n`);
200
212
  }
201
213
 
202
214
  /**
@@ -209,14 +221,14 @@ export class ConfigService {
209
221
  ): Promise<void> {
210
222
  // Save API keys to home directory
211
223
  if (userConfig.claudeApiKey || userConfig.claudeProvider || userConfig.claudeProviderConfig) {
212
- await this.saveHomeSettings(userConfig);
224
+ await ConfigService.saveHomeSettings(userConfig);
213
225
  }
214
226
 
215
227
  // Save other settings to project
216
- await this.saveProjectSettings(projectConfig, cwd);
228
+ await ConfigService.saveProjectSettings(projectConfig, cwd);
217
229
 
218
230
  // Create .gitignore pattern file if it doesn't exist (excluding .local.json)
219
- await this.addGitignore(cwd);
231
+ await ConfigService.addGitignore(cwd);
220
232
  }
221
233
 
222
234
  /**
@@ -235,11 +247,11 @@ export class ConfigService {
235
247
 
236
248
  // Check if pattern already exists
237
249
  if (!content.includes('.sylphx-flow/*.local.json')) {
238
- await fs.appendFile(gitignorePath, patterns.join('\n') + '\n');
250
+ await fs.appendFile(gitignorePath, `${patterns.join('\n')}\n`);
239
251
  }
240
252
  } catch {
241
253
  // .gitignore doesn't exist - create it
242
- await fs.writeFile(gitignorePath, patterns.join('\n').trim() + '\n');
254
+ await fs.writeFile(gitignorePath, `${patterns.join('\n').trim()}\n`);
243
255
  }
244
256
  }
245
257
 
@@ -3,14 +3,14 @@
3
3
  * Manages all Flow settings in ~/.sylphx-flow/
4
4
  */
5
5
 
6
+ import { existsSync } from 'node:fs';
6
7
  import fs from 'node:fs/promises';
7
- import path from 'node:path';
8
8
  import os from 'node:os';
9
- import { existsSync } from 'node:fs';
9
+ import path from 'node:path';
10
10
 
11
11
  export interface GlobalSettings {
12
12
  version: string;
13
- defaultTarget?: 'claude-code' | 'opencode' | 'cursor' | 'ask-every-time';
13
+ defaultTarget?: 'claude-code' | 'opencode' | 'ask-every-time';
14
14
  defaultAgent?: string; // Default agent to use (e.g., 'coder', 'writer', 'reviewer', 'orchestrator')
15
15
  firstRun: boolean;
16
16
  lastUpdated: string;
@@ -1,20 +1,20 @@
1
1
  /**
2
2
  * Target Installation Service
3
- * Auto-detects and installs AI CLI tools (Claude Code, OpenCode, Cursor)
3
+ * Auto-detects and installs AI CLI tools (Claude Code, OpenCode)
4
4
  */
5
5
 
6
6
  import { exec } from 'node:child_process';
7
7
  import { promisify } from 'node:util';
8
8
  import chalk from 'chalk';
9
- import ora from 'ora';
10
9
  import inquirer from 'inquirer';
11
- import { detectPackageManager, type PackageManager } from '../utils/package-manager-detector.js';
10
+ import ora from 'ora';
12
11
  import { UserCancelledError } from '../utils/errors.js';
12
+ import { detectPackageManager, type PackageManager } from '../utils/package-manager-detector.js';
13
13
 
14
14
  const execAsync = promisify(exec);
15
15
 
16
16
  export interface TargetInstallation {
17
- id: 'claude-code' | 'opencode' | 'cursor';
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 {
@@ -136,9 +126,10 @@ export class TargetInstaller {
136
126
  ]);
137
127
 
138
128
  return targetId;
139
- } catch (error: any) {
129
+ } catch (error: unknown) {
140
130
  // Handle user cancellation (Ctrl+C)
141
- if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
131
+ const err = error as Error & { name?: string };
132
+ if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
142
133
  throw new UserCancelledError('Target selection cancelled');
143
134
  }
144
135
  throw error;
@@ -159,13 +150,6 @@ export class TargetInstaller {
159
150
  return false;
160
151
  }
161
152
 
162
- // Special handling for Cursor (not npm-installable)
163
- if (targetId === 'cursor') {
164
- console.log(chalk.yellow('\n⚠️ Cursor requires manual installation'));
165
- console.log(chalk.cyan(' Visit https://cursor.sh to download and install\n'));
166
- return false;
167
- }
168
-
169
153
  // Confirm installation unless auto-confirm is enabled
170
154
  if (!autoConfirm) {
171
155
  try {
@@ -182,9 +166,10 @@ export class TargetInstaller {
182
166
  console.log(chalk.yellow('\n⚠️ Installation cancelled\n'));
183
167
  return false;
184
168
  }
185
- } catch (error: any) {
169
+ } catch (error: unknown) {
186
170
  // Handle user cancellation (Ctrl+C)
187
- if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
171
+ const err = error as Error & { name?: string };
172
+ if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
188
173
  throw new UserCancelledError('Installation cancelled');
189
174
  }
190
175
  throw error;
@@ -199,7 +184,7 @@ export class TargetInstaller {
199
184
 
200
185
  spinner.succeed(chalk.green(`✓ ${installation.name} installed successfully`));
201
186
  return true;
202
- } catch (error) {
187
+ } catch (_error) {
203
188
  spinner.fail(chalk.red(`✗ Failed to install ${installation.name}`));
204
189
 
205
190
  const installCmd = installation.installCommand(this.packageManager);