@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/flow",
3
- "version": "3.12.1",
3
+ "version": "3.13.1",
4
4
  "description": "One CLI to rule them all. Unified orchestration layer for AI coding assistants. Auto-detection, auto-installation, auto-upgrade.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -161,14 +161,15 @@ export async function executeFlowV2(
161
161
  ): Promise<void> {
162
162
  const projectPath = process.cwd();
163
163
 
164
- // Initialize config service early to check for saved preferences
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
- const installedTargets = await targetInstaller.detectInstalledTargets();
171
- const settings = await configService.loadSettings();
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: Auto-upgrade (silent, returns status)
242
- const autoUpgrade = new AutoUpgrade(projectPath);
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 configService.getEnabledRules();
289
- const enabledOutputStyles = await configService.getEnabledOutputStyles();
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
- if (existsSync(skillsDir)) {
54
- templates.skills = await this.loadSkills(skillsDir);
55
- }
32
+ const mcpConfigPath = path.join(this.assetsDir, 'mcp-servers.json');
33
+ const outputStylesDir = path.join(this.assetsDir, 'output-styles');
56
34
 
57
- // Load rules (check multiple possible locations)
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
- templates.rules = await fs.readFile(rulesPath, 'utf-8');
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
- for (const file of files) {
93
- if (!file.endsWith('.md')) {
94
- continue;
95
- }
96
-
97
- const content = await fs.readFile(path.join(agentsDir, file), 'utf-8');
98
- agents.push({ name: file, content });
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
- for (const file of files) {
114
- if (!file.endsWith('.md')) {
115
- continue;
116
- }
117
-
118
- const content = await fs.readFile(path.join(commandsDir, file), 'utf-8');
119
- commands.push({ name: file, content });
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
- for (const domain of domains) {
134
- const domainPath = path.join(skillsDir, domain);
135
- const stat = await fs.stat(domainPath);
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
- if (!stat.isDirectory()) {
138
- continue;
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
- // Name includes subdirectory: "auth/SKILL.md"
146
- skills.push({ name: `${domain}/SKILL.md`, content });
147
- }
148
- }
123
+ return { name: `${domain}/SKILL.md`, content };
124
+ })
125
+ );
149
126
 
150
- return skills;
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 (CLAUDE.md, .cursorrules, etc.)
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
- for (const entry of entries) {
178
- const filePath = path.join(singleFilesDir, entry);
179
- const stat = await fs.stat(filePath);
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
- files.push({ path: entry, content });
184
- }
185
- }
161
+ return { path: entry, content };
162
+ })
163
+ );
186
164
 
187
- return files;
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
- // Read version from package.json
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
- * Non-blocking version check with cache
4
- * Checks in background, shows result on next run
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 chalk from 'chalk';
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
- targetLatest?: Record<string, string>;
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
- }
28
+ export class AutoUpgrade {
29
+ private periodicCheckInterval: NodeJS.Timeout | null = null;
39
30
 
40
- export interface AutoUpgradeOptions {
41
- verbose?: boolean;
42
- skipFlow?: boolean;
43
- skipTarget?: boolean;
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
- export class AutoUpgrade {
47
- private projectPath: string;
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
- constructor(projectPath: string = process.cwd(), options: AutoUpgradeOptions = {}) {
52
- this.projectPath = projectPath;
53
- this.options = options;
54
- this.targetInstaller = new TargetInstaller(projectPath);
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
- * Read version info from last background check
52
+ * Stop background update service
59
53
  */
60
- private async readVersionInfo(): Promise<VersionInfo | null> {
61
- try {
62
- if (!existsSync(VERSION_FILE)) {
63
- return null;
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
- * Write version info
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 async writeVersionInfo(info: VersionInfo): Promise<void> {
76
- try {
77
- const dir = path.dirname(VERSION_FILE);
78
- await fs.mkdir(dir, { recursive: true });
79
- await fs.writeFile(VERSION_FILE, JSON.stringify(info, null, 2));
80
- } catch {
81
- // Silent fail
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
- * Get current Flow version from package.json (instant, local file)
77
+ * Check if enough time has passed since last check
87
78
  */
88
- private async getCurrentFlowVersion(): Promise<string> {
79
+ private async shouldCheck(intervalMs: number): Promise<boolean> {
89
80
  try {
90
- const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
91
- const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
92
- return packageJson.version;
81
+ const info = await this.readVersionInfo();
82
+ if (!info?.lastCheckTime) return true;
83
+ return Date.now() - info.lastCheckTime >= intervalMs;
93
84
  } catch {
94
- return 'unknown';
85
+ return true;
95
86
  }
96
87
  }
97
88
 
98
89
  /**
99
- * Check for available upgrades (instant, reads from last background check)
100
- * Background check runs every time for fresh data next run
90
+ * Read version info from disk
101
91
  */
102
- async checkForUpgrades(targetId?: string): Promise<UpgradeStatus> {
103
- const info = await this.readVersionInfo();
104
- const currentVersion = await this.getCurrentFlowVersion();
105
-
106
- // Trigger background check for next run (non-blocking, every time)
107
- this.checkInBackground(targetId);
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
- * Check versions in background (non-blocking)
145
- * Runs every time, updates info for next run
102
+ * Perform version check and upgrade if needed
146
103
  */
147
- private checkInBackground(targetId?: string): void {
148
- // Fire and forget - don't await
149
- this.performBackgroundCheck(targetId).catch(() => {
150
- // Silent fail
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
- // Check current installed version (local command)
190
- try {
191
- const { stdout } = await execAsync(installation.checkCommand, { timeout: 5000 });
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
- * Detect which package manager was used to install Flow globally
208
- * by checking the executable path
123
+ * Get current Flow version from package.json
209
124
  */
210
- private async detectFlowPackageManager(): Promise<'bun' | 'npm' | 'pnpm' | 'yarn'> {
125
+ private async getCurrentVersion(): Promise<string> {
211
126
  try {
212
- const { stdout } = await execAsync('which flow || where flow');
213
- const flowPath = stdout.trim().toLowerCase();
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
- // Fall through to default
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
- * Upgrade Flow to latest version using the package manager that installed it
234
- * @returns True if upgrade successful, false otherwise
136
+ * Fetch latest version from npm registry
235
137
  */
236
- async upgradeFlow(): Promise<boolean> {
237
- const spinner = ora('Upgrading Flow...').start();
238
-
138
+ private async fetchLatestVersion(): Promise<string | null> {
239
139
  try {
240
- // Detect which package manager was used to install Flow
241
- const flowPm = await this.detectFlowPackageManager();
242
- const upgradeCmd = getUpgradeCommand('@sylphx/flow', flowPm);
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
- * Upgrade target CLI to latest version
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 upgradeTarget(targetId: string): Promise<boolean> {
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
- // For Claude Code, use built-in update command if available
280
- if (targetId === 'claude-code') {
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
- if (status.targetNeedsUpgrade && targetId) {
343
- result.targetUpgraded = await this.upgradeTargetSilent(targetId);
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 silently (no spinner/output)
165
+ * Upgrade Flow to latest version
351
166
  */
352
- private async upgradeFlowSilent(): Promise<boolean> {
167
+ private async upgrade(): Promise<void> {
353
168
  try {
354
- const flowPm = await this.detectFlowPackageManager();
355
- const upgradeCmd = getUpgradeCommand('@sylphx/flow', flowPm);
356
- await execAsync(upgradeCmd);
357
- return true;
169
+ const pm = await this.detectPackageManager();
170
+ const cmd = getUpgradeCommand('@sylphx/flow', pm);
171
+ await execAsync(cmd);
358
172
  } catch {
359
- return false;
173
+ // Silent fail
360
174
  }
361
175
  }
362
176
 
363
177
  /**
364
- * Upgrade target CLI silently (no spinner/output)
178
+ * Save version info to disk
365
179
  */
366
- private async upgradeTargetSilent(targetId: string): Promise<boolean> {
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
- if (targetId === 'claude-code') {
374
- try {
375
- await execAsync('claude update');
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
- return false;
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
- return {
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
- return JSON.parse(data);
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
  /**