@sylphx/flow 1.8.1 → 2.0.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 +79 -0
- package/UPGRADE.md +140 -0
- package/assets/output-styles/silent.md +4 -0
- package/package.json +2 -1
- package/src/commands/flow/execute-v2.ts +278 -0
- package/src/commands/flow/execute.ts +1 -18
- package/src/commands/flow/types.ts +3 -2
- package/src/commands/flow-command.ts +32 -69
- package/src/commands/flow-orchestrator.ts +18 -55
- package/src/commands/run-command.ts +12 -6
- package/src/commands/settings-command.ts +529 -0
- package/src/core/attach-manager.ts +482 -0
- package/src/core/backup-manager.ts +308 -0
- package/src/core/cleanup-handler.ts +166 -0
- package/src/core/flow-executor.ts +323 -0
- package/src/core/git-stash-manager.ts +133 -0
- package/src/core/project-manager.ts +274 -0
- package/src/core/secrets-manager.ts +229 -0
- package/src/core/session-manager.ts +268 -0
- package/src/core/template-loader.ts +189 -0
- package/src/core/upgrade-manager.ts +79 -47
- package/src/index.ts +13 -27
- package/src/services/first-run-setup.ts +220 -0
- package/src/services/global-config.ts +337 -0
- package/src/utils/__tests__/package-manager-detector.test.ts +163 -0
- package/src/utils/agent-enhancer.ts +40 -22
- package/src/utils/errors.ts +9 -0
- package/src/utils/package-manager-detector.ts +139 -0
package/src/index.ts
CHANGED
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
doctorCommand,
|
|
17
17
|
upgradeCommand,
|
|
18
18
|
} from './commands/flow-command.js';
|
|
19
|
-
import {
|
|
19
|
+
import { settingsCommand } from './commands/settings-command.js';
|
|
20
|
+
import { executeFlow } from './commands/flow/execute-v2.js';
|
|
21
|
+
import { UserCancelledError } from './utils/errors.js';
|
|
20
22
|
|
|
21
23
|
// Read version from package.json
|
|
22
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -52,36 +54,13 @@ export function createCLI(): Command {
|
|
|
52
54
|
// This allows `sylphx-flow "prompt"` instead of requiring `sylphx-flow flow "prompt"`
|
|
53
55
|
program
|
|
54
56
|
.argument('[prompt]', 'Prompt to execute with agent (optional, supports @file.txt for file input)')
|
|
55
|
-
.option('--init-only', 'Only initialize, do not run')
|
|
56
|
-
.option('--run-only', 'Only run, skip initialization')
|
|
57
|
-
.option('--sync', 'Synchronize with Flow templates (delete and re-install template files)')
|
|
58
|
-
.option('--upgrade', 'Upgrade Sylphx Flow to latest version')
|
|
59
|
-
.option('--upgrade-target', 'Upgrade target platform (Claude Code/OpenCode)')
|
|
60
|
-
.option('--quick', 'Quick mode: use saved defaults and skip all prompts')
|
|
61
|
-
.option('--select-provider', 'Prompt to select provider each run')
|
|
62
|
-
.option('--select-agent', 'Prompt to select agent each run')
|
|
63
|
-
.option('--use-defaults', 'Skip prompts, use saved defaults')
|
|
64
|
-
.option('--provider <provider>', 'Override provider for this run (anthropic|z.ai|kimi)')
|
|
65
|
-
.option('--target <type>', 'Target platform (opencode, claude-code, auto-detect)')
|
|
66
|
-
.option('--verbose', 'Show detailed output')
|
|
67
|
-
.option('--dry-run', 'Show what would be done without making changes')
|
|
68
|
-
.option('--no-mcp', 'Skip MCP installation')
|
|
69
|
-
.option('--no-agents', 'Skip agents installation')
|
|
70
|
-
.option('--no-rules', 'Skip rules installation')
|
|
71
|
-
.option('--no-output-styles', 'Skip output styles installation')
|
|
72
|
-
.option('--no-slash-commands', 'Skip slash commands installation')
|
|
73
|
-
.option('--no-hooks', 'Skip hooks setup')
|
|
74
57
|
.option('--agent <name>', 'Agent to use (default: coder)', 'coder')
|
|
75
58
|
.option('--agent-file <path>', 'Load agent from specific file')
|
|
59
|
+
.option('--verbose', 'Show detailed output')
|
|
60
|
+
.option('--dry-run', 'Show what would be done without making changes')
|
|
76
61
|
.option('-p, --print', 'Headless print mode (output only, no interactive)')
|
|
77
62
|
.option('-c, --continue', 'Continue previous conversation (requires print mode)')
|
|
78
|
-
|
|
79
|
-
// Loop mode options
|
|
80
|
-
.option('--loop [seconds]', 'Loop mode: wait N seconds between runs (default: 0 = immediate)', (value) => {
|
|
81
|
-
// If no value provided, default to 0 (no wait time)
|
|
82
|
-
return value ? parseInt(value) : 0;
|
|
83
|
-
})
|
|
84
|
-
.option('--max-runs <count>', 'Maximum iterations before stopping (default: infinite)', parseInt)
|
|
63
|
+
.option('--merge', 'Merge Flow settings with existing settings (default: replace all)')
|
|
85
64
|
|
|
86
65
|
.action(async (prompt, options) => {
|
|
87
66
|
await executeFlow(prompt, options);
|
|
@@ -94,6 +73,7 @@ export function createCLI(): Command {
|
|
|
94
73
|
program.addCommand(doctorCommand);
|
|
95
74
|
program.addCommand(upgradeCommand);
|
|
96
75
|
program.addCommand(hookCommand);
|
|
76
|
+
program.addCommand(settingsCommand);
|
|
97
77
|
|
|
98
78
|
return program;
|
|
99
79
|
}
|
|
@@ -168,6 +148,12 @@ function setupGlobalErrorHandling(): void {
|
|
|
168
148
|
*/
|
|
169
149
|
function handleCommandError(error: unknown): void {
|
|
170
150
|
if (error instanceof Error) {
|
|
151
|
+
// Handle user cancellation gracefully
|
|
152
|
+
if (error instanceof UserCancelledError) {
|
|
153
|
+
console.log('\n⚠️ Operation cancelled by user\n');
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
171
157
|
// Handle Commander.js specific errors
|
|
172
158
|
if (error.name === 'CommanderError') {
|
|
173
159
|
const commanderError = error as any;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First Run Setup
|
|
3
|
+
* Quick configuration wizard for new users
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import { GlobalConfigService } from './global-config.js';
|
|
9
|
+
import { UserCancelledError } from '../utils/errors.js';
|
|
10
|
+
|
|
11
|
+
export interface QuickSetupResult {
|
|
12
|
+
target: 'claude-code' | 'opencode';
|
|
13
|
+
provider?: 'default' | 'kimi' | 'zai' | 'ask-every-time';
|
|
14
|
+
mcpServers: string[];
|
|
15
|
+
apiKeys: Record<string, Record<string, string>>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class FirstRunSetup {
|
|
19
|
+
private configService: GlobalConfigService;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
this.configService = new GlobalConfigService();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run setup wizard
|
|
27
|
+
*/
|
|
28
|
+
async run(): Promise<QuickSetupResult> {
|
|
29
|
+
try {
|
|
30
|
+
console.log(chalk.cyan.bold('\n╭─────────────────────────────────────────────────╮'));
|
|
31
|
+
console.log(chalk.cyan.bold('│ │'));
|
|
32
|
+
console.log(chalk.cyan.bold('│ Welcome to Sylphx Flow! │'));
|
|
33
|
+
console.log(chalk.cyan.bold('│ Let\'s configure your environment │'));
|
|
34
|
+
console.log(chalk.cyan.bold('│ │'));
|
|
35
|
+
console.log(chalk.cyan.bold('╰─────────────────────────────────────────────────╯\n'));
|
|
36
|
+
|
|
37
|
+
// Step 1: Select target platform
|
|
38
|
+
console.log(chalk.cyan('🔧 Setup (1/3) - Target Platform\n'));
|
|
39
|
+
const { target } = await inquirer.prompt([
|
|
40
|
+
{
|
|
41
|
+
type: 'list',
|
|
42
|
+
name: 'target',
|
|
43
|
+
message: 'Select your preferred platform:',
|
|
44
|
+
choices: [
|
|
45
|
+
{ name: 'Claude Code', value: 'claude-code' },
|
|
46
|
+
{ name: 'OpenCode', value: 'opencode' },
|
|
47
|
+
],
|
|
48
|
+
default: 'claude-code',
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
let provider: string | undefined = 'ask-every-time';
|
|
53
|
+
const apiKeys: Record<string, Record<string, string>> = {};
|
|
54
|
+
|
|
55
|
+
// Step 2: Provider setup (Claude Code only)
|
|
56
|
+
if (target === 'claude-code') {
|
|
57
|
+
console.log(chalk.cyan('\n🔧 Setup (2/3) - Provider\n'));
|
|
58
|
+
const { selectedProvider } = await inquirer.prompt([
|
|
59
|
+
{
|
|
60
|
+
type: 'list',
|
|
61
|
+
name: 'selectedProvider',
|
|
62
|
+
message: 'Select your preferred provider:',
|
|
63
|
+
choices: [
|
|
64
|
+
{ name: 'Ask me every time', value: 'ask-every-time' },
|
|
65
|
+
{ name: 'Default (Claude Code built-in)', value: 'default' },
|
|
66
|
+
{ name: 'Kimi (requires API key)', value: 'kimi' },
|
|
67
|
+
{ name: 'Z.ai (requires API key)', value: 'zai' },
|
|
68
|
+
],
|
|
69
|
+
default: 'ask-every-time',
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
provider = selectedProvider;
|
|
74
|
+
|
|
75
|
+
// Configure API key if needed
|
|
76
|
+
if (provider === 'kimi' || provider === 'zai') {
|
|
77
|
+
const { apiKey } = await inquirer.prompt([
|
|
78
|
+
{
|
|
79
|
+
type: 'password',
|
|
80
|
+
name: 'apiKey',
|
|
81
|
+
message: provider === 'kimi' ? 'Enter Kimi API key:' : 'Enter Z.ai API key:',
|
|
82
|
+
mask: '*',
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
apiKeys[provider] = { apiKey };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Step 3: MCP Servers
|
|
91
|
+
const stepNumber = target === 'claude-code' ? '3/3' : '2/2';
|
|
92
|
+
console.log(chalk.cyan('\n🔧 Setup (' + stepNumber + ') - MCP Servers\n'));
|
|
93
|
+
|
|
94
|
+
const { mcpServers } = await inquirer.prompt([
|
|
95
|
+
{
|
|
96
|
+
type: 'checkbox',
|
|
97
|
+
name: 'mcpServers',
|
|
98
|
+
message: 'Select MCP servers to enable:',
|
|
99
|
+
choices: [
|
|
100
|
+
{ name: 'GitHub Code Search (grep.app)', value: 'grep', checked: true },
|
|
101
|
+
{ name: 'Context7 Docs', value: 'context7', checked: true },
|
|
102
|
+
{ name: 'Playwright Browser Control', value: 'playwright', checked: true },
|
|
103
|
+
{ name: 'GitHub (requires GITHUB_TOKEN)', value: 'github' },
|
|
104
|
+
{ name: 'Notion (requires NOTION_API_KEY)', value: 'notion' },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
// Configure MCP API keys
|
|
110
|
+
const mcpServerRequirements: Record<string, string[]> = {
|
|
111
|
+
github: ['GITHUB_TOKEN'],
|
|
112
|
+
notion: ['NOTION_API_KEY'],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
for (const serverKey of mcpServers) {
|
|
116
|
+
const requirements = mcpServerRequirements[serverKey];
|
|
117
|
+
if (requirements) {
|
|
118
|
+
const configMessage = 'Configure ' + requirements[0] + ' for ' + serverKey + '?';
|
|
119
|
+
const { shouldConfigure } = await inquirer.prompt([
|
|
120
|
+
{
|
|
121
|
+
type: 'confirm',
|
|
122
|
+
name: 'shouldConfigure',
|
|
123
|
+
message: configMessage,
|
|
124
|
+
default: true,
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
if (shouldConfigure) {
|
|
129
|
+
const questions = requirements.map((key) => {
|
|
130
|
+
return {
|
|
131
|
+
type: 'password' as const,
|
|
132
|
+
name: key,
|
|
133
|
+
message: 'Enter ' + key + ':',
|
|
134
|
+
mask: '*',
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
const answers = await inquirer.prompt(questions);
|
|
138
|
+
|
|
139
|
+
apiKeys[serverKey] = answers;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Save configuration
|
|
145
|
+
await this.saveConfiguration({
|
|
146
|
+
target,
|
|
147
|
+
provider,
|
|
148
|
+
mcpServers,
|
|
149
|
+
apiKeys,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
console.log(chalk.green('\n✓ Configuration saved to ~/.sylphx-flow/\n'));
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
target,
|
|
156
|
+
provider: provider as any,
|
|
157
|
+
mcpServers,
|
|
158
|
+
apiKeys,
|
|
159
|
+
};
|
|
160
|
+
} catch (error: any) {
|
|
161
|
+
// Handle user cancellation (Ctrl+C)
|
|
162
|
+
if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
|
|
163
|
+
throw new UserCancelledError('Setup cancelled by user');
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Save configuration to global config files
|
|
171
|
+
*/
|
|
172
|
+
private async saveConfiguration(result: QuickSetupResult): Promise<void> {
|
|
173
|
+
// Save global settings
|
|
174
|
+
await this.configService.saveSettings({
|
|
175
|
+
version: '1.0.0',
|
|
176
|
+
defaultTarget: result.target,
|
|
177
|
+
firstRun: false,
|
|
178
|
+
lastUpdated: new Date().toISOString(),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Save provider config (Claude Code)
|
|
182
|
+
if (result.target === 'claude-code' && result.provider) {
|
|
183
|
+
const providerConfig = await this.configService.loadProviderConfig();
|
|
184
|
+
providerConfig.claudeCode.defaultProvider = result.provider;
|
|
185
|
+
|
|
186
|
+
// Save API keys
|
|
187
|
+
if (result.provider === 'kimi' && result.apiKeys.kimi) {
|
|
188
|
+
providerConfig.claudeCode.providers.kimi = {
|
|
189
|
+
apiKey: result.apiKeys.kimi.apiKey,
|
|
190
|
+
enabled: true,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (result.provider === 'zai' && result.apiKeys.zai) {
|
|
194
|
+
providerConfig.claudeCode.providers.zai = {
|
|
195
|
+
apiKey: result.apiKeys.zai.apiKey,
|
|
196
|
+
enabled: true,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await this.configService.saveProviderConfig(providerConfig);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Save MCP config
|
|
204
|
+
const mcpConfig = await this.configService.loadMCPConfig();
|
|
205
|
+
for (const serverKey of result.mcpServers) {
|
|
206
|
+
mcpConfig.servers[serverKey] = {
|
|
207
|
+
enabled: true,
|
|
208
|
+
env: result.apiKeys[serverKey] || {},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
await this.configService.saveMCPConfig(mcpConfig);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if we should run quick setup
|
|
216
|
+
*/
|
|
217
|
+
async shouldRun(): Promise<boolean> {
|
|
218
|
+
return await this.configService.isFirstRun();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Configuration Service
|
|
3
|
+
* Manages all Flow settings in ~/.sylphx-flow/
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
|
|
11
|
+
export interface GlobalSettings {
|
|
12
|
+
version: string;
|
|
13
|
+
defaultTarget?: 'claude-code' | 'opencode';
|
|
14
|
+
defaultAgent?: string; // Default agent to use (e.g., 'coder', 'writer', 'reviewer', 'orchestrator')
|
|
15
|
+
firstRun: boolean;
|
|
16
|
+
lastUpdated: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AgentConfig {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RuleConfig {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OutputStyleConfig {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FlowConfig {
|
|
32
|
+
version: string;
|
|
33
|
+
agents: Record<string, AgentConfig>; // e.g., { coder: { enabled: true }, writer: { enabled: false } }
|
|
34
|
+
rules: Record<string, RuleConfig>; // e.g., { core: { enabled: true }, workspace: { enabled: true } }
|
|
35
|
+
outputStyles: Record<string, OutputStyleConfig>; // e.g., { silent: { enabled: true } }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ProviderConfig {
|
|
39
|
+
claudeCode: {
|
|
40
|
+
defaultProvider: 'default' | 'kimi' | 'zai' | 'ask-every-time';
|
|
41
|
+
providers: {
|
|
42
|
+
kimi?: {
|
|
43
|
+
apiKey?: string;
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
};
|
|
46
|
+
zai?: {
|
|
47
|
+
apiKey?: string;
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface MCPServerConfig {
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
command?: string;
|
|
57
|
+
args?: string[];
|
|
58
|
+
env?: Record<string, string>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface MCPConfig {
|
|
62
|
+
version: string;
|
|
63
|
+
servers: Record<string, MCPServerConfig>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class GlobalConfigService {
|
|
67
|
+
private flowHomeDir: string;
|
|
68
|
+
|
|
69
|
+
constructor() {
|
|
70
|
+
this.flowHomeDir = path.join(os.homedir(), '.sylphx-flow');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get Flow home directory
|
|
75
|
+
*/
|
|
76
|
+
getFlowHomeDir(): string {
|
|
77
|
+
return this.flowHomeDir;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Initialize Flow home directory structure
|
|
82
|
+
*/
|
|
83
|
+
async initialize(): Promise<void> {
|
|
84
|
+
const dirs = [
|
|
85
|
+
this.flowHomeDir,
|
|
86
|
+
path.join(this.flowHomeDir, 'sessions'),
|
|
87
|
+
path.join(this.flowHomeDir, 'backups'),
|
|
88
|
+
path.join(this.flowHomeDir, 'secrets'),
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
for (const dir of dirs) {
|
|
92
|
+
await fs.mkdir(dir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get global settings file path
|
|
98
|
+
*/
|
|
99
|
+
private getSettingsPath(): string {
|
|
100
|
+
return path.join(this.flowHomeDir, 'settings.json');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get provider config file path
|
|
105
|
+
*/
|
|
106
|
+
private getProviderConfigPath(): string {
|
|
107
|
+
return path.join(this.flowHomeDir, 'provider-config.json');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get MCP config file path
|
|
112
|
+
*/
|
|
113
|
+
private getMCPConfigPath(): string {
|
|
114
|
+
return path.join(this.flowHomeDir, 'mcp-config.json');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get Flow config file path
|
|
119
|
+
*/
|
|
120
|
+
private getFlowConfigPath(): string {
|
|
121
|
+
return path.join(this.flowHomeDir, 'flow-config.json');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if this is first run
|
|
126
|
+
*/
|
|
127
|
+
async isFirstRun(): Promise<boolean> {
|
|
128
|
+
const settingsPath = this.getSettingsPath();
|
|
129
|
+
if (!existsSync(settingsPath)) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const settings = await this.loadSettings();
|
|
135
|
+
return settings.firstRun !== false;
|
|
136
|
+
} catch {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Load global settings
|
|
143
|
+
*/
|
|
144
|
+
async loadSettings(): Promise<GlobalSettings> {
|
|
145
|
+
const settingsPath = this.getSettingsPath();
|
|
146
|
+
|
|
147
|
+
if (!existsSync(settingsPath)) {
|
|
148
|
+
return {
|
|
149
|
+
version: '1.0.0',
|
|
150
|
+
firstRun: true,
|
|
151
|
+
lastUpdated: new Date().toISOString(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const data = await fs.readFile(settingsPath, 'utf-8');
|
|
156
|
+
return JSON.parse(data);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Save global settings
|
|
161
|
+
*/
|
|
162
|
+
async saveSettings(settings: GlobalSettings): Promise<void> {
|
|
163
|
+
await this.initialize();
|
|
164
|
+
const settingsPath = this.getSettingsPath();
|
|
165
|
+
settings.lastUpdated = new Date().toISOString();
|
|
166
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Load provider config
|
|
171
|
+
*/
|
|
172
|
+
async loadProviderConfig(): Promise<ProviderConfig> {
|
|
173
|
+
const configPath = this.getProviderConfigPath();
|
|
174
|
+
|
|
175
|
+
if (!existsSync(configPath)) {
|
|
176
|
+
return {
|
|
177
|
+
claudeCode: {
|
|
178
|
+
defaultProvider: 'ask-every-time',
|
|
179
|
+
providers: {
|
|
180
|
+
kimi: { enabled: false },
|
|
181
|
+
zai: { enabled: false },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const data = await fs.readFile(configPath, 'utf-8');
|
|
188
|
+
return JSON.parse(data);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Save provider config
|
|
193
|
+
*/
|
|
194
|
+
async saveProviderConfig(config: ProviderConfig): Promise<void> {
|
|
195
|
+
await this.initialize();
|
|
196
|
+
const configPath = this.getProviderConfigPath();
|
|
197
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Load MCP config
|
|
202
|
+
*/
|
|
203
|
+
async loadMCPConfig(): Promise<MCPConfig> {
|
|
204
|
+
const configPath = this.getMCPConfigPath();
|
|
205
|
+
|
|
206
|
+
if (!existsSync(configPath)) {
|
|
207
|
+
return {
|
|
208
|
+
version: '1.0.0',
|
|
209
|
+
servers: {},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const data = await fs.readFile(configPath, 'utf-8');
|
|
214
|
+
return JSON.parse(data);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Save MCP config
|
|
219
|
+
*/
|
|
220
|
+
async saveMCPConfig(config: MCPConfig): Promise<void> {
|
|
221
|
+
await this.initialize();
|
|
222
|
+
const configPath = this.getMCPConfigPath();
|
|
223
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get enabled MCP servers
|
|
228
|
+
*/
|
|
229
|
+
async getEnabledMCPServers(): Promise<Record<string, MCPServerConfig>> {
|
|
230
|
+
const config = await this.loadMCPConfig();
|
|
231
|
+
const enabled: Record<string, MCPServerConfig> = {};
|
|
232
|
+
|
|
233
|
+
for (const [name, serverConfig] of Object.entries(config.servers)) {
|
|
234
|
+
if (serverConfig.enabled) {
|
|
235
|
+
enabled[name] = serverConfig;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return enabled;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Load Flow config (agents, rules, output styles)
|
|
244
|
+
*/
|
|
245
|
+
async loadFlowConfig(): Promise<FlowConfig> {
|
|
246
|
+
const configPath = this.getFlowConfigPath();
|
|
247
|
+
|
|
248
|
+
if (!existsSync(configPath)) {
|
|
249
|
+
// Default: all agents, all rules, all output styles enabled
|
|
250
|
+
return {
|
|
251
|
+
version: '1.0.0',
|
|
252
|
+
agents: {
|
|
253
|
+
coder: { enabled: true },
|
|
254
|
+
writer: { enabled: true },
|
|
255
|
+
reviewer: { enabled: true },
|
|
256
|
+
orchestrator: { enabled: true },
|
|
257
|
+
},
|
|
258
|
+
rules: {
|
|
259
|
+
core: { enabled: true },
|
|
260
|
+
'code-standards': { enabled: true },
|
|
261
|
+
workspace: { enabled: true },
|
|
262
|
+
},
|
|
263
|
+
outputStyles: {
|
|
264
|
+
silent: { enabled: true },
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const data = await fs.readFile(configPath, 'utf-8');
|
|
270
|
+
return JSON.parse(data);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Save Flow config
|
|
275
|
+
*/
|
|
276
|
+
async saveFlowConfig(config: FlowConfig): Promise<void> {
|
|
277
|
+
await this.initialize();
|
|
278
|
+
const configPath = this.getFlowConfigPath();
|
|
279
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get enabled agents
|
|
284
|
+
*/
|
|
285
|
+
async getEnabledAgents(): Promise<string[]> {
|
|
286
|
+
const config = await this.loadFlowConfig();
|
|
287
|
+
return Object.entries(config.agents)
|
|
288
|
+
.filter(([_, agentConfig]) => agentConfig.enabled)
|
|
289
|
+
.map(([name]) => name);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get enabled rules
|
|
294
|
+
*/
|
|
295
|
+
async getEnabledRules(): Promise<string[]> {
|
|
296
|
+
const config = await this.loadFlowConfig();
|
|
297
|
+
return Object.entries(config.rules)
|
|
298
|
+
.filter(([_, ruleConfig]) => ruleConfig.enabled)
|
|
299
|
+
.map(([name]) => name);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get enabled output styles
|
|
304
|
+
*/
|
|
305
|
+
async getEnabledOutputStyles(): Promise<string[]> {
|
|
306
|
+
const config = await this.loadFlowConfig();
|
|
307
|
+
return Object.entries(config.outputStyles)
|
|
308
|
+
.filter(([_, styleConfig]) => styleConfig.enabled)
|
|
309
|
+
.map(([name]) => name);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Update default target
|
|
314
|
+
*/
|
|
315
|
+
async setDefaultTarget(target: 'claude-code' | 'opencode'): Promise<void> {
|
|
316
|
+
const settings = await this.loadSettings();
|
|
317
|
+
settings.defaultTarget = target;
|
|
318
|
+
await this.saveSettings(settings);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get default target
|
|
323
|
+
*/
|
|
324
|
+
async getDefaultTarget(): Promise<'claude-code' | 'opencode' | undefined> {
|
|
325
|
+
const settings = await this.loadSettings();
|
|
326
|
+
return settings.defaultTarget;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Mark first run as complete
|
|
331
|
+
*/
|
|
332
|
+
async markFirstRunComplete(): Promise<void> {
|
|
333
|
+
const settings = await this.loadSettings();
|
|
334
|
+
settings.firstRun = false;
|
|
335
|
+
await this.saveSettings(settings);
|
|
336
|
+
}
|
|
337
|
+
}
|