@sylphx/flow 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/assets/slash-commands/audit.md +21 -6
- package/assets/slash-commands/e2e-audit.md +123 -0
- package/package.json +1 -1
- package/src/commands/flow/types.ts +0 -4
- package/src/core/functional/async.ts +0 -213
- package/src/targets/claude-code.ts +41 -12
- package/src/utils/config/settings.ts +8 -4
- package/src/utils/config/target-utils.ts +16 -12
- package/src/utils/files/jsonc.ts +3 -3
- package/src/utils/prompt-helpers.ts +7 -3
- package/src/utils/security/security.ts +0 -41
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @sylphx/flow
|
|
2
2
|
|
|
3
|
+
## 3.4.0 (2026-01-26)
|
|
4
|
+
|
|
5
|
+
### ✨ Features
|
|
6
|
+
|
|
7
|
+
- add /e2e-audit and business logic to /audit ([8e87e10](https://github.com/SylphxAI/flow/commit/8e87e10b49eaec37bb85c2677a0d8b1f602c5584))
|
|
8
|
+
|
|
9
|
+
### ♻️ Refactoring
|
|
10
|
+
|
|
11
|
+
- replace any with proper types in claude-code.ts ([d498d41](https://github.com/SylphxAI/flow/commit/d498d41dcae95cc9d98628f7860be2f48b7c342e))
|
|
12
|
+
- replace any with proper types in target-utils.ts ([aebbf5d](https://github.com/SylphxAI/flow/commit/aebbf5dad8b4705664db1784127a4af0da8f8e5c))
|
|
13
|
+
- remove unused securityMiddleware from security.ts ([a8efc37](https://github.com/SylphxAI/flow/commit/a8efc37fd06db866b5a08715a5141c19fbb3f54e))
|
|
14
|
+
- add generics to jsonc.ts functions ([aac10af](https://github.com/SylphxAI/flow/commit/aac10afa3aa8cbd5c0319932c09d34e7a7ca4fec))
|
|
15
|
+
- replace any with unknown in settings.ts ([8b76e17](https://github.com/SylphxAI/flow/commit/8b76e179ffc6f4533dbc5b58894cb1c0b468f83a))
|
|
16
|
+
- replace any with unknown in prompt-helpers ([56e2b37](https://github.com/SylphxAI/flow/commit/56e2b375992232395ea58682ef7b04d396321b25))
|
|
17
|
+
- remove unused FlowOptions properties ([273f521](https://github.com/SylphxAI/flow/commit/273f521a37e6b63f64b3fe6667f7489681bcb9fa))
|
|
18
|
+
- remove unused async utility functions ([3b2c8fd](https://github.com/SylphxAI/flow/commit/3b2c8fdb6ea997e195a15e19bdd0ab931fae0f15))
|
|
19
|
+
|
|
3
20
|
## 3.3.0 (2026-01-26)
|
|
4
21
|
|
|
5
22
|
### ✨ Features
|
|
@@ -23,7 +23,22 @@ Scan the entire project for issues. Find problems, don't fix them. Open GitHub i
|
|
|
23
23
|
- Inconsistent naming conventions
|
|
24
24
|
- Outdated dependencies with known issues
|
|
25
25
|
|
|
26
|
-
### 2.
|
|
26
|
+
### 2. Business Logic Correctness
|
|
27
|
+
|
|
28
|
+
**Code-correct ≠ Business-correct. Review business rules in the code.**
|
|
29
|
+
|
|
30
|
+
- Region/locale logic: Does HK user see HK-specific data? (not China's 五險三金)
|
|
31
|
+
- Currency handling: Correct currency for user's region?
|
|
32
|
+
- Date/time: Timezone handling, week start day, date formats
|
|
33
|
+
- Tax/legal: Region-specific rules applied correctly?
|
|
34
|
+
- Permissions: Do access rules make business sense?
|
|
35
|
+
- Calculations: Business formulas correct? (not just mathematically)
|
|
36
|
+
- State machines: Valid business state transitions only?
|
|
37
|
+
- Validation rules: Match real-world business constraints?
|
|
38
|
+
- Default values: Sensible for the business context?
|
|
39
|
+
- Edge cases: Business-impossible states prevented?
|
|
40
|
+
|
|
41
|
+
### 3. Architecture
|
|
27
42
|
- Circular dependencies
|
|
28
43
|
- God objects/files doing too much
|
|
29
44
|
- Tight coupling between modules
|
|
@@ -33,7 +48,7 @@ Scan the entire project for issues. Find problems, don't fix them. Open GitHub i
|
|
|
33
48
|
- Missing SSOT (multiple sources of truth)
|
|
34
49
|
- Inconsistent patterns across codebase
|
|
35
50
|
|
|
36
|
-
###
|
|
51
|
+
### 4. UI/UX Issues
|
|
37
52
|
- Confusing user flows
|
|
38
53
|
- Missing loading states
|
|
39
54
|
- Missing error states
|
|
@@ -45,7 +60,7 @@ Scan the entire project for issues. Find problems, don't fix them. Open GitHub i
|
|
|
45
60
|
- Unclear CTAs or labels
|
|
46
61
|
- Information overload
|
|
47
62
|
|
|
48
|
-
###
|
|
63
|
+
### 5. Product Design
|
|
49
64
|
- Unclear value proposition
|
|
50
65
|
- Friction in core user journey
|
|
51
66
|
- Missing onboarding guidance
|
|
@@ -55,7 +70,7 @@ Scan the entire project for issues. Find problems, don't fix them. Open GitHub i
|
|
|
55
70
|
- Power user needs unmet
|
|
56
71
|
- Beginner barriers too high
|
|
57
72
|
|
|
58
|
-
###
|
|
73
|
+
### 6. Performance
|
|
59
74
|
- Slow page loads
|
|
60
75
|
- Unnecessary re-renders
|
|
61
76
|
- Large bundle sizes
|
|
@@ -64,7 +79,7 @@ Scan the entire project for issues. Find problems, don't fix them. Open GitHub i
|
|
|
64
79
|
- Missing caching opportunities
|
|
65
80
|
- Unoptimized images/assets
|
|
66
81
|
|
|
67
|
-
###
|
|
82
|
+
### 7. Security
|
|
68
83
|
- Exposed secrets or credentials
|
|
69
84
|
- Missing input validation
|
|
70
85
|
- XSS vulnerabilities
|
|
@@ -73,7 +88,7 @@ Scan the entire project for issues. Find problems, don't fix them. Open GitHub i
|
|
|
73
88
|
- Missing rate limiting
|
|
74
89
|
- Overly permissive CORS
|
|
75
90
|
|
|
76
|
-
###
|
|
91
|
+
### 8. Developer Experience
|
|
77
92
|
- Missing or outdated documentation
|
|
78
93
|
- Unclear setup instructions
|
|
79
94
|
- Flaky or missing tests
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: e2e-audit
|
|
3
|
+
description: Browser-based audit for business logic and product correctness
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# E2E Audit: Browser-Based Product Validation
|
|
7
|
+
|
|
8
|
+
Open a browser, walk through the product, find business logic and UX issues.
|
|
9
|
+
|
|
10
|
+
**This is NOT about code correctness — code can be "correct" but business logic can be WRONG.**
|
|
11
|
+
|
|
12
|
+
## Why E2E Audit?
|
|
13
|
+
|
|
14
|
+
Static code analysis can't catch:
|
|
15
|
+
- User selects "Hong Kong" but sees China's "五險三金"
|
|
16
|
+
- Calendar showing Monday as week start for US users
|
|
17
|
+
- Currency conversion using stale rates
|
|
18
|
+
- Tax calculations wrong for specific regions
|
|
19
|
+
- Permissions that make no business sense
|
|
20
|
+
- Data inconsistency across related features
|
|
21
|
+
|
|
22
|
+
## Process
|
|
23
|
+
|
|
24
|
+
### 1. Launch Browser
|
|
25
|
+
```
|
|
26
|
+
mcp__playwright__browser_navigate - Go to product URL
|
|
27
|
+
mcp__playwright__browser_snapshot - Get current page state
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 2. Walk Through User Journeys
|
|
31
|
+
|
|
32
|
+
**Authentication:**
|
|
33
|
+
- [ ] Signup flow (all fields, validations, error messages)
|
|
34
|
+
- [ ] Login flow (remember me, forgot password)
|
|
35
|
+
- [ ] Logout and session handling
|
|
36
|
+
|
|
37
|
+
**Core Features:**
|
|
38
|
+
- [ ] Primary user workflow end-to-end
|
|
39
|
+
- [ ] Secondary features
|
|
40
|
+
- [ ] Settings and configuration
|
|
41
|
+
- [ ] Profile management
|
|
42
|
+
|
|
43
|
+
**Edge Cases:**
|
|
44
|
+
- [ ] Empty states
|
|
45
|
+
- [ ] Error states
|
|
46
|
+
- [ ] Boundary values (min/max)
|
|
47
|
+
- [ ] Invalid inputs
|
|
48
|
+
|
|
49
|
+
### 3. Test Business Logic
|
|
50
|
+
|
|
51
|
+
**Region/Locale Consistency:**
|
|
52
|
+
- Change user region → verify ALL related data updates
|
|
53
|
+
- Currency, date format, language, legal requirements
|
|
54
|
+
- Region-specific features show/hide correctly
|
|
55
|
+
|
|
56
|
+
**Data Consistency:**
|
|
57
|
+
- Related fields stay in sync
|
|
58
|
+
- Calculations produce business-correct results
|
|
59
|
+
- Aggregates match detail records
|
|
60
|
+
|
|
61
|
+
**Business Rules:**
|
|
62
|
+
- Permissions enforced correctly
|
|
63
|
+
- Workflow states valid
|
|
64
|
+
- Business constraints respected
|
|
65
|
+
|
|
66
|
+
### 4. Document Issues
|
|
67
|
+
|
|
68
|
+
For each issue found:
|
|
69
|
+
```bash
|
|
70
|
+
mcp__playwright__browser_take_screenshot # Capture evidence
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Issue Categories
|
|
74
|
+
|
|
75
|
+
### Business Logic Errors
|
|
76
|
+
- Data doesn't match business expectations
|
|
77
|
+
- Rules applied incorrectly for context
|
|
78
|
+
- Cross-feature data inconsistency
|
|
79
|
+
|
|
80
|
+
### UX Issues
|
|
81
|
+
- Confusing user flows
|
|
82
|
+
- Missing feedback
|
|
83
|
+
- Unclear error messages
|
|
84
|
+
- Accessibility problems
|
|
85
|
+
|
|
86
|
+
### Visual Issues
|
|
87
|
+
- Layout broken
|
|
88
|
+
- Responsive issues
|
|
89
|
+
- Inconsistent styling
|
|
90
|
+
|
|
91
|
+
## Browser Tools Reference
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
mcp__playwright__browser_navigate - Go to URL
|
|
95
|
+
mcp__playwright__browser_snapshot - Get page state (use this frequently)
|
|
96
|
+
mcp__playwright__browser_click - Click elements
|
|
97
|
+
mcp__playwright__browser_fill_form - Fill forms
|
|
98
|
+
mcp__playwright__browser_type - Type text
|
|
99
|
+
mcp__playwright__browser_select_option - Select dropdown
|
|
100
|
+
mcp__playwright__browser_take_screenshot - Capture evidence
|
|
101
|
+
mcp__playwright__browser_press_key - Keyboard input
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Output
|
|
105
|
+
|
|
106
|
+
### Issues Found
|
|
107
|
+
| # | Type | Description | Severity | Screenshot |
|
|
108
|
+
|---|------|-------------|----------|------------|
|
|
109
|
+
| 1 | Business Logic | HK user sees CN insurance | High | screenshot_1.png |
|
|
110
|
+
| 2 | UX | No loading state on submit | Medium | screenshot_2.png |
|
|
111
|
+
|
|
112
|
+
### Open GitHub Issues
|
|
113
|
+
```bash
|
|
114
|
+
gh issue create --title "[E2E] Brief description" --body "..." --label "e2e-audit"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Mindset
|
|
118
|
+
|
|
119
|
+
* **Code-correct ≠ Business-correct** — always verify business logic
|
|
120
|
+
* Think like a real user, not a developer
|
|
121
|
+
* Test the unhappy paths
|
|
122
|
+
* Question every assumption
|
|
123
|
+
* If something feels wrong, it probably is
|
package/package.json
CHANGED
|
@@ -7,8 +7,6 @@ export interface FlowOptions {
|
|
|
7
7
|
verbose?: boolean;
|
|
8
8
|
dryRun?: boolean;
|
|
9
9
|
sync?: boolean;
|
|
10
|
-
initOnly?: boolean;
|
|
11
|
-
runOnly?: boolean;
|
|
12
10
|
repair?: boolean;
|
|
13
11
|
upgrade?: boolean;
|
|
14
12
|
upgradeTarget?: boolean;
|
|
@@ -23,7 +21,6 @@ export interface FlowOptions {
|
|
|
23
21
|
|
|
24
22
|
// Smart configuration options
|
|
25
23
|
selectProvider?: boolean;
|
|
26
|
-
selectAgent?: boolean;
|
|
27
24
|
provider?: string;
|
|
28
25
|
|
|
29
26
|
// Execution modes
|
|
@@ -35,5 +32,4 @@ export interface FlowOptions {
|
|
|
35
32
|
|
|
36
33
|
// Loop mode
|
|
37
34
|
loop?: number;
|
|
38
|
-
maxRuns?: number;
|
|
39
35
|
}
|
|
@@ -35,33 +35,6 @@ export const fromPromise = async <T>(
|
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
* Map over async result
|
|
40
|
-
*/
|
|
41
|
-
export const mapAsync =
|
|
42
|
-
<T, U>(fn: (value: T) => U | Promise<U>) =>
|
|
43
|
-
async <E>(result: AsyncResult<T, E>): AsyncResult<U, E> => {
|
|
44
|
-
const resolved = await result;
|
|
45
|
-
if (isSuccess(resolved)) {
|
|
46
|
-
const mapped = await fn(resolved.value);
|
|
47
|
-
return success(mapped);
|
|
48
|
-
}
|
|
49
|
-
return resolved;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* FlatMap over async result
|
|
54
|
-
*/
|
|
55
|
-
export const flatMapAsync =
|
|
56
|
-
<T, U, E>(fn: (value: T) => AsyncResult<U, E>) =>
|
|
57
|
-
async (result: AsyncResult<T, E>): AsyncResult<U, E> => {
|
|
58
|
-
const resolved = await result;
|
|
59
|
-
if (isSuccess(resolved)) {
|
|
60
|
-
return fn(resolved.value);
|
|
61
|
-
}
|
|
62
|
-
return resolved;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
38
|
/**
|
|
66
39
|
* Run async operation with timeout
|
|
67
40
|
*/
|
|
@@ -120,195 +93,9 @@ export const retry = async <T>(
|
|
|
120
93
|
return failure(lastError);
|
|
121
94
|
};
|
|
122
95
|
|
|
123
|
-
/**
|
|
124
|
-
* Run promises in parallel and collect results
|
|
125
|
-
* Fails if any promise fails
|
|
126
|
-
*/
|
|
127
|
-
export const allAsync = async <T>(promises: AsyncResult<T, AppError>[]): AsyncResult<T[]> => {
|
|
128
|
-
const results = await Promise.all(promises);
|
|
129
|
-
|
|
130
|
-
const values: T[] = [];
|
|
131
|
-
for (const result of results) {
|
|
132
|
-
if (isSuccess(result)) {
|
|
133
|
-
values.push(result.value);
|
|
134
|
-
} else {
|
|
135
|
-
return result;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return success(values);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Run promises in parallel and collect all results
|
|
144
|
-
* Returns both successes and failures
|
|
145
|
-
*/
|
|
146
|
-
export const allSettledAsync = async <T>(
|
|
147
|
-
promises: AsyncResult<T, AppError>[]
|
|
148
|
-
): Promise<Result<T, AppError>[]> => {
|
|
149
|
-
return Promise.all(promises);
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Run promises sequentially
|
|
154
|
-
*/
|
|
155
|
-
export const sequenceAsync = async <T>(
|
|
156
|
-
promises: Array<() => AsyncResult<T, AppError>>
|
|
157
|
-
): AsyncResult<T[]> => {
|
|
158
|
-
const values: T[] = [];
|
|
159
|
-
|
|
160
|
-
for (const promiseFn of promises) {
|
|
161
|
-
const result = await promiseFn();
|
|
162
|
-
if (isSuccess(result)) {
|
|
163
|
-
values.push(result.value);
|
|
164
|
-
} else {
|
|
165
|
-
return result;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return success(values);
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Race promises - return first to complete
|
|
174
|
-
*/
|
|
175
|
-
export const raceAsync = async <T>(promises: AsyncResult<T, AppError>[]): AsyncResult<T> => {
|
|
176
|
-
return Promise.race(promises);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
96
|
/**
|
|
180
97
|
* Delay execution
|
|
181
98
|
*/
|
|
182
99
|
export const delay = (ms: number): Promise<void> => {
|
|
183
100
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
184
101
|
};
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Run with concurrency limit
|
|
188
|
-
*/
|
|
189
|
-
export const withConcurrency = async <T>(
|
|
190
|
-
tasks: Array<() => AsyncResult<T, AppError>>,
|
|
191
|
-
concurrency: number
|
|
192
|
-
): AsyncResult<T[]> => {
|
|
193
|
-
const results: T[] = [];
|
|
194
|
-
const executing: Promise<void>[] = [];
|
|
195
|
-
|
|
196
|
-
for (const task of tasks) {
|
|
197
|
-
const promise = (async () => {
|
|
198
|
-
const result = await task();
|
|
199
|
-
if (isSuccess(result)) {
|
|
200
|
-
results.push(result.value);
|
|
201
|
-
} else {
|
|
202
|
-
throw result.error;
|
|
203
|
-
}
|
|
204
|
-
})();
|
|
205
|
-
|
|
206
|
-
executing.push(promise);
|
|
207
|
-
|
|
208
|
-
if (executing.length >= concurrency) {
|
|
209
|
-
await Promise.race(executing);
|
|
210
|
-
executing.splice(executing.indexOf(promise), 1);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
await Promise.all(executing);
|
|
216
|
-
return success(results);
|
|
217
|
-
} catch (error) {
|
|
218
|
-
return failure(toAppError(error));
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Memoize async function
|
|
224
|
-
*/
|
|
225
|
-
export const memoizeAsync = <Args extends any[], T>(
|
|
226
|
-
fn: (...args: Args) => AsyncResult<T, AppError>,
|
|
227
|
-
keyFn?: (...args: Args) => string
|
|
228
|
-
): ((...args: Args) => AsyncResult<T, AppError>) => {
|
|
229
|
-
const cache = new Map<string, AsyncResult<T, AppError>>();
|
|
230
|
-
|
|
231
|
-
return (...args: Args): AsyncResult<T, AppError> => {
|
|
232
|
-
const key = keyFn ? keyFn(...args) : JSON.stringify(args);
|
|
233
|
-
|
|
234
|
-
const cached = cache.get(key);
|
|
235
|
-
if (cached !== undefined) {
|
|
236
|
-
return cached;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const result = fn(...args);
|
|
240
|
-
cache.set(key, result);
|
|
241
|
-
return result;
|
|
242
|
-
};
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Debounce async function
|
|
247
|
-
*/
|
|
248
|
-
export const debounceAsync = <Args extends any[], T>(
|
|
249
|
-
fn: (...args: Args) => AsyncResult<T, AppError>,
|
|
250
|
-
delayMs: number
|
|
251
|
-
): ((...args: Args) => AsyncResult<T, AppError>) => {
|
|
252
|
-
let timeoutId: NodeJS.Timeout | null = null;
|
|
253
|
-
let latestArgs: Args | null = null;
|
|
254
|
-
let latestPromise: AsyncResult<T, AppError> | null = null;
|
|
255
|
-
|
|
256
|
-
return (...args: Args): AsyncResult<T, AppError> => {
|
|
257
|
-
latestArgs = args;
|
|
258
|
-
|
|
259
|
-
if (timeoutId) {
|
|
260
|
-
clearTimeout(timeoutId);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (!latestPromise) {
|
|
264
|
-
latestPromise = new Promise((resolve) => {
|
|
265
|
-
timeoutId = setTimeout(async () => {
|
|
266
|
-
if (latestArgs) {
|
|
267
|
-
const result = await fn(...latestArgs);
|
|
268
|
-
resolve(result);
|
|
269
|
-
latestPromise = null;
|
|
270
|
-
timeoutId = null;
|
|
271
|
-
}
|
|
272
|
-
}, delayMs);
|
|
273
|
-
}) as AsyncResult<T, AppError>;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return latestPromise;
|
|
277
|
-
};
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Throttle async function
|
|
282
|
-
*/
|
|
283
|
-
export const throttleAsync = <Args extends any[], T>(
|
|
284
|
-
fn: (...args: Args) => AsyncResult<T, AppError>,
|
|
285
|
-
limitMs: number
|
|
286
|
-
): ((...args: Args) => AsyncResult<T, AppError>) => {
|
|
287
|
-
let lastRun = 0;
|
|
288
|
-
let pending: AsyncResult<T, AppError> | null = null;
|
|
289
|
-
|
|
290
|
-
return (...args: Args): AsyncResult<T, AppError> => {
|
|
291
|
-
const now = Date.now();
|
|
292
|
-
|
|
293
|
-
if (now - lastRun >= limitMs) {
|
|
294
|
-
lastRun = now;
|
|
295
|
-
return fn(...args);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (!pending) {
|
|
299
|
-
pending = new Promise((resolve) => {
|
|
300
|
-
setTimeout(
|
|
301
|
-
async () => {
|
|
302
|
-
lastRun = Date.now();
|
|
303
|
-
const result = await fn(...args);
|
|
304
|
-
pending = null;
|
|
305
|
-
resolve(result);
|
|
306
|
-
},
|
|
307
|
-
limitMs - (now - lastRun)
|
|
308
|
-
);
|
|
309
|
-
}) as AsyncResult<T, AppError>;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return pending;
|
|
313
|
-
};
|
|
314
|
-
};
|
|
@@ -4,10 +4,16 @@ import path from 'node:path';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { installToDirectory } from '../core/installers/file-installer.js';
|
|
6
6
|
import { createMCPInstaller } from '../core/installers/mcp-installer.js';
|
|
7
|
-
import type { AgentMetadata } from '../types/target-config.types.js';
|
|
7
|
+
import type { AgentMetadata, FrontMatterMetadata } from '../types/target-config.types.js';
|
|
8
8
|
import type { CommonOptions, MCPServerConfigUnion, SetupResult, Target } from '../types.js';
|
|
9
9
|
import { getAgentsDir } from '../utils/config/paths.js';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type ConfigData,
|
|
12
|
+
fileUtils,
|
|
13
|
+
generateHelpText,
|
|
14
|
+
pathUtils,
|
|
15
|
+
yamlUtils,
|
|
16
|
+
} from '../utils/config/target-utils.js';
|
|
11
17
|
import { CLIError } from '../utils/error-handler.js';
|
|
12
18
|
import { sanitize } from '../utils/security/security.js';
|
|
13
19
|
import {
|
|
@@ -17,6 +23,23 @@ import {
|
|
|
17
23
|
transformMCPConfig as transformMCP,
|
|
18
24
|
} from './shared/index.js';
|
|
19
25
|
|
|
26
|
+
/** Claude Code configuration data with MCP servers */
|
|
27
|
+
interface ClaudeCodeConfigData extends ConfigData {
|
|
28
|
+
mcpServers?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Claude Code agent metadata */
|
|
32
|
+
interface ClaudeCodeAgentMetadata {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
model?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Error with exit code from child process */
|
|
39
|
+
interface ProcessExitError extends Error {
|
|
40
|
+
code: number | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
/**
|
|
21
44
|
* Claude Code target - composition approach with all original functionality
|
|
22
45
|
*/
|
|
@@ -86,8 +109,11 @@ export const claudeCodeTarget: Target = {
|
|
|
86
109
|
/**
|
|
87
110
|
* Read Claude Code configuration with structure normalization
|
|
88
111
|
*/
|
|
89
|
-
async readConfig(cwd: string): Promise<
|
|
90
|
-
const config = await fileUtils.readConfig(
|
|
112
|
+
async readConfig(cwd: string): Promise<ClaudeCodeConfigData> {
|
|
113
|
+
const config = (await fileUtils.readConfig(
|
|
114
|
+
claudeCodeTarget.config,
|
|
115
|
+
cwd
|
|
116
|
+
)) as ClaudeCodeConfigData;
|
|
91
117
|
|
|
92
118
|
// Ensure the config has the expected structure for Claude Code
|
|
93
119
|
if (!config.mcpServers) {
|
|
@@ -258,7 +284,7 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
258
284
|
if (code === 0) {
|
|
259
285
|
resolve();
|
|
260
286
|
} else {
|
|
261
|
-
const error = new Error(`Claude Code exited with code ${code}`) as
|
|
287
|
+
const error = new Error(`Claude Code exited with code ${code}`) as ProcessExitError;
|
|
262
288
|
error.code = code;
|
|
263
289
|
reject(error);
|
|
264
290
|
}
|
|
@@ -494,26 +520,29 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
494
520
|
* Convert OpenCode frontmatter to Claude Code format
|
|
495
521
|
*/
|
|
496
522
|
function convertToClaudeCodeFormat(
|
|
497
|
-
openCodeMetadata:
|
|
523
|
+
openCodeMetadata: FrontMatterMetadata,
|
|
498
524
|
content: string,
|
|
499
525
|
sourcePath?: string
|
|
500
|
-
):
|
|
526
|
+
): ClaudeCodeAgentMetadata {
|
|
501
527
|
// Use explicit name from metadata if available, otherwise extract from content or path
|
|
528
|
+
const metadataName = openCodeMetadata.name as string | undefined;
|
|
502
529
|
const agentName =
|
|
503
|
-
|
|
530
|
+
metadataName || pathUtils.extractAgentName(content, openCodeMetadata, sourcePath);
|
|
504
531
|
|
|
505
532
|
// Extract description from metadata or content
|
|
506
|
-
const
|
|
533
|
+
const metadataDescription = openCodeMetadata.description as string | undefined;
|
|
534
|
+
const description = metadataDescription || pathUtils.extractDescription(content);
|
|
507
535
|
|
|
508
536
|
// Only keep supported fields for Claude Code
|
|
509
|
-
const result:
|
|
537
|
+
const result: ClaudeCodeAgentMetadata = {
|
|
510
538
|
name: agentName,
|
|
511
539
|
description: description,
|
|
512
540
|
};
|
|
513
541
|
|
|
514
542
|
// Only add model if it exists and is not 'inherit' (default)
|
|
515
|
-
|
|
516
|
-
|
|
543
|
+
const model = openCodeMetadata.model as string | undefined;
|
|
544
|
+
if (model && model !== 'inherit') {
|
|
545
|
+
result.model = model;
|
|
517
546
|
}
|
|
518
547
|
|
|
519
548
|
// Remove unsupported fields that might cause issues
|
|
@@ -49,12 +49,13 @@ export const loadSettings = async (
|
|
|
49
49
|
const content = await fs.readFile(settingsPath, 'utf8');
|
|
50
50
|
return JSON.parse(content) as ProjectSettings;
|
|
51
51
|
},
|
|
52
|
-
(error:
|
|
52
|
+
(error: unknown) => {
|
|
53
53
|
// File not found is not an error - return empty settings
|
|
54
|
-
|
|
54
|
+
const nodeError = error as { code?: string; message?: string };
|
|
55
|
+
if (nodeError.code === 'ENOENT') {
|
|
55
56
|
return new Error('EMPTY_SETTINGS');
|
|
56
57
|
}
|
|
57
|
-
return new Error(`Failed to load settings: ${
|
|
58
|
+
return new Error(`Failed to load settings: ${nodeError.message ?? 'Unknown error'}`);
|
|
58
59
|
}
|
|
59
60
|
).then((result) => {
|
|
60
61
|
// Convert EMPTY_SETTINGS error to success with empty object
|
|
@@ -89,7 +90,10 @@ export const saveSettings = async (
|
|
|
89
90
|
// Write settings with proper formatting
|
|
90
91
|
await fs.writeFile(settingsPath, `${JSON.stringify(settingsWithVersion, null, 2)}\n`, 'utf8');
|
|
91
92
|
},
|
|
92
|
-
(error:
|
|
93
|
+
(error: unknown) => {
|
|
94
|
+
const nodeError = error as { message?: string };
|
|
95
|
+
return new Error(`Failed to save settings: ${nodeError.message ?? 'Unknown error'}`);
|
|
96
|
+
}
|
|
93
97
|
);
|
|
94
98
|
};
|
|
95
99
|
|
|
@@ -2,9 +2,13 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
4
4
|
import type { MCPServerConfigUnion, TargetConfig } from '../../types.js';
|
|
5
|
+
import type { FrontMatterMetadata } from '../../types/target-config.types.js';
|
|
5
6
|
import { readJSONCFile, writeJSONCFile } from '../files/jsonc.js';
|
|
6
7
|
import { pathSecurity, sanitize } from '../security/security.js';
|
|
7
8
|
|
|
9
|
+
/** Configuration data structure returned by readConfig */
|
|
10
|
+
export type ConfigData = Record<string, unknown>;
|
|
11
|
+
|
|
8
12
|
/**
|
|
9
13
|
* File system utilities for targets
|
|
10
14
|
*/
|
|
@@ -17,7 +21,7 @@ export const fileUtils = {
|
|
|
17
21
|
return pathSecurity.safeJoin(cwd, configFileName);
|
|
18
22
|
},
|
|
19
23
|
|
|
20
|
-
async readConfig(config: TargetConfig, cwd: string): Promise<
|
|
24
|
+
async readConfig(config: TargetConfig, cwd: string): Promise<ConfigData> {
|
|
21
25
|
const configPath = fileUtils.getConfigPath(config, cwd);
|
|
22
26
|
|
|
23
27
|
try {
|
|
@@ -40,7 +44,7 @@ export const fileUtils = {
|
|
|
40
44
|
throw new Error(`Unsupported config file format: ${config.configFile}`);
|
|
41
45
|
},
|
|
42
46
|
|
|
43
|
-
async writeConfig(config: TargetConfig, cwd: string, data:
|
|
47
|
+
async writeConfig(config: TargetConfig, cwd: string, data: ConfigData): Promise<void> {
|
|
44
48
|
const configPath = fileUtils.getConfigPath(config, cwd);
|
|
45
49
|
|
|
46
50
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
@@ -91,7 +95,7 @@ export const fileUtils = {
|
|
|
91
95
|
* YAML utilities for targets
|
|
92
96
|
*/
|
|
93
97
|
export const yamlUtils = {
|
|
94
|
-
async extractFrontMatter(content: string): Promise<{ metadata:
|
|
98
|
+
async extractFrontMatter(content: string): Promise<{ metadata: FrontMatterMetadata; content: string }> {
|
|
95
99
|
const yamlRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
96
100
|
const match = content.match(yamlRegex);
|
|
97
101
|
|
|
@@ -111,7 +115,7 @@ export const yamlUtils = {
|
|
|
111
115
|
return { metadata: {}, content };
|
|
112
116
|
},
|
|
113
117
|
|
|
114
|
-
async addFrontMatter(content: string, metadata:
|
|
118
|
+
async addFrontMatter(content: string, metadata: FrontMatterMetadata): Promise<string> {
|
|
115
119
|
if (!metadata || Object.keys(metadata).length === 0) {
|
|
116
120
|
return content;
|
|
117
121
|
}
|
|
@@ -136,14 +140,14 @@ export const yamlUtils = {
|
|
|
136
140
|
return yamlRegex.test(content);
|
|
137
141
|
},
|
|
138
142
|
|
|
139
|
-
async ensureFrontMatter(content: string, defaultMetadata:
|
|
143
|
+
async ensureFrontMatter(content: string, defaultMetadata: FrontMatterMetadata = {}): Promise<string> {
|
|
140
144
|
if (yamlUtils.hasValidFrontMatter(content)) {
|
|
141
145
|
return content;
|
|
142
146
|
}
|
|
143
147
|
return yamlUtils.addFrontMatter(content, defaultMetadata);
|
|
144
148
|
},
|
|
145
149
|
|
|
146
|
-
async extractAgentMetadata(content: string): Promise<
|
|
150
|
+
async extractAgentMetadata(content: string): Promise<FrontMatterMetadata> {
|
|
147
151
|
const { metadata } = await yamlUtils.extractFrontMatter(content);
|
|
148
152
|
|
|
149
153
|
if (typeof metadata === 'string') {
|
|
@@ -157,14 +161,14 @@ export const yamlUtils = {
|
|
|
157
161
|
return metadata || {};
|
|
158
162
|
},
|
|
159
163
|
|
|
160
|
-
async updateAgentMetadata(content: string, updates:
|
|
164
|
+
async updateAgentMetadata(content: string, updates: Partial<FrontMatterMetadata>): Promise<string> {
|
|
161
165
|
const { metadata: existingMetadata, content: baseContent } =
|
|
162
166
|
await yamlUtils.extractFrontMatter(content);
|
|
163
167
|
const updatedMetadata = { ...existingMetadata, ...updates };
|
|
164
168
|
return yamlUtils.addFrontMatter(baseContent, updatedMetadata);
|
|
165
169
|
},
|
|
166
170
|
|
|
167
|
-
validateClaudeCodeFrontMatter(metadata:
|
|
171
|
+
validateClaudeCodeFrontMatter(metadata: unknown): metadata is FrontMatterMetadata {
|
|
168
172
|
if (typeof metadata !== 'object' || metadata === null) {
|
|
169
173
|
return false;
|
|
170
174
|
}
|
|
@@ -183,7 +187,7 @@ export const yamlUtils = {
|
|
|
183
187
|
return true;
|
|
184
188
|
},
|
|
185
189
|
|
|
186
|
-
normalizeClaudeCodeFrontMatter(metadata:
|
|
190
|
+
normalizeClaudeCodeFrontMatter(metadata: FrontMatterMetadata): FrontMatterMetadata {
|
|
187
191
|
const normalized = { ...metadata };
|
|
188
192
|
|
|
189
193
|
if (normalized.tools && typeof normalized.tools === 'string') {
|
|
@@ -276,7 +280,7 @@ export const pathUtils = {
|
|
|
276
280
|
return kebabName || null;
|
|
277
281
|
},
|
|
278
282
|
|
|
279
|
-
extractAgentName(content: string, metadata:
|
|
283
|
+
extractAgentName(content: string, metadata: FrontMatterMetadata, sourcePath?: string): string {
|
|
280
284
|
// Try to extract from file path first
|
|
281
285
|
if (sourcePath) {
|
|
282
286
|
const pathName = pathUtils.extractNameFromPath(sourcePath);
|
|
@@ -363,13 +367,13 @@ ${basePrompt}`;
|
|
|
363
367
|
export const transformUtils = {
|
|
364
368
|
defaultTransformAgentContent(
|
|
365
369
|
content: string,
|
|
366
|
-
_metadata?:
|
|
370
|
+
_metadata?: FrontMatterMetadata,
|
|
367
371
|
_sourcePath?: string
|
|
368
372
|
): Promise<string> {
|
|
369
373
|
return Promise.resolve(content);
|
|
370
374
|
},
|
|
371
375
|
|
|
372
|
-
defaultTransformMCPConfig(config: MCPServerConfigUnion):
|
|
376
|
+
defaultTransformMCPConfig(config: MCPServerConfigUnion): MCPServerConfigUnion {
|
|
373
377
|
return config;
|
|
374
378
|
},
|
|
375
379
|
};
|
package/src/utils/files/jsonc.ts
CHANGED
|
@@ -21,16 +21,16 @@ function stripComments(content: string): string {
|
|
|
21
21
|
/**
|
|
22
22
|
* Read and parse JSONC file
|
|
23
23
|
*/
|
|
24
|
-
export async function readJSONCFile(filePath: string): Promise<
|
|
24
|
+
export async function readJSONCFile<T = unknown>(filePath: string): Promise<T> {
|
|
25
25
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
26
26
|
const stripped = stripComments(content);
|
|
27
|
-
return JSON.parse(stripped);
|
|
27
|
+
return JSON.parse(stripped) as T;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Write JSONC file (writes as regular JSON)
|
|
32
32
|
*/
|
|
33
|
-
export async function writeJSONCFile(filePath: string, data:
|
|
33
|
+
export async function writeJSONCFile<T>(filePath: string, data: T): Promise<void> {
|
|
34
34
|
const content = JSON.stringify(data, null, 2);
|
|
35
35
|
await fs.writeFile(filePath, content, 'utf-8');
|
|
36
36
|
}
|
|
@@ -10,8 +10,12 @@ import { UserCancelledError } from './errors.js';
|
|
|
10
10
|
* @param error - Error to check
|
|
11
11
|
* @returns True if error represents user cancellation
|
|
12
12
|
*/
|
|
13
|
-
export function isUserCancellation(error:
|
|
14
|
-
|
|
13
|
+
export function isUserCancellation(error: unknown): boolean {
|
|
14
|
+
if (error === null || typeof error !== 'object') {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const errorObj = error as { name?: string; message?: string };
|
|
18
|
+
return errorObj.name === 'ExitPromptError' || errorObj.message?.includes('force closed') === true;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/**
|
|
@@ -22,7 +26,7 @@ export function isUserCancellation(error: any): boolean {
|
|
|
22
26
|
* @throws {UserCancelledError} If user cancelled the prompt
|
|
23
27
|
* @throws Original error if not a cancellation
|
|
24
28
|
*/
|
|
25
|
-
export function handlePromptError(error:
|
|
29
|
+
export function handlePromptError(error: unknown, message: string): never {
|
|
26
30
|
if (isUserCancellation(error)) {
|
|
27
31
|
throw new UserCancelledError(message);
|
|
28
32
|
}
|
|
@@ -489,46 +489,6 @@ export class RateLimiter {
|
|
|
489
489
|
}
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
// ============================================================================
|
|
493
|
-
// SECURITY MIDDLEWARE
|
|
494
|
-
// ============================================================================
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Security middleware for common patterns
|
|
498
|
-
*/
|
|
499
|
-
export const securityMiddleware = {
|
|
500
|
-
/**
|
|
501
|
-
* Rate limiting middleware
|
|
502
|
-
*/
|
|
503
|
-
rateLimit: (limiter: RateLimiter, getIdentifier: (req: any) => string) => {
|
|
504
|
-
return (req: any, res: any, next: any) => {
|
|
505
|
-
const identifier = getIdentifier(req);
|
|
506
|
-
|
|
507
|
-
if (!limiter.isAllowed(identifier)) {
|
|
508
|
-
return res.status(429).json({ error: 'Too many requests' });
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
next();
|
|
512
|
-
};
|
|
513
|
-
},
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Input validation middleware
|
|
517
|
-
*/
|
|
518
|
-
validateInput: (schema: z.ZodSchema, source: 'body' | 'query' | 'params' = 'body') => {
|
|
519
|
-
return (req: any, res: any, next: any) => {
|
|
520
|
-
try {
|
|
521
|
-
const data = req[source];
|
|
522
|
-
const validated = schema.parse(data);
|
|
523
|
-
req[source] = validated;
|
|
524
|
-
next();
|
|
525
|
-
} catch (error) {
|
|
526
|
-
return res.status(400).json({ error: 'Invalid input', details: error });
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
},
|
|
530
|
-
};
|
|
531
|
-
|
|
532
492
|
export default {
|
|
533
493
|
securitySchemas,
|
|
534
494
|
pathSecurity,
|
|
@@ -537,5 +497,4 @@ export default {
|
|
|
537
497
|
envSecurity,
|
|
538
498
|
cryptoUtils,
|
|
539
499
|
RateLimiter,
|
|
540
|
-
securityMiddleware,
|
|
541
500
|
};
|