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

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 (240) hide show
  1. package/commonjs/src/add-api.action.js +39 -0
  2. package/commonjs/src/add-api.command.js +41 -0
  3. package/commonjs/src/add.command.js +24 -0
  4. package/commonjs/src/add.module.js +26 -0
  5. package/commonjs/src/cli.js +9 -0
  6. package/commonjs/src/create.command.js +24 -0
  7. package/commonjs/src/create.module.js +26 -0
  8. package/commonjs/src/domain/prompt.js +64 -0
  9. package/commonjs/src/index.js +38 -0
  10. package/commonjs/src/models/compose.model.js +150 -0
  11. package/commonjs/src/models/config-yaml.model.js +88 -0
  12. package/commonjs/src/models/const.js +32 -0
  13. package/commonjs/src/models/dev-container.model.js +116 -0
  14. package/commonjs/src/models/docker-image.model.js +259 -0
  15. package/commonjs/src/models/env-file.model.js +50 -0
  16. package/commonjs/src/models/gitignore.model.js +43 -0
  17. package/commonjs/src/models/launch-json.model.js +74 -0
  18. package/commonjs/src/models/package-json.model.js +149 -0
  19. package/commonjs/src/models/project.model.js +134 -0
  20. package/commonjs/src/models/source-file.js +6 -0
  21. package/commonjs/src/models/tsconfig.model.js +34 -0
  22. package/commonjs/src/models/vitest-config.model.js +35 -0
  23. package/commonjs/src/models/vscode-settings.model.js +31 -0
  24. package/commonjs/src/program.js +57 -0
  25. package/commonjs/src/resolve-target-dir.js +19 -0
  26. package/commonjs/src/services/scaffold.service.js +62 -0
  27. package/commonjs/src/version.js +57 -0
  28. package/commonjs/src/webapi/api-name.js +95 -0
  29. package/commonjs/src/webapi/cli-options.js +101 -0
  30. package/commonjs/src/webapi/const.js +50 -0
  31. package/commonjs/src/webapi/create-api.js +237 -0
  32. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.js +166 -0
  33. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
  34. package/commonjs/src/webapi/devcontainer/host-scripts.js +52 -0
  35. package/commonjs/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
  36. package/commonjs/src/webapi/devcontainer/templates/initialize.sh +169 -0
  37. package/commonjs/src/webapi/devcontainer/templates/post-create.sh +35 -0
  38. package/commonjs/src/webapi/devcontainer/templates/post-start.sh +65 -0
  39. package/commonjs/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
  40. package/commonjs/src/webapi/features/auth.feature.js +441 -0
  41. package/commonjs/src/webapi/features/auth0-cli.js +435 -0
  42. package/commonjs/src/webapi/features/biome-format.js +49 -0
  43. package/commonjs/src/webapi/features/database.feature.js +433 -0
  44. package/commonjs/src/webapi/features/finalize.feature.js +95 -0
  45. package/commonjs/src/webapi/features/postman.feature.js +324 -0
  46. package/commonjs/src/webapi/models/api-package-json.model.js +60 -0
  47. package/commonjs/src/webapi/models/auth-config.model.js +26 -0
  48. package/commonjs/src/webapi/models/controller-source-file.model.js +193 -0
  49. package/commonjs/src/webapi/models/github-ci.model.js +193 -0
  50. package/commonjs/src/webapi/models/main-source-file.model.js +167 -0
  51. package/commonjs/src/webapi/models/production-dockerfile.model.js +157 -0
  52. package/commonjs/src/webapi/models/sample-files.model.js +60 -0
  53. package/commonjs/src/webapi/models/webapi-image.model.js +27 -0
  54. package/commonjs/src/webapi/postman/hello-openapi.js +175 -0
  55. package/commonjs/src/webapi/postman/local-view.js +184 -0
  56. package/commonjs/src/webapi/postman/openapi-to-postman.js +202 -0
  57. package/commonjs/src/webapi/workspace-readme.js +110 -0
  58. package/commonjs/src/webapi.action.js +30 -0
  59. package/commonjs/src/webapi.command.js +45 -0
  60. package/es6/src/add-api.action.js +29 -0
  61. package/es6/src/add-api.command.js +31 -0
  62. package/es6/src/add.command.js +14 -0
  63. package/es6/src/add.module.js +16 -0
  64. package/es6/src/cli.js +5 -0
  65. package/es6/src/create.command.js +14 -0
  66. package/es6/src/create.module.js +16 -0
  67. package/es6/src/domain/prompt.js +43 -0
  68. package/es6/src/index.js +8 -0
  69. package/es6/src/models/compose.model.js +144 -0
  70. package/es6/src/models/config-yaml.model.js +76 -0
  71. package/es6/src/models/const.js +22 -0
  72. package/es6/src/models/dev-container.model.js +106 -0
  73. package/es6/src/models/docker-image.model.js +217 -0
  74. package/es6/src/models/env-file.model.js +46 -0
  75. package/es6/src/models/gitignore.model.js +33 -0
  76. package/es6/src/models/launch-json.model.js +64 -0
  77. package/es6/src/models/package-json.model.js +137 -0
  78. package/es6/src/models/project.model.js +119 -0
  79. package/es6/src/models/source-file.js +3 -0
  80. package/es6/src/models/tsconfig.model.js +24 -0
  81. package/es6/src/models/vitest-config.model.js +25 -0
  82. package/es6/src/models/vscode-settings.model.js +21 -0
  83. package/es6/src/program.js +39 -0
  84. package/es6/src/resolve-target-dir.js +9 -0
  85. package/es6/src/services/scaffold.service.js +52 -0
  86. package/es6/src/version.js +36 -0
  87. package/es6/src/webapi/api-name.js +59 -0
  88. package/es6/src/webapi/cli-options.js +36 -0
  89. package/es6/src/webapi/const.js +20 -0
  90. package/es6/src/webapi/create-api.js +217 -0
  91. package/es6/src/webapi/devcontainer/codebase-rag-mcp.js +156 -0
  92. package/es6/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
  93. package/es6/src/webapi/devcontainer/host-scripts.js +31 -0
  94. package/es6/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
  95. package/es6/src/webapi/devcontainer/templates/initialize.sh +169 -0
  96. package/es6/src/webapi/devcontainer/templates/post-create.sh +35 -0
  97. package/es6/src/webapi/devcontainer/templates/post-start.sh +65 -0
  98. package/es6/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
  99. package/es6/src/webapi/features/auth.feature.js +390 -0
  100. package/es6/src/webapi/features/auth0-cli.js +375 -0
  101. package/es6/src/webapi/features/biome-format.js +42 -0
  102. package/es6/src/webapi/features/database.feature.js +423 -0
  103. package/es6/src/webapi/features/finalize.feature.js +85 -0
  104. package/es6/src/webapi/features/postman.feature.js +258 -0
  105. package/es6/src/webapi/models/api-package-json.model.js +55 -0
  106. package/es6/src/webapi/models/auth-config.model.js +22 -0
  107. package/es6/src/webapi/models/controller-source-file.model.js +184 -0
  108. package/es6/src/webapi/models/github-ci.model.js +186 -0
  109. package/es6/src/webapi/models/main-source-file.model.js +163 -0
  110. package/es6/src/webapi/models/production-dockerfile.model.js +148 -0
  111. package/es6/src/webapi/models/sample-files.model.js +42 -0
  112. package/es6/src/webapi/models/webapi-image.model.js +20 -0
  113. package/es6/src/webapi/postman/hello-openapi.js +168 -0
  114. package/es6/src/webapi/postman/local-view.js +169 -0
  115. package/es6/src/webapi/postman/openapi-to-postman.js +179 -0
  116. package/es6/src/webapi/workspace-readme.js +89 -0
  117. package/es6/src/webapi.action.js +20 -0
  118. package/es6/src/webapi.command.js +35 -0
  119. package/package.json +8 -8
  120. package/types/src/add-api.action.d.ts +13 -0
  121. package/types/src/add-api.command.d.ts +7 -0
  122. package/types/src/add.command.d.ts +6 -0
  123. package/types/src/add.module.d.ts +14 -0
  124. package/types/src/cli.d.ts +2 -0
  125. package/types/src/create.command.d.ts +6 -0
  126. package/types/src/create.module.d.ts +14 -0
  127. package/types/src/domain/prompt.d.ts +4 -0
  128. package/types/src/models/compose.model.d.ts +64 -0
  129. package/types/src/models/config-yaml.model.d.ts +27 -0
  130. package/types/src/models/const.d.ts +20 -0
  131. package/types/src/models/dev-container.model.d.ts +37 -0
  132. package/types/src/models/docker-image.model.d.ts +84 -0
  133. package/types/src/models/env-file.model.d.ts +16 -0
  134. package/types/src/models/gitignore.model.d.ts +7 -0
  135. package/types/src/models/launch-json.model.d.ts +7 -0
  136. package/types/src/models/package-json.model.d.ts +35 -0
  137. package/types/src/models/project.model.d.ts +60 -0
  138. package/{src/models/source-file.ts → types/src/models/source-file.d.ts} +4 -6
  139. package/types/src/models/tsconfig.model.d.ts +5 -0
  140. package/types/src/models/vitest-config.model.d.ts +5 -0
  141. package/types/src/models/vscode-settings.model.d.ts +7 -0
  142. package/types/src/program.d.ts +3 -0
  143. package/{src/resolve-target-dir.ts → types/src/resolve-target-dir.d.ts} +1 -4
  144. package/types/src/services/scaffold.service.d.ts +22 -0
  145. package/types/src/version.d.ts +6 -0
  146. package/types/src/webapi/api-name.d.ts +14 -0
  147. package/types/src/webapi/cli-options.d.ts +6 -0
  148. package/{src/webapi/const.ts → types/src/webapi/const.d.ts} +7 -21
  149. package/types/src/webapi/create-api.d.ts +15 -0
  150. package/types/src/webapi/devcontainer/codebase-rag-mcp.d.ts +32 -0
  151. package/types/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
  152. package/types/src/webapi/devcontainer/host-scripts.d.ts +7 -0
  153. package/types/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
  154. package/types/src/webapi/devcontainer/templates/initialize.sh +169 -0
  155. package/types/src/webapi/devcontainer/templates/post-create.sh +35 -0
  156. package/types/src/webapi/devcontainer/templates/post-start.sh +65 -0
  157. package/types/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
  158. package/types/src/webapi/features/auth.feature.d.ts +10 -0
  159. package/types/src/webapi/features/auth0-cli.d.ts +80 -0
  160. package/types/src/webapi/features/biome-format.d.ts +5 -0
  161. package/types/src/webapi/features/database.feature.d.ts +7 -0
  162. package/types/src/webapi/features/finalize.feature.d.ts +2 -0
  163. package/types/src/webapi/features/postman.feature.d.ts +16 -0
  164. package/types/src/webapi/models/api-package-json.model.d.ts +18 -0
  165. package/types/src/webapi/models/auth-config.model.d.ts +26 -0
  166. package/types/src/webapi/models/controller-source-file.model.d.ts +46 -0
  167. package/types/src/webapi/models/github-ci.model.d.ts +9 -0
  168. package/types/src/webapi/models/main-source-file.model.d.ts +54 -0
  169. package/types/src/webapi/models/production-dockerfile.model.d.ts +23 -0
  170. package/types/src/webapi/models/sample-files.model.d.ts +7 -0
  171. package/types/src/webapi/models/webapi-image.model.d.ts +12 -0
  172. package/types/src/webapi/postman/hello-openapi.d.ts +19 -0
  173. package/types/src/webapi/postman/local-view.d.ts +17 -0
  174. package/types/src/webapi/postman/openapi-to-postman.d.ts +62 -0
  175. package/types/src/webapi/workspace-readme.d.ts +18 -0
  176. package/types/src/webapi.action.d.ts +14 -0
  177. package/types/src/webapi.command.d.ts +11 -0
  178. package/src/add-api.action.ts +0 -25
  179. package/src/add-api.command.ts +0 -31
  180. package/src/add.command.ts +0 -20
  181. package/src/add.module.ts +0 -11
  182. package/src/cli.ts +0 -4
  183. package/src/create.command.ts +0 -20
  184. package/src/create.module.ts +0 -11
  185. package/src/domain/prompt.ts +0 -50
  186. package/src/models/compose.model.ts +0 -205
  187. package/src/models/config-yaml.model.ts +0 -83
  188. package/src/models/const.ts +0 -20
  189. package/src/models/dev-container.model.ts +0 -136
  190. package/src/models/docker-image.model.ts +0 -343
  191. package/src/models/env-file.model.ts +0 -45
  192. package/src/models/gitignore.model.ts +0 -35
  193. package/src/models/launch-json.model.ts +0 -60
  194. package/src/models/package-json.model.ts +0 -169
  195. package/src/models/project.model.ts +0 -141
  196. package/src/models/tsconfig.model.ts +0 -29
  197. package/src/models/vitest-config.model.ts +0 -26
  198. package/src/models/vscode-settings.model.ts +0 -24
  199. package/src/program.ts +0 -63
  200. package/src/services/scaffold.service.ts +0 -89
  201. package/src/version.ts +0 -56
  202. package/src/webapi/api-name.ts +0 -73
  203. package/src/webapi/cli-options.ts +0 -56
  204. package/src/webapi/create-api.ts +0 -275
  205. package/src/webapi/devcontainer/host-scripts.ts +0 -35
  206. package/src/webapi/features/auth.feature.ts +0 -454
  207. package/src/webapi/features/auth0-cli.ts +0 -421
  208. package/src/webapi/features/biome-format.ts +0 -34
  209. package/src/webapi/features/database.feature.ts +0 -522
  210. package/src/webapi/features/finalize.feature.ts +0 -112
  211. package/src/webapi/features/postman.feature.ts +0 -287
  212. package/src/webapi/models/api-package-json.model.ts +0 -60
  213. package/src/webapi/models/auth-config.model.ts +0 -36
  214. package/src/webapi/models/controller-source-file.model.ts +0 -232
  215. package/src/webapi/models/github-ci.model.ts +0 -188
  216. package/src/webapi/models/main-source-file.model.ts +0 -208
  217. package/src/webapi/models/production-dockerfile.model.ts +0 -162
  218. package/src/webapi/models/sample-files.model.ts +0 -45
  219. package/src/webapi/models/webapi-image.model.ts +0 -21
  220. package/src/webapi/postman/hello-openapi.ts +0 -92
  221. package/src/webapi/postman/local-view.ts +0 -195
  222. package/src/webapi/postman/openapi-to-postman.ts +0 -208
  223. package/src/webapi/workspace-readme.ts +0 -192
  224. package/src/webapi.action.ts +0 -22
  225. package/src/webapi.command.ts +0 -41
  226. package/tests/auth-postman.wiring.spec.ts +0 -263
  227. package/tests/auth0-cli.spec.ts +0 -232
  228. package/tests/biome-format.spec.ts +0 -49
  229. package/tests/cli.integration.spec.ts +0 -658
  230. package/tests/compose.model.spec.ts +0 -51
  231. package/tests/container-image.model.spec.ts +0 -162
  232. package/tests/dev-container.model.spec.ts +0 -29
  233. package/tests/devcontainer-base-image.spec.ts +0 -32
  234. package/tests/hello-di-inject.scaffold.spec.ts +0 -18
  235. package/tests/hello-route.contract.spec.ts +0 -94
  236. package/tests/package-json.order.spec.ts +0 -59
  237. package/tests/postman-from-openapi.spec.ts +0 -197
  238. package/tests/seed-devcontainer-templates.ts +0 -40
  239. package/tests/webapi/devcontainer/codebase-rag-mcp.spec.ts +0 -96
  240. /package/{src/index.ts → types/src/index.d.ts} +0 -0
@@ -1,658 +0,0 @@
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
- });