@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.
- package/CHANGELOG.md +28 -0
- package/README.md +44 -0
- package/package.json +79 -73
- package/src/commands/flow/execute-v2.ts +37 -29
- package/src/commands/flow/prompt.ts +5 -3
- package/src/commands/flow/types.ts +0 -2
- package/src/commands/flow-command.ts +20 -13
- package/src/commands/hook-command.ts +1 -3
- package/src/commands/settings/checkbox-config.ts +128 -0
- package/src/commands/settings/index.ts +6 -0
- package/src/commands/settings-command.ts +84 -156
- package/src/config/ai-config.ts +60 -41
- package/src/core/agent-loader.ts +11 -6
- package/src/core/attach/file-attacher.ts +172 -0
- package/src/core/attach/index.ts +5 -0
- package/src/core/attach-manager.ts +117 -171
- 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 +3 -3
- package/src/services/target-installer.ts +11 -26
- package/src/targets/claude-code.ts +35 -81
- package/src/targets/opencode.ts +28 -68
- package/src/targets/shared/index.ts +7 -0
- package/src/targets/shared/mcp-transforms.ts +132 -0
- package/src/targets/shared/target-operations.ts +135 -0
- 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 +4 -4
- 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 +6 -8
- package/src/utils/version.ts +4 -2
- 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/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,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
|
|
@@ -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(
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
9
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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(
|
|
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
|
|
220
|
-
const configPath = target
|
|
221
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
240
|
-
// Claude Code AGENTS.md
|
|
241
|
-
if (target
|
|
242
|
-
const rulesPath = path.join(
|
|
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
|
|
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(
|
|
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:
|
|
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 =
|
|
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 /
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
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
|
|
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
|
/**
|