@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +44 -0
  3. package/package.json +79 -73
  4. package/src/commands/flow/execute-v2.ts +37 -29
  5. package/src/commands/flow/prompt.ts +5 -3
  6. package/src/commands/flow/types.ts +0 -2
  7. package/src/commands/flow-command.ts +20 -13
  8. package/src/commands/hook-command.ts +1 -3
  9. package/src/commands/settings/checkbox-config.ts +128 -0
  10. package/src/commands/settings/index.ts +6 -0
  11. package/src/commands/settings-command.ts +84 -156
  12. package/src/config/ai-config.ts +60 -41
  13. package/src/core/agent-loader.ts +11 -6
  14. package/src/core/attach/file-attacher.ts +172 -0
  15. package/src/core/attach/index.ts +5 -0
  16. package/src/core/attach-manager.ts +117 -171
  17. package/src/core/backup-manager.ts +35 -29
  18. package/src/core/cleanup-handler.ts +11 -8
  19. package/src/core/error-handling.ts +23 -30
  20. package/src/core/flow-executor.ts +58 -76
  21. package/src/core/formatting/bytes.ts +2 -4
  22. package/src/core/functional/async.ts +5 -4
  23. package/src/core/functional/error-handler.ts +2 -2
  24. package/src/core/git-stash-manager.ts +21 -10
  25. package/src/core/installers/file-installer.ts +0 -1
  26. package/src/core/installers/mcp-installer.ts +0 -1
  27. package/src/core/project-manager.ts +24 -18
  28. package/src/core/secrets-manager.ts +54 -73
  29. package/src/core/session-manager.ts +20 -22
  30. package/src/core/state-detector.ts +139 -80
  31. package/src/core/template-loader.ts +13 -31
  32. package/src/core/upgrade-manager.ts +122 -69
  33. package/src/index.ts +8 -5
  34. package/src/services/auto-upgrade.ts +1 -1
  35. package/src/services/config-service.ts +41 -29
  36. package/src/services/global-config.ts +3 -3
  37. package/src/services/target-installer.ts +11 -26
  38. package/src/targets/claude-code.ts +35 -81
  39. package/src/targets/opencode.ts +28 -68
  40. package/src/targets/shared/index.ts +7 -0
  41. package/src/targets/shared/mcp-transforms.ts +132 -0
  42. package/src/targets/shared/target-operations.ts +135 -0
  43. package/src/types/cli.types.ts +2 -2
  44. package/src/types/provider.types.ts +1 -7
  45. package/src/types/session.types.ts +11 -11
  46. package/src/types/target.types.ts +3 -1
  47. package/src/types/todo.types.ts +1 -1
  48. package/src/types.ts +1 -1
  49. package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
  50. package/src/utils/agent-enhancer.ts +4 -4
  51. package/src/utils/config/paths.ts +3 -1
  52. package/src/utils/config/target-utils.ts +2 -2
  53. package/src/utils/display/banner.ts +2 -2
  54. package/src/utils/display/notifications.ts +58 -45
  55. package/src/utils/display/status.ts +29 -12
  56. package/src/utils/files/file-operations.ts +1 -1
  57. package/src/utils/files/sync-utils.ts +38 -41
  58. package/src/utils/index.ts +19 -27
  59. package/src/utils/package-manager-detector.ts +15 -5
  60. package/src/utils/security/security.ts +8 -4
  61. package/src/utils/target-selection.ts +6 -8
  62. package/src/utils/version.ts +4 -2
  63. package/src/commands/flow-orchestrator.ts +0 -328
  64. package/src/commands/init-command.ts +0 -92
  65. package/src/commands/init-core.ts +0 -331
  66. package/src/core/agent-manager.ts +0 -174
  67. package/src/core/loop-controller.ts +0 -200
  68. package/src/core/rule-loader.ts +0 -147
  69. package/src/core/rule-manager.ts +0 -240
  70. package/src/services/claude-config-service.ts +0 -252
  71. package/src/services/first-run-setup.ts +0 -220
  72. package/src/services/smart-config-service.ts +0 -269
  73. package/src/types/api.types.ts +0 -9
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { projectSettings } from '../utils/config/settings.js';
5
- import { targetManager } from './target-manager.js';
6
4
  import { ConfigService } from '../services/config-service.js';
5
+ import type { Target } from '../types/target.types.js';
6
+ import { targetManager } from './target-manager.js';
7
7
 
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
@@ -28,7 +28,13 @@ export interface ProjectState {
28
28
  lastUpdated: Date | null;
29
29
  }
30
30
 
31
- export type RecommendedAction = 'FULL_INIT' | 'RUN_ONLY' | 'REPAIR' | 'UPGRADE' | 'UPGRADE_TARGET' | 'CLEAN_INIT';
31
+ export type RecommendedAction =
32
+ | 'FULL_INIT'
33
+ | 'RUN_ONLY'
34
+ | 'REPAIR'
35
+ | 'UPGRADE'
36
+ | 'UPGRADE_TARGET'
37
+ | 'CLEAN_INIT';
32
38
 
33
39
  export class StateDetector {
34
40
  private projectPath: string;
@@ -37,6 +43,17 @@ export class StateDetector {
37
43
  this.projectPath = projectPath;
38
44
  }
39
45
 
46
+ /**
47
+ * Resolve target from ID string to Target object
48
+ */
49
+ private resolveTarget(targetId: string): Target | null {
50
+ const targetOption = targetManager.getTarget(targetId);
51
+ if (targetOption._tag === 'None') {
52
+ return null;
53
+ }
54
+ return targetOption.value;
55
+ }
56
+
40
57
  async detect(): Promise<ProjectState> {
41
58
  const state: ProjectState = {
42
59
  initialized: false,
@@ -77,37 +94,16 @@ export class StateDetector {
77
94
  state.outdated = this.isVersionOutdated(state.version, state.latestVersion);
78
95
  }
79
96
 
80
- // Check components based on target
81
- if (state.target === 'opencode') {
82
- // OpenCode uses different directory structure
83
- await this.checkComponent('agents', '.opencode/agent', '*.md', state);
84
- // OpenCode uses AGENTS.md for rules
85
- await this.checkFileComponent('rules', 'AGENTS.md', state);
86
- // OpenCode doesn't have separate hooks directory (hooks config in opencode.jsonc)
87
- state.components.hooks.installed = false;
88
- // OpenCode appends output styles to AGENTS.md
89
- state.components.outputStyles.installed = await this.checkOutputStylesInAGENTS();
90
- await this.checkComponent('slashCommands', '.opencode/command', '*.md', state);
91
- } else {
92
- // Claude Code (default)
93
- await this.checkComponent('agents', '.claude/agents', '*.md', state);
94
-
95
- // Claude Code includes rules and output styles in agent files
96
- // So we mark them as installed if agents are installed
97
- state.components.rules.installed = state.components.agents.installed;
98
- state.components.rules.count = state.components.agents.count;
99
-
100
- state.components.outputStyles.installed = state.components.agents.installed;
101
-
102
- // Check hooks (optional for Claude Code)
103
- await this.checkComponent('hooks', '.claude/hooks', '*.js', state);
104
-
105
- // Check slash commands
106
- await this.checkComponent('slashCommands', '.claude/commands', '*.md', state);
97
+ // Resolve target to get config
98
+ const target = state.target ? this.resolveTarget(state.target) : null;
99
+
100
+ // Check components based on target config
101
+ if (target) {
102
+ await this.checkComponentsForTarget(target, state);
107
103
  }
108
104
 
109
105
  // Check MCP
110
- const mcpConfig = await this.checkMCPConfig(state.target);
106
+ const mcpConfig = await this.checkMCPConfig(target);
111
107
  state.components.mcp.installed = mcpConfig.exists;
112
108
  state.components.mcp.serverCount = mcpConfig.serverCount;
113
109
  state.components.mcp.version = mcpConfig.version;
@@ -121,14 +117,45 @@ export class StateDetector {
121
117
 
122
118
  // Check corruption
123
119
  state.corrupted = await this.checkCorruption(state);
124
-
125
- } catch (error) {
120
+ } catch (_error) {
126
121
  state.corrupted = true;
127
122
  }
128
123
 
129
124
  return state;
130
125
  }
131
126
 
127
+ /**
128
+ * Check components based on target configuration
129
+ */
130
+ private async checkComponentsForTarget(target: Target, state: ProjectState): Promise<void> {
131
+ // Check agents using target's agentDir
132
+ await this.checkComponent('agents', target.config.agentDir, '*.md', state);
133
+
134
+ // Check rules based on target config
135
+ if (target.config.rulesFile) {
136
+ // Target has separate rules file (e.g., OpenCode's AGENTS.md)
137
+ await this.checkFileComponent('rules', target.config.rulesFile, state);
138
+ // Check output styles in rules file
139
+ state.components.outputStyles.installed = await this.checkOutputStylesInFile(
140
+ target.config.rulesFile
141
+ );
142
+ } else {
143
+ // Rules are included in agent files (e.g., Claude Code)
144
+ state.components.rules.installed = state.components.agents.installed;
145
+ state.components.rules.count = state.components.agents.count;
146
+ state.components.outputStyles.installed = state.components.agents.installed;
147
+ }
148
+
149
+ // Check hooks - look for hooks directory in configDir
150
+ const hooksDir = path.join(target.config.configDir, 'hooks');
151
+ await this.checkComponent('hooks', hooksDir, '*.js', state);
152
+
153
+ // Check slash commands using target's slashCommandsDir
154
+ if (target.config.slashCommandsDir) {
155
+ await this.checkComponent('slashCommands', target.config.slashCommandsDir, '*.md', state);
156
+ }
157
+ }
158
+
132
159
  recommendAction(state: ProjectState): RecommendedAction {
133
160
  if (!state.initialized) {
134
161
  return 'FULL_INIT';
@@ -142,8 +169,11 @@ export class StateDetector {
142
169
  return 'UPGRADE';
143
170
  }
144
171
 
145
- if (state.targetVersion && state.targetLatestVersion &&
146
- this.isVersionOutdated(state.targetVersion, state.targetLatestVersion)) {
172
+ if (
173
+ state.targetVersion &&
174
+ state.targetLatestVersion &&
175
+ this.isVersionOutdated(state.targetVersion, state.targetLatestVersion)
176
+ ) {
147
177
  return 'UPGRADE_TARGET';
148
178
  }
149
179
 
@@ -170,8 +200,11 @@ export class StateDetector {
170
200
  explanations.push('Run `bun dev:flow upgrade` to upgrade');
171
201
  }
172
202
 
173
- if (state.targetVersion && state.targetLatestVersion &&
174
- this.isVersionOutdated(state.targetVersion, state.targetLatestVersion)) {
203
+ if (
204
+ state.targetVersion &&
205
+ state.targetLatestVersion &&
206
+ this.isVersionOutdated(state.targetVersion, state.targetLatestVersion)
207
+ ) {
175
208
  explanations.push(`${state.target} update available`);
176
209
  explanations.push(`Run \`bun dev:flow upgrade-target\` to upgrade`);
177
210
  }
@@ -210,7 +243,10 @@ export class StateDetector {
210
243
  ): Promise<void> {
211
244
  try {
212
245
  const fullPath = path.join(this.projectPath, componentPath);
213
- const exists = await fs.access(fullPath).then(() => true).catch(() => false);
246
+ const exists = await fs
247
+ .access(fullPath)
248
+ .then(() => true)
249
+ .catch(() => false);
214
250
 
215
251
  if (!exists) {
216
252
  state.components[componentName].installed = false;
@@ -219,19 +255,30 @@ export class StateDetector {
219
255
 
220
256
  // 计算文件数量
221
257
  const files = await fs.readdir(fullPath).catch(() => []);
222
- const count = pattern === '*.js' ? files.filter(f => f.endsWith('.js')).length :
223
- pattern === '*.md' ? files.filter(f => f.endsWith('.md')).length : files.length;
258
+ const count =
259
+ pattern === '*.js'
260
+ ? files.filter((f) => f.endsWith('.js')).length
261
+ : pattern === '*.md'
262
+ ? files.filter((f) => f.endsWith('.md')).length
263
+ : files.length;
224
264
 
225
265
  // Component is only installed if it has files
226
266
  state.components[componentName].installed = count > 0;
227
267
 
228
- if (componentName === 'agents' || componentName === 'slashCommands' || componentName === 'rules') {
268
+ if (
269
+ componentName === 'agents' ||
270
+ componentName === 'slashCommands' ||
271
+ componentName === 'rules'
272
+ ) {
229
273
  state.components[componentName].count = count;
230
274
  }
231
275
 
232
276
  // 这里可以读取版本信息(如果保存了的话)
233
277
  const versionPath = path.join(fullPath, '.version');
234
- const versionExists = await fs.access(versionPath).then(() => true).catch(() => false);
278
+ const versionExists = await fs
279
+ .access(versionPath)
280
+ .then(() => true)
281
+ .catch(() => false);
235
282
  if (versionExists) {
236
283
  state.components[componentName].version = await fs.readFile(versionPath, 'utf-8');
237
284
  }
@@ -247,7 +294,10 @@ export class StateDetector {
247
294
  ): Promise<void> {
248
295
  try {
249
296
  const fullPath = path.join(this.projectPath, filePath);
250
- const exists = await fs.access(fullPath).then(() => true).catch(() => false);
297
+ const exists = await fs
298
+ .access(fullPath)
299
+ .then(() => true)
300
+ .catch(() => false);
251
301
 
252
302
  state.components[componentName].installed = exists;
253
303
 
@@ -260,53 +310,53 @@ export class StateDetector {
260
310
  }
261
311
  }
262
312
 
263
- private async checkOutputStylesInAGENTS(): Promise<boolean> {
313
+ private async checkOutputStylesInFile(filePath: string): Promise<boolean> {
264
314
  try {
265
- const agentsPath = path.join(this.projectPath, 'AGENTS.md');
266
- const exists = await fs.access(agentsPath).then(() => true).catch(() => false);
315
+ const fullPath = path.join(this.projectPath, filePath);
316
+ const exists = await fs
317
+ .access(fullPath)
318
+ .then(() => true)
319
+ .catch(() => false);
267
320
 
268
321
  if (!exists) {
269
322
  return false;
270
323
  }
271
324
 
272
- // Check if AGENTS.md contains output styles section
273
- const content = await fs.readFile(agentsPath, 'utf-8');
325
+ // Check if file contains output styles section
326
+ const content = await fs.readFile(fullPath, 'utf-8');
274
327
  return content.includes('# Output Styles');
275
328
  } catch {
276
329
  return false;
277
330
  }
278
331
  }
279
332
 
280
- private async checkMCPConfig(target?: string | null): Promise<{ exists: boolean; serverCount: number; version: string | null }> {
333
+ private async checkMCPConfig(
334
+ target?: Target | null
335
+ ): Promise<{ exists: boolean; serverCount: number; version: string | null }> {
281
336
  try {
282
- let mcpPath: string;
283
- let serversKey: string;
284
-
285
- if (target === 'opencode') {
286
- // OpenCode uses opencode.jsonc with mcp key
287
- mcpPath = path.join(this.projectPath, 'opencode.jsonc');
288
- serversKey = 'mcp';
289
- } else {
290
- // Claude Code uses .mcp.json with mcpServers key
291
- mcpPath = path.join(this.projectPath, '.mcp.json');
292
- serversKey = 'mcpServers';
337
+ if (!target) {
338
+ return { exists: false, serverCount: 0, version: null };
293
339
  }
294
340
 
295
- const exists = await fs.access(mcpPath).then(() => true).catch(() => false);
341
+ // Use target config for MCP file path and servers key
342
+ const mcpPath = path.join(this.projectPath, target.config.configFile);
343
+ const serversKey = target.config.mcpConfigPath;
344
+
345
+ const exists = await fs
346
+ .access(mcpPath)
347
+ .then(() => true)
348
+ .catch(() => false);
296
349
 
297
350
  if (!exists) {
298
351
  return { exists: false, serverCount: 0, version: null };
299
352
  }
300
353
 
301
- // Use proper JSONC parser for OpenCode (handles comments)
354
+ // Use target's readConfig method for proper parsing (handles JSONC, etc.)
302
355
  let content: any;
303
- if (target === 'opencode') {
304
- // Import dynamically to avoid circular dependency
305
- const { fileUtils } = await import('../utils/target-utils.js');
306
- const { opencodeTarget } = await import('../targets/opencode.js');
307
- content = await fileUtils.readConfig(opencodeTarget.config, this.projectPath);
308
- } else {
309
- // Claude Code uses plain JSON
356
+ try {
357
+ content = await target.readConfig(this.projectPath);
358
+ } catch {
359
+ // Fallback to plain JSON parsing
310
360
  content = JSON.parse(await fs.readFile(mcpPath, 'utf-8'));
311
361
  }
312
362
 
@@ -322,19 +372,24 @@ export class StateDetector {
322
372
  }
323
373
  }
324
374
 
325
- private async checkTargetVersion(target: string): Promise<{ version: string | null; latestVersion: string | null }> {
375
+ private async checkTargetVersion(
376
+ targetId: string
377
+ ): Promise<{ version: string | null; latestVersion: string | null }> {
326
378
  try {
327
- // 这里可以检查目标平台的版本
328
- // 例如检查 claude CLI 版本或 opencode 版本
329
- if (target === 'claude-code') {
330
- // 检查 claude --version
379
+ const target = this.resolveTarget(targetId);
380
+ if (!target) {
381
+ return { version: null, latestVersion: null };
382
+ }
383
+
384
+ // Check if target has executeCommand (CLI-based target)
385
+ // Only CLI targets like claude-code have version checking capability
386
+ if (target.executeCommand && target.id === 'claude-code') {
331
387
  const { exec } = await import('node:child_process');
332
388
  const { promisify } = await import('node:util');
333
389
  const execAsync = promisify(exec);
334
390
 
335
391
  try {
336
392
  const { stdout } = await execAsync('claude --version');
337
- // 解析版本号
338
393
  const match = stdout.match(/v?(\d+\.\d+\.\d+)/);
339
394
  return {
340
395
  version: match ? match[1] : null,
@@ -366,14 +421,18 @@ export class StateDetector {
366
421
  }
367
422
 
368
423
  private async checkCorruption(state: ProjectState): Promise<boolean> {
369
- // 检查是否存在矛盾的状态
424
+ // Check for contradictory states
370
425
  if (state.initialized && !state.target) {
371
- return true; // 初始化咗但冇 target
426
+ return true; // Initialized but no target
372
427
  }
373
428
 
374
- // 检查必需组件 - only check agents for claude-code
375
- if (state.initialized && state.target === 'claude-code' && !state.components.agents.installed) {
376
- return true; // claude-code 初始化咗但冇 agents
429
+ // Check required components based on target
430
+ if (state.initialized && state.target) {
431
+ const target = this.resolveTarget(state.target);
432
+ // CLI-based targets (category: 'cli') require agents to be installed
433
+ if (target && target.category === 'cli' && !state.components.agents.installed) {
434
+ return true; // CLI target initialized but no agents
435
+ }
377
436
  }
378
437
 
379
438
  return false;
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Template Loader
3
3
  * Loads Flow templates from assets directory
4
- * Supports both claude-code and opencode targets
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(target: 'claude-code' | 'opencode'): Promise<FlowTemplates> {
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')) continue;
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')) continue;
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(target: 'claude-code' | 'opencode'): Promise<boolean> {
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');