@sylphx/flow 2.1.2 → 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 (70) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +44 -0
  3. package/package.json +79 -73
  4. package/src/commands/flow/execute-v2.ts +39 -30
  5. package/src/commands/flow/index.ts +2 -4
  6. package/src/commands/flow/prompt.ts +5 -3
  7. package/src/commands/flow/types.ts +0 -9
  8. package/src/commands/flow-command.ts +20 -13
  9. package/src/commands/hook-command.ts +1 -3
  10. package/src/commands/settings-command.ts +36 -33
  11. package/src/config/ai-config.ts +60 -41
  12. package/src/core/agent-loader.ts +11 -6
  13. package/src/core/attach-manager.ts +92 -84
  14. package/src/core/backup-manager.ts +35 -29
  15. package/src/core/cleanup-handler.ts +11 -8
  16. package/src/core/error-handling.ts +23 -30
  17. package/src/core/flow-executor.ts +58 -76
  18. package/src/core/formatting/bytes.ts +2 -4
  19. package/src/core/functional/async.ts +5 -4
  20. package/src/core/functional/error-handler.ts +2 -2
  21. package/src/core/git-stash-manager.ts +21 -10
  22. package/src/core/installers/file-installer.ts +0 -1
  23. package/src/core/installers/mcp-installer.ts +0 -1
  24. package/src/core/project-manager.ts +24 -18
  25. package/src/core/secrets-manager.ts +54 -73
  26. package/src/core/session-manager.ts +20 -22
  27. package/src/core/state-detector.ts +139 -80
  28. package/src/core/template-loader.ts +13 -31
  29. package/src/core/upgrade-manager.ts +122 -69
  30. package/src/index.ts +8 -5
  31. package/src/services/auto-upgrade.ts +1 -1
  32. package/src/services/config-service.ts +41 -29
  33. package/src/services/global-config.ts +2 -2
  34. package/src/services/target-installer.ts +9 -7
  35. package/src/targets/claude-code.ts +28 -15
  36. package/src/targets/opencode.ts +17 -6
  37. package/src/types/cli.types.ts +2 -2
  38. package/src/types/provider.types.ts +1 -7
  39. package/src/types/session.types.ts +11 -11
  40. package/src/types/target.types.ts +3 -1
  41. package/src/types/todo.types.ts +1 -1
  42. package/src/types.ts +1 -1
  43. package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
  44. package/src/utils/agent-enhancer.ts +111 -3
  45. package/src/utils/config/paths.ts +3 -1
  46. package/src/utils/config/target-utils.ts +2 -2
  47. package/src/utils/display/banner.ts +2 -2
  48. package/src/utils/display/notifications.ts +58 -45
  49. package/src/utils/display/status.ts +29 -12
  50. package/src/utils/files/file-operations.ts +1 -1
  51. package/src/utils/files/sync-utils.ts +38 -41
  52. package/src/utils/index.ts +19 -27
  53. package/src/utils/package-manager-detector.ts +15 -5
  54. package/src/utils/security/security.ts +8 -4
  55. package/src/utils/target-selection.ts +5 -2
  56. package/src/utils/version.ts +4 -2
  57. package/src/commands/flow/execute.ts +0 -453
  58. package/src/commands/flow/setup.ts +0 -312
  59. package/src/commands/flow-orchestrator.ts +0 -328
  60. package/src/commands/init-command.ts +0 -92
  61. package/src/commands/init-core.ts +0 -331
  62. package/src/commands/run-command.ts +0 -126
  63. package/src/core/agent-manager.ts +0 -174
  64. package/src/core/loop-controller.ts +0 -200
  65. package/src/core/rule-loader.ts +0 -147
  66. package/src/core/rule-manager.ts +0 -240
  67. package/src/services/claude-config-service.ts +0 -252
  68. package/src/services/first-run-setup.ts +0 -220
  69. package/src/services/smart-config-service.ts +0 -269
  70. package/src/types/api.types.ts +0 -9
@@ -4,15 +4,17 @@
4
4
  * Strategy: Direct override with backup, restore on cleanup
5
5
  */
6
6
 
7
+ import { createHash } from 'node:crypto';
8
+ import { existsSync } from 'node:fs';
7
9
  import fs from 'node:fs/promises';
8
10
  import path from 'node:path';
9
- import { existsSync } from 'node:fs';
10
- import { createHash } from 'node:crypto';
11
11
  import chalk from 'chalk';
12
- import { ProjectManager } from './project-manager.js';
13
- import type { BackupManifest } from './backup-manager.js';
14
- import { GlobalConfigService } from '../services/global-config.js';
15
12
  import { MCP_SERVER_REGISTRY } from '../config/servers.js';
13
+ import { GlobalConfigService } from '../services/global-config.js';
14
+ import type { Target } from '../types/target.types.js';
15
+ import type { BackupManifest } from './backup-manager.js';
16
+ import type { ProjectManager } from './project-manager.js';
17
+ import { targetManager } from './target-manager.js';
16
18
 
17
19
  export interface AttachResult {
18
20
  agentsAdded: string[];
@@ -39,13 +41,12 @@ export interface FlowTemplates {
39
41
  agents: Array<{ name: string; content: string }>;
40
42
  commands: Array<{ name: string; content: string }>;
41
43
  rules?: string;
42
- mcpServers: Array<{ name: string; config: any }>;
44
+ mcpServers: Array<{ name: string; config: Record<string, unknown> }>;
43
45
  hooks: Array<{ name: string; content: string }>;
44
46
  singleFiles: Array<{ path: string; content: string }>;
45
47
  }
46
48
 
47
49
  export class AttachManager {
48
- private projectManager: ProjectManager;
49
50
  private configService: GlobalConfigService;
50
51
 
51
52
  constructor(projectManager: ProjectManager) {
@@ -66,26 +67,25 @@ export class AttachManager {
66
67
  }
67
68
 
68
69
  /**
69
- * Get target-specific directory names
70
+ * Resolve target from ID string to Target object
70
71
  */
71
- private getTargetDirs(target: 'claude-code' | 'opencode'): {
72
- agents: string;
73
- commands: string;
74
- } {
75
- return target === 'claude-code'
76
- ? { agents: 'agents', commands: 'commands' }
77
- : { agents: 'agent', commands: 'command' };
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
78
  }
79
79
 
80
80
  /**
81
81
  * Load global MCP servers from ~/.sylphx-flow/mcp-config.json
82
82
  */
83
83
  private async loadGlobalMCPServers(
84
- target: 'claude-code' | 'opencode'
85
- ): Promise<Array<{ name: string; config: any }>> {
84
+ _target: Target
85
+ ): Promise<Array<{ name: string; config: Record<string, unknown> }>> {
86
86
  try {
87
87
  const enabledServers = await this.configService.getEnabledMCPServers();
88
- const servers: Array<{ name: string; config: any }> = [];
88
+ const servers: Array<{ name: string; config: Record<string, unknown> }> = [];
89
89
 
90
90
  for (const [serverKey, serverConfig] of Object.entries(enabledServers)) {
91
91
  // Lookup server definition in registry
@@ -97,7 +97,7 @@ export class AttachManager {
97
97
  }
98
98
 
99
99
  // Clone the server config from registry
100
- let config: any = { ...serverDef.config };
100
+ const config: Record<string, unknown> = { ...serverDef.config };
101
101
 
102
102
  // Merge environment variables from global config
103
103
  if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
@@ -110,7 +110,7 @@ export class AttachManager {
110
110
  }
111
111
 
112
112
  return servers;
113
- } catch (error) {
113
+ } catch (_error) {
114
114
  // If global config doesn't exist or fails to load, return empty array
115
115
  return [];
116
116
  }
@@ -119,15 +119,21 @@ export class AttachManager {
119
119
  /**
120
120
  * Attach Flow templates to project
121
121
  * Strategy: Override with warning, backup handles restoration
122
+ * @param projectPath - Project root path
123
+ * @param _projectHash - Project hash (unused but kept for API compatibility)
124
+ * @param targetOrId - Target object or target ID string
125
+ * @param templates - Flow templates to attach
126
+ * @param manifest - Backup manifest to track changes
122
127
  */
123
128
  async attach(
124
129
  projectPath: string,
125
- projectHash: string,
126
- target: 'claude-code' | 'opencode',
130
+ _projectHash: string,
131
+ targetOrId: Target | string,
127
132
  templates: FlowTemplates,
128
133
  manifest: BackupManifest
129
134
  ): Promise<AttachResult> {
130
- const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
135
+ // Resolve target from ID if needed
136
+ const target = typeof targetOrId === 'string' ? this.resolveTarget(targetOrId) : targetOrId;
131
137
 
132
138
  const result: AttachResult = {
133
139
  agentsAdded: [],
@@ -143,18 +149,17 @@ export class AttachManager {
143
149
  conflicts: [],
144
150
  };
145
151
 
146
- // Ensure target directory exists
147
- await fs.mkdir(targetDir, { recursive: true });
152
+ // All paths are relative to projectPath, using target.config.* directly
148
153
 
149
154
  // 1. Attach agents
150
- await this.attachAgents(targetDir, target, templates.agents, result, manifest);
155
+ await this.attachAgents(projectPath, target, templates.agents, result, manifest);
151
156
 
152
157
  // 2. Attach commands
153
- await this.attachCommands(targetDir, target, templates.commands, result, manifest);
158
+ await this.attachCommands(projectPath, target, templates.commands, result, manifest);
154
159
 
155
160
  // 3. Attach rules (if applicable)
156
161
  if (templates.rules) {
157
- await this.attachRules(targetDir, target, templates.rules, result, manifest);
162
+ await this.attachRules(projectPath, target, templates.rules, result, manifest);
158
163
  }
159
164
 
160
165
  // 4. Attach MCP servers (merge global + template servers)
@@ -162,18 +167,12 @@ export class AttachManager {
162
167
  const allMCPServers = [...globalMCPServers, ...templates.mcpServers];
163
168
 
164
169
  if (allMCPServers.length > 0) {
165
- await this.attachMCPServers(
166
- targetDir,
167
- target,
168
- allMCPServers,
169
- result,
170
- manifest
171
- );
170
+ await this.attachMCPServers(projectPath, target, allMCPServers, result, manifest);
172
171
  }
173
172
 
174
173
  // 5. Attach hooks
175
174
  if (templates.hooks.length > 0) {
176
- await this.attachHooks(targetDir, templates.hooks, result, manifest);
175
+ await this.attachHooks(projectPath, target, templates.hooks, result, manifest);
177
176
  }
178
177
 
179
178
  // 6. Attach single files
@@ -191,14 +190,14 @@ export class AttachManager {
191
190
  * Attach agents (override strategy)
192
191
  */
193
192
  private async attachAgents(
194
- targetDir: string,
195
- target: 'claude-code' | 'opencode',
193
+ projectPath: string,
194
+ target: Target,
196
195
  agents: Array<{ name: string; content: string }>,
197
196
  result: AttachResult,
198
197
  manifest: BackupManifest
199
198
  ): Promise<void> {
200
- const dirs = this.getTargetDirs(target);
201
- const agentsDir = path.join(targetDir, dirs.agents);
199
+ // Use full path from target config
200
+ const agentsDir = path.join(projectPath, target.config.agentDir);
202
201
  await fs.mkdir(agentsDir, { recursive: true });
203
202
 
204
203
  for (const agent of agents) {
@@ -233,14 +232,14 @@ export class AttachManager {
233
232
  * Attach commands (override strategy)
234
233
  */
235
234
  private async attachCommands(
236
- targetDir: string,
237
- target: 'claude-code' | 'opencode',
235
+ projectPath: string,
236
+ target: Target,
238
237
  commands: Array<{ name: string; content: string }>,
239
238
  result: AttachResult,
240
239
  manifest: BackupManifest
241
240
  ): Promise<void> {
242
- const dirs = this.getTargetDirs(target);
243
- const commandsDir = path.join(targetDir, dirs.commands);
241
+ // Use full path from target config
242
+ const commandsDir = path.join(projectPath, target.config.slashCommandsDir);
244
243
  await fs.mkdir(commandsDir, { recursive: true });
245
244
 
246
245
  for (const command of commands) {
@@ -275,19 +274,18 @@ export class AttachManager {
275
274
  * Attach rules (append strategy for AGENTS.md)
276
275
  */
277
276
  private async attachRules(
278
- targetDir: string,
279
- target: 'claude-code' | 'opencode',
277
+ projectPath: string,
278
+ target: Target,
280
279
  rules: string,
281
280
  result: AttachResult,
282
281
  manifest: BackupManifest
283
282
  ): Promise<void> {
284
- // Claude Code: .claude/agents/AGENTS.md
285
- // OpenCode: .opencode/AGENTS.md
286
- const dirs = this.getTargetDirs(target);
287
- const rulesPath =
288
- target === 'claude-code'
289
- ? path.join(targetDir, dirs.agents, 'AGENTS.md')
290
- : path.join(targetDir, 'AGENTS.md');
283
+ // Use full paths from target config:
284
+ // - rulesFile defined (e.g., OpenCode): projectPath/rulesFile
285
+ // - rulesFile undefined (e.g., Claude Code): projectPath/agentDir/AGENTS.md
286
+ const rulesPath = target.config.rulesFile
287
+ ? path.join(projectPath, target.config.rulesFile)
288
+ : path.join(projectPath, target.config.agentDir, 'AGENTS.md');
291
289
 
292
290
  if (existsSync(rulesPath)) {
293
291
  // User has AGENTS.md, append Flow rules
@@ -332,34 +330,43 @@ ${rules}
332
330
 
333
331
  /**
334
332
  * Attach MCP servers (merge strategy)
333
+ * Uses target.config.configFile and target.config.mcpConfigPath
334
+ * Note: configFile is relative to project root, not targetDir
335
335
  */
336
336
  private async attachMCPServers(
337
- targetDir: string,
338
- target: 'claude-code' | 'opencode',
339
- mcpServers: Array<{ name: string; config: any }>,
337
+ projectPath: string,
338
+ target: Target,
339
+ mcpServers: Array<{ name: string; config: Record<string, unknown> }>,
340
340
  result: AttachResult,
341
341
  manifest: BackupManifest
342
342
  ): Promise<void> {
343
- // Claude Code: .claude/settings.json (mcp.servers)
344
- // OpenCode: .opencode/.mcp.json
345
- const configPath =
346
- target === 'claude-code'
347
- ? path.join(targetDir, 'settings.json')
348
- : path.join(targetDir, '.mcp.json');
343
+ // Use target config for file path and MCP config structure
344
+ // Claude Code: .mcp.json at project root with mcpServers key
345
+ // OpenCode: opencode.jsonc at project root with mcp key
346
+ const configPath = path.join(projectPath, target.config.configFile);
347
+ const mcpPath = target.config.mcpConfigPath;
349
348
 
350
- let config: any = {};
349
+ let config: Record<string, unknown> = {};
351
350
 
352
351
  if (existsSync(configPath)) {
353
352
  config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
354
353
  }
355
354
 
356
- // Ensure mcp.servers exists
357
- if (!config.mcp) config.mcp = {};
358
- if (!config.mcp.servers) config.mcp.servers = {};
355
+ // Get or create the MCP servers object at the correct path
356
+ // Claude Code: config.mcpServers = {}
357
+ // OpenCode: config.mcp = {}
358
+ let mcpContainer = config[mcpPath] as Record<string, unknown> | undefined;
359
+ if (!mcpContainer) {
360
+ mcpContainer = {};
361
+ config[mcpPath] = mcpContainer;
362
+ }
359
363
 
360
364
  // Add Flow MCP servers
361
365
  for (const server of mcpServers) {
362
- if (config.mcp.servers[server.name]) {
366
+ // Transform the server config for this target
367
+ const transformedConfig = target.transformMCPConfig(server.config as any, server.name);
368
+
369
+ if (mcpContainer[server.name]) {
363
370
  // Conflict: user has same MCP server
364
371
  result.mcpServersOverridden.push(server.name);
365
372
  result.conflicts.push({
@@ -372,8 +379,8 @@ ${rules}
372
379
  result.mcpServersAdded.push(server.name);
373
380
  }
374
381
 
375
- // Override with Flow config
376
- config.mcp.servers[server.name] = server.config;
382
+ // Override with Flow config (transformed for target)
383
+ mcpContainer[server.name] = transformedConfig;
377
384
  }
378
385
 
379
386
  // Write updated config
@@ -383,7 +390,7 @@ ${rules}
383
390
  manifest.backup.config = {
384
391
  path: configPath,
385
392
  hash: await this.calculateFileHash(configPath),
386
- mcpServersCount: Object.keys(config.mcp.servers).length,
393
+ mcpServersCount: Object.keys(mcpContainer).length,
387
394
  };
388
395
  }
389
396
 
@@ -391,12 +398,14 @@ ${rules}
391
398
  * Attach hooks (override strategy)
392
399
  */
393
400
  private async attachHooks(
394
- targetDir: string,
401
+ projectPath: string,
402
+ target: Target,
395
403
  hooks: Array<{ name: string; content: string }>,
396
404
  result: AttachResult,
397
- manifest: BackupManifest
405
+ _manifest: BackupManifest
398
406
  ): Promise<void> {
399
- const hooksDir = path.join(targetDir, 'hooks');
407
+ // Hooks are in configDir/hooks
408
+ const hooksDir = path.join(projectPath, target.config.configDir, 'hooks');
400
409
  await fs.mkdir(hooksDir, { recursive: true });
401
410
 
402
411
  for (const hook of hooks) {
@@ -430,13 +439,16 @@ ${rules}
430
439
  result: AttachResult,
431
440
  manifest: BackupManifest
432
441
  ): Promise<void> {
433
- // Get target from manifest to determine correct directory
434
- const target = manifest.target;
435
- const targetDir = this.projectManager.getTargetConfigDir(projectPath, target);
442
+ // Get target from manifest to determine config directory
443
+ const targetOption = targetManager.getTarget(manifest.target);
444
+ if (targetOption._tag === 'None') {
445
+ return; // Unknown target, skip
446
+ }
447
+ const target = targetOption.value;
436
448
 
437
449
  for (const file of singleFiles) {
438
- // Write to target config directory, not project root
439
- const filePath = path.join(targetDir, file.path);
450
+ // Write to target config directory (e.g., .claude/ or .opencode/)
451
+ const filePath = path.join(projectPath, target.config.configDir, file.path);
440
452
  const existed = existsSync(filePath);
441
453
 
442
454
  if (existed) {
@@ -474,13 +486,9 @@ ${rules}
474
486
  console.log(chalk.yellow('\n⚠️ Conflicts detected:\n'));
475
487
 
476
488
  for (const conflict of result.conflicts) {
477
- console.log(
478
- chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`)
479
- );
489
+ console.log(chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`));
480
490
  }
481
491
 
482
- console.log(
483
- chalk.dim('\n Don\'t worry! All overridden content will be restored on exit.\n')
484
- );
492
+ console.log(chalk.dim("\n Don't worry! All overridden content will be restored on exit.\n"));
485
493
  }
486
494
  }
@@ -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