@sylphx/flow 3.13.0 → 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 +12 -0
- package/package.json +1 -1
- package/src/commands/flow/execute-v2.ts +14 -23
- package/src/core/template-loader.ts +72 -94
- package/src/index.ts +3 -9
- package/src/services/auto-upgrade.ts +96 -370
- package/src/services/global-config.ts +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
3
15
|
## 3.13.0 (2026-02-04)
|
|
4
16
|
|
|
5
17
|
### ✨ Features
|
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,20 +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
|
-
// Start periodic background checks (every 30 minutes)
|
|
246
|
-
autoUpgrade.startPeriodicCheck(selectedTargetId);
|
|
247
|
-
|
|
248
|
-
// Show upgrade notice (minimal - only if upgraded)
|
|
249
|
-
if (upgradeResult.flowUpgraded && upgradeResult.flowVersion) {
|
|
250
|
-
console.log(chalk.dim(`↑ flow ${upgradeResult.flowVersion.latest}`));
|
|
251
|
-
}
|
|
252
|
-
if (upgradeResult.targetUpgraded && upgradeResult.targetVersion) {
|
|
253
|
-
console.log(chalk.dim(`↑ ${targetName.toLowerCase()} ${upgradeResult.targetVersion.latest}`));
|
|
254
|
-
}
|
|
242
|
+
// Step 2: Start background auto-upgrade (non-blocking)
|
|
243
|
+
new AutoUpgrade().start();
|
|
255
244
|
|
|
256
245
|
// Create executor
|
|
257
246
|
const executor = new FlowExecutor();
|
|
@@ -287,9 +276,11 @@ export async function executeFlowV2(
|
|
|
287
276
|
agent = enabledAgents.length > 0 ? enabledAgents[0] : 'builder';
|
|
288
277
|
}
|
|
289
278
|
|
|
290
|
-
// Load agent content
|
|
291
|
-
const enabledRules = await
|
|
292
|
-
|
|
279
|
+
// Load agent content (parallel fetch rules and styles)
|
|
280
|
+
const [enabledRules, enabledOutputStyles] = await Promise.all([
|
|
281
|
+
configService.getEnabledRules(),
|
|
282
|
+
configService.getEnabledOutputStyles(),
|
|
283
|
+
]);
|
|
293
284
|
|
|
294
285
|
const agentContent = await loadAgentContent(
|
|
295
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,463 +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 interface UpgradeStatus {
|
|
34
|
-
flowNeedsUpgrade: boolean;
|
|
35
|
-
targetNeedsUpgrade: boolean;
|
|
36
|
-
flowVersion: { current: string; latest: string } | null;
|
|
37
|
-
targetVersion: { current: string; latest: string } | null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface AutoUpgradeOptions {
|
|
41
|
-
verbose?: boolean;
|
|
42
|
-
skipFlow?: boolean;
|
|
43
|
-
skipTarget?: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Default interval: 30 minutes
|
|
47
|
-
const DEFAULT_CHECK_INTERVAL_MS = 30 * 60 * 1000;
|
|
48
|
-
|
|
49
28
|
export class AutoUpgrade {
|
|
50
|
-
private projectPath: string;
|
|
51
|
-
private options: AutoUpgradeOptions;
|
|
52
|
-
private targetInstaller: TargetInstaller;
|
|
53
29
|
private periodicCheckInterval: NodeJS.Timeout | null = null;
|
|
54
30
|
|
|
55
|
-
constructor(projectPath: string = process.cwd(), options: AutoUpgradeOptions = {}) {
|
|
56
|
-
this.projectPath = projectPath;
|
|
57
|
-
this.options = options;
|
|
58
|
-
this.targetInstaller = new TargetInstaller(projectPath);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
31
|
/**
|
|
62
|
-
*
|
|
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
|
|
63
35
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (!existsSync(VERSION_FILE)) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
const data = await fs.readFile(VERSION_FILE, 'utf-8');
|
|
70
|
-
return JSON.parse(data);
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
36
|
+
start(intervalMs: number = DEFAULT_CHECK_INTERVAL_MS): void {
|
|
37
|
+
this.stop();
|
|
75
38
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*/
|
|
79
|
-
private async writeVersionInfo(info: VersionInfo): Promise<void> {
|
|
80
|
-
try {
|
|
81
|
-
const dir = path.dirname(VERSION_FILE);
|
|
82
|
-
await fs.mkdir(dir, { recursive: true });
|
|
83
|
-
await fs.writeFile(VERSION_FILE, JSON.stringify(info, null, 2));
|
|
84
|
-
} catch {
|
|
85
|
-
// Silent fail
|
|
86
|
-
}
|
|
87
|
-
}
|
|
39
|
+
// First check immediately (non-blocking) - skips if recently checked
|
|
40
|
+
this.checkAndUpgrade(intervalMs);
|
|
88
41
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return packageJson.version;
|
|
97
|
-
} catch {
|
|
98
|
-
return 'unknown';
|
|
99
|
-
}
|
|
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();
|
|
100
49
|
}
|
|
101
50
|
|
|
102
51
|
/**
|
|
103
|
-
*
|
|
104
|
-
* Background check runs every time for fresh data next run
|
|
52
|
+
* Stop background update service
|
|
105
53
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Trigger background check for next run (non-blocking, every time)
|
|
111
|
-
this.checkInBackground(targetId);
|
|
112
|
-
|
|
113
|
-
// No previous check = no upgrade info yet
|
|
114
|
-
if (!info) {
|
|
115
|
-
return {
|
|
116
|
-
flowNeedsUpgrade: false,
|
|
117
|
-
targetNeedsUpgrade: false,
|
|
118
|
-
flowVersion: null,
|
|
119
|
-
targetVersion: null,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Check if Flow needs upgrade
|
|
124
|
-
const flowVersion =
|
|
125
|
-
info.flowLatest && info.flowLatest !== currentVersion
|
|
126
|
-
? { current: currentVersion, latest: info.flowLatest }
|
|
127
|
-
: null;
|
|
128
|
-
|
|
129
|
-
// Check if target needs upgrade
|
|
130
|
-
let targetVersion: { current: string; latest: string } | null = null;
|
|
131
|
-
if (targetId && info.targetLatest?.[targetId] && info.targetCurrent?.[targetId]) {
|
|
132
|
-
const current = info.targetCurrent[targetId];
|
|
133
|
-
const latest = info.targetLatest[targetId];
|
|
134
|
-
if (current !== latest) {
|
|
135
|
-
targetVersion = { current, latest };
|
|
136
|
-
}
|
|
54
|
+
stop(): void {
|
|
55
|
+
if (this.periodicCheckInterval) {
|
|
56
|
+
clearInterval(this.periodicCheckInterval);
|
|
57
|
+
this.periodicCheckInterval = null;
|
|
137
58
|
}
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
flowNeedsUpgrade: !!flowVersion,
|
|
141
|
-
targetNeedsUpgrade: !!targetVersion,
|
|
142
|
-
flowVersion,
|
|
143
|
-
targetVersion,
|
|
144
|
-
};
|
|
145
59
|
}
|
|
146
60
|
|
|
147
61
|
/**
|
|
148
|
-
* Check
|
|
149
|
-
*
|
|
62
|
+
* Check for updates and upgrade if available (non-blocking)
|
|
63
|
+
* Skips if checked within intervalMs
|
|
64
|
+
* Fire and forget - errors are silently ignored
|
|
150
65
|
*/
|
|
151
|
-
private
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
}
|
|
155
73
|
});
|
|
156
74
|
}
|
|
157
75
|
|
|
158
76
|
/**
|
|
159
|
-
*
|
|
77
|
+
* Check if enough time has passed since last check
|
|
160
78
|
*/
|
|
161
|
-
private async
|
|
162
|
-
const oldInfo = await this.readVersionInfo();
|
|
163
|
-
|
|
164
|
-
const newInfo: VersionInfo = {
|
|
165
|
-
targetLatest: oldInfo?.targetLatest || {},
|
|
166
|
-
targetCurrent: oldInfo?.targetCurrent || {},
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
// Check Flow version from npm (with timeout)
|
|
79
|
+
private async shouldCheck(intervalMs: number): Promise<boolean> {
|
|
170
80
|
try {
|
|
171
|
-
const
|
|
172
|
-
|
|
81
|
+
const info = await this.readVersionInfo();
|
|
82
|
+
if (!info?.lastCheckTime) return true;
|
|
83
|
+
return Date.now() - info.lastCheckTime >= intervalMs;
|
|
173
84
|
} catch {
|
|
174
|
-
|
|
175
|
-
newInfo.flowLatest = oldInfo?.flowLatest;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Check target version from npm and local (with timeout)
|
|
179
|
-
if (targetId) {
|
|
180
|
-
const installation = this.targetInstaller.getInstallationInfo(targetId);
|
|
181
|
-
if (installation) {
|
|
182
|
-
// Check latest version from npm
|
|
183
|
-
try {
|
|
184
|
-
const { stdout } = await execAsync(`npm view ${installation.package} version`, {
|
|
185
|
-
timeout: 5000,
|
|
186
|
-
});
|
|
187
|
-
newInfo.targetLatest = newInfo.targetLatest || {};
|
|
188
|
-
newInfo.targetLatest[targetId] = stdout.trim();
|
|
189
|
-
} catch {
|
|
190
|
-
// Keep old value
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Check current installed version (local command)
|
|
194
|
-
try {
|
|
195
|
-
const { stdout } = await execAsync(installation.checkCommand, { timeout: 5000 });
|
|
196
|
-
const match = stdout.match(/v?(\d+\.\d+\.\d+)/);
|
|
197
|
-
if (match) {
|
|
198
|
-
newInfo.targetCurrent = newInfo.targetCurrent || {};
|
|
199
|
-
newInfo.targetCurrent[targetId] = match[1];
|
|
200
|
-
}
|
|
201
|
-
} catch {
|
|
202
|
-
// Keep old value
|
|
203
|
-
}
|
|
204
|
-
}
|
|
85
|
+
return true;
|
|
205
86
|
}
|
|
206
|
-
|
|
207
|
-
await this.writeVersionInfo(newInfo);
|
|
208
87
|
}
|
|
209
88
|
|
|
210
89
|
/**
|
|
211
|
-
*
|
|
212
|
-
* by checking the executable path
|
|
90
|
+
* Read version info from disk
|
|
213
91
|
*/
|
|
214
|
-
private async
|
|
92
|
+
private async readVersionInfo(): Promise<VersionInfo | null> {
|
|
215
93
|
try {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (flowPath.includes('bun')) {
|
|
220
|
-
return 'bun';
|
|
221
|
-
}
|
|
222
|
-
if (flowPath.includes('pnpm')) {
|
|
223
|
-
return 'pnpm';
|
|
224
|
-
}
|
|
225
|
-
if (flowPath.includes('yarn')) {
|
|
226
|
-
return 'yarn';
|
|
227
|
-
}
|
|
94
|
+
const data = await fs.readFile(VERSION_FILE, 'utf-8');
|
|
95
|
+
return JSON.parse(data);
|
|
228
96
|
} catch {
|
|
229
|
-
|
|
97
|
+
return null;
|
|
230
98
|
}
|
|
231
|
-
|
|
232
|
-
// Default to bun as it's the recommended install method
|
|
233
|
-
return 'bun';
|
|
234
99
|
}
|
|
235
100
|
|
|
236
101
|
/**
|
|
237
|
-
*
|
|
238
|
-
* @returns True if upgrade successful, false otherwise
|
|
102
|
+
* Perform version check and upgrade if needed
|
|
239
103
|
*/
|
|
240
|
-
async
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
console.log(chalk.dim(` Using ${flowPm}: ${upgradeCmd}`));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
await execAsync(upgradeCmd);
|
|
253
|
-
|
|
254
|
-
spinner.succeed(chalk.green('✓ Flow upgraded (new version used on next run)'));
|
|
255
|
-
return true;
|
|
256
|
-
} catch (error) {
|
|
257
|
-
spinner.fail(chalk.red('✗ Flow upgrade failed'));
|
|
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(),
|
|
112
|
+
});
|
|
258
113
|
|
|
259
|
-
|
|
260
|
-
console.error(error);
|
|
261
|
-
}
|
|
114
|
+
if (!latestVersion) return;
|
|
262
115
|
|
|
263
|
-
|
|
116
|
+
// Upgrade if newer version available
|
|
117
|
+
if (latestVersion !== currentVersion) {
|
|
118
|
+
await this.upgrade();
|
|
264
119
|
}
|
|
265
120
|
}
|
|
266
121
|
|
|
267
122
|
/**
|
|
268
|
-
*
|
|
269
|
-
* Tries built-in upgrade command first, falls back to package manager
|
|
270
|
-
* @param targetId - Target CLI ID to upgrade
|
|
271
|
-
* @returns True if upgrade successful, false otherwise
|
|
123
|
+
* Get current Flow version from package.json
|
|
272
124
|
*/
|
|
273
|
-
async
|
|
274
|
-
const installation = this.targetInstaller.getInstallationInfo(targetId);
|
|
275
|
-
if (!installation) {
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const packageManager = detectPackageManager(this.projectPath);
|
|
280
|
-
const spinner = ora(`Upgrading ${installation.name}...`).start();
|
|
281
|
-
|
|
125
|
+
private async getCurrentVersion(): Promise<string> {
|
|
282
126
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return true;
|
|
289
|
-
} catch {
|
|
290
|
-
// Fall back to npm upgrade
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// For OpenCode, use built-in upgrade command if available
|
|
295
|
-
if (targetId === 'opencode') {
|
|
296
|
-
try {
|
|
297
|
-
await execAsync('opencode upgrade');
|
|
298
|
-
spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
|
|
299
|
-
return true;
|
|
300
|
-
} catch {
|
|
301
|
-
// Fall back to npm upgrade
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Fall back to npm/bun/pnpm/yarn upgrade
|
|
306
|
-
const upgradeCmd = getUpgradeCommand(installation.package, packageManager);
|
|
307
|
-
await execAsync(upgradeCmd);
|
|
308
|
-
|
|
309
|
-
spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
|
|
310
|
-
return true;
|
|
311
|
-
} catch (error) {
|
|
312
|
-
spinner.fail(chalk.red(`✗ ${installation.name} upgrade failed`));
|
|
313
|
-
|
|
314
|
-
if (this.options.verbose) {
|
|
315
|
-
console.error(error);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Run auto-upgrade check and upgrade if needed (silent)
|
|
324
|
-
* @param targetId - Optional target CLI ID to check and upgrade
|
|
325
|
-
* @returns Upgrade result with status info
|
|
326
|
-
*/
|
|
327
|
-
async runAutoUpgrade(targetId?: string): Promise<{
|
|
328
|
-
flowUpgraded: boolean;
|
|
329
|
-
flowVersion?: { current: string; latest: string };
|
|
330
|
-
targetUpgraded: boolean;
|
|
331
|
-
targetVersion?: { current: string; latest: string };
|
|
332
|
-
}> {
|
|
333
|
-
const status = await this.checkForUpgrades(targetId);
|
|
334
|
-
const result = {
|
|
335
|
-
flowUpgraded: false,
|
|
336
|
-
flowVersion: status.flowVersion ?? undefined,
|
|
337
|
-
targetUpgraded: false,
|
|
338
|
-
targetVersion: status.targetVersion ?? undefined,
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
// Perform upgrades silently
|
|
342
|
-
if (status.flowNeedsUpgrade) {
|
|
343
|
-
result.flowUpgraded = await this.upgradeFlowSilent();
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (status.targetNeedsUpgrade && targetId) {
|
|
347
|
-
result.targetUpgraded = await this.upgradeTargetSilent(targetId);
|
|
127
|
+
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
|
|
128
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
|
|
129
|
+
return packageJson.version;
|
|
130
|
+
} catch {
|
|
131
|
+
return 'unknown';
|
|
348
132
|
}
|
|
349
|
-
|
|
350
|
-
return result;
|
|
351
133
|
}
|
|
352
134
|
|
|
353
135
|
/**
|
|
354
|
-
*
|
|
136
|
+
* Fetch latest version from npm registry
|
|
355
137
|
*/
|
|
356
|
-
private async
|
|
138
|
+
private async fetchLatestVersion(): Promise<string | null> {
|
|
357
139
|
try {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
await execAsync(upgradeCmd);
|
|
361
|
-
return true;
|
|
140
|
+
const { stdout } = await execAsync('npm view @sylphx/flow version', { timeout: 5000 });
|
|
141
|
+
return stdout.trim();
|
|
362
142
|
} catch {
|
|
363
|
-
return
|
|
143
|
+
return null;
|
|
364
144
|
}
|
|
365
145
|
}
|
|
366
146
|
|
|
367
147
|
/**
|
|
368
|
-
*
|
|
148
|
+
* Detect package manager used to install Flow
|
|
369
149
|
*/
|
|
370
|
-
private async
|
|
371
|
-
const installation = this.targetInstaller.getInstallationInfo(targetId);
|
|
372
|
-
if (!installation) {
|
|
373
|
-
return false;
|
|
374
|
-
}
|
|
375
|
-
|
|
150
|
+
private async detectPackageManager(): Promise<'bun' | 'npm' | 'pnpm' | 'yarn'> {
|
|
376
151
|
try {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
await execAsync('claude update');
|
|
380
|
-
return true;
|
|
381
|
-
} catch {
|
|
382
|
-
// Fall back to npm
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (targetId === 'opencode') {
|
|
387
|
-
try {
|
|
388
|
-
await execAsync('opencode upgrade');
|
|
389
|
-
return true;
|
|
390
|
-
} catch {
|
|
391
|
-
// Fall back to npm
|
|
392
|
-
}
|
|
393
|
-
}
|
|
152
|
+
const { stdout } = await execAsync('which flow || where flow');
|
|
153
|
+
const flowPath = stdout.trim().toLowerCase();
|
|
394
154
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return true;
|
|
155
|
+
if (flowPath.includes('bun')) return 'bun';
|
|
156
|
+
if (flowPath.includes('pnpm')) return 'pnpm';
|
|
157
|
+
if (flowPath.includes('yarn')) return 'yarn';
|
|
399
158
|
} catch {
|
|
400
|
-
|
|
159
|
+
// Fall through
|
|
401
160
|
}
|
|
161
|
+
return 'bun'; // Default
|
|
402
162
|
}
|
|
403
163
|
|
|
404
164
|
/**
|
|
405
|
-
*
|
|
406
|
-
* Runs every intervalMs (default 30 minutes)
|
|
407
|
-
* Silently upgrades if updates are available
|
|
408
|
-
* @param targetId - Optional target CLI ID to check
|
|
409
|
-
* @param intervalMs - Check interval in milliseconds (default 30 minutes)
|
|
410
|
-
*/
|
|
411
|
-
startPeriodicCheck(targetId?: string, intervalMs: number = DEFAULT_CHECK_INTERVAL_MS): void {
|
|
412
|
-
// Clear any existing interval
|
|
413
|
-
this.stopPeriodicCheck();
|
|
414
|
-
|
|
415
|
-
// Start periodic check
|
|
416
|
-
this.periodicCheckInterval = setInterval(() => {
|
|
417
|
-
this.performPeriodicUpgrade(targetId).catch(() => {
|
|
418
|
-
// Silent fail
|
|
419
|
-
});
|
|
420
|
-
}, intervalMs);
|
|
421
|
-
|
|
422
|
-
// Don't prevent process from exiting
|
|
423
|
-
this.periodicCheckInterval.unref();
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Stop periodic background checks
|
|
165
|
+
* Upgrade Flow to latest version
|
|
428
166
|
*/
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
167
|
+
private async upgrade(): Promise<void> {
|
|
168
|
+
try {
|
|
169
|
+
const pm = await this.detectPackageManager();
|
|
170
|
+
const cmd = getUpgradeCommand('@sylphx/flow', pm);
|
|
171
|
+
await execAsync(cmd);
|
|
172
|
+
} catch {
|
|
173
|
+
// Silent fail
|
|
433
174
|
}
|
|
434
175
|
}
|
|
435
176
|
|
|
436
177
|
/**
|
|
437
|
-
*
|
|
178
|
+
* Save version info to disk
|
|
438
179
|
*/
|
|
439
|
-
private async
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const currentVersion = await this.getCurrentFlowVersion();
|
|
448
|
-
|
|
449
|
-
// Silently upgrade Flow if needed
|
|
450
|
-
if (info.flowLatest && info.flowLatest !== currentVersion) {
|
|
451
|
-
await this.upgradeFlowSilent();
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Silently upgrade target if needed
|
|
455
|
-
if (targetId && info.targetLatest?.[targetId] && info.targetCurrent?.[targetId]) {
|
|
456
|
-
const current = info.targetCurrent[targetId];
|
|
457
|
-
const latest = info.targetLatest[targetId];
|
|
458
|
-
if (current !== latest) {
|
|
459
|
-
await this.upgradeTargetSilent(targetId);
|
|
460
|
-
}
|
|
180
|
+
private async saveVersionInfo(info: VersionInfo): Promise<void> {
|
|
181
|
+
try {
|
|
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));
|
|
185
|
+
} catch {
|
|
186
|
+
// Silent fail
|
|
461
187
|
}
|
|
462
188
|
}
|
|
463
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
|
/**
|