@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
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import {
3
+ detectPackageManagerFromUserAgent,
4
+ detectPackageManagerFromLockFiles,
5
+ detectPackageManager,
6
+ getPackageManagerInfo,
7
+ getUpgradeCommand,
8
+ } from '../package-manager-detector';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import os from 'node:os';
12
+
13
+ describe('Package Manager Detection', () => {
14
+ const originalEnv = process.env;
15
+
16
+ beforeEach(() => {
17
+ process.env = { ...originalEnv };
18
+ });
19
+
20
+ afterEach(() => {
21
+ process.env = originalEnv;
22
+ });
23
+
24
+ describe('detectPackageManagerFromUserAgent', () => {
25
+ it('should detect bun from user agent', () => {
26
+ process.env.npm_config_user_agent = 'bun/1.0.0';
27
+ expect(detectPackageManagerFromUserAgent()).toBe('bun');
28
+ });
29
+
30
+ it('should detect pnpm from user agent', () => {
31
+ process.env.npm_config_user_agent = 'pnpm/8.0.0 npm/? node/v18.0.0';
32
+ expect(detectPackageManagerFromUserAgent()).toBe('pnpm');
33
+ });
34
+
35
+ it('should detect yarn from user agent', () => {
36
+ process.env.npm_config_user_agent = 'yarn/1.22.0 npm/? node/v18.0.0';
37
+ expect(detectPackageManagerFromUserAgent()).toBe('yarn');
38
+ });
39
+
40
+ it('should detect npm from user agent', () => {
41
+ process.env.npm_config_user_agent = 'npm/9.0.0 node/v18.0.0';
42
+ expect(detectPackageManagerFromUserAgent()).toBe('npm');
43
+ });
44
+
45
+ it('should return null when no user agent', () => {
46
+ delete process.env.npm_config_user_agent;
47
+ expect(detectPackageManagerFromUserAgent()).toBe(null);
48
+ });
49
+ });
50
+
51
+ describe('detectPackageManagerFromLockFiles', () => {
52
+ let tempDir: string;
53
+
54
+ beforeEach(() => {
55
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-test-'));
56
+ });
57
+
58
+ afterEach(() => {
59
+ fs.rmSync(tempDir, { recursive: true, force: true });
60
+ });
61
+
62
+ it('should detect bun from bun.lockb', () => {
63
+ fs.writeFileSync(path.join(tempDir, 'bun.lockb'), '');
64
+ expect(detectPackageManagerFromLockFiles(tempDir)).toBe('bun');
65
+ });
66
+
67
+ it('should detect bun from bun.lock', () => {
68
+ fs.writeFileSync(path.join(tempDir, 'bun.lock'), '');
69
+ expect(detectPackageManagerFromLockFiles(tempDir)).toBe('bun');
70
+ });
71
+
72
+ it('should detect pnpm from pnpm-lock.yaml', () => {
73
+ fs.writeFileSync(path.join(tempDir, 'pnpm-lock.yaml'), '');
74
+ expect(detectPackageManagerFromLockFiles(tempDir)).toBe('pnpm');
75
+ });
76
+
77
+ it('should detect yarn from yarn.lock', () => {
78
+ fs.writeFileSync(path.join(tempDir, 'yarn.lock'), '');
79
+ expect(detectPackageManagerFromLockFiles(tempDir)).toBe('yarn');
80
+ });
81
+
82
+ it('should detect npm from package-lock.json', () => {
83
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '');
84
+ expect(detectPackageManagerFromLockFiles(tempDir)).toBe('npm');
85
+ });
86
+
87
+ it('should prioritize bun over others', () => {
88
+ fs.writeFileSync(path.join(tempDir, 'bun.lock'), '');
89
+ fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '');
90
+ expect(detectPackageManagerFromLockFiles(tempDir)).toBe('bun');
91
+ });
92
+
93
+ it('should return null when no lock files', () => {
94
+ expect(detectPackageManagerFromLockFiles(tempDir)).toBe(null);
95
+ });
96
+ });
97
+
98
+ describe('detectPackageManager', () => {
99
+ it('should prioritize user agent over lock files', () => {
100
+ process.env.npm_config_user_agent = 'pnpm/8.0.0';
101
+ const result = detectPackageManager();
102
+ expect(result).toBe('pnpm');
103
+ });
104
+
105
+ it('should default to npm when no detection methods work', () => {
106
+ delete process.env.npm_config_user_agent;
107
+ const result = detectPackageManager('/nonexistent');
108
+ expect(result).toBe('npm');
109
+ });
110
+ });
111
+
112
+ describe('getPackageManagerInfo', () => {
113
+ it('should return correct info for npm', () => {
114
+ const info = getPackageManagerInfo('npm');
115
+ expect(info.name).toBe('npm');
116
+ expect(info.installCommand).toBe('npm install');
117
+ expect(info.globalInstallCommand('foo')).toBe('npm install -g foo');
118
+ });
119
+
120
+ it('should return correct info for bun', () => {
121
+ const info = getPackageManagerInfo('bun');
122
+ expect(info.name).toBe('bun');
123
+ expect(info.installCommand).toBe('bun install');
124
+ expect(info.globalInstallCommand('foo')).toBe('bun install -g foo');
125
+ });
126
+
127
+ it('should return correct info for pnpm', () => {
128
+ const info = getPackageManagerInfo('pnpm');
129
+ expect(info.name).toBe('pnpm');
130
+ expect(info.installCommand).toBe('pnpm install');
131
+ expect(info.globalInstallCommand('foo')).toBe('pnpm install -g foo');
132
+ });
133
+
134
+ it('should return correct info for yarn', () => {
135
+ const info = getPackageManagerInfo('yarn');
136
+ expect(info.name).toBe('yarn');
137
+ expect(info.installCommand).toBe('yarn install');
138
+ expect(info.globalInstallCommand('foo')).toBe('yarn global add foo');
139
+ });
140
+ });
141
+
142
+ describe('getUpgradeCommand', () => {
143
+ it('should return correct upgrade command for npm', () => {
144
+ const cmd = getUpgradeCommand('my-package', 'npm');
145
+ expect(cmd).toBe('npm install -g my-package@latest');
146
+ });
147
+
148
+ it('should return correct upgrade command for bun', () => {
149
+ const cmd = getUpgradeCommand('my-package', 'bun');
150
+ expect(cmd).toBe('bun install -g my-package@latest');
151
+ });
152
+
153
+ it('should return correct upgrade command for pnpm', () => {
154
+ const cmd = getUpgradeCommand('my-package', 'pnpm');
155
+ expect(cmd).toBe('pnpm install -g my-package@latest');
156
+ });
157
+
158
+ it('should return correct upgrade command for yarn', () => {
159
+ const cmd = getUpgradeCommand('my-package', 'yarn');
160
+ expect(cmd).toBe('yarn global add my-package@latest');
161
+ });
162
+ });
163
+ });
@@ -18,17 +18,20 @@ import { yamlUtils } from './config/target-utils.js';
18
18
  /**
19
19
  * Load and combine rules and output styles
20
20
  */
21
- export async function loadRulesAndStyles(ruleNames?: string[]): Promise<string> {
21
+ export async function loadRulesAndStyles(
22
+ ruleNames?: string[],
23
+ outputStyleNames?: string[]
24
+ ): Promise<string> {
22
25
  const sections: string[] = [];
23
26
 
24
- // Load rules (either specified rules or default to 'core')
27
+ // Load rules (either specified rules or default to all)
25
28
  const rulesContent = await loadRules(ruleNames);
26
29
  if (rulesContent) {
27
30
  sections.push(rulesContent);
28
31
  }
29
32
 
30
- // Load output styles
31
- const stylesContent = await loadOutputStyles();
33
+ // Load output styles (either specified or all)
34
+ const stylesContent = await loadOutputStyles(outputStyleNames);
32
35
  if (stylesContent) {
33
36
  sections.push(stylesContent);
34
37
  }
@@ -69,26 +72,36 @@ async function loadRules(ruleNames?: string[]): Promise<string> {
69
72
 
70
73
  /**
71
74
  * Load output styles from assets/output-styles/
75
+ * @param styleNames - Array of style file names (without .md extension). If not provided, loads all styles.
72
76
  */
73
- async function loadOutputStyles(): Promise<string> {
77
+ async function loadOutputStyles(styleNames?: string[]): Promise<string> {
74
78
  try {
75
79
  const outputStylesDir = getOutputStylesDir();
76
- const files = await fs.readdir(outputStylesDir);
77
- const mdFiles = files.filter((f) => f.endsWith('.md'));
78
-
79
- if (mdFiles.length === 0) {
80
- return '';
81
- }
82
-
83
80
  const sections: string[] = [];
84
81
 
85
- for (const file of mdFiles) {
86
- const filePath = path.join(outputStylesDir, file);
87
- const content = await fs.readFile(filePath, 'utf8');
88
-
89
- // Strip YAML front matter
90
- const stripped = await yamlUtils.stripFrontMatter(content);
91
- sections.push(stripped);
82
+ // If specific styles are requested, load only those
83
+ if (styleNames && styleNames.length > 0) {
84
+ for (const styleName of styleNames) {
85
+ const filePath = path.join(outputStylesDir, `${styleName}.md`);
86
+ try {
87
+ const content = await fs.readFile(filePath, 'utf8');
88
+ const stripped = await yamlUtils.stripFrontMatter(content);
89
+ sections.push(stripped);
90
+ } catch (error) {
91
+ console.warn(`Warning: Output style file not found: ${styleName}.md`);
92
+ }
93
+ }
94
+ } else {
95
+ // Load all styles
96
+ const files = await fs.readdir(outputStylesDir);
97
+ const mdFiles = files.filter((f) => f.endsWith('.md'));
98
+
99
+ for (const file of mdFiles) {
100
+ const filePath = path.join(outputStylesDir, file);
101
+ const content = await fs.readFile(filePath, 'utf8');
102
+ const stripped = await yamlUtils.stripFrontMatter(content);
103
+ sections.push(stripped);
104
+ }
92
105
  }
93
106
 
94
107
  return sections.join('\n\n');
@@ -101,10 +114,15 @@ async function loadOutputStyles(): Promise<string> {
101
114
  /**
102
115
  * Enhance agent content by appending rules and output styles
103
116
  * @param agentContent - The agent markdown content
104
- * @param ruleNames - Optional array of rule file names to include (defaults to ['core'])
117
+ * @param ruleNames - Optional array of rule file names to include
118
+ * @param outputStyleNames - Optional array of output style file names to include
105
119
  */
106
- export async function enhanceAgentContent(agentContent: string, ruleNames?: string[]): Promise<string> {
107
- const rulesAndStyles = await loadRulesAndStyles(ruleNames);
120
+ export async function enhanceAgentContent(
121
+ agentContent: string,
122
+ ruleNames?: string[],
123
+ outputStyleNames?: string[]
124
+ ): Promise<string> {
125
+ const rulesAndStyles = await loadRulesAndStyles(ruleNames, outputStyleNames);
108
126
 
109
127
  if (!rulesAndStyles) {
110
128
  return agentContent;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Custom error for user cancellation (Ctrl+C)
3
+ */
4
+ export class UserCancelledError extends Error {
5
+ constructor(message = 'Operation cancelled by user') {
6
+ super(message);
7
+ this.name = 'UserCancelledError';
8
+ }
9
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Package Manager Detection
3
+ * Detects which package manager is being used (npm, bun, pnpm, yarn)
4
+ * Based on lock files and environment variables
5
+ */
6
+
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ export type PackageManager = 'npm' | 'bun' | 'pnpm' | 'yarn';
11
+
12
+ export interface PackageManagerInfo {
13
+ name: PackageManager;
14
+ installCommand: string;
15
+ globalInstallCommand: (packageName: string) => string;
16
+ version?: string;
17
+ }
18
+
19
+ /**
20
+ * Detect package manager from environment variable (npm_config_user_agent)
21
+ * This is set when running through npm/bun/pnpm/yarn scripts
22
+ */
23
+ export function detectPackageManagerFromUserAgent(): PackageManager | null {
24
+ const userAgent = process.env.npm_config_user_agent;
25
+
26
+ if (!userAgent) {
27
+ return null;
28
+ }
29
+
30
+ if (userAgent.includes('bun')) return 'bun';
31
+ if (userAgent.includes('pnpm')) return 'pnpm';
32
+ if (userAgent.includes('yarn')) return 'yarn';
33
+ if (userAgent.includes('npm')) return 'npm';
34
+
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Detect package manager from lock files in directory
40
+ */
41
+ export function detectPackageManagerFromLockFiles(dir: string = process.cwd()): PackageManager | null {
42
+ const lockFiles: Record<PackageManager, string[]> = {
43
+ bun: ['bun.lockb', 'bun.lock'],
44
+ pnpm: ['pnpm-lock.yaml'],
45
+ yarn: ['yarn.lock'],
46
+ npm: ['package-lock.json'],
47
+ };
48
+
49
+ // Check in priority order: bun > pnpm > yarn > npm
50
+ const priority: PackageManager[] = ['bun', 'pnpm', 'yarn', 'npm'];
51
+
52
+ for (const pm of priority) {
53
+ const files = lockFiles[pm];
54
+ for (const file of files) {
55
+ if (fs.existsSync(path.join(dir, file))) {
56
+ return pm;
57
+ }
58
+ }
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Detect which package manager to use
66
+ * Priority: user agent > lock files > npm (default)
67
+ */
68
+ export function detectPackageManager(dir: string = process.cwd()): PackageManager {
69
+ // 1. Try user agent (most reliable when running as script)
70
+ const fromUserAgent = detectPackageManagerFromUserAgent();
71
+ if (fromUserAgent) {
72
+ return fromUserAgent;
73
+ }
74
+
75
+ // 2. Try lock files
76
+ const fromLockFiles = detectPackageManagerFromLockFiles(dir);
77
+ if (fromLockFiles) {
78
+ return fromLockFiles;
79
+ }
80
+
81
+ // 3. Default to npm
82
+ return 'npm';
83
+ }
84
+
85
+ /**
86
+ * Get package manager info with commands
87
+ */
88
+ export function getPackageManagerInfo(pm?: PackageManager): PackageManagerInfo {
89
+ const detected = pm || detectPackageManager();
90
+
91
+ const info: Record<PackageManager, PackageManagerInfo> = {
92
+ npm: {
93
+ name: 'npm',
94
+ installCommand: 'npm install',
95
+ globalInstallCommand: (pkg) => `npm install -g ${pkg}`,
96
+ },
97
+ bun: {
98
+ name: 'bun',
99
+ installCommand: 'bun install',
100
+ globalInstallCommand: (pkg) => `bun install -g ${pkg}`,
101
+ },
102
+ pnpm: {
103
+ name: 'pnpm',
104
+ installCommand: 'pnpm install',
105
+ globalInstallCommand: (pkg) => `pnpm install -g ${pkg}`,
106
+ },
107
+ yarn: {
108
+ name: 'yarn',
109
+ installCommand: 'yarn install',
110
+ globalInstallCommand: (pkg) => `yarn global add ${pkg}`,
111
+ },
112
+ };
113
+
114
+ return info[detected];
115
+ }
116
+
117
+ /**
118
+ * Get upgrade command for a package
119
+ */
120
+ export function getUpgradeCommand(packageName: string, pm?: PackageManager): string {
121
+ const pmInfo = getPackageManagerInfo(pm);
122
+ return pmInfo.globalInstallCommand(`${packageName}@latest`);
123
+ }
124
+
125
+ /**
126
+ * Check if package manager is available in system
127
+ */
128
+ export async function isPackageManagerAvailable(pm: PackageManager): Promise<boolean> {
129
+ const { exec } = await import('node:child_process');
130
+ const { promisify } = await import('node:util');
131
+ const execAsync = promisify(exec);
132
+
133
+ try {
134
+ await execAsync(`${pm} --version`);
135
+ return true;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
@@ -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
+ }