@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 +40 -0
- package/assets/slash-commands/review-i18n.md +9 -0
- package/package.json +1 -1
- package/src/commands/flow/execute-v2.ts +78 -111
- package/src/core/attach-manager.ts +0 -21
- package/src/core/flow-executor.ts +21 -89
- package/src/services/auto-upgrade.ts +66 -46
- package/src/utils/display/banner.ts +10 -15
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.
|
|
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
|
-
*
|
|
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 {
|
|
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
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
218
|
+
// User has a specific target preference
|
|
231
219
|
selectedTargetId = settings.defaultTarget;
|
|
232
|
-
const installation = targetInstaller.getInstallationInfo(selectedTargetId);
|
|
233
220
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
console.log(chalk.
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
264
|
-
if (
|
|
265
|
-
console.log(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
console.log(chalk.
|
|
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
|
|
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
|
-
//
|
|
291
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
314
|
-
options.agent = fallbackAgent;
|
|
289
|
+
agent = enabledAgents.length > 0 ? enabledAgents[0] : 'coder';
|
|
315
290
|
}
|
|
316
291
|
|
|
317
|
-
|
|
292
|
+
// Show running agent
|
|
293
|
+
console.log(chalk.dim(`\n Running: ${agent}\n`));
|
|
318
294
|
|
|
319
|
-
// Load
|
|
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 (
|
|
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
|
|
330
|
+
console.log(chalk.yellow('\n Cancelled'));
|
|
363
331
|
try {
|
|
364
332
|
await executor.cleanup(projectPath);
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
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
|
|
378
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
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<
|
|
240
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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 (
|
|
287
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
}
|