@jordanalec/dtk 1.0.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/LICENSE +21 -0
- package/README.md +730 -0
- package/dist/add.js +89 -0
- package/dist/cli.js +12 -0
- package/dist/init.js +20 -0
- package/dist/utils/patch.js +8 -0
- package/package.json +52 -0
- package/templates/init/.env.template +2 -0
- package/templates/init/GUIDE.md +543 -0
- package/templates/init/README.md +59 -0
- package/templates/init/jest.config.ts +19 -0
- package/templates/init/package.json +22 -0
- package/templates/init/src/lib/auth.test.ts +48 -0
- package/templates/init/src/lib/basic-auth.ts +6 -0
- package/templates/init/src/lib/bearer-token.ts +5 -0
- package/templates/init/src/lib/http.test.ts +197 -0
- package/templates/init/src/lib/http.ts +81 -0
- package/templates/init/src/lib/oauth.test.ts +61 -0
- package/templates/init/src/lib/oauth.ts +15 -0
- package/templates/init/src/lib/token.ts +5 -0
- package/templates/init/src/load-env.ts +4 -0
- package/templates/init/src/runbooks/example.ts +33 -0
- package/templates/init/src/suite.test.ts +94 -0
- package/templates/init/src/suite.ts +70 -0
- package/templates/init/src/types/http.ts +12 -0
- package/templates/init/src/types/oauth.ts +13 -0
- package/templates/init/src/types/suite.ts +37 -0
- package/templates/init/tsconfig.json +14 -0
- package/templates/init/tsconfig.test.json +8 -0
- package/templates/plugins/aws-dynamo/env.txt +2 -0
- package/templates/plugins/aws-dynamo/example.ts +75 -0
- package/templates/plugins/aws-dynamo/plugin.json +38 -0
- package/templates/plugins/aws-dynamo/service.test.ts +180 -0
- package/templates/plugins/aws-dynamo/service.ts +73 -0
- package/templates/plugins/aws-dynamo/types.ts +29 -0
- package/templates/plugins/aws-s3/env.txt +2 -0
- package/templates/plugins/aws-s3/example.ts +41 -0
- package/templates/plugins/aws-s3/plugin.json +38 -0
- package/templates/plugins/aws-s3/service.test.ts +150 -0
- package/templates/plugins/aws-s3/service.ts +43 -0
- package/templates/plugins/aws-s3/types.ts +28 -0
- package/templates/plugins/aws-sns/env.txt +2 -0
- package/templates/plugins/aws-sns/example.ts +18 -0
- package/templates/plugins/aws-sns/plugin.json +37 -0
- package/templates/plugins/aws-sns/service.test.ts +79 -0
- package/templates/plugins/aws-sns/service.ts +28 -0
- package/templates/plugins/aws-sns/types.ts +8 -0
- package/templates/plugins/aws-sqs/env.txt +2 -0
- package/templates/plugins/aws-sqs/example.ts +16 -0
- package/templates/plugins/aws-sqs/plugin.json +37 -0
- package/templates/plugins/aws-sqs/service.test.ts +63 -0
- package/templates/plugins/aws-sqs/service.ts +27 -0
- package/templates/plugins/aws-sqs/types.ts +8 -0
- package/templates/plugins/open-ai/env.txt +1 -0
- package/templates/plugins/open-ai/example.ts +27 -0
- package/templates/plugins/open-ai/plugin.json +36 -0
- package/templates/plugins/open-ai/service.test.ts +55 -0
- package/templates/plugins/open-ai/service.ts +26 -0
- package/templates/plugins/open-ai/types.ts +61 -0
- package/templates/plugins/tsconfig.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
# dtk
|
|
2
|
+
|
|
3
|
+
A CLI scaffolding tool for generating self-contained TypeScript runbook projects. Install dtk globally, scaffold a project, add service plugins, and write runbooks. Your generated project has no runtime dependency on dtk -- you own all the files.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Installation](#installation)
|
|
10
|
+
2. [dtk init](#dtk-init)
|
|
11
|
+
3. [dtk add](#dtk-add)
|
|
12
|
+
4. [Available plugins](#available-plugins)
|
|
13
|
+
5. [Writing runbooks](#writing-runbooks)
|
|
14
|
+
6. [Writing a custom service](#writing-a-custom-service)
|
|
15
|
+
7. [Creating a new plugin](#creating-a-new-plugin)
|
|
16
|
+
8. [Generated project structure](#generated-project-structure)
|
|
17
|
+
9. [dtk source structure](#dtk-source-structure)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @jordanalec/dtk
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**For contributors** — clone and link locally:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone <repo-url>
|
|
31
|
+
cd <repo>/dtk
|
|
32
|
+
npm install
|
|
33
|
+
npm run build
|
|
34
|
+
npm link
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Verify it works:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
dtk --help
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
To unlink:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm unlink -g dtk
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## dtk init
|
|
52
|
+
|
|
53
|
+
Scaffolds a new project in the current directory.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
mkdir my-project
|
|
57
|
+
cd my-project
|
|
58
|
+
dtk init
|
|
59
|
+
npm install
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run the included example runbook to verify the setup:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run runbook:example
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Expected output:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
login: torvalds
|
|
72
|
+
name: Linus Torvalds
|
|
73
|
+
[OK] fetch-github-user
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## dtk add
|
|
79
|
+
|
|
80
|
+
Adds a service plugin to your project. Run from inside your generated project directory.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
dtk add <plugin>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each plugin:
|
|
87
|
+
- Copies a service file into `src/services/`
|
|
88
|
+
- Copies a types file into `src/types/`
|
|
89
|
+
- Patches `src/suite.ts` to wire the service in (imports, config field, builder method, service instance)
|
|
90
|
+
- Patches `src/types/suite.ts` to add the service type shape to `StepContext`
|
|
91
|
+
- Appends required env vars to `.env.template`
|
|
92
|
+
- Creates an example runbook at `src/runbooks/<plugin>.ts`
|
|
93
|
+
- Adds a `runbook:<plugin>` script to `package.json`
|
|
94
|
+
- Runs `npm install` for any required dependencies automatically
|
|
95
|
+
|
|
96
|
+
Running `dtk add` on a plugin that has already been added is safe -- files and patches are not duplicated.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Available plugins
|
|
101
|
+
|
|
102
|
+
### aws-sqs
|
|
103
|
+
|
|
104
|
+
Sends messages to an AWS SQS queue.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
dtk add aws-sqs
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Env vars appended to `.env.template`:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
SQS_QUEUE_URL=
|
|
114
|
+
AWS_REGION=
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Usage:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
await suite()
|
|
121
|
+
.sqs({
|
|
122
|
+
queueUrl: process.env.SQS_QUEUE_URL!,
|
|
123
|
+
region: process.env.AWS_REGION!,
|
|
124
|
+
})
|
|
125
|
+
.step("send", async (ctx) => {
|
|
126
|
+
const result = await ctx.services.sqs.sendMessage("hello world", {
|
|
127
|
+
source: "my-runbook",
|
|
128
|
+
});
|
|
129
|
+
console.log("messageId:", result.messageId);
|
|
130
|
+
return result;
|
|
131
|
+
})
|
|
132
|
+
.run("throwOnError");
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
AWS credentials are resolved from the environment via the SDK default provider chain (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`).
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### aws-sns
|
|
140
|
+
|
|
141
|
+
Publishes messages to an AWS SNS topic.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
dtk add aws-sns
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Env vars appended to `.env.template`:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
SNS_TOPIC_ARN=
|
|
151
|
+
AWS_REGION=
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Usage:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
await suite()
|
|
158
|
+
.sns({
|
|
159
|
+
topicArn: process.env.SNS_TOPIC_ARN!,
|
|
160
|
+
region: process.env.AWS_REGION!,
|
|
161
|
+
})
|
|
162
|
+
.step("publish", async (ctx) => {
|
|
163
|
+
const result = await ctx.services.sns.publish(
|
|
164
|
+
"hello world",
|
|
165
|
+
"optional subject",
|
|
166
|
+
{ source: "my-runbook" }
|
|
167
|
+
);
|
|
168
|
+
console.log("messageId:", result.messageId);
|
|
169
|
+
return result;
|
|
170
|
+
})
|
|
171
|
+
.run("throwOnError");
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### aws-dynamo
|
|
177
|
+
|
|
178
|
+
Reads and writes items in AWS DynamoDB tables.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
dtk add aws-dynamo
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Env vars appended to `.env.template`:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
AWS_REGION=
|
|
188
|
+
DYNAMO_TABLE_NAME=
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Usage:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
await suite()
|
|
195
|
+
.dynamo({
|
|
196
|
+
region: process.env.AWS_REGION!,
|
|
197
|
+
})
|
|
198
|
+
.step("put", async (ctx) => {
|
|
199
|
+
return ctx.services.dynamo.putItem(process.env.DYNAMO_TABLE_NAME!, {
|
|
200
|
+
id: "user-123",
|
|
201
|
+
name: "John Doe",
|
|
202
|
+
});
|
|
203
|
+
})
|
|
204
|
+
.step("get", async (ctx) => {
|
|
205
|
+
return ctx.services.dynamo.getItem(process.env.DYNAMO_TABLE_NAME!, { id: "user-123" });
|
|
206
|
+
})
|
|
207
|
+
.step("update", async (ctx) => {
|
|
208
|
+
return ctx.services.dynamo.updateItem(
|
|
209
|
+
process.env.DYNAMO_TABLE_NAME!,
|
|
210
|
+
{ id: "user-123" },
|
|
211
|
+
{
|
|
212
|
+
UpdateExpression: "SET #n = :n",
|
|
213
|
+
ExpressionAttributeNames: { "#n": "name" },
|
|
214
|
+
ExpressionAttributeValues: { ":n": { S: "Jane Doe" } },
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
})
|
|
218
|
+
.step("query", async (ctx) => {
|
|
219
|
+
return ctx.services.dynamo.queryItems(process.env.DYNAMO_TABLE_NAME!, {
|
|
220
|
+
KeyConditionExpression: "#pk = :pk",
|
|
221
|
+
ExpressionAttributeNames: { "#pk": "id" },
|
|
222
|
+
ExpressionAttributeValues: { ":pk": { S: "user-123" } },
|
|
223
|
+
});
|
|
224
|
+
})
|
|
225
|
+
.step("scan", async (ctx) => {
|
|
226
|
+
return ctx.services.dynamo.scanItems(process.env.DYNAMO_TABLE_NAME!, { Limit: 10 });
|
|
227
|
+
})
|
|
228
|
+
.step("delete", async (ctx) => {
|
|
229
|
+
return ctx.services.dynamo.deleteItem(process.env.DYNAMO_TABLE_NAME!, { id: "user-123" });
|
|
230
|
+
})
|
|
231
|
+
.run("throwOnError");
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
AWS credentials are resolved from the environment via the SDK default provider chain (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`).
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### aws-s3
|
|
239
|
+
|
|
240
|
+
Uploads files, downloads files, and generates presigned URLs for AWS S3.
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
dtk add aws-s3
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Env vars appended to `.env.template`:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
AWS_REGION=
|
|
250
|
+
S3_BUCKET_NAME=
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Usage:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
await suite()
|
|
257
|
+
.s3({ region: process.env.AWS_REGION! })
|
|
258
|
+
.step("upload", async (ctx) => {
|
|
259
|
+
return ctx.services.s3.uploadFile(
|
|
260
|
+
process.env.S3_BUCKET_NAME!,
|
|
261
|
+
"uploads/example.txt",
|
|
262
|
+
"./example.txt",
|
|
263
|
+
{ contentType: "text/plain", metadata: { source: "my-runbook" } }
|
|
264
|
+
);
|
|
265
|
+
})
|
|
266
|
+
.step("presign", async (ctx) => {
|
|
267
|
+
const result = await ctx.services.s3.getPresignedUrl(
|
|
268
|
+
process.env.S3_BUCKET_NAME!,
|
|
269
|
+
"uploads/example.txt",
|
|
270
|
+
300
|
|
271
|
+
);
|
|
272
|
+
console.log("url:", result.url);
|
|
273
|
+
return result;
|
|
274
|
+
})
|
|
275
|
+
.step("download", async (ctx) => {
|
|
276
|
+
return ctx.services.s3.downloadFile(
|
|
277
|
+
process.env.S3_BUCKET_NAME!,
|
|
278
|
+
"uploads/example.txt",
|
|
279
|
+
"./downloaded.txt"
|
|
280
|
+
);
|
|
281
|
+
})
|
|
282
|
+
.run("throwOnError");
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
AWS credentials are resolved from the environment via the SDK default provider chain (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`).
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### open-ai
|
|
290
|
+
|
|
291
|
+
Lists models and sends responses via the OpenAI API.
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
dtk add open-ai
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Env vars appended to `.env.template`:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
OPENAI_API_KEY=
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Usage:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
await suite()
|
|
307
|
+
.openAi({ baseUrl: "https://api.openai.com" })
|
|
308
|
+
.step("list-models", async (ctx) => {
|
|
309
|
+
const token = `Bearer ${process.env.OPENAI_API_KEY!}`;
|
|
310
|
+
return ctx.services.openAi.listModels(token);
|
|
311
|
+
})
|
|
312
|
+
.step("send-response", async (ctx) => {
|
|
313
|
+
const token = `Bearer ${process.env.OPENAI_API_KEY!}`;
|
|
314
|
+
return ctx.services.openAi.response(token, "gpt-4o-mini", "text", "Say hello.");
|
|
315
|
+
})
|
|
316
|
+
.run("throwOnError");
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Writing runbooks
|
|
322
|
+
|
|
323
|
+
A runbook is a TypeScript file that uses the `suite()` builder to chain steps and run them in sequence.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import "../load-env.js";
|
|
327
|
+
import { suite } from "../suite.js";
|
|
328
|
+
|
|
329
|
+
await suite()
|
|
330
|
+
.step("step-one", async (ctx) => {
|
|
331
|
+
const data = await ctx.http.get<{ id: number }>("https://api.example.com/thing/1");
|
|
332
|
+
console.log("id:", data.id);
|
|
333
|
+
return data;
|
|
334
|
+
})
|
|
335
|
+
.step("step-two", async (ctx) => {
|
|
336
|
+
const prev = ctx.outputs["step-one"] as { id: number };
|
|
337
|
+
console.log("previous id:", prev.id);
|
|
338
|
+
})
|
|
339
|
+
.run("throwOnError");
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Add a script to `package.json` to run it:
|
|
343
|
+
|
|
344
|
+
```json
|
|
345
|
+
"runbook:my-runbook": "tsx src/runbooks/my-runbook.ts"
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
npm run runbook:my-runbook
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Run options
|
|
353
|
+
|
|
354
|
+
| Option | Behaviour |
|
|
355
|
+
|---|---|
|
|
356
|
+
| `"throwOnError"` | Stops on first failure and throws |
|
|
357
|
+
| `"stopOnError"` | Logs the failure and stops without throwing |
|
|
358
|
+
|
|
359
|
+
### Step context
|
|
360
|
+
|
|
361
|
+
Every step receives `ctx`:
|
|
362
|
+
|
|
363
|
+
| Property | Description |
|
|
364
|
+
|---|---|
|
|
365
|
+
| `ctx.outputs` | Return values from all previous steps, keyed by step name |
|
|
366
|
+
| `ctx.auth` | Auth helpers: `clientCredentials`, `basicAuth`, `bearerToken`, `getClaimValues` |
|
|
367
|
+
| `ctx.http` | Generic HTTP client: `get`, `post` |
|
|
368
|
+
| `ctx.services` | All wired service instances (populated by plugins or custom services) |
|
|
369
|
+
|
|
370
|
+
### Auth
|
|
371
|
+
|
|
372
|
+
**OAuth client credentials:**
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
.oauth({
|
|
376
|
+
clientId: process.env.CLIENT_ID!,
|
|
377
|
+
clientSecret: process.env.CLIENT_SECRET!,
|
|
378
|
+
tokenUrl: process.env.TOKEN_URL!,
|
|
379
|
+
scope: "openid",
|
|
380
|
+
})
|
|
381
|
+
.step("fetch", async (ctx) => {
|
|
382
|
+
const token = await ctx.auth.clientCredentials();
|
|
383
|
+
return ctx.http.get("https://api.example.com/data", {
|
|
384
|
+
headers: { Authorization: `Bearer ${token.access_token}` },
|
|
385
|
+
});
|
|
386
|
+
})
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**Basic auth:**
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
.basicAuth({ username: process.env.USER!, password: process.env.PASS! })
|
|
393
|
+
.step("fetch", async (ctx) => {
|
|
394
|
+
const header = await ctx.auth.basicAuth();
|
|
395
|
+
return ctx.http.get("https://api.example.com/data", {
|
|
396
|
+
headers: { Authorization: header },
|
|
397
|
+
});
|
|
398
|
+
})
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Bearer token:**
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
.bearerToken({ token: process.env.API_TOKEN!, prefix: "Bearer" })
|
|
405
|
+
.step("fetch", async (ctx) => {
|
|
406
|
+
const header = await ctx.auth.bearerToken();
|
|
407
|
+
return ctx.http.get("https://api.example.com/data", {
|
|
408
|
+
headers: { Authorization: header },
|
|
409
|
+
});
|
|
410
|
+
})
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**JWT claim extraction:**
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
const claims = ctx.auth.getClaimValues(token.access_token);
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Passing data between steps
|
|
420
|
+
|
|
421
|
+
Each step's return value is stored in `ctx.outputs` under the step name:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
.step("get-user", async (ctx) => {
|
|
425
|
+
return ctx.http.get<User>("https://api.example.com/user/1");
|
|
426
|
+
})
|
|
427
|
+
.step("use-user", async (ctx) => {
|
|
428
|
+
const user = ctx.outputs["get-user"] as User;
|
|
429
|
+
console.log(user.name);
|
|
430
|
+
})
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Writing a custom service
|
|
436
|
+
|
|
437
|
+
If there is no plugin for the service you need, wire one in manually across four files.
|
|
438
|
+
|
|
439
|
+
### 1. Create `src/services/my-service.ts`
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
import { httpGet, httpPost, httpPut, httpDelete } from "../lib/http.js";
|
|
443
|
+
|
|
444
|
+
export interface MyServiceConfig {
|
|
445
|
+
baseUrl: string;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function createMyService(config?: MyServiceConfig) {
|
|
449
|
+
return {
|
|
450
|
+
getItem: async (id: number): Promise<{ id: number; name: string }> => {
|
|
451
|
+
return httpGet(`${config!.baseUrl}/items/${id}`);
|
|
452
|
+
},
|
|
453
|
+
createItem: async (name: string): Promise<{ id: number }> => {
|
|
454
|
+
return httpPost(`${config!.baseUrl}/items`, { name });
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### 2. Add the type to `src/types/suite.ts`
|
|
461
|
+
|
|
462
|
+
Add your config interface and the service shape to `StepContext`, using the existing sentinel comments as your guide for placement:
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
export interface MyServiceConfig { baseUrl: string; }
|
|
466
|
+
|
|
467
|
+
// inside StepContext.services:
|
|
468
|
+
services: {
|
|
469
|
+
myService: {
|
|
470
|
+
getItem(id: number): Promise<{ id: number; name: string }>;
|
|
471
|
+
createItem(name: string): Promise<{ id: number }>;
|
|
472
|
+
};
|
|
473
|
+
// dtk:service-types <-- keep this, leave it in place
|
|
474
|
+
};
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### 3. Wire into `src/suite.ts`
|
|
478
|
+
|
|
479
|
+
Add the import near the top (before `// dtk:imports`):
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
import { createMyService } from "./services/my-service.js";
|
|
483
|
+
import type { MyServiceConfig } from "./services/my-service.js";
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Add a private field inside the class (before `// dtk:configs`):
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
private myServiceConfig?: MyServiceConfig;
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Add a builder method (before `// dtk:methods`):
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
myService(config: MyServiceConfig): this { this.myServiceConfig = config; return this; }
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Add the service instance in `buildContext` (before `// dtk:services`):
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
myService: createMyService(this.myServiceConfig),
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### 4. Use it in a runbook
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
await suite()
|
|
508
|
+
.myService({ baseUrl: "https://api.example.com" })
|
|
509
|
+
.step("get-item", async (ctx) => {
|
|
510
|
+
return ctx.services.myService.getItem(1);
|
|
511
|
+
})
|
|
512
|
+
.run("throwOnError");
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Creating a new plugin
|
|
518
|
+
|
|
519
|
+
A plugin is a directory inside `templates/plugins/` containing a manifest, service file, types file, env file, and example runbook. Once created, `dtk add <plugin>` handles everything else automatically.
|
|
520
|
+
|
|
521
|
+
### 1. Create the plugin directory
|
|
522
|
+
|
|
523
|
+
```
|
|
524
|
+
templates/plugins/my-plugin/
|
|
525
|
+
plugin.json
|
|
526
|
+
service.ts
|
|
527
|
+
types.ts
|
|
528
|
+
env.txt
|
|
529
|
+
example.ts
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### 2. Define types in `types.ts`
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
export interface MyPluginConfig {
|
|
536
|
+
baseUrl: string;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export interface MyPluginResult {
|
|
540
|
+
id: string;
|
|
541
|
+
status: string;
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### 3. Implement the service in `service.ts`
|
|
546
|
+
|
|
547
|
+
Import from the generated project's paths (e.g. `../lib/http.js`, `../types/my-plugin.js`):
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
import { httpGet, httpPost, httpPut, httpDelete } from "../lib/http.js";
|
|
551
|
+
import type { MyPluginConfig, MyPluginResult } from "../types/my-plugin.js";
|
|
552
|
+
|
|
553
|
+
export function createMyPluginService(config?: MyPluginConfig) {
|
|
554
|
+
return {
|
|
555
|
+
doThing: async (payload: string): Promise<MyPluginResult> => {
|
|
556
|
+
return httpPost(`${config!.baseUrl}/things`, { payload });
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### 4. List env vars in `env.txt`
|
|
563
|
+
|
|
564
|
+
One variable per line, no values:
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
MY_PLUGIN_BASE_URL=
|
|
568
|
+
MY_PLUGIN_API_KEY=
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### 5. Write an example runbook in `example.ts`
|
|
572
|
+
|
|
573
|
+
The example is copied to `src/runbooks/<plugin>.ts` in the user's project. Imports are relative to `src/runbooks/`:
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
import "../load-env.js";
|
|
577
|
+
import { suite } from "../suite.js";
|
|
578
|
+
|
|
579
|
+
await suite()
|
|
580
|
+
.myPlugin({
|
|
581
|
+
baseUrl: process.env.MY_PLUGIN_BASE_URL!,
|
|
582
|
+
})
|
|
583
|
+
.step("do-thing", async (ctx) => {
|
|
584
|
+
const result = await ctx.services.myPlugin.doThing("hello");
|
|
585
|
+
console.log("result:", result.status);
|
|
586
|
+
return result;
|
|
587
|
+
})
|
|
588
|
+
.run("throwOnError");
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### 6. Write `plugin.json`
|
|
592
|
+
|
|
593
|
+
The manifest drives everything `dtk add` does. The `patches` keys must exactly match the sentinel comment names in the generated `suite.ts` and `types/suite.ts`.
|
|
594
|
+
|
|
595
|
+
```json
|
|
596
|
+
{
|
|
597
|
+
"name": "my-plugin",
|
|
598
|
+
"description": "My plugin -- does a thing",
|
|
599
|
+
"dependencies": {
|
|
600
|
+
"some-sdk": "^1.0.0"
|
|
601
|
+
},
|
|
602
|
+
"files": [
|
|
603
|
+
{ "src": "service.ts", "dest": "src/services/my-plugin.ts" },
|
|
604
|
+
{ "src": "types.ts", "dest": "src/types/my-plugin.ts" }
|
|
605
|
+
],
|
|
606
|
+
"env": "env.txt",
|
|
607
|
+
"example": "example.ts",
|
|
608
|
+
"patches": {
|
|
609
|
+
"src/suite.ts": {
|
|
610
|
+
"imports": [
|
|
611
|
+
"import { createMyPluginService } from \"./services/my-plugin.js\";",
|
|
612
|
+
"import type { MyPluginConfig } from \"./types/my-plugin.js\";"
|
|
613
|
+
],
|
|
614
|
+
"configs": " private myPluginConfig?: MyPluginConfig;",
|
|
615
|
+
"methods": " myPlugin(config: MyPluginConfig): this { this.myPluginConfig = config; return this; }",
|
|
616
|
+
"services": " myPlugin: createMyPluginService(this.myPluginConfig),"
|
|
617
|
+
},
|
|
618
|
+
"src/types/suite.ts": {
|
|
619
|
+
"type-imports": "import type { MyPluginConfig, MyPluginResult } from \"./my-plugin.js\";",
|
|
620
|
+
"service-types": " myPlugin: { doThing(payload: string): Promise<MyPluginResult>; };"
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### 7. Register the plugin in `cli/add.ts`
|
|
627
|
+
|
|
628
|
+
Add your plugin key to `PLUGIN_MAP`:
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
const PLUGIN_MAP: Record<string, string> = {
|
|
632
|
+
'aws-sqs': 'aws-sqs',
|
|
633
|
+
'aws-sns': 'aws-sns',
|
|
634
|
+
'aws-dynamo': 'aws-dynamo',
|
|
635
|
+
'aws-s3': 'aws-s3',
|
|
636
|
+
'open-ai': 'open-ai',
|
|
637
|
+
'my-plugin': 'my-plugin', // add this
|
|
638
|
+
};
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### 8. Rebuild dtk
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
npm run build
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
`dtk add my-plugin` is now available.
|
|
648
|
+
|
|
649
|
+
### Sentinel reference
|
|
650
|
+
|
|
651
|
+
The `patches` keys in `plugin.json` correspond to these comments in the generated project:
|
|
652
|
+
|
|
653
|
+
| Sentinel | File | What gets injected |
|
|
654
|
+
|---|---|---|
|
|
655
|
+
| `imports` | `src/suite.ts` | Service and type imports |
|
|
656
|
+
| `configs` | `src/suite.ts` | Private config field on the class |
|
|
657
|
+
| `methods` | `src/suite.ts` | Builder method (e.g. `.myPlugin(config)`) |
|
|
658
|
+
| `services` | `src/suite.ts` | Service instance in `buildContext` |
|
|
659
|
+
| `type-imports` | `src/types/suite.ts` | Plugin type imports |
|
|
660
|
+
| `service-types` | `src/types/suite.ts` | Service shape on `StepContext.services` |
|
|
661
|
+
|
|
662
|
+
Each sentinel value can be a single string or an array of strings. Arrays inject multiple lines before the same sentinel, in order. Injection is idempotent -- if a line is already present it is not duplicated.
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## Generated project structure
|
|
667
|
+
|
|
668
|
+
This is the structure inside a project created by `dtk init` after running `dtk add aws-sqs`:
|
|
669
|
+
|
|
670
|
+
```
|
|
671
|
+
my-project/
|
|
672
|
+
src/
|
|
673
|
+
suite.ts # TestSuite builder and runner -- do not delete sentinel comments
|
|
674
|
+
load-env.ts # dotenv bootstrap -- import this first in every runbook
|
|
675
|
+
lib/
|
|
676
|
+
http.ts # httpGet / httpPost / httpPut / httpDelete (axios wrapper)
|
|
677
|
+
oauth.ts # client credentials OAuth flow
|
|
678
|
+
basic-auth.ts # base64 Basic auth header builder
|
|
679
|
+
bearer-token.ts # Bearer token header builder
|
|
680
|
+
token.ts # JWT claim decoder
|
|
681
|
+
types/
|
|
682
|
+
suite.ts # StepContext, SuiteRunOption string union, auth types -- do not delete sentinel comments
|
|
683
|
+
oauth.ts # OAuthConfig, TokenResponse
|
|
684
|
+
aws-sqs.ts # SqsConfig, SendMessageResult (added by plugin)
|
|
685
|
+
services/
|
|
686
|
+
sqs.ts # SQS service factory (added by plugin)
|
|
687
|
+
runbooks/
|
|
688
|
+
example.ts # starter runbook (GitHub API)
|
|
689
|
+
aws-sqs.ts # example runbook (added by plugin)
|
|
690
|
+
.env.template # env var list -- copy to .env and fill in values
|
|
691
|
+
tsconfig.json
|
|
692
|
+
package.json
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## dtk source structure
|
|
698
|
+
|
|
699
|
+
This section is for contributors and plugin authors working inside the dtk repo itself.
|
|
700
|
+
|
|
701
|
+
```
|
|
702
|
+
cli/
|
|
703
|
+
cli.ts # entry point -- registers init and add commands
|
|
704
|
+
init.ts # dtk init handler -- copies templates/init/src + root config files
|
|
705
|
+
add.ts # dtk add handler -- reads plugin.json, copies files, patches, installs
|
|
706
|
+
utils/
|
|
707
|
+
patch.ts # sentinel injection utility (injectAtSentinel)
|
|
708
|
+
templates/
|
|
709
|
+
init/ # scaffold copied by dtk init
|
|
710
|
+
src/ # the TypeScript source that lands in the user's src/
|
|
711
|
+
suite.ts
|
|
712
|
+
load-env.ts
|
|
713
|
+
lib/
|
|
714
|
+
types/
|
|
715
|
+
runbooks/
|
|
716
|
+
.env.template
|
|
717
|
+
tsconfig.json
|
|
718
|
+
package.json
|
|
719
|
+
plugins/ # one directory per plugin
|
|
720
|
+
aws-sqs/
|
|
721
|
+
plugin.json # manifest: files, patches, env, example, dependencies
|
|
722
|
+
service.ts # service factory (copied to src/services/)
|
|
723
|
+
types.ts # types (copied to src/types/)
|
|
724
|
+
env.txt # env var fragment (appended to .env.template)
|
|
725
|
+
example.ts # example runbook (copied to src/runbooks/)
|
|
726
|
+
aws-sns/
|
|
727
|
+
aws-dynamo/
|
|
728
|
+
aws-s3/
|
|
729
|
+
open-ai/
|
|
730
|
+
```
|