@decohere/scaffold 0.0.1-dev.125 → 0.0.1-dev.18

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 (264) hide show
  1. package/package.json +9 -19
  2. package/src/add-api.action.ts +25 -0
  3. package/src/add-api.command.ts +31 -0
  4. package/src/add.command.ts +20 -0
  5. package/src/add.module.ts +11 -0
  6. package/src/cli.ts +4 -0
  7. package/src/create.command.ts +20 -0
  8. package/src/create.module.ts +11 -0
  9. package/src/domain/prompt.ts +50 -0
  10. package/{types/src/index.d.ts → src/index.ts} +0 -4
  11. package/src/models/compose.model.ts +205 -0
  12. package/src/models/config-yaml.model.ts +83 -0
  13. package/src/models/const.ts +20 -0
  14. package/src/models/dev-container.model.ts +136 -0
  15. package/src/models/docker-image.model.ts +343 -0
  16. package/src/models/env-file.model.ts +45 -0
  17. package/src/models/gitignore.model.ts +35 -0
  18. package/src/models/launch-json.model.ts +60 -0
  19. package/src/models/package-json.model.ts +169 -0
  20. package/src/models/project.model.ts +141 -0
  21. package/{types/src/models/source-file.d.ts → src/models/source-file.ts} +6 -4
  22. package/src/models/tsconfig.model.ts +29 -0
  23. package/src/models/vitest-config.model.ts +26 -0
  24. package/src/models/vscode-settings.model.ts +24 -0
  25. package/src/program.ts +63 -0
  26. package/{types/src/resolve-target-dir.d.ts → src/resolve-target-dir.ts} +4 -1
  27. package/src/services/scaffold.service.ts +89 -0
  28. package/src/version.ts +56 -0
  29. package/src/webapi/api-name.ts +73 -0
  30. package/src/webapi/cli-options.ts +56 -0
  31. package/{types/src/webapi/const.d.ts → src/webapi/const.ts} +21 -9
  32. package/src/webapi/create-api.ts +275 -0
  33. package/src/webapi/devcontainer/host-scripts.ts +35 -0
  34. package/src/webapi/features/auth.feature.ts +454 -0
  35. package/src/webapi/features/auth0-cli.ts +421 -0
  36. package/src/webapi/features/biome-format.ts +34 -0
  37. package/src/webapi/features/database.feature.ts +522 -0
  38. package/src/webapi/features/finalize.feature.ts +112 -0
  39. package/src/webapi/features/postman.feature.ts +287 -0
  40. package/src/webapi/models/api-package-json.model.ts +60 -0
  41. package/src/webapi/models/auth-config.model.ts +36 -0
  42. package/src/webapi/models/controller-source-file.model.ts +232 -0
  43. package/{es6/src/webapi/models/github-ci.model.js → src/webapi/models/github-ci.model.ts} +29 -27
  44. package/src/webapi/models/main-source-file.model.ts +208 -0
  45. package/src/webapi/models/production-dockerfile.model.ts +162 -0
  46. package/src/webapi/models/sample-files.model.ts +45 -0
  47. package/src/webapi/models/webapi-image.model.ts +21 -0
  48. package/src/webapi/postman/hello-openapi.ts +92 -0
  49. package/src/webapi/postman/local-view.ts +195 -0
  50. package/src/webapi/postman/openapi-to-postman.ts +208 -0
  51. package/src/webapi/workspace-readme.ts +192 -0
  52. package/src/webapi.action.ts +22 -0
  53. package/src/webapi.command.ts +41 -0
  54. package/tests/auth-postman.wiring.spec.ts +263 -0
  55. package/tests/auth0-cli.spec.ts +232 -0
  56. package/tests/biome-format.spec.ts +49 -0
  57. package/tests/cli.integration.spec.ts +658 -0
  58. package/tests/compose.model.spec.ts +51 -0
  59. package/tests/container-image.model.spec.ts +162 -0
  60. package/tests/dev-container.model.spec.ts +29 -0
  61. package/tests/devcontainer-base-image.spec.ts +32 -0
  62. package/tests/hello-di-inject.scaffold.spec.ts +18 -0
  63. package/tests/hello-route.contract.spec.ts +94 -0
  64. package/tests/package-json.order.spec.ts +59 -0
  65. package/tests/postman-from-openapi.spec.ts +197 -0
  66. package/tests/seed-devcontainer-templates.ts +40 -0
  67. package/tests/webapi/devcontainer/codebase-rag-mcp.spec.ts +96 -0
  68. package/LICENSE +0 -15
  69. package/commonjs/src/add-api.action.js +0 -37
  70. package/commonjs/src/add-api.command.js +0 -39
  71. package/commonjs/src/add.command.js +0 -22
  72. package/commonjs/src/add.module.js +0 -24
  73. package/commonjs/src/auth0-configure.action.js +0 -28
  74. package/commonjs/src/auth0-configure.command.js +0 -24
  75. package/commonjs/src/auth0-login.action.js +0 -28
  76. package/commonjs/src/auth0-login.command.js +0 -23
  77. package/commonjs/src/auth0.command.js +0 -22
  78. package/commonjs/src/auth0.module.js +0 -28
  79. package/commonjs/src/cli.js +0 -8
  80. package/commonjs/src/create.command.js +0 -22
  81. package/commonjs/src/create.module.js +0 -24
  82. package/commonjs/src/domain/prompt.js +0 -62
  83. package/commonjs/src/index.js +0 -48
  84. package/commonjs/src/models/compose.model.js +0 -258
  85. package/commonjs/src/models/config-yaml.model.js +0 -86
  86. package/commonjs/src/models/const.js +0 -30
  87. package/commonjs/src/models/dev-container.model.js +0 -172
  88. package/commonjs/src/models/docker-image.model.js +0 -277
  89. package/commonjs/src/models/env-file.model.js +0 -48
  90. package/commonjs/src/models/gitignore.model.js +0 -48
  91. package/commonjs/src/models/launch-json.model.js +0 -70
  92. package/commonjs/src/models/package-json.model.js +0 -156
  93. package/commonjs/src/models/project.model.js +0 -132
  94. package/commonjs/src/models/source-file.js +0 -4
  95. package/commonjs/src/models/tsconfig.model.js +0 -32
  96. package/commonjs/src/models/vitest-config.model.js +0 -33
  97. package/commonjs/src/models/vscode-settings.model.js +0 -29
  98. package/commonjs/src/program.js +0 -64
  99. package/commonjs/src/resolve-target-dir.js +0 -17
  100. package/commonjs/src/services/scaffold.service.js +0 -60
  101. package/commonjs/src/version.js +0 -55
  102. package/commonjs/src/webapi/api-name.js +0 -93
  103. package/commonjs/src/webapi/auth0-commands.js +0 -250
  104. package/commonjs/src/webapi/authorize-scopes.js +0 -79
  105. package/commonjs/src/webapi/cli-options.js +0 -99
  106. package/commonjs/src/webapi/const.js +0 -54
  107. package/commonjs/src/webapi/create-api.js +0 -265
  108. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.js +0 -164
  109. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.ts +0 -182
  110. package/commonjs/src/webapi/devcontainer/host-scripts.js +0 -50
  111. package/commonjs/src/webapi/devcontainer/templates/codebase-rag.env.example +0 -3
  112. package/commonjs/src/webapi/devcontainer/templates/initialize.sh +0 -169
  113. package/commonjs/src/webapi/devcontainer/templates/post-create.sh +0 -35
  114. package/commonjs/src/webapi/devcontainer/templates/post-start.sh +0 -65
  115. package/commonjs/src/webapi/devcontainer/templates/tty-prompt.sh +0 -38
  116. package/commonjs/src/webapi/features/auth.feature.js +0 -408
  117. package/commonjs/src/webapi/features/auth0-cli.js +0 -472
  118. package/commonjs/src/webapi/features/biome-format.js +0 -47
  119. package/commonjs/src/webapi/features/database.feature.js +0 -433
  120. package/commonjs/src/webapi/features/finalize.feature.js +0 -93
  121. package/commonjs/src/webapi/features/postman.feature.js +0 -322
  122. package/commonjs/src/webapi/models/api-package-json.model.js +0 -58
  123. package/commonjs/src/webapi/models/auth-config.model.js +0 -24
  124. package/commonjs/src/webapi/models/controller-source-file.model.js +0 -190
  125. package/commonjs/src/webapi/models/github-ci.model.js +0 -193
  126. package/commonjs/src/webapi/models/main-source-file.model.js +0 -165
  127. package/commonjs/src/webapi/models/production-dockerfile.model.js +0 -75
  128. package/commonjs/src/webapi/models/sample-files.model.js +0 -58
  129. package/commonjs/src/webapi/models/webapi-image.model.js +0 -25
  130. package/commonjs/src/webapi/postman/hello-openapi.js +0 -173
  131. package/commonjs/src/webapi/postman/local-view.js +0 -182
  132. package/commonjs/src/webapi/postman/openapi-to-postman.js +0 -238
  133. package/commonjs/src/webapi/workspace-readme.js +0 -109
  134. package/commonjs/src/webapi.action.js +0 -28
  135. package/commonjs/src/webapi.command.js +0 -43
  136. package/es6/src/add-api.action.js +0 -27
  137. package/es6/src/add-api.command.js +0 -29
  138. package/es6/src/add.command.js +0 -12
  139. package/es6/src/add.module.js +0 -14
  140. package/es6/src/auth0-configure.action.js +0 -18
  141. package/es6/src/auth0-configure.command.js +0 -14
  142. package/es6/src/auth0-login.action.js +0 -18
  143. package/es6/src/auth0-login.command.js +0 -13
  144. package/es6/src/auth0.command.js +0 -12
  145. package/es6/src/auth0.module.js +0 -18
  146. package/es6/src/cli.js +0 -4
  147. package/es6/src/create.command.js +0 -12
  148. package/es6/src/create.module.js +0 -14
  149. package/es6/src/domain/prompt.js +0 -41
  150. package/es6/src/index.js +0 -9
  151. package/es6/src/models/compose.model.js +0 -254
  152. package/es6/src/models/config-yaml.model.js +0 -74
  153. package/es6/src/models/const.js +0 -20
  154. package/es6/src/models/dev-container.model.js +0 -162
  155. package/es6/src/models/docker-image.model.js +0 -235
  156. package/es6/src/models/env-file.model.js +0 -44
  157. package/es6/src/models/gitignore.model.js +0 -38
  158. package/es6/src/models/launch-json.model.js +0 -60
  159. package/es6/src/models/package-json.model.js +0 -141
  160. package/es6/src/models/project.model.js +0 -117
  161. package/es6/src/models/source-file.js +0 -1
  162. package/es6/src/models/tsconfig.model.js +0 -22
  163. package/es6/src/models/vitest-config.model.js +0 -23
  164. package/es6/src/models/vscode-settings.model.js +0 -19
  165. package/es6/src/program.js +0 -50
  166. package/es6/src/resolve-target-dir.js +0 -7
  167. package/es6/src/services/scaffold.service.js +0 -50
  168. package/es6/src/version.js +0 -34
  169. package/es6/src/webapi/api-name.js +0 -57
  170. package/es6/src/webapi/auth0-commands.js +0 -185
  171. package/es6/src/webapi/authorize-scopes.js +0 -55
  172. package/es6/src/webapi/cli-options.js +0 -34
  173. package/es6/src/webapi/const.js +0 -21
  174. package/es6/src/webapi/create-api.js +0 -245
  175. package/es6/src/webapi/devcontainer/codebase-rag-mcp.js +0 -154
  176. package/es6/src/webapi/devcontainer/codebase-rag-mcp.ts +0 -182
  177. package/es6/src/webapi/devcontainer/host-scripts.js +0 -29
  178. package/es6/src/webapi/devcontainer/templates/codebase-rag.env.example +0 -3
  179. package/es6/src/webapi/devcontainer/templates/initialize.sh +0 -169
  180. package/es6/src/webapi/devcontainer/templates/post-create.sh +0 -35
  181. package/es6/src/webapi/devcontainer/templates/post-start.sh +0 -65
  182. package/es6/src/webapi/devcontainer/templates/tty-prompt.sh +0 -38
  183. package/es6/src/webapi/features/auth.feature.js +0 -357
  184. package/es6/src/webapi/features/auth0-cli.js +0 -406
  185. package/es6/src/webapi/features/biome-format.js +0 -40
  186. package/es6/src/webapi/features/database.feature.js +0 -423
  187. package/es6/src/webapi/features/finalize.feature.js +0 -83
  188. package/es6/src/webapi/features/postman.feature.js +0 -256
  189. package/es6/src/webapi/models/api-package-json.model.js +0 -53
  190. package/es6/src/webapi/models/auth-config.model.js +0 -20
  191. package/es6/src/webapi/models/controller-source-file.model.js +0 -181
  192. package/es6/src/webapi/models/main-source-file.model.js +0 -161
  193. package/es6/src/webapi/models/production-dockerfile.model.js +0 -66
  194. package/es6/src/webapi/models/sample-files.model.js +0 -40
  195. package/es6/src/webapi/models/webapi-image.model.js +0 -18
  196. package/es6/src/webapi/postman/hello-openapi.js +0 -166
  197. package/es6/src/webapi/postman/local-view.js +0 -167
  198. package/es6/src/webapi/postman/openapi-to-postman.js +0 -179
  199. package/es6/src/webapi/workspace-readme.js +0 -88
  200. package/es6/src/webapi.action.js +0 -18
  201. package/es6/src/webapi.command.js +0 -33
  202. package/types/src/add-api.action.d.ts +0 -13
  203. package/types/src/add-api.command.d.ts +0 -7
  204. package/types/src/add.command.d.ts +0 -6
  205. package/types/src/add.module.d.ts +0 -14
  206. package/types/src/auth0-configure.action.d.ts +0 -7
  207. package/types/src/auth0-configure.command.d.ts +0 -6
  208. package/types/src/auth0-login.action.d.ts +0 -4
  209. package/types/src/auth0-login.command.d.ts +0 -6
  210. package/types/src/auth0.command.d.ts +0 -6
  211. package/types/src/auth0.module.d.ts +0 -18
  212. package/types/src/cli.d.ts +0 -2
  213. package/types/src/create.command.d.ts +0 -6
  214. package/types/src/create.module.d.ts +0 -14
  215. package/types/src/domain/prompt.d.ts +0 -4
  216. package/types/src/models/compose.model.d.ts +0 -73
  217. package/types/src/models/config-yaml.model.d.ts +0 -27
  218. package/types/src/models/const.d.ts +0 -20
  219. package/types/src/models/dev-container.model.d.ts +0 -49
  220. package/types/src/models/docker-image.model.d.ts +0 -90
  221. package/types/src/models/env-file.model.d.ts +0 -16
  222. package/types/src/models/gitignore.model.d.ts +0 -9
  223. package/types/src/models/launch-json.model.d.ts +0 -7
  224. package/types/src/models/package-json.model.d.ts +0 -36
  225. package/types/src/models/project.model.d.ts +0 -60
  226. package/types/src/models/tsconfig.model.d.ts +0 -5
  227. package/types/src/models/vitest-config.model.d.ts +0 -5
  228. package/types/src/models/vscode-settings.model.d.ts +0 -7
  229. package/types/src/program.d.ts +0 -8
  230. package/types/src/services/scaffold.service.d.ts +0 -22
  231. package/types/src/version.d.ts +0 -6
  232. package/types/src/webapi/api-name.d.ts +0 -14
  233. package/types/src/webapi/auth0-commands.d.ts +0 -11
  234. package/types/src/webapi/authorize-scopes.d.ts +0 -7
  235. package/types/src/webapi/cli-options.d.ts +0 -6
  236. package/types/src/webapi/create-api.d.ts +0 -15
  237. package/types/src/webapi/devcontainer/codebase-rag-mcp.d.ts +0 -32
  238. package/types/src/webapi/devcontainer/codebase-rag-mcp.ts +0 -182
  239. package/types/src/webapi/devcontainer/host-scripts.d.ts +0 -7
  240. package/types/src/webapi/devcontainer/templates/codebase-rag.env.example +0 -3
  241. package/types/src/webapi/devcontainer/templates/initialize.sh +0 -169
  242. package/types/src/webapi/devcontainer/templates/post-create.sh +0 -35
  243. package/types/src/webapi/devcontainer/templates/post-start.sh +0 -65
  244. package/types/src/webapi/devcontainer/templates/tty-prompt.sh +0 -38
  245. package/types/src/webapi/features/auth.feature.d.ts +0 -10
  246. package/types/src/webapi/features/auth0-cli.d.ts +0 -84
  247. package/types/src/webapi/features/biome-format.d.ts +0 -5
  248. package/types/src/webapi/features/database.feature.d.ts +0 -7
  249. package/types/src/webapi/features/finalize.feature.d.ts +0 -2
  250. package/types/src/webapi/features/postman.feature.d.ts +0 -16
  251. package/types/src/webapi/models/api-package-json.model.d.ts +0 -18
  252. package/types/src/webapi/models/auth-config.model.d.ts +0 -26
  253. package/types/src/webapi/models/controller-source-file.model.d.ts +0 -46
  254. package/types/src/webapi/models/github-ci.model.d.ts +0 -9
  255. package/types/src/webapi/models/main-source-file.model.d.ts +0 -54
  256. package/types/src/webapi/models/production-dockerfile.model.d.ts +0 -18
  257. package/types/src/webapi/models/sample-files.model.d.ts +0 -7
  258. package/types/src/webapi/models/webapi-image.model.d.ts +0 -12
  259. package/types/src/webapi/postman/hello-openapi.d.ts +0 -19
  260. package/types/src/webapi/postman/local-view.d.ts +0 -17
  261. package/types/src/webapi/postman/openapi-to-postman.d.ts +0 -62
  262. package/types/src/webapi/workspace-readme.d.ts +0 -18
  263. package/types/src/webapi.action.d.ts +0 -14
  264. package/types/src/webapi.command.d.ts +0 -11
@@ -0,0 +1,658 @@
1
+ /**
2
+ * Behavior: running the create CLI the way Node runs the package bin writes a webapi tree.
3
+ *
4
+ * One suite — every example uses the same launch:
5
+ * argv = [node, /node_modules/.bin/decohere, …userArgs]
6
+ * then load the bin entry (writes go to memfs).
7
+ *
8
+ * Examples are argv permutations only.
9
+ */
10
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { fileURLToPath, pathToFileURL } from "node:url";
13
+ import { resetMemfs } from "@decohere/nexus/testing";
14
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
15
+ import { seedDevcontainerTemplates } from "./seed-devcontainer-templates.js";
16
+
17
+ const realFs = await vi.importActual<typeof import("node:fs")>("node:fs");
18
+
19
+ const { mockAsk, mockPick, mockInstall } = vi.hoisted(() => ({
20
+ mockAsk: vi.fn((_q: string, fallback = "") => Promise.resolve(fallback)),
21
+ mockPick: vi.fn(
22
+ <T>(_label: string, items: T[], _display: (item: T) => string): Promise<T | null> =>
23
+ Promise.resolve(items[0] ?? null),
24
+ ),
25
+ mockInstall: vi.fn().mockResolvedValue({ message: "", data: [] }),
26
+ }));
27
+
28
+ const mockPm = {
29
+ name: "pnpm" as const,
30
+ install: mockInstall,
31
+ syncLockFile: vi.fn().mockResolvedValue({ message: "", data: [] }),
32
+ };
33
+
34
+ vi.mock("../src/domain/prompt.js", async () => {
35
+ const actual = await import("../src/domain/prompt.js");
36
+ return { ...actual, ask: mockAsk, pick: mockPick };
37
+ });
38
+
39
+ vi.mock(import("../../nexus/src/interop/index.js"), async (importOriginal) => {
40
+ const actual = await importOriginal();
41
+ return {
42
+ ...actual,
43
+ detectPackageManager: () => mockPm,
44
+ promptForPackageManager: () => Promise.resolve(mockPm),
45
+ };
46
+ });
47
+
48
+ vi.mock(import("../src/webapi/features/auth0-cli.js"), () => ({
49
+ isInstalled: () => false,
50
+ login: () => true,
51
+ listTenants: () => [],
52
+ }));
53
+
54
+ vi.mock("../src/webapi/features/biome-format.js", () => ({
55
+ formatScaffoldedFiles: vi.fn(),
56
+ }));
57
+
58
+ const NODE = process.execPath;
59
+ const CLI_FILE = realFs.realpathSync(fileURLToPath(new URL("../src/cli.ts", import.meta.url)));
60
+ /** Same argv[1] shape npm/npx use for the package bin. */
61
+ const NPM_BIN = "/node_modules/.bin/decohere";
62
+
63
+ /** Feature flags only — does not skip install (use with builder env or `--install no`). */
64
+ const NON_INTERACTIVE_FEATURES = [
65
+ "--database",
66
+ "none",
67
+ "--auth",
68
+ "none",
69
+ "--postman",
70
+ "none",
71
+ ] as const;
72
+
73
+ const NON_INTERACTIVE = [...NON_INTERACTIVE_FEATURES, "--install", "no"] as const;
74
+
75
+ function expectWebapiTree(root: string): void {
76
+ expect(existsSync(join(root, "package.json")), `${root}/package.json`).toBe(true);
77
+ expect(existsSync(join(root, "Dockerfile")), `${root}/Dockerfile`).toBe(true);
78
+ expect(existsSync(join(root, "tsconfig.json")), `${root}/tsconfig.json`).toBe(true);
79
+ expect(existsSync(join(root, "vitest.config.ts")), `${root}/vitest.config.ts`).toBe(true);
80
+ const vitestConfig = readFileSync(join(root, "vitest.config.ts"), "utf8");
81
+ expect(vitestConfig).toContain('from "@decohere/forge/vitest"');
82
+ expect(vitestConfig).toContain("forgeBuild");
83
+ expect(
84
+ existsSync(join(root, "projects/api/package.json")),
85
+ `${root}/projects/api/package.json`,
86
+ ).toBe(true);
87
+ expect(
88
+ existsSync(join(root, "projects/api/src/main.ts")),
89
+ `${root}/projects/api/src/main.ts`,
90
+ ).toBe(true);
91
+ expect(readFileSync(join(root, "projects/api/src/main.ts"), "utf8")).toContain(
92
+ "withHealth()",
93
+ );
94
+ expect(readFileSync(join(root, "projects/api/src/main.ts"), "utf8")).toContain(
95
+ "Number(process.env.PORT ?? 3000)",
96
+ );
97
+ expect(readFileSync(join(root, "projects/api/src/main.ts"), "utf8")).toContain(
98
+ "scribe.withPinoPretty().and.withPinoRoll",
99
+ );
100
+ expect(readFileSync(join(root, ".gitignore"), "utf8")).toContain("logs");
101
+ expect(readFileSync(join(root, ".gitignore"), "utf8")).toContain(".cursor/codebase-rag.env");
102
+ expect(readFileSync(join(root, ".gitignore"), "utf8")).toContain(".cursor/mcp.json");
103
+ expect(
104
+ existsSync(join(root, "projects/api/src/controllers/hello.controller.ts")),
105
+ `${root}/projects/api/src/controllers/hello.controller.ts`,
106
+ ).toBe(true);
107
+
108
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
109
+ scripts?: Record<string, string>;
110
+ dependencies?: Record<string, string>;
111
+ devDependencies?: Record<string, string>;
112
+ };
113
+ expect(pkg.scripts?.build).toBeTruthy();
114
+ expect(pkg.scripts?.start).toBeTruthy();
115
+ expect(pkg.scripts?.dev).toBeTruthy();
116
+ expect(pkg.dependencies?.["pino-pretty"]).toBeTruthy();
117
+ expect(pkg.dependencies?.["pino-roll"]).toBeTruthy();
118
+ expect(pkg.devDependencies?.["@decohere/forge"]).toBeTruthy();
119
+
120
+ const apiPkg = JSON.parse(readFileSync(join(root, "projects/api/package.json"), "utf8")) as {
121
+ private?: boolean;
122
+ };
123
+ expect(apiPkg.private).toBe(true);
124
+
125
+ const dockerfile = readFileSync(join(root, "Dockerfile"), "utf8");
126
+ expect(dockerfile).toContain("pnpm run build");
127
+ expect(dockerfile).toContain("pnpm install --frozen-lockfile --prod");
128
+ expect(dockerfile).toContain("COPY --from=prod-deps /app/node_modules ./node_modules");
129
+ expect(dockerfile).not.toContain("COPY --from=build /app/node_modules");
130
+
131
+ const devcontainerJson = readFileSync(join(root, ".devcontainer/devcontainer.json"), "utf8");
132
+ expect(devcontainerJson).toContain(
133
+ '"postCreateCommand": "bash .devcontainer/post-create.sh"',
134
+ );
135
+ expect(devcontainerJson).toContain(
136
+ '"updateContentCommand": "bash .devcontainer/post-create.sh"',
137
+ );
138
+ expect(devcontainerJson).toContain('"postStartCommand": "bash .devcontainer/post-start.sh"');
139
+
140
+ const postCreate = readFileSync(join(root, ".devcontainer/post-create.sh"), "utf8");
141
+ expect(postCreate).toContain("pnpm install --frozen-lockfile");
142
+ expect(postCreate).toContain("pnpm install");
143
+
144
+ const postStart = readFileSync(join(root, ".devcontainer/post-start.sh"), "utf8");
145
+ expect(postStart).not.toContain("pnpm install");
146
+ expect(postStart).toContain("codebase-rag-mcp.ts");
147
+ expect(postStart).toContain('prompt_yn "Log in to npm?"');
148
+ expect(postStart).toContain('prompt_yn "Log in to GitHub CLI (gh)?"');
149
+ expect(postStart).toContain("gh auth login");
150
+ expect(postStart).toContain(".devcontainer/gh-auth/");
151
+ expect(postStart).not.toContain("exit 1");
152
+
153
+ const initialize = readFileSync(join(root, ".devcontainer/initialize.sh"), "utf8");
154
+ expect(initialize).toContain('prompt_yn "Configure npm auth for this workspace?"');
155
+ expect(initialize).toContain('prompt_yn "Configure GitHub CLI auth for this workspace?"');
156
+ expect(initialize).toContain('prompt_yn "Configure codebase RAG MCP (semantic search)?"');
157
+ expect(initialize).toContain('gh_dir="$root/.devcontainer/gh-auth"');
158
+ expect(initialize).toContain('gh_env="$root/.devcontainer/gh.env"');
159
+ expect(initialize).not.toContain("python3");
160
+ expect(existsSync(join(root, ".devcontainer/codebase-rag-mcp.ts"))).toBe(true);
161
+ expect(existsSync(join(root, ".devcontainer/tty-prompt.sh"))).toBe(true);
162
+
163
+ expect(initialize).toContain('cursor_dir="$root/.cursor"');
164
+ expect(initialize).toContain("codebase-rag.env");
165
+
166
+ const compose = readFileSync(join(root, ".devcontainer/docker-compose.yml"), "utf8");
167
+ expect(compose).toContain("codebase-rag-mcp.ts merge");
168
+ expect(compose).toContain("postStartCommand runs after Cursor's first MCP snapshot");
169
+ expect(compose).toContain("./gh-auth:/home/node/.config/gh");
170
+ expect(compose).toContain("./gh.env");
171
+ expect(compose).not.toContain("cursor-mcp");
172
+ expect(compose).not.toContain("${HOME}/.config/cursor/codebase-rag.env");
173
+ expect(compose).toContain("bashhistory:/commandhistory");
174
+ expect(compose).toMatch(/^ {2}bashhistory:\s*$/m);
175
+
176
+ const appDevDockerfile = readFileSync(join(root, ".devcontainer/Dockerfile"), "utf8");
177
+ expect(appDevDockerfile).toMatch(/^FROM ghcr\.io\/typesmiths\/decohere\/devcontainer:/m);
178
+ expect(appDevDockerfile).not.toContain("apt-get install -y --no-install-recommends gh");
179
+
180
+ const gitignore = readFileSync(join(root, ".gitignore"), "utf8");
181
+ expect(gitignore).toContain(".devcontainer/gh.env");
182
+ }
183
+
184
+ /** argv as `node node_modules/.bin/decohere …args`, then load the bin entry. */
185
+ async function runCliAsNodeBin(
186
+ userArgs: string[],
187
+ options?: { resetFs?: boolean; builder?: boolean },
188
+ ): Promise<void> {
189
+ vi.resetModules();
190
+ if (options?.resetFs !== false) {
191
+ resetMemfs();
192
+ seedDevcontainerTemplates();
193
+ }
194
+ vi.spyOn(process, "cwd").mockReturnValue("/");
195
+ if (options?.builder === false) {
196
+ delete process.env.DECOHERE_BUILDER;
197
+ } else {
198
+ process.env.DECOHERE_BUILDER = "1";
199
+ }
200
+ process.argv = [NODE, NPM_BIN, ...userArgs];
201
+
202
+ const exited = new Promise<void>((resolve) => {
203
+ vi.spyOn(process, "exit").mockImplementation(() => {
204
+ resolve();
205
+ return undefined as never;
206
+ });
207
+ });
208
+
209
+ await import(`${pathToFileURL(CLI_FILE).href}?t=${Date.now()}`);
210
+ await Promise.race([
211
+ exited,
212
+ new Promise<void>((_, reject) => {
213
+ setTimeout(() => reject(new Error("CLI did not exit")), 5000);
214
+ }),
215
+ ]);
216
+ }
217
+
218
+ beforeEach(() => {
219
+ mockInstall.mockClear();
220
+ mockAsk.mockClear();
221
+ mockPick.mockClear();
222
+ });
223
+
224
+ afterEach(() => {
225
+ resetMemfs();
226
+ vi.restoreAllMocks();
227
+ delete process.env.DECOHERE_BUILDER;
228
+ delete process.env.DECOHERE_PROJECT_NAME;
229
+ });
230
+
231
+ describe("scaffolder writes a webapi project", () => {
232
+ it("writes files for: webapi my-app (npm create after stealing -d)", async () => {
233
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...NON_INTERACTIVE]);
234
+ expectWebapiTree("/my-app");
235
+ });
236
+
237
+ it("writes files for: webapi -d my-app (npx / direct bin)", async () => {
238
+ await runCliAsNodeBin(["create", "webapi", "-d", "my-app", ...NON_INTERACTIVE]);
239
+ expectWebapiTree("/my-app");
240
+ });
241
+
242
+ it("writes files for: webapi ./apps/api (relative directory)", async () => {
243
+ await runCliAsNodeBin(["create", "webapi", "./apps/api", ...NON_INTERACTIVE]);
244
+ expectWebapiTree("/apps/api");
245
+ });
246
+
247
+ it("writes files into cwd for: webapi (no directory)", async () => {
248
+ process.env.DECOHERE_PROJECT_NAME = "cwd-app";
249
+ await runCliAsNodeBin(["create", "webapi", ...NON_INTERACTIVE]);
250
+ expectWebapiTree("/");
251
+ });
252
+
253
+ it("uses DECOHERE_PROJECT_NAME when scaffolding into /workspace", async () => {
254
+ process.env.DECOHERE_PROJECT_NAME = "test1";
255
+ await runCliAsNodeBin(["create", "webapi", "-d", "/workspace", ...NON_INTERACTIVE]);
256
+
257
+ const settings = JSON.parse(
258
+ readFileSync(join("/workspace", ".vscode/settings.json"), "utf8"),
259
+ ) as { "window.title"?: string };
260
+ expect(settings["window.title"]).toContain("test1");
261
+
262
+ const pkg = JSON.parse(readFileSync(join("/workspace", "package.json"), "utf8")) as {
263
+ name?: string;
264
+ };
265
+ expect(pkg.name).toBe("test1.api");
266
+ });
267
+ });
268
+
269
+ describe("scaffolder install behavior", () => {
270
+ it("installs dependencies when not in builder env and without --install no", async () => {
271
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...NON_INTERACTIVE_FEATURES], {
272
+ builder: false,
273
+ });
274
+ expectWebapiTree("/my-app");
275
+ expect(mockInstall).toHaveBeenCalledOnce();
276
+ });
277
+
278
+ it("does not install when DECOHERE_BUILDER=1 even without --install no", async () => {
279
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...NON_INTERACTIVE_FEATURES]);
280
+ expectWebapiTree("/my-app");
281
+ expect(mockInstall).not.toHaveBeenCalled();
282
+ });
283
+
284
+ it("does not install when --install no is set", async () => {
285
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...NON_INTERACTIVE], {
286
+ builder: false,
287
+ });
288
+ expectWebapiTree("/my-app");
289
+ expect(mockInstall).not.toHaveBeenCalled();
290
+ });
291
+ });
292
+
293
+ function readJson(path: string): {
294
+ scripts?: Record<string, string>;
295
+ name?: string;
296
+ private?: boolean;
297
+ } {
298
+ return JSON.parse(readFileSync(path, "utf8")) as {
299
+ scripts?: Record<string, string>;
300
+ name?: string;
301
+ private?: boolean;
302
+ };
303
+ }
304
+
305
+ function expectApiProject(root: string, apiName: string): void {
306
+ expect(existsSync(join(root, `projects/${apiName}/package.json`))).toBe(true);
307
+ expect(existsSync(join(root, `projects/${apiName}/src/main.ts`))).toBe(true);
308
+ const apiPkg = readJson(join(root, `projects/${apiName}/package.json`));
309
+ expect(apiPkg.private).toBe(true);
310
+ expect(apiPkg.name).toBe(`@${root === "/" ? "cwd-app" : root.slice(1)}/${apiName}`);
311
+ }
312
+
313
+ describe("scaffolder names APIs and can add more later", () => {
314
+ it("writes projects/billing when --name billing", async () => {
315
+ await runCliAsNodeBin([
316
+ "create",
317
+ "webapi",
318
+ "my-app",
319
+ "--name",
320
+ "billing",
321
+ ...NON_INTERACTIVE,
322
+ ]);
323
+
324
+ expect(existsSync("/my-app/projects/api")).toBe(false);
325
+ expectApiProject("/my-app", "billing");
326
+
327
+ const pkg = readJson("/my-app/package.json");
328
+ expect(pkg.scripts?.start).toContain("projects/billing");
329
+ expect(pkg.scripts?.["start:billing"]).toContain("projects/billing");
330
+
331
+ const docker = readFileSync("/my-app/Dockerfile", "utf8");
332
+ expect(docker).toContain("COPY projects/billing");
333
+ expect(docker).not.toContain("COPY projects/api");
334
+ expect(existsSync("/my-app/Dockerfile.billing")).toBe(true);
335
+ });
336
+
337
+ it("writes multiple APIs for repeated --name", async () => {
338
+ await runCliAsNodeBin([
339
+ "create",
340
+ "webapi",
341
+ "my-app",
342
+ "--name",
343
+ "api",
344
+ "--name",
345
+ "billing",
346
+ ...NON_INTERACTIVE,
347
+ ]);
348
+
349
+ expectApiProject("/my-app", "api");
350
+ expectApiProject("/my-app", "billing");
351
+
352
+ const pkg = readJson("/my-app/package.json");
353
+ expect(pkg.scripts?.start).toContain("projects/api");
354
+ expect(pkg.scripts?.["start:billing"]).toContain("projects/billing");
355
+
356
+ expect(readFileSync("/my-app/Dockerfile", "utf8")).toContain("COPY projects/api");
357
+ expect(existsSync("/my-app/Dockerfile.billing")).toBe(true);
358
+ });
359
+
360
+ it("adds a named API to an existing workspace without clobbering the first", async () => {
361
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...NON_INTERACTIVE]);
362
+ writeFileSync("/my-app/biome.json", '{ "custom": true }\n');
363
+ writeFileSync("/my-app/Dockerfile", "# keep-root-dockerfile\n");
364
+ writeFileSync("/my-app/vitest.config.ts", "// keep-vitest-config\n");
365
+
366
+ await runCliAsNodeBin(["add", "api", "billing", "-d", "my-app", ...NON_INTERACTIVE], {
367
+ resetFs: false,
368
+ });
369
+
370
+ expect(readFileSync("/my-app/biome.json", "utf8")).toContain("custom");
371
+ expect(readFileSync("/my-app/Dockerfile", "utf8")).toBe("# keep-root-dockerfile\n");
372
+ expect(readFileSync("/my-app/vitest.config.ts", "utf8")).toBe("// keep-vitest-config\n");
373
+ expectApiProject("/my-app", "api");
374
+ expectApiProject("/my-app", "billing");
375
+
376
+ const pkg = readJson("/my-app/package.json");
377
+ expect(pkg.scripts?.start).toContain("projects/api");
378
+ expect(pkg.scripts?.["start:billing"]).toContain("projects/billing");
379
+ expect(existsSync("/my-app/Dockerfile.billing")).toBe(true);
380
+ });
381
+
382
+ it("refuses add when the API already exists", async () => {
383
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...NON_INTERACTIVE]);
384
+ const errors: string[] = [];
385
+ vi.spyOn(console, "error").mockImplementation((msg?: unknown) => {
386
+ errors.push(String(msg ?? ""));
387
+ });
388
+
389
+ await runCliAsNodeBin(["add", "api", "api", "-d", "my-app", ...NON_INTERACTIVE], {
390
+ resetFs: false,
391
+ });
392
+
393
+ expect(errors.join("\n")).toMatch(/already exists/);
394
+ });
395
+
396
+ it("refuses add outside a Decohere workspace", async () => {
397
+ const errors: string[] = [];
398
+ vi.spyOn(console, "error").mockImplementation((msg?: unknown) => {
399
+ errors.push(String(msg ?? ""));
400
+ });
401
+
402
+ await runCliAsNodeBin(["add", "api", "billing", ...NON_INTERACTIVE]);
403
+
404
+ expect(errors.join("\n")).toMatch(/Not a Decohere workspace/);
405
+ expect(existsSync("/projects/billing")).toBe(false);
406
+ });
407
+
408
+ it("refuses an invalid --name", async () => {
409
+ const errors: string[] = [];
410
+ vi.spyOn(console, "error").mockImplementation((msg?: unknown) => {
411
+ errors.push(String(msg ?? ""));
412
+ });
413
+
414
+ await runCliAsNodeBin([
415
+ "create",
416
+ "webapi",
417
+ "my-app",
418
+ "--name",
419
+ "Billing",
420
+ ...NON_INTERACTIVE,
421
+ ]);
422
+
423
+ expect(errors.join("\n")).toMatch(/Invalid API name/);
424
+ expect(existsSync("/my-app/projects/Billing")).toBe(false);
425
+ });
426
+ });
427
+
428
+ const POSTGRES = [
429
+ "--database",
430
+ "postgres/typeorm",
431
+ "--auth",
432
+ "none",
433
+ "--postman",
434
+ "none",
435
+ "--install",
436
+ "no",
437
+ ] as const;
438
+
439
+ describe("scaffolder writes a root README with CLI examples", () => {
440
+ it("includes webapi add and pnpm dev, and omits db:* when database is none", async () => {
441
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...NON_INTERACTIVE]);
442
+
443
+ const readme = readFileSync("/my-app/README.md", "utf8");
444
+ expect(readme).toContain("decohere add api <name>");
445
+ expect(readme).toContain("pnpm dev");
446
+ expect(readme).toContain("pnpm install");
447
+ expect(readme).toContain("Reopen in Container");
448
+ expect(readme).toContain("y/N");
449
+ expect(readme).toContain("codebase RAG MCP");
450
+ expect(readme).toContain(".cursor/");
451
+ expect(readme).toContain("pnpm install --prod");
452
+ expect(readme).toContain("## Logging");
453
+ expect(readme).toContain("NODE_ENV=production");
454
+ expect(readme).not.toContain("pnpm db:up");
455
+ expect(readme).not.toContain("## Auth0");
456
+ expect(readme).not.toContain("## Postman");
457
+ });
458
+
459
+ it("includes db:up when postgres is enabled", async () => {
460
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...POSTGRES]);
461
+
462
+ const readme = readFileSync("/my-app/README.md", "utf8");
463
+ expect(readme).toContain("pnpm db:up");
464
+ expect(readme).toContain("pnpm db:down");
465
+ expect(readme).toContain("pnpm db:reset");
466
+ expect(readme).toContain("decohere add api <name>");
467
+ expect(readme).toContain("pnpm dev");
468
+ });
469
+
470
+ it("refreshes API scripts when webapi add runs", async () => {
471
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...NON_INTERACTIVE]);
472
+ await runCliAsNodeBin(["add", "api", "billing", "-d", "my-app", ...NON_INTERACTIVE], {
473
+ resetFs: false,
474
+ });
475
+
476
+ const readme = readFileSync("/my-app/README.md", "utf8");
477
+ expect(readme).toContain("pnpm run start:api");
478
+ expect(readme).toContain("pnpm run start:billing");
479
+ expect(readme).toContain("pnpm run dev:billing");
480
+ expect(readme).toContain("decohere add api <name>");
481
+ });
482
+
483
+ it("keeps db:* after webapi add when the workspace already has postgres", async () => {
484
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...POSTGRES]);
485
+ await runCliAsNodeBin(["add", "api", "billing", "-d", "my-app", ...NON_INTERACTIVE], {
486
+ resetFs: false,
487
+ });
488
+
489
+ const readme = readFileSync("/my-app/README.md", "utf8");
490
+ expect(readme).toContain("pnpm db:up");
491
+ expect(readme).toContain("pnpm run start:billing");
492
+ });
493
+ });
494
+
495
+ describe("each API owns a database on the shared Postgres instance", () => {
496
+ it("names the default API datasource and prefixes env", async () => {
497
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...POSTGRES]);
498
+
499
+ const config = readFileSync("/my-app/config/config.yaml", "utf8");
500
+ expect(config).toContain("name: api");
501
+ expect(config).toContain("${API_DB_HOST}");
502
+ expect(config).not.toContain("name: default");
503
+ expect(config).not.toContain("${DB_HOST}");
504
+
505
+ const env = readFileSync("/my-app/.env", "utf8");
506
+ expect(env).toContain("API_DB_HOST=postgres");
507
+ expect(env).toContain("API_DB_PORT=5432");
508
+ expect(env).toMatch(/API_DB_NAME=/);
509
+ expect(env).not.toMatch(/^DB_NAME=/m);
510
+ expect(env).toContain("DB_ADMIN_USER=decohere_bootstrap");
511
+
512
+ const main = readFileSync("/my-app/projects/api/src/main.ts", "utf8");
513
+ expect(main).toContain("withHealth()");
514
+ expect(main).toContain('withTypeOrm({ name: "api"');
515
+
516
+ const compose = readFileSync("/my-app/.devcontainer/docker-compose.yml", "utf8");
517
+ expect(compose).toContain("samples-my-app-postgres");
518
+ expect(compose).toContain("build: ./postgres");
519
+ expect(compose).toContain("./postgres-data:/var/lib/postgresql/data");
520
+ expect(compose).toMatch(/bind mount|dies with the working tree/i);
521
+ expect(compose).not.toContain("postgres-init");
522
+ expect(compose).not.toMatch(/^\s+my-app-postgres-data:\s*$/m);
523
+ expect(compose).not.toContain("adminer");
524
+
525
+ const gitignore = readFileSync("/my-app/.gitignore", "utf8");
526
+ expect(gitignore).toContain(".devcontainer/postgres-data");
527
+
528
+ const rootPkg = JSON.parse(readFileSync("/my-app/package.json", "utf8")) as {
529
+ scripts: Record<string, string>;
530
+ dependencies: Record<string, string>;
531
+ };
532
+ expect(rootPkg.scripts["db:reset"]).toContain("rm -rf .devcontainer/postgres-data");
533
+ expect(rootPkg.scripts["db:reset"]).not.toContain("down -v");
534
+
535
+ expect(
536
+ readFileSync("/my-app/.devcontainer/postgres/ensure-bootstrap-role.sh", "utf8"),
537
+ ).toContain("ALTER ROLE");
538
+ expect(readFileSync("/my-app/.devcontainer/postgres/Dockerfile", "utf8")).toContain(
539
+ "FROM postgres:16",
540
+ );
541
+
542
+ const appDevDockerfile = readFileSync("/my-app/.devcontainer/Dockerfile", "utf8");
543
+ expect(appDevDockerfile).toContain("postgresql-client");
544
+ expect(appDevDockerfile).toContain("USER root");
545
+ expect(appDevDockerfile).toMatch(/^FROM ghcr\.io\/typesmiths\/decohere\/devcontainer:/m);
546
+ const devcontainerJson = readFileSync("/my-app/.devcontainer/devcontainer.json", "utf8");
547
+ expect(devcontainerJson).toContain("mtxr.sqltools-driver-pg");
548
+
549
+ const depKeys = Object.keys(rootPkg.dependencies);
550
+ expect(depKeys).toEqual([...depKeys].sort((a, b) => a.localeCompare(b, "en")));
551
+ expect(rootPkg.dependencies.tsx).toBeTruthy();
552
+ expect(rootPkg.scripts["db:migrate"]).toContain("migration:run");
553
+ expect(rootPkg.scripts["db:migrate"]).toContain("typeorm.data-source.ts");
554
+ expect(rootPkg.scripts["db:migrate:generate"]).toContain("migration:generate");
555
+ expect(rootPkg.scripts["db:migrate:api"]).toContain("projects/api");
556
+
557
+ expect(existsSync("/my-app/docs/database.md")).toBe(true);
558
+ const dbDocs = readFileSync("/my-app/docs/database.md", "utf8");
559
+ expect(dbDocs).toMatch(/synchronize: true/);
560
+ expect(dbDocs).toMatch(/migrations only/i);
561
+ expect(dbDocs).toContain("db:migrate");
562
+ expect(dbDocs).toMatch(/Adminer is \*\*not\*\* scaffolded/i);
563
+ expect(dbDocs).toContain("image: adminer");
564
+
565
+ expect(existsSync("/my-app/projects/api/src/typeorm.data-source.ts")).toBe(true);
566
+ const dataSource = readFileSync("/my-app/projects/api/src/typeorm.data-source.ts", "utf8");
567
+ expect(dataSource).toContain("API_DB_MIGRATE_USER");
568
+ expect(dataSource).toContain("synchronize: false");
569
+ expect(existsSync("/my-app/projects/api/src/migrations/.gitkeep")).toBe(true);
570
+ });
571
+
572
+ it("emits per-API migrate scripts and data sources for named APIs", async () => {
573
+ await runCliAsNodeBin([
574
+ "create",
575
+ "webapi",
576
+ "my-app",
577
+ "--name",
578
+ "api",
579
+ "--name",
580
+ "billing",
581
+ ...POSTGRES,
582
+ ]);
583
+
584
+ const pkg = JSON.parse(readFileSync("/my-app/package.json", "utf8")) as {
585
+ scripts: Record<string, string>;
586
+ };
587
+ expect(pkg.scripts["db:migrate:billing"]).toContain("projects/billing");
588
+ expect(pkg.scripts["db:migrate:generate:billing"]).toContain("migration:generate");
589
+ expect(existsSync("/my-app/projects/billing/src/typeorm.data-source.ts")).toBe(true);
590
+ expect(
591
+ readFileSync("/my-app/projects/billing/src/typeorm.data-source.ts", "utf8"),
592
+ ).toContain("BILLING_DB_MIGRATE_USER");
593
+ });
594
+
595
+ it("gives each named API its own database on the same host", async () => {
596
+ await runCliAsNodeBin([
597
+ "create",
598
+ "webapi",
599
+ "my-app",
600
+ "--name",
601
+ "api",
602
+ "--name",
603
+ "billing",
604
+ ...POSTGRES,
605
+ ]);
606
+
607
+ const config = readFileSync("/my-app/config/config.yaml", "utf8");
608
+ expect(config).toContain("name: api");
609
+ expect(config).toContain("name: billing");
610
+ expect(config).toContain("${BILLING_DB_HOST}");
611
+
612
+ const env = readFileSync("/my-app/.env", "utf8");
613
+ expect(env).toContain("API_DB_HOST=postgres");
614
+ expect(env).toContain("BILLING_DB_HOST=postgres");
615
+ const apiDb = env.split("\n").find((l) => l.startsWith("API_DB_NAME="));
616
+ const billingDb = env.split("\n").find((l) => l.startsWith("BILLING_DB_NAME="));
617
+ expect(apiDb).toBeTruthy();
618
+ expect(billingDb).toBeTruthy();
619
+ expect(apiDb).not.toBe(billingDb);
620
+
621
+ expect(readFileSync("/my-app/projects/api/src/main.ts", "utf8")).toContain(
622
+ 'withTypeOrm({ name: "api"',
623
+ );
624
+ expect(readFileSync("/my-app/projects/billing/src/main.ts", "utf8")).toContain(
625
+ 'withTypeOrm({ name: "billing"',
626
+ );
627
+
628
+ const compose = readFileSync("/my-app/.devcontainer/docker-compose.yml", "utf8");
629
+ expect(compose).toContain("container_name: samples-my-app-postgres");
630
+ expect(compose).not.toContain("samples-my-app-billing-postgres");
631
+ });
632
+
633
+ it("adds a datasource for webapi add without replacing the first", async () => {
634
+ await runCliAsNodeBin(["create", "webapi", "my-app", ...POSTGRES]);
635
+ await runCliAsNodeBin(["add", "api", "billing", "-d", "my-app", ...POSTGRES], {
636
+ resetFs: false,
637
+ });
638
+
639
+ const config = readFileSync("/my-app/config/config.yaml", "utf8");
640
+ expect(config).toContain("name: api");
641
+ expect(config).toContain("name: billing");
642
+ expect(config.match(/^database:/gm)?.length).toBe(1);
643
+
644
+ const env = readFileSync("/my-app/.env", "utf8");
645
+ expect(env).toContain("API_DB_HOST=postgres");
646
+ expect(env).toContain("BILLING_DB_HOST=postgres");
647
+ expect(readFileSync("/my-app/projects/billing/src/main.ts", "utf8")).toContain(
648
+ 'withTypeOrm({ name: "billing"',
649
+ );
650
+
651
+ const pkg = JSON.parse(readFileSync("/my-app/package.json", "utf8")) as {
652
+ scripts: Record<string, string>;
653
+ };
654
+ expect(pkg.scripts["db:migrate"]).toContain("projects/api");
655
+ expect(pkg.scripts["db:migrate:billing"]).toContain("projects/billing");
656
+ expect(existsSync("/my-app/projects/billing/src/typeorm.data-source.ts")).toBe(true);
657
+ });
658
+ });