@sylphx/flow 2.1.2 → 2.1.4
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 +23 -0
- package/README.md +44 -0
- package/package.json +79 -73
- package/src/commands/flow/execute-v2.ts +39 -30
- package/src/commands/flow/index.ts +2 -4
- package/src/commands/flow/prompt.ts +5 -3
- package/src/commands/flow/types.ts +0 -9
- package/src/commands/flow-command.ts +20 -13
- package/src/commands/hook-command.ts +1 -3
- package/src/commands/settings-command.ts +36 -33
- package/src/config/ai-config.ts +60 -41
- package/src/core/agent-loader.ts +11 -6
- package/src/core/attach-manager.ts +92 -84
- 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 +2 -2
- package/src/services/target-installer.ts +9 -7
- package/src/targets/claude-code.ts +28 -15
- package/src/targets/opencode.ts +17 -6
- 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 +111 -3
- 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 +5 -2
- package/src/utils/version.ts +4 -2
- package/src/commands/flow/execute.ts +0 -453
- package/src/commands/flow/setup.ts +0 -312
- 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/commands/run-command.ts +0 -126
- 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,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Template Loader
|
|
3
3
|
* Loads Flow templates from assets directory
|
|
4
|
-
* Supports
|
|
4
|
+
* Supports any target with consistent template structure
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
7
8
|
import fs from 'node:fs/promises';
|
|
8
9
|
import path from 'node:path';
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import type { Target } from '../types/target.types.js';
|
|
11
12
|
import type { FlowTemplates } from './attach-manager.js';
|
|
12
13
|
|
|
13
14
|
export class TemplateLoader {
|
|
@@ -24,7 +25,7 @@ export class TemplateLoader {
|
|
|
24
25
|
* Load all templates for target
|
|
25
26
|
* Uses flat assets directory structure (no target-specific subdirectories)
|
|
26
27
|
*/
|
|
27
|
-
async loadTemplates(
|
|
28
|
+
async loadTemplates(_target: Target | string): Promise<FlowTemplates> {
|
|
28
29
|
const templates: FlowTemplates = {
|
|
29
30
|
agents: [],
|
|
30
31
|
commands: [],
|
|
@@ -77,14 +78,14 @@ export class TemplateLoader {
|
|
|
77
78
|
/**
|
|
78
79
|
* Load agents from directory
|
|
79
80
|
*/
|
|
80
|
-
private async loadAgents(
|
|
81
|
-
agentsDir: string
|
|
82
|
-
): Promise<Array<{ name: string; content: string }>> {
|
|
81
|
+
private async loadAgents(agentsDir: string): Promise<Array<{ name: string; content: string }>> {
|
|
83
82
|
const agents = [];
|
|
84
83
|
const files = await fs.readdir(agentsDir);
|
|
85
84
|
|
|
86
85
|
for (const file of files) {
|
|
87
|
-
if (!file.endsWith('.md'))
|
|
86
|
+
if (!file.endsWith('.md')) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
88
89
|
|
|
89
90
|
const content = await fs.readFile(path.join(agentsDir, file), 'utf-8');
|
|
90
91
|
agents.push({ name: file, content });
|
|
@@ -103,7 +104,9 @@ export class TemplateLoader {
|
|
|
103
104
|
const files = await fs.readdir(commandsDir);
|
|
104
105
|
|
|
105
106
|
for (const file of files) {
|
|
106
|
-
if (!file.endsWith('.md'))
|
|
107
|
+
if (!file.endsWith('.md')) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
107
110
|
|
|
108
111
|
const content = await fs.readFile(path.join(commandsDir, file), 'utf-8');
|
|
109
112
|
commands.push({ name: file, content });
|
|
@@ -115,9 +118,7 @@ export class TemplateLoader {
|
|
|
115
118
|
/**
|
|
116
119
|
* Load MCP servers configuration
|
|
117
120
|
*/
|
|
118
|
-
private async loadMCPServers(
|
|
119
|
-
configPath: string
|
|
120
|
-
): Promise<Array<{ name: string; config: any }>> {
|
|
121
|
+
private async loadMCPServers(configPath: string): Promise<Array<{ name: string; config: any }>> {
|
|
121
122
|
const data = await fs.readFile(configPath, 'utf-8');
|
|
122
123
|
const config = JSON.parse(data);
|
|
123
124
|
|
|
@@ -129,25 +130,6 @@ export class TemplateLoader {
|
|
|
129
130
|
return servers;
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
/**
|
|
133
|
-
* Load hooks from directory
|
|
134
|
-
*/
|
|
135
|
-
private async loadHooks(
|
|
136
|
-
hooksDir: string
|
|
137
|
-
): Promise<Array<{ name: string; content: string }>> {
|
|
138
|
-
const hooks = [];
|
|
139
|
-
const files = await fs.readdir(hooksDir);
|
|
140
|
-
|
|
141
|
-
for (const file of files) {
|
|
142
|
-
if (!file.endsWith('.js')) continue;
|
|
143
|
-
|
|
144
|
-
const content = await fs.readFile(path.join(hooksDir, file), 'utf-8');
|
|
145
|
-
hooks.push({ name: file, content });
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return hooks;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
133
|
/**
|
|
152
134
|
* Load single files (CLAUDE.md, .cursorrules, etc.)
|
|
153
135
|
*/
|
|
@@ -180,7 +162,7 @@ export class TemplateLoader {
|
|
|
180
162
|
/**
|
|
181
163
|
* Check if templates exist (uses flat directory structure)
|
|
182
164
|
*/
|
|
183
|
-
async hasTemplates(
|
|
165
|
+
async hasTemplates(_target: Target | string): Promise<boolean> {
|
|
184
166
|
// Check if any template directories exist
|
|
185
167
|
const agentsDir = path.join(this.assetsDir, 'agents');
|
|
186
168
|
const commandsDir = path.join(this.assetsDir, 'slash-commands');
|
|
@@ -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,10 +3,10 @@
|
|
|
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;
|
|
@@ -6,10 +6,10 @@
|
|
|
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
|
|
|
@@ -136,9 +136,10 @@ export class TargetInstaller {
|
|
|
136
136
|
]);
|
|
137
137
|
|
|
138
138
|
return targetId;
|
|
139
|
-
} catch (error:
|
|
139
|
+
} catch (error: unknown) {
|
|
140
140
|
// Handle user cancellation (Ctrl+C)
|
|
141
|
-
|
|
141
|
+
const err = error as Error & { name?: string };
|
|
142
|
+
if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
|
|
142
143
|
throw new UserCancelledError('Target selection cancelled');
|
|
143
144
|
}
|
|
144
145
|
throw error;
|
|
@@ -182,9 +183,10 @@ export class TargetInstaller {
|
|
|
182
183
|
console.log(chalk.yellow('\n⚠️ Installation cancelled\n'));
|
|
183
184
|
return false;
|
|
184
185
|
}
|
|
185
|
-
} catch (error:
|
|
186
|
+
} catch (error: unknown) {
|
|
186
187
|
// Handle user cancellation (Ctrl+C)
|
|
187
|
-
|
|
188
|
+
const err = error as Error & { name?: string };
|
|
189
|
+
if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
|
|
188
190
|
throw new UserCancelledError('Installation cancelled');
|
|
189
191
|
}
|
|
190
192
|
throw error;
|
|
@@ -199,7 +201,7 @@ export class TargetInstaller {
|
|
|
199
201
|
|
|
200
202
|
spinner.succeed(chalk.green(`✓ ${installation.name} installed successfully`));
|
|
201
203
|
return true;
|
|
202
|
-
} catch (
|
|
204
|
+
} catch (_error) {
|
|
203
205
|
spinner.fail(chalk.red(`✗ Failed to install ${installation.name}`));
|
|
204
206
|
|
|
205
207
|
const installCmd = installation.installCommand(this.packageManager);
|