@sylphx/flow 2.1.3 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +12 -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-command.ts +36 -33
  10. package/src/config/ai-config.ts +60 -41
  11. package/src/core/agent-loader.ts +11 -6
  12. package/src/core/attach-manager.ts +92 -84
  13. package/src/core/backup-manager.ts +35 -29
  14. package/src/core/cleanup-handler.ts +11 -8
  15. package/src/core/error-handling.ts +23 -30
  16. package/src/core/flow-executor.ts +58 -76
  17. package/src/core/formatting/bytes.ts +2 -4
  18. package/src/core/functional/async.ts +5 -4
  19. package/src/core/functional/error-handler.ts +2 -2
  20. package/src/core/git-stash-manager.ts +21 -10
  21. package/src/core/installers/file-installer.ts +0 -1
  22. package/src/core/installers/mcp-installer.ts +0 -1
  23. package/src/core/project-manager.ts +24 -18
  24. package/src/core/secrets-manager.ts +54 -73
  25. package/src/core/session-manager.ts +20 -22
  26. package/src/core/state-detector.ts +139 -80
  27. package/src/core/template-loader.ts +13 -31
  28. package/src/core/upgrade-manager.ts +122 -69
  29. package/src/index.ts +8 -5
  30. package/src/services/auto-upgrade.ts +1 -1
  31. package/src/services/config-service.ts +41 -29
  32. package/src/services/global-config.ts +2 -2
  33. package/src/services/target-installer.ts +9 -7
  34. package/src/targets/claude-code.ts +24 -12
  35. package/src/targets/opencode.ts +17 -6
  36. package/src/types/cli.types.ts +2 -2
  37. package/src/types/provider.types.ts +1 -7
  38. package/src/types/session.types.ts +11 -11
  39. package/src/types/target.types.ts +3 -1
  40. package/src/types/todo.types.ts +1 -1
  41. package/src/types.ts +1 -1
  42. package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
  43. package/src/utils/agent-enhancer.ts +4 -4
  44. package/src/utils/config/paths.ts +3 -1
  45. package/src/utils/config/target-utils.ts +2 -2
  46. package/src/utils/display/banner.ts +2 -2
  47. package/src/utils/display/notifications.ts +58 -45
  48. package/src/utils/display/status.ts +29 -12
  49. package/src/utils/files/file-operations.ts +1 -1
  50. package/src/utils/files/sync-utils.ts +38 -41
  51. package/src/utils/index.ts +19 -27
  52. package/src/utils/package-manager-detector.ts +15 -5
  53. package/src/utils/security/security.ts +8 -4
  54. package/src/utils/target-selection.ts +5 -2
  55. package/src/utils/version.ts +4 -2
  56. package/src/commands/flow-orchestrator.ts +0 -328
  57. package/src/commands/init-command.ts +0 -92
  58. package/src/commands/init-core.ts +0 -331
  59. package/src/core/agent-manager.ts +0 -174
  60. package/src/core/loop-controller.ts +0 -200
  61. package/src/core/rule-loader.ts +0 -147
  62. package/src/core/rule-manager.ts +0 -240
  63. package/src/services/claude-config-service.ts +0 -252
  64. package/src/services/first-run-setup.ts +0 -220
  65. package/src/services/smart-config-service.ts +0 -269
  66. package/src/types/api.types.ts +0 -9
@@ -4,17 +4,19 @@
4
4
  * Supports multi-project isolation in ~/.sylphx-flow/backups/
5
5
  */
6
6
 
7
+ import { existsSync } from 'node:fs';
7
8
  import fs from 'node:fs/promises';
8
9
  import path from 'node:path';
9
- import { existsSync } from 'node:fs';
10
10
  import ora from 'ora';
11
- import { ProjectManager } from './project-manager.js';
11
+ import type { Target } from '../types/target.types.js';
12
+ import type { ProjectManager } from './project-manager.js';
13
+ import { targetManager } from './target-manager.js';
12
14
 
13
15
  export interface BackupInfo {
14
16
  sessionId: string;
15
17
  timestamp: string;
16
18
  projectPath: string;
17
- target: 'claude-code' | 'opencode';
19
+ target: string;
18
20
  backupPath: string;
19
21
  }
20
22
 
@@ -22,7 +24,7 @@ export interface BackupManifest {
22
24
  sessionId: string;
23
25
  timestamp: string;
24
26
  projectPath: string;
25
- target: 'claude-code' | 'opencode';
27
+ target: string;
26
28
  backup: {
27
29
  config?: {
28
30
  path: string;
@@ -64,14 +66,27 @@ export class BackupManager {
64
66
  this.projectManager = projectManager;
65
67
  }
66
68
 
69
+ /**
70
+ * Resolve target from ID string to Target object
71
+ */
72
+ private resolveTarget(targetId: string): Target {
73
+ const targetOption = targetManager.getTarget(targetId);
74
+ if (targetOption._tag === 'None') {
75
+ throw new Error(`Unknown target: ${targetId}`);
76
+ }
77
+ return targetOption.value;
78
+ }
79
+
67
80
  /**
68
81
  * Create full backup of project environment
69
82
  */
70
83
  async createBackup(
71
84
  projectPath: string,
72
85
  projectHash: string,
73
- target: 'claude-code' | 'opencode'
86
+ targetOrId: Target | string
74
87
  ): Promise<BackupInfo> {
88
+ const target = typeof targetOrId === 'string' ? this.resolveTarget(targetOrId) : targetOrId;
89
+ const targetId = target.id;
75
90
  const sessionId = `session-${Date.now()}`;
76
91
  const timestamp = new Date().toISOString();
77
92
 
@@ -89,19 +104,17 @@ export class BackupManager {
89
104
 
90
105
  // Backup entire target directory if it exists
91
106
  if (existsSync(targetConfigDir)) {
92
- const backupTargetDir = path.join(
93
- backupPath,
94
- target === 'claude-code' ? '.claude' : '.opencode'
95
- );
107
+ // Use configDir from target config (e.g., '.claude', '.opencode')
108
+ const backupTargetDir = path.join(backupPath, target.config.configDir);
96
109
  await this.copyDirectory(targetConfigDir, backupTargetDir);
97
110
  }
98
111
 
99
- // Create manifest
112
+ // Create manifest (store target ID as string for JSON serialization)
100
113
  const manifest: BackupManifest = {
101
114
  sessionId,
102
115
  timestamp,
103
116
  projectPath,
104
- target,
117
+ target: targetId,
105
118
  backup: {
106
119
  agents: { user: [], flow: [] },
107
120
  commands: { user: [], flow: [] },
@@ -113,10 +126,7 @@ export class BackupManager {
113
126
  },
114
127
  };
115
128
 
116
- await fs.writeFile(
117
- path.join(backupPath, 'manifest.json'),
118
- JSON.stringify(manifest, null, 2)
119
- );
129
+ await fs.writeFile(path.join(backupPath, 'manifest.json'), JSON.stringify(manifest, null, 2));
120
130
 
121
131
  // Create symlink to latest
122
132
  const latestLink = paths.latestBackup;
@@ -131,7 +141,7 @@ export class BackupManager {
131
141
  sessionId,
132
142
  timestamp,
133
143
  projectPath,
134
- target,
144
+ target: targetId,
135
145
  backupPath,
136
146
  };
137
147
  } catch (error) {
@@ -156,12 +166,13 @@ export class BackupManager {
156
166
  try {
157
167
  // Read manifest
158
168
  const manifestPath = path.join(backupPath, 'manifest.json');
159
- const manifest: BackupManifest = JSON.parse(
160
- await fs.readFile(manifestPath, 'utf-8')
161
- );
169
+ const manifest: BackupManifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
162
170
 
163
171
  const projectPath = manifest.projectPath;
164
- const target = manifest.target;
172
+ const targetId = manifest.target;
173
+
174
+ // Resolve target to get config
175
+ const target = this.resolveTarget(targetId);
165
176
 
166
177
  // Get target config directory
167
178
  const targetConfigDir = this.projectManager.getTargetConfigDir(projectPath, target);
@@ -171,11 +182,8 @@ export class BackupManager {
171
182
  await fs.rm(targetConfigDir, { recursive: true, force: true });
172
183
  }
173
184
 
174
- // Restore from backup
175
- const backupTargetDir = path.join(
176
- backupPath,
177
- target === 'claude-code' ? '.claude' : '.opencode'
178
- );
185
+ // Restore from backup using target config's configDir
186
+ const backupTargetDir = path.join(backupPath, target.config.configDir);
179
187
 
180
188
  if (existsSync(backupTargetDir)) {
181
189
  await this.copyDirectory(backupTargetDir, targetConfigDir);
@@ -232,7 +240,7 @@ export class BackupManager {
232
240
  .filter((e) => e.isDirectory() && e.name.startsWith('session-'))
233
241
  .map((e) => ({
234
242
  name: e.name,
235
- timestamp: parseInt(e.name.replace('session-', '')),
243
+ timestamp: parseInt(e.name.replace('session-', ''), 10),
236
244
  }))
237
245
  .sort((a, b) => b.timestamp - a.timestamp);
238
246
 
@@ -290,9 +298,7 @@ export class BackupManager {
290
298
 
291
299
  const manifestPath = path.join(paths.backupsDir, entry.name, 'manifest.json');
292
300
  if (existsSync(manifestPath)) {
293
- const manifest: BackupManifest = JSON.parse(
294
- await fs.readFile(manifestPath, 'utf-8')
295
- );
301
+ const manifest: BackupManifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
296
302
  backups.push({
297
303
  sessionId: manifest.sessionId,
298
304
  timestamp: manifest.timestamp,
@@ -5,12 +5,11 @@
5
5
  */
6
6
 
7
7
  import chalk from 'chalk';
8
- import { ProjectManager } from './project-manager.js';
9
- import { SessionManager } from './session-manager.js';
10
- import { BackupManager } from './backup-manager.js';
8
+ import type { BackupManager } from './backup-manager.js';
9
+ import type { ProjectManager } from './project-manager.js';
10
+ import type { SessionManager } from './session-manager.js';
11
11
 
12
12
  export class CleanupHandler {
13
- private projectManager: ProjectManager;
14
13
  private sessionManager: SessionManager;
15
14
  private backupManager: BackupManager;
16
15
  private registered = false;
@@ -82,14 +81,16 @@ export class CleanupHandler {
82
81
  }
83
82
 
84
83
  try {
85
- const { shouldRestore, session } = await this.sessionManager.endSession(this.currentProjectHash);
84
+ const { shouldRestore, session } = await this.sessionManager.endSession(
85
+ this.currentProjectHash
86
+ );
86
87
 
87
88
  if (shouldRestore && session) {
88
89
  // Last session - restore backup silently on normal exit
89
90
  await this.backupManager.restoreBackup(this.currentProjectHash, session.sessionId);
90
91
  await this.backupManager.cleanupOldBackups(this.currentProjectHash, 3);
91
92
  }
92
- } catch (error) {
93
+ } catch (_error) {
93
94
  // Silent fail on exit
94
95
  }
95
96
  }
@@ -97,7 +98,7 @@ export class CleanupHandler {
97
98
  /**
98
99
  * Signal-based cleanup (SIGINT, SIGTERM, etc.) with multi-session support
99
100
  */
100
- private async onSignal(signal: string): Promise<void> {
101
+ private async onSignal(_signal: string): Promise<void> {
101
102
  if (!this.currentProjectHash) {
102
103
  return;
103
104
  }
@@ -105,7 +106,9 @@ export class CleanupHandler {
105
106
  try {
106
107
  console.log(chalk.cyan('🧹 Cleaning up...'));
107
108
 
108
- const { shouldRestore, session } = await this.sessionManager.endSession(this.currentProjectHash);
109
+ const { shouldRestore, session } = await this.sessionManager.endSession(
110
+ this.currentProjectHash
111
+ );
109
112
 
110
113
  if (shouldRestore && session) {
111
114
  // Last session - restore environment
@@ -14,12 +14,7 @@ export class BaseError extends Error {
14
14
  public readonly statusCode: number;
15
15
  public readonly details?: Record<string, unknown>;
16
16
 
17
- constructor(
18
- message: string,
19
- code: string,
20
- statusCode = 500,
21
- details?: Record<string, unknown>
22
- ) {
17
+ constructor(message: string, code: string, statusCode = 500, details?: Record<string, unknown>) {
23
18
  super(message);
24
19
  this.name = this.constructor.name;
25
20
  this.code = code;
@@ -176,7 +171,7 @@ export interface ErrorHandler {
176
171
  export class LoggerErrorHandler implements ErrorHandler {
177
172
  constructor(private level: 'error' | 'warn' | 'info' | 'debug' = 'error') {}
178
173
 
179
- canHandle(error: Error): boolean {
174
+ canHandle(_error: Error): boolean {
180
175
  return true; // Logger can handle all errors
181
176
  }
182
177
 
@@ -211,7 +206,7 @@ export class LoggerErrorHandler implements ErrorHandler {
211
206
  * Console error handler
212
207
  */
213
208
  export class ConsoleErrorHandler implements ErrorHandler {
214
- canHandle(error: Error): boolean {
209
+ canHandle(_error: Error): boolean {
215
210
  return true; // Console can handle all errors
216
211
  }
217
212
 
@@ -269,9 +264,7 @@ export class ErrorHandlerChain {
269
264
  /**
270
265
  * Global error handler
271
266
  */
272
- export const globalErrorHandler = new ErrorHandlerChain([
273
- new LoggerErrorHandler('error'),
274
- ]);
267
+ export const globalErrorHandler = new ErrorHandlerChain([new LoggerErrorHandler('error')]);
275
268
 
276
269
  /**
277
270
  * Set up global error handlers
@@ -371,7 +364,7 @@ export async function withRetry<T>(
371
364
  onRetry,
372
365
  } = options;
373
366
 
374
- let lastError: Error;
367
+ let lastError: Error = new Error('Unknown error');
375
368
 
376
369
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
377
370
  try {
@@ -381,7 +374,8 @@ export async function withRetry<T>(
381
374
  lastError = error instanceof Error ? error : new Error(String(error));
382
375
 
383
376
  // Check if error is retryable
384
- const isRetryable = retryableErrors.length === 0 ||
377
+ const isRetryable =
378
+ retryableErrors.length === 0 ||
385
379
  retryableErrors.includes((lastError as BaseError).code) ||
386
380
  retryableErrors.includes(lastError.constructor.name);
387
381
 
@@ -396,23 +390,21 @@ export async function withRetry<T>(
396
390
  }
397
391
 
398
392
  // Calculate delay
399
- const retryDelay = backoff === 'exponential'
400
- ? delay * Math.pow(2, attempt - 1)
401
- : delay * attempt;
393
+ const retryDelay = backoff === 'exponential' ? delay * 2 ** (attempt - 1) : delay * attempt;
402
394
 
403
395
  // Wait before retry
404
- await new Promise(resolve => setTimeout(resolve, retryDelay));
396
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
405
397
  }
406
398
  }
407
399
 
408
- await globalErrorHandler.handle(lastError!);
409
- return { success: false, error: lastError! };
400
+ await globalErrorHandler.handle(lastError);
401
+ return { success: false, error: lastError };
410
402
  }
411
403
 
412
404
  /**
413
405
  * Timeout wrapper
414
406
  */
415
- export async function withTimeout<T>(
407
+ export function withTimeout<T>(
416
408
  fn: () => Promise<T>,
417
409
  timeoutMs: number,
418
410
  timeoutMessage = 'Operation timed out'
@@ -447,26 +439,27 @@ export class CircuitBreaker {
447
439
  private failures = 0;
448
440
  private lastFailureTime = 0;
449
441
  private state: 'closed' | 'open' | 'half-open' = 'closed';
442
+ private readonly config: {
443
+ failureThreshold: number;
444
+ recoveryTimeMs: number;
445
+ monitoringPeriodMs: number;
446
+ };
450
447
 
451
448
  constructor(
452
- private options: {
449
+ options: {
453
450
  failureThreshold?: number;
454
451
  recoveryTimeMs?: number;
455
452
  monitoringPeriodMs?: number;
456
453
  } = {}
457
454
  ) {
458
- const {
459
- failureThreshold = 5,
460
- recoveryTimeMs = 60000,
461
- monitoringPeriodMs = 10000,
462
- } = options;
455
+ const { failureThreshold = 5, recoveryTimeMs = 60000, monitoringPeriodMs = 10000 } = options;
463
456
 
464
- this.options = { failureThreshold, recoveryTimeMs, monitoringPeriodMs };
457
+ this.config = { failureThreshold, recoveryTimeMs, monitoringPeriodMs };
465
458
  }
466
459
 
467
460
  async execute<T>(fn: () => Promise<T>): Promise<Result<T>> {
468
461
  if (this.state === 'open') {
469
- if (Date.now() - this.lastFailureTime > this.options.recoveryTimeMs!) {
462
+ if (Date.now() - this.lastFailureTime > this.config.recoveryTimeMs) {
470
463
  this.state = 'half-open';
471
464
  } else {
472
465
  return {
@@ -498,7 +491,7 @@ export class CircuitBreaker {
498
491
  this.failures++;
499
492
  this.lastFailureTime = Date.now();
500
493
 
501
- if (this.failures >= this.options.failureThreshold!) {
494
+ if (this.failures >= this.config.failureThreshold) {
502
495
  this.state = 'open';
503
496
  }
504
497
  }
@@ -516,4 +509,4 @@ export class CircuitBreaker {
516
509
  this.state = 'closed';
517
510
  this.lastFailureTime = 0;
518
511
  }
519
- }
512
+ }
@@ -5,14 +5,16 @@
5
5
  */
6
6
 
7
7
  import chalk from 'chalk';
8
- import { ProjectManager } from './project-manager.js';
9
- import { SessionManager } from './session-manager.js';
8
+ import type { Target } from '../types/target.types.js';
9
+ import { AttachManager, type AttachResult } from './attach-manager.js';
10
10
  import { BackupManager } from './backup-manager.js';
11
- import { AttachManager } from './attach-manager.js';
12
- import { SecretsManager } from './secrets-manager.js';
13
11
  import { CleanupHandler } from './cleanup-handler.js';
14
- import { TemplateLoader } from './template-loader.js';
15
12
  import { GitStashManager } from './git-stash-manager.js';
13
+ import { ProjectManager } from './project-manager.js';
14
+ import { SecretsManager } from './secrets-manager.js';
15
+ import { SessionManager } from './session-manager.js';
16
+ import { targetManager } from './target-manager.js';
17
+ import { TemplateLoader } from './template-loader.js';
16
18
 
17
19
  export interface FlowExecutorOptions {
18
20
  verbose?: boolean;
@@ -49,10 +51,7 @@ export class FlowExecutor {
49
51
  /**
50
52
  * Execute complete flow with attach mode (with multi-session support)
51
53
  */
52
- async execute(
53
- projectPath: string,
54
- options: FlowExecutorOptions = {}
55
- ): Promise<void> {
54
+ async execute(projectPath: string, options: FlowExecutorOptions = {}): Promise<void> {
56
55
  // Initialize Flow directories
57
56
  await this.projectManager.initialize();
58
57
 
@@ -76,7 +75,7 @@ export class FlowExecutor {
76
75
  // Joining existing session
77
76
  console.log(chalk.cyan('🔗 Joining existing session...'));
78
77
 
79
- const { session, isFirstSession } = await this.sessionManager.startSession(
78
+ const { session } = await this.sessionManager.startSession(
80
79
  projectPath,
81
80
  projectHash,
82
81
  target,
@@ -97,31 +96,21 @@ export class FlowExecutor {
97
96
  await this.gitStashManager.stashSettingsChanges(projectPath);
98
97
 
99
98
  console.log(chalk.cyan('💾 Creating backup...'));
100
- const backup = await this.backupManager.createBackup(
101
- projectPath,
102
- projectHash,
103
- target
104
- );
99
+ const backup = await this.backupManager.createBackup(projectPath, projectHash, target);
105
100
 
106
101
  // Step 4: Extract and save secrets
107
102
  if (!options.skipSecrets) {
108
103
  console.log(chalk.cyan('🔐 Extracting secrets...'));
109
- const secrets = await this.secretsManager.extractMCPSecrets(
110
- projectPath,
111
- projectHash,
112
- target
113
- );
104
+ const secrets = await this.secretsManager.extractMCPSecrets(projectPath, projectHash, target);
114
105
 
115
106
  if (Object.keys(secrets.servers).length > 0) {
116
107
  await this.secretsManager.saveSecrets(projectHash, secrets);
117
- console.log(
118
- chalk.green(` ✓ Saved ${Object.keys(secrets.servers).length} MCP secret(s)`)
119
- );
108
+ console.log(chalk.green(` ✓ Saved ${Object.keys(secrets.servers).length} MCP secret(s)`));
120
109
  }
121
110
  }
122
111
 
123
112
  // Step 5: Start session (use backup's sessionId to ensure consistency)
124
- const { session, isFirstSession } = await this.sessionManager.startSession(
113
+ const { session } = await this.sessionManager.startSession(
125
114
  projectPath,
126
115
  projectHash,
127
116
  target,
@@ -167,30 +156,31 @@ export class FlowExecutor {
167
156
  console.log(chalk.green('\n✓ Flow environment ready!\n'));
168
157
  }
169
158
 
159
+ /**
160
+ * Resolve target from ID string to Target object
161
+ */
162
+ private resolveTarget(targetId: string): Target {
163
+ const targetOption = targetManager.getTarget(targetId);
164
+ if (targetOption._tag === 'None') {
165
+ throw new Error(`Unknown target: ${targetId}`);
166
+ }
167
+ return targetOption.value;
168
+ }
169
+
170
170
  /**
171
171
  * Clear user settings in replace mode
172
172
  * This ensures a clean slate for Flow's configuration
173
173
  */
174
- private async clearUserSettings(
175
- projectPath: string,
176
- target: 'claude-code' | 'opencode'
177
- ): Promise<void> {
178
- const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
174
+ private async clearUserSettings(projectPath: string, targetOrId: Target | string): Promise<void> {
175
+ const target = typeof targetOrId === 'string' ? this.resolveTarget(targetOrId) : targetOrId;
179
176
  const fs = await import('node:fs/promises');
180
177
  const path = await import('node:path');
181
178
  const { existsSync } = await import('node:fs');
182
179
 
183
- if (!existsSync(targetDir)) {
184
- return;
185
- }
186
-
187
- // Get directory names for this target
188
- const dirs = target === 'claude-code'
189
- ? { agents: 'agents', commands: 'commands' }
190
- : { agents: 'agent', commands: 'command' };
180
+ // All paths use target.config.* directly (full paths relative to projectPath)
191
181
 
192
- // 1. Clear agents directory (including AGENTS.md rules file)
193
- const agentsDir = path.join(targetDir, dirs.agents);
182
+ // 1. Clear agents directory (including AGENTS.md rules file for Claude Code)
183
+ const agentsDir = path.join(projectPath, target.config.agentDir);
194
184
  if (existsSync(agentsDir)) {
195
185
  const files = await fs.readdir(agentsDir);
196
186
  for (const file of files) {
@@ -198,17 +188,19 @@ export class FlowExecutor {
198
188
  }
199
189
  }
200
190
 
201
- // 2. Clear commands directory
202
- const commandsDir = path.join(targetDir, dirs.commands);
203
- if (existsSync(commandsDir)) {
204
- const files = await fs.readdir(commandsDir);
205
- for (const file of files) {
206
- await fs.unlink(path.join(commandsDir, file));
191
+ // 2. Clear commands directory (if target supports slash commands)
192
+ if (target.config.slashCommandsDir) {
193
+ const commandsDir = path.join(projectPath, target.config.slashCommandsDir);
194
+ if (existsSync(commandsDir)) {
195
+ const files = await fs.readdir(commandsDir);
196
+ for (const file of files) {
197
+ await fs.unlink(path.join(commandsDir, file));
198
+ }
207
199
  }
208
200
  }
209
201
 
210
- // 3. Clear hooks directory
211
- const hooksDir = path.join(targetDir, 'hooks');
202
+ // 3. Clear hooks directory (in configDir)
203
+ const hooksDir = path.join(projectPath, target.config.configDir, 'hooks');
212
204
  if (existsSync(hooksDir)) {
213
205
  const files = await fs.readdir(hooksDir);
214
206
  for (const file of files) {
@@ -216,40 +208,33 @@ export class FlowExecutor {
216
208
  }
217
209
  }
218
210
 
219
- // 4. Clear MCP configuration completely
220
- const configPath = target === 'claude-code'
221
- ? path.join(targetDir, 'settings.json')
222
- : path.join(targetDir, '.mcp.json');
211
+ // 4. Clear MCP configuration using target config
212
+ const configPath = path.join(projectPath, target.config.configFile);
213
+ const mcpPath = target.config.mcpConfigPath;
223
214
 
224
215
  if (existsSync(configPath)) {
225
- if (target === 'claude-code') {
226
- // For Claude Code, clear entire MCP section to remove all user config
227
- const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
228
- if (config.mcp) {
229
- // Remove entire MCP configuration, not just servers
230
- delete config.mcp;
231
- await fs.writeFile(configPath, JSON.stringify(config, null, 2));
232
- }
233
- } else {
234
- // For OpenCode, clear the entire .mcp.json file
235
- await fs.writeFile(configPath, JSON.stringify({ servers: {} }, null, 2));
216
+ const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
217
+ if (config[mcpPath]) {
218
+ // Remove entire MCP configuration section
219
+ delete config[mcpPath];
220
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
236
221
  }
237
222
  }
238
223
 
239
- // 5. Clear AGENTS.md rules file (for OpenCode)
240
- // Claude Code AGENTS.md is already handled in agents directory
241
- if (target === 'opencode') {
242
- const rulesPath = path.join(targetDir, 'AGENTS.md');
224
+ // 5. Clear rules file if target has one defined (for targets like OpenCode)
225
+ // Claude Code puts AGENTS.md in agents directory, handled above
226
+ if (target.config.rulesFile) {
227
+ const rulesPath = path.join(projectPath, target.config.rulesFile);
243
228
  if (existsSync(rulesPath)) {
244
229
  await fs.unlink(rulesPath);
245
230
  }
246
231
  }
247
232
 
248
233
  // 6. Clear single files (output styles like silent.md)
249
- // These are now in the target directory, not project root
234
+ // These are in the configDir
250
235
  const singleFiles = ['silent.md']; // Add other known single files here
251
236
  for (const fileName of singleFiles) {
252
- const filePath = path.join(targetDir, fileName);
237
+ const filePath = path.join(projectPath, target.config.configDir, fileName);
253
238
  if (existsSync(filePath)) {
254
239
  await fs.unlink(filePath);
255
240
  }
@@ -294,13 +279,11 @@ export class FlowExecutor {
294
279
  /**
295
280
  * Show attach summary
296
281
  */
297
- private showAttachSummary(result: any): void {
282
+ private showAttachSummary(result: AttachResult): void {
298
283
  const items = [];
299
284
 
300
285
  if (result.agentsAdded.length > 0) {
301
- items.push(
302
- `${result.agentsAdded.length} agent${result.agentsAdded.length > 1 ? 's' : ''}`
303
- );
286
+ items.push(`${result.agentsAdded.length} agent${result.agentsAdded.length > 1 ? 's' : ''}`);
304
287
  }
305
288
 
306
289
  if (result.commandsAdded.length > 0) {
@@ -316,9 +299,7 @@ export class FlowExecutor {
316
299
  }
317
300
 
318
301
  if (result.hooksAdded.length > 0) {
319
- items.push(
320
- `${result.hooksAdded.length} hook${result.hooksAdded.length > 1 ? 's' : ''}`
321
- );
302
+ items.push(`${result.hooksAdded.length} hook${result.hooksAdded.length > 1 ? 's' : ''}`);
322
303
  }
323
304
 
324
305
  if (result.rulesAppended) {
@@ -329,7 +310,8 @@ export class FlowExecutor {
329
310
  console.log(chalk.green(` ✓ Added: ${items.join(', ')}`));
330
311
  }
331
312
 
332
- const overridden = result.agentsOverridden.length +
313
+ const overridden =
314
+ result.agentsOverridden.length +
333
315
  result.commandsOverridden.length +
334
316
  result.mcpServersOverridden.length +
335
317
  result.hooksOverridden.length;
@@ -28,16 +28,14 @@ export interface ByteFormatOptions {
28
28
  export function formatBytes(bytes: number, options: ByteFormatOptions = {}): string {
29
29
  const { decimals = 2, shortUnits = false } = options;
30
30
 
31
- const units = shortUnits
32
- ? ['B', 'KB', 'MB', 'GB', 'TB']
33
- : ['Bytes', 'KB', 'MB', 'GB', 'TB'];
31
+ const units = shortUnits ? ['B', 'KB', 'MB', 'GB', 'TB'] : ['Bytes', 'KB', 'MB', 'GB', 'TB'];
34
32
 
35
33
  if (bytes === 0) {
36
34
  return `0 ${units[0]}`;
37
35
  }
38
36
 
39
37
  const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
40
- const value = bytes / Math.pow(1024, i);
38
+ const value = bytes / 1024 ** i;
41
39
 
42
40
  // Format with specified decimal places
43
41
  const formatted = value.toFixed(decimals);
@@ -95,7 +95,7 @@ export const retry = async <T>(
95
95
  ): AsyncResult<T> => {
96
96
  const { maxAttempts, delayMs = 1000, backoff = 2, onRetry } = options;
97
97
 
98
- let lastError: AppError | null = null;
98
+ let lastError: AppError = { type: 'unknown', message: 'No attempts made', code: 'NO_ATTEMPTS' };
99
99
  let currentDelay = delayMs;
100
100
 
101
101
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -117,7 +117,7 @@ export const retry = async <T>(
117
117
  }
118
118
  }
119
119
 
120
- return failure(lastError!);
120
+ return failure(lastError);
121
121
  };
122
122
 
123
123
  /**
@@ -231,8 +231,9 @@ export const memoizeAsync = <Args extends any[], T>(
231
231
  return (...args: Args): AsyncResult<T, AppError> => {
232
232
  const key = keyFn ? keyFn(...args) : JSON.stringify(args);
233
233
 
234
- if (cache.has(key)) {
235
- return cache.get(key)!;
234
+ const cached = cache.get(key);
235
+ if (cached !== undefined) {
236
+ return cached;
236
237
  }
237
238
 
238
239
  const result = fn(...args);
@@ -81,7 +81,7 @@ export const retry = async <T>(
81
81
  ): Promise<Result<T, AppError>> => {
82
82
  const { maxRetries, delayMs, backoff = 2, onRetry } = options;
83
83
 
84
- let lastError: AppError | null = null;
84
+ let lastError: AppError = { type: 'unknown', message: 'Unknown error', code: 'UNKNOWN' };
85
85
  let currentDelay = delayMs;
86
86
 
87
87
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
@@ -103,7 +103,7 @@ export const retry = async <T>(
103
103
  }
104
104
  }
105
105
 
106
- return failure(lastError!);
106
+ return failure(lastError);
107
107
  };
108
108
 
109
109
  /**