@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,195 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ type Auth0Env,
4
+ openApiPathParamsToColon,
5
+ type PostmanCollection,
6
+ type PostmanItem,
7
+ type PostmanRequest,
8
+ } from "./openapi-to-postman.js";
9
+
10
+ function yamlQuote(value: string): string {
11
+ if (/[:#{}[\],&*?|<>=!%@`]/.test(value) || value.includes("\n") || value === "") {
12
+ return JSON.stringify(value);
13
+ }
14
+ return value;
15
+ }
16
+
17
+ /** Local View / Native Git environment (YAML). */
18
+ export function buildEnvironmentYaml(
19
+ name: string,
20
+ auth: Auth0Env | undefined,
21
+ options: { callbacks: readonly string[]; scopes: string },
22
+ ): string {
23
+ const values: Array<{ key: string; value: string }> = [
24
+ { key: "baseUrl", value: "http://localhost:3000" },
25
+ ];
26
+ if (auth?.auth0Domain) {
27
+ values.push(
28
+ { key: "auth0Domain", value: auth.auth0Domain },
29
+ { key: "auth0Audience", value: auth.auth0Audience ?? "" },
30
+ { key: "auth0ClientId", value: auth.auth0ClientId ?? "" },
31
+ {
32
+ key: "authUrl",
33
+ value: `https://${auth.auth0Domain}/authorize?audience=${auth.auth0Audience ?? ""}`,
34
+ },
35
+ {
36
+ key: "accessTokenUrl",
37
+ value: `https://${auth.auth0Domain}/oauth/token`,
38
+ },
39
+ { key: "callbackUrl", value: options.callbacks[0] },
40
+ { key: "scope", value: options.scopes },
41
+ );
42
+ }
43
+
44
+ const lines = [`name: ${yamlQuote(name)}`, "values:"];
45
+ for (const v of values) {
46
+ lines.push(` - key: ${v.key}`);
47
+ lines.push(` value: ${yamlQuote(v.value)}`);
48
+ lines.push(" enabled: true");
49
+ }
50
+ return `${lines.join("\n")}\n`;
51
+ }
52
+
53
+ /**
54
+ * Build Postman Local View multi-file YAML from a v2.1 collection (routes)
55
+ * plus hand-authored Auth0 overlay.
56
+ */
57
+ export function buildLocalViewFilesFromCollection(
58
+ collection: PostmanCollection,
59
+ projectName: string,
60
+ auth: Auth0Env | undefined,
61
+ options: { callbacks: readonly string[]; scopes: string },
62
+ ): Array<{ filePath: string; content: string }> {
63
+ const collectionDir = `postman/collections/${projectName} WebAPI`;
64
+ const files: Array<{ filePath: string; content: string }> = [];
65
+
66
+ const authBlock = auth?.auth0Domain
67
+ ? [
68
+ "auth:",
69
+ ` - id: ${randomUUID()}`,
70
+ " type: oauth2",
71
+ " name: OAuth 2.0",
72
+ " credentials:",
73
+ " addTokenTo: header",
74
+ " grant_type: authorization_code_with_pkce",
75
+ ` authUrl: https://${auth.auth0Domain}/authorize?audience=${auth.auth0Audience ?? ""}`,
76
+ ` accessTokenUrl: https://${auth.auth0Domain}/oauth/token`,
77
+ ` clientId: ${yamlQuote(auth.auth0ClientId ?? "")}`,
78
+ ` scope: ${yamlQuote(options.scopes)}`,
79
+ ` redirect_uri: ${options.callbacks[0]}`,
80
+ " useBrowser: true",
81
+ " challengeAlgorithm: S256",
82
+ " client_authentication: body",
83
+ ' clientSecret: ""',
84
+ ` tokenName: ${yamlQuote(`${projectName} Access Token`)}`,
85
+ "",
86
+ ].join("\n")
87
+ : "";
88
+
89
+ files.push({
90
+ filePath: `${collectionDir}/.resources/definition.yaml`,
91
+ content: [
92
+ "$kind: collection",
93
+ `name: ${yamlQuote(`${projectName} WebAPI`)}`,
94
+ "schemaVersion: '3.0'",
95
+ "description: >-",
96
+ " Open this project root in Postman Local View (Files → Open folder).",
97
+ " Select the Local environment, then Authorization → Get New Access Token (PKCE).",
98
+ " Route requests are generated from OpenAPI; Auth0 overlay is hand-authored.",
99
+ authBlock,
100
+ ]
101
+ .filter((l) => l !== undefined)
102
+ .join("\n")
103
+ .replace(/\n+$/, "\n"),
104
+ });
105
+
106
+ let folderOrder = 1000;
107
+ for (const folder of collection.item ?? []) {
108
+ if (folder.item == null) {
109
+ continue;
110
+ }
111
+ const folderName = folder.name ?? "Requests";
112
+ files.push({
113
+ filePath: `${collectionDir}/${folderName}/.resources/definition.yaml`,
114
+ content: [
115
+ "$kind: folder",
116
+ `name: ${yamlQuote(folderName)}`,
117
+ `order: ${folderOrder}`,
118
+ "",
119
+ ].join("\n"),
120
+ });
121
+ folderOrder += 1000;
122
+
123
+ let requestOrder = 1000;
124
+ for (const child of folder.item) {
125
+ const yaml = requestToLocalViewYaml(child, requestOrder);
126
+ if (yaml == null) {
127
+ continue;
128
+ }
129
+ const fileName = `${sanitizeFileName(child.name ?? "Request")}.request.yaml`;
130
+ files.push({
131
+ filePath: `${collectionDir}/${folderName}/${fileName}`,
132
+ content: yaml,
133
+ });
134
+ requestOrder += 1000;
135
+ }
136
+ }
137
+
138
+ return files;
139
+ }
140
+
141
+ function requestToLocalViewYaml(item: PostmanItem, order: number): string | null {
142
+ const request = item.request;
143
+ if (request == null) {
144
+ return null;
145
+ }
146
+ const method = (request.method ?? "GET").toUpperCase();
147
+ const url = requestUrlForLocalView(request);
148
+ const lines = [
149
+ "$kind: http-request",
150
+ `name: ${yamlQuote(item.name ?? method)}`,
151
+ `method: ${method}`,
152
+ `url: ${yamlQuote(url)}`,
153
+ ];
154
+
155
+ if (request.auth?.type === "noauth") {
156
+ lines.push("auth:", " type: noauth");
157
+ }
158
+
159
+ const contentType = request.header?.find(
160
+ (h) => h.key.toLowerCase() === "content-type",
161
+ )?.value;
162
+ const rawBody = request.body?.raw?.trim();
163
+ if (rawBody) {
164
+ lines.push("headers:");
165
+ lines.push(` Content-Type: ${contentType ?? "application/json"}`);
166
+ lines.push("body:");
167
+ lines.push(" type: json");
168
+ lines.push(" content: |-");
169
+ for (const bodyLine of rawBody.split("\n")) {
170
+ lines.push(` ${bodyLine}`);
171
+ }
172
+ }
173
+
174
+ lines.push(`order: ${order}`, "");
175
+ return lines.join("\n");
176
+ }
177
+
178
+ function requestUrlForLocalView(request: PostmanRequest): string {
179
+ const url = request.url;
180
+ if (typeof url === "string") {
181
+ return openApiPathParamsToColon(url);
182
+ }
183
+ if (url?.raw) {
184
+ return openApiPathParamsToColon(url.raw);
185
+ }
186
+ const host = url?.host?.[0] ?? "{{baseUrl}}";
187
+ const path = (url?.path ?? [])
188
+ .map((segment) => openApiPathParamsToColon(String(segment)))
189
+ .join("/");
190
+ return path ? `${host}/${path}` : host;
191
+ }
192
+
193
+ function sanitizeFileName(name: string): string {
194
+ return name.replace(/[^\w\s()-]+/g, "").trim() || "Request";
195
+ }
@@ -0,0 +1,208 @@
1
+ import converter from "openapi-to-postmanv2";
2
+ import type { ScaffoldOpenApiDocument } from "./hello-openapi.js";
3
+
4
+ export type PostmanCollection = {
5
+ info: {
6
+ _postman_id?: string;
7
+ name: string;
8
+ schema: string;
9
+ description?: string;
10
+ };
11
+ auth?: unknown;
12
+ item?: PostmanItem[];
13
+ variable?: Array<{ key: string; value: string }>;
14
+ [key: string]: unknown;
15
+ };
16
+
17
+ export type PostmanItem = {
18
+ name?: string;
19
+ item?: PostmanItem[];
20
+ request?: PostmanRequest;
21
+ [key: string]: unknown;
22
+ };
23
+
24
+ export type PostmanRequest = {
25
+ method?: string;
26
+ header?: Array<{ key: string; value: string }>;
27
+ body?: { mode?: string; raw?: string };
28
+ url?: string | { raw?: string; host?: string[]; path?: string[]; variable?: unknown[] };
29
+ auth?: { type: string; [key: string]: unknown };
30
+ [key: string]: unknown;
31
+ };
32
+
33
+ export type Auth0Env = {
34
+ auth0Domain?: string;
35
+ auth0Audience?: string;
36
+ auth0ClientId?: string;
37
+ };
38
+
39
+ type ConvertResult = {
40
+ result: boolean;
41
+ reason?: string;
42
+ output?: Array<{ type: string; data: PostmanCollection }>;
43
+ };
44
+
45
+ function convertAsync(
46
+ data: ScaffoldOpenApiDocument,
47
+ options: Record<string, unknown>,
48
+ ): Promise<ConvertResult> {
49
+ return new Promise((resolve, reject) => {
50
+ converter.convert(
51
+ { type: "json", data },
52
+ options,
53
+ (err: Error | null, result: ConvertResult) => {
54
+ if (err) {
55
+ reject(err);
56
+ return;
57
+ }
58
+ resolve(result);
59
+ },
60
+ );
61
+ });
62
+ }
63
+
64
+ /** Convert an OpenAPI document to a Postman Collection v2.1 via openapi-to-postmanv2. */
65
+ export async function openApiToPostmanCollection(
66
+ document: ScaffoldOpenApiDocument,
67
+ ): Promise<PostmanCollection> {
68
+ const result = await convertAsync(document, {
69
+ folderStrategy: "Tags",
70
+ requestNameSource: "Fallback",
71
+ includeAuthInfoInExample: false,
72
+ });
73
+ if (!result.result || result.output?.[0]?.data == null) {
74
+ throw new Error(result.reason ?? "openapi-to-postmanv2 conversion failed");
75
+ }
76
+ return result.output[0].data;
77
+ }
78
+
79
+ /**
80
+ * Overlay Auth0 OAuth2 (PKCE) collection auth and fix Local View-friendly URLs.
81
+ * Route items come from OpenAPI; auth/env remain hand-authored.
82
+ */
83
+ export function applyPostmanAuthOverlay(
84
+ collection: PostmanCollection,
85
+ projectName: string,
86
+ auth: Auth0Env | undefined,
87
+ options: {
88
+ callbacks: readonly string[];
89
+ scopes: string;
90
+ description: string;
91
+ },
92
+ ): PostmanCollection {
93
+ const next: PostmanCollection = {
94
+ ...collection,
95
+ info: {
96
+ ...collection.info,
97
+ name: `${projectName} WebAPI`,
98
+ description: options.description,
99
+ schema:
100
+ collection.info.schema ??
101
+ "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
102
+ },
103
+ };
104
+
105
+ if (auth?.auth0Domain) {
106
+ next.auth = {
107
+ type: "oauth2",
108
+ oauth2: [
109
+ { key: "grant_type", value: "authorization_code_with_pkce", type: "string" },
110
+ {
111
+ key: "authUrl",
112
+ value: "https://{{auth0Domain}}/authorize?audience={{auth0Audience}}",
113
+ type: "string",
114
+ },
115
+ {
116
+ key: "accessTokenUrl",
117
+ value: "https://{{auth0Domain}}/oauth/token",
118
+ type: "string",
119
+ },
120
+ { key: "clientId", value: "{{auth0ClientId}}", type: "string" },
121
+ { key: "scope", value: options.scopes, type: "string" },
122
+ { key: "redirect_uri", value: options.callbacks[0], type: "string" },
123
+ { key: "useBrowser", value: "true", type: "boolean" },
124
+ { key: "addTokenTo", value: "header", type: "string" },
125
+ { key: "challengeAlgorithm", value: "S256", type: "string" },
126
+ { key: "client_authentication", value: "body", type: "string" },
127
+ { key: "clientSecret", value: "", type: "string" },
128
+ { key: "tokenName", value: `${projectName} Access Token`, type: "string" },
129
+ ],
130
+ };
131
+ }
132
+
133
+ next.item = rewriteCollectionItems(collection.item ?? [], {
134
+ markListAnonymous: true,
135
+ });
136
+ return next;
137
+ }
138
+
139
+ function rewriteCollectionItems(
140
+ items: PostmanItem[],
141
+ opts: { markListAnonymous: boolean },
142
+ ): PostmanItem[] {
143
+ return items.map((item) => {
144
+ if (item.item != null) {
145
+ return { ...item, item: rewriteCollectionItems(item.item, opts) };
146
+ }
147
+ const request = item.request;
148
+ if (request == null) {
149
+ return item;
150
+ }
151
+ const method = (request.method ?? "GET").toUpperCase();
152
+ const rawUrl = requestUrlRaw(request);
153
+ const nextRequest: PostmanRequest = {
154
+ ...request,
155
+ url: toPostmanUrl(rawUrl),
156
+ };
157
+ // List Hello stays anonymous (matches scaffold AllowAnonymous on GET /hello).
158
+ if (
159
+ opts.markListAnonymous &&
160
+ method === "GET" &&
161
+ normalizePath(rawUrl) === "/hello" &&
162
+ !rawUrl.includes("/:")
163
+ ) {
164
+ nextRequest.auth = { type: "noauth" };
165
+ }
166
+ return { ...item, request: nextRequest };
167
+ });
168
+ }
169
+
170
+ function requestUrlRaw(request: PostmanRequest): string {
171
+ const url = request.url;
172
+ if (typeof url === "string") {
173
+ return url;
174
+ }
175
+ if (url?.raw) {
176
+ return url.raw;
177
+ }
178
+ const host = url?.host?.[0] ?? "{{baseUrl}}";
179
+ const path = (url?.path ?? []).join("/");
180
+ return path ? `${host}/${path}` : host;
181
+ }
182
+
183
+ /** OpenAPI `{id}` → Postman `:id`, without touching `{{variables}}`. */
184
+ export function openApiPathParamsToColon(url: string): string {
185
+ return url.replace(/(?<!\{)\{([A-Za-z_][\w]*)\}(?!\})/g, ":$1");
186
+ }
187
+
188
+ function normalizePath(rawUrl: string): string {
189
+ const withoutBase = rawUrl.replace(/\{\{baseUrl\}\}/g, "").split("?")[0] ?? "";
190
+ const withSlash = withoutBase.startsWith("/") ? withoutBase : `/${withoutBase}`;
191
+ return openApiPathParamsToColon(withSlash);
192
+ }
193
+
194
+ /** Prefer Postman template URL shape used by existing Local View / API uploads. */
195
+ function toPostmanUrl(rawUrl: string): PostmanRequest["url"] {
196
+ let pathPart = rawUrl.replace(/\{\{baseUrl\}\}/g, "");
197
+ if (!pathPart.startsWith("/")) {
198
+ pathPart = `/${pathPart}`;
199
+ }
200
+ // openapi-to-postman uses {id}; Local View / prior collections use :id
201
+ pathPart = openApiPathParamsToColon(pathPart);
202
+ const segments = pathPart.split("/").filter(Boolean);
203
+ return {
204
+ raw: `{{baseUrl}}${pathPart}`,
205
+ host: ["{{baseUrl}}"],
206
+ path: segments,
207
+ };
208
+ }
@@ -0,0 +1,192 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { WELL_KNOWN_FILES } from "../models/const.js";
4
+ import type { EnvFile } from "../models/env-file.model.js";
5
+ import type { PackageJson } from "../models/package-json.model.js";
6
+ import type { WorkspaceScaffold } from "../models/project.model.js";
7
+ import { apiNameFromProjectDir, listExistingApiNames } from "./api-name.js";
8
+
9
+ export type WorkspaceReadmeOptions = {
10
+ add?: boolean;
11
+ databaseProvider?: "none" | "postgres/typeorm";
12
+ authProvider?: "auth0" | "none";
13
+ postmanProvider?: "api" | "local" | "none";
14
+ };
15
+
16
+ export type WorkspaceReadmeInput = {
17
+ projectName: string;
18
+ apiNames: readonly string[];
19
+ hasPostgres: boolean;
20
+ hasAuth0: boolean;
21
+ hasPostman: boolean;
22
+ };
23
+
24
+ /** Pure renderer — keep feature sections short and copy-paste ready. */
25
+ export function renderWorkspaceReadme(input: WorkspaceReadmeInput): string {
26
+ const { projectName, apiNames, hasPostgres, hasAuth0, hasPostman } = input;
27
+ const apis = apiNames.length > 0 ? apiNames : ["api"];
28
+ const lines: string[] = [
29
+ `# ${projectName}`,
30
+ "",
31
+ "Decohere web API workspace. Scaffolded with [`@decohere/create`](https://www.npmjs.com/package/@decohere/create).",
32
+ "",
33
+ "## Get started",
34
+ "",
35
+ "1. Open this folder in VS Code / Cursor.",
36
+ "2. **Reopen in Container** (or rebuild the Dev Container).",
37
+ " Host `initialize.sh` and container `post-start.sh` ask y/N (TTY only) before npm auth or codebase RAG MCP.",
38
+ " Skip or decline; missing TTY skips. RAG credentials land in `.cursor/` (gitignored).",
39
+ "3. Install dependencies if needed:",
40
+ "",
41
+ "```bash",
42
+ "pnpm install",
43
+ "```",
44
+ "",
45
+ "## Develop",
46
+ "",
47
+ "```bash",
48
+ "pnpm dev # Flux HMR (first API)",
49
+ "pnpm start # run the compiled first API",
50
+ "pnpm run build # forge release build",
51
+ ];
52
+
53
+ for (const name of apis) {
54
+ lines.push(`pnpm run dev:${name} # HMR for ${name}`);
55
+ lines.push(`pnpm run start:${name} # production entry for ${name}`);
56
+ }
57
+
58
+ lines.push("```", "");
59
+
60
+ lines.push(
61
+ "## Logging",
62
+ "",
63
+ "In development, Scribe uses pino-pretty and rolling files under `logs/` (gitignored).",
64
+ "When `NODE_ENV=production`, the process writes structured JSON to stdout and does not rotate files.",
65
+ "Ship and retain those streams outside the app (platform logs, a sidecar, or a log agent) so shipping can change without rebuilding.",
66
+ "",
67
+ "Override roll path and rotation without changing app code:",
68
+ "`SCRIBE_LOG_FILE`, `SCRIBE_LOG_SIZE`, `SCRIBE_LOG_FREQUENCY`, `SCRIBE_LOG_KEEP`.",
69
+ "",
70
+ 'Opt into in-process pretty or roll in production only with `{ when: "always" }` on `scribe.withPinoPretty` / `scribe.withPinoRoll`.',
71
+ "",
72
+ );
73
+
74
+ if (hasPostgres) {
75
+ lines.push(
76
+ "## Database",
77
+ "",
78
+ "Postgres runs via Dev Container Compose:",
79
+ "",
80
+ "```bash",
81
+ "pnpm db:up",
82
+ "pnpm db:down",
83
+ "pnpm db:reset",
84
+ "```",
85
+ "",
86
+ );
87
+ }
88
+
89
+ lines.push(
90
+ "## Deploy",
91
+ "",
92
+ "PaaS hosts (`docker build` from the repo root) use the scaffolded `Dockerfile`.",
93
+ "The runtime image installs production dependencies only (`pnpm install --prod`);",
94
+ "Forge, TypeScript, Biome, and Vitest stay in the build stage.",
95
+ "",
96
+ "```bash",
97
+ `docker build -t ${projectName} .`,
98
+ "```",
99
+ "",
100
+ "Extra APIs: `docker build -f Dockerfile.<name> .`",
101
+ "",
102
+ );
103
+
104
+ lines.push(
105
+ "## Add another API",
106
+ "",
107
+ "```bash",
108
+ "decohere add api <name>",
109
+ "```",
110
+ "",
111
+ "Example: `decohere add api billing`",
112
+ "",
113
+ );
114
+
115
+ if (hasAuth0) {
116
+ lines.push(
117
+ "## Auth0",
118
+ "",
119
+ "JWT auth is wired from `.env` (`AUTH0_ISSUER_BASE_URL`, `AUTH0_AUDIENCE`).",
120
+ "Set those values (or re-run scaffold with `--auth auth0`) before calling protected routes.",
121
+ "",
122
+ );
123
+ }
124
+
125
+ if (hasPostman) {
126
+ lines.push(
127
+ "## Postman",
128
+ "",
129
+ "Open this **project root** in Postman Local View (Files → Open folder), not the `postman/` subfolder.",
130
+ "Select the Local environment, then Authorization → Get New Access Token when Auth0 is enabled.",
131
+ "",
132
+ );
133
+ }
134
+
135
+ lines.push(
136
+ "## More",
137
+ "",
138
+ "- Package docs: https://www.npmjs.com/package/@decohere/create",
139
+ "- After `decohere add api`, this README is refreshed so scripts and APIs stay accurate.",
140
+ "",
141
+ );
142
+
143
+ return `${lines.join("\n")}`;
144
+ }
145
+
146
+ export function resolveWorkspaceReadmeInput(
147
+ workspace: WorkspaceScaffold,
148
+ options: WorkspaceReadmeOptions,
149
+ ): WorkspaceReadmeInput {
150
+ const newNames = workspace
151
+ .getProjects()
152
+ .map((project) => apiNameFromProjectDir(project.dir));
153
+ const apiNames = options.add
154
+ ? mergeApiNames(listExistingApiNames(workspace.targetDir), newNames)
155
+ : newNames;
156
+
157
+ const pkg = workspace.getModel<PackageJson>(WELL_KNOWN_FILES.PACKAGE_JSON);
158
+ const env = workspace.getModel<EnvFile>(WELL_KNOWN_FILES.ENV_FILE);
159
+
160
+ const hasPostgres =
161
+ options.databaseProvider === "postgres/typeorm" || Boolean(pkg?.hasScript("db:up"));
162
+ const hasAuth0 =
163
+ options.authProvider === "auth0" || Boolean(env?.hasEntry("AUTH0_ISSUER_BASE_URL"));
164
+ const hasPostman =
165
+ options.postmanProvider === "local" ||
166
+ options.postmanProvider === "api" ||
167
+ existsSync(join(workspace.targetDir, "postman")) ||
168
+ existsSync(join(workspace.targetDir, ".postman"));
169
+
170
+ return {
171
+ projectName: workspace.name,
172
+ apiNames,
173
+ hasPostgres,
174
+ hasAuth0,
175
+ hasPostman,
176
+ };
177
+ }
178
+
179
+ export function writeWorkspaceReadme(
180
+ workspace: WorkspaceScaffold,
181
+ options: WorkspaceReadmeOptions,
182
+ ): void {
183
+ const content = renderWorkspaceReadme(resolveWorkspaceReadmeInput(workspace, options));
184
+ workspace.addFile({
185
+ files: () => [{ filePath: "README.md", content }],
186
+ });
187
+ }
188
+
189
+ function mergeApiNames(existing: readonly string[], added: readonly string[]): string[] {
190
+ const seen = new Set(existing);
191
+ return [...existing, ...added.filter((name) => !seen.has(name))];
192
+ }
@@ -0,0 +1,22 @@
1
+ import { injectable } from "@decohere/crucible/decorators";
2
+ import { type Action, ActionResponse } from "@decohere/nexus/cli";
3
+ import { runScaffoldApi, toApiOptions } from "./webapi/cli-options.js";
4
+
5
+ export type WebapiOptions = {
6
+ directory?: string;
7
+ d?: string;
8
+ name?: string | string[];
9
+ database?: string;
10
+ auth?: string;
11
+ postman?: string;
12
+ yes?: boolean;
13
+ install?: string;
14
+ };
15
+
16
+ @injectable()
17
+ export class WebapiAction implements Action<WebapiOptions> {
18
+ public async run(options: WebapiOptions): Promise<ActionResponse> {
19
+ await runScaffoldApi(toApiOptions(options.directory, options));
20
+ return ActionResponse.Success("webapi");
21
+ }
22
+ }
@@ -0,0 +1,41 @@
1
+ import { DecohereCommand } from "@decohere/nexus/cli";
2
+ import type { OptionValues } from "commander";
3
+ import { addWebapiFeatureOptions, collectName } from "./webapi/cli-options.js";
4
+ import { WebapiAction } from "./webapi.action.js";
5
+
6
+ export class WebapiCommand extends DecohereCommand {
7
+ public override get actions() {
8
+ return [WebapiAction];
9
+ }
10
+
11
+ public constructor(name: string) {
12
+ super(name);
13
+ this.description("Scaffold a Cortex web API workspace");
14
+ this.argument("[directory]", "Target directory (default: -d or current directory)");
15
+ this.option("-d <directory>", "Target directory (default: current directory)", ".");
16
+ this.option(
17
+ "--name <slug>",
18
+ "API name under projects/ (repeatable; default: api)",
19
+ collectName,
20
+ [] as string[],
21
+ );
22
+ addWebapiFeatureOptions(this);
23
+ }
24
+
25
+ /**
26
+ * Commander passes positional args before options when `[directory]` is declared.
27
+ * ModuleFactory expects `(opts, cmd)` — stash the directory on the command.
28
+ */
29
+ public override action(fn: (...args: unknown[]) => void | Promise<void>): this {
30
+ return super.action((...args: unknown[]) => {
31
+ const cmd = args.at(-1) as DecohereCommand;
32
+ const first = args[0];
33
+ const hasPositional = typeof first === "string" || first === undefined;
34
+ const opts = hasPositional ? (args[1] as OptionValues) : (args[0] as OptionValues);
35
+ if (typeof first === "string" && first !== "") {
36
+ cmd.setOptionValue("directory", first);
37
+ }
38
+ return fn(opts, cmd);
39
+ });
40
+ }
41
+ }