@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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import fs from 'node:fs/promises';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
10
11
|
import chalk from 'chalk';
|
|
11
12
|
import { ProjectManager } from './project-manager.js';
|
|
12
13
|
import type { BackupManifest } from './backup-manager.js';
|
|
@@ -52,6 +53,18 @@ export class AttachManager {
|
|
|
52
53
|
this.configService = new GlobalConfigService();
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Calculate SHA256 hash of file content
|
|
58
|
+
*/
|
|
59
|
+
private async calculateFileHash(filePath: string): Promise<string> {
|
|
60
|
+
try {
|
|
61
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
62
|
+
return createHash('sha256').update(content).digest('hex');
|
|
63
|
+
} catch {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
/**
|
|
56
69
|
* Get target-specific directory names
|
|
57
70
|
*/
|
|
@@ -369,7 +382,7 @@ ${rules}
|
|
|
369
382
|
// Track in manifest
|
|
370
383
|
manifest.backup.config = {
|
|
371
384
|
path: configPath,
|
|
372
|
-
hash:
|
|
385
|
+
hash: await this.calculateFileHash(configPath),
|
|
373
386
|
mcpServersCount: Object.keys(config.mcp.servers).length,
|
|
374
387
|
};
|
|
375
388
|
}
|
|
@@ -257,60 +257,3 @@ export async function installFile(
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
/**
|
|
261
|
-
* File installer interface for backward compatibility
|
|
262
|
-
*/
|
|
263
|
-
export interface FileInstaller {
|
|
264
|
-
installToDirectory(
|
|
265
|
-
sourceDir: string,
|
|
266
|
-
targetDir: string,
|
|
267
|
-
transform: FileTransformFn,
|
|
268
|
-
options?: InstallOptions
|
|
269
|
-
): Promise<ProcessResult[]>;
|
|
270
|
-
appendToFile(
|
|
271
|
-
sourceDir: string,
|
|
272
|
-
targetFile: string,
|
|
273
|
-
transform: FileTransformFn,
|
|
274
|
-
options?: InstallOptions
|
|
275
|
-
): Promise<void>;
|
|
276
|
-
installFile(
|
|
277
|
-
sourceFile: string,
|
|
278
|
-
targetFile: string,
|
|
279
|
-
transform: FileTransformFn,
|
|
280
|
-
options?: InstallOptions
|
|
281
|
-
): Promise<void>;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Composable file installer
|
|
286
|
-
* Handles copying files from source to destination with optional transformation
|
|
287
|
-
* @deprecated Use standalone functions (installToDirectory, appendToFile, installFile) for new code
|
|
288
|
-
*/
|
|
289
|
-
export class FileInstaller {
|
|
290
|
-
async installToDirectory(
|
|
291
|
-
sourceDir: string,
|
|
292
|
-
targetDir: string,
|
|
293
|
-
transform: FileTransformFn,
|
|
294
|
-
options: InstallOptions = {}
|
|
295
|
-
): Promise<ProcessResult[]> {
|
|
296
|
-
return installToDirectory(sourceDir, targetDir, transform, options);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async appendToFile(
|
|
300
|
-
sourceDir: string,
|
|
301
|
-
targetFile: string,
|
|
302
|
-
transform: FileTransformFn,
|
|
303
|
-
options: InstallOptions = {}
|
|
304
|
-
): Promise<void> {
|
|
305
|
-
return appendToFile(sourceDir, targetFile, transform, options);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async installFile(
|
|
309
|
-
sourceFile: string,
|
|
310
|
-
targetFile: string,
|
|
311
|
-
transform: FileTransformFn,
|
|
312
|
-
options: InstallOptions = {}
|
|
313
|
-
): Promise<void> {
|
|
314
|
-
return installFile(sourceFile, targetFile, transform, options);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
@@ -178,36 +178,3 @@ export function createMCPInstaller(target: Target): MCPInstaller {
|
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
/**
|
|
182
|
-
* @deprecated Use createMCPInstaller() for new code
|
|
183
|
-
*/
|
|
184
|
-
export class MCPInstaller {
|
|
185
|
-
private installer: ReturnType<typeof createMCPInstaller>;
|
|
186
|
-
|
|
187
|
-
constructor(target: Target) {
|
|
188
|
-
this.installer = createMCPInstaller(target);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async selectServers(options: { quiet?: boolean } = {}): Promise<MCPServerID[]> {
|
|
192
|
-
return this.installer.selectServers(options);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async configureServers(
|
|
196
|
-
selectedServers: MCPServerID[],
|
|
197
|
-
options: { quiet?: boolean } = {}
|
|
198
|
-
): Promise<Record<MCPServerID, Record<string, string>>> {
|
|
199
|
-
return this.installer.configureServers(selectedServers, options);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async installServers(
|
|
203
|
-
selectedServers: MCPServerID[],
|
|
204
|
-
serverConfigsMap: Record<MCPServerID, Record<string, string>>,
|
|
205
|
-
options: { quiet?: boolean } = {}
|
|
206
|
-
): Promise<void> {
|
|
207
|
-
return this.installer.installServers(selectedServers, serverConfigsMap, options);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async setupMCP(options: { quiet?: boolean; dryRun?: boolean } = {}): Promise<MCPInstallResult> {
|
|
211
|
-
return this.installer.setupMCP(options);
|
|
212
|
-
}
|
|
213
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -10,7 +10,7 @@ import { existsSync } from 'node:fs';
|
|
|
10
10
|
|
|
11
11
|
export interface GlobalSettings {
|
|
12
12
|
version: string;
|
|
13
|
-
defaultTarget?: 'claude-code' | 'opencode';
|
|
13
|
+
defaultTarget?: 'claude-code' | 'opencode' | 'cursor' | 'ask-every-time';
|
|
14
14
|
defaultAgent?: string; // Default agent to use (e.g., 'coder', 'writer', 'reviewer', 'orchestrator')
|
|
15
15
|
firstRun: boolean;
|
|
16
16
|
lastUpdated: string;
|
|
@@ -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) => {
|