@sylphx/flow 1.8.2 → 2.1.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/UPGRADE.md +151 -0
  3. package/package.json +11 -6
  4. package/src/commands/flow/execute-v2.ts +372 -0
  5. package/src/commands/flow/execute.ts +1 -18
  6. package/src/commands/flow/types.ts +3 -2
  7. package/src/commands/flow-command.ts +32 -69
  8. package/src/commands/flow-orchestrator.ts +18 -55
  9. package/src/commands/run-command.ts +12 -6
  10. package/src/commands/settings-command.ts +536 -0
  11. package/src/config/ai-config.ts +2 -69
  12. package/src/config/targets.ts +0 -11
  13. package/src/core/attach-manager.ts +495 -0
  14. package/src/core/backup-manager.ts +308 -0
  15. package/src/core/cleanup-handler.ts +166 -0
  16. package/src/core/flow-executor.ts +323 -0
  17. package/src/core/git-stash-manager.ts +133 -0
  18. package/src/core/installers/file-installer.ts +0 -57
  19. package/src/core/installers/mcp-installer.ts +0 -33
  20. package/src/core/project-manager.ts +274 -0
  21. package/src/core/secrets-manager.ts +229 -0
  22. package/src/core/session-manager.ts +268 -0
  23. package/src/core/template-loader.ts +189 -0
  24. package/src/core/upgrade-manager.ts +79 -47
  25. package/src/index.ts +15 -29
  26. package/src/services/auto-upgrade.ts +248 -0
  27. package/src/services/first-run-setup.ts +220 -0
  28. package/src/services/global-config.ts +337 -0
  29. package/src/services/target-installer.ts +254 -0
  30. package/src/targets/claude-code.ts +5 -7
  31. package/src/targets/opencode.ts +6 -26
  32. package/src/utils/__tests__/package-manager-detector.test.ts +163 -0
  33. package/src/utils/agent-enhancer.ts +40 -22
  34. package/src/utils/errors.ts +9 -0
  35. package/src/utils/package-manager-detector.ts +139 -0
  36. package/src/utils/prompt-helpers.ts +48 -0
  37. package/src/utils/target-selection.ts +169 -0
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Sylphx Flow - Legacy CLI
4
- * Project initialization and development flow management
3
+ * Sylphx Flow CLI
4
+ * AI-powered development flow management
5
5
  */
6
6
 
7
7
  import { readFileSync } from 'node:fs';
@@ -16,7 +16,9 @@ import {
16
16
  doctorCommand,
17
17
  upgradeCommand,
18
18
  } from './commands/flow-command.js';
19
- import { executeFlow } from './commands/flow/execute.js';
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,248 @@
1
+ /**
2
+ * Auto-Upgrade Service
3
+ * Automatically checks and upgrades Flow and target CLI before each execution
4
+ */
5
+
6
+ import { exec } from 'node:child_process';
7
+ import { promisify } from 'node:util';
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import chalk from 'chalk';
11
+ import ora from 'ora';
12
+ import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
13
+ import { TargetInstaller } from './target-installer.js';
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ export interface UpgradeStatus {
18
+ flowNeedsUpgrade: boolean;
19
+ targetNeedsUpgrade: boolean;
20
+ flowVersion: { current: string; latest: string } | null;
21
+ targetVersion: { current: string; latest: string } | null;
22
+ }
23
+
24
+ export interface AutoUpgradeOptions {
25
+ verbose?: boolean;
26
+ skipFlow?: boolean;
27
+ skipTarget?: boolean;
28
+ }
29
+
30
+ export class AutoUpgrade {
31
+ private projectPath: string;
32
+ private options: AutoUpgradeOptions;
33
+ private targetInstaller: TargetInstaller;
34
+
35
+ constructor(projectPath: string = process.cwd(), options: AutoUpgradeOptions = {}) {
36
+ this.projectPath = projectPath;
37
+ this.options = options;
38
+ this.targetInstaller = new TargetInstaller(projectPath);
39
+ }
40
+
41
+ /**
42
+ * Check for available upgrades for Flow and target CLI
43
+ * @param targetId - Optional target CLI ID to check for upgrades
44
+ * @returns Upgrade status indicating what needs upgrading
45
+ */
46
+ async checkForUpgrades(targetId?: string): Promise<UpgradeStatus> {
47
+ const [flowVersion, targetVersion] = await Promise.all([
48
+ this.options.skipFlow ? null : this.checkFlowVersion(),
49
+ this.options.skipTarget || !targetId ? null : this.checkTargetVersion(targetId),
50
+ ]);
51
+
52
+ return {
53
+ flowNeedsUpgrade: !!flowVersion,
54
+ targetNeedsUpgrade: !!targetVersion,
55
+ flowVersion,
56
+ targetVersion,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Check Flow version
62
+ */
63
+ private async checkFlowVersion(): Promise<{ current: string; latest: string } | null> {
64
+ try {
65
+ // Get current version from package.json
66
+ const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
67
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
68
+ const currentVersion = packageJson.version;
69
+
70
+ // Get latest version from npm
71
+ const { stdout } = await execAsync('npm view @sylphx/flow version');
72
+ const latestVersion = stdout.trim();
73
+
74
+ if (currentVersion !== latestVersion) {
75
+ return { current: currentVersion, latest: latestVersion };
76
+ }
77
+
78
+ return null;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Check target CLI version
86
+ */
87
+ private async checkTargetVersion(
88
+ targetId: string
89
+ ): Promise<{ current: string; latest: string } | null> {
90
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
91
+ if (!installation) {
92
+ return null;
93
+ }
94
+
95
+ try {
96
+ // Get current version
97
+ const { stdout: currentOutput } = await execAsync(installation.checkCommand);
98
+ const currentMatch = currentOutput.match(/v?(\d+\.\d+\.\d+)/);
99
+ if (!currentMatch) {
100
+ return null;
101
+ }
102
+ const currentVersion = currentMatch[1];
103
+
104
+ // Get latest version from npm
105
+ const { stdout: latestOutput } = await execAsync(`npm view ${installation.package} version`);
106
+ const latestVersion = latestOutput.trim();
107
+
108
+ if (currentVersion !== latestVersion) {
109
+ return { current: currentVersion, latest: latestVersion };
110
+ }
111
+
112
+ return null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Upgrade Flow to latest version using detected package manager
120
+ * @returns True if upgrade successful, false otherwise
121
+ */
122
+ async upgradeFlow(): Promise<boolean> {
123
+ const packageManager = detectPackageManager(this.projectPath);
124
+ const spinner = ora('Upgrading Flow...').start();
125
+
126
+ try {
127
+ const upgradeCmd = getUpgradeCommand('@sylphx/flow', packageManager);
128
+ await execAsync(upgradeCmd);
129
+
130
+ spinner.succeed(chalk.green('✓ Flow upgraded to latest version'));
131
+ return true;
132
+ } catch (error) {
133
+ spinner.fail(chalk.red('✗ Flow upgrade failed'));
134
+
135
+ if (this.options.verbose) {
136
+ console.error(error);
137
+ }
138
+
139
+ return false;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Upgrade target CLI to latest version
145
+ * Tries built-in upgrade command first, falls back to package manager
146
+ * @param targetId - Target CLI ID to upgrade
147
+ * @returns True if upgrade successful, false otherwise
148
+ */
149
+ async upgradeTarget(targetId: string): Promise<boolean> {
150
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
151
+ if (!installation) {
152
+ return false;
153
+ }
154
+
155
+ const packageManager = detectPackageManager(this.projectPath);
156
+ const spinner = ora(`Upgrading ${installation.name}...`).start();
157
+
158
+ try {
159
+ // For Claude Code, use built-in update command if available
160
+ if (targetId === 'claude-code') {
161
+ try {
162
+ await execAsync('claude update');
163
+ spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
164
+ return true;
165
+ } catch {
166
+ // Fall back to npm upgrade
167
+ }
168
+ }
169
+
170
+ // For OpenCode, use built-in upgrade command if available
171
+ if (targetId === 'opencode') {
172
+ try {
173
+ await execAsync('opencode upgrade');
174
+ spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
175
+ return true;
176
+ } catch {
177
+ // Fall back to npm upgrade
178
+ }
179
+ }
180
+
181
+ // Fall back to npm/bun/pnpm/yarn upgrade
182
+ const upgradeCmd = getUpgradeCommand(installation.package, packageManager);
183
+ await execAsync(upgradeCmd);
184
+
185
+ spinner.succeed(chalk.green(`✓ ${installation.name} upgraded`));
186
+ return true;
187
+ } catch (error) {
188
+ spinner.fail(chalk.red(`✗ ${installation.name} upgrade failed`));
189
+
190
+ if (this.options.verbose) {
191
+ console.error(error);
192
+ }
193
+
194
+ return false;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Run auto-upgrade check and upgrade if needed
200
+ * Shows upgrade status and performs upgrades automatically
201
+ * @param targetId - Optional target CLI ID to check and upgrade
202
+ */
203
+ async runAutoUpgrade(targetId?: string): Promise<void> {
204
+ console.log(chalk.cyan('🔄 Checking for updates...\n'));
205
+
206
+ const status = await this.checkForUpgrades(targetId);
207
+
208
+ // Show upgrade status
209
+ if (status.flowNeedsUpgrade && status.flowVersion) {
210
+ console.log(
211
+ chalk.yellow(
212
+ `📦 Flow update available: ${status.flowVersion.current} → ${status.flowVersion.latest}`
213
+ )
214
+ );
215
+ } else if (!this.options.skipFlow) {
216
+ console.log(chalk.green('✓ Flow is up to date'));
217
+ }
218
+
219
+ if (status.targetNeedsUpgrade && status.targetVersion && targetId) {
220
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
221
+ console.log(
222
+ chalk.yellow(
223
+ `📦 ${installation?.name} update available: ${status.targetVersion.current} → ${status.targetVersion.latest}`
224
+ )
225
+ );
226
+ } else if (!this.options.skipTarget && targetId) {
227
+ const installation = this.targetInstaller.getInstallationInfo(targetId);
228
+ console.log(chalk.green(`✓ ${installation?.name} is up to date`));
229
+ }
230
+
231
+ // Perform upgrades if needed
232
+ if (status.flowNeedsUpgrade || status.targetNeedsUpgrade) {
233
+ console.log(chalk.cyan('\n📦 Installing updates...\n'));
234
+
235
+ if (status.flowNeedsUpgrade) {
236
+ await this.upgradeFlow();
237
+ }
238
+
239
+ if (status.targetNeedsUpgrade && targetId) {
240
+ await this.upgradeTarget(targetId);
241
+ }
242
+
243
+ console.log(chalk.green('\n✓ All tools upgraded\n'));
244
+ } else {
245
+ console.log();
246
+ }
247
+ }
248
+ }
@@ -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
+ }