@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,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addRouteResolver = exports.AddRouteResolver = void 0;
4
+ const errors_1 = require("../core/errors");
5
+ const route_plan_1 = require("../commands/add/route-plan");
6
+ class AddRouteResolver {
7
+ resolve(input) {
8
+ const config = requireRouteConfig(input.projectConfig);
9
+ const flags = input.flags;
10
+ const promptAnswers = input.promptAnswers ?? {};
11
+ const preset = input.preset ?? {};
12
+ const method = normalizeRouteMethod(pick(flags.method, promptAnswers.method, preset.method, "get"));
13
+ const auth = normalizeRouteAuth(pick(flags.auth, promptAnswers.auth, preset.auth, config.resource.auth));
14
+ const zone = pick(flags.zone, promptAnswers.zone, preset.zone, config.resource.zone);
15
+ const useCase = pick(flags.useCase, promptAnswers.useCase, preset.useCase);
16
+ const command = pick(flags.command, promptAnswers.command, preset.command);
17
+ const query = pick(flags.query, promptAnswers.query, preset.query);
18
+ assertSingleRouteTarget({ useCase, command, query });
19
+ if ((command || query) && config.project.project.architecture !== "cqrs") {
20
+ throw new errors_1.CliError("CQRS route targets require a project created with `--architecture cqrs`.");
21
+ }
22
+ assertCapability("auth", auth, enabledRouteAuthValues(config.project.project.capabilities.auth));
23
+ assertCapability("zone", zone, config.project.project.zones);
24
+ return {
25
+ method,
26
+ path: pick(flags.path, promptAnswers.path, preset.path),
27
+ useCase,
28
+ command,
29
+ query,
30
+ auth,
31
+ zone,
32
+ };
33
+ }
34
+ }
35
+ exports.AddRouteResolver = AddRouteResolver;
36
+ function requireRouteConfig(config) {
37
+ if (!config) {
38
+ throw new errors_1.CliError("Project and resource config are required to resolve route input.");
39
+ }
40
+ return config;
41
+ }
42
+ function pick(...values) {
43
+ return values.find((item) => item !== undefined);
44
+ }
45
+ function normalizeRouteMethod(value) {
46
+ const method = (value ?? "get").toLowerCase();
47
+ if (!route_plan_1.routeMethods.includes(method)) {
48
+ throw new errors_1.CliError(`Unsupported HTTP method "${value}". Allowed values: ${route_plan_1.routeMethods.join(", ")}.`);
49
+ }
50
+ return method;
51
+ }
52
+ function normalizeRouteAuth(value) {
53
+ if (value === "local") {
54
+ return "jwt";
55
+ }
56
+ if (value === "jwt" || value === "api-key" || value === "none") {
57
+ return value;
58
+ }
59
+ throw new errors_1.CliError(`Auth "${value}" cannot protect routes. Use jwt, api-key, or none.`);
60
+ }
61
+ function enabledRouteAuthValues(values) {
62
+ const allowed = ["none"];
63
+ if (values.includes("jwt") || values.includes("local")) {
64
+ allowed.push("jwt");
65
+ }
66
+ if (values.includes("api-key")) {
67
+ allowed.push("api-key");
68
+ }
69
+ return allowed;
70
+ }
71
+ function assertCapability(label, value, allowed) {
72
+ if (!allowed.includes(value)) {
73
+ const allowedValues = allowed.length > 0 ? allowed.join(", ") : "none";
74
+ throw new errors_1.CliError(`${label} "${value}" is not enabled for this project. Allowed values: ${allowedValues}.`);
75
+ }
76
+ }
77
+ function assertSingleRouteTarget(options) {
78
+ const targets = [options.useCase, options.command, options.query].filter(Boolean);
79
+ if (targets.length > 1) {
80
+ throw new errors_1.CliError("Route target options are mutually exclusive. Use only one of --use-case, --command, or --query.");
81
+ }
82
+ }
83
+ exports.addRouteResolver = new AddRouteResolver();
@@ -0,0 +1,32 @@
1
+ import { PackageManager } from "../core/command-context";
2
+ import { ApiZone, Architecture, ProjectCapabilities } from "../config/schemas/types";
3
+ import { CommandInputResolver } from "./resolver.types";
4
+ export interface CreateConfigInput {
5
+ framework?: string;
6
+ architecture?: Architecture;
7
+ db?: string | string[];
8
+ auth?: string | string[];
9
+ messaging?: string | string[];
10
+ telemetry?: string | string[];
11
+ docs?: string | string[];
12
+ contracts?: string | string[];
13
+ apiClient?: string | string[];
14
+ realtime?: string | string[];
15
+ zones?: string | string[];
16
+ packageManager?: PackageManager;
17
+ }
18
+ export interface CreateConfigResult {
19
+ framework: "express";
20
+ architecture: Architecture;
21
+ capabilities: ProjectCapabilities;
22
+ zones: ApiZone[];
23
+ packageManager?: PackageManager;
24
+ }
25
+ export declare class CreateConfigResolver implements CommandInputResolver<CreateConfigInput, CreateConfigInput, undefined, CreateConfigResult> {
26
+ resolve(input: {
27
+ flags: CreateConfigInput;
28
+ promptAnswers?: CreateConfigInput;
29
+ preset?: Partial<CreateConfigResult>;
30
+ }): CreateConfigResult;
31
+ }
32
+ export declare const createConfigResolver: CreateConfigResolver;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createConfigResolver = exports.CreateConfigResolver = void 0;
4
+ const errors_1 = require("../core/errors");
5
+ const project_plan_1 = require("../commands/create/project-plan");
6
+ class CreateConfigResolver {
7
+ resolve(input) {
8
+ const flags = input.flags;
9
+ const promptAnswers = input.promptAnswers ?? {};
10
+ const preset = input.preset ?? {};
11
+ const capabilities = (0, project_plan_1.createDefaultCapabilities)();
12
+ const framework = pick(flags.framework, promptAnswers.framework, preset.framework, "express");
13
+ if (framework !== "express") {
14
+ throw new errors_1.CliError("Only framework \"express\" is supported.");
15
+ }
16
+ const architecture = pick(flags.architecture, promptAnswers.architecture, preset.architecture, "regular");
17
+ if (architecture !== "regular" && architecture !== "cqrs") {
18
+ throw new errors_1.CliError("Architecture must be regular or cqrs.");
19
+ }
20
+ capabilities.databases = parseOptionList(pickList(flags.db, promptAnswers.db, preset.capabilities?.databases), project_plan_1.databaseOptions, "database");
21
+ capabilities.auth = parseOptionList(pickList(flags.auth, promptAnswers.auth, preset.capabilities?.auth), project_plan_1.authOptions, "auth");
22
+ capabilities.messaging = parseOptionList(pickList(flags.messaging, promptAnswers.messaging, preset.capabilities?.messaging), project_plan_1.messagingOptions, "messaging");
23
+ capabilities.telemetry = parseOptionList(pickList(flags.telemetry, promptAnswers.telemetry, preset.capabilities?.telemetry), project_plan_1.telemetryOptions, "telemetry");
24
+ capabilities.docs = parseOptionList(pickList(flags.docs, promptAnswers.docs, preset.capabilities?.docs), project_plan_1.docsOptions, "docs");
25
+ capabilities.contracts = parseOptionList(pickList(flags.contracts, promptAnswers.contracts, preset.capabilities?.contracts), project_plan_1.contractsOptions, "contracts");
26
+ capabilities.apiClient = parseOptionList(pickList(flags.apiClient, promptAnswers.apiClient, preset.capabilities?.apiClient), ["bruno"], "api-client");
27
+ capabilities.realtime = parseOptionList(pickList(flags.realtime, promptAnswers.realtime, preset.capabilities?.realtime), project_plan_1.realtimeOptions, "realtime");
28
+ if (capabilities.messaging.length === 0)
29
+ capabilities.messaging = ["in-memory"];
30
+ if (capabilities.telemetry.length === 0)
31
+ capabilities.telemetry = ["logs"];
32
+ const zones = parseOptionList(pick(flags.zones, promptAnswers.zones, preset.zones, "public,private,admin"), project_plan_1.zonesOptions, "zone");
33
+ return {
34
+ framework,
35
+ architecture,
36
+ capabilities,
37
+ zones: zones.length > 0 ? zones : ["public", "private", "admin"],
38
+ packageManager: pick(flags.packageManager, promptAnswers.packageManager, preset.packageManager),
39
+ };
40
+ }
41
+ }
42
+ exports.CreateConfigResolver = CreateConfigResolver;
43
+ function pick(...values) {
44
+ return values.find((value) => value !== undefined);
45
+ }
46
+ function pickList(...values) {
47
+ return values.find((value) => value !== undefined && (!Array.isArray(value) || value.length > 0));
48
+ }
49
+ function parseOptionList(value, allowed, label) {
50
+ try {
51
+ return (0, project_plan_1.parseCsvOption)(value, allowed);
52
+ }
53
+ catch (error) {
54
+ throw new errors_1.CliError(`Invalid ${label} option: ${error.message}`);
55
+ }
56
+ }
57
+ exports.createConfigResolver = new CreateConfigResolver();
@@ -0,0 +1,17 @@
1
+ import { SoapConfig } from "../config/schemas/types";
2
+ import { CommandInputResolver } from "./resolver.types";
3
+ export interface GenerateBrunoInput {
4
+ e2e?: boolean;
5
+ }
6
+ export interface GenerateBrunoResult {
7
+ e2e: boolean;
8
+ }
9
+ export declare class GenerateBrunoResolver implements CommandInputResolver<GenerateBrunoInput, GenerateBrunoInput, SoapConfig, GenerateBrunoResult> {
10
+ resolve(input: {
11
+ flags: GenerateBrunoInput;
12
+ promptAnswers?: GenerateBrunoInput;
13
+ projectConfig?: SoapConfig;
14
+ preset?: Partial<GenerateBrunoResult>;
15
+ }): GenerateBrunoResult;
16
+ }
17
+ export declare const generateBrunoResolver: GenerateBrunoResolver;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateBrunoResolver = exports.GenerateBrunoResolver = void 0;
4
+ const errors_1 = require("../core/errors");
5
+ class GenerateBrunoResolver {
6
+ resolve(input) {
7
+ const config = input.projectConfig;
8
+ if (!config) {
9
+ throw new errors_1.CliError("Project config is required to resolve Bruno generation input.");
10
+ }
11
+ if (!config.project.capabilities.apiClient.includes("bruno") && !config.api.bruno.enabled) {
12
+ throw new errors_1.CliError("Bruno is not enabled for this project. Create the project with `--api-client bruno`.");
13
+ }
14
+ return {
15
+ e2e: Boolean(pick(input.flags.e2e, input.promptAnswers?.e2e, input.preset?.e2e, false)),
16
+ };
17
+ }
18
+ }
19
+ exports.GenerateBrunoResolver = GenerateBrunoResolver;
20
+ function pick(...values) {
21
+ return values.find((value) => value !== undefined);
22
+ }
23
+ exports.generateBrunoResolver = new GenerateBrunoResolver();
@@ -0,0 +1,5 @@
1
+ export * from "./resolver.types";
2
+ export * from "./create-config.resolver";
3
+ export * from "./add-resource.resolver";
4
+ export * from "./add-route.resolver";
5
+ export * from "./generate-bruno.resolver";
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./resolver.types"), exports);
18
+ __exportStar(require("./create-config.resolver"), exports);
19
+ __exportStar(require("./add-resource.resolver"), exports);
20
+ __exportStar(require("./add-route.resolver"), exports);
21
+ __exportStar(require("./generate-bruno.resolver"), exports);
@@ -0,0 +1,8 @@
1
+ export interface CommandInputResolver<TFlags, TPromptAnswers, TConfig, TResult, TPreset = Partial<TResult>> {
2
+ resolve(input: {
3
+ flags: TFlags;
4
+ promptAnswers?: TPromptAnswers;
5
+ projectConfig?: TConfig;
6
+ preset?: TPreset;
7
+ }): TResult;
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ import { ProjectPlan } from "../commands/create/project-plan";
2
+ export declare function formatCreateSummary(plan: ProjectPlan): string;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatCreateSummary = void 0;
4
+ function formatCreateSummary(plan) {
5
+ return [
6
+ `Project: ${plan.name}`,
7
+ `Framework: ${plan.framework}`,
8
+ `Architecture: ${plan.architecture}`,
9
+ `Databases: ${formatList(plan.capabilities.databases)}`,
10
+ `Auth: ${formatList(plan.capabilities.auth)}`,
11
+ `Messaging: ${formatList(plan.capabilities.messaging)}`,
12
+ `Realtime: ${formatList(plan.capabilities.realtime)}`,
13
+ `Telemetry: ${formatList(plan.capabilities.telemetry)}`,
14
+ `Docs: ${formatList(plan.capabilities.docs)}`,
15
+ `Contracts: ${formatList(plan.capabilities.contracts)}`,
16
+ `API client: ${formatList(plan.capabilities.apiClient)}`,
17
+ `Zones: ${formatList(plan.zones)}`,
18
+ `Package manager: ${plan.packageManager}`,
19
+ ].join("\n");
20
+ }
21
+ exports.formatCreateSummary = formatCreateSummary;
22
+ function formatList(values) {
23
+ return values.length > 0 ? values.join(", ") : "none";
24
+ }
@@ -0,0 +1 @@
1
+ export * from "./create-summary";
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./create-summary"), exports);
@@ -0,0 +1,11 @@
1
+ export interface NameVariants {
2
+ rawName: string;
3
+ pascalName: string;
4
+ camelName: string;
5
+ kebabName: string;
6
+ snakeName: string;
7
+ constantName: string;
8
+ pluralName: string;
9
+ }
10
+ export declare function pluralize(value: string): string;
11
+ export declare function createNameVariants(name: string): NameVariants;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createNameVariants = exports.pluralize = void 0;
4
+ const change_case_1 = require("change-case");
5
+ function pluralize(value) {
6
+ if (value.endsWith("s")) {
7
+ return value;
8
+ }
9
+ if (value.endsWith("y") && !/[aeiou]y$/i.test(value)) {
10
+ return `${value.slice(0, -1)}ies`;
11
+ }
12
+ if (/(s|x|z|ch|sh)$/i.test(value)) {
13
+ return `${value}es`;
14
+ }
15
+ return `${value}s`;
16
+ }
17
+ exports.pluralize = pluralize;
18
+ function createNameVariants(name) {
19
+ const kebabName = (0, change_case_1.paramCase)(name);
20
+ return {
21
+ rawName: name,
22
+ pascalName: (0, change_case_1.pascalCase)(name),
23
+ camelName: (0, change_case_1.camelCase)(name),
24
+ kebabName,
25
+ snakeName: (0, change_case_1.snakeCase)(name),
26
+ constantName: (0, change_case_1.constantCase)(name),
27
+ pluralName: pluralize(kebabName),
28
+ };
29
+ }
30
+ exports.createNameVariants = createNameVariants;
@@ -0,0 +1,6 @@
1
+ import { NameVariants } from "./naming";
2
+ import { ProjectPlan } from "../commands/create/project-plan";
3
+ export interface TemplateContext {
4
+ project: ProjectPlan;
5
+ names: NameVariants;
6
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export declare function renderTemplate(template: string, values: Record<string, string | number | boolean | undefined>): string;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderTemplate = void 0;
4
+ function renderTemplate(template, values) {
5
+ return template.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_, key) => {
6
+ const value = values[key];
7
+ return value === undefined ? "" : String(value);
8
+ });
9
+ }
10
+ exports.renderTemplate = renderTemplate;
@@ -0,0 +1,2 @@
1
+ export declare function registerTemplate(name: string, content: string): void;
2
+ export declare function resolveTemplate(name: string): string;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveTemplate = exports.registerTemplate = void 0;
4
+ const errors_1 = require("../core/errors");
5
+ const templates = new Map();
6
+ function registerTemplate(name, content) {
7
+ templates.set(name, content);
8
+ }
9
+ exports.registerTemplate = registerTemplate;
10
+ function resolveTemplate(name) {
11
+ const content = templates.get(name);
12
+ if (!content) {
13
+ throw new errors_1.CliError(`Missing template: ${name}`);
14
+ }
15
+ return content;
16
+ }
17
+ exports.resolveTemplate = resolveTemplate;
@@ -0,0 +1,6 @@
1
+ export interface TerminalCapabilities {
2
+ stdinIsTty: boolean;
3
+ stdoutIsTty: boolean;
4
+ }
5
+ export declare function getTerminalCapabilities(): TerminalCapabilities;
6
+ export declare function canRunInteractiveMode(capabilities?: TerminalCapabilities): boolean;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.canRunInteractiveMode = exports.getTerminalCapabilities = void 0;
4
+ function getTerminalCapabilities() {
5
+ return {
6
+ stdinIsTty: Boolean(process.stdin.isTTY),
7
+ stdoutIsTty: Boolean(process.stdout.isTTY),
8
+ };
9
+ }
10
+ exports.getTerminalCapabilities = getTerminalCapabilities;
11
+ function canRunInteractiveMode(capabilities = getTerminalCapabilities()) {
12
+ return capabilities.stdinIsTty && capabilities.stdoutIsTty;
13
+ }
14
+ exports.canRunInteractiveMode = canRunInteractiveMode;
@@ -0,0 +1,108 @@
1
+ # ADR 0001: Soap CLI Project-Aware Generator
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ SoapJS CLI generates service projects and later modifies them by adding resources, routes, API clients, documentation, auth, and infrastructure capabilities. The generator has to be repeatable, safe around user edits, and explicit about which files it owns.
10
+
11
+ The MVP intentionally targets predictable project shapes instead of arbitrary existing codebases.
12
+
13
+ ## Decision
14
+
15
+ SoapJS CLI is a template-based, project-aware generator.
16
+
17
+ Generated projects include a `.soap` directory:
18
+
19
+ - `.soap/project.json` stores selected capabilities and project metadata.
20
+ - `.soap/structure.json` stores source layout conventions.
21
+ - `.soap/api.json` stores API runtime settings used by generators.
22
+ - `.soap/registry.json` stores resources, routes, generated files, owners, and hashes.
23
+
24
+ The registry is the authority for follow-up commands such as `soap add`, `soap generate`, `soap check`, `soap remove`, and `soap update config`.
25
+
26
+ ## Why `.soap` Exists
27
+
28
+ The CLI needs project-local state that is independent of package manager files and source code formatting. `.soap` gives commands a stable contract for:
29
+
30
+ - enabled capabilities
31
+ - expected project structure
32
+ - API conventions
33
+ - generated resource and route metadata
34
+ - generated file ownership and hashes
35
+
36
+ This avoids guessing project state from source files.
37
+
38
+ ## Why The Registry Exists
39
+
40
+ The registry makes generated changes deterministic and safe.
41
+
42
+ It records:
43
+
44
+ - generated resources and route paths
45
+ - generated file paths
46
+ - generated file type and owner
47
+ - content hashes from the last CLI write
48
+
49
+ This allows the CLI to detect manual edits and skip, overwrite, or write `.new` files depending on command options.
50
+
51
+ Without a registry, commands like `soap remove resource users` would have to infer ownership from filenames and could delete user-owned files.
52
+
53
+ ## Why The MVP Avoids Broad AST Mutation
54
+
55
+ AST mutation is useful when editing arbitrary source code, but it increases complexity and risk:
56
+
57
+ - user formatting and local patterns vary
58
+ - imports and decorators can be organized many ways
59
+ - partial edits can leave code in a hard-to-debug state
60
+ - reliable idempotence requires many language-aware edge cases
61
+
62
+ The MVP instead regenerates known composition files and generated artifacts from templates. This keeps behavior predictable and makes safety checks based on registry hashes straightforward.
63
+
64
+ Future versions can add narrow AST transforms where there is a clear contract and test coverage.
65
+
66
+ ## Why Bruno Is Generated
67
+
68
+ Bruno collections are generated from the route registry so API test artifacts stay aligned with generated routes.
69
+
70
+ This gives every generated project a runnable local API smoke path:
71
+
72
+ - health check
73
+ - auth requests when auth is enabled
74
+ - resource CRUD requests
75
+ - optional E2E CRUD flow
76
+
77
+ Because Bruno files are tracked in the registry, user edits are protected by the same hash-based safety behavior as TypeScript files.
78
+
79
+ ## Why Optional Adapters Are Wired In The Composition Root
80
+
81
+ Optional adapters such as Mongo, Postgres, Kafka, OpenAPI, auth, and WebSocket support are wired in generated composition files:
82
+
83
+ - `src/index.ts`
84
+ - `src/config/config.ts`
85
+ - `src/config/dependencies.ts`
86
+ - `src/config/controllers.ts`
87
+ - `src/config/resources.ts`
88
+
89
+ This keeps feature code focused on domain/application concerns and keeps infrastructure selection in one predictable layer.
90
+
91
+ It also makes `soap update config` practical: adding a capability rewrites known infrastructure files and adds missing adapter files without invasive edits across resource code.
92
+
93
+ ## Consequences
94
+
95
+ Positive:
96
+
97
+ - deterministic generated output
98
+ - safer overwrite/remove behavior
99
+ - simple validation through `soap check routes`
100
+ - straightforward generated project structure
101
+ - capability updates are practical in the MVP
102
+
103
+ Tradeoffs:
104
+
105
+ - arbitrary app migrations are out of scope
106
+ - manually rewritten composition files may be skipped unless `--force` is used
107
+ - the CLI favors project conventions over flexible source discovery
108
+ - some future changes may require explicit migration commands
@@ -0,0 +1,127 @@
1
+ # `soap add resource`
2
+
3
+ Add a resource to an existing SoapJS project.
4
+
5
+ ```bash
6
+ soap add resource invoice --crud --db postgres --auth jwt --zone public
7
+ soap add resource invoice --crud --db mysql --auth jwt --zone private
8
+ soap add resource note --crud --db sqlite
9
+ soap add resource product --crud --field title:string --field price:number --field active:boolean:optional
10
+ soap add resource report --crud --auth jwt --policy roles:admin,editor
11
+ soap add resource invoice -i
12
+ ```
13
+
14
+ ## Interactive Flow
15
+
16
+ ```bash
17
+ soap add resource invoice -i
18
+ ```
19
+
20
+ The command reads `.soap` before prompting and only offers enabled project capabilities:
21
+
22
+ - CRUD generation
23
+ - storage from enabled databases plus `none`
24
+ - route auth from enabled auth strategies plus `none`
25
+ - API zone from project zones
26
+ - Bruno generation when Bruno is enabled
27
+ - enabling Bruno when it is not enabled
28
+ - optional dry-run-first preview
29
+
30
+ Interactive mode prints the expanded resource plan before writing and asks for confirmation.
31
+
32
+ Use `--yes` to skip final confirmation:
33
+
34
+ ```bash
35
+ soap add resource invoice -i --yes
36
+ ```
37
+
38
+ ## Dry Run
39
+
40
+ ```bash
41
+ soap add resource invoice --crud --db postgres --dry-run
42
+ ```
43
+
44
+ Dry run prints the generated file groups without writing files.
45
+
46
+ ## Fields
47
+
48
+ Store resource field metadata in `.soap/registry.json`:
49
+
50
+ ```bash
51
+ soap add resource product --crud \
52
+ --field title:string \
53
+ --field price:number \
54
+ --field active:boolean:optional
55
+ ```
56
+
57
+ Supported field types:
58
+
59
+ - `string`
60
+ - `number`
61
+ - `boolean`
62
+ - `date`
63
+
64
+ Fields are used by generated entity props, Zod contracts when enabled, and Bruno request bodies. If no fields are provided, the generator uses `name:string`.
65
+
66
+ ## Storage
67
+
68
+ Resource repositories are generated for:
69
+
70
+ - `none` -> in-memory repository
71
+ - `mongo` -> Mongo repository
72
+ - `postgres` -> SQL repository using PostgreSQL config
73
+ - `mysql` -> SQL repository using MySQL config
74
+ - `sqlite` -> SQL repository using SQLite config
75
+
76
+ `redis` is currently infrastructure-only and does not generate a resource repository adapter.
77
+
78
+ ## Auth Policies
79
+
80
+ Attach a policy to protected generated routes:
81
+
82
+ ```bash
83
+ soap add resource report --crud --auth jwt --policy roles:admin,editor
84
+ soap add resource audit-log --crud --auth api-key --policy admin
85
+ soap add resource invoice --crud --auth jwt --policy custom:billing-owner
86
+ ```
87
+
88
+ Supported policies:
89
+
90
+ - `admin` -> `@AdminOnly('<strategy>')`
91
+ - `roles:a,b` -> `@Auth('<strategy>', { roles: ['a', 'b'] })`
92
+ - `custom:name` -> `@Auth('<strategy>', { policy: 'name' })`
93
+
94
+ Policies require route auth. Use `--auth jwt` or `--auth api-key`.
95
+
96
+ ## CRUD Route Matrix
97
+
98
+ Override per-operation route metadata:
99
+
100
+ ```bash
101
+ soap add resource report --crud \
102
+ --crud-route list:get:/search:jwt:private:admin:no-bruno \
103
+ --crud-route create:post:/submit:jwt:private:roles=admin,editor:bruno \
104
+ --crud-route update:patch:/:id:jwt:private:custom=report-owner:bruno
105
+ ```
106
+
107
+ Format:
108
+
109
+ ```txt
110
+ operation:method:path[:auth][:zone][:policy][:bruno|no-bruno]
111
+ ```
112
+
113
+ Supported operations are `list`, `get`, `create`, `update`, and `delete`. Paths are relative to the resource base unless they already start with the resource path. Matrix policies use `admin`, `roles=a,b`, or `custom=name`. `no-bruno` excludes the operation from generated Bruno requests.
114
+
115
+ ## Contracts
116
+
117
+ When the project has `contracts: ["zod"]`, generated resource route contracts parse input through Zod schemas.
118
+
119
+ ## Bruno
120
+
121
+ If Bruno is enabled, resource generation refreshes the Bruno collection.
122
+
123
+ If Bruno is disabled, interactive mode can enable it. Non-interactive mode can do the same explicitly:
124
+
125
+ ```bash
126
+ soap add resource invoice --enable-bruno --bruno
127
+ ```