@grimoire-cc/cli 0.4.0 → 0.5.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/dist/bin.js +8 -2
- package/dist/bin.js.map +1 -1
- package/dist/prompt.js +1 -1
- package/dist/prompt.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +10 -0
- package/dist/setup.js.map +1 -1
- package/dist/static/log-viewer.html +1 -0
- package/dist/static/static/log-viewer.html +1 -0
- package/package.json +1 -1
- package/packs/ts-pack/grimoire.json +33 -0
- package/packs/ts-pack/skills/grimoire:modern-typescript/SKILL.md +336 -0
- package/packs/ts-pack/skills/grimoire:modern-typescript/reference/modern-features.md +373 -0
- package/packs/ts-pack/skills/grimoire:modern-typescript/reference/patterns-and-idioms.md +477 -0
- package/packs/ts-pack/skills/grimoire:modern-typescript/reference/type-system.md +389 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# Code Patterns & Idioms
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Functional Patterns](#functional-patterns)
|
|
6
|
+
- [Builder Pattern](#builder-pattern)
|
|
7
|
+
- [State Machines](#state-machines)
|
|
8
|
+
- [Immutable Data Patterns](#immutable-data-patterns)
|
|
9
|
+
- [Async Patterns](#async-patterns)
|
|
10
|
+
- [Dependency Injection](#dependency-injection)
|
|
11
|
+
- [Module Organization](#module-organization)
|
|
12
|
+
|
|
13
|
+
## Functional Patterns
|
|
14
|
+
|
|
15
|
+
### Pipe and Compose
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Type-safe pipe (left to right)
|
|
19
|
+
function pipe<A, B>(fn1: (a: A) => B): (a: A) => B;
|
|
20
|
+
function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => C;
|
|
21
|
+
function pipe<A, B, C, D>(
|
|
22
|
+
fn1: (a: A) => B,
|
|
23
|
+
fn2: (b: B) => C,
|
|
24
|
+
fn3: (c: C) => D,
|
|
25
|
+
): (a: A) => D;
|
|
26
|
+
function pipe(...fns: Function[]) {
|
|
27
|
+
return (arg: unknown) => fns.reduce((acc, fn) => fn(acc), arg);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Usage
|
|
31
|
+
const processUser = pipe(
|
|
32
|
+
validateInput,
|
|
33
|
+
normalizeEmail,
|
|
34
|
+
createUser,
|
|
35
|
+
);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Option / Maybe Pattern
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
type Option<T> = Some<T> | None;
|
|
42
|
+
interface Some<T> { readonly _tag: "Some"; readonly value: T }
|
|
43
|
+
interface None { readonly _tag: "None" }
|
|
44
|
+
|
|
45
|
+
const some = <T>(value: T): Option<T> => ({ _tag: "Some", value });
|
|
46
|
+
const none: Option<never> = { _tag: "None" };
|
|
47
|
+
|
|
48
|
+
function map<A, B>(opt: Option<A>, fn: (a: A) => B): Option<B> {
|
|
49
|
+
return opt._tag === "Some" ? some(fn(opt.value)) : none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function flatMap<A, B>(opt: Option<A>, fn: (a: A) => Option<B>): Option<B> {
|
|
53
|
+
return opt._tag === "Some" ? fn(opt.value) : none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getOrElse<T>(opt: Option<T>, fallback: () => T): T {
|
|
57
|
+
return opt._tag === "Some" ? opt.value : fallback();
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Exhaustive Pattern Matching
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
type Shape =
|
|
65
|
+
| { kind: "circle"; radius: number }
|
|
66
|
+
| { kind: "rect"; width: number; height: number }
|
|
67
|
+
| { kind: "triangle"; base: number; height: number };
|
|
68
|
+
|
|
69
|
+
function area(shape: Shape): number {
|
|
70
|
+
switch (shape.kind) {
|
|
71
|
+
case "circle":
|
|
72
|
+
return Math.PI * shape.radius ** 2;
|
|
73
|
+
case "rect":
|
|
74
|
+
return shape.width * shape.height;
|
|
75
|
+
case "triangle":
|
|
76
|
+
return (shape.base * shape.height) / 2;
|
|
77
|
+
default: {
|
|
78
|
+
const _exhaustive: never = shape;
|
|
79
|
+
throw new Error(`Unhandled shape: ${_exhaustive}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Currying
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
function curry<A, B, C>(fn: (a: A, b: B) => C): (a: A) => (b: B) => C {
|
|
89
|
+
return (a) => (b) => fn(a, b);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const multiply = curry((a: number, b: number) => a * b);
|
|
93
|
+
const double = multiply(2);
|
|
94
|
+
double(5); // 10
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Builder Pattern
|
|
98
|
+
|
|
99
|
+
### Type-Safe Builder with Required Fields
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
interface QueryConfig {
|
|
103
|
+
table: string;
|
|
104
|
+
select: string[];
|
|
105
|
+
where?: string;
|
|
106
|
+
limit?: number;
|
|
107
|
+
orderBy?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type RequiredFields = "table" | "select";
|
|
111
|
+
|
|
112
|
+
class QueryBuilder<Built extends string = never> {
|
|
113
|
+
private config: Partial<QueryConfig> = {};
|
|
114
|
+
|
|
115
|
+
table(name: string): QueryBuilder<Built | "table"> {
|
|
116
|
+
this.config.table = name;
|
|
117
|
+
return this as QueryBuilder<Built | "table">;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
select(...fields: string[]): QueryBuilder<Built | "select"> {
|
|
121
|
+
this.config.select = fields;
|
|
122
|
+
return this as QueryBuilder<Built | "select">;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
where(condition: string): this {
|
|
126
|
+
this.config.where = condition;
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
limit(n: number): this {
|
|
131
|
+
this.config.limit = n;
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Only callable when all required fields are set
|
|
136
|
+
build(this: QueryBuilder<RequiredFields>): QueryConfig {
|
|
137
|
+
return this.config as QueryConfig;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Usage — build() only available after table() and select()
|
|
142
|
+
const query = new QueryBuilder()
|
|
143
|
+
.table("users")
|
|
144
|
+
.select("id", "name")
|
|
145
|
+
.where("active = true")
|
|
146
|
+
.build(); // OK
|
|
147
|
+
|
|
148
|
+
// new QueryBuilder().select("id").build(); // Error — missing table()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Fluent Configuration
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
function createServer() {
|
|
155
|
+
const config = {
|
|
156
|
+
port: 3000,
|
|
157
|
+
host: "localhost",
|
|
158
|
+
cors: false,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const builder = {
|
|
162
|
+
port(p: number) { config.port = p; return builder; },
|
|
163
|
+
host(h: string) { config.host = h; return builder; },
|
|
164
|
+
cors(enabled: boolean) { config.cors = enabled; return builder; },
|
|
165
|
+
build() { return Object.freeze(config); },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return builder;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const server = createServer()
|
|
172
|
+
.port(8080)
|
|
173
|
+
.cors(true)
|
|
174
|
+
.build();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## State Machines
|
|
178
|
+
|
|
179
|
+
### Discriminated Union State Machine
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
type ConnectionState =
|
|
183
|
+
| { status: "disconnected" }
|
|
184
|
+
| { status: "connecting"; attempt: number }
|
|
185
|
+
| { status: "connected"; socket: WebSocket }
|
|
186
|
+
| { status: "error"; error: Error; retryAfter: number };
|
|
187
|
+
|
|
188
|
+
type ConnectionEvent =
|
|
189
|
+
| { type: "CONNECT" }
|
|
190
|
+
| { type: "CONNECTED"; socket: WebSocket }
|
|
191
|
+
| { type: "DISCONNECT" }
|
|
192
|
+
| { type: "ERROR"; error: Error };
|
|
193
|
+
|
|
194
|
+
function transition(
|
|
195
|
+
state: ConnectionState,
|
|
196
|
+
event: ConnectionEvent,
|
|
197
|
+
): ConnectionState {
|
|
198
|
+
switch (state.status) {
|
|
199
|
+
case "disconnected":
|
|
200
|
+
if (event.type === "CONNECT") {
|
|
201
|
+
return { status: "connecting", attempt: 1 };
|
|
202
|
+
}
|
|
203
|
+
return state;
|
|
204
|
+
|
|
205
|
+
case "connecting":
|
|
206
|
+
if (event.type === "CONNECTED") {
|
|
207
|
+
return { status: "connected", socket: event.socket };
|
|
208
|
+
}
|
|
209
|
+
if (event.type === "ERROR") {
|
|
210
|
+
return {
|
|
211
|
+
status: "error",
|
|
212
|
+
error: event.error,
|
|
213
|
+
retryAfter: state.attempt * 1000,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return state;
|
|
217
|
+
|
|
218
|
+
case "connected":
|
|
219
|
+
if (event.type === "DISCONNECT") {
|
|
220
|
+
return { status: "disconnected" };
|
|
221
|
+
}
|
|
222
|
+
if (event.type === "ERROR") {
|
|
223
|
+
return { status: "error", error: event.error, retryAfter: 1000 };
|
|
224
|
+
}
|
|
225
|
+
return state;
|
|
226
|
+
|
|
227
|
+
case "error":
|
|
228
|
+
if (event.type === "CONNECT") {
|
|
229
|
+
return { status: "connecting", attempt: 1 };
|
|
230
|
+
}
|
|
231
|
+
return state;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Immutable Data Patterns
|
|
237
|
+
|
|
238
|
+
### Immutable Updates
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
// Spread for shallow updates
|
|
242
|
+
function updateUser(user: Readonly<User>, patch: Partial<User>): User {
|
|
243
|
+
return { ...user, ...patch };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Nested updates with helper
|
|
247
|
+
function updateNested<T extends object, K extends keyof T>(
|
|
248
|
+
obj: Readonly<T>,
|
|
249
|
+
key: K,
|
|
250
|
+
updater: (value: T[K]) => T[K],
|
|
251
|
+
): T {
|
|
252
|
+
return { ...obj, [key]: updater(obj[key]) };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Usage
|
|
256
|
+
const updated = updateNested(state, "settings", (s) => ({
|
|
257
|
+
...s,
|
|
258
|
+
theme: "dark",
|
|
259
|
+
}));
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Readonly Collections
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Immutable map operations
|
|
266
|
+
function mapSet<K, V>(
|
|
267
|
+
map: ReadonlyMap<K, V>,
|
|
268
|
+
key: K,
|
|
269
|
+
value: V,
|
|
270
|
+
): ReadonlyMap<K, V> {
|
|
271
|
+
const copy = new Map(map);
|
|
272
|
+
copy.set(key, value);
|
|
273
|
+
return copy;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function mapDelete<K, V>(
|
|
277
|
+
map: ReadonlyMap<K, V>,
|
|
278
|
+
key: K,
|
|
279
|
+
): ReadonlyMap<K, V> {
|
|
280
|
+
const copy = new Map(map);
|
|
281
|
+
copy.delete(key);
|
|
282
|
+
return copy;
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### `as const` for Frozen Data
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
const PERMISSIONS = {
|
|
290
|
+
admin: ["read", "write", "delete"],
|
|
291
|
+
editor: ["read", "write"],
|
|
292
|
+
viewer: ["read"],
|
|
293
|
+
} as const;
|
|
294
|
+
|
|
295
|
+
// Type is deeply readonly with literal types
|
|
296
|
+
type Role = keyof typeof PERMISSIONS;
|
|
297
|
+
type Permission = (typeof PERMISSIONS)[Role][number];
|
|
298
|
+
// "read" | "write" | "delete"
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Async Patterns
|
|
302
|
+
|
|
303
|
+
### Typed Async Utilities
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// Promise with timeout
|
|
307
|
+
function withTimeout<T>(
|
|
308
|
+
promise: Promise<T>,
|
|
309
|
+
ms: number,
|
|
310
|
+
): Promise<T> {
|
|
311
|
+
const timeout = new Promise<never>((_, reject) =>
|
|
312
|
+
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms),
|
|
313
|
+
);
|
|
314
|
+
return Promise.race([promise, timeout]);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Retry with backoff
|
|
318
|
+
async function retry<T>(
|
|
319
|
+
fn: () => Promise<T>,
|
|
320
|
+
options: { attempts: number; delay: number },
|
|
321
|
+
): Promise<T> {
|
|
322
|
+
for (let i = 0; i < options.attempts; i++) {
|
|
323
|
+
try {
|
|
324
|
+
return await fn();
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (i === options.attempts - 1) throw error;
|
|
327
|
+
await new Promise((r) => setTimeout(r, options.delay * 2 ** i));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
throw new Error("Unreachable");
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### AsyncIterable Patterns
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// Async generator for paginated APIs
|
|
338
|
+
async function* paginate<T>(
|
|
339
|
+
fetchPage: (cursor?: string) => Promise<{ data: T[]; next?: string }>,
|
|
340
|
+
): AsyncGenerator<T> {
|
|
341
|
+
let cursor: string | undefined;
|
|
342
|
+
do {
|
|
343
|
+
const page = await fetchPage(cursor);
|
|
344
|
+
yield* page.data;
|
|
345
|
+
cursor = page.next;
|
|
346
|
+
} while (cursor);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Usage
|
|
350
|
+
for await (const user of paginate(fetchUsers)) {
|
|
351
|
+
process(user);
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Concurrent Task Limiter
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
async function mapConcurrent<T, R>(
|
|
359
|
+
items: readonly T[],
|
|
360
|
+
fn: (item: T) => Promise<R>,
|
|
361
|
+
concurrency: number,
|
|
362
|
+
): Promise<R[]> {
|
|
363
|
+
const results: R[] = [];
|
|
364
|
+
const executing = new Set<Promise<void>>();
|
|
365
|
+
|
|
366
|
+
for (const [index, item] of items.entries()) {
|
|
367
|
+
const task = fn(item).then((result) => {
|
|
368
|
+
results[index] = result;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const tracked = task.then(() => executing.delete(tracked));
|
|
372
|
+
executing.add(tracked);
|
|
373
|
+
|
|
374
|
+
if (executing.size >= concurrency) {
|
|
375
|
+
await Promise.race(executing);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await Promise.all(executing);
|
|
380
|
+
return results;
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Dependency Injection
|
|
385
|
+
|
|
386
|
+
### Function-Based DI
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// Define dependencies as an interface
|
|
390
|
+
interface Dependencies {
|
|
391
|
+
logger: Logger;
|
|
392
|
+
db: Database;
|
|
393
|
+
cache: Cache;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Functions accept dependencies explicitly
|
|
397
|
+
function createUserService(deps: Dependencies) {
|
|
398
|
+
return {
|
|
399
|
+
async getUser(id: string): Promise<User | null> {
|
|
400
|
+
const cached = await deps.cache.get<User>(`user:${id}`);
|
|
401
|
+
if (cached) return cached;
|
|
402
|
+
|
|
403
|
+
const user = await deps.db.query<User>("SELECT * FROM users WHERE id = $1", [id]);
|
|
404
|
+
if (user) await deps.cache.set(`user:${id}`, user);
|
|
405
|
+
return user;
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Wire up at composition root
|
|
411
|
+
const deps: Dependencies = { logger, db, cache };
|
|
412
|
+
const userService = createUserService(deps);
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Token-Based DI (for larger applications)
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// Branded tokens for type safety
|
|
419
|
+
type Token<T> = symbol & { __type: T };
|
|
420
|
+
|
|
421
|
+
function createToken<T>(description: string): Token<T> {
|
|
422
|
+
return Symbol(description) as Token<T>;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const TOKENS = {
|
|
426
|
+
Logger: createToken<Logger>("Logger"),
|
|
427
|
+
Database: createToken<Database>("Database"),
|
|
428
|
+
} as const;
|
|
429
|
+
|
|
430
|
+
class Container {
|
|
431
|
+
private bindings = new Map<symbol, unknown>();
|
|
432
|
+
|
|
433
|
+
bind<T>(token: Token<T>, factory: () => T): void {
|
|
434
|
+
this.bindings.set(token, factory);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
get<T>(token: Token<T>): T {
|
|
438
|
+
const factory = this.bindings.get(token) as (() => T) | undefined;
|
|
439
|
+
if (!factory) throw new Error(`No binding for ${token.toString()}`);
|
|
440
|
+
return factory();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Module Organization
|
|
446
|
+
|
|
447
|
+
### Feature-Based Structure
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
src/
|
|
451
|
+
├── features/
|
|
452
|
+
│ ├── users/
|
|
453
|
+
│ │ ├── index.ts # Public API
|
|
454
|
+
│ │ ├── types.ts # Feature types
|
|
455
|
+
│ │ ├── service.ts # Business logic
|
|
456
|
+
│ │ ├── repository.ts # Data access
|
|
457
|
+
│ │ └── validation.ts # Input validation
|
|
458
|
+
│ └── orders/
|
|
459
|
+
│ ├── index.ts
|
|
460
|
+
│ └── ...
|
|
461
|
+
├── shared/
|
|
462
|
+
│ ├── types.ts # Shared types
|
|
463
|
+
│ ├── errors.ts # Error classes
|
|
464
|
+
│ └── utils.ts # Pure utility functions
|
|
465
|
+
└── index.ts # App entry point
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Encapsulation via Index Exports
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
// features/users/index.ts — public API only
|
|
472
|
+
export { createUser, getUser, updateUser } from "./service.js";
|
|
473
|
+
export type { User, CreateUserInput } from "./types.js";
|
|
474
|
+
|
|
475
|
+
// Internal modules (service.ts, repository.ts) are NOT exported
|
|
476
|
+
// Other features import only from the index
|
|
477
|
+
```
|