@sylphx/flow 2.1.3 → 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.
Files changed (66) hide show
  1. package/CHANGELOG.md +12 -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-command.ts +36 -33
  10. package/src/config/ai-config.ts +60 -41
  11. package/src/core/agent-loader.ts +11 -6
  12. package/src/core/attach-manager.ts +92 -84
  13. package/src/core/backup-manager.ts +35 -29
  14. package/src/core/cleanup-handler.ts +11 -8
  15. package/src/core/error-handling.ts +23 -30
  16. package/src/core/flow-executor.ts +58 -76
  17. package/src/core/formatting/bytes.ts +2 -4
  18. package/src/core/functional/async.ts +5 -4
  19. package/src/core/functional/error-handler.ts +2 -2
  20. package/src/core/git-stash-manager.ts +21 -10
  21. package/src/core/installers/file-installer.ts +0 -1
  22. package/src/core/installers/mcp-installer.ts +0 -1
  23. package/src/core/project-manager.ts +24 -18
  24. package/src/core/secrets-manager.ts +54 -73
  25. package/src/core/session-manager.ts +20 -22
  26. package/src/core/state-detector.ts +139 -80
  27. package/src/core/template-loader.ts +13 -31
  28. package/src/core/upgrade-manager.ts +122 -69
  29. package/src/index.ts +8 -5
  30. package/src/services/auto-upgrade.ts +1 -1
  31. package/src/services/config-service.ts +41 -29
  32. package/src/services/global-config.ts +2 -2
  33. package/src/services/target-installer.ts +9 -7
  34. package/src/targets/claude-code.ts +24 -12
  35. package/src/targets/opencode.ts +17 -6
  36. package/src/types/cli.types.ts +2 -2
  37. package/src/types/provider.types.ts +1 -7
  38. package/src/types/session.types.ts +11 -11
  39. package/src/types/target.types.ts +3 -1
  40. package/src/types/todo.types.ts +1 -1
  41. package/src/types.ts +1 -1
  42. package/src/utils/__tests__/package-manager-detector.test.ts +6 -6
  43. package/src/utils/agent-enhancer.ts +4 -4
  44. package/src/utils/config/paths.ts +3 -1
  45. package/src/utils/config/target-utils.ts +2 -2
  46. package/src/utils/display/banner.ts +2 -2
  47. package/src/utils/display/notifications.ts +58 -45
  48. package/src/utils/display/status.ts +29 -12
  49. package/src/utils/files/file-operations.ts +1 -1
  50. package/src/utils/files/sync-utils.ts +38 -41
  51. package/src/utils/index.ts +19 -27
  52. package/src/utils/package-manager-detector.ts +15 -5
  53. package/src/utils/security/security.ts +8 -4
  54. package/src/utils/target-selection.ts +5 -2
  55. package/src/utils/version.ts +4 -2
  56. package/src/commands/flow-orchestrator.ts +0 -328
  57. package/src/commands/init-command.ts +0 -92
  58. package/src/commands/init-core.ts +0 -331
  59. package/src/core/agent-manager.ts +0 -174
  60. package/src/core/loop-controller.ts +0 -200
  61. package/src/core/rule-loader.ts +0 -147
  62. package/src/core/rule-manager.ts +0 -240
  63. package/src/services/claude-config-service.ts +0 -252
  64. package/src/services/first-run-setup.ts +0 -220
  65. package/src/services/smart-config-service.ts +0 -269
  66. package/src/types/api.types.ts +0 -9
@@ -1,200 +0,0 @@
1
- /**
2
- * Loop Controller - Simple continuous execution
3
- *
4
- * Core concept: Keep executing the same task with context persistence
5
- * - First run: Fresh start
6
- * - Subsequent runs: Auto-continue (builds on previous work)
7
- * - Stop: Manual (Ctrl+C) or max-runs limit
8
- *
9
- * Use case: "Keep working on X until I stop you"
10
- */
11
-
12
- import chalk from 'chalk';
13
- import type { FlowOptions } from '../commands/flow/types.js';
14
-
15
- export interface LoopOptions {
16
- enabled: boolean;
17
- interval: number; // Wait time in seconds between runs (0 = no wait)
18
- maxRuns?: number; // Optional max iterations (default: infinite)
19
- }
20
-
21
- export interface LoopResult {
22
- exitCode: number;
23
- error?: Error;
24
- }
25
-
26
- export interface LoopState {
27
- iteration: number;
28
- startTime: Date;
29
- successCount: number;
30
- errorCount: number;
31
- }
32
-
33
- /**
34
- * Controller for loop execution mode
35
- */
36
- export class LoopController {
37
- private state: LoopState;
38
- private shouldStop: boolean = false;
39
-
40
- constructor() {
41
- this.state = {
42
- iteration: 0,
43
- startTime: new Date(),
44
- successCount: 0,
45
- errorCount: 0,
46
- };
47
-
48
- // Handle graceful shutdown
49
- this.setupSignalHandlers();
50
- }
51
-
52
- /**
53
- * Execute task in loop mode
54
- * Simple: Keep running same task until manual stop or max-runs
55
- */
56
- async run(
57
- executor: () => Promise<LoopResult>,
58
- options: LoopOptions
59
- ): Promise<void> {
60
- console.log(chalk.cyan.bold('\n━━━ 🔄 Loop Mode Activated\n'));
61
- console.log(chalk.dim(` Wait time: ${options.interval}s`));
62
- console.log(chalk.dim(` Max runs: ${options.maxRuns || '∞'}`));
63
- console.log(chalk.dim(` Stop: Ctrl+C or max-runs limit\n`));
64
-
65
- while (this.shouldContinue(options)) {
66
- this.state.iteration++;
67
-
68
- try {
69
- await this.executeIteration(executor, options);
70
- } catch (error) {
71
- this.handleError(error as Error);
72
- }
73
-
74
- // Wait for next iteration
75
- if (this.shouldContinue(options)) {
76
- await this.waitForNextRun(options);
77
- }
78
- }
79
-
80
- this.printSummary();
81
- }
82
-
83
- /**
84
- * Execute single iteration
85
- * Simple: Run task, track success/error, continue
86
- */
87
- private async executeIteration(
88
- executor: () => Promise<LoopResult>,
89
- options: LoopOptions
90
- ): Promise<void> {
91
- const maxDisplay = options.maxRuns || '∞';
92
- console.log(
93
- chalk.cyan(
94
- `\n🔄 Loop iteration ${this.state.iteration}/${maxDisplay}`
95
- )
96
- );
97
- console.log(chalk.dim(`Started: ${new Date().toLocaleTimeString()}\n`));
98
-
99
- const result = await executor();
100
-
101
- // Update state (just count success/error)
102
- if (result.error || result.exitCode !== 0) {
103
- this.state.errorCount++;
104
- console.log(chalk.yellow(`\n⚠️ Task encountered error (continuing...)`));
105
- } else {
106
- this.state.successCount++;
107
- }
108
- }
109
-
110
- /**
111
- * Handle execution error
112
- * Simple: Log and continue (resilient)
113
- */
114
- private handleError(error: Error): void {
115
- this.state.errorCount++;
116
- console.error(chalk.yellow('\n⚠️ Error occurred - continuing to next iteration'));
117
- console.error(chalk.dim(`Error: ${error.message}\n`));
118
- }
119
-
120
- /**
121
- * Wait for next iteration
122
- */
123
- private async waitForNextRun(options: LoopOptions): Promise<void> {
124
- const maxDisplay = options.maxRuns || '∞';
125
- const progress = `${this.state.iteration}/${maxDisplay}`;
126
-
127
- console.log(
128
- chalk.dim(
129
- `\n⏳ Waiting ${options.interval}s until next run... (completed: ${progress})`
130
- )
131
- );
132
-
133
- // Countdown display (optional, can be removed if too verbose)
134
- const startTime = Date.now();
135
- const endTime = startTime + options.interval * 1000;
136
-
137
- while (Date.now() < endTime && !this.shouldStop) {
138
- await this.sleep(1000);
139
-
140
- const remaining = Math.ceil((endTime - Date.now()) / 1000);
141
- if (remaining > 0 && remaining % 10 === 0) {
142
- process.stdout.write(chalk.dim(`\r⏳ ${remaining}s remaining...`));
143
- }
144
- }
145
-
146
- if (!this.shouldStop) {
147
- process.stdout.write('\r' + ' '.repeat(50) + '\r'); // Clear line
148
- }
149
- }
150
-
151
- /**
152
- * Check if should continue looping
153
- * Simple: Stop only on manual interrupt or max-runs
154
- */
155
- private shouldContinue(options: LoopOptions): boolean {
156
- if (this.shouldStop) return false;
157
- if (options.maxRuns && this.state.iteration >= options.maxRuns) return false;
158
- return true;
159
- }
160
-
161
- /**
162
- * Print execution summary
163
- */
164
- private printSummary(): void {
165
- const duration = Date.now() - this.state.startTime.getTime();
166
- const minutes = Math.floor(duration / 60000);
167
- const seconds = Math.floor((duration % 60000) / 1000);
168
-
169
- console.log(chalk.cyan.bold('\n━━━ 🏁 Loop Summary\n'));
170
- console.log(` Total iterations: ${this.state.iteration}`);
171
- console.log(
172
- ` Successful: ${chalk.green(this.state.successCount.toString())}`
173
- );
174
- console.log(` Errors: ${chalk.red(this.state.errorCount.toString())}`);
175
- console.log(` Duration: ${minutes}m ${seconds}s`);
176
- console.log();
177
- }
178
-
179
- /**
180
- * Setup signal handlers for graceful shutdown
181
- */
182
- private setupSignalHandlers(): void {
183
- const handler = () => {
184
- console.log(
185
- chalk.yellow('\n\n⚠️ Interrupt received - finishing current iteration...')
186
- );
187
- this.shouldStop = true;
188
- };
189
-
190
- process.on('SIGINT', handler);
191
- process.on('SIGTERM', handler);
192
- }
193
-
194
- /**
195
- * Sleep helper
196
- */
197
- private sleep(ms: number): Promise<void> {
198
- return new Promise((resolve) => setTimeout(resolve, ms));
199
- }
200
- }
@@ -1,147 +0,0 @@
1
- /**
2
- * Rule Loader
3
- * Loads rule definitions from markdown files with front matter
4
- */
5
-
6
- import { readFile, readdir, access } from 'node:fs/promises';
7
- import { join, parse, relative, dirname } from 'node:path';
8
- import { homedir } from 'node:os';
9
- import { fileURLToPath } from 'node:url';
10
- import matter from 'gray-matter';
11
- import type { Rule, RuleMetadata } from '../types/rule.types.js';
12
-
13
- /**
14
- * Load a single rule from a markdown file
15
- */
16
- export async function loadRuleFromFile(
17
- filePath: string,
18
- isBuiltin: boolean = false,
19
- ruleId?: string
20
- ): Promise<Rule | null> {
21
- try {
22
- const fileContent = await readFile(filePath, 'utf-8');
23
- const { data, content } = matter(fileContent);
24
-
25
- // Validate front matter
26
- if (!data.name || typeof data.name !== 'string') {
27
- console.error(`Rule file ${filePath} missing required 'name' field`);
28
- return null;
29
- }
30
-
31
- const metadata: RuleMetadata = {
32
- name: data.name,
33
- description: data.description || '',
34
- enabled: data.enabled !== undefined ? Boolean(data.enabled) : true,
35
- };
36
-
37
- // Get rule ID from parameter or filename
38
- const id = ruleId || parse(filePath).name;
39
-
40
- return {
41
- id,
42
- metadata,
43
- content: content.trim(),
44
- isBuiltin,
45
- filePath,
46
- };
47
- } catch (error) {
48
- console.error(`Failed to load rule from ${filePath}:`, error);
49
- return null;
50
- }
51
- }
52
-
53
- /**
54
- * Load all rules from a directory (recursively)
55
- */
56
- export async function loadRulesFromDirectory(dirPath: string, isBuiltin: boolean = false): Promise<Rule[]> {
57
- try {
58
- // Read directory recursively to support subdirectories
59
- const files = await readdir(dirPath, { recursive: true, withFileTypes: true });
60
-
61
- // Filter for .md files and calculate rule IDs from relative paths
62
- const ruleFiles = files
63
- .filter((f) => f.isFile() && f.name.endsWith('.md'))
64
- .map((f) => {
65
- const fullPath = join(f.parentPath || f.path, f.name);
66
- // Calculate relative path from dirPath and remove .md extension
67
- const relativePath = relative(dirPath, fullPath).replace(/\.md$/, '');
68
- return { fullPath, ruleId: relativePath };
69
- });
70
-
71
- const rules = await Promise.all(
72
- ruleFiles.map(({ fullPath, ruleId }) => loadRuleFromFile(fullPath, isBuiltin, ruleId))
73
- );
74
-
75
- return rules.filter((rule): rule is Rule => rule !== null);
76
- } catch (error) {
77
- // Directory doesn't exist or can't be read
78
- return [];
79
- }
80
- }
81
-
82
- /**
83
- * Get system rules path (bundled with the app)
84
- */
85
- export async function getSystemRulesPath(): Promise<string> {
86
- // Get the directory of the current module (cross-platform)
87
- const currentFile = fileURLToPath(import.meta.url);
88
- const currentDir = dirname(currentFile);
89
-
90
- // In production (dist), assets are at dist/assets/rules
91
- // In development (src), go up to project root: src/core -> project root
92
- const distPath = join(currentDir, '..', 'assets', 'rules');
93
- const devPath = join(currentDir, '..', '..', 'assets', 'rules');
94
-
95
- // Check which one exists (try dist first, then dev)
96
- try {
97
- await access(distPath);
98
- return distPath;
99
- } catch {
100
- return devPath;
101
- }
102
- }
103
-
104
- /**
105
- * Get all rule search paths
106
- */
107
- export function getRuleSearchPaths(cwd: string): string[] {
108
- const globalPath = join(homedir(), '.sylphx-flow', 'rules');
109
- const projectPath = join(cwd, '.sylphx-flow', 'rules');
110
-
111
- return [globalPath, projectPath];
112
- }
113
-
114
- /**
115
- * Load all available rules from all sources
116
- */
117
- export async function loadAllRules(cwd: string): Promise<Rule[]> {
118
- const systemPath = await getSystemRulesPath();
119
- const [globalPath, projectPath] = getRuleSearchPaths(cwd);
120
-
121
- const [systemRules, globalRules, projectRules] = await Promise.all([
122
- loadRulesFromDirectory(systemPath, true), // System rules are marked as builtin
123
- loadRulesFromDirectory(globalPath, false),
124
- loadRulesFromDirectory(projectPath, false),
125
- ]);
126
-
127
- // Priority: system < global < project
128
- // Use Map to deduplicate by ID (later entries override earlier ones)
129
- const ruleMap = new Map<string, Rule>();
130
-
131
- // Add system rules first (lowest priority)
132
- for (const rule of systemRules) {
133
- ruleMap.set(rule.id, rule);
134
- }
135
-
136
- // Add global rules (override system)
137
- for (const rule of globalRules) {
138
- ruleMap.set(rule.id, rule);
139
- }
140
-
141
- // Add project rules (override globals and system)
142
- for (const rule of projectRules) {
143
- ruleMap.set(rule.id, rule);
144
- }
145
-
146
- return Array.from(ruleMap.values());
147
- }
@@ -1,240 +0,0 @@
1
- /**
2
- * Rule Manager
3
- * Manages rule state and operations
4
- */
5
-
6
- import type { Rule } from '../types/rule.types.js';
7
- import { loadAllRules } from './rule-loader.js';
8
-
9
- /**
10
- * Rule manager state
11
- */
12
- interface RuleManagerState {
13
- rules: Map<string, Rule>;
14
- cwd: string;
15
- }
16
-
17
- let state: RuleManagerState | null = null;
18
-
19
- /**
20
- * Get the app store (lazy import to avoid circular dependencies)
21
- */
22
- let getAppStore: (() => any) | null = null;
23
-
24
- /**
25
- * Set the app store getter (called during initialization)
26
- */
27
- export function setRuleAppStoreGetter(getter: () => any): void {
28
- getAppStore = getter;
29
- }
30
-
31
- /**
32
- * Initialize rule manager
33
- */
34
- export async function initializeRuleManager(cwd: string): Promise<void> {
35
- const allRules = await loadAllRules(cwd);
36
-
37
- const ruleMap = new Map<string, Rule>();
38
- for (const rule of allRules) {
39
- ruleMap.set(rule.id, rule);
40
- }
41
-
42
- state = {
43
- rules: ruleMap,
44
- cwd,
45
- };
46
-
47
- // Initialize store with default enabled rules
48
- if (getAppStore) {
49
- const store = getAppStore();
50
- if (store.getState) {
51
- const currentEnabledRules = store.getState().enabledRuleIds || [];
52
-
53
- // If no rules are enabled yet, enable all rules that have enabled: true in metadata
54
- if (currentEnabledRules.length === 0) {
55
- const defaultEnabledRules = allRules
56
- .filter((rule) => rule.metadata.enabled !== false)
57
- .map((rule) => rule.id);
58
-
59
- if (defaultEnabledRules.length > 0) {
60
- store.getState().setEnabledRuleIds(defaultEnabledRules);
61
- }
62
- }
63
- }
64
- }
65
- }
66
-
67
- /**
68
- * Get all available rules
69
- */
70
- export function getAllRules(): Rule[] {
71
- if (!state) {
72
- return [];
73
- }
74
- return Array.from(state.rules.values());
75
- }
76
-
77
- /**
78
- * Get rule by ID
79
- */
80
- export function getRuleById(id: string): Rule | null {
81
- if (!state) {
82
- return null;
83
- }
84
- return state.rules.get(id) || null;
85
- }
86
-
87
- /**
88
- * Get enabled rule IDs from store
89
- */
90
- export function getEnabledRuleIds(): string[] {
91
- if (getAppStore) {
92
- const store = getAppStore();
93
- if (store.getState) {
94
- return store.getState().enabledRuleIds || [];
95
- }
96
- }
97
- return [];
98
- }
99
-
100
- /**
101
- * Get enabled rules
102
- */
103
- export function getEnabledRules(): Rule[] {
104
- if (!state) {
105
- return [];
106
- }
107
-
108
- const enabledIds = getEnabledRuleIds();
109
- return enabledIds
110
- .map((id) => state!.rules.get(id))
111
- .filter((rule): rule is Rule => rule !== null);
112
- }
113
-
114
- /**
115
- * Toggle a rule on/off
116
- */
117
- export function toggleRule(ruleId: string): boolean {
118
- if (!state || !state.rules.has(ruleId)) {
119
- return false;
120
- }
121
-
122
- if (getAppStore) {
123
- const store = getAppStore();
124
- if (store.getState) {
125
- const currentEnabled = store.getState().enabledRuleIds || [];
126
-
127
- if (currentEnabled.includes(ruleId)) {
128
- // Disable: remove from list
129
- store.getState().setEnabledRuleIds(currentEnabled.filter((id) => id !== ruleId));
130
- } else {
131
- // Enable: add to list
132
- store.getState().setEnabledRuleIds([...currentEnabled, ruleId]);
133
- }
134
- return true;
135
- }
136
- }
137
-
138
- return false;
139
- }
140
-
141
- /**
142
- * Enable a rule
143
- */
144
- export function enableRule(ruleId: string): boolean {
145
- if (!state || !state.rules.has(ruleId)) {
146
- return false;
147
- }
148
-
149
- if (getAppStore) {
150
- const store = getAppStore();
151
- if (store.getState) {
152
- const currentEnabled = store.getState().enabledRuleIds || [];
153
-
154
- if (!currentEnabled.includes(ruleId)) {
155
- store.getState().setEnabledRuleIds([...currentEnabled, ruleId]);
156
- }
157
- return true;
158
- }
159
- }
160
-
161
- return false;
162
- }
163
-
164
- /**
165
- * Disable a rule
166
- */
167
- export function disableRule(ruleId: string): boolean {
168
- if (!state || !state.rules.has(ruleId)) {
169
- return false;
170
- }
171
-
172
- if (getAppStore) {
173
- const store = getAppStore();
174
- if (store.getState) {
175
- const currentEnabled = store.getState().enabledRuleIds || [];
176
- store.getState().setEnabledRuleIds(currentEnabled.filter((id) => id !== ruleId));
177
- return true;
178
- }
179
- }
180
-
181
- return false;
182
- }
183
-
184
- /**
185
- * Reload rules from disk
186
- */
187
- export async function reloadRules(): Promise<void> {
188
- if (!state) {
189
- return;
190
- }
191
-
192
- const cwd = state.cwd;
193
- const currentEnabled = getEnabledRuleIds();
194
-
195
- await initializeRuleManager(cwd);
196
-
197
- // Keep only enabled rules that still exist
198
- if (state && getAppStore) {
199
- const store = getAppStore();
200
- if (store.getState) {
201
- const validEnabled = currentEnabled.filter((id) => state!.rules.has(id));
202
- store.getState().setEnabledRuleIds(validEnabled);
203
- }
204
- }
205
- }
206
-
207
- /**
208
- * Set enabled rules (replaces current enabled rules)
209
- */
210
- export function setEnabledRules(ruleIds: string[]): boolean {
211
- if (!state) {
212
- return false;
213
- }
214
-
215
- // Validate all rule IDs exist
216
- const validRuleIds = ruleIds.filter((id) => state!.rules.has(id));
217
-
218
- if (getAppStore) {
219
- const store = getAppStore();
220
- if (store.getState) {
221
- store.getState().setEnabledRuleIds(validRuleIds);
222
- return true;
223
- }
224
- }
225
-
226
- return false;
227
- }
228
-
229
- /**
230
- * Get combined content of all enabled rules
231
- */
232
- export function getEnabledRulesContent(): string {
233
- const enabledRules = getEnabledRules();
234
-
235
- if (enabledRules.length === 0) {
236
- return '';
237
- }
238
-
239
- return enabledRules.map((rule) => rule.content).join('\n\n');
240
- }