@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.
- package/CHANGELOG.md +159 -0
- package/UPGRADE.md +151 -0
- package/package.json +11 -6
- package/src/commands/flow/execute-v2.ts +372 -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 +536 -0
- package/src/config/ai-config.ts +2 -69
- package/src/config/targets.ts +0 -11
- package/src/core/attach-manager.ts +495 -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/installers/file-installer.ts +0 -57
- package/src/core/installers/mcp-installer.ts +0 -33
- 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 +15 -29
- package/src/services/auto-upgrade.ts +248 -0
- package/src/services/first-run-setup.ts +220 -0
- package/src/services/global-config.ts +337 -0
- 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/__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/utils/prompt-helpers.ts +48 -0
- package/src/utils/target-selection.ts +169 -0
|
@@ -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' | 'cursor' | 'ask-every-time';
|
|
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
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Target Installation Service
|
|
3
|
+
* Auto-detects and installs AI CLI tools (Claude Code, OpenCode, Cursor)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { exec } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import inquirer from 'inquirer';
|
|
11
|
+
import { detectPackageManager, type PackageManager } from '../utils/package-manager-detector.js';
|
|
12
|
+
import { UserCancelledError } from '../utils/errors.js';
|
|
13
|
+
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
|
|
16
|
+
export interface TargetInstallation {
|
|
17
|
+
id: 'claude-code' | 'opencode' | 'cursor';
|
|
18
|
+
name: string;
|
|
19
|
+
package: string;
|
|
20
|
+
checkCommand: string;
|
|
21
|
+
installCommand: (pm: PackageManager) => string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Supported target installations
|
|
26
|
+
*/
|
|
27
|
+
const TARGET_INSTALLATIONS: TargetInstallation[] = [
|
|
28
|
+
{
|
|
29
|
+
id: 'claude-code',
|
|
30
|
+
name: 'Claude Code',
|
|
31
|
+
package: '@anthropic-ai/claude-code',
|
|
32
|
+
checkCommand: 'claude --version',
|
|
33
|
+
installCommand: (pm: PackageManager) => {
|
|
34
|
+
switch (pm) {
|
|
35
|
+
case 'npm':
|
|
36
|
+
return 'npm install -g @anthropic-ai/claude-code';
|
|
37
|
+
case 'bun':
|
|
38
|
+
return 'bun install -g @anthropic-ai/claude-code';
|
|
39
|
+
case 'pnpm':
|
|
40
|
+
return 'pnpm install -g @anthropic-ai/claude-code';
|
|
41
|
+
case 'yarn':
|
|
42
|
+
return 'yarn global add @anthropic-ai/claude-code';
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'opencode',
|
|
48
|
+
name: 'OpenCode',
|
|
49
|
+
package: 'opencode-ai',
|
|
50
|
+
checkCommand: 'opencode --version',
|
|
51
|
+
installCommand: (pm: PackageManager) => {
|
|
52
|
+
switch (pm) {
|
|
53
|
+
case 'npm':
|
|
54
|
+
return 'npm install -g opencode-ai@latest';
|
|
55
|
+
case 'bun':
|
|
56
|
+
return 'bun install -g opencode-ai@latest';
|
|
57
|
+
case 'pnpm':
|
|
58
|
+
return 'pnpm install -g opencode-ai@latest';
|
|
59
|
+
case 'yarn':
|
|
60
|
+
return 'yarn global add opencode-ai@latest';
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'cursor',
|
|
66
|
+
name: 'Cursor',
|
|
67
|
+
package: 'cursor',
|
|
68
|
+
checkCommand: 'cursor --version',
|
|
69
|
+
installCommand: () => {
|
|
70
|
+
// Cursor is typically installed via installer, not npm
|
|
71
|
+
return 'Visit https://cursor.sh to download and install';
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
export class TargetInstaller {
|
|
77
|
+
private packageManager: PackageManager;
|
|
78
|
+
|
|
79
|
+
constructor(projectPath: string = process.cwd()) {
|
|
80
|
+
this.packageManager = detectPackageManager(projectPath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a target CLI is installed
|
|
85
|
+
* @param targetId - Target ID to check
|
|
86
|
+
* @returns True if installed, false otherwise
|
|
87
|
+
*/
|
|
88
|
+
async isInstalled(targetId: string): Promise<boolean> {
|
|
89
|
+
const installation = TARGET_INSTALLATIONS.find((t) => t.id === targetId);
|
|
90
|
+
if (!installation) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await execAsync(installation.checkCommand);
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Detect which target CLIs are currently installed
|
|
104
|
+
* @returns Array of installed target IDs
|
|
105
|
+
*/
|
|
106
|
+
async detectInstalledTargets(): Promise<string[]> {
|
|
107
|
+
const installed: string[] = [];
|
|
108
|
+
|
|
109
|
+
for (const installation of TARGET_INSTALLATIONS) {
|
|
110
|
+
if (await this.isInstalled(installation.id)) {
|
|
111
|
+
installed.push(installation.id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return installed;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Prompt user to select a target to install
|
|
120
|
+
* @returns Selected target ID
|
|
121
|
+
* @throws {UserCancelledError} If user cancels the prompt
|
|
122
|
+
*/
|
|
123
|
+
async promptForTargetSelection(): Promise<string> {
|
|
124
|
+
try {
|
|
125
|
+
const { targetId } = await inquirer.prompt([
|
|
126
|
+
{
|
|
127
|
+
type: 'list',
|
|
128
|
+
name: 'targetId',
|
|
129
|
+
message: 'No AI CLI detected. Which would you like to use?',
|
|
130
|
+
choices: TARGET_INSTALLATIONS.map((t) => ({
|
|
131
|
+
name: t.name,
|
|
132
|
+
value: t.id,
|
|
133
|
+
})),
|
|
134
|
+
default: 'claude-code',
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
return targetId;
|
|
139
|
+
} catch (error: any) {
|
|
140
|
+
// Handle user cancellation (Ctrl+C)
|
|
141
|
+
if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
|
|
142
|
+
throw new UserCancelledError('Target selection cancelled');
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Install a target CLI using detected package manager
|
|
150
|
+
* @param targetId - Target ID to install
|
|
151
|
+
* @param autoConfirm - Skip confirmation prompt if true
|
|
152
|
+
* @returns True if installation successful, false otherwise
|
|
153
|
+
* @throws {UserCancelledError} If user cancels installation
|
|
154
|
+
*/
|
|
155
|
+
async install(targetId: string, autoConfirm: boolean = false): Promise<boolean> {
|
|
156
|
+
const installation = TARGET_INSTALLATIONS.find((t) => t.id === targetId);
|
|
157
|
+
if (!installation) {
|
|
158
|
+
console.log(chalk.red(`✗ Unknown target: ${targetId}`));
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Special handling for Cursor (not npm-installable)
|
|
163
|
+
if (targetId === 'cursor') {
|
|
164
|
+
console.log(chalk.yellow('\n⚠️ Cursor requires manual installation'));
|
|
165
|
+
console.log(chalk.cyan(' Visit https://cursor.sh to download and install\n'));
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Confirm installation unless auto-confirm is enabled
|
|
170
|
+
if (!autoConfirm) {
|
|
171
|
+
try {
|
|
172
|
+
const { confirmInstall } = await inquirer.prompt([
|
|
173
|
+
{
|
|
174
|
+
type: 'confirm',
|
|
175
|
+
name: 'confirmInstall',
|
|
176
|
+
message: `Install ${installation.name}?`,
|
|
177
|
+
default: true,
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
if (!confirmInstall) {
|
|
182
|
+
console.log(chalk.yellow('\n⚠️ Installation cancelled\n'));
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
} catch (error: any) {
|
|
186
|
+
// Handle user cancellation (Ctrl+C)
|
|
187
|
+
if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
|
|
188
|
+
throw new UserCancelledError('Installation cancelled');
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const spinner = ora(`Installing ${installation.name}...`).start();
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const installCmd = installation.installCommand(this.packageManager);
|
|
198
|
+
await execAsync(installCmd);
|
|
199
|
+
|
|
200
|
+
spinner.succeed(chalk.green(`✓ ${installation.name} installed successfully`));
|
|
201
|
+
return true;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
spinner.fail(chalk.red(`✗ Failed to install ${installation.name}`));
|
|
204
|
+
|
|
205
|
+
const installCmd = installation.installCommand(this.packageManager);
|
|
206
|
+
console.log(chalk.yellow('\n⚠️ Auto-install failed. Please run manually:'));
|
|
207
|
+
console.log(chalk.cyan(` ${installCmd}\n`));
|
|
208
|
+
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Auto-detect installed targets or prompt to install one
|
|
215
|
+
* @returns Target ID if found or installed, null if installation failed
|
|
216
|
+
* @throws {UserCancelledError} If user cancels selection or installation
|
|
217
|
+
*/
|
|
218
|
+
async autoDetectAndInstall(): Promise<string | null> {
|
|
219
|
+
console.log(chalk.cyan('🔍 Detecting installed AI CLIs...\n'));
|
|
220
|
+
|
|
221
|
+
const installedTargets = await this.detectInstalledTargets();
|
|
222
|
+
|
|
223
|
+
// If we found installed targets, return the first one (priority order)
|
|
224
|
+
if (installedTargets.length > 0) {
|
|
225
|
+
const targetId = installedTargets[0];
|
|
226
|
+
const installation = TARGET_INSTALLATIONS.find((t) => t.id === targetId);
|
|
227
|
+
console.log(chalk.green(`✓ Found ${installation?.name}\n`));
|
|
228
|
+
return targetId;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// No targets found - prompt user to select one
|
|
232
|
+
console.log(chalk.yellow('⚠️ No AI CLI detected\n'));
|
|
233
|
+
const selectedTargetId = await this.promptForTargetSelection();
|
|
234
|
+
|
|
235
|
+
// Try to install the selected target
|
|
236
|
+
console.log();
|
|
237
|
+
const installed = await this.install(selectedTargetId, false);
|
|
238
|
+
|
|
239
|
+
if (!installed) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return selectedTargetId;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get installation metadata for a target
|
|
248
|
+
* @param targetId - Target ID to get info for
|
|
249
|
+
* @returns Installation info or undefined if target not found
|
|
250
|
+
*/
|
|
251
|
+
getInstallationInfo(targetId: string): TargetInstallation | undefined {
|
|
252
|
+
return TARGET_INSTALLATIONS.find((t) => t.id === targetId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -3,8 +3,8 @@ import fs from 'node:fs';
|
|
|
3
3
|
import fsPromises from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { installToDirectory } 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 { CLIError } from '../utils/error-handler.js';
|
|
@@ -432,10 +432,9 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
432
432
|
*/
|
|
433
433
|
async setupAgents(cwd: string, options: CommonOptions): Promise<SetupResult> {
|
|
434
434
|
const { enhanceAgentContent } = await import('../utils/agent-enhancer.js');
|
|
435
|
-
const installer = new FileInstaller();
|
|
436
435
|
const agentsDir = path.join(cwd, this.config.agentDir);
|
|
437
436
|
|
|
438
|
-
const results = await
|
|
437
|
+
const results = await installToDirectory(
|
|
439
438
|
getAgentsDir(),
|
|
440
439
|
agentsDir,
|
|
441
440
|
async (content, sourcePath) => {
|
|
@@ -491,7 +490,7 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
491
490
|
* Select, configure, install, and approve MCP servers
|
|
492
491
|
*/
|
|
493
492
|
async setupMCP(cwd: string, options: CommonOptions): Promise<SetupResult> {
|
|
494
|
-
const installer =
|
|
493
|
+
const installer = createMCPInstaller(this);
|
|
495
494
|
const result = await installer.setupMCP({ ...options, quiet: true });
|
|
496
495
|
|
|
497
496
|
// Approve servers in Claude Code settings
|
|
@@ -513,10 +512,9 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
513
512
|
return { count: 0 };
|
|
514
513
|
}
|
|
515
514
|
|
|
516
|
-
const installer = new FileInstaller();
|
|
517
515
|
const slashCommandsDir = path.join(cwd, this.config.slashCommandsDir);
|
|
518
516
|
|
|
519
|
-
const results = await
|
|
517
|
+
const results = await installToDirectory(
|
|
520
518
|
getSlashCommandsDir(),
|
|
521
519
|
slashCommandsDir,
|
|
522
520
|
async (content) => {
|
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) => {
|