@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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +730 -0
  3. package/dist/add.js +89 -0
  4. package/dist/cli.js +12 -0
  5. package/dist/init.js +20 -0
  6. package/dist/utils/patch.js +8 -0
  7. package/package.json +52 -0
  8. package/templates/init/.env.template +2 -0
  9. package/templates/init/GUIDE.md +543 -0
  10. package/templates/init/README.md +59 -0
  11. package/templates/init/jest.config.ts +19 -0
  12. package/templates/init/package.json +22 -0
  13. package/templates/init/src/lib/auth.test.ts +48 -0
  14. package/templates/init/src/lib/basic-auth.ts +6 -0
  15. package/templates/init/src/lib/bearer-token.ts +5 -0
  16. package/templates/init/src/lib/http.test.ts +197 -0
  17. package/templates/init/src/lib/http.ts +81 -0
  18. package/templates/init/src/lib/oauth.test.ts +61 -0
  19. package/templates/init/src/lib/oauth.ts +15 -0
  20. package/templates/init/src/lib/token.ts +5 -0
  21. package/templates/init/src/load-env.ts +4 -0
  22. package/templates/init/src/runbooks/example.ts +33 -0
  23. package/templates/init/src/suite.test.ts +94 -0
  24. package/templates/init/src/suite.ts +70 -0
  25. package/templates/init/src/types/http.ts +12 -0
  26. package/templates/init/src/types/oauth.ts +13 -0
  27. package/templates/init/src/types/suite.ts +37 -0
  28. package/templates/init/tsconfig.json +14 -0
  29. package/templates/init/tsconfig.test.json +8 -0
  30. package/templates/plugins/aws-dynamo/env.txt +2 -0
  31. package/templates/plugins/aws-dynamo/example.ts +75 -0
  32. package/templates/plugins/aws-dynamo/plugin.json +38 -0
  33. package/templates/plugins/aws-dynamo/service.test.ts +180 -0
  34. package/templates/plugins/aws-dynamo/service.ts +73 -0
  35. package/templates/plugins/aws-dynamo/types.ts +29 -0
  36. package/templates/plugins/aws-s3/env.txt +2 -0
  37. package/templates/plugins/aws-s3/example.ts +41 -0
  38. package/templates/plugins/aws-s3/plugin.json +38 -0
  39. package/templates/plugins/aws-s3/service.test.ts +150 -0
  40. package/templates/plugins/aws-s3/service.ts +43 -0
  41. package/templates/plugins/aws-s3/types.ts +28 -0
  42. package/templates/plugins/aws-sns/env.txt +2 -0
  43. package/templates/plugins/aws-sns/example.ts +18 -0
  44. package/templates/plugins/aws-sns/plugin.json +37 -0
  45. package/templates/plugins/aws-sns/service.test.ts +79 -0
  46. package/templates/plugins/aws-sns/service.ts +28 -0
  47. package/templates/plugins/aws-sns/types.ts +8 -0
  48. package/templates/plugins/aws-sqs/env.txt +2 -0
  49. package/templates/plugins/aws-sqs/example.ts +16 -0
  50. package/templates/plugins/aws-sqs/plugin.json +37 -0
  51. package/templates/plugins/aws-sqs/service.test.ts +63 -0
  52. package/templates/plugins/aws-sqs/service.ts +27 -0
  53. package/templates/plugins/aws-sqs/types.ts +8 -0
  54. package/templates/plugins/open-ai/env.txt +1 -0
  55. package/templates/plugins/open-ai/example.ts +27 -0
  56. package/templates/plugins/open-ai/plugin.json +36 -0
  57. package/templates/plugins/open-ai/service.test.ts +55 -0
  58. package/templates/plugins/open-ai/service.ts +26 -0
  59. package/templates/plugins/open-ai/types.ts +61 -0
  60. 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
+ ```