@sylphx/flow 2.15.0 → 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,27 @@
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
+
9
+ ## 2.15.1 (2025-12-17)
10
+
11
+ ### Improvements
12
+
13
+ - **cli**: Make CLI output truly minimal and modern
14
+ - Remove all verbose status messages (backup, restore, session recovery)
15
+ - Remove emoji icons (🔧, ✔) for cleaner output
16
+ - Remove "Attached" and "Running" status lines
17
+ - Silent git worktree operations
18
+ - Silent crash recovery on startup
19
+ - Only show header: `flow {version} → {target}`
20
+
21
+ ### ♻️ Refactoring
22
+
23
+ - **cli:** make output truly minimal and modern ([3315e41](https://github.com/SylphxAI/flow/commit/3315e41fa34839d7c866af1ebbe3b6f2d9e3ee71))
24
+
3
25
  ## 2.15.0 (2025-12-17)
4
26
 
5
27
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/flow",
3
- "version": "2.15.0",
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": {
@@ -242,12 +242,12 @@ export async function executeFlowV2(
242
242
  const autoUpgrade = new AutoUpgrade(projectPath);
243
243
  const upgradeResult = await autoUpgrade.runAutoUpgrade(selectedTargetId);
244
244
 
245
- // Show upgrade status (only if something was upgraded)
245
+ // Show upgrade notice (minimal - only if upgraded)
246
246
  if (upgradeResult.flowUpgraded && upgradeResult.flowVersion) {
247
- console.log(chalk.cyan(` ↑ Flow ${upgradeResult.flowVersion.latest} (next run)`));
247
+ console.log(chalk.dim(`↑ flow ${upgradeResult.flowVersion.latest}`));
248
248
  }
249
249
  if (upgradeResult.targetUpgraded && upgradeResult.targetVersion) {
250
- console.log(chalk.cyan(` ↑ ${targetName} ${upgradeResult.targetVersion.latest}`));
250
+ console.log(chalk.dim(`↑ ${targetName.toLowerCase()} ${upgradeResult.targetVersion.latest}`));
251
251
  }
252
252
 
253
253
  // Create executor
@@ -263,15 +263,6 @@ export async function executeFlowV2(
263
263
  merge: options.merge || false,
264
264
  });
265
265
 
266
- // Show attach summary (single line)
267
- if (!attachResult.joined) {
268
- console.log(
269
- chalk.green(
270
- ` ✓ Attached ${attachResult.agents} agents, ${attachResult.commands} commands, ${attachResult.mcp} MCP`
271
- )
272
- );
273
- }
274
-
275
266
  const targetId = selectedTargetId;
276
267
 
277
268
  // Provider selection (Claude Code only, silent unless prompting)
@@ -289,9 +280,6 @@ export async function executeFlowV2(
289
280
  agent = enabledAgents.length > 0 ? enabledAgents[0] : 'coder';
290
281
  }
291
282
 
292
- // Show running agent
293
- console.log(chalk.dim(`\n Running: ${agent}\n`));
294
-
295
283
  // Load agent content
296
284
  const enabledRules = await configService.getEnabledRules();
297
285
  const enabledOutputStyles = await configService.getEnabledOutputStyles();
@@ -7,7 +7,6 @@
7
7
  import { existsSync } from 'node:fs';
8
8
  import fs from 'node:fs/promises';
9
9
  import path from 'node:path';
10
- import ora from 'ora';
11
10
  import type { Target } from '../types/target.types.js';
12
11
  import type { ProjectManager } from './project-manager.js';
13
12
  import { targetManager } from './target-manager.js';
@@ -96,72 +95,63 @@ export class BackupManager {
96
95
  // Ensure backup directory exists
97
96
  await fs.mkdir(backupPath, { recursive: true });
98
97
 
99
- const spinner = ora('Creating backup...').start();
98
+ // Get target config directory
99
+ const targetConfigDir = this.projectManager.getTargetConfigDir(projectPath, target);
100
100
 
101
- try {
102
- // Get target config directory
103
- const targetConfigDir = this.projectManager.getTargetConfigDir(projectPath, target);
104
-
105
- // Backup entire target directory if it exists
106
- if (existsSync(targetConfigDir)) {
107
- // Use configDir from target config (e.g., '.claude', '.opencode')
108
- const backupTargetDir = path.join(backupPath, target.config.configDir);
109
- await this.copyDirectory(targetConfigDir, backupTargetDir);
110
- }
101
+ // Backup entire target directory if it exists
102
+ if (existsSync(targetConfigDir)) {
103
+ // Use configDir from target config (e.g., '.claude', '.opencode')
104
+ const backupTargetDir = path.join(backupPath, target.config.configDir);
105
+ await this.copyDirectory(targetConfigDir, backupTargetDir);
106
+ }
111
107
 
112
- // Create manifest (store target ID as string for JSON serialization)
113
- const manifest: BackupManifest = {
114
- sessionId,
115
- timestamp,
116
- projectPath,
117
- target: targetId,
118
- backup: {
119
- agents: { user: [], flow: [] },
120
- commands: { user: [], flow: [] },
121
- singleFiles: {},
122
- },
123
- secrets: {
124
- mcpEnvExtracted: false,
125
- storedAt: '',
126
- },
127
- };
128
-
129
- await fs.writeFile(path.join(backupPath, 'manifest.json'), JSON.stringify(manifest, null, 2));
130
-
131
- // Create symlink to latest (with fallback for Windows)
132
- const latestLink = paths.latestBackup;
133
- if (existsSync(latestLink)) {
134
- await fs.unlink(latestLink);
135
- }
136
- try {
137
- await fs.symlink(sessionId, latestLink);
138
- } catch (symlinkError: unknown) {
139
- // Windows without admin/Developer Mode can't create symlinks
140
- // Fall back to writing session ID to a file
141
- if (
142
- symlinkError instanceof Error &&
143
- 'code' in symlinkError &&
144
- symlinkError.code === 'EPERM'
145
- ) {
146
- await fs.writeFile(latestLink, sessionId, 'utf-8');
147
- } else {
148
- throw symlinkError;
149
- }
150
- }
108
+ // Create manifest (store target ID as string for JSON serialization)
109
+ const manifest: BackupManifest = {
110
+ sessionId,
111
+ timestamp,
112
+ projectPath,
113
+ target: targetId,
114
+ backup: {
115
+ agents: { user: [], flow: [] },
116
+ commands: { user: [], flow: [] },
117
+ singleFiles: {},
118
+ },
119
+ secrets: {
120
+ mcpEnvExtracted: false,
121
+ storedAt: '',
122
+ },
123
+ };
124
+
125
+ await fs.writeFile(path.join(backupPath, 'manifest.json'), JSON.stringify(manifest, null, 2));
151
126
 
152
- spinner.succeed(`Backup created: ${sessionId}`);
153
-
154
- return {
155
- sessionId,
156
- timestamp,
157
- projectPath,
158
- target: targetId,
159
- backupPath,
160
- };
161
- } catch (error) {
162
- spinner.fail('Backup failed');
163
- throw error;
127
+ // Create symlink to latest (with fallback for Windows)
128
+ const latestLink = paths.latestBackup;
129
+ if (existsSync(latestLink)) {
130
+ await fs.unlink(latestLink);
164
131
  }
132
+ try {
133
+ await fs.symlink(sessionId, latestLink);
134
+ } catch (symlinkError: unknown) {
135
+ // Windows without admin/Developer Mode can't create symlinks
136
+ // Fall back to writing session ID to a file
137
+ if (
138
+ symlinkError instanceof Error &&
139
+ 'code' in symlinkError &&
140
+ symlinkError.code === 'EPERM'
141
+ ) {
142
+ await fs.writeFile(latestLink, sessionId, 'utf-8');
143
+ } else {
144
+ throw symlinkError;
145
+ }
146
+ }
147
+
148
+ return {
149
+ sessionId,
150
+ timestamp,
151
+ projectPath,
152
+ target: targetId,
153
+ backupPath,
154
+ };
165
155
  }
166
156
 
167
157
  /**
@@ -175,38 +165,29 @@ export class BackupManager {
175
165
  throw new Error(`Backup not found: ${sessionId}`);
176
166
  }
177
167
 
178
- const spinner = ora('Restoring backup...').start();
179
-
180
- try {
181
- // Read manifest
182
- const manifestPath = path.join(backupPath, 'manifest.json');
183
- const manifest: BackupManifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
184
-
185
- const projectPath = manifest.projectPath;
186
- const targetId = manifest.target;
168
+ // Read manifest
169
+ const manifestPath = path.join(backupPath, 'manifest.json');
170
+ const manifest: BackupManifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
187
171
 
188
- // Resolve target to get config
189
- const target = this.resolveTarget(targetId);
172
+ const projectPath = manifest.projectPath;
173
+ const targetId = manifest.target;
190
174
 
191
- // Get target config directory
192
- const targetConfigDir = this.projectManager.getTargetConfigDir(projectPath, target);
175
+ // Resolve target to get config
176
+ const target = this.resolveTarget(targetId);
193
177
 
194
- // Remove current target directory
195
- if (existsSync(targetConfigDir)) {
196
- await fs.rm(targetConfigDir, { recursive: true, force: true });
197
- }
178
+ // Get target config directory
179
+ const targetConfigDir = this.projectManager.getTargetConfigDir(projectPath, target);
198
180
 
199
- // Restore from backup using target config's configDir
200
- const backupTargetDir = path.join(backupPath, target.config.configDir);
181
+ // Remove current target directory
182
+ if (existsSync(targetConfigDir)) {
183
+ await fs.rm(targetConfigDir, { recursive: true, force: true });
184
+ }
201
185
 
202
- if (existsSync(backupTargetDir)) {
203
- await this.copyDirectory(backupTargetDir, targetConfigDir);
204
- }
186
+ // Restore from backup using target config's configDir
187
+ const backupTargetDir = path.join(backupPath, target.config.configDir);
205
188
 
206
- spinner.succeed('Backup restored');
207
- } catch (error) {
208
- spinner.fail('Restore failed');
209
- throw error;
189
+ if (existsSync(backupTargetDir)) {
190
+ await this.copyDirectory(backupTargetDir, targetConfigDir);
210
191
  }
211
192
  }
212
193
 
@@ -4,7 +4,6 @@
4
4
  * Handles process signals and ensures backup restoration
5
5
  */
6
6
 
7
- import chalk from 'chalk';
8
7
  import type { BackupManager } from './backup-manager.js';
9
8
  import type { ProjectManager } from './project-manager.js';
10
9
  import type { SessionManager } from './session-manager.js';
@@ -43,30 +42,26 @@ export class CleanupHandler {
43
42
 
44
43
  // SIGINT (Ctrl+C)
45
44
  process.on('SIGINT', async () => {
46
- console.log(chalk.yellow('\n⚠️ Interrupted by user, cleaning up...'));
47
45
  await this.onSignal('SIGINT');
48
46
  process.exit(0);
49
47
  });
50
48
 
51
49
  // SIGTERM
52
50
  process.on('SIGTERM', async () => {
53
- console.log(chalk.yellow('\n⚠️ Terminated, cleaning up...'));
54
51
  await this.onSignal('SIGTERM');
55
52
  process.exit(0);
56
53
  });
57
54
 
58
55
  // Uncaught exceptions
59
56
  process.on('uncaughtException', async (error) => {
60
- console.error(chalk.red('\n✗ Uncaught Exception:'));
61
- console.error(error);
57
+ console.error('\nUncaught Exception:', error);
62
58
  await this.onSignal('uncaughtException');
63
59
  process.exit(1);
64
60
  });
65
61
 
66
62
  // Unhandled rejections
67
63
  process.on('unhandledRejection', async (reason) => {
68
- console.error(chalk.red('\n✗ Unhandled Rejection:'));
69
- console.error(reason);
64
+ console.error('\nUnhandled Rejection:', reason);
70
65
  await this.onSignal('unhandledRejection');
71
66
  process.exit(1);
72
67
  });
@@ -97,6 +92,7 @@ export class CleanupHandler {
97
92
 
98
93
  /**
99
94
  * Signal-based cleanup (SIGINT, SIGTERM, etc.) with multi-session support
95
+ * Silent operation - no console output
100
96
  */
101
97
  private async onSignal(_signal: string): Promise<void> {
102
98
  if (!this.currentProjectHash) {
@@ -104,29 +100,22 @@ export class CleanupHandler {
104
100
  }
105
101
 
106
102
  try {
107
- console.log(chalk.cyan('🧹 Cleaning up...'));
108
-
109
103
  const { shouldRestore, session } = await this.sessionManager.endSession(
110
104
  this.currentProjectHash
111
105
  );
112
106
 
113
107
  if (shouldRestore && session) {
114
- // Last session - restore environment
115
- console.log(chalk.cyan(' Restoring environment...'));
116
108
  await this.backupManager.restoreBackup(this.currentProjectHash, session.sessionId);
117
- console.log(chalk.green('✓ Environment restored'));
118
- } else if (!shouldRestore && session) {
119
- // Other sessions still running
120
- console.log(chalk.yellow(` ${session.refCount} session(s) still running`));
121
109
  }
122
- } catch (error) {
123
- console.error(chalk.red('✗ Cleanup failed:'), error);
110
+ } catch (_error) {
111
+ // Silent fail
124
112
  }
125
113
  }
126
114
 
127
115
  /**
128
116
  * Recover on startup (for all projects)
129
117
  * Checks for orphaned sessions from crashes
118
+ * Silent operation - no console output
130
119
  */
131
120
  async recoverOnStartup(): Promise<void> {
132
121
  const orphanedSessions = await this.sessionManager.detectOrphanedSessions();
@@ -135,21 +124,12 @@ export class CleanupHandler {
135
124
  return;
136
125
  }
137
126
 
138
- console.log(chalk.cyan(`\n🔧 Recovering ${orphanedSessions.size} crashed session(s)...\n`));
139
-
140
127
  for (const [projectHash, session] of orphanedSessions) {
141
- console.log(chalk.dim(` Project: ${session.projectPath}`));
142
-
143
128
  try {
144
- // Restore backup
145
129
  await this.backupManager.restoreBackup(projectHash, session.sessionId);
146
-
147
- // Clean up session
148
130
  await this.sessionManager.recoverSession(projectHash, session);
149
-
150
- console.log(chalk.green(' Environment restored\n'));
151
- } catch (error) {
152
- console.error(chalk.red(' ✗ Recovery failed:'), error);
131
+ } catch (_error) {
132
+ // Silent fail - don't interrupt startup
153
133
  }
154
134
  }
155
135
  }
@@ -8,7 +8,6 @@ import { exec } from 'node:child_process';
8
8
  import { existsSync } from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { promisify } from 'node:util';
11
- import chalk from 'chalk';
12
11
 
13
12
  const execAsync = promisify(exec);
14
13
 
@@ -93,14 +92,8 @@ export class GitStashManager {
93
92
  // File might not exist or not tracked, skip it
94
93
  }
95
94
  }
96
-
97
- if (this.skipWorktreeFiles.length > 0) {
98
- console.log(
99
- chalk.dim(` ✓ Hiding ${this.skipWorktreeFiles.length} settings file(s) from git\n`)
100
- );
101
- }
102
95
  } catch (_error) {
103
- console.log(chalk.yellow(' ⚠️ Could not hide settings from git\n'));
96
+ // Silent fail
104
97
  }
105
98
  }
106
99
 
@@ -123,15 +116,9 @@ export class GitStashManager {
123
116
  }
124
117
  }
125
118
 
126
- console.log(
127
- chalk.dim(` ✓ Restored git tracking for ${this.skipWorktreeFiles.length} file(s)\n`)
128
- );
129
119
  this.skipWorktreeFiles = [];
130
- } catch {
131
- console.log(chalk.yellow(' ⚠️ Could not restore git tracking'));
132
- console.log(
133
- chalk.yellow(' Run manually: git update-index --no-skip-worktree .claude/* .opencode/*\n')
134
- );
120
+ } catch (_error) {
121
+ // Silent fail
135
122
  }
136
123
  }
137
124
 
@@ -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
  /**
@@ -59,8 +59,7 @@ async function loadRules(ruleNames?: string[]): Promise<string> {
59
59
  const stripped = await yamlUtils.stripFrontMatter(content);
60
60
  sections.push(stripped);
61
61
  } catch (_error) {
62
- // Log warning if rule file not found, but continue with other rules
63
- console.warn(`Warning: Rule file not found: ${ruleName}.md`);
62
+ // Silent - rule file not found, continue with other rules
64
63
  }
65
64
  }
66
65
 
@@ -89,7 +88,7 @@ async function loadOutputStyles(styleNames?: string[]): Promise<string> {
89
88
  const stripped = await yamlUtils.stripFrontMatter(content);
90
89
  sections.push(stripped);
91
90
  } catch (_error) {
92
- console.warn(`Warning: Output style file not found: ${styleName}.md`);
91
+ // Silent - output style file not found, continue
93
92
  }
94
93
  }
95
94
  } else {