@sylphx/flow 2.1.3 → 2.1.5
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 +28 -0
- package/README.md +44 -0
- package/package.json +79 -73
- package/src/commands/flow/execute-v2.ts +37 -29
- package/src/commands/flow/prompt.ts +5 -3
- package/src/commands/flow/types.ts +0 -2
- package/src/commands/flow-command.ts +20 -13
- package/src/commands/hook-command.ts +1 -3
- package/src/commands/settings/checkbox-config.ts +128 -0
- package/src/commands/settings/index.ts +6 -0
- package/src/commands/settings-command.ts +84 -156
- package/src/config/ai-config.ts +60 -41
- package/src/core/agent-loader.ts +11 -6
- package/src/core/attach/file-attacher.ts +172 -0
- package/src/core/attach/index.ts +5 -0
- package/src/core/attach-manager.ts +117 -171
- package/src/core/backup-manager.ts +35 -29
- package/src/core/cleanup-handler.ts +11 -8
- package/src/core/error-handling.ts +23 -30
- package/src/core/flow-executor.ts +58 -76
- package/src/core/formatting/bytes.ts +2 -4
- package/src/core/functional/async.ts +5 -4
- package/src/core/functional/error-handler.ts +2 -2
- package/src/core/git-stash-manager.ts +21 -10
- package/src/core/installers/file-installer.ts +0 -1
- package/src/core/installers/mcp-installer.ts +0 -1
- package/src/core/project-manager.ts +24 -18
- package/src/core/secrets-manager.ts +54 -73
- package/src/core/session-manager.ts +20 -22
- package/src/core/state-detector.ts +139 -80
- package/src/core/template-loader.ts +13 -31
- package/src/core/upgrade-manager.ts +122 -69
- package/src/index.ts +8 -5
- package/src/services/auto-upgrade.ts +1 -1
- package/src/services/config-service.ts +41 -29
- package/src/services/global-config.ts +3 -3
- package/src/services/target-installer.ts +11 -26
- package/src/targets/claude-code.ts +35 -81
- package/src/targets/opencode.ts +28 -68
- package/src/targets/shared/index.ts +7 -0
- package/src/targets/shared/mcp-transforms.ts +132 -0
- package/src/targets/shared/target-operations.ts +135 -0
- package/src/types/cli.types.ts +2 -2
- package/src/types/provider.types.ts +1 -7
- package/src/types/session.types.ts +11 -11
- package/src/types/target.types.ts +3 -1
- package/src/types/todo.types.ts +1 -1
- package/src/types.ts +1 -1
- package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
- package/src/utils/agent-enhancer.ts +4 -4
- package/src/utils/config/paths.ts +3 -1
- package/src/utils/config/target-utils.ts +2 -2
- package/src/utils/display/banner.ts +2 -2
- package/src/utils/display/notifications.ts +58 -45
- package/src/utils/display/status.ts +29 -12
- package/src/utils/files/file-operations.ts +1 -1
- package/src/utils/files/sync-utils.ts +38 -41
- package/src/utils/index.ts +19 -27
- package/src/utils/package-manager-detector.ts +15 -5
- package/src/utils/security/security.ts +8 -4
- package/src/utils/target-selection.ts +6 -8
- package/src/utils/version.ts +4 -2
- package/src/commands/flow-orchestrator.ts +0 -328
- package/src/commands/init-command.ts +0 -92
- package/src/commands/init-core.ts +0 -331
- package/src/core/agent-manager.ts +0 -174
- package/src/core/loop-controller.ts +0 -200
- package/src/core/rule-loader.ts +0 -147
- package/src/core/rule-manager.ts +0 -240
- package/src/services/claude-config-service.ts +0 -252
- package/src/services/first-run-setup.ts +0 -220
- package/src/services/smart-config-service.ts +0 -269
- package/src/types/api.types.ts +0 -9
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import { exec } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import ora from 'ora';
|
|
7
|
-
import type { ProjectState } from './state-detector.js';
|
|
8
|
-
import { CLIError } from '../utils/error-handler.js';
|
|
9
|
-
import { ConfigService } from '../services/config-service.js';
|
|
10
7
|
import { getProjectSettingsFile } from '../config/constants.js';
|
|
11
|
-
import {
|
|
8
|
+
import type { Target } from '../types/target.types.js';
|
|
9
|
+
import { CLIError } from '../utils/error-handler.js';
|
|
10
|
+
import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
|
|
11
|
+
import type { ProjectState } from './state-detector.js';
|
|
12
|
+
import { targetManager } from './target-manager.js';
|
|
12
13
|
|
|
13
14
|
const execAsync = promisify(exec);
|
|
14
15
|
|
|
@@ -150,67 +151,83 @@ export class UpgradeManager {
|
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Resolve target from ID string to Target object
|
|
156
|
+
*/
|
|
157
|
+
private resolveTarget(targetId: string): Target | null {
|
|
158
|
+
const targetOption = targetManager.getTarget(targetId);
|
|
159
|
+
if (targetOption._tag === 'None') {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return targetOption.value;
|
|
163
|
+
}
|
|
164
|
+
|
|
153
165
|
async upgradeTarget(state: ProjectState, autoInstall: boolean = false): Promise<boolean> {
|
|
154
166
|
if (!state.target || !state.targetLatestVersion) {
|
|
155
167
|
return false;
|
|
156
168
|
}
|
|
157
169
|
|
|
158
|
-
const
|
|
170
|
+
const target = this.resolveTarget(state.target);
|
|
171
|
+
if (!target) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const spinner = ora(`Upgrading ${target.name}...`).start();
|
|
159
176
|
|
|
160
177
|
try {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
await this.upgradeOpenCode();
|
|
165
|
-
}
|
|
178
|
+
// Use target-specific upgrade logic based on target ID
|
|
179
|
+
// This is necessary because each CLI has different upgrade commands
|
|
180
|
+
await this.upgradeTargetCLI(target, autoInstall);
|
|
166
181
|
|
|
167
|
-
spinner.succeed(`${
|
|
182
|
+
spinner.succeed(`${target.name} upgraded to latest version`);
|
|
168
183
|
return true;
|
|
169
184
|
} catch (error) {
|
|
170
|
-
spinner.fail(`${
|
|
185
|
+
spinner.fail(`${target.name} upgrade failed`);
|
|
171
186
|
throw new CLIError(
|
|
172
|
-
`Failed to upgrade ${
|
|
187
|
+
`Failed to upgrade ${target.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
173
188
|
'TARGET_UPGRADE_FAILED'
|
|
174
189
|
);
|
|
175
190
|
}
|
|
176
191
|
}
|
|
177
192
|
|
|
178
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Upgrade target CLI - handles target-specific upgrade commands
|
|
195
|
+
*/
|
|
196
|
+
private async upgradeTargetCLI(target: Target, autoInstall: boolean = false): Promise<void> {
|
|
179
197
|
if (this.options.dryRun) {
|
|
180
|
-
console.log(
|
|
198
|
+
console.log(`Dry run: upgrade ${target.id}`);
|
|
181
199
|
return;
|
|
182
200
|
}
|
|
183
201
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
// Each CLI target has specific upgrade commands
|
|
203
|
+
// This is inherently target-specific and can't be fully abstracted
|
|
204
|
+
switch (target.id) {
|
|
205
|
+
case 'claude-code':
|
|
206
|
+
if (autoInstall) {
|
|
207
|
+
const packageManager = detectPackageManager(this.projectPath);
|
|
208
|
+
const installCmd = getUpgradeCommand('@anthropic-ai/claude-code', packageManager);
|
|
209
|
+
const { stdout } = await execAsync(installCmd);
|
|
210
|
+
if (this.options.verbose) {
|
|
211
|
+
console.log(stdout);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
const { stdout } = await execAsync('claude update');
|
|
215
|
+
if (this.options.verbose) {
|
|
216
|
+
console.log(stdout);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
196
220
|
|
|
197
|
-
|
|
198
|
-
|
|
221
|
+
case 'opencode': {
|
|
222
|
+
const { stdout: ocStdout } = await execAsync('opencode upgrade');
|
|
223
|
+
if (this.options.verbose) {
|
|
224
|
+
console.log(ocStdout);
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
199
227
|
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private async upgradeOpenCode(): Promise<void> {
|
|
204
|
-
if (this.options.dryRun) {
|
|
205
|
-
console.log('模拟: opencode upgrade');
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// OpenCode has built-in upgrade command
|
|
210
|
-
const { stdout } = await execAsync('opencode upgrade');
|
|
211
228
|
|
|
212
|
-
|
|
213
|
-
|
|
229
|
+
default:
|
|
230
|
+
console.log(chalk.yellow(`No upgrade command available for ${target.name}`));
|
|
214
231
|
}
|
|
215
232
|
}
|
|
216
233
|
|
|
@@ -235,40 +252,63 @@ export class UpgradeManager {
|
|
|
235
252
|
return upgraded;
|
|
236
253
|
}
|
|
237
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Get the current target from project settings
|
|
257
|
+
*/
|
|
258
|
+
private async getCurrentTarget(): Promise<Target | null> {
|
|
259
|
+
try {
|
|
260
|
+
const configPath = path.join(this.projectPath, getProjectSettingsFile());
|
|
261
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
|
|
262
|
+
if (config.target) {
|
|
263
|
+
return this.resolveTarget(config.target);
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
// Cannot read config
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
238
271
|
private async upgradeComponent(component: string): Promise<void> {
|
|
239
|
-
//
|
|
240
|
-
const
|
|
272
|
+
// Get target config for correct directory
|
|
273
|
+
const target = await this.getCurrentTarget();
|
|
274
|
+
const configDir = target?.config.configDir || '.claude';
|
|
275
|
+
|
|
276
|
+
// Delete old version
|
|
277
|
+
const componentPath = path.join(this.projectPath, configDir, component);
|
|
241
278
|
await fs.rm(componentPath, { recursive: true, force: true });
|
|
242
279
|
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
// 这里用 dry-run 模式模拟
|
|
280
|
+
// Reinstall latest version
|
|
281
|
+
// Actual implementation would call the appropriate installer
|
|
246
282
|
if (this.options.dryRun) {
|
|
247
|
-
console.log(
|
|
283
|
+
console.log(`Dry run: reinstall ${component}`);
|
|
248
284
|
}
|
|
249
285
|
}
|
|
250
286
|
|
|
251
287
|
private async backupConfig(): Promise<string> {
|
|
252
|
-
|
|
288
|
+
// Get target config for correct directories
|
|
289
|
+
const target = await this.getCurrentTarget();
|
|
290
|
+
const configDir = target?.config.configDir || '.claude';
|
|
291
|
+
|
|
292
|
+
const backupDir = this.options.backupPath || path.join(this.projectPath, `${configDir}-backup`);
|
|
253
293
|
await fs.mkdir(backupDir, { recursive: true });
|
|
254
294
|
|
|
255
295
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
256
296
|
const backupPath = path.join(backupDir, `backup-${timestamp}`);
|
|
257
297
|
|
|
258
|
-
//
|
|
259
|
-
const
|
|
298
|
+
// Backup target config directory
|
|
299
|
+
const targetConfigPath = path.join(this.projectPath, configDir);
|
|
260
300
|
try {
|
|
261
|
-
await fs.cp(
|
|
301
|
+
await fs.cp(targetConfigPath, path.join(backupPath, configDir), { recursive: true });
|
|
262
302
|
} catch {
|
|
263
|
-
//
|
|
303
|
+
// Config directory may not exist
|
|
264
304
|
}
|
|
265
305
|
|
|
266
|
-
//
|
|
267
|
-
const
|
|
306
|
+
// Backup project settings file
|
|
307
|
+
const settingsPath = path.join(this.projectPath, getProjectSettingsFile());
|
|
268
308
|
try {
|
|
269
|
-
await fs.cp(
|
|
309
|
+
await fs.cp(settingsPath, path.join(backupPath, getProjectSettingsFile()));
|
|
270
310
|
} catch {
|
|
271
|
-
//
|
|
311
|
+
// Settings file may not exist
|
|
272
312
|
}
|
|
273
313
|
|
|
274
314
|
return backupPath;
|
|
@@ -281,7 +321,7 @@ export class UpgradeManager {
|
|
|
281
321
|
const packagePath = path.join(__dirname, '..', '..', 'package.json');
|
|
282
322
|
const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf-8'));
|
|
283
323
|
return packageJson.version || null;
|
|
284
|
-
} catch (
|
|
324
|
+
} catch (_error) {
|
|
285
325
|
// Fallback: try to get version from globally installed package
|
|
286
326
|
try {
|
|
287
327
|
const { stdout } = await execAsync('npm list -g @sylphx/flow --depth=0 --json');
|
|
@@ -311,18 +351,31 @@ export class UpgradeManager {
|
|
|
311
351
|
}
|
|
312
352
|
}
|
|
313
353
|
|
|
314
|
-
private async getCurrentTargetVersion(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const match = stdout.match(/v?(\d+\.\d+\.\d+)/);
|
|
319
|
-
return match ? match[1] : null;
|
|
320
|
-
} catch {
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
354
|
+
private async getCurrentTargetVersion(targetId: string): Promise<string | null> {
|
|
355
|
+
const target = this.resolveTarget(targetId);
|
|
356
|
+
if (!target) {
|
|
357
|
+
return null;
|
|
323
358
|
}
|
|
324
359
|
|
|
325
|
-
|
|
360
|
+
// Each CLI target has specific version commands
|
|
361
|
+
try {
|
|
362
|
+
switch (target.id) {
|
|
363
|
+
case 'claude-code': {
|
|
364
|
+
const { stdout } = await execAsync('claude --version');
|
|
365
|
+
const match = stdout.match(/v?(\d+\.\d+\.\d+)/);
|
|
366
|
+
return match ? match[1] : null;
|
|
367
|
+
}
|
|
368
|
+
case 'opencode': {
|
|
369
|
+
const { stdout } = await execAsync('opencode --version');
|
|
370
|
+
const match = stdout.match(/v?(\d+\.\d+\.\d+)/);
|
|
371
|
+
return match ? match[1] : null;
|
|
372
|
+
}
|
|
373
|
+
default:
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
326
379
|
}
|
|
327
380
|
|
|
328
381
|
private async getLatestTargetVersion(): Promise<string | null> {
|
package/src/index.ts
CHANGED
|
@@ -8,16 +8,16 @@ import { readFileSync } from 'node:fs';
|
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { Command } from 'commander';
|
|
11
|
-
import {
|
|
11
|
+
import { executeFlow } from './commands/flow/execute-v2.js';
|
|
12
12
|
import {
|
|
13
|
+
doctorCommand,
|
|
13
14
|
flowCommand,
|
|
14
|
-
statusCommand,
|
|
15
15
|
setupCommand,
|
|
16
|
-
|
|
16
|
+
statusCommand,
|
|
17
17
|
upgradeCommand,
|
|
18
18
|
} from './commands/flow-command.js';
|
|
19
|
+
import { hookCommand } from './commands/hook-command.js';
|
|
19
20
|
import { settingsCommand } from './commands/settings-command.js';
|
|
20
|
-
import { executeFlow } from './commands/flow/execute-v2.js';
|
|
21
21
|
import { UserCancelledError } from './utils/errors.js';
|
|
22
22
|
|
|
23
23
|
// Read version from package.json
|
|
@@ -53,7 +53,10 @@ export function createCLI(): Command {
|
|
|
53
53
|
// Default action: delegate to flow command for convenience
|
|
54
54
|
// This allows `sylphx-flow "prompt"` instead of requiring `sylphx-flow flow "prompt"`
|
|
55
55
|
program
|
|
56
|
-
.argument(
|
|
56
|
+
.argument(
|
|
57
|
+
'[prompt]',
|
|
58
|
+
'Prompt to execute with agent (optional, supports @file.txt for file input)'
|
|
59
|
+
)
|
|
57
60
|
.option('--agent <name>', 'Agent to use (default: coder)', 'coder')
|
|
58
61
|
.option('--agent-file <path>', 'Load agent from specific file')
|
|
59
62
|
.option('--verbose', 'Show detailed output')
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { exec } from 'node:child_process';
|
|
7
|
-
import { promisify } from 'node:util';
|
|
8
7
|
import fs from 'node:fs/promises';
|
|
9
8
|
import path from 'node:path';
|
|
9
|
+
import { promisify } from 'node:util';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import ora from 'ora';
|
|
12
12
|
import { detectPackageManager, getUpgradeCommand } from '../utils/package-manager-detector.js';
|
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import os from 'node:os';
|
|
9
8
|
import {
|
|
10
9
|
CONFIG_DIR,
|
|
11
|
-
|
|
10
|
+
getProjectLocalSettingsFile,
|
|
12
11
|
getProjectSettingsFile,
|
|
13
|
-
|
|
12
|
+
USER_SETTINGS_FILE,
|
|
14
13
|
} from '../config/constants.js';
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -27,8 +26,8 @@ export interface UserSettings {
|
|
|
27
26
|
|
|
28
27
|
// API keys for providers
|
|
29
28
|
apiKeys?: {
|
|
30
|
-
kimi?: string;
|
|
31
|
-
'z.ai'?: string;
|
|
29
|
+
kimi?: string; // Kimi provider
|
|
30
|
+
'z.ai'?: string; // Z.ai proxy
|
|
32
31
|
};
|
|
33
32
|
|
|
34
33
|
// User preferences (can be changed anytime)
|
|
@@ -45,7 +44,7 @@ export interface UserSettings {
|
|
|
45
44
|
export interface ProjectSettings {
|
|
46
45
|
target?: string;
|
|
47
46
|
version?: string;
|
|
48
|
-
defaultAgent?: string;
|
|
47
|
+
defaultAgent?: string; // Can override user default per project
|
|
49
48
|
|
|
50
49
|
[key: string]: unknown;
|
|
51
50
|
}
|
|
@@ -55,9 +54,9 @@ export interface ProjectSettings {
|
|
|
55
54
|
* These are selected each run but can be overridden by CLI flags
|
|
56
55
|
*/
|
|
57
56
|
export interface RuntimeChoices {
|
|
58
|
-
provider?: string;
|
|
59
|
-
agent?: string;
|
|
60
|
-
prompt?: string;
|
|
57
|
+
provider?: string; // Selected for this run
|
|
58
|
+
agent?: string; // Selected for this run
|
|
59
|
+
prompt?: string; // User prompt for this run
|
|
61
60
|
|
|
62
61
|
[key: string]: unknown;
|
|
63
62
|
}
|
|
@@ -71,9 +70,9 @@ export class ConfigService {
|
|
|
71
70
|
project: ProjectSettings;
|
|
72
71
|
choices: RuntimeChoices;
|
|
73
72
|
}> {
|
|
74
|
-
const userSettings = await
|
|
75
|
-
const projectSettings = await
|
|
76
|
-
const localSettings = await
|
|
73
|
+
const userSettings = await ConfigService.loadHomeSettings();
|
|
74
|
+
const projectSettings = await ConfigService.loadProjectSettings(cwd);
|
|
75
|
+
const localSettings = await ConfigService.loadLocalSettings(cwd);
|
|
77
76
|
|
|
78
77
|
// Runtime choices merge: local > project > user defaults
|
|
79
78
|
const choices: RuntimeChoices = {
|
|
@@ -92,7 +91,7 @@ export class ConfigService {
|
|
|
92
91
|
* Legacy method for backward compatibility
|
|
93
92
|
*/
|
|
94
93
|
static async loadSettings(cwd: string = process.cwd()): Promise<any> {
|
|
95
|
-
const config = await
|
|
94
|
+
const config = await ConfigService.loadConfiguration(cwd);
|
|
96
95
|
return {
|
|
97
96
|
...config.user,
|
|
98
97
|
...config.project,
|
|
@@ -120,18 +119,21 @@ export class ConfigService {
|
|
|
120
119
|
await fs.mkdir(USER_SETTINGS_FILE.replace('/settings.json', ''), { recursive: true });
|
|
121
120
|
|
|
122
121
|
// Merge with existing settings and save
|
|
123
|
-
const existing = await
|
|
122
|
+
const existing = await ConfigService.loadHomeSettings();
|
|
124
123
|
const merged = { ...existing, ...settings };
|
|
125
|
-
await fs.writeFile(USER_SETTINGS_FILE, JSON.stringify(merged, null, 2)
|
|
124
|
+
await fs.writeFile(USER_SETTINGS_FILE, `${JSON.stringify(merged, null, 2)}\n`);
|
|
126
125
|
}
|
|
127
126
|
|
|
128
127
|
/**
|
|
129
128
|
* Check if user has completed initial setup (API keys configured)
|
|
130
129
|
*/
|
|
131
130
|
static async hasInitialSetup(): Promise<boolean> {
|
|
132
|
-
const userSettings = await
|
|
131
|
+
const userSettings = await ConfigService.loadHomeSettings();
|
|
133
132
|
// Check if user has completed setup (either has API keys OR has explicitly chosen default)
|
|
134
|
-
return !!(
|
|
133
|
+
return !!(
|
|
134
|
+
userSettings.hasCompletedSetup ||
|
|
135
|
+
(userSettings.apiKeys && Object.keys(userSettings.apiKeys).length > 0)
|
|
136
|
+
);
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
/**
|
|
@@ -140,8 +142,12 @@ export class ConfigService {
|
|
|
140
142
|
*/
|
|
141
143
|
static getAvailableProviders(userSettings: UserSettings): string[] {
|
|
142
144
|
const providers: string[] = ['default']; // Always available
|
|
143
|
-
if (userSettings.apiKeys?.kimi)
|
|
144
|
-
|
|
145
|
+
if (userSettings.apiKeys?.kimi) {
|
|
146
|
+
providers.push('kimi');
|
|
147
|
+
}
|
|
148
|
+
if (userSettings.apiKeys?.['z.ai']) {
|
|
149
|
+
providers.push('z.ai');
|
|
150
|
+
}
|
|
145
151
|
return providers;
|
|
146
152
|
}
|
|
147
153
|
|
|
@@ -161,17 +167,20 @@ export class ConfigService {
|
|
|
161
167
|
/**
|
|
162
168
|
* Save project-level settings
|
|
163
169
|
*/
|
|
164
|
-
static async saveProjectSettings(
|
|
170
|
+
static async saveProjectSettings(
|
|
171
|
+
settings: ProjectSettings,
|
|
172
|
+
cwd: string = process.cwd()
|
|
173
|
+
): Promise<void> {
|
|
165
174
|
// Ensure directory exists
|
|
166
175
|
const configDir = path.join(cwd, CONFIG_DIR);
|
|
167
176
|
await fs.mkdir(configDir, { recursive: true });
|
|
168
177
|
|
|
169
178
|
// Merge with existing settings and save
|
|
170
|
-
const existing = await
|
|
179
|
+
const existing = await ConfigService.loadProjectSettings(cwd);
|
|
171
180
|
const merged = { ...existing, ...settings };
|
|
172
181
|
|
|
173
182
|
const configPath = getProjectSettingsFile(cwd);
|
|
174
|
-
await fs.writeFile(configPath, JSON.stringify(merged, null, 2)
|
|
183
|
+
await fs.writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
/**
|
|
@@ -190,13 +199,16 @@ export class ConfigService {
|
|
|
190
199
|
/**
|
|
191
200
|
* Save project-local settings
|
|
192
201
|
*/
|
|
193
|
-
static async saveLocalSettings(
|
|
202
|
+
static async saveLocalSettings(
|
|
203
|
+
settings: RuntimeChoices,
|
|
204
|
+
cwd: string = process.cwd()
|
|
205
|
+
): Promise<void> {
|
|
194
206
|
// Ensure directory exists
|
|
195
207
|
const configDir = path.join(cwd, CONFIG_DIR);
|
|
196
208
|
await fs.mkdir(configDir, { recursive: true });
|
|
197
209
|
|
|
198
210
|
const configPath = getProjectLocalSettingsFile(cwd);
|
|
199
|
-
await fs.writeFile(configPath, JSON.stringify(settings, null, 2)
|
|
211
|
+
await fs.writeFile(configPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
/**
|
|
@@ -209,14 +221,14 @@ export class ConfigService {
|
|
|
209
221
|
): Promise<void> {
|
|
210
222
|
// Save API keys to home directory
|
|
211
223
|
if (userConfig.claudeApiKey || userConfig.claudeProvider || userConfig.claudeProviderConfig) {
|
|
212
|
-
await
|
|
224
|
+
await ConfigService.saveHomeSettings(userConfig);
|
|
213
225
|
}
|
|
214
226
|
|
|
215
227
|
// Save other settings to project
|
|
216
|
-
await
|
|
228
|
+
await ConfigService.saveProjectSettings(projectConfig, cwd);
|
|
217
229
|
|
|
218
230
|
// Create .gitignore pattern file if it doesn't exist (excluding .local.json)
|
|
219
|
-
await
|
|
231
|
+
await ConfigService.addGitignore(cwd);
|
|
220
232
|
}
|
|
221
233
|
|
|
222
234
|
/**
|
|
@@ -235,11 +247,11 @@ export class ConfigService {
|
|
|
235
247
|
|
|
236
248
|
// Check if pattern already exists
|
|
237
249
|
if (!content.includes('.sylphx-flow/*.local.json')) {
|
|
238
|
-
await fs.appendFile(gitignorePath, patterns.join('\n')
|
|
250
|
+
await fs.appendFile(gitignorePath, `${patterns.join('\n')}\n`);
|
|
239
251
|
}
|
|
240
252
|
} catch {
|
|
241
253
|
// .gitignore doesn't exist - create it
|
|
242
|
-
await fs.writeFile(gitignorePath, patterns.join('\n').trim()
|
|
254
|
+
await fs.writeFile(gitignorePath, `${patterns.join('\n').trim()}\n`);
|
|
243
255
|
}
|
|
244
256
|
}
|
|
245
257
|
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* Manages all Flow settings in ~/.sylphx-flow/
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
6
7
|
import fs from 'node:fs/promises';
|
|
7
|
-
import path from 'node:path';
|
|
8
8
|
import os from 'node:os';
|
|
9
|
-
import
|
|
9
|
+
import path from 'node:path';
|
|
10
10
|
|
|
11
11
|
export interface GlobalSettings {
|
|
12
12
|
version: string;
|
|
13
|
-
defaultTarget?: 'claude-code' | 'opencode' | '
|
|
13
|
+
defaultTarget?: 'claude-code' | 'opencode' | '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;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Target Installation Service
|
|
3
|
-
* Auto-detects and installs AI CLI tools (Claude Code, OpenCode
|
|
3
|
+
* Auto-detects and installs AI CLI tools (Claude Code, OpenCode)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { exec } from 'node:child_process';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
|
-
import ora from 'ora';
|
|
10
9
|
import inquirer from 'inquirer';
|
|
11
|
-
import
|
|
10
|
+
import ora from 'ora';
|
|
12
11
|
import { UserCancelledError } from '../utils/errors.js';
|
|
12
|
+
import { detectPackageManager, type PackageManager } from '../utils/package-manager-detector.js';
|
|
13
13
|
|
|
14
14
|
const execAsync = promisify(exec);
|
|
15
15
|
|
|
16
16
|
export interface TargetInstallation {
|
|
17
|
-
id: 'claude-code' | 'opencode'
|
|
17
|
+
id: 'claude-code' | 'opencode';
|
|
18
18
|
name: string;
|
|
19
19
|
package: string;
|
|
20
20
|
checkCommand: string;
|
|
@@ -61,16 +61,6 @@ const TARGET_INSTALLATIONS: TargetInstallation[] = [
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
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
64
|
];
|
|
75
65
|
|
|
76
66
|
export class TargetInstaller {
|
|
@@ -136,9 +126,10 @@ export class TargetInstaller {
|
|
|
136
126
|
]);
|
|
137
127
|
|
|
138
128
|
return targetId;
|
|
139
|
-
} catch (error:
|
|
129
|
+
} catch (error: unknown) {
|
|
140
130
|
// Handle user cancellation (Ctrl+C)
|
|
141
|
-
|
|
131
|
+
const err = error as Error & { name?: string };
|
|
132
|
+
if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
|
|
142
133
|
throw new UserCancelledError('Target selection cancelled');
|
|
143
134
|
}
|
|
144
135
|
throw error;
|
|
@@ -159,13 +150,6 @@ export class TargetInstaller {
|
|
|
159
150
|
return false;
|
|
160
151
|
}
|
|
161
152
|
|
|
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
153
|
// Confirm installation unless auto-confirm is enabled
|
|
170
154
|
if (!autoConfirm) {
|
|
171
155
|
try {
|
|
@@ -182,9 +166,10 @@ export class TargetInstaller {
|
|
|
182
166
|
console.log(chalk.yellow('\n⚠️ Installation cancelled\n'));
|
|
183
167
|
return false;
|
|
184
168
|
}
|
|
185
|
-
} catch (error:
|
|
169
|
+
} catch (error: unknown) {
|
|
186
170
|
// Handle user cancellation (Ctrl+C)
|
|
187
|
-
|
|
171
|
+
const err = error as Error & { name?: string };
|
|
172
|
+
if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
|
|
188
173
|
throw new UserCancelledError('Installation cancelled');
|
|
189
174
|
}
|
|
190
175
|
throw error;
|
|
@@ -199,7 +184,7 @@ export class TargetInstaller {
|
|
|
199
184
|
|
|
200
185
|
spinner.succeed(chalk.green(`✓ ${installation.name} installed successfully`));
|
|
201
186
|
return true;
|
|
202
|
-
} catch (
|
|
187
|
+
} catch (_error) {
|
|
203
188
|
spinner.fail(chalk.red(`✗ Failed to install ${installation.name}`));
|
|
204
189
|
|
|
205
190
|
const installCmd = installation.installCommand(this.packageManager);
|