@azure-net/cli 4.0.0-0 → 4.1.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 (45) hide show
  1. package/bin/azure-net.js +73 -34
  2. package/bin/azure-net.ts +81 -34
  3. package/eslint.config.js +15 -0
  4. package/package.json +1 -1
  5. package/src/checks/checkDomain.js +201 -0
  6. package/src/checks/checkDomain.ts +223 -0
  7. package/src/checks/checkFoldersStructure.js +70 -0
  8. package/src/checks/checkFoldersStructure.ts +85 -0
  9. package/src/checks/checkImports.ts +166 -0
  10. package/src/checks/checkInternal.js +37 -0
  11. package/src/checks/checkInternal.ts +44 -0
  12. package/src/checks/checkLayerBoundaries.js +178 -0
  13. package/src/checks/checkLayerBoundaries.ts +216 -0
  14. package/src/checks/checkPresenterNames.js +202 -0
  15. package/src/checks/checkPresenterNames.ts +208 -0
  16. package/src/checks/checkProject.js +42 -0
  17. package/src/checks/checkProject.ts +45 -0
  18. package/src/checks/checkProviderGraph.js +221 -0
  19. package/src/checks/checkProviderGraph.ts +234 -0
  20. package/src/checks/checkProviderNames.js +158 -0
  21. package/src/checks/checkProviderNames.ts +165 -0
  22. package/src/checks/index.js +8 -0
  23. package/src/checks/index.ts +9 -0
  24. package/src/generate/layers/generateRepository.ts +123 -0
  25. package/src/generate/layers/generateRepositoryMethod.ts +102 -0
  26. package/src/generate/layers/generateUseCases.ts +50 -0
  27. package/src/generate/layers/repositoryShared.ts +657 -0
  28. package/src/generate/module/generateModulePreset.js +307 -0
  29. package/src/generate/module/generateModulePreset.ts +337 -0
  30. package/src/generate/module/generateSchemaFromType.js +46 -8
  31. package/src/generate/module/generateSchemaFromType.ts +52 -8
  32. package/src/generate/module/repositoryModuleShared.ts +665 -0
  33. package/src/utils/contextUtils.ts +89 -0
  34. package/src/utils/fileUtils.ts +100 -0
  35. package/src/utils/loadConfig.ts +69 -0
  36. package/src/commands/generate/generateCrudBase.js +0 -188
  37. package/src/commands/generate/generateCrudPresenter.js +0 -225
  38. package/src/commands/generate/generateModule.js +0 -31
  39. package/src/commands/generate/generateModuleBase.js +0 -130
  40. package/src/commands/generate/generateModulePresenter.js +0 -103
  41. package/src/commands/generate/generatePresenter.js +0 -80
  42. package/src/commands/generate/generateRepository.js +0 -70
  43. package/src/commands/generate/generateSchema.js +0 -100
  44. package/src/commands/generate/generateService.js +0 -66
  45. package/src/commands/generate/index.js +0 -9
@@ -0,0 +1,307 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import prompts from 'prompts';
4
+ import { toKebabCase, toPascalCase } from '../../utils/contextUtils.js';
5
+ import { updateIndexTs, writeIfNotExists } from '../../utils/fileUtils.js';
6
+ import generatePresenter from './generatePresenter.js';
7
+ import {
8
+ buildRepositoryContent,
9
+ createUseCasesForRepository,
10
+ getAvailableTsNames,
11
+ getConfigState,
12
+ getRepositoryMeta,
13
+ resolveContextAlias,
14
+ resolveCoreDatasourcesPath,
15
+ resolveDatasourcesPath,
16
+ resolveDomainRootPath,
17
+ resolveRepositoriesPath,
18
+ selectContext,
19
+ writeRepositoryInterface
20
+ } from './repositoryModuleShared.js';
21
+
22
+ const toInterfaceFileName = (interfaceName) => `${String(interfaceName ?? '').replace(/^I/, '') || 'Type'}.ts`;
23
+
24
+ const createInterfaceContent = (interfaceName) => `export interface ${interfaceName} {
25
+ \t[key: string]: unknown;
26
+ }
27
+ `;
28
+
29
+ const writeDomainIndexes = async (domainPath, modelPath, portsPath) => {
30
+ await updateIndexTs(modelPath);
31
+ await updateIndexTs(portsPath);
32
+ await fs.writeFile(path.join(domainPath, 'index.ts'), `export * from './model';\nexport * from './ports';\n`, 'utf-8');
33
+ };
34
+
35
+ const ensureDomainScaffold = async ({ contextName, entityNamePascal, domainName }) => {
36
+ const domainPath = resolveDomainRootPath(contextName, domainName);
37
+ const modelPath = path.join(domainPath, 'model');
38
+ const portsPath = path.join(domainPath, 'ports');
39
+
40
+ await fs.mkdir(modelPath, { recursive: true });
41
+ await fs.mkdir(portsPath, { recursive: true });
42
+
43
+ const modelInterfaces = [`I${entityNamePascal}`];
44
+ const portInterfaces = [
45
+ `I${entityNamePascal}CreateRequest`,
46
+ `I${entityNamePascal}UpdateRequest`,
47
+ `I${entityNamePascal}CollectionResponse`,
48
+ `I${entityNamePascal}CollectionQuery`
49
+ ];
50
+
51
+ for (const interfaceName of modelInterfaces) {
52
+ const filePath = path.join(modelPath, toInterfaceFileName(interfaceName));
53
+ // eslint-disable-next-line no-await-in-loop
54
+ await writeIfNotExists(filePath, createInterfaceContent(interfaceName));
55
+ }
56
+
57
+ for (const interfaceName of portInterfaces) {
58
+ const filePath = path.join(portsPath, toInterfaceFileName(interfaceName));
59
+ // eslint-disable-next-line no-await-in-loop
60
+ await writeIfNotExists(filePath, createInterfaceContent(interfaceName));
61
+ }
62
+
63
+ await writeDomainIndexes(domainPath, modelPath, portsPath);
64
+ return { domainPath, modelPath, portsPath };
65
+ };
66
+
67
+ const resolveDatasourceForPreset = async (contextName) => {
68
+ const { contexts, coreAlias } = await getConfigState();
69
+ const contextAlias = resolveContextAlias(contexts, contextName);
70
+
71
+ const coreDatasources = await getAvailableTsNames(resolveCoreDatasourcesPath());
72
+ const contextDatasources = await getAvailableTsNames(resolveDatasourcesPath(contextName));
73
+
74
+ if (coreDatasources.includes('ApiDatasource')) {
75
+ return { name: 'ApiDatasource', importPath: `${coreAlias}/datasource` };
76
+ }
77
+
78
+ if (contextDatasources.includes('ApiDatasource')) {
79
+ return { name: 'ApiDatasource', importPath: `${contextAlias}/layers/infrastructure/http/datasources` };
80
+ }
81
+
82
+ if (coreDatasources.length) {
83
+ return { name: coreDatasources[0], importPath: `${coreAlias}/datasource` };
84
+ }
85
+
86
+ if (contextDatasources.length) {
87
+ return { name: contextDatasources[0], importPath: `${contextAlias}/layers/infrastructure/http/datasources` };
88
+ }
89
+
90
+ return null;
91
+ };
92
+
93
+ const buildPresetMethods = ({ entityNamePascal, route, pathParamType }) => [
94
+ {
95
+ name: 'collection',
96
+ httpMethod: 'get',
97
+ route,
98
+ response: { kind: 'domain', type: `I${entityNamePascal}CollectionResponse`, layer: 'ports' },
99
+ queryType: { name: `I${entityNamePascal}CollectionQuery`, layer: 'ports' },
100
+ bodyType: null,
101
+ pathParamType: null
102
+ },
103
+ {
104
+ name: 'resource',
105
+ httpMethod: 'get',
106
+ route,
107
+ response: { kind: 'domain', type: `I${entityNamePascal}`, layer: 'model' },
108
+ queryType: null,
109
+ bodyType: null,
110
+ pathParamType
111
+ },
112
+ {
113
+ name: 'create',
114
+ httpMethod: 'post',
115
+ route,
116
+ response: { kind: 'domain', type: `I${entityNamePascal}`, layer: 'model' },
117
+ queryType: null,
118
+ bodyType: { name: `I${entityNamePascal}CreateRequest`, layer: 'ports' },
119
+ pathParamType: null
120
+ },
121
+ {
122
+ name: 'update',
123
+ httpMethod: 'put',
124
+ route,
125
+ response: { kind: 'domain', type: `I${entityNamePascal}`, layer: 'model' },
126
+ queryType: null,
127
+ bodyType: { name: `I${entityNamePascal}UpdateRequest`, layer: 'ports' },
128
+ pathParamType
129
+ },
130
+ {
131
+ name: 'destroy',
132
+ httpMethod: 'delete',
133
+ route,
134
+ response: { kind: 'primitive', type: 'never', layer: null },
135
+ queryType: null,
136
+ bodyType: null,
137
+ pathParamType
138
+ }
139
+ ];
140
+
141
+ const findProviderFile = async (contextName, layerName, preferredFileName) => {
142
+ const providersPath = path.join(process.cwd(), 'src', 'app', contextName, 'layers', layerName, 'providers');
143
+ const preferredPath = path.join(providersPath, preferredFileName);
144
+ try {
145
+ await fs.access(preferredPath);
146
+ return preferredPath;
147
+ } catch {
148
+ try {
149
+ const files = await fs.readdir(providersPath);
150
+ const firstTs = files.find((item) => item.endsWith('.ts') && item !== 'index.ts');
151
+ return firstTs ? path.join(providersPath, firstTs) : preferredPath;
152
+ } catch {
153
+ return preferredPath;
154
+ }
155
+ }
156
+ };
157
+
158
+ const parseProviderConstName = async (providerPath, fallbackName) => {
159
+ try {
160
+ const content = await fs.readFile(providerPath, 'utf-8');
161
+ const match = content.match(/export\s+const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*createBoundaryProvider/);
162
+ return match?.[1] ?? fallbackName;
163
+ } catch {
164
+ return fallbackName;
165
+ }
166
+ };
167
+
168
+ export default async function generateModulePreset() {
169
+ const contextName = await selectContext('Select context for preset module:');
170
+ if (!contextName) {
171
+ process.exitCode = 1;
172
+ return;
173
+ }
174
+
175
+ const { entityNameRaw } = await prompts({
176
+ type: 'text',
177
+ name: 'entityNameRaw',
178
+ message: 'Module name:',
179
+ validate: (value) => (String(value ?? '').trim().length > 0 ? true : 'Module name is required')
180
+ });
181
+
182
+ const { routeRaw } = await prompts({
183
+ type: 'text',
184
+ name: 'routeRaw',
185
+ message: 'Base request path (e.g. /users):',
186
+ initial: '/resource',
187
+ validate: (value) => {
188
+ const route = String(value ?? '').trim();
189
+ if (!route.length) return 'Path is required';
190
+ return route.startsWith('/') ? true : 'Path must start with "/"';
191
+ }
192
+ });
193
+
194
+ const { pathParamType } = await prompts({
195
+ type: 'select',
196
+ name: 'pathParamType',
197
+ message: 'Path param type:',
198
+ choices: [
199
+ { title: 'string', value: 'string' },
200
+ { title: 'number', value: 'number' }
201
+ ],
202
+ initial: 0
203
+ });
204
+
205
+ const entityNamePascal = toPascalCase(String(entityNameRaw ?? '').trim());
206
+ if (!entityNamePascal) {
207
+ console.error('❌ Invalid module name.');
208
+ process.exitCode = 1;
209
+ return;
210
+ }
211
+
212
+ const route = String(routeRaw ?? '').trim();
213
+ if (!route) {
214
+ console.error('❌ Invalid route.');
215
+ process.exitCode = 1;
216
+ return;
217
+ }
218
+
219
+ const resolvedPathParamType = pathParamType === 'number' ? 'number' : 'string';
220
+ const domainName = toKebabCase(entityNamePascal);
221
+
222
+ await ensureDomainScaffold({ contextName, entityNamePascal, domainName });
223
+
224
+ const datasource = await resolveDatasourceForPreset(contextName);
225
+ if (!datasource) {
226
+ console.error('❌ No datasource found in core or selected context. Create datasource first.');
227
+ process.exitCode = 1;
228
+ return;
229
+ }
230
+
231
+ const methods = buildPresetMethods({
232
+ entityNamePascal,
233
+ route,
234
+ pathParamType: resolvedPathParamType
235
+ });
236
+
237
+ const repositoryClassName = `${entityNamePascal}Repository`;
238
+ const meta = getRepositoryMeta(repositoryClassName);
239
+ const { contexts } = await getConfigState();
240
+ const contextAlias = resolveContextAlias(contexts, contextName);
241
+
242
+ await writeRepositoryInterface({
243
+ contextName,
244
+ domainName,
245
+ meta,
246
+ methods
247
+ });
248
+
249
+ const repositoryContent = buildRepositoryContent({
250
+ meta,
251
+ contextAlias,
252
+ domainName,
253
+ datasource,
254
+ methods
255
+ });
256
+
257
+ const repositoriesPath = resolveRepositoriesPath(contextName);
258
+ await fs.mkdir(repositoriesPath, { recursive: true });
259
+ const repositoryPath = path.join(repositoriesPath, `${meta.className}.ts`);
260
+ const createdRepository = await writeIfNotExists(repositoryPath, repositoryContent);
261
+ await updateIndexTs(repositoriesPath);
262
+
263
+ if (!createdRepository) {
264
+ console.error(`❌ Repository \"${meta.className}\" already exists. Preset flow stopped.`);
265
+ process.exitCode = 1;
266
+ return;
267
+ }
268
+
269
+ await createUseCasesForRepository({
270
+ contextName,
271
+ contextAlias,
272
+ domainName,
273
+ repositoryName: meta.className
274
+ });
275
+
276
+ const infrastructureProviderPath = await findProviderFile(contextName, 'infrastructure', 'InfrastructureProvider.ts');
277
+ const applicationProviderPath = await findProviderFile(contextName, 'application', 'ApplicationProvider.ts');
278
+ const infrastructureProviderName = await parseProviderConstName(infrastructureProviderPath, 'InfrastructureProvider');
279
+
280
+ console.log('\n⚠️ Manual provider registration is required before presenter generation.');
281
+ console.log(`Infrastructure provider file: ${infrastructureProviderPath}`);
282
+ console.log(`Application provider file: ${applicationProviderPath}`);
283
+ console.log('\nSuggested register snippets:');
284
+ console.log(`- Infrastructure register: ${meta.className}: () => new ${meta.className}(/* datasource */),`);
285
+ console.log(
286
+ `- Application register: ${meta.useCasesClassName}: () => new ${meta.useCasesClassName}(${infrastructureProviderName}().${meta.className}),`
287
+ );
288
+
289
+ const { readyAnswer } = await prompts({
290
+ type: 'text',
291
+ name: 'readyAnswer',
292
+ message: 'Type "y" when provider wiring is done:'
293
+ });
294
+
295
+ const normalizedReadyAnswer = String(readyAnswer ?? '')
296
+ .trim()
297
+ .toLowerCase();
298
+ if (normalizedReadyAnswer !== 'y') {
299
+ console.log('ℹ️ Preset module flow paused. Complete provider wiring and run `azure-net generate presenter`.');
300
+ return;
301
+ }
302
+
303
+ await generatePresenter({
304
+ contextName,
305
+ useCasesName: meta.useCasesClassName
306
+ });
307
+ }
@@ -0,0 +1,337 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import prompts from 'prompts';
4
+ import { toKebabCase, toPascalCase } from '../../utils/contextUtils.js';
5
+ import { updateIndexTs, writeIfNotExists } from '../../utils/fileUtils.js';
6
+ import generatePresenter from './generatePresenter.js';
7
+ import {
8
+ buildRepositoryContent,
9
+ createUseCasesForRepository,
10
+ getAvailableTsNames,
11
+ getConfigState,
12
+ getRepositoryMeta,
13
+ resolveContextAlias,
14
+ resolveCoreDatasourcesPath,
15
+ resolveDatasourcesPath,
16
+ resolveDomainRootPath,
17
+ resolveRepositoriesPath,
18
+ selectContext,
19
+ writeRepositoryInterface
20
+ } from './repositoryModuleShared.js';
21
+
22
+ type DatasourceDescriptor = { name: string; importPath: string };
23
+
24
+ type PresetMethod = {
25
+ name: string;
26
+ httpMethod: 'get' | 'post' | 'put' | 'delete';
27
+ route: string;
28
+ response: { kind: 'domain' | 'primitive'; type: string; layer: 'model' | 'ports' | null };
29
+ queryType: { name: string; layer: 'ports' | 'model' } | null;
30
+ bodyType: { name: string; layer: 'ports' | 'model' } | null;
31
+ pathParamType: 'string' | 'number' | null;
32
+ };
33
+
34
+ const toInterfaceFileName = (interfaceName: string): string => `${String(interfaceName ?? '').replace(/^I/, '') || 'Type'}.ts`;
35
+
36
+ const createInterfaceContent = (interfaceName: string): string => `export interface ${interfaceName} {
37
+ \t[key: string]: unknown;
38
+ }
39
+ `;
40
+
41
+ const writeDomainIndexes = async (domainPath: string, modelPath: string, portsPath: string): Promise<void> => {
42
+ await updateIndexTs(modelPath);
43
+ await updateIndexTs(portsPath);
44
+ await fs.writeFile(path.join(domainPath, 'index.ts'), `export * from './model';\nexport * from './ports';\n`, 'utf-8');
45
+ };
46
+
47
+ const ensureDomainScaffold = async ({
48
+ contextName,
49
+ entityNamePascal,
50
+ domainName
51
+ }: {
52
+ contextName: string;
53
+ entityNamePascal: string;
54
+ domainName: string;
55
+ }): Promise<{ domainPath: string; modelPath: string; portsPath: string }> => {
56
+ const domainPath = resolveDomainRootPath(contextName, domainName);
57
+ const modelPath = path.join(domainPath, 'model');
58
+ const portsPath = path.join(domainPath, 'ports');
59
+
60
+ await fs.mkdir(modelPath, { recursive: true });
61
+ await fs.mkdir(portsPath, { recursive: true });
62
+
63
+ const modelInterfaces = [`I${entityNamePascal}`];
64
+ const portInterfaces = [
65
+ `I${entityNamePascal}CreateRequest`,
66
+ `I${entityNamePascal}UpdateRequest`,
67
+ `I${entityNamePascal}CollectionResponse`,
68
+ `I${entityNamePascal}CollectionQuery`
69
+ ];
70
+
71
+ for (const interfaceName of modelInterfaces) {
72
+ const filePath = path.join(modelPath, toInterfaceFileName(interfaceName));
73
+ await writeIfNotExists(filePath, createInterfaceContent(interfaceName));
74
+ }
75
+
76
+ for (const interfaceName of portInterfaces) {
77
+ const filePath = path.join(portsPath, toInterfaceFileName(interfaceName));
78
+ await writeIfNotExists(filePath, createInterfaceContent(interfaceName));
79
+ }
80
+
81
+ await writeDomainIndexes(domainPath, modelPath, portsPath);
82
+ return { domainPath, modelPath, portsPath };
83
+ };
84
+
85
+ const resolveDatasourceForPreset = async (contextName: string): Promise<DatasourceDescriptor | null> => {
86
+ const { contexts, coreAlias } = await getConfigState();
87
+ const contextAlias = resolveContextAlias(contexts, contextName);
88
+
89
+ const coreDatasources = await getAvailableTsNames(resolveCoreDatasourcesPath());
90
+ const contextDatasources = await getAvailableTsNames(resolveDatasourcesPath(contextName));
91
+
92
+ if (coreDatasources.includes('ApiDatasource')) {
93
+ return { name: 'ApiDatasource', importPath: `${coreAlias}/datasource` };
94
+ }
95
+
96
+ if (contextDatasources.includes('ApiDatasource')) {
97
+ return { name: 'ApiDatasource', importPath: `${contextAlias}/layers/infrastructure/http/datasources` };
98
+ }
99
+
100
+ if (coreDatasources.length) {
101
+ return { name: coreDatasources[0] as string, importPath: `${coreAlias}/datasource` };
102
+ }
103
+
104
+ if (contextDatasources.length) {
105
+ return { name: contextDatasources[0] as string, importPath: `${contextAlias}/layers/infrastructure/http/datasources` };
106
+ }
107
+
108
+ return null;
109
+ };
110
+
111
+ const buildPresetMethods = ({
112
+ entityNamePascal,
113
+ route,
114
+ pathParamType
115
+ }: {
116
+ entityNamePascal: string;
117
+ route: string;
118
+ pathParamType: 'string' | 'number';
119
+ }): PresetMethod[] => [
120
+ {
121
+ name: 'collection',
122
+ httpMethod: 'get',
123
+ route,
124
+ response: { kind: 'domain', type: `I${entityNamePascal}CollectionResponse`, layer: 'ports' },
125
+ queryType: { name: `I${entityNamePascal}CollectionQuery`, layer: 'ports' },
126
+ bodyType: null,
127
+ pathParamType: null
128
+ },
129
+ {
130
+ name: 'resource',
131
+ httpMethod: 'get',
132
+ route,
133
+ response: { kind: 'domain', type: `I${entityNamePascal}`, layer: 'model' },
134
+ queryType: null,
135
+ bodyType: null,
136
+ pathParamType
137
+ },
138
+ {
139
+ name: 'create',
140
+ httpMethod: 'post',
141
+ route,
142
+ response: { kind: 'domain', type: `I${entityNamePascal}`, layer: 'model' },
143
+ queryType: null,
144
+ bodyType: { name: `I${entityNamePascal}CreateRequest`, layer: 'ports' },
145
+ pathParamType: null
146
+ },
147
+ {
148
+ name: 'update',
149
+ httpMethod: 'put',
150
+ route,
151
+ response: { kind: 'domain', type: `I${entityNamePascal}`, layer: 'model' },
152
+ queryType: null,
153
+ bodyType: { name: `I${entityNamePascal}UpdateRequest`, layer: 'ports' },
154
+ pathParamType
155
+ },
156
+ {
157
+ name: 'destroy',
158
+ httpMethod: 'delete',
159
+ route,
160
+ response: { kind: 'primitive', type: 'never', layer: null },
161
+ queryType: null,
162
+ bodyType: null,
163
+ pathParamType
164
+ }
165
+ ];
166
+
167
+ const findProviderFile = async (
168
+ contextName: string,
169
+ layerName: 'application' | 'infrastructure',
170
+ preferredFileName: string
171
+ ): Promise<string> => {
172
+ const providersPath = path.join(process.cwd(), 'src', 'app', contextName, 'layers', layerName, 'providers');
173
+ const preferredPath = path.join(providersPath, preferredFileName);
174
+ try {
175
+ await fs.access(preferredPath);
176
+ return preferredPath;
177
+ } catch {
178
+ try {
179
+ const files = await fs.readdir(providersPath);
180
+ const firstTs = files.find((item) => item.endsWith('.ts') && item !== 'index.ts');
181
+ return firstTs ? path.join(providersPath, firstTs) : preferredPath;
182
+ } catch {
183
+ return preferredPath;
184
+ }
185
+ }
186
+ };
187
+
188
+ const parseProviderConstName = async (providerPath: string, fallbackName: string): Promise<string> => {
189
+ try {
190
+ const content = await fs.readFile(providerPath, 'utf-8');
191
+ const match = content.match(/export\s+const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*createBoundaryProvider/);
192
+ return match?.[1] ?? fallbackName;
193
+ } catch {
194
+ return fallbackName;
195
+ }
196
+ };
197
+
198
+ export default async function generateModulePreset(): Promise<void> {
199
+ const contextName = await selectContext('Select context for preset module:');
200
+ if (!contextName) {
201
+ process.exitCode = 1;
202
+ return;
203
+ }
204
+
205
+ const { entityNameRaw } = await prompts({
206
+ type: 'text',
207
+ name: 'entityNameRaw',
208
+ message: 'Module name:',
209
+ validate: (value: string) => (String(value ?? '').trim().length > 0 ? true : 'Module name is required')
210
+ });
211
+
212
+ const { routeRaw } = await prompts({
213
+ type: 'text',
214
+ name: 'routeRaw',
215
+ message: 'Base request path (e.g. /users):',
216
+ initial: '/resource',
217
+ validate: (value: string) => {
218
+ const route = String(value ?? '').trim();
219
+ if (!route.length) return 'Path is required';
220
+ return route.startsWith('/') ? true : 'Path must start with "/"';
221
+ }
222
+ });
223
+
224
+ const { pathParamType } = await prompts({
225
+ type: 'select',
226
+ name: 'pathParamType',
227
+ message: 'Path param type:',
228
+ choices: [
229
+ { title: 'string', value: 'string' },
230
+ { title: 'number', value: 'number' }
231
+ ],
232
+ initial: 0
233
+ });
234
+
235
+ const entityNamePascal = toPascalCase(String(entityNameRaw ?? '').trim());
236
+ if (!entityNamePascal) {
237
+ console.error('❌ Invalid module name.');
238
+ process.exitCode = 1;
239
+ return;
240
+ }
241
+
242
+ const route = String(routeRaw ?? '').trim();
243
+ if (!route) {
244
+ console.error('❌ Invalid route.');
245
+ process.exitCode = 1;
246
+ return;
247
+ }
248
+
249
+ const resolvedPathParamType: 'string' | 'number' = pathParamType === 'number' ? 'number' : 'string';
250
+ const domainName = toKebabCase(entityNamePascal);
251
+
252
+ await ensureDomainScaffold({ contextName, entityNamePascal, domainName });
253
+
254
+ const datasource = await resolveDatasourceForPreset(contextName);
255
+ if (!datasource) {
256
+ console.error('❌ No datasource found in core or selected context. Create datasource first.');
257
+ process.exitCode = 1;
258
+ return;
259
+ }
260
+
261
+ const methods = buildPresetMethods({
262
+ entityNamePascal,
263
+ route,
264
+ pathParamType: resolvedPathParamType
265
+ });
266
+
267
+ const repositoryClassName = `${entityNamePascal}Repository`;
268
+ const meta = getRepositoryMeta(repositoryClassName);
269
+ const { contexts } = await getConfigState();
270
+ const contextAlias = resolveContextAlias(contexts, contextName);
271
+
272
+ await writeRepositoryInterface({
273
+ contextName,
274
+ domainName,
275
+ meta,
276
+ methods
277
+ });
278
+
279
+ const repositoryContent = buildRepositoryContent({
280
+ meta,
281
+ contextAlias,
282
+ domainName,
283
+ datasource,
284
+ methods
285
+ });
286
+
287
+ const repositoriesPath = resolveRepositoriesPath(contextName);
288
+ await fs.mkdir(repositoriesPath, { recursive: true });
289
+ const repositoryPath = path.join(repositoriesPath, `${meta.className}.ts`);
290
+ const createdRepository = await writeIfNotExists(repositoryPath, repositoryContent);
291
+ await updateIndexTs(repositoriesPath);
292
+
293
+ if (!createdRepository) {
294
+ console.error(`❌ Repository "${meta.className}" already exists. Preset flow stopped.`);
295
+ process.exitCode = 1;
296
+ return;
297
+ }
298
+
299
+ await createUseCasesForRepository({
300
+ contextName,
301
+ contextAlias,
302
+ domainName,
303
+ repositoryName: meta.className
304
+ });
305
+
306
+ const infrastructureProviderPath = await findProviderFile(contextName, 'infrastructure', 'InfrastructureProvider.ts');
307
+ const applicationProviderPath = await findProviderFile(contextName, 'application', 'ApplicationProvider.ts');
308
+ const infrastructureProviderName = await parseProviderConstName(infrastructureProviderPath, 'InfrastructureProvider');
309
+
310
+ console.log('\n⚠️ Manual provider registration is required before presenter generation.');
311
+ console.log(`Infrastructure provider file: ${infrastructureProviderPath}`);
312
+ console.log(`Application provider file: ${applicationProviderPath}`);
313
+ console.log('\nSuggested register snippets:');
314
+ console.log(`- Infrastructure register: ${meta.className}: () => new ${meta.className}(/* datasource */),`);
315
+ console.log(
316
+ `- Application register: ${meta.useCasesClassName}: () => new ${meta.useCasesClassName}(${infrastructureProviderName}().${meta.className}),`
317
+ );
318
+
319
+ const { readyAnswer } = await prompts({
320
+ type: 'text',
321
+ name: 'readyAnswer',
322
+ message: 'Type "y" when provider wiring is done:'
323
+ });
324
+
325
+ const normalizedReadyAnswer = String(readyAnswer ?? '')
326
+ .trim()
327
+ .toLowerCase();
328
+ if (normalizedReadyAnswer !== 'y') {
329
+ console.log('ℹ️ Preset module flow paused. Complete provider wiring and run `azure-net generate presenter`.');
330
+ return;
331
+ }
332
+
333
+ await generatePresenter({
334
+ contextName,
335
+ useCasesName: meta.useCasesClassName
336
+ });
337
+ }