@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
@@ -1,14 +1,18 @@
1
1
  import { WELL_KNOWN_FILES } from "../../models/const.js";
2
+ import type { KeyedModel, SourceFile } from "../../models/source-file.js";
3
+
2
4
  /**
3
5
  * Minimal GitHub Actions for scaffolded apps: lint ∥ test, build gate on default branch,
4
6
  * npm publish after CI (skips private packages). No default forge docker --push.
5
- */ export class GithubCiWorkflows {
6
- key = WELL_KNOWN_FILES.GITHUB_CI;
7
- files() {
8
- return [
9
- {
10
- filePath: ".github/actions/setup-node-pnpm/action.yml",
11
- content: `name: Setup Node + pnpm
7
+ */
8
+ export class GithubCiWorkflows implements KeyedModel {
9
+ public readonly key = WELL_KNOWN_FILES.GITHUB_CI;
10
+
11
+ public files(): SourceFile[] {
12
+ return [
13
+ {
14
+ filePath: ".github/actions/setup-node-pnpm/action.yml",
15
+ content: `name: Setup Node + pnpm
12
16
  description: Install pnpm and Node, configure npm registry auth, install deps
13
17
  inputs:
14
18
  node-version:
@@ -53,11 +57,11 @@ runs:
53
57
  else
54
58
  pnpm install
55
59
  fi
56
- `
57
- },
58
- {
59
- filePath: ".github/workflows/ci.yml",
60
- content: `name: CI
60
+ `,
61
+ },
62
+ {
63
+ filePath: ".github/workflows/ci.yml",
64
+ content: `name: CI
61
65
  on:
62
66
  push:
63
67
  branches:
@@ -115,9 +119,9 @@ jobs:
115
119
  - name: Test
116
120
  run: pnpm test
117
121
 
118
- - name: Build and package (default branch)
122
+ - name: Build (default branch)
119
123
  if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
120
- run: pnpm run package
124
+ run: pnpm run build
121
125
 
122
126
  ci:
123
127
  needs: [lint, test]
@@ -132,11 +136,11 @@ jobs:
132
136
  exit 1
133
137
  fi
134
138
  echo "CI passed"
135
- `
136
- },
137
- {
138
- filePath: ".github/workflows/publish.yml",
139
- content: `name: Publish
139
+ `,
140
+ },
141
+ {
142
+ filePath: ".github/workflows/publish.yml",
143
+ content: `name: Publish
140
144
 
141
145
  on:
142
146
  workflow_run:
@@ -175,12 +179,10 @@ jobs:
175
179
  - name: Publish packages
176
180
  run: pnpm exec forge publish --configuration release
177
181
  # Skips packages with "private": true (e.g. the webapi app).
178
- # Optional registry images: replace "Build release" above with \`pnpm run package\`, then add
179
- # \`pnpm exec forge docker --configuration release --push\` after configuring GHCR/OIDC.
180
- # Not enabled by default. Each API's Dockerfile copies .package/, which \`forge package\`
181
- # builds, so the image build itself needs no npm token.
182
- `
183
- }
184
- ];
185
- }
182
+ # Optional registry images: add \`pnpm exec forge docker --configuration release --push\`
183
+ # after configuring GHCR/OIDC — not enabled by default (PaaS builds the Dockerfile).
184
+ `,
185
+ },
186
+ ];
187
+ }
186
188
  }
@@ -0,0 +1,208 @@
1
+ import { WELL_KNOWN_FILES } from "../../models/const.js";
2
+ import type { KeyedModel, SourceFile } from "../../models/source-file.js";
3
+
4
+ export type TypeOrmImport = { className: string; importPath: string };
5
+ export type ServiceImport = TypeOrmImport & { scope?: "singleton" | "request" };
6
+
7
+ /** Tracks named imports from a single module path, split into values and types. */
8
+ class ImportGroup {
9
+ private readonly _values: string[] = [];
10
+ private readonly _types: string[] = [];
11
+
12
+ public add(name: string, typeOnly = false): this {
13
+ if (typeOnly) {
14
+ // Value import already present — value wins, skip
15
+ if (!this._values.includes(name) && !this._types.includes(name)) {
16
+ this._types.push(name);
17
+ }
18
+ } else {
19
+ // Promote: remove from types if it was previously type-only
20
+ const typeIdx = this._types.indexOf(name);
21
+ if (typeIdx !== -1) this._types.splice(typeIdx, 1);
22
+ if (!this._values.includes(name)) this._values.push(name);
23
+ }
24
+ return this;
25
+ }
26
+
27
+ public render(from: string): string {
28
+ const values = [...this._values].sort();
29
+ const types = [...this._types].sort().map((n) => `type ${n}`);
30
+ return `import { ${[...values, ...types].join(", ")} } from "${from}";`;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Model for `projects/api/src/main.ts`.
36
+ *
37
+ * Starts with the minimal HTTP + YAML config setup. Features mutate the model
38
+ * by calling addSideEffectImport / addImport / addBuilderCall / addEntity /
39
+ * addRepository. files() renders whatever has been accumulated.
40
+ */
41
+ export class MainSourceFile implements KeyedModel {
42
+ public readonly key = WELL_KNOWN_FILES.MAIN_TS;
43
+
44
+ private readonly _sideEffectImports: string[] = [];
45
+ private readonly _extraImports = new Map<string, ImportGroup>();
46
+ private readonly _builderCalls: string[] = [];
47
+ private readonly _entities: TypeOrmImport[] = [];
48
+ private readonly _repositories: TypeOrmImport[] = [];
49
+ private readonly _services: ServiceImport[] = [];
50
+ private _dataSourceName: string | undefined;
51
+ private _typeOrmSeed: string | undefined;
52
+
53
+ public constructor(private readonly appName: string) {}
54
+
55
+ /** Adds a bare side-effect import, e.g. `@decohere/cortex/auth`. */
56
+ public addSideEffectImport(path: string): this {
57
+ if (!this._sideEffectImports.includes(path)) this._sideEffectImports.push(path);
58
+ return this;
59
+ }
60
+
61
+ /** Adds a named import, e.g. `import { Foo } from "bar"`. */
62
+ public addImport(name: string, from: string, typeOnly = false): this {
63
+ const group = this._extraImports.get(from) ?? new ImportGroup();
64
+ group.add(name, typeOnly);
65
+ this._extraImports.set(from, group);
66
+ return this;
67
+ }
68
+
69
+ /**
70
+ * Adds a builder call inserted after `.withConfiguration()` and before
71
+ * `.withServices()`. Pass the call without leading dot, e.g. `.withAuth()`.
72
+ */
73
+ public addBuilderCall(call: string): this {
74
+ this._builderCalls.push(call);
75
+ return this;
76
+ }
77
+
78
+ /** Adds a TypeORM entity — import and entry in `withTypeOrm({ entities })`. */
79
+ public addEntity(entity: TypeOrmImport): this {
80
+ this._entities.push(entity);
81
+ return this;
82
+ }
83
+
84
+ /** Adds a TypeORM repository — import and entry in `withTypeOrm({ repositories })`. */
85
+ public addRepository(repo: TypeOrmImport): this {
86
+ this._repositories.push(repo);
87
+ return this;
88
+ }
89
+
90
+ /** Adds a service registered in `.withServices()`. */
91
+ public addService(service: ServiceImport): this {
92
+ this._services.push(service);
93
+ return this;
94
+ }
95
+
96
+ /**
97
+ * Datasource name passed to `withTypeOrm({ name })`. Must match
98
+ * `database.dataSources[].name` in config.yaml.
99
+ */
100
+ public setDataSourceName(name: string): this {
101
+ this._dataSourceName = name;
102
+ return this;
103
+ }
104
+
105
+ /**
106
+ * Inline seed callback body for `withTypeOrm({ seed })`.
107
+ * Receives `dataSource` in scope. Example: `await dataSource.getRepository(User).save(...)`.
108
+ */
109
+ public setTypeOrmSeed(seedBody: string): this {
110
+ this._typeOrmSeed = seedBody;
111
+ return this;
112
+ }
113
+
114
+ public files(): SourceFile[] {
115
+ const imports: string[] = [
116
+ 'import "@decohere/scribe-pino";',
117
+ 'import "@decohere/cortex/http";',
118
+ ];
119
+
120
+ for (const path of this._sideEffectImports) {
121
+ imports.push(`import "${path}";`);
122
+ }
123
+
124
+ imports.push(
125
+ 'import "@decohere/protocol";',
126
+ 'import { YamlDataSource } from "@decohere/protocol";',
127
+ 'import { Builder } from "@decohere/cortex";',
128
+ 'import { FastifyProvider } from "@decohere/cortex/providers";',
129
+ 'import { scribe } from "@decohere/scribe";',
130
+ 'import { SampleService } from "./services/sample.service.js";',
131
+ );
132
+
133
+ for (const e of this._entities) {
134
+ imports.push(`import { ${e.className} } from "${e.importPath}";`);
135
+ }
136
+
137
+ for (const r of this._repositories) {
138
+ imports.push(`import { ${r.className} } from "${r.importPath}";`);
139
+ }
140
+
141
+ for (const s of this._services) {
142
+ imports.push(`import { ${s.className} } from "${s.importPath}";`);
143
+ }
144
+
145
+ for (const [from, group] of this._extraImports) {
146
+ imports.push(group.render(from));
147
+ }
148
+
149
+ imports.push('import { HelloController } from "./controllers/hello.controller.js";');
150
+
151
+ const serviceRegs = [
152
+ "cb.registerSingleton(SampleService);",
153
+ ...this._services.map((s) =>
154
+ s.scope === "request"
155
+ ? `cb.registerRequestScoped(${s.className});`
156
+ : `cb.registerSingleton(${s.className});`,
157
+ ),
158
+ ].join(" ");
159
+
160
+ const builderCalls: string[] = [
161
+ `\t.withHttp(FastifyProvider)`,
162
+ `\t.withConfiguration(YamlDataSource)`,
163
+ `\t.withHealth()`,
164
+ `\t.withOpenApi({ info: { title: "${this.appName}", version: "1.0.0" } })`,
165
+ ...this._builderCalls.map((c) => `\t${c}`),
166
+ `\t.withServices((cb) => { ${serviceRegs} })`,
167
+ ];
168
+
169
+ if (this._entities.length > 0) {
170
+ const entities = this._entities.map((e) => e.className).join(", ");
171
+ const repos = this._repositories.map((r) => r.className).join(", ");
172
+ const named = this._dataSourceName != null ? `name: "${this._dataSourceName}", ` : "";
173
+ if (this._typeOrmSeed) {
174
+ builderCalls.push(
175
+ `\t.withTypeOrm({ ${named}entities: [${entities}], repositories: [${repos}], seed: async (dataSource) => { ${this._typeOrmSeed} } })`,
176
+ );
177
+ } else {
178
+ builderCalls.push(
179
+ `\t.withTypeOrm({ ${named}entities: [${entities}], repositories: [${repos}] })`,
180
+ );
181
+ }
182
+ }
183
+
184
+ builderCalls.push(`\t.withControllers(HelloController)`);
185
+
186
+ const content = [
187
+ ...imports,
188
+ "",
189
+ `scribe.withPinoPretty().and.withPinoRoll({ file: "logs/${this.appName}.log" });`,
190
+ "",
191
+ `const app = await Builder.create({ applicationName: "${this.appName}" })`,
192
+ ...builderCalls,
193
+ `\t.build();`,
194
+ "",
195
+ "await app.start({ port: Number(process.env.PORT ?? 3000) });",
196
+ "",
197
+ "export default app;",
198
+ "",
199
+ ].join("\n");
200
+
201
+ return [
202
+ {
203
+ filePath: "src/main.ts",
204
+ content,
205
+ },
206
+ ];
207
+ }
208
+ }
@@ -0,0 +1,162 @@
1
+ import { WELL_KNOWN_FILES } from "../../models/const.js";
2
+ import type { KeyedModel, SourceFile } from "../../models/source-file.js";
3
+ import { DEFAULT_WEBAPI_NAME, webapiProjectDir, webapiReleaseEntry } from "../const.js";
4
+
5
+ export type ProductionDockerfileOptions = {
6
+ /** When true, only write `Dockerfile.<name>` (existing workspace — do not replace root Dockerfile). */
7
+ namedOnly?: boolean;
8
+ };
9
+
10
+ /**
11
+ * Production Dockerfiles for PaaS (Railway/Fly) and local `docker build`.
12
+ *
13
+ * Context is always the **repo root** (`docker build -f Dockerfile.billing .`).
14
+ * Root `Dockerfile` is the first API (`pnpm start`). Extra or custom-named APIs
15
+ * also get `Dockerfile.<name>` so each service can be deployed on its own.
16
+ *
17
+ * Runtime `node_modules` come from a **prod-deps** stage (`pnpm install --prod`),
18
+ * not the full build install (Forge, TypeScript, Biome, Vitest).
19
+ */
20
+ export class WebapiProductionDockerfile implements KeyedModel {
21
+ public readonly key = WELL_KNOWN_FILES.PRODUCTION_DOCKERFILE;
22
+
23
+ public constructor(
24
+ private readonly apiNames: readonly string[] = [DEFAULT_WEBAPI_NAME],
25
+ private readonly options: ProductionDockerfileOptions = {},
26
+ ) {}
27
+
28
+ public files(): SourceFile[] {
29
+ const names = this.apiNames.length > 0 ? this.apiNames : [DEFAULT_WEBAPI_NAME];
30
+ const first = names[0] ?? DEFAULT_WEBAPI_NAME;
31
+ const files: SourceFile[] = [];
32
+
33
+ if (!this.options.namedOnly) {
34
+ files.push({
35
+ filePath: ".dockerignore",
36
+ content: dockerignoreContent(),
37
+ });
38
+ files.push({
39
+ filePath: "Dockerfile",
40
+ content: renderProductionDockerfile(first, "start"),
41
+ });
42
+ }
43
+
44
+ const emitNamed =
45
+ this.options.namedOnly || names.length > 1 || first !== DEFAULT_WEBAPI_NAME;
46
+ if (emitNamed) {
47
+ for (const name of names) {
48
+ files.push({
49
+ filePath: `Dockerfile.${name}`,
50
+ content: renderProductionDockerfile(name, `start:${name}`),
51
+ });
52
+ }
53
+ }
54
+
55
+ return files;
56
+ }
57
+ }
58
+
59
+ export function renderProductionDockerfile(apiName: string, startScript: string): string {
60
+ const projectDir = webapiProjectDir(apiName);
61
+ const startCmd =
62
+ startScript === "start"
63
+ ? 'CMD ["pnpm", "start"]'
64
+ : `CMD ["pnpm", "run", "${startScript}"]`;
65
+ const startEntry = webapiReleaseEntry(apiName);
66
+ const dockerfileFlag = startScript === "start" ? "" : `.${apiName}`;
67
+
68
+ return `# syntax=docker/dockerfile:1
69
+ # Self-contained production image: install + forge build happen inside docker build.
70
+ # Hosts (Railway/Fly/etc.) clone git and run \`docker build\` — they do not receive CI dist/.
71
+ # Build from the repo root: docker build -f Dockerfile${dockerfileFlag} .
72
+ #
73
+ # Runtime node_modules are a production-only install (prod-deps). The build stage's
74
+ # full install (Forge, TypeScript, Biome, Vitest) is not copied into the run image.
75
+ #
76
+ # Private @decohere/* packages: pass a BuildKit secret named npm_token (npm auth token), e.g.
77
+ # docker build --secret id=npm_token,env=NODE_AUTH_TOKEN .
78
+ # Or set NODE_AUTH_TOKEN as a build-arg on hosts that only support build-args.
79
+
80
+ FROM node:24-bookworm AS build
81
+ WORKDIR /app
82
+ RUN corepack enable && corepack prepare pnpm@10 --activate
83
+
84
+ COPY package.json ./
85
+ COPY pnpm-lock.yaml* ./
86
+ COPY tsconfig.json biome.json ./
87
+ COPY config ./config
88
+ COPY ${projectDir} ./${projectDir}
89
+
90
+ ${renderPnpmInstall("all")}
91
+
92
+ RUN pnpm run build
93
+
94
+ FROM node:24-bookworm AS prod-deps
95
+ WORKDIR /app
96
+ ENV NODE_ENV=production
97
+ RUN corepack enable && corepack prepare pnpm@10 --activate
98
+
99
+ COPY package.json ./
100
+ COPY pnpm-lock.yaml* ./
101
+ COPY ${projectDir}/package.json ./${projectDir}/package.json
102
+
103
+ ${renderPnpmInstall("prod")}
104
+
105
+ FROM node:24-bookworm-slim AS run
106
+ WORKDIR /app
107
+ ENV NODE_ENV=production
108
+ RUN corepack enable && corepack prepare pnpm@10 --activate
109
+
110
+ COPY --from=prod-deps /app/package.json ./
111
+ COPY --from=prod-deps /app/pnpm-lock.yaml* ./
112
+ COPY --from=build /app/config ./config
113
+ COPY --from=build /app/dist ./dist
114
+ COPY --from=prod-deps /app/node_modules ./node_modules
115
+ COPY --from=prod-deps /app/${projectDir}/package.json ./${projectDir}/package.json
116
+
117
+ EXPOSE 3000
118
+ ${startCmd}
119
+ # Entry resolved by package.json "${startScript}" → node ${startEntry}
120
+ `;
121
+ }
122
+
123
+ /** npm auth + pnpm install. Token is required again in prod-deps (separate stage, no store). */
124
+ function renderPnpmInstall(mode: "all" | "prod"): string {
125
+ const flags = mode === "prod" ? " --prod" : "";
126
+ return `ARG NODE_AUTH_TOKEN=
127
+ RUN --mount=type=secret,id=npm_token,required=false \\
128
+ if [ -f /run/secrets/npm_token ]; then \\
129
+ TOKEN="$(cat /run/secrets/npm_token)"; \\
130
+ else \\
131
+ TOKEN="\${NODE_AUTH_TOKEN}"; \\
132
+ fi; \\
133
+ if [ -n "$TOKEN" ]; then \\
134
+ echo "//registry.npmjs.org/:_authToken=\${TOKEN}" > .npmrc; \\
135
+ fi; \\
136
+ if [ -f pnpm-lock.yaml ]; then \\
137
+ pnpm install --frozen-lockfile${flags}; \\
138
+ else \\
139
+ pnpm install${flags}; \\
140
+ fi; \\
141
+ rm -f .npmrc`;
142
+ }
143
+
144
+ function dockerignoreContent(): string {
145
+ return [
146
+ "node_modules",
147
+ "dist",
148
+ ".git",
149
+ ".devcontainer",
150
+ ".vscode",
151
+ ".github",
152
+ "postman",
153
+ ".postman",
154
+ ".env",
155
+ ".env.*",
156
+ "!.env.example",
157
+ "coverage",
158
+ "**/*.spec.ts",
159
+ "**/*.test.ts",
160
+ "",
161
+ ].join("\n");
162
+ }
@@ -0,0 +1,45 @@
1
+ import type { SourceFile, SourceFileProvider } from "../../models/source-file.js";
2
+
3
+ const SAMPLE_CONFIG_CONTENT = `export class SampleConfig {\n\tpublic greeting: string = "";\n}\n`;
4
+
5
+ const SAMPLE_SERVICE_CONTENT = [
6
+ `import { inject, injectable } from "@decohere/crucible/decorators";`,
7
+ `import { ConfigurationServiceToken } from "@decohere/nexus/configuration";`,
8
+ `import type { IConfigurationService } from "@decohere/nexus/configuration";`,
9
+ `import { SampleConfig } from "../sample.config.js";`,
10
+ ``,
11
+ `@injectable()`,
12
+ `export class SampleService {`,
13
+ `\tpublic constructor(`,
14
+ `\t\t@inject(ConfigurationServiceToken) private readonly config: IConfigurationService,`,
15
+ `\t) {}`,
16
+ ``,
17
+ `\tpublic async getGreeting(): Promise<string> {`,
18
+ `\t\tconst config = await this.config.get(SampleConfig);`,
19
+ `\t\treturn config.greeting;`,
20
+ `\t}`,
21
+ `}`,
22
+ ``,
23
+ ].join("\n");
24
+
25
+ export class SampleConfigModel implements SourceFileProvider {
26
+ public files(): SourceFile[] {
27
+ return [
28
+ {
29
+ filePath: "src/sample.config.ts",
30
+ content: SAMPLE_CONFIG_CONTENT,
31
+ },
32
+ ];
33
+ }
34
+ }
35
+
36
+ export class SampleServiceModel implements SourceFileProvider {
37
+ public files(): SourceFile[] {
38
+ return [
39
+ {
40
+ filePath: "src/services/sample.service.ts",
41
+ content: SAMPLE_SERVICE_CONTENT,
42
+ },
43
+ ];
44
+ }
45
+ }
@@ -0,0 +1,21 @@
1
+ import { DockerfileModel } from "../../models/docker-image.model.js";
2
+ import { CREATE_VERSION, DEVCONTAINER_IMAGE_REPOSITORY } from "../../version.js";
3
+
4
+ /**
5
+ * Scaffolded app `.devcontainer/Dockerfile` — thin `FROM` the published base image.
6
+ * Optional feature layers (e.g. postgresql-client) append via `addLayer`.
7
+ */
8
+ export class WebapiDevContainer extends DockerfileModel {
9
+ public constructor(baseImage = `${DEVCONTAINER_IMAGE_REPOSITORY}:${CREATE_VERSION}`) {
10
+ super(baseImage);
11
+ }
12
+
13
+ public files() {
14
+ return [
15
+ {
16
+ filePath: ".devcontainer/Dockerfile",
17
+ content: this.build(),
18
+ },
19
+ ];
20
+ }
21
+ }
@@ -0,0 +1,92 @@
1
+ /** Minimal OpenAPI 3 document shape used for Postman conversion. */
2
+ export type ScaffoldOpenApiDocument = {
3
+ openapi: "3.0.3";
4
+ info: { title: string; version: string; description?: string };
5
+ servers?: Array<{ url: string; description?: string }>;
6
+ paths: Record<string, Record<string, unknown>>;
7
+ };
8
+
9
+ /**
10
+ * Canonical Hello WebAPI OpenAPI document used to generate Postman requests.
11
+ * Keep paths aligned with scaffolded HelloController (`/hello`, `/hello/{id}`).
12
+ */
13
+ export function buildHelloOpenApiDocument(projectName: string): ScaffoldOpenApiDocument {
14
+ return {
15
+ openapi: "3.0.3",
16
+ info: {
17
+ title: `${projectName} WebAPI`,
18
+ version: "1.0.0",
19
+ description:
20
+ "Generated from OpenAPI for Postman. Auth/env overlays are applied separately.",
21
+ },
22
+ servers: [{ url: "{{baseUrl}}" }],
23
+ paths: {
24
+ "/hello": {
25
+ get: {
26
+ operationId: "HelloController.getAll",
27
+ summary: "List (anonymous)",
28
+ tags: ["Hello"],
29
+ parameters: [
30
+ { name: "filter", in: "query", required: false, schema: { type: "string" } },
31
+ ],
32
+ responses: { "200": { description: "Successful response" } },
33
+ },
34
+ post: {
35
+ operationId: "HelloController.create",
36
+ summary: "Create",
37
+ tags: ["Hello"],
38
+ requestBody: {
39
+ required: true,
40
+ content: {
41
+ "application/json": {
42
+ schema: {
43
+ type: "object",
44
+ properties: { message: { type: "string" } },
45
+ required: ["message"],
46
+ example: { message: "hello" },
47
+ },
48
+ },
49
+ },
50
+ },
51
+ responses: { "200": { description: "Successful response" } },
52
+ },
53
+ },
54
+ "/hello/{id}": {
55
+ get: {
56
+ operationId: "HelloController.getOne",
57
+ summary: "Get by id",
58
+ tags: ["Hello"],
59
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
60
+ responses: { "200": { description: "Successful response" } },
61
+ },
62
+ put: {
63
+ operationId: "HelloController.update",
64
+ summary: "Update",
65
+ tags: ["Hello"],
66
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
67
+ requestBody: {
68
+ required: true,
69
+ content: {
70
+ "application/json": {
71
+ schema: {
72
+ type: "object",
73
+ properties: { message: { type: "string" } },
74
+ required: ["message"],
75
+ example: { message: "updated" },
76
+ },
77
+ },
78
+ },
79
+ },
80
+ responses: { "200": { description: "Successful response" } },
81
+ },
82
+ delete: {
83
+ operationId: "HelloController.remove",
84
+ summary: "Delete",
85
+ tags: ["Hello"],
86
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
87
+ responses: { "200": { description: "Successful response" } },
88
+ },
89
+ },
90
+ },
91
+ };
92
+ }