@sylphx/flow 2.14.1 → 2.15.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,47 @@
1
1
  # @sylphx/flow
2
2
 
3
+ ## 2.15.1 (2025-12-17)
4
+
5
+ ### Improvements
6
+
7
+ - **cli**: Make CLI output truly minimal and modern
8
+ - Remove all verbose status messages (backup, restore, session recovery)
9
+ - Remove emoji icons (šŸ”§, āœ”) for cleaner output
10
+ - Remove "Attached" and "Running" status lines
11
+ - Silent git worktree operations
12
+ - Silent crash recovery on startup
13
+ - Only show header: `flow {version} → {target}`
14
+
15
+ ### ā™»ļø Refactoring
16
+
17
+ - **cli:** make output truly minimal and modern ([3315e41](https://github.com/SylphxAI/flow/commit/3315e41fa34839d7c866af1ebbe3b6f2d9e3ee71))
18
+
19
+ ## 2.15.0 (2025-12-17)
20
+
21
+ ### Features
22
+
23
+ - **cli**: Redesign CLI output with modern, minimalist interface
24
+ - Single-line header: `flow {version} → {target}`
25
+ - Consolidated status: `āœ“ Attached {n} agents, {n} commands, {n} MCP`
26
+ - Silent operations by default (backup, cleanup, provider selection)
27
+ - Upgrade notification: `↑ Flow {version} (next run)`
28
+
29
+ ### Bug Fixes
30
+
31
+ - **auto-upgrade**: Remove auto-restart after Flow upgrade - new version used on next run instead
32
+
33
+ ### ✨ Features
34
+
35
+ - **cli:** redesign output with modern minimalist interface ([654146d](https://github.com/SylphxAI/flow/commit/654146d3ba7a635b39a599125300e60cfdba2bec))
36
+
37
+ ### šŸ› Bug Fixes
38
+
39
+ - **auto-upgrade:** don't restart after Flow upgrade ([8207417](https://github.com/SylphxAI/flow/commit/8207417921dc60357e97a2ed8db7d6d79f0b5a3e))
40
+
41
+ ### šŸ”§ Chores
42
+
43
+ - fix package.json formatting ([1e9d940](https://github.com/SylphxAI/flow/commit/1e9d94086e7186fa2a3e80c8f6c98483840d865d))
44
+
3
45
  ## 2.14.1 (2025-12-17)
4
46
 
5
47
  ### Documentation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/flow",
3
- "version": "2.14.1",
3
+ "version": "2.15.1",
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,77 @@ 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 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}`));
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
292
266
  const targetId = selectedTargetId;
293
267
 
294
- // Step 3.5: Provider selection (Claude Code only)
268
+ // Provider selection (Claude Code only, silent unless prompting)
295
269
  if (targetId === 'claude-code') {
296
270
  await selectProvider(configService);
297
271
  }
298
272
 
299
- // Step 3.6: Load Flow settings and determine agent to use
300
- const settings = await configService.loadSettings();
273
+ // Determine which agent to use
301
274
  const flowConfig = await configService.loadFlowConfig();
275
+ let agent = options.agent || settings.defaultAgent || 'coder';
302
276
 
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
277
+ // Check if agent is enabled (silent fallback)
307
278
  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
279
  const enabledAgents = await configService.getEnabledAgents();
313
- const fallbackAgent = enabledAgents.length > 0 ? enabledAgents[0] : 'coder';
314
- options.agent = fallbackAgent;
280
+ agent = enabledAgents.length > 0 ? enabledAgents[0] : 'coder';
315
281
  }
316
282
 
317
- console.log(chalk.cyan(`šŸ¤– Running agent: ${agent}\n`));
318
-
319
- // Load enabled rules and output styles from config
283
+ // Load agent content
320
284
  const enabledRules = await configService.getEnabledRules();
321
285
  const enabledOutputStyles = await configService.getEnabledOutputStyles();
322
286
 
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
287
  const agentContent = await loadAgentContent(
329
288
  agent,
330
289
  options.agentFile,
@@ -334,7 +293,6 @@ export async function executeFlowV2(
334
293
  const agentInstructions = extractAgentInstructions(agentContent);
335
294
 
336
295
  const systemPrompt = `AGENT INSTRUCTIONS:\n${agentInstructions}`;
337
-
338
296
  const userPrompt = prompt?.trim() || '';
339
297
 
340
298
  // Prepare run options
@@ -352,30 +310,27 @@ export async function executeFlowV2(
352
310
  // Step 4: Execute command
353
311
  await executeTargetCommand(targetId, systemPrompt, userPrompt, runOptions);
354
312
 
355
- // Step 5: Cleanup (restore environment)
313
+ // Step 5: Cleanup (silent)
356
314
  await executor.cleanup(projectPath);
357
-
358
- console.log(chalk.green('āœ“ Session complete\n'));
359
315
  } catch (error) {
360
316
  // Handle user cancellation gracefully
361
317
  if (error instanceof UserCancelledError) {
362
- console.log(chalk.yellow('\nāš ļø Operation cancelled by user'));
318
+ console.log(chalk.yellow('\n Cancelled'));
363
319
  try {
364
320
  await executor.cleanup(projectPath);
365
- console.log(chalk.green(' āœ“ Settings restored\n'));
366
- } catch (cleanupError) {
367
- console.error(chalk.red(' āœ— Cleanup failed:'), cleanupError);
321
+ } catch {
322
+ // Silent cleanup failure
368
323
  }
369
324
  process.exit(0);
370
325
  }
371
326
 
372
- console.error(chalk.red.bold('\nāœ— Execution failed:'), error);
327
+ console.error(chalk.red('\n Error:'), error);
373
328
 
374
329
  // Ensure cleanup even on error
375
330
  try {
376
331
  await executor.cleanup(projectPath);
377
- } catch (cleanupError) {
378
- console.error(chalk.red('āœ— Cleanup failed:'), cleanupError);
332
+ } catch {
333
+ // Silent cleanup failure
379
334
  }
380
335
 
381
336
  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
  }
@@ -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
  }
@@ -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
  /**
@@ -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
 
@@ -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
  }
@@ -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 {
@@ -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
  }