@sylphx/flow 3.12.1 → 3.13.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 +18 -0
- package/package.json +1 -1
- package/src/commands/flow/execute-v2.ts +14 -20
- package/src/core/template-loader.ts +72 -94
- package/src/index.ts +3 -9
- package/src/services/auto-upgrade.ts +99 -309
- package/src/services/global-config.ts +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @sylphx/flow
|
|
2
2
|
|
|
3
|
+
## 3.13.1 (2026-02-04)
|
|
4
|
+
|
|
5
|
+
### ⚡️ Performance
|
|
6
|
+
|
|
7
|
+
- major performance optimizations ([aaaf25c](https://github.com/SylphxAI/flow/commit/aaaf25cd20555a2fa2a52713ae78c15368565c34))
|
|
8
|
+
|
|
9
|
+
### ♻️ Refactoring
|
|
10
|
+
|
|
11
|
+
- **auto-upgrade:** fully non-blocking background updates ([eb0a2da](https://github.com/SylphxAI/flow/commit/eb0a2da355ad6418e3c96b19dfe3ee1d6222724c))
|
|
12
|
+
- **auto-upgrade:** remove target CLI upgrade logic - only manage Flow updates ([65ad241](https://github.com/SylphxAI/flow/commit/65ad241e40aaf772b9e9e3a55c8f11693adfefe5))
|
|
13
|
+
- **auto-upgrade:** remove blocking startup check, keep only periodic background check ([4f29c01](https://github.com/SylphxAI/flow/commit/4f29c01ce31bb30457240b9ff90114d3dd03c66e))
|
|
14
|
+
|
|
15
|
+
## 3.13.0 (2026-02-04)
|
|
16
|
+
|
|
17
|
+
### ✨ Features
|
|
18
|
+
|
|
19
|
+
- **auto-upgrade:** add periodic background update check every 30 minutes ([4a14414](https://github.com/SylphxAI/flow/commit/4a14414350f31899a5dadaf596216c53406f7ffc))
|
|
20
|
+
|
|
3
21
|
## 3.12.1 (2026-02-04)
|
|
4
22
|
|
|
5
23
|
### 🐛 Bug Fixes
|
package/package.json
CHANGED
|
@@ -161,14 +161,15 @@ export async function executeFlowV2(
|
|
|
161
161
|
): Promise<void> {
|
|
162
162
|
const projectPath = process.cwd();
|
|
163
163
|
|
|
164
|
-
// Initialize
|
|
164
|
+
// Initialize services and load config in parallel
|
|
165
165
|
const configService = new GlobalConfigService();
|
|
166
|
-
await configService.initialize();
|
|
167
|
-
|
|
168
|
-
// Step 1: Determine target (silent auto-detect, only prompt when necessary)
|
|
169
166
|
const targetInstaller = new TargetInstaller(projectPath);
|
|
170
|
-
|
|
171
|
-
const settings = await
|
|
167
|
+
|
|
168
|
+
const [, installedTargets, settings] = await Promise.all([
|
|
169
|
+
configService.initialize(),
|
|
170
|
+
targetInstaller.detectInstalledTargets(),
|
|
171
|
+
configService.loadSettings(),
|
|
172
|
+
]);
|
|
172
173
|
|
|
173
174
|
let selectedTargetId: string | null = null;
|
|
174
175
|
|
|
@@ -238,17 +239,8 @@ export async function executeFlowV2(
|
|
|
238
239
|
// Show minimal header
|
|
239
240
|
showHeader(version, targetName);
|
|
240
241
|
|
|
241
|
-
// Step 2:
|
|
242
|
-
|
|
243
|
-
const upgradeResult = await autoUpgrade.runAutoUpgrade(selectedTargetId);
|
|
244
|
-
|
|
245
|
-
// Show upgrade notice (minimal - only if upgraded)
|
|
246
|
-
if (upgradeResult.flowUpgraded && upgradeResult.flowVersion) {
|
|
247
|
-
console.log(chalk.dim(`↑ flow ${upgradeResult.flowVersion.latest}`));
|
|
248
|
-
}
|
|
249
|
-
if (upgradeResult.targetUpgraded && upgradeResult.targetVersion) {
|
|
250
|
-
console.log(chalk.dim(`↑ ${targetName.toLowerCase()} ${upgradeResult.targetVersion.latest}`));
|
|
251
|
-
}
|
|
242
|
+
// Step 2: Start background auto-upgrade (non-blocking)
|
|
243
|
+
new AutoUpgrade().start();
|
|
252
244
|
|
|
253
245
|
// Create executor
|
|
254
246
|
const executor = new FlowExecutor();
|
|
@@ -284,9 +276,11 @@ export async function executeFlowV2(
|
|
|
284
276
|
agent = enabledAgents.length > 0 ? enabledAgents[0] : 'builder';
|
|
285
277
|
}
|
|
286
278
|
|
|
287
|
-
// Load agent content
|
|
288
|
-
const enabledRules = await
|
|
289
|
-
|
|
279
|
+
// Load agent content (parallel fetch rules and styles)
|
|
280
|
+
const [enabledRules, enabledOutputStyles] = await Promise.all([
|
|
281
|
+
configService.getEnabledRules(),
|
|
282
|
+
configService.getEnabledOutputStyles(),
|
|
283
|
+
]);
|
|
290
284
|
|
|
291
285
|
const agentContent = await loadAgentContent(
|
|
292
286
|
agent,
|
|
@@ -22,39 +22,41 @@ export class TemplateLoader {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Load all templates for target
|
|
25
|
+
* Load all templates for target (parallel loading for performance)
|
|
26
26
|
* Uses flat assets directory structure (no target-specific subdirectories)
|
|
27
27
|
*/
|
|
28
28
|
async loadTemplates(_target: Target | string): Promise<FlowTemplates> {
|
|
29
|
-
const templates: FlowTemplates = {
|
|
30
|
-
agents: [],
|
|
31
|
-
commands: [],
|
|
32
|
-
skills: [],
|
|
33
|
-
rules: undefined,
|
|
34
|
-
mcpServers: [],
|
|
35
|
-
hooks: [],
|
|
36
|
-
singleFiles: [],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Load agents
|
|
40
29
|
const agentsDir = path.join(this.assetsDir, 'agents');
|
|
41
|
-
if (existsSync(agentsDir)) {
|
|
42
|
-
templates.agents = await this.loadAgents(agentsDir);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Load commands (slash-commands directory)
|
|
46
30
|
const commandsDir = path.join(this.assetsDir, 'slash-commands');
|
|
47
|
-
if (existsSync(commandsDir)) {
|
|
48
|
-
templates.commands = await this.loadCommands(commandsDir);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Load skills (skills/<domain>/SKILL.md structure)
|
|
52
31
|
const skillsDir = path.join(this.assetsDir, 'skills');
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
32
|
+
const mcpConfigPath = path.join(this.assetsDir, 'mcp-servers.json');
|
|
33
|
+
const outputStylesDir = path.join(this.assetsDir, 'output-styles');
|
|
56
34
|
|
|
57
|
-
// Load
|
|
35
|
+
// Load all directories in parallel
|
|
36
|
+
const [agents, commands, skills, mcpServers, singleFiles, rules] = await Promise.all([
|
|
37
|
+
existsSync(agentsDir) ? this.loadAgents(agentsDir) : [],
|
|
38
|
+
existsSync(commandsDir) ? this.loadCommands(commandsDir) : [],
|
|
39
|
+
existsSync(skillsDir) ? this.loadSkills(skillsDir) : [],
|
|
40
|
+
existsSync(mcpConfigPath) ? this.loadMCPServers(mcpConfigPath) : [],
|
|
41
|
+
existsSync(outputStylesDir) ? this.loadSingleFiles(outputStylesDir) : [],
|
|
42
|
+
this.loadRules(),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
agents,
|
|
47
|
+
commands,
|
|
48
|
+
skills,
|
|
49
|
+
rules,
|
|
50
|
+
mcpServers,
|
|
51
|
+
hooks: [],
|
|
52
|
+
singleFiles,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load rules from possible locations
|
|
58
|
+
*/
|
|
59
|
+
private async loadRules(): Promise<string | undefined> {
|
|
58
60
|
const rulesLocations = [
|
|
59
61
|
path.join(this.assetsDir, 'rules', 'AGENTS.md'),
|
|
60
62
|
path.join(this.assetsDir, 'AGENTS.md'),
|
|
@@ -62,92 +64,67 @@ export class TemplateLoader {
|
|
|
62
64
|
|
|
63
65
|
for (const rulesPath of rulesLocations) {
|
|
64
66
|
if (existsSync(rulesPath)) {
|
|
65
|
-
|
|
66
|
-
break;
|
|
67
|
+
return fs.readFile(rulesPath, 'utf-8');
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
// Load MCP servers (if any)
|
|
71
|
-
const mcpConfigPath = path.join(this.assetsDir, 'mcp-servers.json');
|
|
72
|
-
if (existsSync(mcpConfigPath)) {
|
|
73
|
-
templates.mcpServers = await this.loadMCPServers(mcpConfigPath);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Load output styles (single files)
|
|
77
|
-
const outputStylesDir = path.join(this.assetsDir, 'output-styles');
|
|
78
|
-
if (existsSync(outputStylesDir)) {
|
|
79
|
-
templates.singleFiles = await this.loadSingleFiles(outputStylesDir);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return templates;
|
|
70
|
+
return undefined;
|
|
83
71
|
}
|
|
84
72
|
|
|
85
73
|
/**
|
|
86
|
-
* Load agents from directory
|
|
74
|
+
* Load agents from directory (parallel file reads)
|
|
87
75
|
*/
|
|
88
76
|
private async loadAgents(agentsDir: string): Promise<Array<{ name: string; content: string }>> {
|
|
89
|
-
const agents = [];
|
|
90
77
|
const files = await fs.readdir(agentsDir);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return agents;
|
|
78
|
+
const mdFiles = files.filter((f) => f.endsWith('.md'));
|
|
79
|
+
|
|
80
|
+
return Promise.all(
|
|
81
|
+
mdFiles.map(async (file) => ({
|
|
82
|
+
name: file,
|
|
83
|
+
content: await fs.readFile(path.join(agentsDir, file), 'utf-8'),
|
|
84
|
+
}))
|
|
85
|
+
);
|
|
102
86
|
}
|
|
103
87
|
|
|
104
88
|
/**
|
|
105
|
-
* Load commands/modes from directory
|
|
89
|
+
* Load commands/modes from directory (parallel file reads)
|
|
106
90
|
*/
|
|
107
91
|
private async loadCommands(
|
|
108
92
|
commandsDir: string
|
|
109
93
|
): Promise<Array<{ name: string; content: string }>> {
|
|
110
|
-
const commands = [];
|
|
111
94
|
const files = await fs.readdir(commandsDir);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return commands;
|
|
95
|
+
const mdFiles = files.filter((f) => f.endsWith('.md'));
|
|
96
|
+
|
|
97
|
+
return Promise.all(
|
|
98
|
+
mdFiles.map(async (file) => ({
|
|
99
|
+
name: file,
|
|
100
|
+
content: await fs.readFile(path.join(commandsDir, file), 'utf-8'),
|
|
101
|
+
}))
|
|
102
|
+
);
|
|
123
103
|
}
|
|
124
104
|
|
|
125
105
|
/**
|
|
126
|
-
* Load skills from directory
|
|
106
|
+
* Load skills from directory (parallel loading)
|
|
127
107
|
* Skills are stored as <domain>/SKILL.md subdirectories
|
|
128
108
|
*/
|
|
129
109
|
private async loadSkills(skillsDir: string): Promise<Array<{ name: string; content: string }>> {
|
|
130
|
-
const skills = [];
|
|
131
110
|
const domains = await fs.readdir(skillsDir);
|
|
132
111
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
112
|
+
const results = await Promise.all(
|
|
113
|
+
domains.map(async (domain) => {
|
|
114
|
+
const domainPath = path.join(skillsDir, domain);
|
|
115
|
+
const stat = await fs.stat(domainPath);
|
|
136
116
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
117
|
+
if (!stat.isDirectory()) return null;
|
|
118
|
+
|
|
119
|
+
const skillFile = path.join(domainPath, 'SKILL.md');
|
|
120
|
+
if (!existsSync(skillFile)) return null;
|
|
140
121
|
|
|
141
|
-
// Look for SKILL.md in each domain directory
|
|
142
|
-
const skillFile = path.join(domainPath, 'SKILL.md');
|
|
143
|
-
if (existsSync(skillFile)) {
|
|
144
122
|
const content = await fs.readFile(skillFile, 'utf-8');
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
123
|
+
return { name: `${domain}/SKILL.md`, content };
|
|
124
|
+
})
|
|
125
|
+
);
|
|
149
126
|
|
|
150
|
-
return
|
|
127
|
+
return results.filter((r): r is { name: string; content: string } => r !== null);
|
|
151
128
|
}
|
|
152
129
|
|
|
153
130
|
/**
|
|
@@ -166,25 +143,26 @@ export class TemplateLoader {
|
|
|
166
143
|
}
|
|
167
144
|
|
|
168
145
|
/**
|
|
169
|
-
* Load single files (
|
|
146
|
+
* Load single files (parallel loading)
|
|
170
147
|
*/
|
|
171
148
|
private async loadSingleFiles(
|
|
172
149
|
singleFilesDir: string
|
|
173
150
|
): Promise<Array<{ path: string; content: string }>> {
|
|
174
|
-
const files = [];
|
|
175
151
|
const entries = await fs.readdir(singleFilesDir);
|
|
176
152
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
153
|
+
const results = await Promise.all(
|
|
154
|
+
entries.map(async (entry) => {
|
|
155
|
+
const filePath = path.join(singleFilesDir, entry);
|
|
156
|
+
const stat = await fs.stat(filePath);
|
|
157
|
+
|
|
158
|
+
if (!stat.isFile()) return null;
|
|
180
159
|
|
|
181
|
-
if (stat.isFile()) {
|
|
182
160
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
161
|
+
return { path: entry, content };
|
|
162
|
+
})
|
|
163
|
+
);
|
|
186
164
|
|
|
187
|
-
return
|
|
165
|
+
return results.filter((r): r is { path: string; content: string } => r !== null);
|
|
188
166
|
}
|
|
189
167
|
|
|
190
168
|
/**
|
package/src/index.ts
CHANGED
|
@@ -4,9 +4,6 @@
|
|
|
4
4
|
* AI-powered development flow management
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { readFileSync } from 'node:fs';
|
|
8
|
-
import { dirname, join } from 'node:path';
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
10
7
|
import { Command } from 'commander';
|
|
11
8
|
import { executeFlow } from './commands/flow/execute-v2.js';
|
|
12
9
|
import {
|
|
@@ -20,13 +17,10 @@ import {
|
|
|
20
17
|
import { hookCommand } from './commands/hook-command.js';
|
|
21
18
|
import { settingsCommand } from './commands/settings-command.js';
|
|
22
19
|
import { UserCancelledError } from './utils/errors.js';
|
|
20
|
+
// @ts-expect-error - Bun resolves JSON imports
|
|
21
|
+
import pkg from '../package.json';
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
-
const __dirname = dirname(__filename);
|
|
27
|
-
const packageJsonPath = join(__dirname, '..', 'package.json');
|
|
28
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
29
|
-
const VERSION = packageJson.version;
|
|
23
|
+
const VERSION = pkg.version;
|
|
30
24
|
|
|
31
25
|
/**
|
|
32
26
|
* Create the main CLI application with enhanced Commander.js configuration
|
|
@@ -1,399 +1,189 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-Upgrade Service
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Fully non-blocking background updates
|
|
4
|
+
* Only manages Flow updates - target CLIs manage their own updates
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { exec } from 'node:child_process';
|
|
8
|
-
import { existsSync } from 'node:fs';
|
|
9
8
|
import fs from 'node:fs/promises';
|
|
10
9
|
import os from 'node:os';
|
|
11
10
|
import path from 'node:path';
|
|
12
11
|
import { fileURLToPath } from 'node:url';
|
|
13
12
|
import { promisify } from 'node:util';
|
|
14
|
-
import
|
|
15
|
-
import ora from 'ora';
|
|
16
|
-
import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
|
|
17
|
-
import { TargetInstaller } from './target-installer.js';
|
|
13
|
+
import { getUpgradeCommand } from '../utils/package-manager-detector.js';
|
|
18
14
|
|
|
19
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
16
|
const __dirname = path.dirname(__filename);
|
|
21
17
|
|
|
22
|
-
// Version info file (stores last background check result)
|
|
23
18
|
const VERSION_FILE = path.join(os.homedir(), '.sylphx-flow', 'versions.json');
|
|
19
|
+
const DEFAULT_CHECK_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
|
24
20
|
|
|
25
21
|
interface VersionInfo {
|
|
26
22
|
flowLatest?: string;
|
|
27
|
-
|
|
28
|
-
targetCurrent?: Record<string, string>;
|
|
23
|
+
lastCheckTime?: number;
|
|
29
24
|
}
|
|
30
25
|
|
|
31
26
|
const execAsync = promisify(exec);
|
|
32
27
|
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
targetNeedsUpgrade: boolean;
|
|
36
|
-
flowVersion: { current: string; latest: string } | null;
|
|
37
|
-
targetVersion: { current: string; latest: string } | null;
|
|
38
|
-
}
|
|
28
|
+
export class AutoUpgrade {
|
|
29
|
+
private periodicCheckInterval: NodeJS.Timeout | null = null;
|
|
39
30
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Start background update service
|
|
33
|
+
* Runs first check immediately (if not recently checked), then every intervalMs
|
|
34
|
+
* All checks and upgrades are non-blocking
|
|
35
|
+
*/
|
|
36
|
+
start(intervalMs: number = DEFAULT_CHECK_INTERVAL_MS): void {
|
|
37
|
+
this.stop();
|
|
45
38
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
private options: AutoUpgradeOptions;
|
|
49
|
-
private targetInstaller: TargetInstaller;
|
|
39
|
+
// First check immediately (non-blocking) - skips if recently checked
|
|
40
|
+
this.checkAndUpgrade(intervalMs);
|
|
50
41
|
|
|
51
|
-
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
// Then periodic checks
|
|
43
|
+
this.periodicCheckInterval = setInterval(() => {
|
|
44
|
+
this.checkAndUpgrade(intervalMs);
|
|
45
|
+
}, intervalMs);
|
|
46
|
+
|
|
47
|
+
// Don't prevent process from exiting
|
|
48
|
+
this.periodicCheckInterval.unref();
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
/**
|
|
58
|
-
*
|
|
52
|
+
* Stop background update service
|
|
59
53
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
const data = await fs.readFile(VERSION_FILE, 'utf-8');
|
|
66
|
-
return JSON.parse(data);
|
|
67
|
-
} catch {
|
|
68
|
-
return null;
|
|
54
|
+
stop(): void {
|
|
55
|
+
if (this.periodicCheckInterval) {
|
|
56
|
+
clearInterval(this.periodicCheckInterval);
|
|
57
|
+
this.periodicCheckInterval = null;
|
|
69
58
|
}
|
|
70
59
|
}
|
|
71
60
|
|
|
72
61
|
/**
|
|
73
|
-
*
|
|
62
|
+
* Check for updates and upgrade if available (non-blocking)
|
|
63
|
+
* Skips if checked within intervalMs
|
|
64
|
+
* Fire and forget - errors are silently ignored
|
|
74
65
|
*/
|
|
75
|
-
private
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
66
|
+
private checkAndUpgrade(intervalMs: number = DEFAULT_CHECK_INTERVAL_MS): void {
|
|
67
|
+
this.shouldCheck(intervalMs).then((shouldCheck) => {
|
|
68
|
+
if (shouldCheck) {
|
|
69
|
+
this.performCheck().catch(() => {
|
|
70
|
+
// Silent fail
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
83
74
|
}
|
|
84
75
|
|
|
85
76
|
/**
|
|
86
|
-
*
|
|
77
|
+
* Check if enough time has passed since last check
|
|
87
78
|
*/
|
|
88
|
-
private async
|
|
79
|
+
private async shouldCheck(intervalMs: number): Promise<boolean> {
|
|
89
80
|
try {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
return
|
|
81
|
+
const info = await this.readVersionInfo();
|
|
82
|
+
if (!info?.lastCheckTime) return true;
|
|
83
|
+
return Date.now() - info.lastCheckTime >= intervalMs;
|
|
93
84
|
} catch {
|
|
94
|
-
return
|
|
85
|
+
return true;
|
|
95
86
|
}
|
|
96
87
|
}
|
|
97
88
|
|
|
98
89
|
/**
|
|
99
|
-
*
|
|
100
|
-
* Background check runs every time for fresh data next run
|
|
90
|
+
* Read version info from disk
|
|
101
91
|
*/
|
|
102
|
-
async
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// No previous check = no upgrade info yet
|
|
110
|
-
if (!info) {
|
|
111
|
-
return {
|
|
112
|
-
flowNeedsUpgrade: false,
|
|
113
|
-
targetNeedsUpgrade: false,
|
|
114
|
-
flowVersion: null,
|
|
115
|
-
targetVersion: null,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Check if Flow needs upgrade
|
|
120
|
-
const flowVersion =
|
|
121
|
-
info.flowLatest && info.flowLatest !== currentVersion
|
|
122
|
-
? { current: currentVersion, latest: info.flowLatest }
|
|
123
|
-
: null;
|
|
124
|
-
|
|
125
|
-
// Check if target needs upgrade
|
|
126
|
-
let targetVersion: { current: string; latest: string } | null = null;
|
|
127
|
-
if (targetId && info.targetLatest?.[targetId] && info.targetCurrent?.[targetId]) {
|
|
128
|
-
const current = info.targetCurrent[targetId];
|
|
129
|
-
const latest = info.targetLatest[targetId];
|
|
130
|
-
if (current !== latest) {
|
|
131
|
-
targetVersion = { current, latest };
|
|
132
|
-
}
|
|
92
|
+
private async readVersionInfo(): Promise<VersionInfo | null> {
|
|
93
|
+
try {
|
|
94
|
+
const data = await fs.readFile(VERSION_FILE, 'utf-8');
|
|
95
|
+
return JSON.parse(data);
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
133
98
|
}
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
flowNeedsUpgrade: !!flowVersion,
|
|
137
|
-
targetNeedsUpgrade: !!targetVersion,
|
|
138
|
-
flowVersion,
|
|
139
|
-
targetVersion,
|
|
140
|
-
};
|
|
141
99
|
}
|
|
142
100
|
|
|
143
101
|
/**
|
|
144
|
-
*
|
|
145
|
-
* Runs every time, updates info for next run
|
|
102
|
+
* Perform version check and upgrade if needed
|
|
146
103
|
*/
|
|
147
|
-
private
|
|
148
|
-
|
|
149
|
-
this.
|
|
150
|
-
|
|
104
|
+
private async performCheck(): Promise<void> {
|
|
105
|
+
const currentVersion = await this.getCurrentVersion();
|
|
106
|
+
const latestVersion = await this.fetchLatestVersion();
|
|
107
|
+
|
|
108
|
+
// Save check time regardless of result
|
|
109
|
+
await this.saveVersionInfo({
|
|
110
|
+
flowLatest: latestVersion || undefined,
|
|
111
|
+
lastCheckTime: Date.now(),
|
|
151
112
|
});
|
|
152
|
-
}
|
|
153
113
|
|
|
154
|
-
|
|
155
|
-
* Perform the actual version check (called in background)
|
|
156
|
-
*/
|
|
157
|
-
private async performBackgroundCheck(targetId?: string): Promise<void> {
|
|
158
|
-
const oldInfo = await this.readVersionInfo();
|
|
159
|
-
|
|
160
|
-
const newInfo: VersionInfo = {
|
|
161
|
-
targetLatest: oldInfo?.targetLatest || {},
|
|
162
|
-
targetCurrent: oldInfo?.targetCurrent || {},
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
// Check Flow version from npm (with timeout)
|
|
166
|
-
try {
|
|
167
|
-
const { stdout } = await execAsync('npm view @sylphx/flow version', { timeout: 5000 });
|
|
168
|
-
newInfo.flowLatest = stdout.trim();
|
|
169
|
-
} catch {
|
|
170
|
-
// Keep old value if check fails
|
|
171
|
-
newInfo.flowLatest = oldInfo?.flowLatest;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Check target version from npm and local (with timeout)
|
|
175
|
-
if (targetId) {
|
|
176
|
-
const installation = this.targetInstaller.getInstallationInfo(targetId);
|
|
177
|
-
if (installation) {
|
|
178
|
-
// Check latest version from npm
|
|
179
|
-
try {
|
|
180
|
-
const { stdout } = await execAsync(`npm view ${installation.package} version`, {
|
|
181
|
-
timeout: 5000,
|
|
182
|
-
});
|
|
183
|
-
newInfo.targetLatest = newInfo.targetLatest || {};
|
|
184
|
-
newInfo.targetLatest[targetId] = stdout.trim();
|
|
185
|
-
} catch {
|
|
186
|
-
// Keep old value
|
|
187
|
-
}
|
|
114
|
+
if (!latestVersion) return;
|
|
188
115
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const match = stdout.match(/v?(\d+\.\d+\.\d+)/);
|
|
193
|
-
if (match) {
|
|
194
|
-
newInfo.targetCurrent = newInfo.targetCurrent || {};
|
|
195
|
-
newInfo.targetCurrent[targetId] = match[1];
|
|
196
|
-
}
|
|
197
|
-
} catch {
|
|
198
|
-
// Keep old value
|
|
199
|
-
}
|
|
200
|
-
}
|
|
116
|
+
// Upgrade if newer version available
|
|
117
|
+
if (latestVersion !== currentVersion) {
|
|
118
|
+
await this.upgrade();
|
|
201
119
|
}
|
|
202
|
-
|
|
203
|
-
await this.writeVersionInfo(newInfo);
|
|
204
120
|
}
|
|
205
121
|
|
|
206
122
|
/**
|
|
207
|
-
*
|
|
208
|
-
* by checking the executable path
|
|
123
|
+
* Get current Flow version from package.json
|
|
209
124
|
*/
|
|
210
|
-
private async
|
|
125
|
+
private async getCurrentVersion(): Promise<string> {
|
|
211
126
|
try {
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
if (flowPath.includes('bun')) {
|
|
216
|
-
return 'bun';
|
|
217
|
-
}
|
|
218
|
-
if (flowPath.includes('pnpm')) {
|
|
219
|
-
return 'pnpm';
|
|
220
|
-
}
|
|
221
|
-
if (flowPath.includes('yarn')) {
|
|
222
|
-
return 'yarn';
|
|
223
|
-
}
|
|
127
|
+
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
|
|
128
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
|
|
129
|
+
return packageJson.version;
|
|
224
130
|
} catch {
|
|
225
|
-
|
|
131
|
+
return 'unknown';
|
|
226
132
|
}
|
|
227
|
-
|
|
228
|
-
// Default to bun as it's the recommended install method
|
|
229
|
-
return 'bun';
|
|
230
133
|
}
|
|
231
134
|
|
|
232
135
|
/**
|
|
233
|
-
*
|
|
234
|
-
* @returns True if upgrade successful, false otherwise
|
|
136
|
+
* Fetch latest version from npm registry
|
|
235
137
|
*/
|
|
236
|
-
async
|
|
237
|
-
const spinner = ora('Upgrading Flow...').start();
|
|
238
|
-
|
|
138
|
+
private async fetchLatestVersion(): Promise<string | null> {
|
|
239
139
|
try {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (this.options.verbose) {
|
|
245
|
-
console.log(chalk.dim(` Using ${flowPm}: ${upgradeCmd}`));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
await execAsync(upgradeCmd);
|
|
249
|
-
|
|
250
|
-
spinner.succeed(chalk.green('✓ Flow upgraded (new version used on next run)'));
|
|
251
|
-
return true;
|
|
252
|
-
} catch (error) {
|
|
253
|
-
spinner.fail(chalk.red('✗ Flow upgrade failed'));
|
|
254
|
-
|
|
255
|
-
if (this.options.verbose) {
|
|
256
|
-
console.error(error);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return false;
|
|
140
|
+
const { stdout } = await execAsync('npm view @sylphx/flow version', { timeout: 5000 });
|
|
141
|
+
return stdout.trim();
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
260
144
|
}
|
|
261
145
|
}
|
|
262
146
|
|
|
263
147
|
/**
|
|
264
|
-
*
|
|
265
|
-
* Tries built-in upgrade command first, falls back to package manager
|
|
266
|
-
* @param targetId - Target CLI ID to upgrade
|
|
267
|
-
* @returns True if upgrade successful, false otherwise
|
|
148
|
+
* Detect package manager used to install Flow
|
|
268
149
|
*/
|
|
269
|
-
async
|
|
270
|
-
const installation = this.targetInstaller.getInstallationInfo(targetId);
|
|
271
|
-
if (!installation) {
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const packageManager = detectPackageManager(this.projectPath);
|
|
276
|
-
const spinner = ora(`Upgrading ${installation.name}...`).start();
|
|
277
|
-
|
|
150
|
+
private async detectPackageManager(): Promise<'bun' | 'npm' | 'pnpm' | 'yarn'> {
|
|
278
151
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
await execAsync('claude update');
|
|
283
|
-
spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
|
|
284
|
-
return true;
|
|
285
|
-
} catch {
|
|
286
|
-
// Fall back to npm upgrade
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// For OpenCode, use built-in upgrade command if available
|
|
291
|
-
if (targetId === 'opencode') {
|
|
292
|
-
try {
|
|
293
|
-
await execAsync('opencode upgrade');
|
|
294
|
-
spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
|
|
295
|
-
return true;
|
|
296
|
-
} catch {
|
|
297
|
-
// Fall back to npm upgrade
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Fall back to npm/bun/pnpm/yarn upgrade
|
|
302
|
-
const upgradeCmd = getUpgradeCommand(installation.package, packageManager);
|
|
303
|
-
await execAsync(upgradeCmd);
|
|
304
|
-
|
|
305
|
-
spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
|
|
306
|
-
return true;
|
|
307
|
-
} catch (error) {
|
|
308
|
-
spinner.fail(chalk.red(`✗ ${installation.name} upgrade failed`));
|
|
309
|
-
|
|
310
|
-
if (this.options.verbose) {
|
|
311
|
-
console.error(error);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return false;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Run auto-upgrade check and upgrade if needed (silent)
|
|
320
|
-
* @param targetId - Optional target CLI ID to check and upgrade
|
|
321
|
-
* @returns Upgrade result with status info
|
|
322
|
-
*/
|
|
323
|
-
async runAutoUpgrade(targetId?: string): Promise<{
|
|
324
|
-
flowUpgraded: boolean;
|
|
325
|
-
flowVersion?: { current: string; latest: string };
|
|
326
|
-
targetUpgraded: boolean;
|
|
327
|
-
targetVersion?: { current: string; latest: string };
|
|
328
|
-
}> {
|
|
329
|
-
const status = await this.checkForUpgrades(targetId);
|
|
330
|
-
const result = {
|
|
331
|
-
flowUpgraded: false,
|
|
332
|
-
flowVersion: status.flowVersion ?? undefined,
|
|
333
|
-
targetUpgraded: false,
|
|
334
|
-
targetVersion: status.targetVersion ?? undefined,
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
// Perform upgrades silently
|
|
338
|
-
if (status.flowNeedsUpgrade) {
|
|
339
|
-
result.flowUpgraded = await this.upgradeFlowSilent();
|
|
340
|
-
}
|
|
152
|
+
const { stdout } = await execAsync('which flow || where flow');
|
|
153
|
+
const flowPath = stdout.trim().toLowerCase();
|
|
341
154
|
|
|
342
|
-
|
|
343
|
-
|
|
155
|
+
if (flowPath.includes('bun')) return 'bun';
|
|
156
|
+
if (flowPath.includes('pnpm')) return 'pnpm';
|
|
157
|
+
if (flowPath.includes('yarn')) return 'yarn';
|
|
158
|
+
} catch {
|
|
159
|
+
// Fall through
|
|
344
160
|
}
|
|
345
|
-
|
|
346
|
-
return result;
|
|
161
|
+
return 'bun'; // Default
|
|
347
162
|
}
|
|
348
163
|
|
|
349
164
|
/**
|
|
350
|
-
* Upgrade Flow
|
|
165
|
+
* Upgrade Flow to latest version
|
|
351
166
|
*/
|
|
352
|
-
private async
|
|
167
|
+
private async upgrade(): Promise<void> {
|
|
353
168
|
try {
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
await execAsync(
|
|
357
|
-
return true;
|
|
169
|
+
const pm = await this.detectPackageManager();
|
|
170
|
+
const cmd = getUpgradeCommand('@sylphx/flow', pm);
|
|
171
|
+
await execAsync(cmd);
|
|
358
172
|
} catch {
|
|
359
|
-
|
|
173
|
+
// Silent fail
|
|
360
174
|
}
|
|
361
175
|
}
|
|
362
176
|
|
|
363
177
|
/**
|
|
364
|
-
*
|
|
178
|
+
* Save version info to disk
|
|
365
179
|
*/
|
|
366
|
-
private async
|
|
367
|
-
const installation = this.targetInstaller.getInstallationInfo(targetId);
|
|
368
|
-
if (!installation) {
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
371
|
-
|
|
180
|
+
private async saveVersionInfo(info: VersionInfo): Promise<void> {
|
|
372
181
|
try {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return true;
|
|
377
|
-
} catch {
|
|
378
|
-
// Fall back to npm
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (targetId === 'opencode') {
|
|
383
|
-
try {
|
|
384
|
-
await execAsync('opencode upgrade');
|
|
385
|
-
return true;
|
|
386
|
-
} catch {
|
|
387
|
-
// Fall back to npm
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const packageManager = detectPackageManager(this.projectPath);
|
|
392
|
-
const upgradeCmd = getUpgradeCommand(installation.package, packageManager);
|
|
393
|
-
await execAsync(upgradeCmd);
|
|
394
|
-
return true;
|
|
182
|
+
const dir = path.dirname(VERSION_FILE);
|
|
183
|
+
await fs.mkdir(dir, { recursive: true });
|
|
184
|
+
await fs.writeFile(VERSION_FILE, JSON.stringify(info, null, 2));
|
|
395
185
|
} catch {
|
|
396
|
-
|
|
186
|
+
// Silent fail
|
|
397
187
|
}
|
|
398
188
|
}
|
|
399
189
|
}
|
|
@@ -70,6 +70,7 @@ export interface MCPConfig {
|
|
|
70
70
|
|
|
71
71
|
export class GlobalConfigService {
|
|
72
72
|
private flowHomeDir: string;
|
|
73
|
+
private flowConfigCache: FlowConfig | null = null;
|
|
73
74
|
|
|
74
75
|
constructor() {
|
|
75
76
|
this.flowHomeDir = path.join(os.homedir(), '.sylphx-flow');
|
|
@@ -259,13 +260,18 @@ export class GlobalConfigService {
|
|
|
259
260
|
|
|
260
261
|
/**
|
|
261
262
|
* Load Flow config (agents, rules, output styles)
|
|
263
|
+
* Cached for performance - call invalidateFlowConfigCache() after saving
|
|
262
264
|
*/
|
|
263
265
|
async loadFlowConfig(): Promise<FlowConfig> {
|
|
266
|
+
if (this.flowConfigCache) {
|
|
267
|
+
return this.flowConfigCache;
|
|
268
|
+
}
|
|
269
|
+
|
|
264
270
|
const configPath = this.getFlowConfigPath();
|
|
265
271
|
|
|
266
272
|
if (!existsSync(configPath)) {
|
|
267
273
|
// Default: all agents, all rules, all output styles enabled
|
|
268
|
-
|
|
274
|
+
this.flowConfigCache = {
|
|
269
275
|
version: '1.0.0',
|
|
270
276
|
agents: {
|
|
271
277
|
builder: { enabled: true },
|
|
@@ -279,10 +285,19 @@ export class GlobalConfigService {
|
|
|
279
285
|
},
|
|
280
286
|
outputStyles: {},
|
|
281
287
|
};
|
|
288
|
+
return this.flowConfigCache;
|
|
282
289
|
}
|
|
283
290
|
|
|
284
291
|
const data = await fs.readFile(configPath, 'utf-8');
|
|
285
|
-
|
|
292
|
+
this.flowConfigCache = JSON.parse(data);
|
|
293
|
+
return this.flowConfigCache;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Invalidate flow config cache (call after saving)
|
|
298
|
+
*/
|
|
299
|
+
invalidateFlowConfigCache(): void {
|
|
300
|
+
this.flowConfigCache = null;
|
|
286
301
|
}
|
|
287
302
|
|
|
288
303
|
/**
|
|
@@ -292,6 +307,7 @@ export class GlobalConfigService {
|
|
|
292
307
|
await this.initialize();
|
|
293
308
|
const configPath = this.getFlowConfigPath();
|
|
294
309
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
310
|
+
this.flowConfigCache = config; // Update cache
|
|
295
311
|
}
|
|
296
312
|
|
|
297
313
|
/**
|