@ranimontagna/agent-toolkit 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +282 -277
- package/docs/assets/install-plan.svg +29 -0
- package/docs/assets/install-skill-packages.svg +31 -0
- package/docs/assets/install-status.svg +32 -0
- package/package.json +10 -9
- package/setup-agent-toolkit.sh +1 -1
- package/skills/backend/fastify-best-practices/LICENSE +21 -0
- package/skills/backend/fastify-best-practices/NOTICE.md +11 -0
- package/skills/backend/fastify-best-practices/SKILL.md +75 -0
- package/skills/backend/fastify-best-practices/rules/authentication.md +521 -0
- package/skills/backend/fastify-best-practices/rules/configuration.md +217 -0
- package/skills/backend/fastify-best-practices/rules/content-type.md +387 -0
- package/skills/backend/fastify-best-practices/rules/cors-security.md +445 -0
- package/skills/backend/fastify-best-practices/rules/database.md +320 -0
- package/skills/backend/fastify-best-practices/rules/decorators.md +416 -0
- package/skills/backend/fastify-best-practices/rules/deployment.md +423 -0
- package/skills/backend/fastify-best-practices/rules/error-handling.md +412 -0
- package/skills/backend/fastify-best-practices/rules/hooks.md +464 -0
- package/skills/backend/fastify-best-practices/rules/http-proxy.md +247 -0
- package/skills/backend/fastify-best-practices/rules/logging.md +402 -0
- package/skills/backend/fastify-best-practices/rules/performance.md +425 -0
- package/skills/backend/fastify-best-practices/rules/plugins.md +320 -0
- package/skills/backend/fastify-best-practices/rules/routes.md +467 -0
- package/skills/backend/fastify-best-practices/rules/schemas.md +585 -0
- package/skills/backend/fastify-best-practices/rules/serialization.md +475 -0
- package/skills/backend/fastify-best-practices/rules/testing.md +536 -0
- package/skills/backend/fastify-best-practices/rules/typescript.md +458 -0
- package/skills/backend/fastify-best-practices/rules/websockets.md +421 -0
- package/skills/backend/fastify-best-practices/tile.json +11 -0
- package/skills/core/agent-toolkit-maintainer/SKILL.md +16 -14
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: websockets
|
|
3
|
+
description: WebSocket support in Fastify
|
|
4
|
+
metadata:
|
|
5
|
+
tags: websockets, realtime, ws, socket
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# WebSocket Support
|
|
9
|
+
|
|
10
|
+
## Using @fastify/websocket
|
|
11
|
+
|
|
12
|
+
Add WebSocket support to Fastify:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
import websocket from '@fastify/websocket';
|
|
17
|
+
|
|
18
|
+
const app = Fastify();
|
|
19
|
+
|
|
20
|
+
app.register(websocket);
|
|
21
|
+
|
|
22
|
+
app.get('/ws', { websocket: true }, (socket, request) => {
|
|
23
|
+
socket.on('message', (message) => {
|
|
24
|
+
const data = message.toString();
|
|
25
|
+
console.log('Received:', data);
|
|
26
|
+
|
|
27
|
+
// Echo back
|
|
28
|
+
socket.send(`Echo: ${data}`);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
socket.on('close', () => {
|
|
32
|
+
console.log('Client disconnected');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
socket.on('error', (error) => {
|
|
36
|
+
console.error('WebSocket error:', error);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await app.listen({ port: 3000 });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## WebSocket with Hooks
|
|
44
|
+
|
|
45
|
+
Use Fastify hooks with WebSocket routes:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
app.register(async function wsRoutes(fastify) {
|
|
49
|
+
// This hook runs before WebSocket upgrade
|
|
50
|
+
fastify.addHook('preValidation', async (request, reply) => {
|
|
51
|
+
const token = request.headers.authorization;
|
|
52
|
+
if (!token) {
|
|
53
|
+
reply.code(401).send({ error: 'Unauthorized' });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
request.user = await verifyToken(token);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
fastify.get('/ws', { websocket: true }, (socket, request) => {
|
|
60
|
+
console.log('Connected user:', request.user.id);
|
|
61
|
+
|
|
62
|
+
socket.on('message', (message) => {
|
|
63
|
+
// Handle authenticated messages
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Connection Options
|
|
70
|
+
|
|
71
|
+
Configure WebSocket server options:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
app.register(websocket, {
|
|
75
|
+
options: {
|
|
76
|
+
maxPayload: 1048576, // 1MB max message size
|
|
77
|
+
clientTracking: true,
|
|
78
|
+
perMessageDeflate: {
|
|
79
|
+
zlibDeflateOptions: {
|
|
80
|
+
chunkSize: 1024,
|
|
81
|
+
memLevel: 7,
|
|
82
|
+
level: 3,
|
|
83
|
+
},
|
|
84
|
+
zlibInflateOptions: {
|
|
85
|
+
chunkSize: 10 * 1024,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Broadcast to All Clients
|
|
93
|
+
|
|
94
|
+
Broadcast messages to connected clients:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const clients = new Set<WebSocket>();
|
|
98
|
+
|
|
99
|
+
app.get('/ws', { websocket: true }, (socket, request) => {
|
|
100
|
+
clients.add(socket);
|
|
101
|
+
|
|
102
|
+
socket.on('close', () => {
|
|
103
|
+
clients.delete(socket);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
socket.on('message', (message) => {
|
|
107
|
+
// Broadcast to all other clients
|
|
108
|
+
for (const client of clients) {
|
|
109
|
+
if (client !== socket && client.readyState === WebSocket.OPEN) {
|
|
110
|
+
client.send(message);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Broadcast from HTTP route
|
|
117
|
+
app.post('/broadcast', async (request) => {
|
|
118
|
+
const { message } = request.body;
|
|
119
|
+
|
|
120
|
+
for (const client of clients) {
|
|
121
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
122
|
+
client.send(JSON.stringify({ type: 'broadcast', message }));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { sent: clients.size };
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Rooms/Channels Pattern
|
|
131
|
+
|
|
132
|
+
Organize connections into rooms:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const rooms = new Map<string, Set<WebSocket>>();
|
|
136
|
+
|
|
137
|
+
function joinRoom(socket: WebSocket, roomId: string) {
|
|
138
|
+
if (!rooms.has(roomId)) {
|
|
139
|
+
rooms.set(roomId, new Set());
|
|
140
|
+
}
|
|
141
|
+
rooms.get(roomId)!.add(socket);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function leaveRoom(socket: WebSocket, roomId: string) {
|
|
145
|
+
rooms.get(roomId)?.delete(socket);
|
|
146
|
+
if (rooms.get(roomId)?.size === 0) {
|
|
147
|
+
rooms.delete(roomId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function broadcastToRoom(roomId: string, message: string, exclude?: WebSocket) {
|
|
152
|
+
const room = rooms.get(roomId);
|
|
153
|
+
if (!room) return;
|
|
154
|
+
|
|
155
|
+
for (const client of room) {
|
|
156
|
+
if (client !== exclude && client.readyState === WebSocket.OPEN) {
|
|
157
|
+
client.send(message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
app.get('/ws/:roomId', { websocket: true }, (socket, request) => {
|
|
163
|
+
const { roomId } = request.params as { roomId: string };
|
|
164
|
+
|
|
165
|
+
joinRoom(socket, roomId);
|
|
166
|
+
|
|
167
|
+
socket.on('message', (message) => {
|
|
168
|
+
broadcastToRoom(roomId, message.toString(), socket);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
socket.on('close', () => {
|
|
172
|
+
leaveRoom(socket, roomId);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Structured Message Protocol
|
|
178
|
+
|
|
179
|
+
Use JSON for structured messages:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
interface WSMessage {
|
|
183
|
+
type: string;
|
|
184
|
+
payload?: unknown;
|
|
185
|
+
id?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
app.get('/ws', { websocket: true }, (socket, request) => {
|
|
189
|
+
function send(message: WSMessage) {
|
|
190
|
+
socket.send(JSON.stringify(message));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
socket.on('message', (raw) => {
|
|
194
|
+
let message: WSMessage;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
message = JSON.parse(raw.toString());
|
|
198
|
+
} catch {
|
|
199
|
+
send({ type: 'error', payload: 'Invalid JSON' });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
switch (message.type) {
|
|
204
|
+
case 'ping':
|
|
205
|
+
send({ type: 'pong', id: message.id });
|
|
206
|
+
break;
|
|
207
|
+
|
|
208
|
+
case 'subscribe':
|
|
209
|
+
handleSubscribe(socket, message.payload);
|
|
210
|
+
send({ type: 'subscribed', payload: message.payload, id: message.id });
|
|
211
|
+
break;
|
|
212
|
+
|
|
213
|
+
case 'message':
|
|
214
|
+
handleMessage(socket, message.payload);
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
default:
|
|
218
|
+
send({ type: 'error', payload: 'Unknown message type' });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Heartbeat/Ping-Pong
|
|
225
|
+
|
|
226
|
+
Keep connections alive:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
const HEARTBEAT_INTERVAL = 30000;
|
|
230
|
+
const clients = new Map<WebSocket, { isAlive: boolean }>();
|
|
231
|
+
|
|
232
|
+
app.get('/ws', { websocket: true }, (socket, request) => {
|
|
233
|
+
clients.set(socket, { isAlive: true });
|
|
234
|
+
|
|
235
|
+
socket.on('pong', () => {
|
|
236
|
+
const client = clients.get(socket);
|
|
237
|
+
if (client) client.isAlive = true;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
socket.on('close', () => {
|
|
241
|
+
clients.delete(socket);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Heartbeat interval
|
|
246
|
+
setInterval(() => {
|
|
247
|
+
for (const [socket, state] of clients) {
|
|
248
|
+
if (!state.isAlive) {
|
|
249
|
+
socket.terminate();
|
|
250
|
+
clients.delete(socket);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
state.isAlive = false;
|
|
255
|
+
socket.ping();
|
|
256
|
+
}
|
|
257
|
+
}, HEARTBEAT_INTERVAL);
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Authentication
|
|
261
|
+
|
|
262
|
+
Authenticate WebSocket connections:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
app.get('/ws', {
|
|
266
|
+
websocket: true,
|
|
267
|
+
preValidation: async (request, reply) => {
|
|
268
|
+
// Authenticate via query parameter or header
|
|
269
|
+
const token = request.query.token || request.headers.authorization?.replace('Bearer ', '');
|
|
270
|
+
|
|
271
|
+
if (!token) {
|
|
272
|
+
reply.code(401).send({ error: 'Token required' });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
request.user = await verifyToken(token);
|
|
278
|
+
} catch {
|
|
279
|
+
reply.code(401).send({ error: 'Invalid token' });
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
}, (socket, request) => {
|
|
283
|
+
console.log('Authenticated user:', request.user);
|
|
284
|
+
|
|
285
|
+
socket.on('message', (message) => {
|
|
286
|
+
// Handle authenticated messages
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Error Handling
|
|
292
|
+
|
|
293
|
+
Handle WebSocket errors properly:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
app.get('/ws', { websocket: true }, (socket, request) => {
|
|
297
|
+
socket.on('error', (error) => {
|
|
298
|
+
request.log.error({ err: error }, 'WebSocket error');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
socket.on('message', async (raw) => {
|
|
302
|
+
try {
|
|
303
|
+
const message = JSON.parse(raw.toString());
|
|
304
|
+
const result = await processMessage(message);
|
|
305
|
+
socket.send(JSON.stringify({ success: true, result }));
|
|
306
|
+
} catch (error) {
|
|
307
|
+
request.log.error({ err: error }, 'Message processing error');
|
|
308
|
+
socket.send(JSON.stringify({
|
|
309
|
+
success: false,
|
|
310
|
+
error: error.message,
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Rate Limiting WebSocket Messages
|
|
318
|
+
|
|
319
|
+
Limit message frequency:
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
const rateLimits = new Map<WebSocket, { count: number; resetAt: number }>();
|
|
323
|
+
|
|
324
|
+
function checkRateLimit(socket: WebSocket, limit: number, window: number): boolean {
|
|
325
|
+
const now = Date.now();
|
|
326
|
+
let state = rateLimits.get(socket);
|
|
327
|
+
|
|
328
|
+
if (!state || now > state.resetAt) {
|
|
329
|
+
state = { count: 0, resetAt: now + window };
|
|
330
|
+
rateLimits.set(socket, state);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
state.count++;
|
|
334
|
+
|
|
335
|
+
if (state.count > limit) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
app.get('/ws', { websocket: true }, (socket, request) => {
|
|
343
|
+
socket.on('message', (message) => {
|
|
344
|
+
if (!checkRateLimit(socket, 100, 60000)) {
|
|
345
|
+
socket.send(JSON.stringify({ error: 'Rate limit exceeded' }));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Process message
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
socket.on('close', () => {
|
|
353
|
+
rateLimits.delete(socket);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Graceful Shutdown
|
|
359
|
+
|
|
360
|
+
Close WebSocket connections on shutdown:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import closeWithGrace from 'close-with-grace';
|
|
364
|
+
|
|
365
|
+
const connections = new Set<WebSocket>();
|
|
366
|
+
|
|
367
|
+
app.get('/ws', { websocket: true }, (socket, request) => {
|
|
368
|
+
connections.add(socket);
|
|
369
|
+
|
|
370
|
+
socket.on('close', () => {
|
|
371
|
+
connections.delete(socket);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
closeWithGrace({ delay: 5000 }, async ({ signal }) => {
|
|
376
|
+
// Notify clients
|
|
377
|
+
for (const socket of connections) {
|
|
378
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
379
|
+
socket.send(JSON.stringify({ type: 'shutdown', message: 'Server is shutting down' }));
|
|
380
|
+
socket.close(1001, 'Server shutdown');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
await app.close();
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Full-Duplex Stream Pattern
|
|
389
|
+
|
|
390
|
+
Use WebSocket for streaming data:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
app.get('/ws/stream', { websocket: true }, async (socket, request) => {
|
|
394
|
+
const stream = createDataStream();
|
|
395
|
+
|
|
396
|
+
stream.on('data', (data) => {
|
|
397
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
398
|
+
socket.send(JSON.stringify({ type: 'data', payload: data }));
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
stream.on('end', () => {
|
|
403
|
+
socket.send(JSON.stringify({ type: 'end' }));
|
|
404
|
+
socket.close();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
socket.on('message', (message) => {
|
|
408
|
+
const { type, payload } = JSON.parse(message.toString());
|
|
409
|
+
|
|
410
|
+
if (type === 'pause') {
|
|
411
|
+
stream.pause();
|
|
412
|
+
} else if (type === 'resume') {
|
|
413
|
+
stream.resume();
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
socket.on('close', () => {
|
|
418
|
+
stream.destroy();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcollina/fastify-best-practices",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"summary": "Guides development of Fastify Node.js backend servers and REST APIs using TypeScript or JavaScript. Use when building, configuring, or debugging a Fastify application — including defining routes, implementing plugins, setting up JSON Schema validation, handling errors, optimising performance, managing authentication, configuring CORS and security headers, integrating databases, working with WebSockets, and deploying to production. Covers the full Fastify request lifecycle (hooks, serialization, logging with Pino) and TypeScript integration via strip types. Trigger terms: Fastify, Node.js server, REST API, API routes, backend framework, fastify.config, server.ts, app.ts.",
|
|
6
|
+
"skills": {
|
|
7
|
+
"fastify-best-practices": {
|
|
8
|
+
"path": "SKILL.md"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -11,8 +11,9 @@ Use this skill when working in the Agent Toolkit repository.
|
|
|
11
11
|
|
|
12
12
|
- Keep the repository public-safe: no internal company names, private URLs, tokens, or credentials.
|
|
13
13
|
- Keep the installer focused on Claude Code, Codex CLI, OpenCode and Gemini CLI.
|
|
14
|
-
- Keep bundled skills organized by
|
|
15
|
-
-
|
|
14
|
+
- Keep bundled skills organized by package under `skills/<package>/<skill-name>/SKILL.md`.
|
|
15
|
+
- Prefer external installers for third-party skills, but third-party skills may be bundled when the user explicitly asks for vendoring and the upstream license allows copying.
|
|
16
|
+
- When bundling third-party skills, preserve the upstream license, add clear attribution, and document source ownership in README.
|
|
16
17
|
- Install skills flat into runtime skill directories using the skill directory name.
|
|
17
18
|
- Prefer configurable sources over hard-coded local paths.
|
|
18
19
|
- Keep external tool defaults pinned in `tools.lock.json`; do not reintroduce mutable defaults like `@latest`.
|
|
@@ -39,30 +40,31 @@ When changing `bin/agent-toolkit.ts`, `src/**/*.ts` or `setup-agent-toolkit.sh`:
|
|
|
39
40
|
|
|
40
41
|
## Skill Changes
|
|
41
42
|
|
|
42
|
-
When adding a
|
|
43
|
+
When adding a bundled skill:
|
|
43
44
|
|
|
44
|
-
1. Create `skills/<
|
|
45
|
+
1. Create `skills/<package>/<skill-name>/SKILL.md`.
|
|
45
46
|
2. Use concise YAML frontmatter with `name` and `description`.
|
|
46
47
|
3. Keep `SKILL.md` procedural and short.
|
|
47
48
|
4. Move large examples or detailed references into `references/`.
|
|
48
49
|
5. Do not add README or changelog files inside a skill directory.
|
|
49
50
|
|
|
50
|
-
When adding third-party skills, add a dedicated installer or
|
|
51
|
-
|
|
52
|
-
license
|
|
51
|
+
When adding third-party skills, either add a dedicated external installer or
|
|
52
|
+
bundle the skill only when explicitly requested and allowed by license. Preserve
|
|
53
|
+
the upstream license text, add attribution with source URL and commit, and
|
|
54
|
+
document the license/source in README.
|
|
53
55
|
|
|
54
56
|
## Verification
|
|
55
57
|
|
|
56
58
|
Run:
|
|
57
59
|
|
|
58
60
|
```bash
|
|
59
|
-
rtk
|
|
60
|
-
rtk
|
|
61
|
-
rtk
|
|
62
|
-
rtk
|
|
63
|
-
rtk
|
|
64
|
-
rtk
|
|
65
|
-
rtk
|
|
61
|
+
rtk pnpm run check
|
|
62
|
+
rtk pnpm run security
|
|
63
|
+
rtk pnpm run lint
|
|
64
|
+
rtk pnpm run build
|
|
65
|
+
rtk pnpm run typecheck
|
|
66
|
+
rtk pnpm run test:unit
|
|
67
|
+
rtk pnpm run test:integration
|
|
66
68
|
rtk rg -n "private-company-or-old-tooling-pattern" .
|
|
67
69
|
rtk graphify update .
|
|
68
70
|
```
|