@sylphx/flow 2.18.1 → 2.18.2
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 +8 -0
- package/assets/slash-commands/continue.md +21 -66
- package/assets/slash-commands/review.md +17 -13
- package/package.json +1 -1
- package/src/core/backup-manager.ts +3 -14
- package/src/core/secrets-manager.ts +3 -14
- package/src/core/target-resolver.ts +28 -0
- package/src/utils/index.ts +1 -5
- package/src/utils/functional/array.ts +0 -355
- package/src/utils/functional/index.ts +0 -15
- package/src/utils/functional/object.ts +0 -279
- package/src/utils/functional/string.ts +0 -281
- package/src/utils/functional.ts +0 -543
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @sylphx/flow
|
|
2
2
|
|
|
3
|
+
## 2.18.2 (2025-12-18)
|
|
4
|
+
|
|
5
|
+
### ♻️ Refactoring
|
|
6
|
+
|
|
7
|
+
- **commands:** align /review with /continue - skills as step 1 ([b700fa5](https://github.com/SylphxAI/flow/commit/b700fa5272d6a656cd88acf94e3851cf31d29727))
|
|
8
|
+
- **commands:** simplify /continue - half the length, skills in execution flow ([b04facb](https://github.com/SylphxAI/flow/commit/b04facb60138155b0e203ac6fbf3f03d5252f226))
|
|
9
|
+
- **core:** remove dead code and consolidate duplications ([ee54d3f](https://github.com/SylphxAI/flow/commit/ee54d3f885f8e7a7011e39d95e5403c70fa595be))
|
|
10
|
+
|
|
3
11
|
## 2.18.1 (2025-12-18)
|
|
4
12
|
|
|
5
13
|
### 🐛 Bug Fixes
|
|
@@ -10,85 +10,40 @@ Find what's incomplete. Finish it.
|
|
|
10
10
|
|
|
11
11
|
## Mandate
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* **One pass.** No deferrals. Complete each fix fully.
|
|
17
|
-
|
|
18
|
-
## How to Find Incomplete Work
|
|
19
|
-
|
|
20
|
-
Don't grep for TODO and call it done. Incomplete work hides in:
|
|
21
|
-
|
|
22
|
-
**What's explicitly unfinished** — Yes, scan for TODO/FIXME/HACK. But ask: why are they there? What was the person avoiding?
|
|
23
|
-
|
|
24
|
-
**What's implicitly broken** — Code that "works" but:
|
|
25
|
-
- Fails silently (empty catch blocks, swallowed errors)
|
|
26
|
-
- Works only in happy path (no validation, no edge cases)
|
|
27
|
-
- Works but confuses users (unclear errors, missing feedback)
|
|
28
|
-
- Works but can't be debugged (no logs, no context)
|
|
29
|
-
|
|
30
|
-
**What's missing entirely** — Features referenced but not implemented. UI that leads nowhere. Promises in docs that code doesn't deliver.
|
|
31
|
-
|
|
32
|
-
## The Real Test
|
|
33
|
-
|
|
34
|
-
For each part of the system, ask:
|
|
35
|
-
|
|
36
|
-
> "If I were a user trying to accomplish their goal, where would I get stuck?"
|
|
37
|
-
|
|
38
|
-
> "If this broke at 3am, could someone figure out why?"
|
|
39
|
-
|
|
40
|
-
> "If requirements changed tomorrow, what would be painful to modify?"
|
|
41
|
-
|
|
42
|
-
> "If we had 100x traffic, what would fall over first?"
|
|
43
|
-
|
|
44
|
-
These questions reveal incompleteness that checklists miss.
|
|
13
|
+
- **Think, don't checklist.** Understand the project. What would "done" look like?
|
|
14
|
+
- **Delegate workers** for parallel research. You synthesize and verify.
|
|
15
|
+
- **Fix, don't report.** Implement solutions directly.
|
|
45
16
|
|
|
46
17
|
## Execution
|
|
47
18
|
|
|
48
|
-
1. **Understand
|
|
19
|
+
1. **Understand** — Read README, entry points, core flows
|
|
49
20
|
|
|
50
|
-
2. **
|
|
21
|
+
2. **Find gaps** — Not just TODO/FIXME, but:
|
|
22
|
+
- Implicit broken (empty catch, happy path only, no logs)
|
|
23
|
+
- Missing entirely (referenced but not implemented)
|
|
51
24
|
|
|
52
|
-
3. **
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
|
|
25
|
+
3. **Invoke skills** — Before fixing, load guidelines for relevant domains:
|
|
26
|
+
```
|
|
27
|
+
auth, account-security, billing, security, database, performance, observability...
|
|
28
|
+
```
|
|
29
|
+
Skills contain: tech stack decisions, non-negotiables, patterns, anti-patterns.
|
|
56
30
|
|
|
57
|
-
4. **
|
|
31
|
+
4. **Fix by impact** — What blocks users? Causes data loss? Fix those first. Fix completely.
|
|
58
32
|
|
|
59
|
-
5. **
|
|
60
|
-
|
|
61
|
-
## Skills (Guidelines)
|
|
62
|
-
|
|
63
|
-
**Skills contain implementation guidelines** — tech stack decisions, non-negotiables, patterns, anti-patterns for each domain.
|
|
64
|
-
|
|
65
|
-
Available skills:
|
|
66
|
-
auth, account-security, privacy, billing, pricing, ledger, security, trust-safety, uiux, seo, pwa, performance, i18n, database, data-architecture, storage, observability, operability, delivery, growth, referral, support, admin, discovery, code-quality
|
|
67
|
-
|
|
68
|
-
**You MUST invoke relevant skills** using the Skill tool before fixing. This loads the guidelines for that domain.
|
|
69
|
-
|
|
70
|
-
## Loop
|
|
71
|
-
|
|
72
|
-
After fixing: "Did my fixes introduce new gaps? Did fixing X reveal Y was also broken?"
|
|
73
|
-
|
|
74
|
-
If yes → run `/continue` again. If no Critical/High issues remain → done.
|
|
33
|
+
5. **Loop** — New gaps from fixes? → `/continue` again
|
|
75
34
|
|
|
76
35
|
## Output
|
|
77
36
|
|
|
78
37
|
```
|
|
79
|
-
##
|
|
80
|
-
|
|
81
|
-
[Describe the gaps discovered — not a checklist, but an understanding of what's incomplete and why]
|
|
82
|
-
|
|
83
|
-
## What I Fixed
|
|
38
|
+
## Found
|
|
39
|
+
[What's incomplete and why]
|
|
84
40
|
|
|
85
|
-
|
|
41
|
+
## Fixed
|
|
42
|
+
[Changes made]
|
|
86
43
|
|
|
87
|
-
##
|
|
88
|
-
|
|
89
|
-
- [Issues that need human decision or are blocked]
|
|
44
|
+
## Remains
|
|
45
|
+
[Needs human decision]
|
|
90
46
|
|
|
91
47
|
## Next
|
|
92
|
-
|
|
93
|
-
[/continue again | done]
|
|
48
|
+
[/continue | done]
|
|
94
49
|
```
|
|
@@ -12,19 +12,23 @@ args:
|
|
|
12
12
|
|
|
13
13
|
## Mandate
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* **Fix, don't report.** Implement solutions directly.
|
|
15
|
+
- **Think like the failure mode.** Security → attacker. Performance → slow network. Auth → confused user.
|
|
16
|
+
- **Delegate workers** for parallel research. You synthesize and verify.
|
|
17
|
+
- **Fix, don't report.** Implement solutions directly.
|
|
19
18
|
|
|
20
|
-
##
|
|
19
|
+
## Execution
|
|
21
20
|
|
|
22
|
-
**
|
|
21
|
+
1. **Invoke skills** — Load guidelines for relevant domains:
|
|
22
|
+
```
|
|
23
|
+
auth, account-security, billing, security, database, performance, observability...
|
|
24
|
+
```
|
|
25
|
+
Skills contain: tech stack decisions, non-negotiables, patterns, anti-patterns.
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
auth, account-security, privacy, billing, pricing, ledger, security, trust-safety, uiux, seo, pwa, performance, i18n, database, data-architecture, storage, observability, operability, delivery, growth, referral, support, admin, discovery, code-quality
|
|
27
|
+
2. **Understand** — How is this implemented? Architecture, choices, tradeoffs.
|
|
26
28
|
|
|
27
|
-
**
|
|
29
|
+
3. **Find issues** — What violates the guidelines? What's wrong and why it matters?
|
|
30
|
+
|
|
31
|
+
4. **Fix** — Implement solutions directly.
|
|
28
32
|
|
|
29
33
|
## Output
|
|
30
34
|
|
|
@@ -32,14 +36,14 @@ auth, account-security, privacy, billing, pricing, ledger, security, trust-safet
|
|
|
32
36
|
## Review: [topic]
|
|
33
37
|
|
|
34
38
|
### Understanding
|
|
35
|
-
[
|
|
39
|
+
[Architecture, choices, tradeoffs]
|
|
36
40
|
|
|
37
41
|
### Issues
|
|
38
|
-
[What's wrong and why
|
|
42
|
+
[What's wrong and why]
|
|
39
43
|
|
|
40
44
|
### Fixed
|
|
41
45
|
[Changes made]
|
|
42
46
|
|
|
43
|
-
###
|
|
44
|
-
[Needs human decision
|
|
47
|
+
### Remains
|
|
48
|
+
[Needs human decision]
|
|
45
49
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/flow",
|
|
3
|
-
"version": "2.18.
|
|
3
|
+
"version": "2.18.2",
|
|
4
4
|
"description": "One CLI to rule them all. Unified orchestration layer for Claude Code, OpenCode, Cursor and all AI development tools. Auto-detection, auto-installation, auto-upgrade.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@ import fs from 'node:fs/promises';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import type { Target } from '../types/target.types.js';
|
|
11
11
|
import type { ProjectManager } from './project-manager.js';
|
|
12
|
-
import {
|
|
12
|
+
import { resolveTarget, resolveTargetOrId } from './target-resolver.js';
|
|
13
13
|
|
|
14
14
|
export interface BackupInfo {
|
|
15
15
|
sessionId: string;
|
|
@@ -69,17 +69,6 @@ export class BackupManager {
|
|
|
69
69
|
this.projectManager = projectManager;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/**
|
|
73
|
-
* Resolve target from ID string to Target object
|
|
74
|
-
*/
|
|
75
|
-
private resolveTarget(targetId: string): Target {
|
|
76
|
-
const targetOption = targetManager.getTarget(targetId);
|
|
77
|
-
if (targetOption._tag === 'None') {
|
|
78
|
-
throw new Error(`Unknown target: ${targetId}`);
|
|
79
|
-
}
|
|
80
|
-
return targetOption.value;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
72
|
/**
|
|
84
73
|
* Create full backup of project environment
|
|
85
74
|
*/
|
|
@@ -88,7 +77,7 @@ export class BackupManager {
|
|
|
88
77
|
projectHash: string,
|
|
89
78
|
targetOrId: Target | string
|
|
90
79
|
): Promise<BackupInfo> {
|
|
91
|
-
const target =
|
|
80
|
+
const target = resolveTargetOrId(targetOrId);
|
|
92
81
|
const targetId = target.id;
|
|
93
82
|
const sessionId = `session-${Date.now()}`;
|
|
94
83
|
const timestamp = new Date().toISOString();
|
|
@@ -178,7 +167,7 @@ export class BackupManager {
|
|
|
178
167
|
const targetId = manifest.target;
|
|
179
168
|
|
|
180
169
|
// Resolve target to get config
|
|
181
|
-
const target =
|
|
170
|
+
const target = resolveTarget(targetId);
|
|
182
171
|
|
|
183
172
|
// Get target config directory
|
|
184
173
|
const targetConfigDir = this.projectManager.getTargetConfigDir(projectPath, target);
|
|
@@ -9,7 +9,7 @@ import fs from 'node:fs/promises';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import type { Target } from '../types/target.types.js';
|
|
11
11
|
import type { ProjectManager } from './project-manager.js';
|
|
12
|
-
import {
|
|
12
|
+
import { resolveTargetOrId } from './target-resolver.js';
|
|
13
13
|
|
|
14
14
|
export interface MCPSecrets {
|
|
15
15
|
version: string;
|
|
@@ -30,17 +30,6 @@ export class SecretsManager {
|
|
|
30
30
|
this.projectManager = projectManager;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
* Resolve target from ID string to Target object
|
|
35
|
-
*/
|
|
36
|
-
private resolveTarget(targetId: string): Target {
|
|
37
|
-
const targetOption = targetManager.getTarget(targetId);
|
|
38
|
-
if (targetOption._tag === 'None') {
|
|
39
|
-
throw new Error(`Unknown target: ${targetId}`);
|
|
40
|
-
}
|
|
41
|
-
return targetOption.value;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
33
|
/**
|
|
45
34
|
* Extract MCP secrets from project config
|
|
46
35
|
*/
|
|
@@ -49,7 +38,7 @@ export class SecretsManager {
|
|
|
49
38
|
_projectHash: string,
|
|
50
39
|
targetOrId: Target | string
|
|
51
40
|
): Promise<MCPSecrets> {
|
|
52
|
-
const target =
|
|
41
|
+
const target = resolveTargetOrId(targetOrId);
|
|
53
42
|
// configFile is at project root, not in targetDir
|
|
54
43
|
const configPath = path.join(projectPath, target.config.configFile);
|
|
55
44
|
const mcpPath = target.config.mcpConfigPath;
|
|
@@ -145,7 +134,7 @@ export class SecretsManager {
|
|
|
145
134
|
return;
|
|
146
135
|
}
|
|
147
136
|
|
|
148
|
-
const target =
|
|
137
|
+
const target = resolveTargetOrId(targetOrId);
|
|
149
138
|
// configFile is at project root, not in targetDir
|
|
150
139
|
const configPath = path.join(projectPath, target.config.configFile);
|
|
151
140
|
const mcpPath = target.config.mcpConfigPath;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Target Resolver
|
|
3
|
+
* Shared utility for resolving target IDs to Target objects
|
|
4
|
+
* Eliminates duplication across BackupManager and SecretsManager
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Target } from '../types/target.types.js';
|
|
8
|
+
import { targetManager } from './target-manager.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a target from ID string to Target object
|
|
12
|
+
* @throws Error if target ID is not found
|
|
13
|
+
*/
|
|
14
|
+
export function resolveTarget(targetId: string): Target {
|
|
15
|
+
const targetOption = targetManager.getTarget(targetId);
|
|
16
|
+
if (targetOption._tag === 'None') {
|
|
17
|
+
throw new Error(`Unknown target: ${targetId}`);
|
|
18
|
+
}
|
|
19
|
+
return targetOption.value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve target, accepting either string ID or Target object
|
|
24
|
+
* Returns the Target object in both cases
|
|
25
|
+
*/
|
|
26
|
+
export function resolveTargetOrId(targetOrId: Target | string): Target {
|
|
27
|
+
return typeof targetOrId === 'string' ? resolveTarget(targetOrId) : targetOrId;
|
|
28
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -37,13 +37,9 @@ export * from './error-handler.js';
|
|
|
37
37
|
export * from './files/file-operations.js';
|
|
38
38
|
export * from './files/sync-utils.js';
|
|
39
39
|
// ============================================================================
|
|
40
|
-
// FUNCTIONAL PROGRAMMING
|
|
41
|
-
// ============================================================================
|
|
42
|
-
export * from './functional.js';
|
|
43
|
-
export * from './security/secret-utils.js';
|
|
44
|
-
// ============================================================================
|
|
45
40
|
// SECURITY
|
|
46
41
|
// ============================================================================
|
|
42
|
+
export * from './security/secret-utils.js';
|
|
47
43
|
export * from './security/security.js';
|
|
48
44
|
// ============================================================================
|
|
49
45
|
// VERSIONING
|
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Functional array utilities
|
|
3
|
-
* Pure array transformation functions
|
|
4
|
-
*
|
|
5
|
-
* DESIGN RATIONALE:
|
|
6
|
-
* - Pure functions for array operations
|
|
7
|
-
* - Composable transformations
|
|
8
|
-
* - Type-safe operations
|
|
9
|
-
* - No side effects (no mutations)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Map over array
|
|
14
|
-
*/
|
|
15
|
-
export const map =
|
|
16
|
-
<T, U>(fn: (item: T, index: number) => U) =>
|
|
17
|
-
(arr: T[]): U[] =>
|
|
18
|
-
arr.map(fn);
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Filter array
|
|
22
|
-
*/
|
|
23
|
-
export const filter =
|
|
24
|
-
<T>(predicate: (item: T, index: number) => boolean) =>
|
|
25
|
-
(arr: T[]): T[] =>
|
|
26
|
-
arr.filter(predicate);
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Reduce array
|
|
30
|
-
*/
|
|
31
|
-
export const reduce =
|
|
32
|
-
<T, U>(fn: (acc: U, item: T, index: number) => U, initial: U) =>
|
|
33
|
-
(arr: T[]): U =>
|
|
34
|
-
arr.reduce(fn, initial);
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Find first element matching predicate
|
|
38
|
-
*/
|
|
39
|
-
export const find =
|
|
40
|
-
<T>(predicate: (item: T, index: number) => boolean) =>
|
|
41
|
-
(arr: T[]): T | undefined =>
|
|
42
|
-
arr.find(predicate);
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Find index of first element matching predicate
|
|
46
|
-
*/
|
|
47
|
-
export const findIndex =
|
|
48
|
-
<T>(predicate: (item: T, index: number) => boolean) =>
|
|
49
|
-
(arr: T[]): number =>
|
|
50
|
-
arr.findIndex(predicate);
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Check if any element matches predicate
|
|
54
|
-
*/
|
|
55
|
-
export const some =
|
|
56
|
-
<T>(predicate: (item: T, index: number) => boolean) =>
|
|
57
|
-
(arr: T[]): boolean =>
|
|
58
|
-
arr.some(predicate);
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Check if all elements match predicate
|
|
62
|
-
*/
|
|
63
|
-
export const every =
|
|
64
|
-
<T>(predicate: (item: T, index: number) => boolean) =>
|
|
65
|
-
(arr: T[]): boolean =>
|
|
66
|
-
arr.every(predicate);
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Flatten array one level
|
|
70
|
-
*/
|
|
71
|
-
export const flatten = <T>(arr: T[][]): T[] => arr.flat();
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Flatten array deeply
|
|
75
|
-
*/
|
|
76
|
-
export const flattenDeep = <T>(arr: any[]): T[] => arr.flat(Number.POSITIVE_INFINITY);
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Map and flatten (flatMap)
|
|
80
|
-
*/
|
|
81
|
-
export const flatMap =
|
|
82
|
-
<T, U>(fn: (item: T, index: number) => U[]) =>
|
|
83
|
-
(arr: T[]): U[] =>
|
|
84
|
-
arr.flatMap(fn);
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Take first n elements
|
|
88
|
-
*/
|
|
89
|
-
export const take =
|
|
90
|
-
(n: number) =>
|
|
91
|
-
<T>(arr: T[]): T[] =>
|
|
92
|
-
arr.slice(0, n);
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Skip first n elements
|
|
96
|
-
*/
|
|
97
|
-
export const skip =
|
|
98
|
-
(n: number) =>
|
|
99
|
-
<T>(arr: T[]): T[] =>
|
|
100
|
-
arr.slice(n);
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Take while predicate is true
|
|
104
|
-
*/
|
|
105
|
-
export const takeWhile =
|
|
106
|
-
<T>(predicate: (item: T) => boolean) =>
|
|
107
|
-
(arr: T[]): T[] => {
|
|
108
|
-
const index = arr.findIndex((item) => !predicate(item));
|
|
109
|
-
return index === -1 ? arr : arr.slice(0, index);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Skip while predicate is true
|
|
114
|
-
*/
|
|
115
|
-
export const skipWhile =
|
|
116
|
-
<T>(predicate: (item: T) => boolean) =>
|
|
117
|
-
(arr: T[]): T[] => {
|
|
118
|
-
const index = arr.findIndex((item) => !predicate(item));
|
|
119
|
-
return index === -1 ? [] : arr.slice(index);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Reverse array
|
|
124
|
-
*/
|
|
125
|
-
export const reverse = <T>(arr: T[]): T[] => [...arr].reverse();
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Sort array
|
|
129
|
-
*/
|
|
130
|
-
export const sort =
|
|
131
|
-
<T>(compareFn?: (a: T, b: T) => number) =>
|
|
132
|
-
(arr: T[]): T[] =>
|
|
133
|
-
[...arr].sort(compareFn);
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Sort by key
|
|
137
|
-
*/
|
|
138
|
-
export const sortBy =
|
|
139
|
-
<T, K extends keyof T>(key: K, order: 'asc' | 'desc' = 'asc') =>
|
|
140
|
-
(arr: T[]): T[] => {
|
|
141
|
-
return [...arr].sort((a, b) => {
|
|
142
|
-
const aVal = a[key];
|
|
143
|
-
const bVal = b[key];
|
|
144
|
-
|
|
145
|
-
if (aVal < bVal) {
|
|
146
|
-
return order === 'asc' ? -1 : 1;
|
|
147
|
-
}
|
|
148
|
-
if (aVal > bVal) {
|
|
149
|
-
return order === 'asc' ? 1 : -1;
|
|
150
|
-
}
|
|
151
|
-
return 0;
|
|
152
|
-
});
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Remove duplicates
|
|
157
|
-
*/
|
|
158
|
-
export const unique = <T>(arr: T[]): T[] => [...new Set(arr)];
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Remove duplicates by key
|
|
162
|
-
*/
|
|
163
|
-
export const uniqueBy =
|
|
164
|
-
<T, K extends keyof T>(key: K) =>
|
|
165
|
-
(arr: T[]): T[] => {
|
|
166
|
-
const seen = new Set();
|
|
167
|
-
return arr.filter((item) => {
|
|
168
|
-
const value = item[key];
|
|
169
|
-
if (seen.has(value)) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
seen.add(value);
|
|
173
|
-
return true;
|
|
174
|
-
});
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Partition array into two based on predicate
|
|
179
|
-
*/
|
|
180
|
-
export const partition =
|
|
181
|
-
<T>(predicate: (item: T) => boolean) =>
|
|
182
|
-
(arr: T[]): [T[], T[]] => {
|
|
183
|
-
const pass: T[] = [];
|
|
184
|
-
const fail: T[] = [];
|
|
185
|
-
|
|
186
|
-
for (const item of arr) {
|
|
187
|
-
if (predicate(item)) {
|
|
188
|
-
pass.push(item);
|
|
189
|
-
} else {
|
|
190
|
-
fail.push(item);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return [pass, fail];
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Group by key
|
|
199
|
-
*/
|
|
200
|
-
export const groupBy =
|
|
201
|
-
<T, K extends keyof T>(key: K) =>
|
|
202
|
-
(arr: T[]): Record<string, T[]> => {
|
|
203
|
-
return arr.reduce(
|
|
204
|
-
(acc, item) => {
|
|
205
|
-
const groupKey = String(item[key]);
|
|
206
|
-
if (!acc[groupKey]) {
|
|
207
|
-
acc[groupKey] = [];
|
|
208
|
-
}
|
|
209
|
-
acc[groupKey].push(item);
|
|
210
|
-
return acc;
|
|
211
|
-
},
|
|
212
|
-
{} as Record<string, T[]>
|
|
213
|
-
);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Count occurrences
|
|
218
|
-
*/
|
|
219
|
-
export const countBy =
|
|
220
|
-
<T>(fn: (item: T) => string) =>
|
|
221
|
-
(arr: T[]): Record<string, number> => {
|
|
222
|
-
return arr.reduce(
|
|
223
|
-
(acc, item) => {
|
|
224
|
-
const key = fn(item);
|
|
225
|
-
acc[key] = (acc[key] || 0) + 1;
|
|
226
|
-
return acc;
|
|
227
|
-
},
|
|
228
|
-
{} as Record<string, number>
|
|
229
|
-
);
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Chunk array into smaller arrays
|
|
234
|
-
*/
|
|
235
|
-
export const chunk =
|
|
236
|
-
(size: number) =>
|
|
237
|
-
<T>(arr: T[]): T[][] => {
|
|
238
|
-
const chunks: T[][] = [];
|
|
239
|
-
for (let i = 0; i < arr.length; i += size) {
|
|
240
|
-
chunks.push(arr.slice(i, i + size));
|
|
241
|
-
}
|
|
242
|
-
return chunks;
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Zip arrays together
|
|
247
|
-
*/
|
|
248
|
-
export const zip = <T, U>(arr1: T[], arr2: U[]): [T, U][] => {
|
|
249
|
-
const length = Math.min(arr1.length, arr2.length);
|
|
250
|
-
const result: [T, U][] = [];
|
|
251
|
-
|
|
252
|
-
for (let i = 0; i < length; i++) {
|
|
253
|
-
result.push([arr1[i], arr2[i]]);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return result;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Unzip array of tuples
|
|
261
|
-
*/
|
|
262
|
-
export const unzip = <T, U>(arr: [T, U][]): [T[], U[]] => {
|
|
263
|
-
const first: T[] = [];
|
|
264
|
-
const second: U[] = [];
|
|
265
|
-
|
|
266
|
-
for (const [a, b] of arr) {
|
|
267
|
-
first.push(a);
|
|
268
|
-
second.push(b);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return [first, second];
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Intersperse value between array elements
|
|
276
|
-
*/
|
|
277
|
-
export const intersperse =
|
|
278
|
-
<T>(separator: T) =>
|
|
279
|
-
(arr: T[]): T[] => {
|
|
280
|
-
if (arr.length === 0) {
|
|
281
|
-
return [];
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const result: T[] = [arr[0]];
|
|
285
|
-
for (let i = 1; i < arr.length; i++) {
|
|
286
|
-
result.push(separator, arr[i]);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return result;
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Get first element
|
|
294
|
-
*/
|
|
295
|
-
export const head = <T>(arr: T[]): T | undefined => arr[0];
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Get last element
|
|
299
|
-
*/
|
|
300
|
-
export const last = <T>(arr: T[]): T | undefined => arr[arr.length - 1];
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Get all but first element
|
|
304
|
-
*/
|
|
305
|
-
export const tail = <T>(arr: T[]): T[] => arr.slice(1);
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Get all but last element
|
|
309
|
-
*/
|
|
310
|
-
export const init = <T>(arr: T[]): T[] => arr.slice(0, -1);
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Check if array is empty
|
|
314
|
-
*/
|
|
315
|
-
export const isEmpty = <T>(arr: T[]): boolean => arr.length === 0;
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Check if array is not empty
|
|
319
|
-
*/
|
|
320
|
-
export const isNotEmpty = <T>(arr: T[]): boolean => arr.length > 0;
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Sum numbers in array
|
|
324
|
-
*/
|
|
325
|
-
export const sum = (arr: number[]): number => arr.reduce((a, b) => a + b, 0);
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Get average of numbers in array
|
|
329
|
-
*/
|
|
330
|
-
export const average = (arr: number[]): number => {
|
|
331
|
-
if (arr.length === 0) {
|
|
332
|
-
return 0;
|
|
333
|
-
}
|
|
334
|
-
return sum(arr) / arr.length;
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Get min value
|
|
339
|
-
*/
|
|
340
|
-
export const min = (arr: number[]): number | undefined => {
|
|
341
|
-
if (arr.length === 0) {
|
|
342
|
-
return undefined;
|
|
343
|
-
}
|
|
344
|
-
return Math.min(...arr);
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Get max value
|
|
349
|
-
*/
|
|
350
|
-
export const max = (arr: number[]): number | undefined => {
|
|
351
|
-
if (arr.length === 0) {
|
|
352
|
-
return undefined;
|
|
353
|
-
}
|
|
354
|
-
return Math.max(...arr);
|
|
355
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Functional utilities
|
|
3
|
-
* Pure, composable utility functions
|
|
4
|
-
*
|
|
5
|
-
* DESIGN RATIONALE:
|
|
6
|
-
* - Pure functions for common operations
|
|
7
|
-
* - Composable through currying
|
|
8
|
-
* - Type-safe
|
|
9
|
-
* - No side effects
|
|
10
|
-
* - Point-free style support
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export * as Arr from './array.js';
|
|
14
|
-
export * as Obj from './object.js';
|
|
15
|
-
export * as Str from './string.js';
|