@sylphx/flow 2.14.0 → 2.15.0

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,45 @@
1
1
  # @sylphx/flow
2
2
 
3
+ ## 2.15.0 (2025-12-17)
4
+
5
+ ### Features
6
+
7
+ - **cli**: Redesign CLI output with modern, minimalist interface
8
+ - Single-line header: `flow {version} → {target}`
9
+ - Consolidated status: `✓ Attached {n} agents, {n} commands, {n} MCP`
10
+ - Silent operations by default (backup, cleanup, provider selection)
11
+ - Upgrade notification: `↑ Flow {version} (next run)`
12
+
13
+ ### Bug Fixes
14
+
15
+ - **auto-upgrade**: Remove auto-restart after Flow upgrade - new version used on next run instead
16
+
17
+ ### ✨ Features
18
+
19
+ - **cli:** redesign output with modern minimalist interface ([654146d](https://github.com/SylphxAI/flow/commit/654146d3ba7a635b39a599125300e60cfdba2bec))
20
+
21
+ ### 🐛 Bug Fixes
22
+
23
+ - **auto-upgrade:** don't restart after Flow upgrade ([8207417](https://github.com/SylphxAI/flow/commit/8207417921dc60357e97a2ed8db7d6d79f0b5a3e))
24
+
25
+ ### 🔧 Chores
26
+
27
+ - fix package.json formatting ([1e9d940](https://github.com/SylphxAI/flow/commit/1e9d94086e7186fa2a3e80c8f6c98483840d865d))
28
+
29
+ ## 2.14.1 (2025-12-17)
30
+
31
+ ### Documentation
32
+
33
+ - Add bundle constraints guidance for next-intl in review-i18n command
34
+
35
+ ### 📚 Documentation
36
+
37
+ - **review-i18n:** add bundle constraints for next-intl ([c16d6c0](https://github.com/SylphxAI/flow/commit/c16d6c03284d2215d0e29cb98500d71f04fa01a7))
38
+
39
+ ### 🔧 Chores
40
+
41
+ - trigger release workflow ([ee60f39](https://github.com/SylphxAI/flow/commit/ee60f3967f07a576f79ffe926770fb36414d78c7))
42
+
3
43
  ## 2.14.0 (2025-12-17)
4
44
 
5
45
  ### ✨ Features
@@ -24,6 +24,7 @@ agent: coder
24
24
  * `/en/*` must not exist (permanently redirect to non-prefixed)
25
25
  * Missing translation keys must fail build
26
26
  * No hardcoded user-facing strings outside localization
27
+ * Translation bundles must be split by namespace or route (no monolithic language files)
27
28
 
28
29
  ## Context
29
30
 
@@ -39,3 +40,11 @@ Consider: dates, numbers, currency, pluralization, text direction, cultural norm
39
40
  * How painful is the translation workflow for adding new strings?
40
41
  * What locales are we missing that represent real market opportunity?
41
42
  * Where do we fall back to English in ways users would notice?
43
+ * How large are the translation bundles, and what's being sent to the client?
44
+
45
+ ## Bundle Constraints
46
+
47
+ * No monolithic language files — split by namespace (`common`, `auth`, `dashboard`, etc.)
48
+ * Server Components for translations wherever possible — client bundles must not include translations that could stay on server
49
+ * Each route should load only its required namespaces
50
+ * Measure client bundle size impact of translations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/flow",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
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,8 +1,11 @@
1
1
  /**
2
2
  * Execution Logic for Flow Command (V2 - Attach Mode)
3
- * New execution flow with attach-mode lifecycle
3
+ * Minimal, modern CLI output design
4
4
  */
5
5
 
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
6
9
  import chalk from 'chalk';
7
10
  import inquirer from 'inquirer';
8
11
  import { FlowExecutor } from '../../core/flow-executor.js';
@@ -12,13 +15,29 @@ import { GlobalConfigService } from '../../services/global-config.js';
12
15
  import { TargetInstaller } from '../../services/target-installer.js';
13
16
  import type { RunCommandOptions } from '../../types.js';
14
17
  import { extractAgentInstructions, loadAgentContent } from '../../utils/agent-enhancer.js';
15
- import { showWelcome } from '../../utils/display/banner.js';
18
+ import { showHeader } from '../../utils/display/banner.js';
16
19
  import { CLIError } from '../../utils/error-handler.js';
17
20
  import { UserCancelledError } from '../../utils/errors.js';
18
21
  import { ensureTargetInstalled, promptForTargetSelection } from '../../utils/target-selection.js';
19
22
  import { resolvePrompt } from './prompt.js';
20
23
  import type { FlowOptions } from './types.js';
21
24
 
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+
28
+ /**
29
+ * Get Flow version from package.json
30
+ */
31
+ async function getFlowVersion(): Promise<string> {
32
+ try {
33
+ const packageJsonPath = path.join(__dirname, '..', '..', '..', 'package.json');
34
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
35
+ return packageJson.version;
36
+ } catch {
37
+ return 'unknown';
38
+ }
39
+ }
40
+
22
41
  /**
23
42
  * Configure provider environment variables
24
43
  */
@@ -40,14 +59,14 @@ function configureProviderEnv(provider: 'kimi' | 'zai', apiKey: string): void {
40
59
  }
41
60
 
42
61
  /**
43
- * Select and configure provider for Claude Code
62
+ * Select and configure provider for Claude Code (silent unless prompting)
44
63
  */
45
64
  async function selectProvider(configService: GlobalConfigService): Promise<void> {
46
65
  try {
47
66
  const providerConfig = await configService.loadProviderConfig();
48
67
  const defaultProvider = providerConfig.claudeCode.defaultProvider;
49
68
 
50
- // If not "ask-every-time", use the default provider
69
+ // If not "ask-every-time", use the default provider silently
51
70
  if (defaultProvider !== 'ask-every-time') {
52
71
  if (defaultProvider === 'kimi' || defaultProvider === 'zai') {
53
72
  const provider = providerConfig.claudeCode.providers[defaultProvider];
@@ -63,7 +82,7 @@ async function selectProvider(configService: GlobalConfigService): Promise<void>
63
82
  {
64
83
  type: 'list',
65
84
  name: 'selectedProvider',
66
- message: 'Select provider for this session:',
85
+ message: 'Select provider:',
67
86
  choices: [
68
87
  { name: 'Default (Claude Code built-in)', value: 'default' },
69
88
  { name: 'Kimi', value: 'kimi' },
@@ -83,7 +102,6 @@ async function selectProvider(configService: GlobalConfigService): Promise<void>
83
102
  if (rememberChoice) {
84
103
  providerConfig.claudeCode.defaultProvider = selectedProvider;
85
104
  await configService.saveProviderConfig(providerConfig);
86
- console.log(chalk.dim(' (Saved to settings)\n'));
87
105
  }
88
106
 
89
107
  // Configure environment variables based on selection
@@ -91,16 +109,11 @@ async function selectProvider(configService: GlobalConfigService): Promise<void>
91
109
  const provider = providerConfig.claudeCode.providers[selectedProvider];
92
110
 
93
111
  if (!provider?.apiKey) {
94
- console.log(chalk.yellow('API key not configured. Use: sylphx-flow settings\n'));
112
+ console.log(chalk.yellow(' API key not configured. Use: sylphx-flow settings'));
95
113
  return;
96
114
  }
97
115
 
98
116
  configureProviderEnv(selectedProvider, provider.apiKey);
99
-
100
- const providerName = selectedProvider === 'kimi' ? 'Kimi' : 'Z.ai';
101
- console.log(chalk.green(`✓ Using ${providerName} provider\n`));
102
- } else {
103
- console.log(chalk.green('✓ Using default Claude Code provider\n'));
104
117
  }
105
118
  } catch (error: unknown) {
106
119
  // Handle user cancellation (Ctrl+C)
@@ -140,7 +153,7 @@ function executeTargetCommand(
140
153
  }
141
154
 
142
155
  /**
143
- * Main flow execution with attach mode (V2)
156
+ * Main flow execution with attach mode (V2) - Minimal output design
144
157
  */
145
158
  export async function executeFlowV2(
146
159
  prompt: string | undefined,
@@ -148,39 +161,29 @@ export async function executeFlowV2(
148
161
  ): Promise<void> {
149
162
  const projectPath = process.cwd();
150
163
 
151
- // Show welcome banner
152
- showWelcome();
153
-
154
164
  // Initialize config service early to check for saved preferences
155
165
  const configService = new GlobalConfigService();
156
166
  await configService.initialize();
157
167
 
158
- // Step 1: Determine target
168
+ // Step 1: Determine target (silent auto-detect, only prompt when necessary)
159
169
  const targetInstaller = new TargetInstaller(projectPath);
160
170
  const installedTargets = await targetInstaller.detectInstalledTargets();
161
171
  const settings = await configService.loadSettings();
162
172
 
163
173
  let selectedTargetId: string | null = null;
164
174
 
165
- // Distinguish between three cases:
166
- // 1. User explicitly set "ask-every-time" → always prompt
167
- // 2. User has no setting (undefined/null) → allow auto-detect
168
- // 3. User has specific target → use that target
169
175
  const isAskEveryTime = settings.defaultTarget === 'ask-every-time';
170
176
  const hasNoSetting = !settings.defaultTarget;
171
177
  const hasSpecificTarget = settings.defaultTarget && settings.defaultTarget !== 'ask-every-time';
172
178
 
173
179
  if (isAskEveryTime) {
174
- // User explicitly wants to be asked every time - ALWAYS prompt, never auto-detect
175
- console.log(chalk.cyan('🔍 Detecting installed AI CLIs...\n'));
176
-
180
+ // User explicitly wants to be asked every time
177
181
  selectedTargetId = await promptForTargetSelection(
178
182
  installedTargets,
179
- 'Select AI CLI to use:',
183
+ 'Select AI CLI:',
180
184
  'execution'
181
185
  );
182
186
 
183
- const installation = targetInstaller.getInstallationInfo(selectedTargetId);
184
187
  const installed = await ensureTargetInstalled(
185
188
  selectedTargetId,
186
189
  targetInstaller,
@@ -190,28 +193,17 @@ export async function executeFlowV2(
190
193
  if (!installed) {
191
194
  process.exit(1);
192
195
  }
193
-
194
- if (installedTargets.includes(selectedTargetId)) {
195
- console.log(chalk.green(`✓ Using ${installation?.name}\n`));
196
- }
197
196
  } else if (hasNoSetting) {
198
- // No setting - use auto-detection (smart default behavior)
197
+ // No setting - use auto-detection
199
198
  if (installedTargets.length === 1) {
200
- // Exactly 1 target found - use it automatically
201
199
  selectedTargetId = installedTargets[0];
202
- const installation = targetInstaller.getInstallationInfo(selectedTargetId);
203
- console.log(chalk.green(`✓ Using ${installation?.name} (auto-detected)\n`));
204
200
  } else {
205
- // 0 or multiple targets - prompt for selection
206
- console.log(chalk.cyan('🔍 Detecting installed AI CLIs...\n'));
207
-
208
201
  selectedTargetId = await promptForTargetSelection(
209
202
  installedTargets,
210
- 'Select AI CLI to use:',
203
+ 'Select AI CLI:',
211
204
  'execution'
212
205
  );
213
206
 
214
- const installation = targetInstaller.getInstallationInfo(selectedTargetId);
215
207
  const installed = await ensureTargetInstalled(
216
208
  selectedTargetId,
217
209
  targetInstaller,
@@ -221,110 +213,89 @@ export async function executeFlowV2(
221
213
  if (!installed) {
222
214
  process.exit(1);
223
215
  }
224
-
225
- if (installedTargets.includes(selectedTargetId)) {
226
- console.log(chalk.green(`✓ Using ${installation?.name}\n`));
227
- }
228
216
  }
229
217
  } else if (hasSpecificTarget) {
230
- // User has a specific target preference - ALWAYS use it
218
+ // User has a specific target preference
231
219
  selectedTargetId = settings.defaultTarget;
232
- const installation = targetInstaller.getInstallationInfo(selectedTargetId);
233
220
 
234
- // Check if the preferred target is installed
235
- if (installedTargets.includes(selectedTargetId)) {
236
- console.log(chalk.green(`✓ Using ${installation?.name} (from settings)\n`));
237
- } else {
238
- // Preferred target not installed - try to install it
239
- console.log(chalk.yellow(`⚠️ ${installation?.name} is set as default but not installed\n`));
221
+ if (!installedTargets.includes(selectedTargetId)) {
222
+ const installation = targetInstaller.getInstallationInfo(selectedTargetId);
223
+ console.log(chalk.yellow(`\n ${installation?.name} not installed`));
240
224
  const installed = await targetInstaller.install(selectedTargetId, true);
241
225
 
242
226
  if (!installed) {
243
- // Installation failed - show error and exit
244
- console.log(
245
- chalk.red(
246
- `\n✗ Cannot proceed: ${installation?.name} is not installed and auto-install failed`
247
- )
248
- );
249
- console.log(chalk.yellow(' Please either:'));
250
- console.log(chalk.cyan(' 1. Install manually (see instructions above)'));
251
- console.log(chalk.cyan(' 2. Change default target: sylphx-flow settings\n'));
227
+ console.log(chalk.red(` Cannot proceed: installation failed\n`));
252
228
  process.exit(1);
253
229
  }
254
-
255
- console.log();
256
230
  }
257
231
  }
258
232
 
259
- // Step 2: Auto-upgrade Flow and target CLI
233
+ // Get version and target name for header
234
+ const version = await getFlowVersion();
235
+ const targetInstallation = targetInstaller.getInstallationInfo(selectedTargetId);
236
+ const targetName = targetInstallation?.name || selectedTargetId;
237
+
238
+ // Show minimal header
239
+ showHeader(version, targetName);
240
+
241
+ // Step 2: Auto-upgrade (silent, returns status)
260
242
  const autoUpgrade = new AutoUpgrade(projectPath);
261
- await autoUpgrade.runAutoUpgrade(selectedTargetId);
243
+ const upgradeResult = await autoUpgrade.runAutoUpgrade(selectedTargetId);
262
244
 
263
- // Mode info
264
- if (options.merge) {
265
- console.log(
266
- chalk.cyan('🔗 Merge mode: Flow settings will be merged with your existing settings')
267
- );
268
- console.log(chalk.dim(' Settings will be restored after execution\n'));
269
- } else {
270
- console.log(
271
- chalk.yellow('🔄 Replace mode (default): All settings will use Flow configuration')
272
- );
273
- console.log(chalk.dim(' Use --merge to keep your existing settings\n'));
245
+ // Show upgrade status (only if something was upgraded)
246
+ if (upgradeResult.flowUpgraded && upgradeResult.flowVersion) {
247
+ console.log(chalk.cyan(` ↑ Flow ${upgradeResult.flowVersion.latest} (next run)`));
248
+ }
249
+ if (upgradeResult.targetUpgraded && upgradeResult.targetVersion) {
250
+ console.log(chalk.cyan(` ↑ ${targetName} ${upgradeResult.targetVersion.latest}`));
274
251
  }
275
252
 
276
253
  // Create executor
277
254
  const executor = new FlowExecutor();
278
- const _projectManager = executor.getProjectManager();
279
255
 
280
- // Step 2: Execute attach mode lifecycle
256
+ // Step 3: Execute attach mode lifecycle
281
257
  try {
282
258
  // Attach Flow environment (backup → attach → register cleanup)
283
- await executor.execute(projectPath, {
259
+ const attachResult = await executor.execute(projectPath, {
284
260
  verbose: options.verbose,
285
261
  skipBackup: false,
286
262
  skipSecrets: false,
287
263
  merge: options.merge || false,
288
264
  });
289
265
 
290
- // Step 3: Use the target we already selected (don't re-detect)
291
- // selectedTargetId was determined earlier based on settings/auto-detect/prompt
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
+
292
275
  const targetId = selectedTargetId;
293
276
 
294
- // Step 3.5: Provider selection (Claude Code only)
277
+ // Provider selection (Claude Code only, silent unless prompting)
295
278
  if (targetId === 'claude-code') {
296
279
  await selectProvider(configService);
297
280
  }
298
281
 
299
- // Step 3.6: Load Flow settings and determine agent to use
300
- const settings = await configService.loadSettings();
282
+ // Determine which agent to use
301
283
  const flowConfig = await configService.loadFlowConfig();
284
+ let agent = options.agent || settings.defaultAgent || 'coder';
302
285
 
303
- // Determine which agent to use (CLI option > settings default > 'coder')
304
- const agent = options.agent || settings.defaultAgent || 'coder';
305
-
306
- // Check if agent is enabled
286
+ // Check if agent is enabled (silent fallback)
307
287
  if (!flowConfig.agents[agent]?.enabled) {
308
- console.log(chalk.yellow(`⚠️ Agent '${agent}' is not enabled in settings`));
309
- console.log(chalk.yellow(` Enable it with: sylphx-flow settings`));
310
- console.log(chalk.yellow(` Using 'coder' agent instead\n`));
311
- // Fallback to first enabled agent or coder
312
288
  const enabledAgents = await configService.getEnabledAgents();
313
- const fallbackAgent = enabledAgents.length > 0 ? enabledAgents[0] : 'coder';
314
- options.agent = fallbackAgent;
289
+ agent = enabledAgents.length > 0 ? enabledAgents[0] : 'coder';
315
290
  }
316
291
 
317
- console.log(chalk.cyan(`🤖 Running agent: ${agent}\n`));
292
+ // Show running agent
293
+ console.log(chalk.dim(`\n Running: ${agent}\n`));
318
294
 
319
- // Load enabled rules and output styles from config
295
+ // Load agent content
320
296
  const enabledRules = await configService.getEnabledRules();
321
297
  const enabledOutputStyles = await configService.getEnabledOutputStyles();
322
298
 
323
- console.log(chalk.dim(` Enabled rules: ${enabledRules.join(', ')}`));
324
- console.log(chalk.dim(` Enabled output styles: ${enabledOutputStyles.join(', ')}\n`));
325
-
326
- // Load agent content with enabled rules and output styles
327
- // Rules are filtered: intersection of agent's frontmatter rules and globally enabled rules
328
299
  const agentContent = await loadAgentContent(
329
300
  agent,
330
301
  options.agentFile,
@@ -334,7 +305,6 @@ export async function executeFlowV2(
334
305
  const agentInstructions = extractAgentInstructions(agentContent);
335
306
 
336
307
  const systemPrompt = `AGENT INSTRUCTIONS:\n${agentInstructions}`;
337
-
338
308
  const userPrompt = prompt?.trim() || '';
339
309
 
340
310
  // Prepare run options
@@ -352,30 +322,27 @@ export async function executeFlowV2(
352
322
  // Step 4: Execute command
353
323
  await executeTargetCommand(targetId, systemPrompt, userPrompt, runOptions);
354
324
 
355
- // Step 5: Cleanup (restore environment)
325
+ // Step 5: Cleanup (silent)
356
326
  await executor.cleanup(projectPath);
357
-
358
- console.log(chalk.green('✓ Session complete\n'));
359
327
  } catch (error) {
360
328
  // Handle user cancellation gracefully
361
329
  if (error instanceof UserCancelledError) {
362
- console.log(chalk.yellow('\n⚠️ Operation cancelled by user'));
330
+ console.log(chalk.yellow('\n Cancelled'));
363
331
  try {
364
332
  await executor.cleanup(projectPath);
365
- console.log(chalk.green(' ✓ Settings restored\n'));
366
- } catch (cleanupError) {
367
- console.error(chalk.red(' ✗ Cleanup failed:'), cleanupError);
333
+ } catch {
334
+ // Silent cleanup failure
368
335
  }
369
336
  process.exit(0);
370
337
  }
371
338
 
372
- console.error(chalk.red.bold('\n✗ Execution failed:'), error);
339
+ console.error(chalk.red('\n Error:'), error);
373
340
 
374
341
  // Ensure cleanup even on error
375
342
  try {
376
343
  await executor.cleanup(projectPath);
377
- } catch (cleanupError) {
378
- console.error(chalk.red('✗ Cleanup failed:'), cleanupError);
344
+ } catch {
345
+ // Silent cleanup failure
379
346
  }
380
347
 
381
348
  throw error;
@@ -8,7 +8,6 @@ import { createHash } from 'node:crypto';
8
8
  import { existsSync } from 'node:fs';
9
9
  import fs from 'node:fs/promises';
10
10
  import path from 'node:path';
11
- import chalk from 'chalk';
12
11
  import { MCP_SERVER_REGISTRY, type MCPServerID } from '../config/servers.js';
13
12
  import { GlobalConfigService } from '../services/global-config.js';
14
13
  import type { Target } from '../types/target.types.js';
@@ -184,9 +183,6 @@ export class AttachManager {
184
183
  await this.attachSingleFiles(projectPath, templates.singleFiles, result, manifest);
185
184
  }
186
185
 
187
- // Show conflict warnings
188
- this.showConflictWarnings(result);
189
-
190
186
  return result;
191
187
  }
192
188
 
@@ -421,21 +417,4 @@ export class AttachManager {
421
417
  result.singleFilesMerged.push(file.path);
422
418
  }
423
419
  }
424
-
425
- /**
426
- * Show conflict warnings to user
427
- */
428
- private showConflictWarnings(result: AttachResult): void {
429
- if (result.conflicts.length === 0) {
430
- return;
431
- }
432
-
433
- console.log(chalk.yellow('\n⚠️ Conflicts detected:\n'));
434
-
435
- for (const conflict of result.conflicts) {
436
- console.log(chalk.yellow(` • ${conflict.type}: ${conflict.name} - ${conflict.action}`));
437
- }
438
-
439
- console.log(chalk.dim("\n Don't worry! All overridden content will be restored on exit.\n"));
440
- }
441
420
  }
@@ -6,7 +6,7 @@
6
6
 
7
7
  import chalk from 'chalk';
8
8
  import type { Target } from '../types/target.types.js';
9
- import { AttachManager, type AttachResult } from './attach-manager.js';
9
+ import { AttachManager } from './attach-manager.js';
10
10
  import { BackupManager } from './backup-manager.js';
11
11
  import { CleanupHandler } from './cleanup-handler.js';
12
12
  import { GitStashManager } from './git-stash-manager.js';
@@ -50,8 +50,12 @@ export class FlowExecutor {
50
50
 
51
51
  /**
52
52
  * Execute complete flow with attach mode (with multi-session support)
53
+ * Returns summary for caller to display
53
54
  */
54
- async execute(projectPath: string, options: FlowExecutorOptions = {}): Promise<void> {
55
+ async execute(
56
+ projectPath: string,
57
+ options: FlowExecutorOptions = {}
58
+ ): Promise<{ joined: boolean; agents?: number; commands?: number; mcp?: number }> {
55
59
  // Initialize Flow directories
56
60
  await this.projectManager.initialize();
57
61
 
@@ -72,44 +76,30 @@ export class FlowExecutor {
72
76
  const existingSession = await this.sessionManager.getActiveSession(projectHash);
73
77
 
74
78
  if (existingSession) {
75
- // Joining existing session
76
- console.log(chalk.cyan('🔗 Joining existing session...'));
77
-
78
- const { session } = await this.sessionManager.startSession(
79
+ // Joining existing session - silent
80
+ await this.sessionManager.startSession(
79
81
  projectPath,
80
82
  projectHash,
81
83
  target,
82
84
  existingSession.backupPath
83
85
  );
84
-
85
- // Register cleanup hooks
86
86
  this.cleanupHandler.registerCleanupHooks(projectHash);
87
-
88
- console.log(chalk.green(` ✓ Joined session (${session.refCount} active session(s))\n`));
89
- console.log(chalk.green('✓ Flow environment ready!\n'));
90
- return;
87
+ return { joined: true };
91
88
  }
92
89
 
93
- // First session - stash settings changes, then create backup and attach
94
- // Step 3: Stash git changes to hide Flow's modifications from git status
95
- console.log(chalk.cyan('🔍 Checking git status...'));
90
+ // First session - stash, backup, attach (all silent)
96
91
  await this.gitStashManager.stashSettingsChanges(projectPath);
97
-
98
- console.log(chalk.cyan('💾 Creating backup...'));
99
92
  const backup = await this.backupManager.createBackup(projectPath, projectHash, target);
100
93
 
101
- // Step 4: Extract and save secrets
94
+ // Extract and save secrets (silent)
102
95
  if (!options.skipSecrets) {
103
- console.log(chalk.cyan('🔐 Extracting secrets...'));
104
96
  const secrets = await this.secretsManager.extractMCPSecrets(projectPath, projectHash, target);
105
-
106
97
  if (Object.keys(secrets.servers).length > 0) {
107
98
  await this.secretsManager.saveSecrets(projectHash, secrets);
108
- console.log(chalk.green(` ✓ Saved ${Object.keys(secrets.servers).length} MCP secret(s)`));
109
99
  }
110
100
  }
111
101
 
112
- // Step 5: Start session (use backup's sessionId to ensure consistency)
102
+ // Start session
113
103
  const { session } = await this.sessionManager.startSession(
114
104
  projectPath,
115
105
  projectHash,
@@ -118,21 +108,14 @@ export class FlowExecutor {
118
108
  backup.sessionId
119
109
  );
120
110
 
121
- // Step 6: Register cleanup hooks
122
111
  this.cleanupHandler.registerCleanupHooks(projectHash);
123
112
 
124
- // Step 7: Default replace mode - clear user files before attaching (unless merge flag is set)
113
+ // Clear and attach (silent)
125
114
  if (!options.merge) {
126
- console.log(chalk.cyan('🔄 Clearing existing settings...'));
127
115
  await this.clearUserSettings(projectPath, target);
128
116
  }
129
117
 
130
- // Step 8: Load templates
131
- console.log(chalk.cyan('📦 Loading Flow templates...'));
132
118
  const templates = await this.templateLoader.loadTemplates(target);
133
-
134
- // Step 9: Attach Flow environment
135
- console.log(chalk.cyan('🚀 Attaching Flow environment...'));
136
119
  const manifest = await this.backupManager.getManifest(projectHash, session.sessionId);
137
120
 
138
121
  if (!manifest) {
@@ -147,13 +130,15 @@ export class FlowExecutor {
147
130
  manifest
148
131
  );
149
132
 
150
- // Update manifest with attach results
151
133
  await this.backupManager.updateManifest(projectHash, session.sessionId, manifest);
152
134
 
153
- // Show summary
154
- this.showAttachSummary(attachResult);
155
-
156
- console.log(chalk.green('\n✓ Flow environment ready!\n'));
135
+ // Return summary for caller to display
136
+ return {
137
+ joined: false,
138
+ agents: attachResult.agentsAdded.length,
139
+ commands: attachResult.commandsAdded.length,
140
+ mcp: attachResult.mcpServersAdded.length,
141
+ };
157
142
  }
158
143
 
159
144
  /**
@@ -260,65 +245,12 @@ export class FlowExecutor {
260
245
  }
261
246
 
262
247
  /**
263
- * Cleanup after execution
248
+ * Cleanup after execution (silent)
264
249
  */
265
250
  async cleanup(projectPath: string): Promise<void> {
266
251
  const projectHash = this.projectManager.getProjectHash(projectPath);
267
-
268
- console.log(chalk.cyan('\n🧹 Cleaning up...'));
269
-
270
252
  await this.cleanupHandler.cleanup(projectHash);
271
-
272
- // Restore stashed git changes
273
253
  await this.gitStashManager.popSettingsChanges(projectPath);
274
-
275
- console.log(chalk.green(' ✓ Environment restored'));
276
- console.log(chalk.green(' ✓ Secrets preserved for next run\n'));
277
- }
278
-
279
- /**
280
- * Show attach summary
281
- */
282
- private showAttachSummary(result: AttachResult): void {
283
- const items = [];
284
-
285
- if (result.agentsAdded.length > 0) {
286
- items.push(`${result.agentsAdded.length} agent${result.agentsAdded.length > 1 ? 's' : ''}`);
287
- }
288
-
289
- if (result.commandsAdded.length > 0) {
290
- items.push(
291
- `${result.commandsAdded.length} command${result.commandsAdded.length > 1 ? 's' : ''}`
292
- );
293
- }
294
-
295
- if (result.mcpServersAdded.length > 0) {
296
- items.push(
297
- `${result.mcpServersAdded.length} MCP server${result.mcpServersAdded.length > 1 ? 's' : ''}`
298
- );
299
- }
300
-
301
- if (result.hooksAdded.length > 0) {
302
- items.push(`${result.hooksAdded.length} hook${result.hooksAdded.length > 1 ? 's' : ''}`);
303
- }
304
-
305
- if (result.rulesAppended) {
306
- items.push('rules');
307
- }
308
-
309
- if (items.length > 0) {
310
- console.log(chalk.green(` ✓ Added: ${items.join(', ')}`));
311
- }
312
-
313
- const overridden =
314
- result.agentsOverridden.length +
315
- result.commandsOverridden.length +
316
- result.mcpServersOverridden.length +
317
- result.hooksOverridden.length;
318
-
319
- if (overridden > 0) {
320
- console.log(chalk.yellow(` ⚠ Overridden: ${overridden} item${overridden > 1 ? 's' : ''}`));
321
- }
322
254
  }
323
255
 
324
256
  /**
@@ -163,7 +163,7 @@ export class AutoUpgrade {
163
163
 
164
164
  await execAsync(upgradeCmd);
165
165
 
166
- spinner.succeed(chalk.green('✓ Flow upgraded to latest version'));
166
+ spinner.succeed(chalk.green('✓ Flow upgraded (new version used on next run)'));
167
167
  return true;
168
168
  } catch (error) {
169
169
  spinner.fail(chalk.red('✗ Flow upgrade failed'));
@@ -232,64 +232,84 @@ export class AutoUpgrade {
232
232
  }
233
233
 
234
234
  /**
235
- * Run auto-upgrade check and upgrade if needed
236
- * Shows upgrade status and performs upgrades automatically
235
+ * Run auto-upgrade check and upgrade if needed (silent)
237
236
  * @param targetId - Optional target CLI ID to check and upgrade
237
+ * @returns Upgrade result with status info
238
238
  */
239
- async runAutoUpgrade(targetId?: string): Promise<void> {
240
- console.log(chalk.cyan('🔄 Checking for updates...\n'));
241
-
239
+ async runAutoUpgrade(targetId?: string): Promise<{
240
+ flowUpgraded: boolean;
241
+ flowVersion?: { current: string; latest: string };
242
+ targetUpgraded: boolean;
243
+ targetVersion?: { current: string; latest: string };
244
+ }> {
242
245
  const status = await this.checkForUpgrades(targetId);
246
+ const result = {
247
+ flowUpgraded: false,
248
+ flowVersion: status.flowVersion ?? undefined,
249
+ targetUpgraded: false,
250
+ targetVersion: status.targetVersion ?? undefined,
251
+ };
252
+
253
+ // Perform upgrades silently
254
+ if (status.flowNeedsUpgrade) {
255
+ result.flowUpgraded = await this.upgradeFlowSilent();
256
+ }
257
+
258
+ if (status.targetNeedsUpgrade && targetId) {
259
+ result.targetUpgraded = await this.upgradeTargetSilent(targetId);
260
+ }
261
+
262
+ return result;
263
+ }
243
264
 
244
- // Show upgrade status
245
- if (status.flowNeedsUpgrade && status.flowVersion) {
246
- console.log(
247
- chalk.yellow(
248
- `📦 Flow update available: ${status.flowVersion.current} → ${status.flowVersion.latest}`
249
- )
250
- );
251
- } else if (!this.options.skipFlow) {
252
- console.log(chalk.green('✓ Flow is up to date'));
265
+ /**
266
+ * Upgrade Flow silently (no spinner/output)
267
+ */
268
+ private async upgradeFlowSilent(): Promise<boolean> {
269
+ try {
270
+ const flowPm = await this.detectFlowPackageManager();
271
+ const upgradeCmd = getUpgradeCommand('@sylphx/flow', flowPm);
272
+ await execAsync(upgradeCmd);
273
+ return true;
274
+ } catch {
275
+ return false;
253
276
  }
277
+ }
254
278
 
255
- if (status.targetNeedsUpgrade && status.targetVersion && targetId) {
256
- const installation = this.targetInstaller.getInstallationInfo(targetId);
257
- console.log(
258
- chalk.yellow(
259
- `📦 ${installation?.name} update available: ${status.targetVersion.current} → ${status.targetVersion.latest}`
260
- )
261
- );
262
- } else if (!this.options.skipTarget && targetId) {
263
- const installation = this.targetInstaller.getInstallationInfo(targetId);
264
- console.log(chalk.green(`✓ ${installation?.name} is up to date`));
279
+ /**
280
+ * Upgrade target CLI silently (no spinner/output)
281
+ */
282
+ private async upgradeTargetSilent(targetId: string): Promise<boolean> {
283
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
284
+ if (!installation) {
285
+ return false;
265
286
  }
266
287
 
267
- // Perform upgrades if needed
268
- if (status.flowNeedsUpgrade || status.targetNeedsUpgrade) {
269
- console.log(chalk.cyan('\n📦 Installing updates...\n'));
270
-
271
- if (status.flowNeedsUpgrade && !process.env.SYLPHX_FLOW_UPGRADED) {
272
- const upgraded = await this.upgradeFlow();
273
- if (upgraded) {
274
- // Re-exec the process to use the new version
275
- console.log(chalk.cyan('\n🔄 Restarting with updated version...\n'));
276
- const { spawn } = await import('node:child_process');
277
- const child = spawn(process.argv[0], process.argv.slice(1), {
278
- stdio: 'inherit',
279
- env: { ...process.env, SYLPHX_FLOW_UPGRADED: '1' },
280
- });
281
- child.on('exit', (code) => process.exit(code ?? 0));
282
- return; // Don't continue with old code
288
+ try {
289
+ if (targetId === 'claude-code') {
290
+ try {
291
+ await execAsync('claude update');
292
+ return true;
293
+ } catch {
294
+ // Fall back to npm
283
295
  }
284
296
  }
285
297
 
286
- if (status.targetNeedsUpgrade && targetId) {
287
- await this.upgradeTarget(targetId);
298
+ if (targetId === 'opencode') {
299
+ try {
300
+ await execAsync('opencode upgrade');
301
+ return true;
302
+ } catch {
303
+ // Fall back to npm
304
+ }
288
305
  }
289
306
 
290
- console.log(chalk.green('\n✓ All tools upgraded\n'));
291
- } else {
292
- console.log();
307
+ const packageManager = detectPackageManager(this.projectPath);
308
+ const upgradeCmd = getUpgradeCommand(installation.package, packageManager);
309
+ await execAsync(upgradeCmd);
310
+ return true;
311
+ } catch {
312
+ return false;
293
313
  }
294
314
  }
295
315
  }
@@ -1,25 +1,20 @@
1
1
  /**
2
2
  * Banner Display Utilities
3
- * Welcome messages and branding
3
+ * Minimal, modern CLI output
4
4
  */
5
5
 
6
- import boxen from 'boxen';
7
6
  import chalk from 'chalk';
8
7
 
9
8
  /**
10
- * Display welcome banner
9
+ * Show minimal header: flow {version} → {target}
10
+ */
11
+ export function showHeader(version: string, target: string): void {
12
+ console.log(`\n${chalk.cyan('flow')} ${chalk.dim(version)} ${chalk.dim('→')} ${target}\n`);
13
+ }
14
+
15
+ /**
16
+ * @deprecated Use showHeader instead
11
17
  */
12
18
  export function showWelcome(): void {
13
- console.log(
14
- boxen(
15
- `${chalk.cyan.bold('Sylphx Flow')} ${chalk.dim('- AI-Powered Development Framework')}\n` +
16
- `${chalk.dim('Auto-initialization • Smart upgrades • One-click launch')}`,
17
- {
18
- padding: 1,
19
- margin: { bottom: 1 },
20
- borderStyle: 'round',
21
- borderColor: 'cyan',
22
- }
23
- )
24
- );
19
+ // No-op for backward compatibility during migration
25
20
  }