@travetto/llm-support 8.0.0-alpha.20

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 (84) hide show
  1. package/README.md +101 -0
  2. package/__index__.ts +11 -0
  3. package/llm/consumer/INSTRUCTIONS.md +44 -0
  4. package/llm/maintainer/INSTRUCTIONS.md +37 -0
  5. package/package.json +56 -0
  6. package/resources/snippets/autograph-cli-orchestration.md +21 -0
  7. package/resources/snippets/code/aws-lambda-package-and-deploy.yml.tpl +36 -0
  8. package/resources/snippets/code/cache-enhancements.config.ts.tpl +6 -0
  9. package/resources/snippets/code/cache-enhancements.service.ts.tpl +16 -0
  10. package/resources/snippets/code/create-web-interceptor.ts.tpl +15 -0
  11. package/resources/snippets/code/create-web-route.controller.ts.tpl +15 -0
  12. package/resources/snippets/code/create-web-route.service.ts.tpl +8 -0
  13. package/resources/snippets/code/email-config.ts.tpl +6 -0
  14. package/resources/snippets/code/email-context-schema.ts.tpl +7 -0
  15. package/resources/snippets/code/email-create-template.mustache.tpl +2 -0
  16. package/resources/snippets/code/email-fixture.json.tpl +6 -0
  17. package/resources/snippets/code/email-preview-test.ts.tpl +14 -0
  18. package/resources/snippets/code/email-render-pipeline.ts.tpl +12 -0
  19. package/resources/snippets/code/email-send-controller.ts.tpl +18 -0
  20. package/resources/snippets/code/email-transport-provider.ts.tpl +8 -0
  21. package/resources/snippets/code/enable-auth-session.config.ts.tpl +34 -0
  22. package/resources/snippets/code/enable-auth-session.controller.ts.tpl +30 -0
  23. package/resources/snippets/code/enable-file-upload.config.ts.tpl +6 -0
  24. package/resources/snippets/code/enable-file-upload.controller.ts.tpl +16 -0
  25. package/resources/snippets/code/enable-linting.package.json.tpl +13 -0
  26. package/resources/snippets/code/generate-config.app-config.ts.tpl +7 -0
  27. package/resources/snippets/code/generate-config.application.yml.tpl +3 -0
  28. package/resources/snippets/code/generate-config.local.yml.tpl +2 -0
  29. package/resources/snippets/code/generate-test-suite.fixture.json.tpl +4 -0
  30. package/resources/snippets/code/generate-test-suite.unit.ts.tpl +11 -0
  31. package/resources/snippets/code/model-indexed.indexes.ts.tpl +14 -0
  32. package/resources/snippets/code/model-indexed.model.ts.tpl +8 -0
  33. package/resources/snippets/code/model-indexed.service.ts.tpl +15 -0
  34. package/resources/snippets/code/model-query.service.ts.tpl +18 -0
  35. package/resources/snippets/code/openapi-client-generation.readme.tpl +13 -0
  36. package/resources/snippets/code/openapi-client-generation.yml.tpl +20 -0
  37. package/resources/snippets/code/openapi-spec-pipeline.yml.tpl +21 -0
  38. package/resources/snippets/code/pack-docker-release.yml.tpl +21 -0
  39. package/resources/snippets/code/project-bootstrap.application.yml.tpl +2 -0
  40. package/resources/snippets/code/project-bootstrap.home-controller.ts.tpl +15 -0
  41. package/resources/snippets/code/project-bootstrap.home-service.ts.tpl +8 -0
  42. package/resources/snippets/code/project-bootstrap.monorepo.package.json.tpl +12 -0
  43. package/resources/snippets/code/project-bootstrap.package.json.tpl +18 -0
  44. package/resources/snippets/code/repo-version-release.yml.tpl +25 -0
  45. package/resources/snippets/code/rest-rpc-client.client.ts.tpl +8 -0
  46. package/resources/snippets/code/rest-rpc-client.index.ts.tpl +1 -0
  47. package/resources/snippets/code/workflow-cloudfront-deploy.yml.tpl +18 -0
  48. package/resources/snippets/code/workflow-gcp-deploy.yml.tpl +18 -0
  49. package/resources/snippets/core-aws-lambda-package-and-deploy.md +21 -0
  50. package/resources/snippets/core-email-compiler-pattern.md +21 -0
  51. package/resources/snippets/core-email-module-contract.md +21 -0
  52. package/resources/snippets/core-email-nodemailer-provider.md +21 -0
  53. package/resources/snippets/core-eslint-ruleset.md +21 -0
  54. package/resources/snippets/core-openapi-client-generation.md +21 -0
  55. package/resources/snippets/core-openapi-spec-pipeline.md +21 -0
  56. package/resources/snippets/core-pack-docker-release.md +21 -0
  57. package/resources/snippets/core-repo-version-release.md +21 -0
  58. package/resources/snippets/core-upload-pattern.md +21 -0
  59. package/resources/snippets/core-web-controller-pattern.md +23 -0
  60. package/resources/snippets/core-work-pool-pattern.md +23 -0
  61. package/resources/snippets/embracinglife-cloudfront-deploy.md +21 -0
  62. package/resources/snippets/embracinglife-gcp-deploy.md +21 -0
  63. package/resources/snippets/recipe-auth-google-oauth.md +21 -0
  64. package/resources/snippets/recipe-indexed-model-pattern.md +21 -0
  65. package/resources/snippets/recipe-upload-presigned.md +21 -0
  66. package/resources/snippets/todo-app-test-pattern.md +21 -0
  67. package/src/consumer-docs.ts +23 -0
  68. package/src/execute.ts +624 -0
  69. package/src/install-guidance.ts +187 -0
  70. package/src/mcp.ts +170 -0
  71. package/src/plan.ts +110 -0
  72. package/src/recommendation.ts +306 -0
  73. package/src/snippet-catalog.ts +80 -0
  74. package/src/snippet-shapes.ts +14 -0
  75. package/src/template-shapes.ts +13 -0
  76. package/src/tooling.ts +197 -0
  77. package/src/types.ts +215 -0
  78. package/src/workflow-guidance.ts +220 -0
  79. package/support/base-command.ts +57 -0
  80. package/support/cli.llm_support_execute.ts +80 -0
  81. package/support/cli.llm_support_mcp.ts +62 -0
  82. package/support/cli.llm_support_plan.ts +30 -0
  83. package/support/cli.llm_support_recommend.ts +34 -0
  84. package/support/cli.llm_support_status.ts +30 -0
@@ -0,0 +1,23 @@
1
+ export const CONSUMER_LLM_DOCS = {
2
+ compatibilityNotes: [
3
+ {
4
+ version: '1.0',
5
+ transport: 'json-rpc-2.0-stdio',
6
+ supportedMethods: ['initialize', 'notifications/initialized', 'tools/list', 'tools/call'],
7
+ notes: [
8
+ 'Tool names are stable: llm_support_recommend, llm_support_plan, llm_support_execute.',
9
+ 'Execution defaults to dry-run unless apply is true.'
10
+ ]
11
+ }
12
+ ],
13
+ helperFlows: [
14
+ {
15
+ id: 'recommend-plan-execute',
16
+ helper: 'runLlmSupportFlow',
17
+ description: 'Single-call helper to run recommendation, planning, and execution with optional overrides.'
18
+ }
19
+ ]
20
+ } as const;
21
+
22
+ export const CONSUMER_LLM_DOC_COUNT =
23
+ CONSUMER_LLM_DOCS.compatibilityNotes.length + CONSUMER_LLM_DOCS.helperFlows.length;
package/src/execute.ts ADDED
@@ -0,0 +1,624 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { SchemaValidator } from '@travetto/schema';
5
+ import { FileLoader, JSONUtil, RuntimeResources } from '@travetto/runtime';
6
+
7
+ import type { ExecutionArtifact, ExecutionRequest, ExecutionResponse, PlanStepId } from './types.ts';
8
+ import { PackageJsonSchema, type PackageJsonShape } from './template-shapes.ts';
9
+
10
+ function isRecord(value: unknown): value is Record<string, unknown> {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+
14
+ function toStringMap(value: unknown): Record<string, string> {
15
+ if (!isRecord(value)) {
16
+ return {};
17
+ }
18
+ return Object.fromEntries(
19
+ Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === 'string')
20
+ );
21
+ }
22
+
23
+ function toOptionalString(value: unknown): string | undefined {
24
+ return typeof value === 'string' ? value : undefined;
25
+ }
26
+
27
+ async function validatePackageJsonShape(payload: unknown, source: string): Promise<PackageJsonShape> {
28
+ if (!isRecord(payload)) {
29
+ throw new Error(`Invalid package json shape for ${source}`);
30
+ }
31
+
32
+ const bound = PackageJsonSchema.from(payload);
33
+ await SchemaValidator.validate(PackageJsonSchema, bound);
34
+ return bound;
35
+ }
36
+
37
+ async function readSnippet(name: string): Promise<string> {
38
+ return RuntimeResources.readUTF8(`snippets/code/${name}`);
39
+ }
40
+
41
+ async function renderSnippet(name: string, params: Record<string, string> = {}): Promise<string> {
42
+ const source = await readSnippet(name);
43
+ return source.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, (_all, key: string) => params[key] ?? '');
44
+ }
45
+
46
+ function toClassName(input: string, fallback: string): string {
47
+ const cleaned = input.trim() || fallback;
48
+ return cleaned
49
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
50
+ .split(' ')
51
+ .filter(Boolean)
52
+ .map(part => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
53
+ .join('');
54
+ }
55
+
56
+ function toFileName(input: string, fallback: string): string {
57
+ const cleaned = input.trim() || fallback;
58
+ return cleaned
59
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
60
+ .replace(/[^a-zA-Z0-9]{1,30}/g, '-')
61
+ .replace(/^-{1,10}|-{1,10}$/g, '')
62
+ .toLowerCase();
63
+ }
64
+
65
+ function toPackageName(input: string, fallback: string): string {
66
+ const cleaned = input.trim() || fallback;
67
+ const normalized = cleaned
68
+ .toLowerCase()
69
+ .replace(/[^a-z0-9-]{1,30}/g, '-')
70
+ .replace(/^-{1,10}|-{1,10}$/g, '');
71
+
72
+ const safe = normalized.replace(/^[._-]{1,30}/, '');
73
+ return safe || fallback;
74
+ }
75
+
76
+ function toWorkspacePath(input: string | undefined, fallback = 'packages/app'): string {
77
+ const cleaned = (input ?? '').trim();
78
+ if (!cleaned) {
79
+ return fallback;
80
+ }
81
+
82
+ const segments = cleaned
83
+ .replace(/^\/{1,10}|\/{1,10}$/g, '')
84
+ .split('/')
85
+ .filter(Boolean)
86
+ .map((part, idx) => toPackageName(part, idx === 0 ? 'packages' : 'app'));
87
+
88
+ return segments.length ? segments.join('/') : fallback;
89
+ }
90
+
91
+ async function exists(file: string): Promise<boolean> {
92
+ try {
93
+ await fs.access(file);
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ async function writeFile(
101
+ operationId: string,
102
+ fullPath: string,
103
+ content: string,
104
+ request: ExecutionRequest,
105
+ artifacts: ExecutionArtifact[],
106
+ stepId: PlanStepId = 'generate-artifacts'
107
+ ): Promise<void> {
108
+ const present = await exists(fullPath);
109
+ if (present && !request.overwrite) {
110
+ artifacts.push({
111
+ operationId,
112
+ file: fullPath,
113
+ status: 'skipped',
114
+ stepId,
115
+ reason: 'File already exists. Use --overwrite to replace.'
116
+ });
117
+ return;
118
+ }
119
+
120
+ if (request.dryRun !== false) {
121
+ artifacts.push({ operationId, file: fullPath, status: 'planned', stepId });
122
+ return;
123
+ }
124
+
125
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
126
+ await fs.writeFile(fullPath, content, 'utf8');
127
+ artifacts.push({ operationId, file: fullPath, status: 'created', stepId });
128
+ }
129
+
130
+ type OperationFileSpec = {
131
+ file: string;
132
+ snippet: string;
133
+ params?: Record<string, string> | ((baseDir: string, request: ExecutionRequest) => Record<string, string>);
134
+ };
135
+
136
+ type OperationId = ExecutionRequest['operations'][number];
137
+
138
+ const STATIC_OPERATION_SPECS: Partial<Record<OperationId, OperationFileSpec[]>> = {
139
+ 'email-transport-provider': [
140
+ { file: 'src/email/provider.ts', snippet: 'email-transport-provider.ts.tpl' },
141
+ { file: 'src/config/email.ts', snippet: 'email-config.ts.tpl' }
142
+ ],
143
+ 'email-preview-snapshot': [
144
+ { file: 'test/email/preview.ts', snippet: 'email-preview-test.ts.tpl' }
145
+ ],
146
+ 'email-test-fixtures': [
147
+ { file: 'test/email/fixtures/transactional.json', snippet: 'email-fixture.json.tpl' }
148
+ ],
149
+ 'generate-test-suite': [
150
+ { file: 'test/unit/example.ts', snippet: 'generate-test-suite.unit.ts.tpl' },
151
+ { file: 'test/fixtures/example.json', snippet: 'generate-test-suite.fixture.json.tpl' }
152
+ ],
153
+ 'workflow-gcp-deploy': [
154
+ { file: '.github/workflows/deploy-api.yml', snippet: 'workflow-gcp-deploy.yml.tpl' }
155
+ ],
156
+ 'workflow-cloudfront-deploy': [
157
+ { file: '.github/workflows/deploy-ui.yml', snippet: 'workflow-cloudfront-deploy.yml.tpl' }
158
+ ],
159
+ 'create-web-interceptor': [
160
+ { file: 'src/interceptor/request-logging.ts', snippet: 'create-web-interceptor.ts.tpl' }
161
+ ],
162
+ 'cache-enhancements': [
163
+ { file: 'src/service/cacheable.ts', snippet: 'cache-enhancements.service.ts.tpl' },
164
+ { file: 'src/config/cache.ts', snippet: 'cache-enhancements.config.ts.tpl' }
165
+ ],
166
+ 'enable-file-upload': [
167
+ { file: 'src/web/upload.ts', snippet: 'enable-file-upload.controller.ts.tpl' },
168
+ { file: 'src/config/upload.ts', snippet: 'enable-file-upload.config.ts.tpl' }
169
+ ],
170
+ 'enable-auth-session': [
171
+ { file: 'src/web/auth.ts', snippet: 'enable-auth-session.controller.ts.tpl' },
172
+ { file: 'src/web/auth.config.ts', snippet: 'enable-auth-session.config.ts.tpl' }
173
+ ],
174
+ 'openapi-spec-pipeline': [
175
+ { file: '.github/workflows/openapi-spec.yml', snippet: 'openapi-spec-pipeline.yml.tpl' }
176
+ ],
177
+ 'openapi-client-generation': [
178
+ { file: '.github/workflows/openapi-client.yml', snippet: 'openapi-client-generation.yml.tpl' },
179
+ { file: 'src/client/README.md', snippet: 'openapi-client-generation.readme.tpl' }
180
+ ],
181
+ 'aws-lambda-package-and-deploy': [
182
+ { file: '.github/workflows/deploy-lambda.yml', snippet: 'aws-lambda-package-and-deploy.yml.tpl' }
183
+ ],
184
+ 'pack-docker-release': [
185
+ { file: '.github/workflows/docker-release.yml', snippet: 'pack-docker-release.yml.tpl' }
186
+ ],
187
+ 'repo-version-release': [
188
+ { file: '.github/workflows/repo-version-release.yml', snippet: 'repo-version-release.yml.tpl' }
189
+ ]
190
+ };
191
+
192
+ async function execFromSpecs(
193
+ operationId: string,
194
+ specs: OperationFileSpec[],
195
+ baseDir: string,
196
+ request: ExecutionRequest,
197
+ artifacts: ExecutionArtifact[]
198
+ ): Promise<void> {
199
+ for (const spec of specs) {
200
+ const params = typeof spec.params === 'function' ? spec.params(baseDir, request) : (spec.params ?? {});
201
+ await writeFile(
202
+ operationId,
203
+ path.join(baseDir, spec.file),
204
+ await renderSnippet(spec.snippet, params),
205
+ request,
206
+ artifacts
207
+ );
208
+ }
209
+ }
210
+
211
+ async function mergeLintPackageJson(
212
+ operationId: string,
213
+ baseDir: string,
214
+ relativePath: string,
215
+ fullPath: string,
216
+ lintTemplate: string,
217
+ request: ExecutionRequest,
218
+ artifacts: ExecutionArtifact[],
219
+ stepId: PlanStepId = 'generate-artifacts'
220
+ ): Promise<void> {
221
+ const present = await exists(fullPath);
222
+
223
+ if (request.dryRun !== false) {
224
+ artifacts.push({ operationId, file: fullPath, status: 'planned', stepId });
225
+ return;
226
+ }
227
+
228
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
229
+
230
+ if (!present) {
231
+ await fs.writeFile(fullPath, lintTemplate, 'utf8');
232
+ artifacts.push({ operationId, file: fullPath, status: 'created', stepId });
233
+ return;
234
+ }
235
+
236
+ const loader = new FileLoader([baseDir]);
237
+ const current = await validatePackageJsonShape(JSONUtil.fromUTF8(await loader.readUTF8(relativePath)), fullPath);
238
+ const incoming = await validatePackageJsonShape(JSONUtil.fromUTF8(lintTemplate), 'enable-linting.package.json.tpl');
239
+
240
+ const base = { ...current };
241
+
242
+ const currentScripts = toStringMap(current.scripts);
243
+ const incomingScripts = toStringMap(incoming.scripts);
244
+
245
+ const scripts = { ...currentScripts };
246
+ scripts['lint:register'] = incomingScripts['lint:register'] ?? 'trv eslint:register';
247
+ scripts.lint = scripts.lint ?? incomingScripts.lint ?? 'npm run lint:register && trv eslint';
248
+ scripts['lint:fix'] = scripts['lint:fix'] ?? incomingScripts['lint:fix'] ?? 'npm run lint:register && trv eslint --fix';
249
+
250
+ const devDependencies = { ...toStringMap(current.devDependencies) };
251
+ for (const [name, version] of Object.entries(toStringMap(incoming.devDependencies))) {
252
+ if (!devDependencies[name]) {
253
+ devDependencies[name] = version;
254
+ }
255
+ }
256
+
257
+ const merged = {
258
+ ...base,
259
+ type: toOptionalString(current.type) ?? toOptionalString(incoming.type),
260
+ scripts,
261
+ devDependencies
262
+ };
263
+
264
+ await fs.writeFile(fullPath, `${JSONUtil.toUTF8(merged, { indent: 2 })}\n`, 'utf8');
265
+ artifacts.push({ operationId, file: fullPath, status: 'created', stepId });
266
+ }
267
+
268
+ async function execProjectBootstrap(
269
+ baseDir: string,
270
+ request: ExecutionRequest,
271
+ artifacts: ExecutionArtifact[]
272
+ ): Promise<void> {
273
+ const projectName = request.projectName ?? path.basename(baseDir);
274
+ const packageName = toPackageName(projectName, 'travetto-app');
275
+ const monorepo = request.monorepo === true;
276
+ const workspacePath = monorepo ? toWorkspacePath(request.workspacePath) : '';
277
+ const workspaceName = monorepo ? toPackageName(request.workspaceName ?? `${packageName}-app`, `${packageName}-app`) : packageName;
278
+ const appDir = monorepo ? path.join(baseDir, workspacePath) : baseDir;
279
+
280
+ if (monorepo) {
281
+ await writeFile(
282
+ 'project-bootstrap',
283
+ path.join(baseDir, 'package.json'),
284
+ await renderSnippet('project-bootstrap.monorepo.package.json.tpl', {
285
+ projectName: packageName,
286
+ workspaceName
287
+ }),
288
+ request,
289
+ artifacts
290
+ );
291
+
292
+ await writeFile(
293
+ 'project-bootstrap',
294
+ path.join(appDir, 'package.json'),
295
+ await renderSnippet('project-bootstrap.package.json.tpl', { projectName: workspaceName }),
296
+ request,
297
+ artifacts
298
+ );
299
+ } else {
300
+ await writeFile(
301
+ 'project-bootstrap',
302
+ path.join(baseDir, 'package.json'),
303
+ await renderSnippet('project-bootstrap.package.json.tpl', { projectName: packageName }),
304
+ request,
305
+ artifacts
306
+ );
307
+ }
308
+
309
+ await writeFile(
310
+ 'project-bootstrap',
311
+ path.join(appDir, 'resources/application.yml'),
312
+ await renderSnippet('project-bootstrap.application.yml.tpl', { projectName }),
313
+ request,
314
+ artifacts
315
+ );
316
+
317
+ await writeFile(
318
+ 'project-bootstrap',
319
+ path.join(appDir, 'src/service/home.ts'),
320
+ await renderSnippet('project-bootstrap.home-service.ts.tpl'),
321
+ request,
322
+ artifacts
323
+ );
324
+
325
+ await writeFile(
326
+ 'project-bootstrap',
327
+ path.join(appDir, 'src/web/home.ts'),
328
+ await renderSnippet('project-bootstrap.home-controller.ts.tpl'),
329
+ request,
330
+ artifacts
331
+ );
332
+ }
333
+
334
+ async function execCreateWebRoute(
335
+ baseDir: string,
336
+ request: ExecutionRequest,
337
+ artifacts: ExecutionArtifact[]
338
+ ): Promise<void> {
339
+ const routePath = (request.routePath ?? 'sample').replace(/^\/+/, '');
340
+ const serviceName = toClassName(request.serviceName ?? `${routePath} service`, 'SampleService');
341
+ const controllerName = toClassName(request.controllerName ?? `${routePath} controller`, 'SampleController');
342
+
343
+ const serviceFile = toFileName(serviceName, 'sample-service').replace(/-service$/, '') || 'sample';
344
+ const controllerFile = toFileName(controllerName, 'sample-controller').replace(/-controller$/, '') || 'sample';
345
+
346
+ await writeFile(
347
+ 'create-web-route',
348
+ path.join(baseDir, `src/service/${serviceFile}.ts`),
349
+ await renderSnippet('create-web-route.service.ts.tpl', { serviceName }),
350
+ request,
351
+ artifacts
352
+ );
353
+
354
+ await writeFile(
355
+ 'create-web-route',
356
+ path.join(baseDir, `src/web/${controllerFile}.ts`),
357
+ await renderSnippet('create-web-route.controller.ts.tpl', {
358
+ serviceName,
359
+ serviceFile,
360
+ routePath,
361
+ controllerName
362
+ }),
363
+ request,
364
+ artifacts
365
+ );
366
+ }
367
+
368
+ async function execEmailCreateTemplate(
369
+ baseDir: string,
370
+ request: ExecutionRequest,
371
+ artifacts: ExecutionArtifact[]
372
+ ): Promise<void> {
373
+ const emailName = toFileName(request.emailName ?? 'transactional', 'transactional');
374
+ await writeFile(
375
+ 'email-create-template',
376
+ path.join(baseDir, `src/email/templates/${emailName}.mustache`),
377
+ await renderSnippet('email-create-template.mustache.tpl'),
378
+ request,
379
+ artifacts
380
+ );
381
+ }
382
+
383
+ async function execEmailContextSchema(
384
+ baseDir: string,
385
+ request: ExecutionRequest,
386
+ artifacts: ExecutionArtifact[]
387
+ ): Promise<void> {
388
+ const emailType = toClassName(request.emailName ?? 'transactional email', 'TransactionalEmail');
389
+ await writeFile(
390
+ 'email-context-schema',
391
+ path.join(baseDir, 'src/email/schema.ts'),
392
+ await renderSnippet('email-context-schema.ts.tpl', { emailType }),
393
+ request,
394
+ artifacts
395
+ );
396
+ }
397
+
398
+ async function execEmailRenderPipeline(
399
+ baseDir: string,
400
+ request: ExecutionRequest,
401
+ artifacts: ExecutionArtifact[]
402
+ ): Promise<void> {
403
+ const emailName = toFileName(request.emailName ?? 'transactional', 'transactional');
404
+ await writeFile(
405
+ 'email-render-pipeline',
406
+ path.join(baseDir, 'src/email/render.ts'),
407
+ await renderSnippet('email-render-pipeline.ts.tpl', {
408
+ emailName,
409
+ renderName: toClassName(emailName, 'Transactional')
410
+ }),
411
+ request,
412
+ artifacts
413
+ );
414
+ }
415
+
416
+ async function execEmailSendFlow(
417
+ baseDir: string,
418
+ request: ExecutionRequest,
419
+ artifacts: ExecutionArtifact[]
420
+ ): Promise<void> {
421
+ const routePath = (request.sendRoutePath ?? 'email/send').replace(/^\/+/, '');
422
+ await writeFile(
423
+ 'email-send-flow',
424
+ path.join(baseDir, 'src/web/email.ts'),
425
+ await renderSnippet('email-send-controller.ts.tpl', { routePath }),
426
+ request,
427
+ artifacts
428
+ );
429
+ }
430
+
431
+ async function execModelIndexedAssistant(
432
+ baseDir: string,
433
+ request: ExecutionRequest,
434
+ artifacts: ExecutionArtifact[]
435
+ ): Promise<void> {
436
+ const modelName = toClassName(request.modelName ?? 'sample item', 'SampleItem');
437
+ const modelFile = toFileName(modelName, 'sample-item');
438
+ const modelVar = `${modelFile.replace(/-([a-z])/g, (_, ch: string) => ch.toUpperCase())}`;
439
+
440
+ await writeFile(
441
+ 'model-indexed-assistant',
442
+ path.join(baseDir, `src/model/${modelFile}.ts`),
443
+ await renderSnippet('model-indexed.model.ts.tpl', { modelName }),
444
+ request,
445
+ artifacts
446
+ );
447
+
448
+ await writeFile(
449
+ 'model-indexed-assistant',
450
+ path.join(baseDir, `src/model/${modelFile}.indexes.ts`),
451
+ await renderSnippet('model-indexed.indexes.ts.tpl', {
452
+ modelName,
453
+ modelFile,
454
+ modelVar
455
+ }),
456
+ request,
457
+ artifacts
458
+ );
459
+
460
+ await writeFile(
461
+ 'model-indexed-assistant',
462
+ path.join(baseDir, `src/service/${modelFile}-indexed.ts`),
463
+ await renderSnippet('model-indexed.service.ts.tpl', {
464
+ modelName,
465
+ modelFile,
466
+ modelVar
467
+ }),
468
+ request,
469
+ artifacts
470
+ );
471
+ }
472
+
473
+ async function execModelQueryAssistant(
474
+ baseDir: string,
475
+ request: ExecutionRequest,
476
+ artifacts: ExecutionArtifact[]
477
+ ): Promise<void> {
478
+ const modelName = toClassName(request.modelName ?? 'sample item', 'SampleItem');
479
+ const modelFile = toFileName(modelName, 'sample-item');
480
+
481
+ await writeFile(
482
+ 'model-query-assistant',
483
+ path.join(baseDir, `src/service/${modelFile}-query.ts`),
484
+ await renderSnippet('model-query.service.ts.tpl', {
485
+ modelName,
486
+ modelFile
487
+ }),
488
+ request,
489
+ artifacts
490
+ );
491
+ }
492
+
493
+ async function execRestRpcClient(
494
+ baseDir: string,
495
+ request: ExecutionRequest,
496
+ artifacts: ExecutionArtifact[]
497
+ ): Promise<void> {
498
+ const routePath = (request.routePath ?? 'sample').replace(/^\/+/, '');
499
+ const clientName = toClassName(`${routePath} client`, 'SampleClient');
500
+ const clientFile = toFileName(clientName, 'sample-client').replace(/-client$/, '') || 'sample';
501
+
502
+ await writeFile(
503
+ 'rest-rpc-client',
504
+ path.join(baseDir, `src/client/${clientFile}.ts`),
505
+ await renderSnippet('rest-rpc-client.client.ts.tpl', {
506
+ routePath,
507
+ clientName
508
+ }),
509
+ request,
510
+ artifacts
511
+ );
512
+
513
+ await writeFile(
514
+ 'rest-rpc-client',
515
+ path.join(baseDir, 'src/client/index.ts'),
516
+ await renderSnippet('rest-rpc-client.index.ts.tpl', { clientName, clientFile }),
517
+ request,
518
+ artifacts
519
+ );
520
+ }
521
+
522
+ async function execGenerateConfig(
523
+ baseDir: string,
524
+ request: ExecutionRequest,
525
+ artifacts: ExecutionArtifact[]
526
+ ): Promise<void> {
527
+ await execFromSpecs('generate-config', [
528
+ { file: 'src/config/app.ts', snippet: 'generate-config.app-config.ts.tpl' },
529
+ {
530
+ file: 'resources/application.yml',
531
+ snippet: 'generate-config.application.yml.tpl',
532
+ params: (dir, input): Record<string, string> => ({ projectName: input.projectName ?? path.basename(dir) })
533
+ },
534
+ { file: 'resources/local.yml', snippet: 'generate-config.local.yml.tpl' }
535
+ ], baseDir, request, artifacts);
536
+ }
537
+
538
+ async function execEnableLinting(
539
+ baseDir: string,
540
+ request: ExecutionRequest,
541
+ artifacts: ExecutionArtifact[]
542
+ ): Promise<void> {
543
+ await mergeLintPackageJson(
544
+ 'enable-linting',
545
+ baseDir,
546
+ 'package.json',
547
+ path.join(baseDir, 'package.json'),
548
+ await renderSnippet('enable-linting.package.json.tpl'),
549
+ request,
550
+ artifacts
551
+ );
552
+ }
553
+
554
+ type OperationHandler = (
555
+ baseDir: string,
556
+ request: ExecutionRequest,
557
+ artifacts: ExecutionArtifact[]
558
+ ) => Promise<void>;
559
+
560
+ function createStaticHandler(operationId: OperationId): OperationHandler {
561
+ return async (baseDir: string, request: ExecutionRequest, artifacts: ExecutionArtifact[]): Promise<void> => {
562
+ await execFromSpecs(operationId, STATIC_OPERATION_SPECS[operationId] ?? [], baseDir, request, artifacts);
563
+ };
564
+ }
565
+
566
+ const OPERATION_HANDLERS: Partial<Record<ExecutionRequest['operations'][number], OperationHandler>> = {
567
+ 'project-bootstrap': execProjectBootstrap,
568
+ 'create-web-route': execCreateWebRoute,
569
+ 'email-create-template': execEmailCreateTemplate,
570
+ 'email-context-schema': execEmailContextSchema,
571
+ 'email-render-pipeline': execEmailRenderPipeline,
572
+ 'email-transport-provider': createStaticHandler('email-transport-provider'),
573
+ 'email-preview-snapshot': createStaticHandler('email-preview-snapshot'),
574
+ 'email-send-flow': execEmailSendFlow,
575
+ 'email-test-fixtures': createStaticHandler('email-test-fixtures'),
576
+ 'model-indexed-assistant': execModelIndexedAssistant,
577
+ 'model-query-assistant': execModelQueryAssistant,
578
+ 'rest-rpc-client': execRestRpcClient,
579
+ 'generate-config': execGenerateConfig,
580
+ 'generate-test-suite': createStaticHandler('generate-test-suite'),
581
+ 'workflow-gcp-deploy': createStaticHandler('workflow-gcp-deploy'),
582
+ 'workflow-cloudfront-deploy': createStaticHandler('workflow-cloudfront-deploy'),
583
+ 'create-web-interceptor': createStaticHandler('create-web-interceptor'),
584
+ 'cache-enhancements': createStaticHandler('cache-enhancements'),
585
+ 'enable-file-upload': createStaticHandler('enable-file-upload'),
586
+ 'enable-auth-session': createStaticHandler('enable-auth-session'),
587
+ 'enable-linting': execEnableLinting,
588
+ 'openapi-spec-pipeline': createStaticHandler('openapi-spec-pipeline'),
589
+ 'openapi-client-generation': createStaticHandler('openapi-client-generation'),
590
+ 'aws-lambda-package-and-deploy': createStaticHandler('aws-lambda-package-and-deploy'),
591
+ 'pack-docker-release': createStaticHandler('pack-docker-release'),
592
+ 'repo-version-release': createStaticHandler('repo-version-release')
593
+ };
594
+
595
+ export function getUnimplementedOperations(operationIds: string[]): string[] {
596
+ return operationIds.filter(id => !OPERATION_HANDLERS[id]);
597
+ }
598
+
599
+ export async function executeOperations(input: ExecutionRequest): Promise<ExecutionResponse> {
600
+ const request: ExecutionRequest = {
601
+ ...input,
602
+ dryRun: input.dryRun !== false
603
+ };
604
+
605
+ const targetDir = path.resolve(request.targetDir);
606
+ const artifacts: ExecutionArtifact[] = [];
607
+ const warnings: string[] = [];
608
+
609
+ for (const operationId of request.operations) {
610
+ const handler = OPERATION_HANDLERS[operationId];
611
+ if (handler) {
612
+ await handler(targetDir, request, artifacts);
613
+ } else {
614
+ warnings.push(`Operation ${operationId} has no executor yet.`);
615
+ }
616
+ }
617
+
618
+ return {
619
+ dryRun: request.dryRun !== false,
620
+ targetDir,
621
+ artifacts,
622
+ warnings
623
+ };
624
+ }