@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.
- package/README.md +101 -0
- package/__index__.ts +11 -0
- package/llm/consumer/INSTRUCTIONS.md +44 -0
- package/llm/maintainer/INSTRUCTIONS.md +37 -0
- package/package.json +56 -0
- package/resources/snippets/autograph-cli-orchestration.md +21 -0
- package/resources/snippets/code/aws-lambda-package-and-deploy.yml.tpl +36 -0
- package/resources/snippets/code/cache-enhancements.config.ts.tpl +6 -0
- package/resources/snippets/code/cache-enhancements.service.ts.tpl +16 -0
- package/resources/snippets/code/create-web-interceptor.ts.tpl +15 -0
- package/resources/snippets/code/create-web-route.controller.ts.tpl +15 -0
- package/resources/snippets/code/create-web-route.service.ts.tpl +8 -0
- package/resources/snippets/code/email-config.ts.tpl +6 -0
- package/resources/snippets/code/email-context-schema.ts.tpl +7 -0
- package/resources/snippets/code/email-create-template.mustache.tpl +2 -0
- package/resources/snippets/code/email-fixture.json.tpl +6 -0
- package/resources/snippets/code/email-preview-test.ts.tpl +14 -0
- package/resources/snippets/code/email-render-pipeline.ts.tpl +12 -0
- package/resources/snippets/code/email-send-controller.ts.tpl +18 -0
- package/resources/snippets/code/email-transport-provider.ts.tpl +8 -0
- package/resources/snippets/code/enable-auth-session.config.ts.tpl +34 -0
- package/resources/snippets/code/enable-auth-session.controller.ts.tpl +30 -0
- package/resources/snippets/code/enable-file-upload.config.ts.tpl +6 -0
- package/resources/snippets/code/enable-file-upload.controller.ts.tpl +16 -0
- package/resources/snippets/code/enable-linting.package.json.tpl +13 -0
- package/resources/snippets/code/generate-config.app-config.ts.tpl +7 -0
- package/resources/snippets/code/generate-config.application.yml.tpl +3 -0
- package/resources/snippets/code/generate-config.local.yml.tpl +2 -0
- package/resources/snippets/code/generate-test-suite.fixture.json.tpl +4 -0
- package/resources/snippets/code/generate-test-suite.unit.ts.tpl +11 -0
- package/resources/snippets/code/model-indexed.indexes.ts.tpl +14 -0
- package/resources/snippets/code/model-indexed.model.ts.tpl +8 -0
- package/resources/snippets/code/model-indexed.service.ts.tpl +15 -0
- package/resources/snippets/code/model-query.service.ts.tpl +18 -0
- package/resources/snippets/code/openapi-client-generation.readme.tpl +13 -0
- package/resources/snippets/code/openapi-client-generation.yml.tpl +20 -0
- package/resources/snippets/code/openapi-spec-pipeline.yml.tpl +21 -0
- package/resources/snippets/code/pack-docker-release.yml.tpl +21 -0
- package/resources/snippets/code/project-bootstrap.application.yml.tpl +2 -0
- package/resources/snippets/code/project-bootstrap.home-controller.ts.tpl +15 -0
- package/resources/snippets/code/project-bootstrap.home-service.ts.tpl +8 -0
- package/resources/snippets/code/project-bootstrap.monorepo.package.json.tpl +12 -0
- package/resources/snippets/code/project-bootstrap.package.json.tpl +18 -0
- package/resources/snippets/code/repo-version-release.yml.tpl +25 -0
- package/resources/snippets/code/rest-rpc-client.client.ts.tpl +8 -0
- package/resources/snippets/code/rest-rpc-client.index.ts.tpl +1 -0
- package/resources/snippets/code/workflow-cloudfront-deploy.yml.tpl +18 -0
- package/resources/snippets/code/workflow-gcp-deploy.yml.tpl +18 -0
- package/resources/snippets/core-aws-lambda-package-and-deploy.md +21 -0
- package/resources/snippets/core-email-compiler-pattern.md +21 -0
- package/resources/snippets/core-email-module-contract.md +21 -0
- package/resources/snippets/core-email-nodemailer-provider.md +21 -0
- package/resources/snippets/core-eslint-ruleset.md +21 -0
- package/resources/snippets/core-openapi-client-generation.md +21 -0
- package/resources/snippets/core-openapi-spec-pipeline.md +21 -0
- package/resources/snippets/core-pack-docker-release.md +21 -0
- package/resources/snippets/core-repo-version-release.md +21 -0
- package/resources/snippets/core-upload-pattern.md +21 -0
- package/resources/snippets/core-web-controller-pattern.md +23 -0
- package/resources/snippets/core-work-pool-pattern.md +23 -0
- package/resources/snippets/embracinglife-cloudfront-deploy.md +21 -0
- package/resources/snippets/embracinglife-gcp-deploy.md +21 -0
- package/resources/snippets/recipe-auth-google-oauth.md +21 -0
- package/resources/snippets/recipe-indexed-model-pattern.md +21 -0
- package/resources/snippets/recipe-upload-presigned.md +21 -0
- package/resources/snippets/todo-app-test-pattern.md +21 -0
- package/src/consumer-docs.ts +23 -0
- package/src/execute.ts +624 -0
- package/src/install-guidance.ts +187 -0
- package/src/mcp.ts +170 -0
- package/src/plan.ts +110 -0
- package/src/recommendation.ts +306 -0
- package/src/snippet-catalog.ts +80 -0
- package/src/snippet-shapes.ts +14 -0
- package/src/template-shapes.ts +13 -0
- package/src/tooling.ts +197 -0
- package/src/types.ts +215 -0
- package/src/workflow-guidance.ts +220 -0
- package/support/base-command.ts +57 -0
- package/support/cli.llm_support_execute.ts +80 -0
- package/support/cli.llm_support_mcp.ts +62 -0
- package/support/cli.llm_support_plan.ts +30 -0
- package/support/cli.llm_support_recommend.ts +34 -0
- 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
|
+
}
|