@soapjs/cli 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 (149) hide show
  1. package/.nvmrc +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +360 -0
  4. package/build/cli.d.ts +3 -0
  5. package/build/cli.js +50 -0
  6. package/build/commands/add/add.command.d.ts +2 -0
  7. package/build/commands/add/add.command.js +709 -0
  8. package/build/commands/add/command-plan.d.ts +15 -0
  9. package/build/commands/add/command-plan.js +182 -0
  10. package/build/commands/add/entity-plan.d.ts +7 -0
  11. package/build/commands/add/entity-plan.js +106 -0
  12. package/build/commands/add/event-plan.d.ts +8 -0
  13. package/build/commands/add/event-plan.js +59 -0
  14. package/build/commands/add/query-plan.d.ts +10 -0
  15. package/build/commands/add/query-plan.js +156 -0
  16. package/build/commands/add/repository-plan.d.ts +11 -0
  17. package/build/commands/add/repository-plan.js +252 -0
  18. package/build/commands/add/resource-plan.d.ts +52 -0
  19. package/build/commands/add/resource-plan.js +2031 -0
  20. package/build/commands/add/route-plan.d.ts +24 -0
  21. package/build/commands/add/route-plan.js +256 -0
  22. package/build/commands/add/socket-plan.d.ts +9 -0
  23. package/build/commands/add/socket-plan.js +81 -0
  24. package/build/commands/add/use-case-plan.d.ts +7 -0
  25. package/build/commands/add/use-case-plan.js +86 -0
  26. package/build/commands/check/check.command.d.ts +2 -0
  27. package/build/commands/check/check.command.js +113 -0
  28. package/build/commands/create/create.command.d.ts +2 -0
  29. package/build/commands/create/create.command.js +234 -0
  30. package/build/commands/create/project-plan.d.ts +44 -0
  31. package/build/commands/create/project-plan.js +1430 -0
  32. package/build/commands/doctor/doctor.command.d.ts +2 -0
  33. package/build/commands/doctor/doctor.command.js +38 -0
  34. package/build/commands/generate/bruno-analysis.d.ts +19 -0
  35. package/build/commands/generate/bruno-analysis.js +51 -0
  36. package/build/commands/generate/bruno-plan.d.ts +6 -0
  37. package/build/commands/generate/bruno-plan.js +326 -0
  38. package/build/commands/generate/generate.command.d.ts +2 -0
  39. package/build/commands/generate/generate.command.js +130 -0
  40. package/build/commands/info/info.command.d.ts +2 -0
  41. package/build/commands/info/info.command.js +26 -0
  42. package/build/commands/remove/remove.command.d.ts +2 -0
  43. package/build/commands/remove/remove.command.js +328 -0
  44. package/build/commands/shared/common-options.d.ts +10 -0
  45. package/build/commands/shared/common-options.js +23 -0
  46. package/build/commands/update/update.command.d.ts +2 -0
  47. package/build/commands/update/update.command.js +155 -0
  48. package/build/config/auth-policy.d.ts +4 -0
  49. package/build/config/auth-policy.js +54 -0
  50. package/build/config/find-soap-root.d.ts +1 -0
  51. package/build/config/find-soap-root.js +22 -0
  52. package/build/config/load-soap-config.d.ts +2 -0
  53. package/build/config/load-soap-config.js +30 -0
  54. package/build/config/schemas/types.d.ts +127 -0
  55. package/build/config/schemas/types.js +2 -0
  56. package/build/config/schemas/validation.d.ts +5 -0
  57. package/build/config/schemas/validation.js +130 -0
  58. package/build/config/soap-config.service.d.ts +4 -0
  59. package/build/config/soap-config.service.js +24 -0
  60. package/build/config/write-soap-config.d.ts +8 -0
  61. package/build/config/write-soap-config.js +25 -0
  62. package/build/core/command-context.d.ts +20 -0
  63. package/build/core/command-context.js +30 -0
  64. package/build/core/errors.d.ts +6 -0
  65. package/build/core/errors.js +23 -0
  66. package/build/core/output.d.ts +12 -0
  67. package/build/core/output.js +30 -0
  68. package/build/core/result.d.ts +9 -0
  69. package/build/core/result.js +11 -0
  70. package/build/dependencies/dependency-resolver.d.ts +6 -0
  71. package/build/dependencies/dependency-resolver.js +68 -0
  72. package/build/dependencies/package-manager.d.ts +7 -0
  73. package/build/dependencies/package-manager.js +54 -0
  74. package/build/index.d.ts +2 -0
  75. package/build/index.js +9 -0
  76. package/build/io/conflict-policy.d.ts +10 -0
  77. package/build/io/conflict-policy.js +32 -0
  78. package/build/io/file-writer.d.ts +19 -0
  79. package/build/io/file-writer.js +65 -0
  80. package/build/io/format-file.d.ts +1 -0
  81. package/build/io/format-file.js +13 -0
  82. package/build/presets/create-presets.d.ts +4 -0
  83. package/build/presets/create-presets.js +97 -0
  84. package/build/presets/index.d.ts +2 -0
  85. package/build/presets/index.js +18 -0
  86. package/build/presets/preset.types.d.ts +6 -0
  87. package/build/presets/preset.types.js +2 -0
  88. package/build/prompts/add-resource.prompt.d.ts +13 -0
  89. package/build/prompts/add-resource.prompt.js +80 -0
  90. package/build/prompts/add-route.prompt.d.ts +16 -0
  91. package/build/prompts/add-route.prompt.js +140 -0
  92. package/build/prompts/create-project.prompt.d.ts +11 -0
  93. package/build/prompts/create-project.prompt.js +156 -0
  94. package/build/prompts/generate-bruno.prompt.d.ts +7 -0
  95. package/build/prompts/generate-bruno.prompt.js +21 -0
  96. package/build/prompts/index.d.ts +8 -0
  97. package/build/prompts/index.js +24 -0
  98. package/build/prompts/inquirer-prompt-adapter.d.ts +8 -0
  99. package/build/prompts/inquirer-prompt-adapter.js +52 -0
  100. package/build/prompts/mock-prompt-adapter.d.ts +13 -0
  101. package/build/prompts/mock-prompt-adapter.js +60 -0
  102. package/build/prompts/prompt-adapter.d.ts +7 -0
  103. package/build/prompts/prompt-adapter.js +2 -0
  104. package/build/prompts/prompt.types.d.ts +26 -0
  105. package/build/prompts/prompt.types.js +2 -0
  106. package/build/registry/registry.service.d.ts +19 -0
  107. package/build/registry/registry.service.js +68 -0
  108. package/build/resolvers/add-resource.resolver.d.ts +23 -0
  109. package/build/resolvers/add-resource.resolver.js +73 -0
  110. package/build/resolvers/add-route.resolver.d.ts +34 -0
  111. package/build/resolvers/add-route.resolver.js +83 -0
  112. package/build/resolvers/create-config.resolver.d.ts +32 -0
  113. package/build/resolvers/create-config.resolver.js +57 -0
  114. package/build/resolvers/generate-bruno.resolver.d.ts +17 -0
  115. package/build/resolvers/generate-bruno.resolver.js +23 -0
  116. package/build/resolvers/index.d.ts +5 -0
  117. package/build/resolvers/index.js +21 -0
  118. package/build/resolvers/resolver.types.d.ts +8 -0
  119. package/build/resolvers/resolver.types.js +2 -0
  120. package/build/summary/create-summary.d.ts +2 -0
  121. package/build/summary/create-summary.js +24 -0
  122. package/build/summary/index.d.ts +1 -0
  123. package/build/summary/index.js +17 -0
  124. package/build/templates/naming.d.ts +11 -0
  125. package/build/templates/naming.js +30 -0
  126. package/build/templates/template-context.d.ts +6 -0
  127. package/build/templates/template-context.js +2 -0
  128. package/build/templates/template-engine.d.ts +1 -0
  129. package/build/templates/template-engine.js +10 -0
  130. package/build/templates/template-resolver.d.ts +2 -0
  131. package/build/templates/template-resolver.js +17 -0
  132. package/build/terminal/terminal-capabilities.d.ts +6 -0
  133. package/build/terminal/terminal-capabilities.js +14 -0
  134. package/docs/adr/0001-soap-cli-project-aware-generator.md +108 -0
  135. package/docs/cli/add-resource.md +127 -0
  136. package/docs/cli/add-route.md +79 -0
  137. package/docs/cli/bruno.md +58 -0
  138. package/docs/cli/create.md +73 -0
  139. package/docs/cli/index.md +92 -0
  140. package/docs/cli/interactive-mode.md +61 -0
  141. package/docs/cli/remove.md +45 -0
  142. package/docs/guides/auth.md +90 -0
  143. package/docs/guides/cqrs-events-realtime.md +100 -0
  144. package/docs/guides/index.md +24 -0
  145. package/docs/guides/quality-and-safety.md +88 -0
  146. package/docs/guides/regular-api.md +119 -0
  147. package/docs/guides/storage.md +101 -0
  148. package/docs/plans/interactive-mode-plan.md +601 -0
  149. package/package.json +44 -0
@@ -0,0 +1,709 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAddCommand = void 0;
4
+ const command_context_1 = require("../../core/command-context");
5
+ const errors_1 = require("../../core/errors");
6
+ const load_soap_config_1 = require("../../config/load-soap-config");
7
+ const write_soap_config_1 = require("../../config/write-soap-config");
8
+ const file_writer_1 = require("../../io/file-writer");
9
+ const naming_1 = require("../../templates/naming");
10
+ const auth_policy_1 = require("../../config/auth-policy");
11
+ const bruno_plan_1 = require("../generate/bruno-plan");
12
+ const command_plan_1 = require("./command-plan");
13
+ const entity_plan_1 = require("./entity-plan");
14
+ const event_plan_1 = require("./event-plan");
15
+ const query_plan_1 = require("./query-plan");
16
+ const repository_plan_1 = require("./repository-plan");
17
+ const resource_plan_1 = require("./resource-plan");
18
+ const route_plan_1 = require("./route-plan");
19
+ const socket_plan_1 = require("./socket-plan");
20
+ const use_case_plan_1 = require("./use-case-plan");
21
+ const common_options_1 = require("../shared/common-options");
22
+ const add_resource_resolver_1 = require("../../resolvers/add-resource.resolver");
23
+ const add_route_resolver_1 = require("../../resolvers/add-route.resolver");
24
+ const prompts_1 = require("../../prompts");
25
+ function registerAddCommand(program) {
26
+ const add = program.command("add").description("Add resources, routes, and project components.");
27
+ (0, common_options_1.addConflictOption)((0, common_options_1.addInteractiveOption)(add
28
+ .command("resource <name>")
29
+ .description("Add a resource to an existing SoapJS project.")
30
+ .option("--crud", "generate CRUD route placeholders", false)
31
+ .option("--db <database>", "resource storage target: none, mongo, postgres, mysql, sqlite, redis")
32
+ .option("--auth <auth>", "resource auth strategy: none, jwt, api-key, local")
33
+ .option("--zone <zone>", "API zone: public, private, admin", "public")
34
+ .option("--policy <policy>", "auth policy: admin, roles:a,b, custom:name, none")
35
+ .option("--field <field>", "resource field metadata as name:type or name:type:optional", collect, [])
36
+ .option("--crud-route <route>", "CRUD route override as operation:method:path[:auth][:zone][:bruno|no-bruno]", collect, [])
37
+ .option("--dry-run", "print the expanded resource plan without writing files", false)
38
+ .option("--bruno", "generate Bruno requests when Bruno is enabled", false)
39
+ .option("--enable-bruno", "enable Bruno API client before adding the resource", false)
40
+ .option("--yes", "run the expanded resource plan without prompting", false)
41
+ .option("--force", "overwrite generated files even when modified", false)
42
+ .option("--write-new", "write modified generated files as .new", false)))
43
+ .action(async (name, options, command) => {
44
+ (0, common_options_1.assertInteractiveTerminal)(options);
45
+ const rootContext = (0, command_context_1.getCommandContext)(command);
46
+ const context = { ...rootContext, dryRun: rootContext.dryRun || Boolean(options.dryRun) };
47
+ const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
48
+ const names = (0, naming_1.createNameVariants)(name);
49
+ if (config.registry.resources.some((resource) => resource.name === names.kebabName)) {
50
+ throw new errors_1.CliError(`Resource "${names.kebabName}" already exists.`);
51
+ }
52
+ const prompt = options.interactive ? new prompts_1.InquirerPromptAdapter() : undefined;
53
+ const promptAnswers = prompt
54
+ ? await (0, prompts_1.promptAddResource)(prompt, {
55
+ config,
56
+ provided: createProvidedAddResourceOptions(command),
57
+ })
58
+ : undefined;
59
+ if (options.enableBruno || promptAnswers?.enableBruno) {
60
+ enableBruno(config);
61
+ }
62
+ const resolved = add_resource_resolver_1.addResourceResolver.resolve({
63
+ flags: createExplicitAddResourceFlags(options, command),
64
+ promptAnswers,
65
+ projectConfig: config,
66
+ });
67
+ const crudRoutes = (0, resource_plan_1.parseCrudRouteMatrix)(options.crudRoute);
68
+ const policy = (0, auth_policy_1.parseAuthPolicy)(options.policy);
69
+ assertAuthPolicyAllowed(policy, resolved.auth);
70
+ assertCrudRouteMatrixCapabilities(crudRoutes, resolved.auth, config.project.capabilities.auth, config.project.zones);
71
+ const resourcePlan = {
72
+ name,
73
+ crud: resolved.crud,
74
+ db: resolved.db,
75
+ auth: resolved.auth,
76
+ zone: resolved.zone,
77
+ featuresRoot: config.structure.featuresRoot,
78
+ architecture: config.project.architecture,
79
+ contracts: config.project.capabilities.contracts.includes("zod") ? "zod" : "plain",
80
+ fields: (0, resource_plan_1.parseResourceFieldDefinitions)(options.field),
81
+ crudRoutes,
82
+ policy,
83
+ };
84
+ const planningSummary = (0, resource_plan_1.createResourceAddPlanningSummary)({
85
+ ...resourcePlan,
86
+ architecture: config.project.architecture,
87
+ });
88
+ if (context.dryRun || promptAnswers?.dryRunFirst) {
89
+ context.output.info((0, resource_plan_1.formatResourceAddPlanningSummary)(planningSummary));
90
+ context.output.success(`Planned resource ${names.kebabName} at /${names.pluralName}`);
91
+ return;
92
+ }
93
+ if (options.interactive) {
94
+ context.output.info((0, resource_plan_1.formatResourceAddPlanningSummary)(planningSummary));
95
+ if (!options.yes) {
96
+ const confirmed = await prompt.confirm({
97
+ message: "Add resource?",
98
+ defaultValue: true,
99
+ });
100
+ if (!confirmed) {
101
+ context.output.warn("Resource generation aborted.");
102
+ return;
103
+ }
104
+ }
105
+ }
106
+ const resource = (0, resource_plan_1.createResourceEntry)({
107
+ ...resourcePlan,
108
+ });
109
+ config.registry.resources.push(resource);
110
+ config.registry.routes.push(...createRouteEntries(resource, resolved.auth, resolved.zone, crudRoutes));
111
+ const generatedPaths = config.registry.generatedFiles.map((file) => file.path);
112
+ const existingRouteControllerIndexes = routeControllerIndexResources(generatedPaths, config.structure.featuresRoot, "");
113
+ const usesCrudControllerIndex = resolved.crud;
114
+ const routeControllerIndexes = usesCrudControllerIndex
115
+ ? [...existingRouteControllerIndexes, resource.name]
116
+ : existingRouteControllerIndexes;
117
+ const mainControllerResources = config.registry.resources
118
+ .filter((entry) => !routeControllerIndexes.includes(entry.name))
119
+ .map((entry) => entry.name);
120
+ const cqrsConfigFile = config.project.architecture === "cqrs" && resolved.crud
121
+ ? (0, command_plan_1.createCqrsConfigFile)(config.structure.featuresRoot, {
122
+ commands: [
123
+ ...config.registry.generatedFiles
124
+ .map((file) => (0, command_plan_1.commandFeatureFromPath)(file.path, config.structure.featuresRoot))
125
+ .filter((feature) => Boolean(feature)),
126
+ resource.name,
127
+ ],
128
+ queries: [
129
+ ...config.registry.generatedFiles
130
+ .map((file) => (0, query_plan_1.queryFeatureFromPath)(file.path, config.structure.featuresRoot))
131
+ .filter((feature) => Boolean(feature)),
132
+ resource.name,
133
+ ],
134
+ })
135
+ : undefined;
136
+ const brunoEnabled = config.project.capabilities.apiClient.includes("bruno") || config.api.bruno.enabled;
137
+ const shouldGenerateBruno = options.bruno || promptAnswers?.bruno || (brunoEnabled && !options.interactive);
138
+ const brunoFiles = shouldGenerateBruno
139
+ ? (0, bruno_plan_1.createBrunoFiles)(config)
140
+ : [];
141
+ const files = [
142
+ ...(0, resource_plan_1.createResourceFiles)(resourcePlan),
143
+ (0, resource_plan_1.createResourcesFile)(config.registry.resources, config.structure.featuresRoot),
144
+ (0, resource_plan_1.createFeaturesIndexFile)(config.registry.resources, config.project.capabilities.auth),
145
+ (0, resource_plan_1.createControllersFile)(config.registry.resources, config.structure.featuresRoot, config.project.capabilities.auth, routeControllerIndexes, mainControllerResources),
146
+ ...(cqrsConfigFile ? [cqrsConfigFile] : []),
147
+ ...brunoFiles,
148
+ ];
149
+ await (0, file_writer_1.writePlannedFiles)({
150
+ root: config.root,
151
+ files,
152
+ registry: config.registry,
153
+ force: options.force,
154
+ writeNew: options.writeNew,
155
+ onConflict: options.onConflict,
156
+ }, context);
157
+ await (0, write_soap_config_1.writeSoapConfig)(config.root, config, context);
158
+ context.output.success(`Added resource ${names.kebabName} at ${resource.path}`);
159
+ });
160
+ (0, common_options_1.addConflictOption)((0, common_options_1.addInteractiveOption)(add
161
+ .command("route [resource] [name]")
162
+ .description("Add a route to an existing SoapJS project.")
163
+ .option("--method <method>", "HTTP method: get, post, put, patch, delete, head, options", "get")
164
+ .option("--path <path>", "absolute path under the resource path, or relative path segment")
165
+ .option("--use-case <useCase>", "application use case to call from this route")
166
+ .option("--command <command>", "CQRS command to dispatch from this route")
167
+ .option("--query <query>", "CQRS query to dispatch from this route")
168
+ .option("--auth <auth>", "route auth strategy: none, jwt, api-key, local")
169
+ .option("--zone <zone>", "API zone: public, private, admin")
170
+ .option("--policy <policy>", "auth policy: admin, roles:a,b, custom:name, none")
171
+ .option("--bruno", "generate Bruno requests when Bruno is enabled", false)
172
+ .option("--force", "overwrite generated files even when modified", false)
173
+ .option("--write-new", "write modified generated files as .new", false)))
174
+ .action(async (resourceName, name, options, command) => {
175
+ (0, common_options_1.assertInteractiveTerminal)(options);
176
+ const context = (0, command_context_1.getCommandContext)(command);
177
+ const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
178
+ if (!options.interactive && (!resourceName || !name)) {
179
+ throw new errors_1.CliError("Resource and route name are required. Use `soap add route <resource> <name>`.");
180
+ }
181
+ if (options.interactive && config.registry.resources.length === 0) {
182
+ throw new errors_1.CliError("No resources found. Run `soap add resource <name>` first.");
183
+ }
184
+ const initialResourceNames = resourceName ? (0, naming_1.createNameVariants)(resourceName) : undefined;
185
+ const initialResource = initialResourceNames
186
+ ? config.registry.resources.find((entry) => entry.name === initialResourceNames.kebabName)
187
+ : undefined;
188
+ if (resourceName && !initialResource) {
189
+ throw new errors_1.CliError(`Resource "${initialResourceNames.kebabName}" does not exist. Run \`soap add resource ${initialResourceNames.kebabName}\` first.`);
190
+ }
191
+ const prompt = options.interactive ? new prompts_1.InquirerPromptAdapter() : undefined;
192
+ const promptAnswers = prompt
193
+ ? await (0, prompts_1.promptAddRoute)(prompt, {
194
+ config,
195
+ resource: initialResource,
196
+ resourceName,
197
+ routeName: name,
198
+ provided: createProvidedAddRouteOptions(command, Boolean(resourceName), Boolean(name)),
199
+ })
200
+ : undefined;
201
+ const resolvedResourceName = promptAnswers?.resourceName ?? initialResource.name;
202
+ const resolvedName = promptAnswers?.name ?? name;
203
+ const resourceNames = (0, naming_1.createNameVariants)(resolvedResourceName);
204
+ const routeNames = (0, naming_1.createNameVariants)(resolvedName);
205
+ const resource = config.registry.resources.find((entry) => entry.name === resourceNames.kebabName);
206
+ if (!resource) {
207
+ throw new errors_1.CliError(`Resource "${resourceNames.kebabName}" does not exist. Run \`soap add resource ${resourceNames.kebabName}\` first.`);
208
+ }
209
+ if (config.registry.routes.some((route) => route.resource === resource.name && route.name === routeNames.kebabName)) {
210
+ throw new errors_1.CliError(`Route "${routeNames.kebabName}" already exists on resource "${resource.name}".`);
211
+ }
212
+ const resolved = add_route_resolver_1.addRouteResolver.resolve({
213
+ flags: createExplicitAddRouteFlags(options, command),
214
+ promptAnswers,
215
+ projectConfig: { project: config, resource },
216
+ });
217
+ const policy = (0, auth_policy_1.parseAuthPolicy)(options.policy);
218
+ assertAuthPolicyAllowed(policy, resolved.auth);
219
+ const route = (0, route_plan_1.createRouteEntry)({
220
+ resource,
221
+ name: resolvedName,
222
+ method: resolved.method,
223
+ path: resolved.path,
224
+ useCase: resolved.useCase,
225
+ command: resolved.command,
226
+ query: resolved.query,
227
+ auth: resolved.auth,
228
+ zone: resolved.zone,
229
+ policy,
230
+ featuresRoot: config.structure.featuresRoot,
231
+ contracts: config.project.capabilities.contracts.includes("zod") ? "zod" : "plain",
232
+ });
233
+ config.registry.routes.push(route);
234
+ const routePlan = {
235
+ resource,
236
+ name: resolvedName,
237
+ method: resolved.method,
238
+ path: resolved.path,
239
+ useCase: resolved.useCase,
240
+ command: resolved.command,
241
+ query: resolved.query,
242
+ auth: resolved.auth,
243
+ zone: resolved.zone,
244
+ policy,
245
+ featuresRoot: config.structure.featuresRoot,
246
+ contracts: config.project.capabilities.contracts.includes("zod") ? "zod" : "plain",
247
+ };
248
+ const controllerFile = (0, route_plan_1.createRouteControllerFile)(routePlan);
249
+ const contractFile = (0, route_plan_1.createRouteContractFile)(routePlan);
250
+ const routeControllerNames = routeControllerNamesForResource(config.registry.generatedFiles.map((file) => file.path), config.structure.featuresRoot, resource.name, controllerFile.path);
251
+ const routeControllerIndex = (0, route_plan_1.createRouteControllersIndexFile)(resource, config.structure.featuresRoot, routeControllerNames);
252
+ const controllerIndexes = routeControllerIndexResources(config.registry.generatedFiles.map((file) => file.path), config.structure.featuresRoot, resource.name);
253
+ const controllersFile = (0, resource_plan_1.createControllersFile)(config.registry.resources, config.structure.featuresRoot, config.project.capabilities.auth, controllerIndexes);
254
+ const brunoEnabled = config.project.capabilities.apiClient.includes("bruno") || config.api.bruno.enabled;
255
+ const brunoFiles = options.bruno || promptAnswers?.bruno
256
+ ? (0, bruno_plan_1.createBrunoFiles)(config)
257
+ : [];
258
+ await (0, file_writer_1.writePlannedFiles)({
259
+ root: config.root,
260
+ files: [
261
+ controllerFile,
262
+ contractFile,
263
+ routeControllerIndex,
264
+ controllersFile,
265
+ ...(brunoEnabled ? brunoFiles : []),
266
+ ],
267
+ registry: config.registry,
268
+ force: options.force,
269
+ writeNew: options.writeNew,
270
+ onConflict: options.onConflict,
271
+ }, context);
272
+ await (0, write_soap_config_1.writeSoapConfig)(config.root, config, context);
273
+ context.output.success(`Added route ${route.method} ${route.path}`);
274
+ });
275
+ add
276
+ .command("repository <name>")
277
+ .description("Add a repository port and database adapter to a feature.")
278
+ .requiredOption("--feature <feature>", "feature that owns the repository")
279
+ .requiredOption("--db <database>", "database adapter: mongo, postgres, mysql, sqlite")
280
+ .option("--force", "overwrite generated files even when modified", false)
281
+ .option("--write-new", "write modified generated files as .new", false)
282
+ .action(async (name, options, command) => {
283
+ const context = (0, command_context_1.getCommandContext)(command);
284
+ const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
285
+ const names = (0, naming_1.createNameVariants)(name);
286
+ const db = normalizeRepositoryDb(options.db);
287
+ assertCapability("database", db, config.project.capabilities.databases);
288
+ const files = (0, repository_plan_1.createRepositoryFiles)({
289
+ name,
290
+ feature: options.feature,
291
+ db,
292
+ featuresRoot: config.structure.featuresRoot,
293
+ });
294
+ await (0, file_writer_1.writePlannedFiles)({
295
+ root: config.root,
296
+ files,
297
+ registry: config.registry,
298
+ force: options.force,
299
+ writeNew: options.writeNew,
300
+ }, context);
301
+ await (0, write_soap_config_1.writeSoapConfig)(config.root, config, context);
302
+ context.output.success(`Added ${db} repository ${names.kebabName}`);
303
+ });
304
+ add
305
+ .command("entity <name>")
306
+ .description("Add a domain entity to a feature.")
307
+ .requiredOption("--feature <feature>", "feature that owns the entity")
308
+ .option("--force", "overwrite generated files even when modified", false)
309
+ .option("--write-new", "write modified generated files as .new", false)
310
+ .action(async (name, options, command) => {
311
+ const context = (0, command_context_1.getCommandContext)(command);
312
+ const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
313
+ const names = (0, naming_1.createNameVariants)(name);
314
+ const files = (0, entity_plan_1.createEntityFiles)({
315
+ name,
316
+ feature: options.feature,
317
+ featuresRoot: config.structure.featuresRoot,
318
+ });
319
+ await (0, file_writer_1.writePlannedFiles)({
320
+ root: config.root,
321
+ files,
322
+ registry: config.registry,
323
+ force: options.force,
324
+ writeNew: options.writeNew,
325
+ }, context);
326
+ await (0, write_soap_config_1.writeSoapConfig)(config.root, config, context);
327
+ context.output.success(`Added entity ${names.kebabName}`);
328
+ });
329
+ add
330
+ .command("use-case <name>")
331
+ .description("Add an application use case to a feature.")
332
+ .requiredOption("--feature <feature>", "feature that owns the use case")
333
+ .option("--force", "overwrite generated files even when modified", false)
334
+ .option("--write-new", "write modified generated files as .new", false)
335
+ .action(async (name, options, command) => {
336
+ const context = (0, command_context_1.getCommandContext)(command);
337
+ const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
338
+ const names = (0, naming_1.createNameVariants)(name);
339
+ const files = (0, use_case_plan_1.createUseCaseFiles)({
340
+ name,
341
+ feature: options.feature,
342
+ featuresRoot: config.structure.featuresRoot,
343
+ });
344
+ await (0, file_writer_1.writePlannedFiles)({
345
+ root: config.root,
346
+ files,
347
+ registry: config.registry,
348
+ force: options.force,
349
+ writeNew: options.writeNew,
350
+ }, context);
351
+ await (0, write_soap_config_1.writeSoapConfig)(config.root, config, context);
352
+ context.output.success(`Added use case ${names.kebabName}`);
353
+ });
354
+ add
355
+ .command("command <name>")
356
+ .description("Add a CQRS command and command handler to a feature.")
357
+ .requiredOption("--feature <feature>", "feature that owns the command")
358
+ .option("--force", "overwrite generated files even when modified", false)
359
+ .option("--write-new", "write modified generated files as .new", false)
360
+ .action(async (name, options, command) => {
361
+ const context = (0, command_context_1.getCommandContext)(command);
362
+ const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
363
+ const names = (0, naming_1.createNameVariants)(name);
364
+ const feature = (0, naming_1.createNameVariants)(options.feature).kebabName;
365
+ if (config.project.architecture !== "cqrs") {
366
+ throw new errors_1.CliError("CQRS commands require a project created with `--architecture cqrs`.");
367
+ }
368
+ const commandFiles = (0, command_plan_1.createCommandFiles)({
369
+ name,
370
+ feature,
371
+ featuresRoot: config.structure.featuresRoot,
372
+ });
373
+ const commandNames = commandNamesForFeature(config.registry.generatedFiles.map((file) => file.path), config.structure.featuresRoot, feature, names.kebabName);
374
+ const commandIndex = (0, command_plan_1.createCommandIndexFile)(feature, config.structure.featuresRoot, commandNames);
375
+ const cqrsFeatures = commandFeatures(config.registry.generatedFiles.map((file) => file.path), config.structure.featuresRoot, feature);
376
+ const cqrsConfig = (0, command_plan_1.createCqrsConfigFile)(config.structure.featuresRoot, {
377
+ commands: cqrsFeatures,
378
+ queries: queryFeatures(config.registry.generatedFiles.map((file) => file.path), config.structure.featuresRoot),
379
+ });
380
+ await (0, file_writer_1.writePlannedFiles)({
381
+ root: config.root,
382
+ files: [...commandFiles, commandIndex, cqrsConfig],
383
+ registry: config.registry,
384
+ force: options.force,
385
+ writeNew: options.writeNew,
386
+ }, context);
387
+ await (0, write_soap_config_1.writeSoapConfig)(config.root, config, context);
388
+ context.output.success(`Added command ${names.kebabName}`);
389
+ });
390
+ add
391
+ .command("query <name>")
392
+ .description("Add a CQRS query and query handler to a feature.")
393
+ .requiredOption("--feature <feature>", "feature that owns the query")
394
+ .option("--force", "overwrite generated files even when modified", false)
395
+ .option("--write-new", "write modified generated files as .new", false)
396
+ .action(async (name, options, command) => {
397
+ const context = (0, command_context_1.getCommandContext)(command);
398
+ const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
399
+ const names = (0, naming_1.createNameVariants)(name);
400
+ const feature = (0, naming_1.createNameVariants)(options.feature).kebabName;
401
+ if (config.project.architecture !== "cqrs") {
402
+ throw new errors_1.CliError("CQRS queries require a project created with `--architecture cqrs`.");
403
+ }
404
+ const queryFiles = (0, query_plan_1.createQueryFiles)({
405
+ name,
406
+ feature,
407
+ featuresRoot: config.structure.featuresRoot,
408
+ });
409
+ const queryNames = queryNamesForFeature(config.registry.generatedFiles.map((file) => file.path), config.structure.featuresRoot, feature, names.kebabName);
410
+ const queryIndex = (0, query_plan_1.createQueryIndexFile)(feature, config.structure.featuresRoot, queryNames);
411
+ const cqrsConfig = (0, command_plan_1.createCqrsConfigFile)(config.structure.featuresRoot, {
412
+ commands: commandFeatures(config.registry.generatedFiles.map((file) => file.path), config.structure.featuresRoot),
413
+ queries: queryFeatures(config.registry.generatedFiles.map((file) => file.path), config.structure.featuresRoot, feature),
414
+ });
415
+ await (0, file_writer_1.writePlannedFiles)({
416
+ root: config.root,
417
+ files: [...queryFiles, queryIndex, cqrsConfig],
418
+ registry: config.registry,
419
+ force: options.force,
420
+ writeNew: options.writeNew,
421
+ }, context);
422
+ await (0, write_soap_config_1.writeSoapConfig)(config.root, config, context);
423
+ context.output.success(`Added query ${names.kebabName}`);
424
+ });
425
+ add
426
+ .command("event <name>")
427
+ .description("Add a domain event to a feature.")
428
+ .requiredOption("--feature <feature>", "feature that owns the event")
429
+ .option("--force", "overwrite generated files even when modified", false)
430
+ .option("--write-new", "write modified generated files as .new", false)
431
+ .action(async (name, options, command) => {
432
+ const context = (0, command_context_1.getCommandContext)(command);
433
+ const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
434
+ if (config.project.capabilities.messaging.length === 0) {
435
+ throw new errors_1.CliError("Messaging is not enabled for this project.");
436
+ }
437
+ const files = (0, event_plan_1.createEventFiles)({
438
+ name,
439
+ feature: options.feature,
440
+ architecture: config.project.architecture,
441
+ featuresRoot: config.structure.featuresRoot,
442
+ });
443
+ await (0, file_writer_1.writePlannedFiles)({
444
+ root: config.root,
445
+ files,
446
+ registry: config.registry,
447
+ force: options.force,
448
+ writeNew: options.writeNew,
449
+ }, context);
450
+ await (0, write_soap_config_1.writeSoapConfig)(config.root, config, context);
451
+ context.output.success(`Added event ${(0, naming_1.createNameVariants)(name).kebabName}`);
452
+ });
453
+ add
454
+ .command("socket <name>")
455
+ .description("Add a WebSocket message handler to a feature.")
456
+ .requiredOption("--feature <feature>", "feature that owns the socket handler")
457
+ .option("--auth <auth>", "socket auth strategy placeholder: none, jwt, api-key, local", "none")
458
+ .option("--force", "overwrite generated files even when modified", false)
459
+ .option("--write-new", "write modified generated files as .new", false)
460
+ .action(async (name, options, command) => {
461
+ const context = (0, command_context_1.getCommandContext)(command);
462
+ const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
463
+ if (!config.project.capabilities.realtime.includes("ws")) {
464
+ throw new errors_1.CliError("WebSocket support is not enabled. Create the project with `--realtime ws` first.");
465
+ }
466
+ const auth = normalizeRouteAuth(options.auth ?? "none");
467
+ assertCapability("auth", auth, enabledRouteAuthValues(config.project.capabilities.auth));
468
+ const files = await (0, socket_plan_1.createSocketFiles)(config.root, {
469
+ name,
470
+ feature: options.feature,
471
+ auth,
472
+ featuresRoot: config.structure.featuresRoot,
473
+ });
474
+ await (0, file_writer_1.writePlannedFiles)({
475
+ root: config.root,
476
+ files,
477
+ registry: config.registry,
478
+ force: options.force,
479
+ writeNew: options.writeNew,
480
+ }, context);
481
+ await (0, write_soap_config_1.writeSoapConfig)(config.root, config, context);
482
+ context.output.success(`Added socket ${(0, naming_1.createNameVariants)(name).kebabName}`);
483
+ });
484
+ }
485
+ exports.registerAddCommand = registerAddCommand;
486
+ function firstOrNone(values) {
487
+ return values[0] ?? "none";
488
+ }
489
+ function firstAuthForRoutes(values) {
490
+ if (values.includes("jwt") || values.includes("local")) {
491
+ return "jwt";
492
+ }
493
+ return values[0] ?? "none";
494
+ }
495
+ function normalizeRouteAuth(value) {
496
+ if (value === "local") {
497
+ return "jwt";
498
+ }
499
+ if (value === "jwt" || value === "api-key" || value === "none") {
500
+ return value;
501
+ }
502
+ throw new errors_1.CliError(`Auth "${value}" cannot protect routes. Use jwt, api-key, or none.`);
503
+ }
504
+ function normalizeRepositoryDb(value) {
505
+ if (value === "mongo" || value === "postgres" || value === "mysql" || value === "sqlite") {
506
+ return value;
507
+ }
508
+ throw new errors_1.CliError("Repository database must be mongo, postgres, mysql, or sqlite.");
509
+ }
510
+ function enabledRouteAuthValues(values) {
511
+ const allowed = ["none"];
512
+ if (values.includes("jwt") || values.includes("local")) {
513
+ allowed.push("jwt");
514
+ }
515
+ if (values.includes("api-key")) {
516
+ allowed.push("api-key");
517
+ }
518
+ return allowed;
519
+ }
520
+ function assertCapability(label, value, allowed) {
521
+ if (!allowed.includes(value)) {
522
+ const allowedValues = allowed.length > 0 ? allowed.join(", ") : "none";
523
+ throw new errors_1.CliError(`${label} "${value}" is not enabled for this project. Allowed values: ${allowedValues}.`);
524
+ }
525
+ }
526
+ function createRouteEntries(resource, auth, zone, matrix = {}) {
527
+ const now = new Date().toISOString();
528
+ const routes = [
529
+ ["list", "GET", resource.path],
530
+ ["get", "GET", `${resource.path}/:id`],
531
+ ];
532
+ if (resource.crud) {
533
+ routes.push(["create", "POST", resource.path], ["update", "PUT", `${resource.path}/:id`], ["delete", "DELETE", `${resource.path}/:id`]);
534
+ }
535
+ return routes.map(([name, defaultMethod, defaultPath]) => {
536
+ const override = matrix[name];
537
+ return {
538
+ resource: resource.name,
539
+ name,
540
+ method: (override?.method ?? defaultMethod).toUpperCase(),
541
+ path: resolveCrudRoutePath(resource.path, override?.path ?? defaultPath),
542
+ auth: normalizeRouteAuth(override?.auth ?? auth),
543
+ zone: override?.zone ?? zone,
544
+ policy: override?.policy ?? resource.policy,
545
+ bruno: override?.bruno ?? true,
546
+ generatedAt: now,
547
+ };
548
+ });
549
+ }
550
+ function assertCrudRouteMatrixCapabilities(matrix, defaultAuth, authCapabilities, zones) {
551
+ for (const config of Object.values(matrix)) {
552
+ if (!config) {
553
+ continue;
554
+ }
555
+ const routeAuth = normalizeRouteAuth(config.auth ?? defaultAuth);
556
+ assertCapability("auth", routeAuth, enabledRouteAuthValues(authCapabilities));
557
+ assertAuthPolicyAllowed(config.policy, routeAuth);
558
+ if (config.zone) {
559
+ assertCapability("zone", config.zone, zones);
560
+ }
561
+ }
562
+ }
563
+ function assertAuthPolicyAllowed(policy, auth) {
564
+ if (!policy) {
565
+ return;
566
+ }
567
+ if (normalizeRouteAuth(auth) === "none") {
568
+ throw new errors_1.CliError("Auth policy requires route auth. Use --auth jwt or --auth api-key.");
569
+ }
570
+ }
571
+ function resolveCrudRoutePath(resourcePath, routePath) {
572
+ if (routePath === resourcePath || routePath.startsWith(`${resourcePath}/`)) {
573
+ return routePath;
574
+ }
575
+ if (routePath === "/") {
576
+ return resourcePath;
577
+ }
578
+ return `${resourcePath}${routePath.startsWith("/") ? routePath : `/${routePath}`}`;
579
+ }
580
+ function normalizeRouteMethod(value) {
581
+ const method = value.toLowerCase();
582
+ if (!route_plan_1.routeMethods.includes(method)) {
583
+ throw new errors_1.CliError(`Unsupported HTTP method "${value}". Allowed values: ${route_plan_1.routeMethods.join(", ")}.`);
584
+ }
585
+ return method;
586
+ }
587
+ function assertSingleRouteTarget(options) {
588
+ const targets = [options.useCase, options.command, options.query].filter(Boolean);
589
+ if (targets.length > 1) {
590
+ throw new errors_1.CliError("Route target options are mutually exclusive. Use only one of --use-case, --command, or --query.");
591
+ }
592
+ }
593
+ function createProvidedAddResourceOptions(command) {
594
+ return {
595
+ crud: isCliOption(command, "crud"),
596
+ db: isCliOption(command, "db"),
597
+ auth: isCliOption(command, "auth"),
598
+ zone: isCliOption(command, "zone"),
599
+ bruno: isCliOption(command, "bruno"),
600
+ enableBruno: isCliOption(command, "enableBruno"),
601
+ dryRunFirst: isCliOption(command, "dryRun"),
602
+ };
603
+ }
604
+ function createExplicitAddResourceFlags(options, command) {
605
+ return {
606
+ crud: isCliOption(command, "crud") ? options.crud : undefined,
607
+ db: isCliOption(command, "db") ? options.db : undefined,
608
+ auth: isCliOption(command, "auth") ? options.auth : undefined,
609
+ zone: isCliOption(command, "zone") ? options.zone : undefined,
610
+ };
611
+ }
612
+ function createProvidedAddRouteOptions(command, hasResourceName, hasRouteName) {
613
+ return {
614
+ resourceName: hasResourceName,
615
+ name: hasRouteName,
616
+ method: isCliOption(command, "method"),
617
+ path: isCliOption(command, "path"),
618
+ useCase: isCliOption(command, "useCase"),
619
+ command: isCliOption(command, "command"),
620
+ query: isCliOption(command, "query"),
621
+ auth: isCliOption(command, "auth"),
622
+ zone: isCliOption(command, "zone"),
623
+ bruno: isCliOption(command, "bruno"),
624
+ };
625
+ }
626
+ function createExplicitAddRouteFlags(options, command) {
627
+ return {
628
+ method: isCliOption(command, "method") ? options.method : undefined,
629
+ path: isCliOption(command, "path") ? options.path : undefined,
630
+ useCase: isCliOption(command, "useCase") ? options.useCase : undefined,
631
+ command: isCliOption(command, "command") ? options.command : undefined,
632
+ query: isCliOption(command, "query") ? options.query : undefined,
633
+ auth: isCliOption(command, "auth") ? options.auth : undefined,
634
+ zone: isCliOption(command, "zone") ? options.zone : undefined,
635
+ };
636
+ }
637
+ function isCliOption(command, name) {
638
+ return command.getOptionValueSource(name) === "cli";
639
+ }
640
+ function enableBruno(config) {
641
+ if (!config.project.capabilities.apiClient.includes("bruno")) {
642
+ config.project.capabilities.apiClient.push("bruno");
643
+ }
644
+ config.api.bruno.enabled = true;
645
+ config.api.bruno.collectionPath ||= "bruno";
646
+ config.api.bruno.environment ||= "Local";
647
+ }
648
+ function routeControllerNamesForResource(generatedPaths, featuresRoot, resourceName, nextControllerPath) {
649
+ const apiPrefix = `${featuresRoot}/${resourceName}/api/`;
650
+ const routeControllerPaths = generatedPaths
651
+ .filter((filePath) => filePath.startsWith(apiPrefix))
652
+ .map(route_plan_1.routeControllerNameFromPath)
653
+ .filter((name) => Boolean(name))
654
+ .filter((name) => name !== resourceName);
655
+ const nextName = (0, route_plan_1.routeControllerNameFromPath)(nextControllerPath);
656
+ if (nextName) {
657
+ routeControllerPaths.push(nextName);
658
+ }
659
+ return Array.from(new Set(routeControllerPaths));
660
+ }
661
+ function routeControllerIndexResources(generatedPaths, featuresRoot, nextResourceName) {
662
+ const resources = generatedPaths
663
+ .map((filePath) => (0, route_plan_1.routeControllerIndexResourceFromPath)(filePath, featuresRoot))
664
+ .filter((name) => Boolean(name));
665
+ if (nextResourceName) {
666
+ resources.push(nextResourceName);
667
+ }
668
+ return Array.from(new Set(resources));
669
+ }
670
+ function commandNamesForFeature(generatedPaths, featuresRoot, feature, nextCommandName) {
671
+ const commandsPrefix = `${featuresRoot}/${feature}/application/commands/`;
672
+ const names = generatedPaths
673
+ .filter((filePath) => filePath.startsWith(commandsPrefix))
674
+ .map(command_plan_1.commandNameFromPath)
675
+ .filter((name) => Boolean(name));
676
+ names.push(nextCommandName);
677
+ return Array.from(new Set(names));
678
+ }
679
+ function commandFeatures(generatedPaths, featuresRoot, nextFeature) {
680
+ const features = generatedPaths
681
+ .map((filePath) => (0, command_plan_1.commandFeatureFromPath)(filePath, featuresRoot))
682
+ .filter((name) => Boolean(name));
683
+ if (nextFeature) {
684
+ features.push(nextFeature);
685
+ }
686
+ return Array.from(new Set(features));
687
+ }
688
+ function queryNamesForFeature(generatedPaths, featuresRoot, feature, nextQueryName) {
689
+ const queriesPrefix = `${featuresRoot}/${feature}/application/queries/`;
690
+ const names = generatedPaths
691
+ .filter((filePath) => filePath.startsWith(queriesPrefix))
692
+ .map(query_plan_1.queryNameFromPath)
693
+ .filter((name) => Boolean(name));
694
+ names.push(nextQueryName);
695
+ return Array.from(new Set(names));
696
+ }
697
+ function queryFeatures(generatedPaths, featuresRoot, nextFeature) {
698
+ const features = generatedPaths
699
+ .map((filePath) => (0, query_plan_1.queryFeatureFromPath)(filePath, featuresRoot))
700
+ .filter((name) => Boolean(name));
701
+ if (nextFeature) {
702
+ features.push(nextFeature);
703
+ }
704
+ return Array.from(new Set(features));
705
+ }
706
+ function collect(value, previous) {
707
+ previous.push(value);
708
+ return previous;
709
+ }