@sylphx/flow 3.18.0 → 3.19.1
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 +66 -0
- package/package.json +1 -3
- package/src/commands/flow/execute-v2.ts +126 -128
- package/src/commands/flow-command.ts +52 -42
- package/src/config/index.ts +0 -20
- package/src/config/targets.ts +1 -1
- package/src/core/__tests__/backup-restore.test.ts +1 -1
- package/src/core/__tests__/cleanup-handler.test.ts +292 -0
- package/src/core/__tests__/git-stash-manager.test.ts +246 -0
- package/src/core/__tests__/secrets-manager.test.ts +126 -0
- package/src/core/__tests__/session-cleanup.test.ts +147 -0
- package/src/core/agent-loader.ts +2 -2
- package/src/core/attach-manager.ts +12 -78
- package/src/core/backup-manager.ts +8 -20
- package/src/core/cleanup-handler.ts +187 -11
- package/src/core/flow-executor.ts +139 -126
- package/src/core/functional/index.ts +0 -11
- package/src/core/git-stash-manager.ts +50 -68
- package/src/core/index.ts +1 -1
- package/src/core/project-manager.ts +26 -43
- package/src/core/secrets-manager.ts +15 -18
- package/src/core/session-manager.ts +32 -41
- package/src/core/state-detector.ts +4 -15
- package/src/core/target-manager.ts +6 -3
- package/src/core/target-resolver.ts +14 -9
- package/src/core/template-loader.ts +7 -33
- package/src/core/upgrade-manager.ts +5 -16
- package/src/index.ts +7 -36
- package/src/services/auto-upgrade.ts +6 -14
- package/src/services/config-service.ts +7 -23
- package/src/services/index.ts +1 -1
- package/src/targets/claude-code.ts +24 -109
- package/src/targets/functional/claude-code-logic.ts +47 -103
- package/src/targets/opencode.ts +63 -197
- package/src/targets/shared/mcp-transforms.ts +20 -43
- package/src/targets/shared/target-operations.ts +1 -54
- package/src/types/agent.types.ts +5 -3
- package/src/types/mcp.types.ts +38 -1
- package/src/types/target.types.ts +4 -24
- package/src/types.ts +4 -0
- package/src/utils/agent-enhancer.ts +1 -1
- package/src/utils/config/target-config.ts +8 -14
- package/src/utils/config/target-utils.ts +1 -50
- package/src/utils/errors.ts +13 -0
- package/src/utils/files/file-operations.ts +16 -0
- package/src/utils/files/sync-utils.ts +5 -5
- package/src/utils/index.ts +1 -1
- package/src/utils/object-utils.ts +10 -2
- package/src/utils/security/secret-utils.ts +2 -2
- package/src/core/error-handling.ts +0 -512
- package/src/core/functional/async.ts +0 -101
- package/src/core/functional/either.ts +0 -109
- package/src/core/functional/error-handler.ts +0 -135
- package/src/core/functional/pipe.ts +0 -189
- package/src/core/functional/validation.ts +0 -138
- package/src/types/mcp-config.types.ts +0 -448
- package/src/utils/error-handler.ts +0 -53
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
* All projects store data in ~/.sylphx-flow/ isolated by project hash
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { exec } from 'node:child_process';
|
|
7
8
|
import crypto from 'node:crypto';
|
|
8
9
|
import { existsSync } from 'node:fs';
|
|
9
10
|
import fs from 'node:fs/promises';
|
|
10
11
|
import os from 'node:os';
|
|
11
12
|
import path from 'node:path';
|
|
13
|
+
import { promisify } from 'node:util';
|
|
12
14
|
import type { Target } from '../types/target.types.js';
|
|
15
|
+
import { readJsonFileSafe } from '../utils/files/file-operations.js';
|
|
13
16
|
import { targetManager } from './target-manager.js';
|
|
14
17
|
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
|
|
15
20
|
export interface ProjectPaths {
|
|
16
21
|
sessionFile: string;
|
|
17
22
|
backupsDir: string;
|
|
@@ -63,26 +68,20 @@ export class ProjectManager {
|
|
|
63
68
|
* Initialize Flow directories
|
|
64
69
|
*/
|
|
65
70
|
async initialize(): Promise<void> {
|
|
66
|
-
|
|
67
|
-
this.flowHomeDir,
|
|
68
|
-
path.join(this.flowHomeDir, '
|
|
69
|
-
path.join(this.flowHomeDir, '
|
|
70
|
-
path.join(this.flowHomeDir, '
|
|
71
|
-
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
for (const dir of dirs) {
|
|
75
|
-
await fs.mkdir(dir, { recursive: true });
|
|
76
|
-
}
|
|
71
|
+
await Promise.all([
|
|
72
|
+
fs.mkdir(path.join(this.flowHomeDir, 'sessions'), { recursive: true }),
|
|
73
|
+
fs.mkdir(path.join(this.flowHomeDir, 'backups'), { recursive: true }),
|
|
74
|
+
fs.mkdir(path.join(this.flowHomeDir, 'secrets'), { recursive: true }),
|
|
75
|
+
fs.mkdir(path.join(this.flowHomeDir, 'templates'), { recursive: true }),
|
|
76
|
+
]);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* Check if a command is available on the system
|
|
80
|
+
* Check if a command is available on the system (non-blocking)
|
|
81
81
|
*/
|
|
82
82
|
private async isCommandAvailable(command: string): Promise<boolean> {
|
|
83
83
|
try {
|
|
84
|
-
|
|
85
|
-
execSync(`which ${command}`, { stdio: 'ignore' });
|
|
84
|
+
await execAsync(`which ${command}`, { timeout: 5000 });
|
|
86
85
|
return true;
|
|
87
86
|
} catch {
|
|
88
87
|
return false;
|
|
@@ -111,16 +110,11 @@ export class ProjectManager {
|
|
|
111
110
|
projectHash: string
|
|
112
111
|
): Promise<'claude-code' | 'opencode' | undefined> {
|
|
113
112
|
const prefsPath = path.join(this.flowHomeDir, 'project-preferences.json');
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const prefs = JSON.parse(await fs.readFile(prefsPath, 'utf-8'));
|
|
120
|
-
return prefs.projects?.[projectHash]?.target;
|
|
121
|
-
} catch {
|
|
122
|
-
return undefined;
|
|
123
|
-
}
|
|
113
|
+
const prefs = await readJsonFileSafe<{ projects?: Record<string, { target?: string }> }>(
|
|
114
|
+
prefsPath,
|
|
115
|
+
{}
|
|
116
|
+
);
|
|
117
|
+
return prefs.projects?.[projectHash]?.target as 'claude-code' | 'opencode' | undefined;
|
|
124
118
|
}
|
|
125
119
|
|
|
126
120
|
/**
|
|
@@ -128,15 +122,10 @@ export class ProjectManager {
|
|
|
128
122
|
*/
|
|
129
123
|
async saveProjectTargetPreference(projectHash: string, target: string): Promise<void> {
|
|
130
124
|
const prefsPath = path.join(this.flowHomeDir, 'project-preferences.json');
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
prefs = JSON.parse(await fs.readFile(prefsPath, 'utf-8'));
|
|
136
|
-
} catch {
|
|
137
|
-
// Use default
|
|
138
|
-
}
|
|
139
|
-
}
|
|
125
|
+
const prefs = await readJsonFileSafe<{ projects: Record<string, { target?: string }> }>(
|
|
126
|
+
prefsPath,
|
|
127
|
+
{ projects: {} }
|
|
128
|
+
);
|
|
140
129
|
|
|
141
130
|
if (!prefs.projects) {
|
|
142
131
|
prefs.projects = {};
|
|
@@ -189,16 +178,10 @@ export class ProjectManager {
|
|
|
189
178
|
// If both are installed, use global default
|
|
190
179
|
if (installed.claudeCode && installed.opencode) {
|
|
191
180
|
const globalSettingsPath = path.join(this.flowHomeDir, 'settings.json');
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
await this.saveProjectTargetPreference(projectHash, settings.defaultTarget);
|
|
197
|
-
return settings.defaultTarget;
|
|
198
|
-
}
|
|
199
|
-
} catch {
|
|
200
|
-
// Fall through
|
|
201
|
-
}
|
|
181
|
+
const settings = await readJsonFileSafe<{ defaultTarget?: string }>(globalSettingsPath, {});
|
|
182
|
+
if (settings.defaultTarget) {
|
|
183
|
+
await this.saveProjectTargetPreference(projectHash, settings.defaultTarget);
|
|
184
|
+
return settings.defaultTarget;
|
|
202
185
|
}
|
|
203
186
|
|
|
204
187
|
// Both installed, no global default, use claude-code
|
|
@@ -8,6 +8,7 @@ import { existsSync } from 'node:fs';
|
|
|
8
8
|
import fs from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import type { Target } from '../types/target.types.js';
|
|
11
|
+
import { readJsonFileSafe } from '../utils/files/file-operations.js';
|
|
11
12
|
import type { ProjectManager } from './project-manager.js';
|
|
12
13
|
import { resolveTargetOrId } from './target-resolver.js';
|
|
13
14
|
|
|
@@ -60,20 +61,26 @@ export class SecretsManager {
|
|
|
60
61
|
const mcpServers = config[mcpPath] as Record<string, unknown> | undefined;
|
|
61
62
|
if (mcpServers) {
|
|
62
63
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
63
|
-
|
|
64
|
+
if (typeof serverConfig !== 'object' || serverConfig === null) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const server = serverConfig as Record<string, unknown>;
|
|
64
68
|
|
|
65
69
|
// Extract env vars (sensitive) - handle both 'env' and 'environment' keys
|
|
66
|
-
const envVars = server.env || server.environment;
|
|
67
|
-
if (envVars && Object.keys(envVars).length > 0) {
|
|
70
|
+
const envVars = (server.env || server.environment) as Record<string, string> | undefined;
|
|
71
|
+
if (envVars && typeof envVars === 'object' && Object.keys(envVars).length > 0) {
|
|
68
72
|
secrets.servers[serverName] = {
|
|
69
73
|
env: envVars,
|
|
70
74
|
};
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
// Extract args (may contain secrets) - handle both 'args' and 'command' array
|
|
74
|
-
const args =
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
const args = Array.isArray(server.args)
|
|
79
|
+
? server.args
|
|
80
|
+
: Array.isArray(server.command)
|
|
81
|
+
? (server.command as string[]).slice(1)
|
|
82
|
+
: undefined;
|
|
83
|
+
if (args && args.length > 0) {
|
|
77
84
|
if (!secrets.servers[serverName]) {
|
|
78
85
|
secrets.servers[serverName] = {};
|
|
79
86
|
}
|
|
@@ -105,20 +112,10 @@ export class SecretsManager {
|
|
|
105
112
|
/**
|
|
106
113
|
* Load secrets from storage
|
|
107
114
|
*/
|
|
108
|
-
|
|
115
|
+
loadSecrets(projectHash: string): Promise<MCPSecrets | null> {
|
|
109
116
|
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
110
117
|
const secretsPath = path.join(paths.secretsDir, 'mcp-env.json');
|
|
111
|
-
|
|
112
|
-
if (!existsSync(secretsPath)) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
const data = await fs.readFile(secretsPath, 'utf-8');
|
|
118
|
-
return JSON.parse(data);
|
|
119
|
-
} catch {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
118
|
+
return readJsonFileSafe<MCPSecrets | null>(secretsPath, null);
|
|
122
119
|
}
|
|
123
120
|
|
|
124
121
|
/**
|
|
@@ -8,6 +8,7 @@ import { existsSync } from 'node:fs';
|
|
|
8
8
|
import fs from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import type { Target } from '../types/target.types.js';
|
|
11
|
+
import { readJsonFileSafe } from '../utils/files/file-operations.js';
|
|
11
12
|
import type { ProjectManager } from './project-manager.js';
|
|
12
13
|
|
|
13
14
|
export interface Session {
|
|
@@ -143,14 +144,9 @@ export class SessionManager {
|
|
|
143
144
|
/**
|
|
144
145
|
* Get active session for a project
|
|
145
146
|
*/
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const data = await fs.readFile(paths.sessionFile, 'utf-8');
|
|
150
|
-
return JSON.parse(data);
|
|
151
|
-
} catch {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
147
|
+
getActiveSession(projectHash: string): Promise<Session | null> {
|
|
148
|
+
const paths = this.projectManager.getProjectPaths(projectHash);
|
|
149
|
+
return readJsonFileSafe<Session | null>(paths.sessionFile, null);
|
|
154
150
|
}
|
|
155
151
|
|
|
156
152
|
/**
|
|
@@ -208,6 +204,34 @@ export class SessionManager {
|
|
|
208
204
|
}
|
|
209
205
|
}
|
|
210
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Prune old session history files to prevent unbounded accumulation
|
|
209
|
+
* Keeps the most recent N history entries (sorted by session timestamp in filename)
|
|
210
|
+
*/
|
|
211
|
+
async cleanupSessionHistory(keepLast: number = 50): Promise<void> {
|
|
212
|
+
const flowHome = this.projectManager.getFlowHomeDir();
|
|
213
|
+
const historyDir = path.join(flowHome, 'sessions', 'history');
|
|
214
|
+
|
|
215
|
+
if (!existsSync(historyDir)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const files = await fs.readdir(historyDir);
|
|
220
|
+
const sessionFiles = files
|
|
221
|
+
.filter((f) => f.endsWith('.json'))
|
|
222
|
+
.sort() // session-{timestamp}.json sorts chronologically
|
|
223
|
+
.reverse(); // newest first
|
|
224
|
+
|
|
225
|
+
// Delete files beyond keepLast
|
|
226
|
+
for (const file of sessionFiles.slice(keepLast)) {
|
|
227
|
+
try {
|
|
228
|
+
await fs.unlink(path.join(historyDir, file));
|
|
229
|
+
} catch {
|
|
230
|
+
// Ignore errors — file might already be deleted
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
211
235
|
/**
|
|
212
236
|
* Recover from crashed session
|
|
213
237
|
*/
|
|
@@ -230,37 +254,4 @@ export class SessionManager {
|
|
|
230
254
|
// File might not exist
|
|
231
255
|
}
|
|
232
256
|
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Clean up old session history
|
|
236
|
-
*/
|
|
237
|
-
async cleanupOldSessions(keepLast: number = 10): Promise<void> {
|
|
238
|
-
const flowHome = this.projectManager.getFlowHomeDir();
|
|
239
|
-
const historyDir = path.join(flowHome, 'sessions', 'history');
|
|
240
|
-
|
|
241
|
-
if (!existsSync(historyDir)) {
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const files = await fs.readdir(historyDir);
|
|
246
|
-
const sessions = await Promise.all(
|
|
247
|
-
files.map(async (file) => {
|
|
248
|
-
const filePath = path.join(historyDir, file);
|
|
249
|
-
const data = await fs.readFile(filePath, 'utf-8');
|
|
250
|
-
const session = JSON.parse(data) as Session;
|
|
251
|
-
return { file, session };
|
|
252
|
-
})
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
// Sort by start time (newest first)
|
|
256
|
-
sessions.sort(
|
|
257
|
-
(a, b) => new Date(b.session.startTime).getTime() - new Date(a.session.startTime).getTime()
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
// Remove old sessions
|
|
261
|
-
const toRemove = sessions.slice(keepLast);
|
|
262
|
-
for (const { file } of toRemove) {
|
|
263
|
-
await fs.unlink(path.join(historyDir, file));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
257
|
}
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { ConfigService } from '../services/config-service.js';
|
|
5
5
|
import type { Target } from '../types/target.types.js';
|
|
6
|
-
import {
|
|
6
|
+
import { tryResolveTarget } from './target-resolver.js';
|
|
7
7
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = path.dirname(__filename);
|
|
@@ -43,17 +43,6 @@ export class StateDetector {
|
|
|
43
43
|
this.projectPath = projectPath;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* Resolve target from ID string to Target object
|
|
48
|
-
*/
|
|
49
|
-
private resolveTarget(targetId: string): Target | null {
|
|
50
|
-
const targetOption = targetManager.getTarget(targetId);
|
|
51
|
-
if (targetOption._tag === 'None') {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
return targetOption.value;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
46
|
async detect(): Promise<ProjectState> {
|
|
58
47
|
const state: ProjectState = {
|
|
59
48
|
initialized: false,
|
|
@@ -95,7 +84,7 @@ export class StateDetector {
|
|
|
95
84
|
}
|
|
96
85
|
|
|
97
86
|
// Resolve target to get config
|
|
98
|
-
const target = state.target ?
|
|
87
|
+
const target = state.target ? tryResolveTarget(state.target) : null;
|
|
99
88
|
|
|
100
89
|
// Check components based on target config
|
|
101
90
|
if (target) {
|
|
@@ -376,7 +365,7 @@ export class StateDetector {
|
|
|
376
365
|
targetId: string
|
|
377
366
|
): Promise<{ version: string | null; latestVersion: string | null }> {
|
|
378
367
|
try {
|
|
379
|
-
const target =
|
|
368
|
+
const target = tryResolveTarget(targetId);
|
|
380
369
|
if (!target) {
|
|
381
370
|
return { version: null, latestVersion: null };
|
|
382
371
|
}
|
|
@@ -428,7 +417,7 @@ export class StateDetector {
|
|
|
428
417
|
|
|
429
418
|
// Check required components based on target
|
|
430
419
|
if (state.initialized && state.target) {
|
|
431
|
-
const target =
|
|
420
|
+
const target = tryResolveTarget(state.target);
|
|
432
421
|
// CLI-based targets (category: 'cli') require agents to be installed
|
|
433
422
|
if (target && target.category === 'cli' && !state.components.agents.installed) {
|
|
434
423
|
return true; // CLI target initialized but no agents
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
getTargetsWithMCPSupport,
|
|
9
9
|
isTargetImplemented,
|
|
10
10
|
} from '../config/targets.js';
|
|
11
|
-
import {
|
|
11
|
+
import { isSome, match } from '../core/functional/option.js';
|
|
12
12
|
import { projectSettings } from '../utils/config/settings.js';
|
|
13
13
|
import { promptSelect } from '../utils/prompts/index.js';
|
|
14
14
|
|
|
@@ -85,9 +85,12 @@ export function createTargetManager(): TargetManager {
|
|
|
85
85
|
message: 'Select target platform:',
|
|
86
86
|
options: availableTargets.map((id) => {
|
|
87
87
|
const targetOption = getTarget(id);
|
|
88
|
-
const
|
|
88
|
+
const label = match(
|
|
89
|
+
(t: ReturnType<typeof getAllTargets>[number]) => t.name || id,
|
|
90
|
+
() => id
|
|
91
|
+
)(targetOption);
|
|
89
92
|
return {
|
|
90
|
-
label
|
|
93
|
+
label,
|
|
91
94
|
value: id,
|
|
92
95
|
};
|
|
93
96
|
}),
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Target Resolver
|
|
3
|
-
* Shared utility for resolving target IDs to Target objects
|
|
4
|
-
* Eliminates duplication across BackupManager and SecretsManager
|
|
2
|
+
* Target Resolver — Single Source of Truth for target ID → Target resolution
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
import type { Target } from '../types/target.types.js';
|
|
8
6
|
import { targetManager } from './target-manager.js';
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
|
-
* Resolve
|
|
9
|
+
* Resolve target ID to Target object, returns null if not found
|
|
10
|
+
*/
|
|
11
|
+
export function tryResolveTarget(targetId: string): Target | null {
|
|
12
|
+
const targetOption = targetManager.getTarget(targetId);
|
|
13
|
+
return targetOption._tag === 'Some' ? targetOption.value : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve target ID to Target object
|
|
12
18
|
* @throws Error if target ID is not found
|
|
13
19
|
*/
|
|
14
20
|
export function resolveTarget(targetId: string): Target {
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
21
|
+
const target = tryResolveTarget(targetId);
|
|
22
|
+
if (!target) {
|
|
17
23
|
throw new Error(`Unknown target: ${targetId}`);
|
|
18
24
|
}
|
|
19
|
-
return
|
|
25
|
+
return target;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
/**
|
|
23
|
-
* Resolve target
|
|
24
|
-
* Returns the Target object in both cases
|
|
29
|
+
* Resolve target from either string ID or Target object
|
|
25
30
|
*/
|
|
26
31
|
export function resolveTargetOrId(targetOrId: Target | string): Target {
|
|
27
32
|
return typeof targetOrId === 'string' ? resolveTarget(targetOrId) : targetOrId;
|
|
@@ -30,15 +30,13 @@ export class TemplateLoader {
|
|
|
30
30
|
const commandsDir = path.join(this.assetsDir, 'slash-commands');
|
|
31
31
|
const skillsDir = path.join(this.assetsDir, 'skills');
|
|
32
32
|
const mcpConfigPath = path.join(this.assetsDir, 'mcp-servers.json');
|
|
33
|
-
const outputStylesDir = path.join(this.assetsDir, 'output-styles');
|
|
34
33
|
|
|
35
34
|
// Load all directories in parallel
|
|
36
|
-
const [agents, commands, skills, mcpServers,
|
|
35
|
+
const [agents, commands, skills, mcpServers, rules] = await Promise.all([
|
|
37
36
|
existsSync(agentsDir) ? this.loadAgents(agentsDir) : [],
|
|
38
37
|
existsSync(commandsDir) ? this.loadCommands(commandsDir) : [],
|
|
39
38
|
existsSync(skillsDir) ? this.loadSkills(skillsDir) : [],
|
|
40
39
|
existsSync(mcpConfigPath) ? this.loadMCPServers(mcpConfigPath) : [],
|
|
41
|
-
existsSync(outputStylesDir) ? this.loadSingleFiles(outputStylesDir) : [],
|
|
42
40
|
this.loadRules(),
|
|
43
41
|
]);
|
|
44
42
|
|
|
@@ -48,8 +46,7 @@ export class TemplateLoader {
|
|
|
48
46
|
skills,
|
|
49
47
|
rules,
|
|
50
48
|
mcpServers,
|
|
51
|
-
|
|
52
|
-
singleFiles,
|
|
49
|
+
singleFiles: [],
|
|
53
50
|
};
|
|
54
51
|
}
|
|
55
52
|
|
|
@@ -134,11 +131,13 @@ export class TemplateLoader {
|
|
|
134
131
|
/**
|
|
135
132
|
* Load MCP servers configuration
|
|
136
133
|
*/
|
|
137
|
-
private async loadMCPServers(
|
|
134
|
+
private async loadMCPServers(
|
|
135
|
+
configPath: string
|
|
136
|
+
): Promise<Array<{ name: string; config: Record<string, unknown> }>> {
|
|
138
137
|
const data = await fs.readFile(configPath, 'utf-8');
|
|
139
|
-
const config = JSON.parse(data)
|
|
138
|
+
const config = JSON.parse(data) as Record<string, Record<string, unknown>>;
|
|
140
139
|
|
|
141
|
-
const servers = [];
|
|
140
|
+
const servers: Array<{ name: string; config: Record<string, unknown> }> = [];
|
|
142
141
|
for (const [name, serverConfig] of Object.entries(config)) {
|
|
143
142
|
servers.push({ name, config: serverConfig });
|
|
144
143
|
}
|
|
@@ -146,31 +145,6 @@ export class TemplateLoader {
|
|
|
146
145
|
return servers;
|
|
147
146
|
}
|
|
148
147
|
|
|
149
|
-
/**
|
|
150
|
-
* Load single files (parallel loading)
|
|
151
|
-
*/
|
|
152
|
-
private async loadSingleFiles(
|
|
153
|
-
singleFilesDir: string
|
|
154
|
-
): Promise<Array<{ path: string; content: string }>> {
|
|
155
|
-
const entries = await fs.readdir(singleFilesDir);
|
|
156
|
-
|
|
157
|
-
const results = await Promise.all(
|
|
158
|
-
entries.map(async (entry) => {
|
|
159
|
-
const filePath = path.join(singleFilesDir, entry);
|
|
160
|
-
const stat = await fs.stat(filePath);
|
|
161
|
-
|
|
162
|
-
if (!stat.isFile()) {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
167
|
-
return { path: entry, content };
|
|
168
|
-
})
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
return results.filter((r): r is { path: string; content: string } => r !== null);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
148
|
/**
|
|
175
149
|
* Get assets directory path
|
|
176
150
|
*/
|
|
@@ -5,11 +5,11 @@ import { promisify } from 'node:util';
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { getProjectSettingsFile } from '../config/constants.js';
|
|
7
7
|
import type { Target } from '../types/target.types.js';
|
|
8
|
-
import { CLIError } from '../utils/
|
|
8
|
+
import { CLIError } from '../utils/errors.js';
|
|
9
9
|
import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
|
|
10
10
|
import { createSpinner, log } from '../utils/prompts/index.js';
|
|
11
11
|
import type { ProjectState } from './state-detector.js';
|
|
12
|
-
import {
|
|
12
|
+
import { tryResolveTarget } from './target-resolver.js';
|
|
13
13
|
|
|
14
14
|
const execAsync = promisify(exec);
|
|
15
15
|
|
|
@@ -152,23 +152,12 @@ export class UpgradeManager {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
/**
|
|
156
|
-
* Resolve target from ID string to Target object
|
|
157
|
-
*/
|
|
158
|
-
private resolveTarget(targetId: string): Target | null {
|
|
159
|
-
const targetOption = targetManager.getTarget(targetId);
|
|
160
|
-
if (targetOption._tag === 'None') {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
return targetOption.value;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
155
|
async upgradeTarget(state: ProjectState, autoInstall: boolean = false): Promise<boolean> {
|
|
167
156
|
if (!state.target || !state.targetLatestVersion) {
|
|
168
157
|
return false;
|
|
169
158
|
}
|
|
170
159
|
|
|
171
|
-
const target =
|
|
160
|
+
const target = tryResolveTarget(state.target);
|
|
172
161
|
if (!target) {
|
|
173
162
|
return false;
|
|
174
163
|
}
|
|
@@ -263,7 +252,7 @@ export class UpgradeManager {
|
|
|
263
252
|
const configPath = path.join(this.projectPath, getProjectSettingsFile());
|
|
264
253
|
const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
265
254
|
if (config.target) {
|
|
266
|
-
return
|
|
255
|
+
return tryResolveTarget(config.target);
|
|
267
256
|
}
|
|
268
257
|
} catch {
|
|
269
258
|
// Cannot read config
|
|
@@ -355,7 +344,7 @@ export class UpgradeManager {
|
|
|
355
344
|
}
|
|
356
345
|
|
|
357
346
|
private async getCurrentTargetVersion(targetId: string): Promise<string | null> {
|
|
358
|
-
const target =
|
|
347
|
+
const target = tryResolveTarget(targetId);
|
|
359
348
|
if (!target) {
|
|
360
349
|
return null;
|
|
361
350
|
}
|
package/src/index.ts
CHANGED
|
@@ -98,48 +98,19 @@ export async function runCLI(): Promise<void> {
|
|
|
98
98
|
* Set up global error handlers for uncaught exceptions and unhandled rejections
|
|
99
99
|
*/
|
|
100
100
|
function setupGlobalErrorHandling(): void {
|
|
101
|
-
// Handle
|
|
102
|
-
process.on('
|
|
103
|
-
|
|
104
|
-
console.error(` ${error.message}`);
|
|
105
|
-
if (process.env.NODE_ENV === 'development') {
|
|
106
|
-
console.error(' Stack trace:', error.stack);
|
|
107
|
-
}
|
|
108
|
-
process.exit(1);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Handle unhandled promise rejections
|
|
112
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
113
|
-
// Ignore AbortError - this is expected when user cancels operations
|
|
101
|
+
// Handle unhandled promise rejections (non-fatal, log only)
|
|
102
|
+
process.on('unhandledRejection', (reason) => {
|
|
103
|
+
// Ignore AbortError — expected when user cancels operations
|
|
114
104
|
if (reason instanceof Error && reason.name === 'AbortError') {
|
|
115
105
|
return;
|
|
116
106
|
}
|
|
117
|
-
|
|
118
|
-
// Only log unhandled rejections in development mode
|
|
119
|
-
// Don't exit the process - let the application handle errors gracefully
|
|
120
107
|
if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
|
|
121
|
-
console.error('✗ Unhandled Promise Rejection:');
|
|
122
|
-
console.error(` Reason: ${reason}`);
|
|
123
|
-
console.error(' Promise:', promise);
|
|
108
|
+
console.error('✗ Unhandled Promise Rejection:', reason);
|
|
124
109
|
}
|
|
125
110
|
});
|
|
126
111
|
|
|
127
|
-
//
|
|
128
|
-
process.
|
|
129
|
-
console.log('\nSylphx Flow CLI terminated by user');
|
|
130
|
-
process.exit(0);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
process.on('SIGTERM', () => {
|
|
134
|
-
console.log('\nSylphx Flow CLI terminated');
|
|
135
|
-
process.exit(0);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Ensure clean exit by allowing the event loop to drain
|
|
139
|
-
process.on('beforeExit', () => {
|
|
140
|
-
// Node.js will exit automatically after this handler completes
|
|
141
|
-
// No explicit process.exit() needed
|
|
142
|
-
});
|
|
112
|
+
// SIGINT/SIGTERM are handled by CleanupHandler which does async backup restoration.
|
|
113
|
+
// DO NOT register handlers here — process.exit() would preempt cleanup.
|
|
143
114
|
}
|
|
144
115
|
|
|
145
116
|
/**
|
|
@@ -155,7 +126,7 @@ function handleCommandError(error: unknown): void {
|
|
|
155
126
|
|
|
156
127
|
// Handle Commander.js specific errors
|
|
157
128
|
if (error.name === 'CommanderError') {
|
|
158
|
-
const commanderError = error as
|
|
129
|
+
const commanderError = error as Error & { code?: string; exitCode?: number };
|
|
159
130
|
|
|
160
131
|
// Don't exit for help or version commands - they should already be handled
|
|
161
132
|
if (commanderError.code === 'commander.help' || commanderError.code === 'commander.version') {
|
|
@@ -10,6 +10,7 @@ import os from 'node:os';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { promisify } from 'node:util';
|
|
13
|
+
import { readJsonFileSafe } from '../utils/files/file-operations.js';
|
|
13
14
|
import { getUpgradeCommand } from '../utils/package-manager-detector.js';
|
|
14
15
|
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -91,13 +92,8 @@ export class AutoUpgrade {
|
|
|
91
92
|
/**
|
|
92
93
|
* Read version info from disk
|
|
93
94
|
*/
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
const data = await fs.readFile(VERSION_FILE, 'utf-8');
|
|
97
|
-
return JSON.parse(data);
|
|
98
|
-
} catch {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
95
|
+
private readVersionInfo(): Promise<VersionInfo | null> {
|
|
96
|
+
return readJsonFileSafe<VersionInfo | null>(VERSION_FILE, null);
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
/**
|
|
@@ -127,13 +123,9 @@ export class AutoUpgrade {
|
|
|
127
123
|
* Get current Flow version from package.json
|
|
128
124
|
*/
|
|
129
125
|
private async getCurrentVersion(): Promise<string> {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return packageJson.version;
|
|
134
|
-
} catch {
|
|
135
|
-
return 'unknown';
|
|
136
|
-
}
|
|
126
|
+
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
|
|
127
|
+
const pkg = await readJsonFileSafe<{ version?: string }>(packageJsonPath, {});
|
|
128
|
+
return pkg.version ?? 'unknown';
|
|
137
129
|
}
|
|
138
130
|
|
|
139
131
|
/**
|