@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.
- package/CHANGELOG.md +23 -0
- package/README.md +44 -0
- package/package.json +79 -73
- package/src/commands/flow/execute-v2.ts +39 -30
- package/src/commands/flow/index.ts +2 -4
- package/src/commands/flow/prompt.ts +5 -3
- package/src/commands/flow/types.ts +0 -9
- package/src/commands/flow-command.ts +20 -13
- package/src/commands/hook-command.ts +1 -3
- package/src/commands/settings-command.ts +36 -33
- package/src/config/ai-config.ts +60 -41
- package/src/core/agent-loader.ts +11 -6
- package/src/core/attach-manager.ts +92 -84
- package/src/core/backup-manager.ts +35 -29
- package/src/core/cleanup-handler.ts +11 -8
- package/src/core/error-handling.ts +23 -30
- package/src/core/flow-executor.ts +58 -76
- package/src/core/formatting/bytes.ts +2 -4
- package/src/core/functional/async.ts +5 -4
- package/src/core/functional/error-handler.ts +2 -2
- package/src/core/git-stash-manager.ts +21 -10
- package/src/core/installers/file-installer.ts +0 -1
- package/src/core/installers/mcp-installer.ts +0 -1
- package/src/core/project-manager.ts +24 -18
- package/src/core/secrets-manager.ts +54 -73
- package/src/core/session-manager.ts +20 -22
- package/src/core/state-detector.ts +139 -80
- package/src/core/template-loader.ts +13 -31
- package/src/core/upgrade-manager.ts +122 -69
- package/src/index.ts +8 -5
- package/src/services/auto-upgrade.ts +1 -1
- package/src/services/config-service.ts +41 -29
- package/src/services/global-config.ts +2 -2
- package/src/services/target-installer.ts +9 -7
- package/src/targets/claude-code.ts +28 -15
- package/src/targets/opencode.ts +17 -6
- package/src/types/cli.types.ts +2 -2
- package/src/types/provider.types.ts +1 -7
- package/src/types/session.types.ts +11 -11
- package/src/types/target.types.ts +3 -1
- package/src/types/todo.types.ts +1 -1
- package/src/types.ts +1 -1
- package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
- package/src/utils/agent-enhancer.ts +111 -3
- package/src/utils/config/paths.ts +3 -1
- package/src/utils/config/target-utils.ts +2 -2
- package/src/utils/display/banner.ts +2 -2
- package/src/utils/display/notifications.ts +58 -45
- package/src/utils/display/status.ts +29 -12
- package/src/utils/files/file-operations.ts +1 -1
- package/src/utils/files/sync-utils.ts +38 -41
- package/src/utils/index.ts +19 -27
- package/src/utils/package-manager-detector.ts +15 -5
- package/src/utils/security/security.ts +8 -4
- package/src/utils/target-selection.ts +5 -2
- package/src/utils/version.ts +4 -2
- package/src/commands/flow/execute.ts +0 -453
- package/src/commands/flow/setup.ts +0 -312
- package/src/commands/flow-orchestrator.ts +0 -328
- package/src/commands/init-command.ts +0 -92
- package/src/commands/init-core.ts +0 -331
- package/src/commands/run-command.ts +0 -126
- package/src/core/agent-manager.ts +0 -174
- package/src/core/loop-controller.ts +0 -200
- package/src/core/rule-loader.ts +0 -147
- package/src/core/rule-manager.ts +0 -240
- package/src/services/claude-config-service.ts +0 -252
- package/src/services/first-run-setup.ts +0 -220
- package/src/services/smart-config-service.ts +0 -269
- 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:
|
|
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
|
-
*
|
|
70
|
+
* Resolve target from ID string to Target object
|
|
70
71
|
*/
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
85
|
-
): Promise<Array<{ name: string; config:
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
126
|
-
|
|
130
|
+
_projectHash: string,
|
|
131
|
+
targetOrId: Target | string,
|
|
127
132
|
templates: FlowTemplates,
|
|
128
133
|
manifest: BackupManifest
|
|
129
134
|
): Promise<AttachResult> {
|
|
130
|
-
|
|
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
|
-
//
|
|
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(
|
|
155
|
+
await this.attachAgents(projectPath, target, templates.agents, result, manifest);
|
|
151
156
|
|
|
152
157
|
// 2. Attach commands
|
|
153
|
-
await this.attachCommands(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
195
|
-
target:
|
|
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
|
-
|
|
201
|
-
const agentsDir = path.join(
|
|
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
|
-
|
|
237
|
-
target:
|
|
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
|
-
|
|
243
|
-
const commandsDir = path.join(
|
|
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
|
-
|
|
279
|
-
target:
|
|
277
|
+
projectPath: string,
|
|
278
|
+
target: Target,
|
|
280
279
|
rules: string,
|
|
281
280
|
result: AttachResult,
|
|
282
281
|
manifest: BackupManifest
|
|
283
282
|
): Promise<void> {
|
|
284
|
-
//
|
|
285
|
-
// OpenCode:
|
|
286
|
-
|
|
287
|
-
const rulesPath =
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
338
|
-
target:
|
|
339
|
-
mcpServers: Array<{ name: string; config:
|
|
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
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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:
|
|
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
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
401
|
+
projectPath: string,
|
|
402
|
+
target: Target,
|
|
395
403
|
hooks: Array<{ name: string; content: string }>,
|
|
396
404
|
result: AttachResult,
|
|
397
|
-
|
|
405
|
+
_manifest: BackupManifest
|
|
398
406
|
): Promise<void> {
|
|
399
|
-
|
|
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
|
|
434
|
-
const
|
|
435
|
-
|
|
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
|
|
439
|
-
const filePath = path.join(
|
|
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 {
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
|
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 {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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(
|
|
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 (
|
|
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(
|
|
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(
|
|
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
|