@sylphx/flow 2.15.1 → 2.15.2

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,11 @@
1
1
  # @sylphx/flow
2
2
 
3
+ ## 2.15.2 (2025-12-17)
4
+
5
+ ### ⚡️ Performance
6
+
7
+ - **auto-upgrade:** non-blocking version check with cache ([0550e44](https://github.com/SylphxAI/flow/commit/0550e44ea9b471ae07b2ea13c196354a7a32a605))
8
+
3
9
  ## 2.15.1 (2025-12-17)
4
10
 
5
11
  ### Improvements
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/flow",
3
- "version": "2.15.1",
3
+ "version": "2.15.2",
4
4
  "description": "One CLI to rule them all. Unified orchestration layer for Claude Code, OpenCode, Cursor and all AI development tools. Auto-detection, auto-installation, auto-upgrade.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Auto-Upgrade Service
3
- * Automatically checks and upgrades Flow and target CLI before each execution
3
+ * Non-blocking version check with cache
4
+ * Checks in background, shows result on next run
4
5
  */
5
6
 
6
7
  import { exec } from 'node:child_process';
8
+ import { existsSync } from 'node:fs';
7
9
  import fs from 'node:fs/promises';
10
+ import os from 'node:os';
8
11
  import path from 'node:path';
9
12
  import { fileURLToPath } from 'node:url';
10
13
  import { promisify } from 'node:util';
@@ -16,6 +19,16 @@ import { TargetInstaller } from './target-installer.js';
16
19
  const __filename = fileURLToPath(import.meta.url);
17
20
  const __dirname = path.dirname(__filename);
18
21
 
22
+ // Cache file for version checks (24 hour TTL)
23
+ const CACHE_FILE = path.join(os.homedir(), '.sylphx-flow', 'version-cache.json');
24
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
25
+
26
+ interface VersionCache {
27
+ flowLatest?: string;
28
+ targetLatest?: Record<string, string>;
29
+ checkedAt: number;
30
+ }
31
+
19
32
  const execAsync = promisify(exec);
20
33
 
21
34
  export interface UpgradeStatus {
@@ -43,80 +56,151 @@ export class AutoUpgrade {
43
56
  }
44
57
 
45
58
  /**
46
- * Check for available upgrades for Flow and target CLI
47
- * @param targetId - Optional target CLI ID to check for upgrades
48
- * @returns Upgrade status indicating what needs upgrading
59
+ * Read version cache (instant, no network)
49
60
  */
50
- async checkForUpgrades(targetId?: string): Promise<UpgradeStatus> {
51
- const [flowVersion, targetVersion] = await Promise.all([
52
- this.options.skipFlow ? null : this.checkFlowVersion(),
53
- this.options.skipTarget || !targetId ? null : this.checkTargetVersion(targetId),
54
- ]);
61
+ private async readCache(): Promise<VersionCache | null> {
62
+ try {
63
+ if (!existsSync(CACHE_FILE)) {
64
+ return null;
65
+ }
66
+ const data = await fs.readFile(CACHE_FILE, 'utf-8');
67
+ return JSON.parse(data);
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
55
72
 
56
- return {
57
- flowNeedsUpgrade: !!flowVersion,
58
- targetNeedsUpgrade: !!targetVersion,
59
- flowVersion,
60
- targetVersion,
61
- };
73
+ /**
74
+ * Write version cache
75
+ */
76
+ private async writeCache(cache: VersionCache): Promise<void> {
77
+ try {
78
+ const dir = path.dirname(CACHE_FILE);
79
+ await fs.mkdir(dir, { recursive: true });
80
+ await fs.writeFile(CACHE_FILE, JSON.stringify(cache, null, 2));
81
+ } catch {
82
+ // Silent fail
83
+ }
62
84
  }
63
85
 
64
86
  /**
65
- * Check Flow version
87
+ * Get current Flow version from package.json (instant, local file)
66
88
  */
67
- private async checkFlowVersion(): Promise<{ current: string; latest: string } | null> {
89
+ private async getCurrentFlowVersion(): Promise<string> {
68
90
  try {
69
- // Get current version from package.json
70
91
  const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
71
92
  const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
72
- const currentVersion = packageJson.version;
73
-
74
- // Get latest version from npm
75
- const { stdout } = await execAsync('npm view @sylphx/flow version');
76
- const latestVersion = stdout.trim();
77
-
78
- if (currentVersion !== latestVersion) {
79
- return { current: currentVersion, latest: latestVersion };
80
- }
81
-
82
- return null;
93
+ return packageJson.version;
83
94
  } catch {
84
- return null;
95
+ return 'unknown';
85
96
  }
86
97
  }
87
98
 
88
99
  /**
89
- * Check target CLI version
100
+ * Check for available upgrades using CACHE (instant, no network)
101
+ * Returns cached results from previous background check
90
102
  */
91
- private async checkTargetVersion(
92
- targetId: string
93
- ): Promise<{ current: string; latest: string } | null> {
94
- const installation = this.targetInstaller.getInstallationInfo(targetId);
95
- if (!installation) {
96
- return null;
103
+ async checkForUpgrades(targetId?: string): Promise<UpgradeStatus> {
104
+ const cache = await this.readCache();
105
+ const currentVersion = await this.getCurrentFlowVersion();
106
+
107
+ // Trigger background check for next run (non-blocking)
108
+ this.checkInBackground(targetId);
109
+
110
+ // No cache or expired = no upgrade info yet
111
+ if (!cache) {
112
+ return {
113
+ flowNeedsUpgrade: false,
114
+ targetNeedsUpgrade: false,
115
+ flowVersion: null,
116
+ targetVersion: null,
117
+ };
97
118
  }
98
119
 
99
- try {
100
- // Get current version
101
- const { stdout: currentOutput } = await execAsync(installation.checkCommand);
102
- const currentMatch = currentOutput.match(/v?(\d+\.\d+\.\d+)/);
103
- if (!currentMatch) {
104
- return null;
120
+ // Check if Flow needs upgrade based on cache
121
+ const flowVersion =
122
+ cache.flowLatest && cache.flowLatest !== currentVersion
123
+ ? { current: currentVersion, latest: cache.flowLatest }
124
+ : null;
125
+
126
+ // Check if target needs upgrade based on cache
127
+ let targetVersion: { current: string; latest: string } | null = null;
128
+ if (targetId && cache.targetLatest?.[targetId]) {
129
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
130
+ if (installation) {
131
+ try {
132
+ const { stdout } = await execAsync(installation.checkCommand);
133
+ const match = stdout.match(/v?(\d+\.\d+\.\d+)/);
134
+ if (match && match[1] !== cache.targetLatest[targetId]) {
135
+ targetVersion = { current: match[1], latest: cache.targetLatest[targetId] };
136
+ }
137
+ } catch {
138
+ // Silent
139
+ }
105
140
  }
106
- const currentVersion = currentMatch[1];
141
+ }
107
142
 
108
- // Get latest version from npm
109
- const { stdout: latestOutput } = await execAsync(`npm view ${installation.package} version`);
110
- const latestVersion = latestOutput.trim();
143
+ return {
144
+ flowNeedsUpgrade: !!flowVersion,
145
+ targetNeedsUpgrade: !!targetVersion,
146
+ flowVersion,
147
+ targetVersion,
148
+ };
149
+ }
111
150
 
112
- if (currentVersion !== latestVersion) {
113
- return { current: currentVersion, latest: latestVersion };
114
- }
151
+ /**
152
+ * Check versions in background (non-blocking)
153
+ * Updates cache for next run
154
+ */
155
+ private checkInBackground(targetId?: string): void {
156
+ // Fire and forget - don't await
157
+ this.performBackgroundCheck(targetId).catch(() => {
158
+ // Silent fail
159
+ });
160
+ }
115
161
 
116
- return null;
162
+ /**
163
+ * Perform the actual version check (called in background)
164
+ */
165
+ private async performBackgroundCheck(targetId?: string): Promise<void> {
166
+ const cache = await this.readCache();
167
+
168
+ // Skip if checked recently (within TTL)
169
+ if (cache && Date.now() - cache.checkedAt < CACHE_TTL_MS) {
170
+ return;
171
+ }
172
+
173
+ const newCache: VersionCache = {
174
+ checkedAt: Date.now(),
175
+ targetLatest: cache?.targetLatest || {},
176
+ };
177
+
178
+ // Check Flow version from npm (with timeout)
179
+ try {
180
+ const { stdout } = await execAsync('npm view @sylphx/flow version', { timeout: 5000 });
181
+ newCache.flowLatest = stdout.trim();
117
182
  } catch {
118
- return null;
183
+ // Keep old cache value if check fails
184
+ newCache.flowLatest = cache?.flowLatest;
185
+ }
186
+
187
+ // Check target version from npm (with timeout)
188
+ if (targetId) {
189
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
190
+ if (installation) {
191
+ try {
192
+ const { stdout } = await execAsync(`npm view ${installation.package} version`, {
193
+ timeout: 5000,
194
+ });
195
+ newCache.targetLatest = newCache.targetLatest || {};
196
+ newCache.targetLatest[targetId] = stdout.trim();
197
+ } catch {
198
+ // Keep old cache value
199
+ }
200
+ }
119
201
  }
202
+
203
+ await this.writeCache(newCache);
120
204
  }
121
205
 
122
206
  /**