@sylphx/flow 2.0.0 → 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.
- package/CHANGELOG.md +100 -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 +14 -1
- 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/src/targets/opencode.ts
CHANGED
|
@@ -3,8 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { getRulesPath, ruleFileExists } from '../config/rules.js';
|
|
5
5
|
import { MCP_SERVER_REGISTRY } from '../config/servers.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { installToDirectory, installFile } from '../core/installers/file-installer.js';
|
|
7
|
+
import { createMCPInstaller } from '../core/installers/mcp-installer.js';
|
|
8
8
|
import type { AgentMetadata } from '../types/target-config.types.js';
|
|
9
9
|
import type { CommonOptions, MCPServerConfigUnion, SetupResult, Target } from '../types.js';
|
|
10
10
|
import { getAgentsDir, getOutputStylesDir, getSlashCommandsDir } from '../utils/config/paths.js';
|
|
@@ -232,19 +232,9 @@ export const opencodeTarget: Target = {
|
|
|
232
232
|
* Install agents to .opencode/agent/ directory
|
|
233
233
|
*/
|
|
234
234
|
async setupAgents(cwd: string, options: CommonOptions): Promise<SetupResult> {
|
|
235
|
-
// Clean up old 'commands' directory if it exists (migration from old structure)
|
|
236
|
-
// This ensures OpenCode won't crash with ConfigDirectoryTypoError
|
|
237
|
-
const oldCommandsDir = path.join(cwd, '.opencode/commands');
|
|
238
|
-
try {
|
|
239
|
-
await fs.rm(oldCommandsDir, { recursive: true, force: true });
|
|
240
|
-
} catch {
|
|
241
|
-
// Ignore if doesn't exist
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const installer = new FileInstaller();
|
|
245
235
|
const agentsDir = path.join(cwd, this.config.agentDir);
|
|
246
236
|
|
|
247
|
-
const results = await
|
|
237
|
+
const results = await installToDirectory(
|
|
248
238
|
getAgentsDir(),
|
|
249
239
|
agentsDir,
|
|
250
240
|
async (content, sourcePath) => {
|
|
@@ -336,11 +326,10 @@ export const opencodeTarget: Target = {
|
|
|
336
326
|
throw new Error('Core rules file not found');
|
|
337
327
|
}
|
|
338
328
|
|
|
339
|
-
const installer = new FileInstaller();
|
|
340
329
|
const rulesDestPath = path.join(cwd, this.config.rulesFile);
|
|
341
330
|
const rulePath = getRulesPath('core');
|
|
342
331
|
|
|
343
|
-
await
|
|
332
|
+
await installFile(
|
|
344
333
|
rulePath,
|
|
345
334
|
rulesDestPath,
|
|
346
335
|
async (content) => {
|
|
@@ -371,7 +360,7 @@ export const opencodeTarget: Target = {
|
|
|
371
360
|
}
|
|
372
361
|
|
|
373
362
|
// Install MCP servers
|
|
374
|
-
const installer =
|
|
363
|
+
const installer = createMCPInstaller(this);
|
|
375
364
|
const result = await installer.setupMCP({ ...options, quiet: true });
|
|
376
365
|
|
|
377
366
|
return { count: result.selectedServers.length };
|
|
@@ -386,18 +375,9 @@ export const opencodeTarget: Target = {
|
|
|
386
375
|
return { count: 0 };
|
|
387
376
|
}
|
|
388
377
|
|
|
389
|
-
// Clean up old 'commands' directory if it exists (migration from old structure)
|
|
390
|
-
const oldCommandsDir = path.join(cwd, '.opencode/commands');
|
|
391
|
-
try {
|
|
392
|
-
await fs.rm(oldCommandsDir, { recursive: true, force: true });
|
|
393
|
-
} catch {
|
|
394
|
-
// Ignore if doesn't exist
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const installer = new FileInstaller();
|
|
398
378
|
const slashCommandsDir = path.join(cwd, this.config.slashCommandsDir);
|
|
399
379
|
|
|
400
|
-
const results = await
|
|
380
|
+
const results = await installToDirectory(
|
|
401
381
|
getSlashCommandsDir(),
|
|
402
382
|
slashCommandsDir,
|
|
403
383
|
async (content) => {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Helpers
|
|
3
|
+
* Utilities for handling user prompts and error handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { UserCancelledError } from './errors.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if error is a user cancellation (Ctrl+C or force closed)
|
|
10
|
+
* @param error - Error to check
|
|
11
|
+
* @returns True if error represents user cancellation
|
|
12
|
+
*/
|
|
13
|
+
export function isUserCancellation(error: any): boolean {
|
|
14
|
+
return error?.name === 'ExitPromptError' || error?.message?.includes('force closed');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Handle inquirer prompt errors with consistent error handling
|
|
19
|
+
* Throws UserCancelledError for user cancellations, re-throws other errors
|
|
20
|
+
* @param error - Error from inquirer prompt
|
|
21
|
+
* @param message - Custom cancellation message
|
|
22
|
+
* @throws {UserCancelledError} If user cancelled the prompt
|
|
23
|
+
* @throws Original error if not a cancellation
|
|
24
|
+
*/
|
|
25
|
+
export function handlePromptError(error: any, message: string): never {
|
|
26
|
+
if (isUserCancellation(error)) {
|
|
27
|
+
throw new UserCancelledError(message);
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wrap an inquirer prompt with consistent error handling
|
|
34
|
+
* @param promptFn - Function that returns a promise from inquirer
|
|
35
|
+
* @param errorMessage - Message to use if user cancels
|
|
36
|
+
* @returns Promise with the prompt result
|
|
37
|
+
* @throws {UserCancelledError} If user cancels the prompt
|
|
38
|
+
*/
|
|
39
|
+
export async function withPromptErrorHandling<T>(
|
|
40
|
+
promptFn: () => Promise<T>,
|
|
41
|
+
errorMessage: string
|
|
42
|
+
): Promise<T> {
|
|
43
|
+
try {
|
|
44
|
+
return await promptFn();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
handlePromptError(error, errorMessage);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Target Selection Utilities
|
|
3
|
+
* Shared logic for target selection UI across settings and execution flows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import { TargetInstaller } from '../services/target-installer.js';
|
|
9
|
+
import { handlePromptError } from './prompt-helpers.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Represents a target CLI choice with installation status
|
|
13
|
+
*/
|
|
14
|
+
export interface TargetChoice {
|
|
15
|
+
/** Display name of the target */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Target identifier */
|
|
18
|
+
value: 'claude-code' | 'opencode' | 'cursor';
|
|
19
|
+
/** Whether the target is currently installed */
|
|
20
|
+
installed: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build list of available targets with installation status
|
|
25
|
+
* @param installedTargets - List of currently installed target IDs
|
|
26
|
+
* @returns Array of target choices with installation status
|
|
27
|
+
*/
|
|
28
|
+
export function buildAvailableTargets(installedTargets: string[]): TargetChoice[] {
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
name: 'Claude Code',
|
|
32
|
+
value: 'claude-code',
|
|
33
|
+
installed: installedTargets.includes('claude-code'),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'OpenCode',
|
|
37
|
+
value: 'opencode',
|
|
38
|
+
installed: installedTargets.includes('opencode'),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'Cursor',
|
|
42
|
+
value: 'cursor',
|
|
43
|
+
installed: installedTargets.includes('cursor'),
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Format target choice for display in prompts
|
|
50
|
+
* @param target - Target choice to format
|
|
51
|
+
* @param context - Context where choice is displayed (affects status message)
|
|
52
|
+
* @returns Formatted string with target name and installation status
|
|
53
|
+
*/
|
|
54
|
+
export function formatTargetChoice(target: TargetChoice, context: 'execution' | 'settings'): string {
|
|
55
|
+
const status = target.installed
|
|
56
|
+
? chalk.green(' ✓ installed')
|
|
57
|
+
: context === 'execution'
|
|
58
|
+
? chalk.dim(' (will auto-install)')
|
|
59
|
+
: chalk.dim(' (not installed - will auto-install on first use)');
|
|
60
|
+
|
|
61
|
+
return `${target.name}${status}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Prompt user to select a target
|
|
66
|
+
* @param installedTargets - List of currently installed target IDs
|
|
67
|
+
* @param message - Prompt message to display
|
|
68
|
+
* @param context - Context where prompt is shown (affects status formatting)
|
|
69
|
+
* @returns Selected target ID
|
|
70
|
+
* @throws {UserCancelledError} If user cancels the prompt
|
|
71
|
+
*/
|
|
72
|
+
export async function promptForTargetSelection(
|
|
73
|
+
installedTargets: string[],
|
|
74
|
+
message: string,
|
|
75
|
+
context: 'execution' | 'settings'
|
|
76
|
+
): Promise<string> {
|
|
77
|
+
const availableTargets = buildAvailableTargets(installedTargets);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const { targetId } = await inquirer.prompt([
|
|
81
|
+
{
|
|
82
|
+
type: 'list',
|
|
83
|
+
name: 'targetId',
|
|
84
|
+
message,
|
|
85
|
+
choices: availableTargets.map((target) => ({
|
|
86
|
+
name: formatTargetChoice(target, context),
|
|
87
|
+
value: target.value,
|
|
88
|
+
})),
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
return targetId;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
handlePromptError(error, 'Target selection cancelled');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Prompt for default target with "Ask me every time" option
|
|
100
|
+
* @param installedTargets - List of currently installed target IDs
|
|
101
|
+
* @param currentDefault - Current default target (if any)
|
|
102
|
+
* @returns Selected default target ID or 'ask-every-time'
|
|
103
|
+
* @throws {UserCancelledError} If user cancels the prompt
|
|
104
|
+
*/
|
|
105
|
+
export async function promptForDefaultTarget(
|
|
106
|
+
installedTargets: string[],
|
|
107
|
+
currentDefault?: string
|
|
108
|
+
): Promise<string> {
|
|
109
|
+
const availableTargets = buildAvailableTargets(installedTargets);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const { defaultTarget } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: 'list',
|
|
115
|
+
name: 'defaultTarget',
|
|
116
|
+
message: 'Select default target platform:',
|
|
117
|
+
choices: [
|
|
118
|
+
...availableTargets.map((target) => ({
|
|
119
|
+
name: formatTargetChoice(target, 'settings'),
|
|
120
|
+
value: target.value,
|
|
121
|
+
})),
|
|
122
|
+
new inquirer.Separator(),
|
|
123
|
+
{
|
|
124
|
+
name: 'Ask me every time',
|
|
125
|
+
value: 'ask-every-time',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
default: currentDefault || 'ask-every-time',
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
return defaultTarget;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
handlePromptError(error, 'Target selection cancelled');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Ensure target is installed, auto-installing if needed
|
|
140
|
+
* @param targetId - Target ID to ensure is installed
|
|
141
|
+
* @param targetInstaller - TargetInstaller instance to use
|
|
142
|
+
* @param installedTargets - List of currently installed target IDs
|
|
143
|
+
* @returns True if target is installed (or successfully installed), false otherwise
|
|
144
|
+
*/
|
|
145
|
+
export async function ensureTargetInstalled(
|
|
146
|
+
targetId: string,
|
|
147
|
+
targetInstaller: TargetInstaller,
|
|
148
|
+
installedTargets: string[]
|
|
149
|
+
): Promise<boolean> {
|
|
150
|
+
const installation = targetInstaller.getInstallationInfo(targetId);
|
|
151
|
+
|
|
152
|
+
// Already installed - nothing to do
|
|
153
|
+
if (installedTargets.includes(targetId)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Not installed - try to install
|
|
158
|
+
console.log();
|
|
159
|
+
const installed = await targetInstaller.install(targetId, true);
|
|
160
|
+
|
|
161
|
+
if (!installed) {
|
|
162
|
+
console.log(chalk.red(`\n✗ Failed to install ${installation?.name}`));
|
|
163
|
+
console.log(chalk.yellow(' Please install manually and try again.\n'));
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log();
|
|
168
|
+
return true;
|
|
169
|
+
}
|