@sylphx/flow 2.0.0 → 2.1.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 +117 -0
- package/UPGRADE.md +25 -14
- package/package.json +10 -6
- package/src/commands/flow/execute-v2.ts +161 -67
- package/src/commands/settings-command.ts +22 -15
- package/src/config/ai-config.ts +2 -69
- package/src/config/targets.ts +0 -11
- package/src/core/attach-manager.ts +26 -22
- package/src/core/flow-executor.ts +53 -14
- package/src/core/installers/file-installer.ts +0 -57
- package/src/core/installers/mcp-installer.ts +0 -33
- package/src/index.ts +2 -2
- package/src/services/auto-upgrade.ts +248 -0
- package/src/services/global-config.ts +1 -1
- package/src/services/target-installer.ts +254 -0
- package/src/targets/claude-code.ts +5 -7
- package/src/targets/opencode.ts +6 -26
- package/src/utils/prompt-helpers.ts +48 -0
- package/src/utils/target-selection.ts +169 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,122 @@
|
|
|
1
1
|
# @sylphx/flow
|
|
2
2
|
|
|
3
|
+
## 2.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8ae48d6: Fix singleFiles location and improve settings cleanup
|
|
8
|
+
|
|
9
|
+
**Bug Fixes:**
|
|
10
|
+
|
|
11
|
+
1. Fixed silent.md location bug - output style files were incorrectly written to project root instead of target config directory (.claude/ or .opencode/)
|
|
12
|
+
|
|
13
|
+
2. Enhanced clearUserSettings to ensure complete cleanup in replace mode:
|
|
14
|
+
- Now clears ALL user configuration including hooks, complete MCP config, rules, and singleFiles
|
|
15
|
+
- Removes entire MCP section (not just servers) to properly clear user hooks
|
|
16
|
+
- Added legacy cleanup to remove incorrectly placed files from project root
|
|
17
|
+
|
|
18
|
+
This fixes the issue where user's hooks and MCP configs were still affecting execution even in replace mode (non-merge mode).
|
|
19
|
+
|
|
20
|
+
## 2.1.0
|
|
21
|
+
|
|
22
|
+
### Minor Changes
|
|
23
|
+
|
|
24
|
+
- 09608be: Auto-installation and auto-upgrade features
|
|
25
|
+
|
|
26
|
+
**New Features:**
|
|
27
|
+
|
|
28
|
+
- **Auto-detection**: Automatically detects installed AI CLIs (Claude Code, OpenCode, Cursor)
|
|
29
|
+
- **Auto-installation**: If no AI CLI is detected, prompts user to select and installs it automatically
|
|
30
|
+
- **Auto-upgrade**: Before each Flow execution, automatically checks and upgrades Flow and target CLI to latest versions
|
|
31
|
+
- **Zero-friction setup**: New users can install Flow and start using it immediately without manual setup
|
|
32
|
+
|
|
33
|
+
**Implementation:**
|
|
34
|
+
|
|
35
|
+
- Created `TargetInstaller` service for detecting and installing AI CLI tools
|
|
36
|
+
- Created `AutoUpgrade` service for automatic version checking and upgrading
|
|
37
|
+
- Integrated both services into execution flow (`execute-v2.ts`)
|
|
38
|
+
- Smart package manager detection (npm, bun, pnpm, yarn)
|
|
39
|
+
|
|
40
|
+
**User Experience:**
|
|
41
|
+
|
|
42
|
+
Flow 2.0 now delivers on the promise of "One CLI to rule them all":
|
|
43
|
+
|
|
44
|
+
1. **First run**: User installs Flow → Flow detects no AI CLI → Prompts to select one → Installs it automatically
|
|
45
|
+
2. **Every run**: Flow checks for updates → Upgrades Flow and AI CLI to latest → Runs user's task
|
|
46
|
+
|
|
47
|
+
Example flow:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
$ npm install -g @sylphx/flow
|
|
51
|
+
$ sylphx-flow "build my app"
|
|
52
|
+
|
|
53
|
+
🔍 Detecting installed AI CLIs...
|
|
54
|
+
⚠️ No AI CLI detected
|
|
55
|
+
|
|
56
|
+
? No AI CLI detected. Which would you like to use?
|
|
57
|
+
❯ Claude Code
|
|
58
|
+
OpenCode
|
|
59
|
+
Cursor
|
|
60
|
+
|
|
61
|
+
✓ Claude Code installed successfully
|
|
62
|
+
|
|
63
|
+
🔄 Checking for updates...
|
|
64
|
+
✓ Flow is up to date
|
|
65
|
+
✓ Claude Code is up to date
|
|
66
|
+
|
|
67
|
+
🚀 Starting Flow session...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Philosophy:**
|
|
71
|
+
|
|
72
|
+
This implements Flow's core principle of "Transcendent Simplicity" - users don't need to know which AI CLI to use or how to install/upgrade it. Flow handles everything automatically.
|
|
73
|
+
|
|
74
|
+
### Patch Changes
|
|
75
|
+
|
|
76
|
+
- cc065f2: Code cleanup and refactoring
|
|
77
|
+
|
|
78
|
+
**Removed:**
|
|
79
|
+
|
|
80
|
+
- All legacy config migration code (~70 lines)
|
|
81
|
+
- OpenCode old directory cleanup logic (~16 lines)
|
|
82
|
+
- Deprecated FileInstaller and MCPInstaller classes (~60 lines)
|
|
83
|
+
- Unused deprecated exports (ALL_TARGETS, IMPLEMENTED_TARGETS)
|
|
84
|
+
|
|
85
|
+
**Refactored:**
|
|
86
|
+
|
|
87
|
+
- Migrated from class-based installers to functional API
|
|
88
|
+
- opencode.ts: Direct function calls instead of class wrappers
|
|
89
|
+
- claude-code.ts: Direct function calls instead of class wrappers
|
|
90
|
+
|
|
91
|
+
**Improved:**
|
|
92
|
+
|
|
93
|
+
- Removed ~179 lines of dead code
|
|
94
|
+
- Cleaner functional API
|
|
95
|
+
- Better code organization and modularity
|
|
96
|
+
- Comprehensive JSDoc documentation
|
|
97
|
+
- Consistent error handling patterns
|
|
98
|
+
|
|
99
|
+
**Result:**
|
|
100
|
+
|
|
101
|
+
- Zero technical debt
|
|
102
|
+
- Zero deprecated code
|
|
103
|
+
- Modern, maintainable codebase
|
|
104
|
+
|
|
105
|
+
- edb043c: Fix target selection logic to properly distinguish between three cases
|
|
106
|
+
|
|
107
|
+
**Fixed:**
|
|
108
|
+
|
|
109
|
+
- Target selection now correctly handles three distinct scenarios:
|
|
110
|
+
1. User explicitly set "ask-every-time" → always prompt
|
|
111
|
+
2. User has no setting (undefined/null) → allow auto-detect
|
|
112
|
+
3. User has specific target → use that target
|
|
113
|
+
|
|
114
|
+
**Improved:**
|
|
115
|
+
|
|
116
|
+
- Better code clarity with explicit case handling
|
|
117
|
+
- More predictable behavior for different user preferences
|
|
118
|
+
- Enhanced logic comments for maintainability
|
|
119
|
+
|
|
3
120
|
## 2.0.0
|
|
4
121
|
|
|
5
122
|
### Major Changes
|
package/UPGRADE.md
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
# Upgrade Guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Flow 2.0 includes **automatic upgrade before every session** with smart package manager detection. No more manual version management—Flow keeps itself and your AI CLI tools up to date.
|
|
4
4
|
|
|
5
|
-
## Auto-
|
|
5
|
+
## Auto-Upgrade on Every Execution
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Before each Flow session**, Flow automatically:
|
|
8
8
|
|
|
9
|
+
1. **Checks for Flow updates**: Compares installed version with npm registry
|
|
10
|
+
2. **Checks for AI CLI updates**: Verifies Claude Code, OpenCode, or Cursor versions
|
|
11
|
+
3. **Upgrades automatically**: Installs latest versions if available
|
|
12
|
+
4. **Detects package manager**: Uses npm, bun, pnpm, or yarn based on your environment
|
|
13
|
+
|
|
14
|
+
Example output:
|
|
9
15
|
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
🔄 Checking for updates...
|
|
17
|
+
✓ Flow 2.0.0 → 2.1.0 (latest)
|
|
18
|
+
✓ Claude Code 1.5.0 → 1.6.0 (latest)
|
|
19
|
+
📦 Installing updates...
|
|
20
|
+
✓ All tools upgraded
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
🚀 Starting Flow session...
|
|
23
|
+
```
|
|
16
24
|
|
|
17
|
-
This
|
|
25
|
+
This happens automatically. No flags needed, no configuration required.
|
|
18
26
|
|
|
19
27
|
## Manual Upgrade Check
|
|
20
28
|
|
|
@@ -94,13 +102,16 @@ Sylphx Flow automatically detects which package manager you're using:
|
|
|
94
102
|
- Without `--auto`: Shows manual install command for your package manager
|
|
95
103
|
- With `--auto`: Runs the appropriate install command automatically
|
|
96
104
|
|
|
97
|
-
##
|
|
105
|
+
## Philosophy: Always Latest
|
|
98
106
|
|
|
99
|
-
|
|
107
|
+
Flow 2.0 embraces the principle of **always running the latest stable versions**. This ensures:
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
- Latest features and improvements
|
|
110
|
+
- Security patches applied immediately
|
|
111
|
+
- Compatibility with newest AI models
|
|
112
|
+
- Bug fixes without manual intervention
|
|
113
|
+
|
|
114
|
+
Auto-upgrade cannot be disabled—it's core to Flow's philosophy of zero-friction excellence.
|
|
104
115
|
|
|
105
116
|
## Troubleshooting
|
|
106
117
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/flow",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.1",
|
|
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": {
|
|
7
7
|
"sylphx-flow": "./src/index.ts"
|
|
@@ -50,12 +50,16 @@
|
|
|
50
50
|
"ai",
|
|
51
51
|
"automation",
|
|
52
52
|
"workflow",
|
|
53
|
-
"claude",
|
|
53
|
+
"claude-code",
|
|
54
54
|
"opencode",
|
|
55
|
+
"cursor",
|
|
55
56
|
"cli",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
57
|
+
"orchestration",
|
|
58
|
+
"unified",
|
|
59
|
+
"meta-layer",
|
|
60
|
+
"developer-tools",
|
|
61
|
+
"auto-install",
|
|
62
|
+
"auto-upgrade"
|
|
59
63
|
],
|
|
60
64
|
"repository": {
|
|
61
65
|
"type": "git",
|
|
@@ -16,6 +16,32 @@ import type { FlowOptions } from './types.js';
|
|
|
16
16
|
import { resolvePrompt } from './prompt.js';
|
|
17
17
|
import { GlobalConfigService } from '../../services/global-config.js';
|
|
18
18
|
import { UserCancelledError } from '../../utils/errors.js';
|
|
19
|
+
import { TargetInstaller } from '../../services/target-installer.js';
|
|
20
|
+
import { AutoUpgrade } from '../../services/auto-upgrade.js';
|
|
21
|
+
import { promptForTargetSelection, ensureTargetInstalled } from '../../utils/target-selection.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configure provider environment variables
|
|
25
|
+
*/
|
|
26
|
+
function configureProviderEnv(
|
|
27
|
+
provider: 'kimi' | 'zai',
|
|
28
|
+
apiKey: string
|
|
29
|
+
): void {
|
|
30
|
+
const providerConfig = {
|
|
31
|
+
kimi: {
|
|
32
|
+
baseUrl: 'https://api.moonshot.cn/v1',
|
|
33
|
+
name: 'Kimi',
|
|
34
|
+
},
|
|
35
|
+
zai: {
|
|
36
|
+
baseUrl: 'https://api.z.ai/v1',
|
|
37
|
+
name: 'Z.ai',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const config = providerConfig[provider];
|
|
42
|
+
process.env.ANTHROPIC_BASE_URL = config.baseUrl;
|
|
43
|
+
process.env.ANTHROPIC_API_KEY = apiKey;
|
|
44
|
+
}
|
|
19
45
|
|
|
20
46
|
/**
|
|
21
47
|
* Select and configure provider for Claude Code
|
|
@@ -27,60 +53,48 @@ async function selectProvider(
|
|
|
27
53
|
const providerConfig = await configService.loadProviderConfig();
|
|
28
54
|
const defaultProvider = providerConfig.claudeCode.defaultProvider;
|
|
29
55
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (defaultProvider === 'kimi') {
|
|
37
|
-
process.env.ANTHROPIC_BASE_URL = 'https://api.moonshot.cn/v1';
|
|
38
|
-
process.env.ANTHROPIC_API_KEY = provider.apiKey;
|
|
39
|
-
} else if (defaultProvider === 'zai') {
|
|
40
|
-
process.env.ANTHROPIC_BASE_URL = 'https://api.z.ai/v1';
|
|
41
|
-
process.env.ANTHROPIC_API_KEY = provider.apiKey;
|
|
56
|
+
// If not "ask-every-time", use the default provider
|
|
57
|
+
if (defaultProvider !== 'ask-every-time') {
|
|
58
|
+
if (defaultProvider === 'kimi' || defaultProvider === 'zai') {
|
|
59
|
+
const provider = providerConfig.claudeCode.providers[defaultProvider];
|
|
60
|
+
if (provider?.apiKey) {
|
|
61
|
+
configureProviderEnv(defaultProvider, provider.apiKey);
|
|
42
62
|
}
|
|
43
63
|
}
|
|
64
|
+
return;
|
|
44
65
|
}
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Ask user which provider to use for this session
|
|
49
|
-
const { selectedProvider } = await inquirer.prompt([
|
|
50
|
-
{
|
|
51
|
-
type: 'list',
|
|
52
|
-
name: 'selectedProvider',
|
|
53
|
-
message: 'Select provider for this session:',
|
|
54
|
-
choices: [
|
|
55
|
-
{ name: 'Default (Claude Code built-in)', value: 'default' },
|
|
56
|
-
{ name: 'Kimi', value: 'kimi' },
|
|
57
|
-
{ name: 'Z.ai', value: 'zai' },
|
|
58
|
-
],
|
|
59
|
-
default: 'default',
|
|
60
|
-
},
|
|
61
|
-
]);
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
// Ask user which provider to use for this session
|
|
68
|
+
const { selectedProvider } = await inquirer.prompt([
|
|
69
|
+
{
|
|
70
|
+
type: 'list',
|
|
71
|
+
name: 'selectedProvider',
|
|
72
|
+
message: 'Select provider for this session:',
|
|
73
|
+
choices: [
|
|
74
|
+
{ name: 'Default (Claude Code built-in)', value: 'default' },
|
|
75
|
+
{ name: 'Kimi', value: 'kimi' },
|
|
76
|
+
{ name: 'Z.ai', value: 'zai' },
|
|
77
|
+
],
|
|
78
|
+
default: 'default',
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// Configure environment variables based on selection
|
|
83
|
+
if (selectedProvider === 'kimi' || selectedProvider === 'zai') {
|
|
84
|
+
const provider = providerConfig.claudeCode.providers[selectedProvider];
|
|
85
|
+
|
|
86
|
+
if (!provider?.apiKey) {
|
|
87
|
+
console.log(chalk.yellow('⚠ API key not configured. Use: sylphx-flow settings\n'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
66
90
|
|
|
67
|
-
|
|
68
|
-
console.log(chalk.yellow('⚠ API key not configured. Use: sylphx-flow settings\n'));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
91
|
+
configureProviderEnv(selectedProvider, provider.apiKey);
|
|
71
92
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
console.log(chalk.green('✓ Using
|
|
76
|
-
} else if (selectedProvider === 'zai') {
|
|
77
|
-
process.env.ANTHROPIC_BASE_URL = 'https://api.z.ai/v1';
|
|
78
|
-
process.env.ANTHROPIC_API_KEY = provider.apiKey;
|
|
79
|
-
console.log(chalk.green('✓ Using Z.ai provider\n'));
|
|
93
|
+
const providerName = selectedProvider === 'kimi' ? 'Kimi' : 'Z.ai';
|
|
94
|
+
console.log(chalk.green(`✓ Using ${providerName} provider\n`));
|
|
95
|
+
} else {
|
|
96
|
+
console.log(chalk.green('✓ Using default Claude Code provider\n'));
|
|
80
97
|
}
|
|
81
|
-
} else {
|
|
82
|
-
console.log(chalk.green('✓ Using default Claude Code provider\n'));
|
|
83
|
-
}
|
|
84
98
|
} catch (error: any) {
|
|
85
99
|
// Handle user cancellation (Ctrl+C)
|
|
86
100
|
if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
|
|
@@ -129,6 +143,103 @@ export async function executeFlowV2(
|
|
|
129
143
|
// Show welcome banner
|
|
130
144
|
showWelcome();
|
|
131
145
|
|
|
146
|
+
// Initialize config service early to check for saved preferences
|
|
147
|
+
const configService = new GlobalConfigService();
|
|
148
|
+
await configService.initialize();
|
|
149
|
+
|
|
150
|
+
// Step 1: Determine target
|
|
151
|
+
const targetInstaller = new TargetInstaller(projectPath);
|
|
152
|
+
const installedTargets = await targetInstaller.detectInstalledTargets();
|
|
153
|
+
const settings = await configService.loadSettings();
|
|
154
|
+
|
|
155
|
+
let selectedTargetId: string | null = null;
|
|
156
|
+
|
|
157
|
+
// Distinguish between three cases:
|
|
158
|
+
// 1. User explicitly set "ask-every-time" → always prompt
|
|
159
|
+
// 2. User has no setting (undefined/null) → allow auto-detect
|
|
160
|
+
// 3. User has specific target → use that target
|
|
161
|
+
const isAskEveryTime = settings.defaultTarget === 'ask-every-time';
|
|
162
|
+
const hasNoSetting = !settings.defaultTarget;
|
|
163
|
+
const hasSpecificTarget = settings.defaultTarget && settings.defaultTarget !== 'ask-every-time';
|
|
164
|
+
|
|
165
|
+
if (isAskEveryTime) {
|
|
166
|
+
// User explicitly wants to be asked every time - ALWAYS prompt, never auto-detect
|
|
167
|
+
console.log(chalk.cyan('🔍 Detecting installed AI CLIs...\n'));
|
|
168
|
+
|
|
169
|
+
selectedTargetId = await promptForTargetSelection(
|
|
170
|
+
installedTargets,
|
|
171
|
+
'Select AI CLI to use:',
|
|
172
|
+
'execution'
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const installation = targetInstaller.getInstallationInfo(selectedTargetId);
|
|
176
|
+
const installed = await ensureTargetInstalled(selectedTargetId, targetInstaller, installedTargets);
|
|
177
|
+
|
|
178
|
+
if (!installed) {
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (installedTargets.includes(selectedTargetId)) {
|
|
183
|
+
console.log(chalk.green(`✓ Using ${installation?.name}\n`));
|
|
184
|
+
}
|
|
185
|
+
} else if (hasNoSetting) {
|
|
186
|
+
// No setting - use auto-detection (smart default behavior)
|
|
187
|
+
if (installedTargets.length === 1) {
|
|
188
|
+
// Exactly 1 target found - use it automatically
|
|
189
|
+
selectedTargetId = installedTargets[0];
|
|
190
|
+
const installation = targetInstaller.getInstallationInfo(selectedTargetId);
|
|
191
|
+
console.log(chalk.green(`✓ Using ${installation?.name} (auto-detected)\n`));
|
|
192
|
+
} else {
|
|
193
|
+
// 0 or multiple targets - prompt for selection
|
|
194
|
+
console.log(chalk.cyan('🔍 Detecting installed AI CLIs...\n'));
|
|
195
|
+
|
|
196
|
+
selectedTargetId = await promptForTargetSelection(
|
|
197
|
+
installedTargets,
|
|
198
|
+
'Select AI CLI to use:',
|
|
199
|
+
'execution'
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const installation = targetInstaller.getInstallationInfo(selectedTargetId);
|
|
203
|
+
const installed = await ensureTargetInstalled(selectedTargetId, targetInstaller, installedTargets);
|
|
204
|
+
|
|
205
|
+
if (!installed) {
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (installedTargets.includes(selectedTargetId)) {
|
|
210
|
+
console.log(chalk.green(`✓ Using ${installation?.name}\n`));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} else if (hasSpecificTarget) {
|
|
214
|
+
// User has a specific target preference - ALWAYS use it
|
|
215
|
+
selectedTargetId = settings.defaultTarget;
|
|
216
|
+
const installation = targetInstaller.getInstallationInfo(selectedTargetId);
|
|
217
|
+
|
|
218
|
+
// Check if the preferred target is installed
|
|
219
|
+
if (installedTargets.includes(selectedTargetId)) {
|
|
220
|
+
console.log(chalk.green(`✓ Using ${installation?.name} (from settings)\n`));
|
|
221
|
+
} else {
|
|
222
|
+
// Preferred target not installed - try to install it
|
|
223
|
+
console.log(chalk.yellow(`⚠️ ${installation?.name} is set as default but not installed\n`));
|
|
224
|
+
const installed = await targetInstaller.install(selectedTargetId, true);
|
|
225
|
+
|
|
226
|
+
if (!installed) {
|
|
227
|
+
// Installation failed - show error and exit
|
|
228
|
+
console.log(chalk.red(`\n✗ Cannot proceed: ${installation?.name} is not installed and auto-install failed`));
|
|
229
|
+
console.log(chalk.yellow(' Please either:'));
|
|
230
|
+
console.log(chalk.cyan(' 1. Install manually (see instructions above)'));
|
|
231
|
+
console.log(chalk.cyan(' 2. Change default target: sylphx-flow settings\n'));
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Step 2: Auto-upgrade Flow and target CLI
|
|
240
|
+
const autoUpgrade = new AutoUpgrade(projectPath);
|
|
241
|
+
await autoUpgrade.runAutoUpgrade(selectedTargetId);
|
|
242
|
+
|
|
132
243
|
// Mode info
|
|
133
244
|
if (options.merge) {
|
|
134
245
|
console.log(chalk.cyan('🔗 Merge mode: Flow settings will be merged with your existing settings'));
|
|
@@ -138,24 +249,10 @@ export async function executeFlowV2(
|
|
|
138
249
|
console.log(chalk.dim(' Use --merge to keep your existing settings\n'));
|
|
139
250
|
}
|
|
140
251
|
|
|
141
|
-
// Initialize config service
|
|
142
|
-
const configService = new GlobalConfigService();
|
|
143
|
-
await configService.initialize();
|
|
144
|
-
|
|
145
252
|
// Create executor
|
|
146
253
|
const executor = new FlowExecutor();
|
|
147
254
|
const projectManager = executor.getProjectManager();
|
|
148
255
|
|
|
149
|
-
// Step 1: Check for upgrades (non-intrusive)
|
|
150
|
-
const upgradeManager = new UpgradeManager();
|
|
151
|
-
const updates = await upgradeManager.checkUpdates();
|
|
152
|
-
|
|
153
|
-
if (updates.flowUpdate || updates.targetUpdate) {
|
|
154
|
-
console.log(
|
|
155
|
-
chalk.yellow('📦 Updates available! Run: sylphx-flow upgrade --auto\n')
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
256
|
// Step 2: Execute attach mode lifecycle
|
|
160
257
|
try {
|
|
161
258
|
// Attach Flow environment (backup → attach → register cleanup)
|
|
@@ -166,12 +263,9 @@ export async function executeFlowV2(
|
|
|
166
263
|
merge: options.merge || false,
|
|
167
264
|
});
|
|
168
265
|
|
|
169
|
-
// Step 3:
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
// Map target to targetManager's target IDs
|
|
174
|
-
const targetId = target === 'claude-code' ? 'claude-code' : 'opencode';
|
|
266
|
+
// Step 3: Use the target we already selected (don't re-detect)
|
|
267
|
+
// selectedTargetId was determined earlier based on settings/auto-detect/prompt
|
|
268
|
+
const targetId = selectedTargetId;
|
|
175
269
|
|
|
176
270
|
// Step 3.5: Provider selection (Claude Code only)
|
|
177
271
|
if (targetId === 'claude-code') {
|
|
@@ -8,6 +8,8 @@ import chalk from 'chalk';
|
|
|
8
8
|
import inquirer from 'inquirer';
|
|
9
9
|
import { GlobalConfigService } from '../services/global-config.js';
|
|
10
10
|
import { UserCancelledError } from '../utils/errors.js';
|
|
11
|
+
import { TargetInstaller } from '../services/target-installer.js';
|
|
12
|
+
import { promptForDefaultTarget, buildAvailableTargets } from '../utils/target-selection.js';
|
|
11
13
|
|
|
12
14
|
export const settingsCommand = new Command('settings')
|
|
13
15
|
.description('Configure Sylphx Flow settings')
|
|
@@ -440,25 +442,30 @@ async function configureTarget(configService: GlobalConfigService): Promise<void
|
|
|
440
442
|
console.log(chalk.cyan.bold('\n━━━ 🎯 Target Platform\n'));
|
|
441
443
|
|
|
442
444
|
const settings = await configService.loadSettings();
|
|
445
|
+
const targetInstaller = new TargetInstaller();
|
|
443
446
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
name: 'defaultTarget',
|
|
448
|
-
message: 'Select default target platform:',
|
|
449
|
-
choices: [
|
|
450
|
-
{ name: 'Claude Code', value: 'claude-code' },
|
|
451
|
-
{ name: 'OpenCode', value: 'opencode' },
|
|
452
|
-
],
|
|
453
|
-
default: settings.defaultTarget || 'claude-code',
|
|
454
|
-
},
|
|
455
|
-
]);
|
|
447
|
+
// Detect which targets are installed
|
|
448
|
+
console.log(chalk.dim('Detecting installed AI CLIs...\n'));
|
|
449
|
+
const installedTargets = await targetInstaller.detectInstalledTargets();
|
|
456
450
|
|
|
457
|
-
|
|
451
|
+
const defaultTarget = await promptForDefaultTarget(installedTargets, settings.defaultTarget);
|
|
452
|
+
|
|
453
|
+
settings.defaultTarget = defaultTarget as 'claude-code' | 'opencode' | 'cursor' | 'ask-every-time';
|
|
458
454
|
await configService.saveSettings(settings);
|
|
459
455
|
|
|
460
|
-
|
|
461
|
-
|
|
456
|
+
if (defaultTarget === 'ask-every-time') {
|
|
457
|
+
console.log(chalk.green('\n✓ Target platform saved'));
|
|
458
|
+
console.log(chalk.dim(' Default: Ask every time (auto-detect or prompt)'));
|
|
459
|
+
} else {
|
|
460
|
+
const availableTargets = buildAvailableTargets(installedTargets);
|
|
461
|
+
const selectedTarget = availableTargets.find((t) => t.value === defaultTarget);
|
|
462
|
+
const installStatus = selectedTarget?.installed
|
|
463
|
+
? chalk.green('(installed)')
|
|
464
|
+
: chalk.yellow('(will be installed on first use)');
|
|
465
|
+
|
|
466
|
+
console.log(chalk.green('\n✓ Target platform saved'));
|
|
467
|
+
console.log(chalk.dim(` Default: ${defaultTarget} ${installStatus}`));
|
|
468
|
+
}
|
|
462
469
|
}
|
|
463
470
|
|
|
464
471
|
/**
|
package/src/config/ai-config.ts
CHANGED
|
@@ -58,11 +58,6 @@ const GLOBAL_CONFIG_FILE = path.join(os.homedir(), '.sylphx-flow', 'settings.jso
|
|
|
58
58
|
const PROJECT_CONFIG_FILE = '.sylphx-flow/settings.json';
|
|
59
59
|
const LOCAL_CONFIG_FILE = '.sylphx-flow/settings.local.json';
|
|
60
60
|
|
|
61
|
-
/**
|
|
62
|
-
* Deprecated config file (for migration)
|
|
63
|
-
*/
|
|
64
|
-
const LEGACY_CONFIG_FILE = '.sylphx-flow/ai-config.json';
|
|
65
|
-
|
|
66
61
|
/**
|
|
67
62
|
* Get AI config file paths in priority order
|
|
68
63
|
*/
|
|
@@ -70,12 +65,10 @@ export const getAIConfigPaths = (cwd: string = process.cwd()): {
|
|
|
70
65
|
global: string;
|
|
71
66
|
project: string;
|
|
72
67
|
local: string;
|
|
73
|
-
legacy: string;
|
|
74
68
|
} => ({
|
|
75
69
|
global: GLOBAL_CONFIG_FILE,
|
|
76
70
|
project: path.join(cwd, PROJECT_CONFIG_FILE),
|
|
77
71
|
local: path.join(cwd, LOCAL_CONFIG_FILE),
|
|
78
|
-
legacy: path.join(cwd, LEGACY_CONFIG_FILE),
|
|
79
72
|
});
|
|
80
73
|
|
|
81
74
|
/**
|
|
@@ -140,18 +133,12 @@ export const aiConfigExists = async (cwd: string = process.cwd()): Promise<boole
|
|
|
140
133
|
return true;
|
|
141
134
|
} catch {}
|
|
142
135
|
|
|
143
|
-
try {
|
|
144
|
-
await fs.access(paths.legacy);
|
|
145
|
-
return true;
|
|
146
|
-
} catch {}
|
|
147
|
-
|
|
148
136
|
return false;
|
|
149
137
|
};
|
|
150
138
|
|
|
151
139
|
/**
|
|
152
140
|
* Load AI configuration
|
|
153
141
|
* Merges global, project, and local configs with priority: local > project > global
|
|
154
|
-
* Automatically migrates legacy config on first load
|
|
155
142
|
*/
|
|
156
143
|
export const loadAIConfig = async (cwd: string = process.cwd()): Promise<Result<AIConfig, Error>> => {
|
|
157
144
|
return tryCatchAsync(
|
|
@@ -159,39 +146,19 @@ export const loadAIConfig = async (cwd: string = process.cwd()): Promise<Result<
|
|
|
159
146
|
const paths = getAIConfigPaths(cwd);
|
|
160
147
|
|
|
161
148
|
// Load all config files
|
|
162
|
-
const [globalConfig, projectConfig, localConfig
|
|
149
|
+
const [globalConfig, projectConfig, localConfig] = await Promise.all([
|
|
163
150
|
loadConfigFile(paths.global),
|
|
164
151
|
loadConfigFile(paths.project),
|
|
165
152
|
loadConfigFile(paths.local),
|
|
166
|
-
loadConfigFile(paths.legacy),
|
|
167
153
|
]);
|
|
168
154
|
|
|
169
|
-
// Auto-migrate legacy config if it exists and global doesn't
|
|
170
|
-
if (legacyConfig && !globalConfig) {
|
|
171
|
-
await migrateLegacyConfig(cwd);
|
|
172
|
-
// Reload global config after migration
|
|
173
|
-
const migratedGlobal = await loadConfigFile(paths.global);
|
|
174
|
-
if (migratedGlobal) {
|
|
175
|
-
// Start with empty config
|
|
176
|
-
let merged: AIConfig = {};
|
|
177
|
-
|
|
178
|
-
// Merge in priority order: global < project < local
|
|
179
|
-
merged = mergeConfigs(merged, migratedGlobal);
|
|
180
|
-
if (projectConfig) merged = mergeConfigs(merged, projectConfig);
|
|
181
|
-
if (localConfig) merged = mergeConfigs(merged, localConfig);
|
|
182
|
-
|
|
183
|
-
return merged;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
155
|
// Start with empty config
|
|
188
156
|
let merged: AIConfig = {};
|
|
189
157
|
|
|
190
|
-
// Merge in priority order: global < project < local
|
|
158
|
+
// Merge in priority order: global < project < local
|
|
191
159
|
if (globalConfig) merged = mergeConfigs(merged, globalConfig);
|
|
192
160
|
if (projectConfig) merged = mergeConfigs(merged, projectConfig);
|
|
193
161
|
if (localConfig) merged = mergeConfigs(merged, localConfig);
|
|
194
|
-
if (legacyConfig) merged = mergeConfigs(merged, legacyConfig);
|
|
195
162
|
|
|
196
163
|
return merged;
|
|
197
164
|
},
|
|
@@ -340,37 +307,3 @@ export const getConfiguredProviders = async (
|
|
|
340
307
|
return providers;
|
|
341
308
|
};
|
|
342
309
|
|
|
343
|
-
/**
|
|
344
|
-
* Migrate legacy ai-config.json to new settings system
|
|
345
|
-
* Automatically called on first load if legacy config exists
|
|
346
|
-
*/
|
|
347
|
-
export const migrateLegacyConfig = async (cwd: string = process.cwd()): Promise<Result<void, Error>> => {
|
|
348
|
-
return tryCatchAsync(
|
|
349
|
-
async () => {
|
|
350
|
-
const paths = getAIConfigPaths(cwd);
|
|
351
|
-
|
|
352
|
-
// Check if legacy config exists
|
|
353
|
-
const legacyConfig = await loadConfigFile(paths.legacy);
|
|
354
|
-
if (!legacyConfig) {
|
|
355
|
-
return; // No legacy config to migrate
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Check if global config already exists
|
|
359
|
-
const globalConfig = await loadConfigFile(paths.global);
|
|
360
|
-
if (globalConfig) {
|
|
361
|
-
// Global config exists, don't overwrite it
|
|
362
|
-
console.log('Legacy config found but global config already exists. Skipping migration.');
|
|
363
|
-
console.log(`You can manually delete ${paths.legacy} if migration is complete.`);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Migrate to global config
|
|
368
|
-
await fs.mkdir(path.dirname(paths.global), { recursive: true });
|
|
369
|
-
await fs.writeFile(paths.global, JSON.stringify(legacyConfig, null, 2) + '\n', 'utf8');
|
|
370
|
-
|
|
371
|
-
console.log(`✓ Migrated configuration from ${paths.legacy} to ${paths.global}`);
|
|
372
|
-
console.log(` You can now safely delete the legacy file: ${paths.legacy}`);
|
|
373
|
-
},
|
|
374
|
-
(error: any) => new Error(`Failed to migrate legacy config: ${error.message}`)
|
|
375
|
-
);
|
|
376
|
-
};
|
package/src/config/targets.ts
CHANGED
|
@@ -113,14 +113,3 @@ export const isTargetImplemented = (id: string): boolean => {
|
|
|
113
113
|
* Utility type for target IDs
|
|
114
114
|
*/
|
|
115
115
|
export type TargetID = ReturnType<typeof getAllTargetIDs>[number];
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Legacy aliases for backward compatibility
|
|
119
|
-
* @deprecated Use getAllTargets() instead
|
|
120
|
-
*/
|
|
121
|
-
export const ALL_TARGETS = getAllTargets;
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @deprecated Use getImplementedTargets() instead
|
|
125
|
-
*/
|
|
126
|
-
export const IMPLEMENTED_TARGETS = getImplementedTargets;
|