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

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/LICENSE +15 -0
  2. package/commonjs/src/add-api.action.js +37 -0
  3. package/commonjs/src/add-api.command.js +39 -0
  4. package/commonjs/src/add.command.js +22 -0
  5. package/commonjs/src/add.module.js +24 -0
  6. package/commonjs/src/auth0-configure.action.js +28 -0
  7. package/commonjs/src/auth0-configure.command.js +24 -0
  8. package/commonjs/src/auth0-login.action.js +28 -0
  9. package/commonjs/src/auth0-login.command.js +23 -0
  10. package/commonjs/src/auth0.command.js +22 -0
  11. package/commonjs/src/auth0.module.js +28 -0
  12. package/commonjs/src/cli.js +8 -0
  13. package/commonjs/src/create.command.js +22 -0
  14. package/commonjs/src/create.module.js +24 -0
  15. package/commonjs/src/domain/prompt.js +62 -0
  16. package/commonjs/src/index.js +48 -0
  17. package/commonjs/src/models/compose.model.js +258 -0
  18. package/commonjs/src/models/config-yaml.model.js +86 -0
  19. package/commonjs/src/models/const.js +30 -0
  20. package/commonjs/src/models/dev-container.model.js +172 -0
  21. package/commonjs/src/models/docker-image.model.js +277 -0
  22. package/commonjs/src/models/env-file.model.js +48 -0
  23. package/commonjs/src/models/gitignore.model.js +48 -0
  24. package/commonjs/src/models/launch-json.model.js +70 -0
  25. package/commonjs/src/models/package-json.model.js +157 -0
  26. package/commonjs/src/models/project.model.js +132 -0
  27. package/commonjs/src/models/source-file.js +4 -0
  28. package/commonjs/src/models/tsconfig.model.js +32 -0
  29. package/commonjs/src/models/vitest-config.model.js +33 -0
  30. package/commonjs/src/models/vscode-settings.model.js +29 -0
  31. package/commonjs/src/program.js +64 -0
  32. package/commonjs/src/resolve-target-dir.js +17 -0
  33. package/commonjs/src/services/scaffold.service.js +60 -0
  34. package/commonjs/src/version.js +55 -0
  35. package/commonjs/src/webapi/api-name.js +93 -0
  36. package/commonjs/src/webapi/auth0-commands.js +250 -0
  37. package/commonjs/src/webapi/authorize-scopes.js +79 -0
  38. package/commonjs/src/webapi/cli-options.js +99 -0
  39. package/commonjs/src/webapi/const.js +54 -0
  40. package/commonjs/src/webapi/create-api.js +275 -0
  41. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.js +164 -0
  42. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
  43. package/commonjs/src/webapi/devcontainer/host-scripts.js +50 -0
  44. package/commonjs/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
  45. package/commonjs/src/webapi/devcontainer/templates/initialize.sh +169 -0
  46. package/commonjs/src/webapi/devcontainer/templates/post-create.sh +35 -0
  47. package/commonjs/src/webapi/devcontainer/templates/post-start.sh +65 -0
  48. package/commonjs/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
  49. package/commonjs/src/webapi/features/auth.feature.js +408 -0
  50. package/commonjs/src/webapi/features/auth0-cli.js +472 -0
  51. package/commonjs/src/webapi/features/biome-format.js +47 -0
  52. package/commonjs/src/webapi/features/database.feature.js +433 -0
  53. package/commonjs/src/webapi/features/finalize.feature.js +93 -0
  54. package/commonjs/src/webapi/features/postman.feature.js +322 -0
  55. package/commonjs/src/webapi/models/api-package-json.model.js +58 -0
  56. package/commonjs/src/webapi/models/auth-config.model.js +24 -0
  57. package/commonjs/src/webapi/models/controller-source-file.model.js +190 -0
  58. package/commonjs/src/webapi/models/github-ci.model.js +193 -0
  59. package/commonjs/src/webapi/models/main-source-file.model.js +165 -0
  60. package/commonjs/src/webapi/models/production-dockerfile.model.js +75 -0
  61. package/commonjs/src/webapi/models/sample-files.model.js +58 -0
  62. package/commonjs/src/webapi/models/webapi-image.model.js +25 -0
  63. package/commonjs/src/webapi/postman/hello-openapi.js +173 -0
  64. package/commonjs/src/webapi/postman/local-view.js +182 -0
  65. package/commonjs/src/webapi/postman/openapi-to-postman.js +238 -0
  66. package/commonjs/src/webapi/workspace-readme.js +109 -0
  67. package/commonjs/src/webapi.action.js +28 -0
  68. package/commonjs/src/webapi.command.js +43 -0
  69. package/es6/src/add-api.action.js +27 -0
  70. package/es6/src/add-api.command.js +29 -0
  71. package/es6/src/add.command.js +12 -0
  72. package/es6/src/add.module.js +14 -0
  73. package/es6/src/auth0-configure.action.js +18 -0
  74. package/es6/src/auth0-configure.command.js +14 -0
  75. package/es6/src/auth0-login.action.js +18 -0
  76. package/es6/src/auth0-login.command.js +13 -0
  77. package/es6/src/auth0.command.js +12 -0
  78. package/es6/src/auth0.module.js +18 -0
  79. package/es6/src/cli.js +4 -0
  80. package/es6/src/create.command.js +12 -0
  81. package/es6/src/create.module.js +14 -0
  82. package/es6/src/domain/prompt.js +41 -0
  83. package/es6/src/index.js +9 -0
  84. package/es6/src/models/compose.model.js +254 -0
  85. package/es6/src/models/config-yaml.model.js +74 -0
  86. package/es6/src/models/const.js +20 -0
  87. package/es6/src/models/dev-container.model.js +162 -0
  88. package/es6/src/models/docker-image.model.js +235 -0
  89. package/es6/src/models/env-file.model.js +44 -0
  90. package/es6/src/models/gitignore.model.js +38 -0
  91. package/es6/src/models/launch-json.model.js +60 -0
  92. package/es6/src/models/package-json.model.js +142 -0
  93. package/es6/src/models/project.model.js +117 -0
  94. package/es6/src/models/source-file.js +1 -0
  95. package/es6/src/models/tsconfig.model.js +22 -0
  96. package/es6/src/models/vitest-config.model.js +23 -0
  97. package/es6/src/models/vscode-settings.model.js +19 -0
  98. package/es6/src/program.js +50 -0
  99. package/es6/src/resolve-target-dir.js +7 -0
  100. package/es6/src/services/scaffold.service.js +50 -0
  101. package/es6/src/version.js +34 -0
  102. package/es6/src/webapi/api-name.js +57 -0
  103. package/es6/src/webapi/auth0-commands.js +185 -0
  104. package/es6/src/webapi/authorize-scopes.js +55 -0
  105. package/es6/src/webapi/cli-options.js +34 -0
  106. package/es6/src/webapi/const.js +21 -0
  107. package/es6/src/webapi/create-api.js +255 -0
  108. package/es6/src/webapi/devcontainer/codebase-rag-mcp.js +154 -0
  109. package/es6/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
  110. package/es6/src/webapi/devcontainer/host-scripts.js +29 -0
  111. package/es6/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
  112. package/es6/src/webapi/devcontainer/templates/initialize.sh +169 -0
  113. package/es6/src/webapi/devcontainer/templates/post-create.sh +35 -0
  114. package/es6/src/webapi/devcontainer/templates/post-start.sh +65 -0
  115. package/es6/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
  116. package/es6/src/webapi/features/auth.feature.js +357 -0
  117. package/es6/src/webapi/features/auth0-cli.js +406 -0
  118. package/es6/src/webapi/features/biome-format.js +40 -0
  119. package/es6/src/webapi/features/database.feature.js +423 -0
  120. package/es6/src/webapi/features/finalize.feature.js +83 -0
  121. package/es6/src/webapi/features/postman.feature.js +256 -0
  122. package/es6/src/webapi/models/api-package-json.model.js +53 -0
  123. package/es6/src/webapi/models/auth-config.model.js +20 -0
  124. package/es6/src/webapi/models/controller-source-file.model.js +181 -0
  125. package/{src/webapi/models/github-ci.model.ts → es6/src/webapi/models/github-ci.model.js} +27 -29
  126. package/es6/src/webapi/models/main-source-file.model.js +161 -0
  127. package/es6/src/webapi/models/production-dockerfile.model.js +66 -0
  128. package/es6/src/webapi/models/sample-files.model.js +40 -0
  129. package/es6/src/webapi/models/webapi-image.model.js +18 -0
  130. package/es6/src/webapi/postman/hello-openapi.js +166 -0
  131. package/es6/src/webapi/postman/local-view.js +167 -0
  132. package/es6/src/webapi/postman/openapi-to-postman.js +179 -0
  133. package/es6/src/webapi/workspace-readme.js +88 -0
  134. package/es6/src/webapi.action.js +18 -0
  135. package/es6/src/webapi.command.js +33 -0
  136. package/package.json +19 -9
  137. package/types/src/add-api.action.d.ts +13 -0
  138. package/types/src/add-api.command.d.ts +7 -0
  139. package/types/src/add.command.d.ts +6 -0
  140. package/types/src/add.module.d.ts +14 -0
  141. package/types/src/auth0-configure.action.d.ts +7 -0
  142. package/types/src/auth0-configure.command.d.ts +6 -0
  143. package/types/src/auth0-login.action.d.ts +4 -0
  144. package/types/src/auth0-login.command.d.ts +6 -0
  145. package/types/src/auth0.command.d.ts +6 -0
  146. package/types/src/auth0.module.d.ts +18 -0
  147. package/types/src/cli.d.ts +2 -0
  148. package/types/src/create.command.d.ts +6 -0
  149. package/types/src/create.module.d.ts +14 -0
  150. package/types/src/domain/prompt.d.ts +4 -0
  151. package/{src/index.ts → types/src/index.d.ts} +4 -0
  152. package/types/src/models/compose.model.d.ts +73 -0
  153. package/types/src/models/config-yaml.model.d.ts +27 -0
  154. package/types/src/models/const.d.ts +20 -0
  155. package/types/src/models/dev-container.model.d.ts +49 -0
  156. package/types/src/models/docker-image.model.d.ts +90 -0
  157. package/types/src/models/env-file.model.d.ts +16 -0
  158. package/types/src/models/gitignore.model.d.ts +9 -0
  159. package/types/src/models/launch-json.model.d.ts +7 -0
  160. package/types/src/models/package-json.model.d.ts +36 -0
  161. package/types/src/models/project.model.d.ts +60 -0
  162. package/{src/models/source-file.ts → types/src/models/source-file.d.ts} +4 -6
  163. package/types/src/models/tsconfig.model.d.ts +5 -0
  164. package/types/src/models/vitest-config.model.d.ts +5 -0
  165. package/types/src/models/vscode-settings.model.d.ts +7 -0
  166. package/types/src/program.d.ts +8 -0
  167. package/{src/resolve-target-dir.ts → types/src/resolve-target-dir.d.ts} +1 -4
  168. package/types/src/services/scaffold.service.d.ts +22 -0
  169. package/types/src/version.d.ts +6 -0
  170. package/types/src/webapi/api-name.d.ts +14 -0
  171. package/types/src/webapi/auth0-commands.d.ts +11 -0
  172. package/types/src/webapi/authorize-scopes.d.ts +7 -0
  173. package/types/src/webapi/cli-options.d.ts +6 -0
  174. package/{src/webapi/const.ts → types/src/webapi/const.d.ts} +9 -21
  175. package/types/src/webapi/create-api.d.ts +15 -0
  176. package/types/src/webapi/devcontainer/codebase-rag-mcp.d.ts +32 -0
  177. package/types/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
  178. package/types/src/webapi/devcontainer/host-scripts.d.ts +7 -0
  179. package/types/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
  180. package/types/src/webapi/devcontainer/templates/initialize.sh +169 -0
  181. package/types/src/webapi/devcontainer/templates/post-create.sh +35 -0
  182. package/types/src/webapi/devcontainer/templates/post-start.sh +65 -0
  183. package/types/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
  184. package/types/src/webapi/features/auth.feature.d.ts +10 -0
  185. package/types/src/webapi/features/auth0-cli.d.ts +84 -0
  186. package/types/src/webapi/features/biome-format.d.ts +5 -0
  187. package/types/src/webapi/features/database.feature.d.ts +7 -0
  188. package/types/src/webapi/features/finalize.feature.d.ts +2 -0
  189. package/types/src/webapi/features/postman.feature.d.ts +16 -0
  190. package/types/src/webapi/models/api-package-json.model.d.ts +18 -0
  191. package/types/src/webapi/models/auth-config.model.d.ts +26 -0
  192. package/types/src/webapi/models/controller-source-file.model.d.ts +46 -0
  193. package/types/src/webapi/models/github-ci.model.d.ts +9 -0
  194. package/types/src/webapi/models/main-source-file.model.d.ts +54 -0
  195. package/types/src/webapi/models/production-dockerfile.model.d.ts +18 -0
  196. package/types/src/webapi/models/sample-files.model.d.ts +7 -0
  197. package/types/src/webapi/models/webapi-image.model.d.ts +12 -0
  198. package/types/src/webapi/postman/hello-openapi.d.ts +19 -0
  199. package/types/src/webapi/postman/local-view.d.ts +17 -0
  200. package/types/src/webapi/postman/openapi-to-postman.d.ts +62 -0
  201. package/types/src/webapi/workspace-readme.d.ts +18 -0
  202. package/types/src/webapi.action.d.ts +14 -0
  203. package/types/src/webapi.command.d.ts +11 -0
  204. package/src/add-api.action.ts +0 -25
  205. package/src/add-api.command.ts +0 -31
  206. package/src/add.command.ts +0 -20
  207. package/src/add.module.ts +0 -11
  208. package/src/cli.ts +0 -4
  209. package/src/create.command.ts +0 -20
  210. package/src/create.module.ts +0 -11
  211. package/src/domain/prompt.ts +0 -50
  212. package/src/models/compose.model.ts +0 -205
  213. package/src/models/config-yaml.model.ts +0 -83
  214. package/src/models/const.ts +0 -20
  215. package/src/models/dev-container.model.ts +0 -136
  216. package/src/models/docker-image.model.ts +0 -343
  217. package/src/models/env-file.model.ts +0 -45
  218. package/src/models/gitignore.model.ts +0 -35
  219. package/src/models/launch-json.model.ts +0 -60
  220. package/src/models/package-json.model.ts +0 -169
  221. package/src/models/project.model.ts +0 -141
  222. package/src/models/tsconfig.model.ts +0 -29
  223. package/src/models/vitest-config.model.ts +0 -26
  224. package/src/models/vscode-settings.model.ts +0 -24
  225. package/src/program.ts +0 -63
  226. package/src/services/scaffold.service.ts +0 -89
  227. package/src/version.ts +0 -56
  228. package/src/webapi/api-name.ts +0 -73
  229. package/src/webapi/cli-options.ts +0 -56
  230. package/src/webapi/create-api.ts +0 -275
  231. package/src/webapi/devcontainer/host-scripts.ts +0 -35
  232. package/src/webapi/features/auth.feature.ts +0 -454
  233. package/src/webapi/features/auth0-cli.ts +0 -421
  234. package/src/webapi/features/biome-format.ts +0 -34
  235. package/src/webapi/features/database.feature.ts +0 -522
  236. package/src/webapi/features/finalize.feature.ts +0 -112
  237. package/src/webapi/features/postman.feature.ts +0 -287
  238. package/src/webapi/models/api-package-json.model.ts +0 -60
  239. package/src/webapi/models/auth-config.model.ts +0 -36
  240. package/src/webapi/models/controller-source-file.model.ts +0 -232
  241. package/src/webapi/models/main-source-file.model.ts +0 -208
  242. package/src/webapi/models/production-dockerfile.model.ts +0 -162
  243. package/src/webapi/models/sample-files.model.ts +0 -45
  244. package/src/webapi/models/webapi-image.model.ts +0 -21
  245. package/src/webapi/postman/hello-openapi.ts +0 -92
  246. package/src/webapi/postman/local-view.ts +0 -195
  247. package/src/webapi/postman/openapi-to-postman.ts +0 -208
  248. package/src/webapi/workspace-readme.ts +0 -192
  249. package/src/webapi.action.ts +0 -22
  250. package/src/webapi.command.ts +0 -41
  251. package/tests/auth-postman.wiring.spec.ts +0 -263
  252. package/tests/auth0-cli.spec.ts +0 -232
  253. package/tests/biome-format.spec.ts +0 -49
  254. package/tests/cli.integration.spec.ts +0 -658
  255. package/tests/compose.model.spec.ts +0 -51
  256. package/tests/container-image.model.spec.ts +0 -162
  257. package/tests/dev-container.model.spec.ts +0 -29
  258. package/tests/devcontainer-base-image.spec.ts +0 -32
  259. package/tests/hello-di-inject.scaffold.spec.ts +0 -18
  260. package/tests/hello-route.contract.spec.ts +0 -94
  261. package/tests/package-json.order.spec.ts +0 -59
  262. package/tests/postman-from-openapi.spec.ts +0 -197
  263. package/tests/seed-devcontainer-templates.ts +0 -40
  264. package/tests/webapi/devcontainer/codebase-rag-mcp.spec.ts +0 -96
@@ -0,0 +1,256 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { pick } from "../../domain/prompt.js";
5
+ import { WELL_KNOWN_FILES } from "../../models/const.js";
6
+ import { buildHelloOpenApiDocument } from "../postman/hello-openapi.js";
7
+ import { buildEnvironmentYaml, buildLocalViewFilesFromCollection } from "../postman/local-view.js";
8
+ import { applyPostmanAuthOverlay, openApiToPostmanCollection } from "../postman/openapi-to-postman.js";
9
+ import * as auth0Cli from "./auth0-cli.js";
10
+ /** Postman OAuth callback URLs (popup + "Authorize using browser"). */ const POSTMAN_CALLBACKS = [
11
+ "https://oauth.pstmn.io/v1/callback",
12
+ "https://oauth.pstmn.io/v1/browser-callback"
13
+ ];
14
+ /** Scopes requested for Auth0 access tokens (OIDC + Hello CASL permissions). */ const POSTMAN_SCOPES = [
15
+ "openid",
16
+ "profile",
17
+ "email",
18
+ "read:Hello",
19
+ "create:Hello",
20
+ "update:Hello",
21
+ "delete:Hello"
22
+ ].join(" ");
23
+ const COLLECTION_DESCRIPTION = "Open the project root in Postman Local View (not the postman/ folder). " + "Select the Local environment, then Authorization → Get New Access Token (PKCE). " + "Routes are generated from OpenAPI; Auth0 overlay is hand-authored.";
24
+ async function getOptions(_workspace, cliOptions) {
25
+ if (cliOptions.postmanProvider != null) return {};
26
+ const choices = [
27
+ {
28
+ label: "none",
29
+ description: "skip"
30
+ },
31
+ {
32
+ label: "local",
33
+ description: "Local View / Native Git files (no API key)"
34
+ },
35
+ {
36
+ label: "api",
37
+ description: "also upload to Postman.com (needs API key)"
38
+ }
39
+ ];
40
+ const selected = await pick("\n Postman: ", choices, (c)=>`${c.label} — ${c.description}`);
41
+ const provider = selected?.label ?? "none";
42
+ if (provider === "none") return {
43
+ postmanProvider: "none"
44
+ };
45
+ if (provider === "local") return {
46
+ postmanProvider: "local"
47
+ };
48
+ const apiKey = process.env.POSTMAN_API_KEY ?? await import("../../domain/prompt.js").then((m)=>m.ask("\n Postman API key (app.getpostman.com/settings/me/api-keys): "));
49
+ const headers = {
50
+ "X-Api-Key": apiKey,
51
+ "Content-Type": "application/json"
52
+ };
53
+ const wsRes = await fetch("https://api.getpostman.com/workspaces", {
54
+ headers
55
+ });
56
+ if (!wsRes.ok) {
57
+ console.error("\n Postman API key invalid — falling back to local files.");
58
+ return {
59
+ postmanProvider: "local"
60
+ };
61
+ }
62
+ const { workspaces } = await wsRes.json();
63
+ const wsChoices = [
64
+ {
65
+ label: "<new workspace>",
66
+ id: null
67
+ },
68
+ ...workspaces.map((w)=>({
69
+ label: `${w.name} (${w.type})`,
70
+ id: w.id
71
+ }))
72
+ ];
73
+ const wsPicked = await pick("\n Postman workspace: ", wsChoices, (c)=>c.label);
74
+ let workspaceId;
75
+ if (!wsPicked || wsPicked.id === null) {
76
+ const { ask } = await import("../../domain/prompt.js");
77
+ const wsName = await ask(` Workspace name: `);
78
+ const createRes = await fetch("https://api.getpostman.com/workspaces", {
79
+ method: "POST",
80
+ headers,
81
+ body: JSON.stringify({
82
+ workspace: {
83
+ name: wsName,
84
+ type: "personal"
85
+ }
86
+ })
87
+ });
88
+ const { workspace: ws } = await createRes.json();
89
+ workspaceId = ws.id;
90
+ console.log(` ✓ Workspace created: ${wsName}`);
91
+ } else {
92
+ workspaceId = wsPicked.id;
93
+ }
94
+ return {
95
+ postmanProvider: "api",
96
+ apiKey,
97
+ workspaceId
98
+ };
99
+ }
100
+ /** Build Postman collection: OpenAPI routes + Auth0 auth overlay. */ export async function buildHybridPostmanCollection(projectName, auth) {
101
+ const openapi = buildHelloOpenApiDocument(projectName);
102
+ const base = await openApiToPostmanCollection(openapi);
103
+ if (base.info._postman_id == null) {
104
+ base.info._postman_id = randomUUID();
105
+ }
106
+ return applyPostmanAuthOverlay(base, projectName, auth, {
107
+ callbacks: POSTMAN_CALLBACKS,
108
+ scopes: POSTMAN_SCOPES,
109
+ description: COLLECTION_DESCRIPTION
110
+ });
111
+ }
112
+ /** Read a previously scaffolded auth0ClientId so re-scaffold never blanks it. */ function readExistingSpaClientId(targetDir, projectName) {
113
+ const envPath = join(targetDir, "postman/environments", `${projectName}-Local.environment.yaml`);
114
+ if (!existsSync(envPath)) return undefined;
115
+ const text = readFileSync(envPath, "utf8");
116
+ const match = text.match(/key:\s*auth0ClientId\s*\n\s*value:\s*["']?([^"'\n]+)/);
117
+ const value = match?.[1]?.trim();
118
+ return value && value.length > 0 ? value : undefined;
119
+ }
120
+ /**
121
+ * Resolve the Postman SPA client id. Never returns empty — preserves an existing
122
+ * env value on Auth0 failure, otherwise throws so we do not write a blank env.
123
+ */ function resolveSpaClientId(workspace) {
124
+ const appName = `${workspace.name} Postman`;
125
+ try {
126
+ const app = auth0Cli.ensureSpaApp(appName, [
127
+ ...POSTMAN_CALLBACKS
128
+ ]);
129
+ console.log(app.reused ? ` ✓ Reusing Auth0 SPA app for Postman PKCE (${app.client_id})` : ` ✓ Auth0 SPA app created for Postman PKCE (${app.client_id})`);
130
+ try {
131
+ auth0Cli.enableConnectionForClient(auth0Cli.DB_CONNECTION_NAME, app.client_id);
132
+ console.log(` ✓ Enabled ${auth0Cli.DB_CONNECTION_NAME} on Postman SPA`);
133
+ } catch (err) {
134
+ console.error(` ✗ Failed to enable DB connection on SPA: ${err instanceof Error ? err.message : err}`);
135
+ }
136
+ return app.client_id;
137
+ } catch (err) {
138
+ const existing = readExistingSpaClientId(workspace.targetDir, workspace.name);
139
+ if (existing) {
140
+ console.error(` ✗ Failed to ensure Auth0 SPA app (${err instanceof Error ? err.message : err})`);
141
+ console.log(` ✓ Keeping existing auth0ClientId from Postman environment (${existing})`);
142
+ return existing;
143
+ }
144
+ const detail = err instanceof Error ? err.message : String(err);
145
+ // Entity-limit messages are already actionable; avoid burying them.
146
+ if (auth0Cli.isAuth0EntityLimitError(err)) {
147
+ throw new Error(detail);
148
+ }
149
+ throw new Error(`Failed to ensure Auth0 SPA app for Postman — auth0ClientId would be empty: ${detail}`);
150
+ }
151
+ }
152
+ async function scaffold(workspace, options) {
153
+ if (options.postmanProvider === "none" || options.postmanProvider == null) return;
154
+ const authConfigModel = workspace.getModel(WELL_KNOWN_FILES.AUTH_CONFIG);
155
+ const authCfg = authConfigModel?.config;
156
+ const spaClientId = authCfg?.issuerBaseUrl ? resolveSpaClientId(workspace) : undefined;
157
+ const auth0Env = authCfg?.issuerBaseUrl ? {
158
+ auth0Domain: authCfg.issuerBaseUrl.replace(/^https?:\/\//, ""),
159
+ auth0Audience: authCfg.audience,
160
+ auth0ClientId: spaClientId
161
+ } : undefined;
162
+ const projectName = workspace.name;
163
+ const envName = `${projectName} Local`;
164
+ const collection = await buildHybridPostmanCollection(projectName, auth0Env);
165
+ const localFiles = buildLocalViewFilesFromCollection(collection, projectName, auth0Env, {
166
+ callbacks: POSTMAN_CALLBACKS,
167
+ scopes: POSTMAN_SCOPES
168
+ });
169
+ const envYaml = buildEnvironmentYaml(envName, auth0Env, {
170
+ callbacks: POSTMAN_CALLBACKS,
171
+ scopes: POSTMAN_SCOPES
172
+ });
173
+ workspace.addFile({
174
+ files: ()=>[
175
+ {
176
+ filePath: ".postman/resources.yaml",
177
+ content: [
178
+ "# Open this project root in Postman (Files → Open folder), not the postman/ subfolder.",
179
+ "# Postman fills workspace.id when you connect Local View.",
180
+ "#",
181
+ "# All resources under postman/ are registered automatically.",
182
+ "# Route requests are generated from OpenAPI; Auth0 overlay is hand-authored.",
183
+ ""
184
+ ].join("\n")
185
+ },
186
+ {
187
+ filePath: `postman/environments/${projectName}-Local.environment.yaml`,
188
+ content: envYaml
189
+ },
190
+ ...localFiles
191
+ ]
192
+ });
193
+ if (options.postmanProvider === "api" && options.apiKey) {
194
+ const headers = {
195
+ "X-Api-Key": options.apiKey,
196
+ "Content-Type": "application/json"
197
+ };
198
+ const colRes = await fetch(`https://api.getpostman.com/collections?workspace=${options.workspaceId}`, {
199
+ method: "POST",
200
+ headers,
201
+ body: JSON.stringify({
202
+ collection
203
+ })
204
+ });
205
+ if (colRes.ok) {
206
+ console.log(" ✓ Postman collection uploaded");
207
+ } else {
208
+ console.error(" ✗ Failed to upload Postman collection");
209
+ }
210
+ const envValues = [
211
+ {
212
+ key: "baseUrl",
213
+ value: "http://localhost:3000",
214
+ enabled: true
215
+ },
216
+ ...auth0Env?.auth0Domain ? [
217
+ {
218
+ key: "auth0Domain",
219
+ value: auth0Env.auth0Domain,
220
+ enabled: true
221
+ },
222
+ {
223
+ key: "auth0Audience",
224
+ value: auth0Env.auth0Audience ?? "",
225
+ enabled: true
226
+ },
227
+ {
228
+ key: "auth0ClientId",
229
+ value: auth0Env.auth0ClientId ?? "",
230
+ enabled: true
231
+ }
232
+ ] : []
233
+ ];
234
+ const envRes = await fetch(`https://api.getpostman.com/environments?workspace=${options.workspaceId}`, {
235
+ method: "POST",
236
+ headers,
237
+ body: JSON.stringify({
238
+ environment: {
239
+ name: envName,
240
+ values: envValues
241
+ }
242
+ })
243
+ });
244
+ if (envRes.ok) {
245
+ console.log(" ✓ Postman environment uploaded");
246
+ } else {
247
+ console.error(" ✗ Failed to upload Postman environment");
248
+ }
249
+ }
250
+ }
251
+ export const PostmanFeature = {
252
+ getOptions,
253
+ scaffold
254
+ };
255
+ export { buildHelloOpenApiDocument } from "../postman/hello-openapi.js";
256
+ export { POSTMAN_CALLBACKS, POSTMAN_SCOPES };
@@ -0,0 +1,53 @@
1
+ import { WELL_KNOWN_FILES } from "../../models/const.js";
2
+ import { sortStringMap } from "../../models/package-json.model.js";
3
+ import { CREATE_VERSION } from "../../version.js";
4
+ /**
5
+ * Builder for `projects/<api>/package.json`.
6
+ *
7
+ * Starts with the core `@decohere/http` + `@decohere/crucible` deps. Features can call
8
+ * `addDependency` / `addDevDependency` to extend.
9
+ */ export class ApiPackageJson {
10
+ workspaceName;
11
+ apiName;
12
+ key = WELL_KNOWN_FILES.API_PACKAGE_JSON;
13
+ _dependencies;
14
+ _devDependencies = {};
15
+ constructor(workspaceName, apiName = "api"){
16
+ this.workspaceName = workspaceName;
17
+ this.apiName = apiName;
18
+ this._dependencies = {
19
+ "@decohere/http": CREATE_VERSION,
20
+ "@decohere/crucible": CREATE_VERSION
21
+ };
22
+ }
23
+ addDependency(name, version) {
24
+ this._dependencies[name] = version;
25
+ return this;
26
+ }
27
+ addDevDependency(name, version) {
28
+ this._devDependencies[name] = version;
29
+ return this;
30
+ }
31
+ files() {
32
+ const pkg = {
33
+ name: `@${this.workspaceName}/${this.apiName}`,
34
+ version: "0.0.1",
35
+ private: true,
36
+ type: "module",
37
+ main: "src/main.ts",
38
+ scripts: {
39
+ lint: "biome check --write ."
40
+ },
41
+ dependencies: sortStringMap(this._dependencies)
42
+ };
43
+ if (Object.keys(this._devDependencies).length > 0) {
44
+ pkg.devDependencies = sortStringMap(this._devDependencies);
45
+ }
46
+ return [
47
+ {
48
+ filePath: "package.json",
49
+ content: JSON.stringify(pkg, null, 2)
50
+ }
51
+ ];
52
+ }
53
+ }
@@ -0,0 +1,20 @@
1
+ import { WELL_KNOWN_FILES } from "../../models/const.js";
2
+ /**
3
+ * Data-carrier model for auth configuration.
4
+ *
5
+ * Registered by AuthFeature when auth0 is configured. Other features (e.g.
6
+ * PostmanFeature) retrieve it via workspace.getModel(WELL_KNOWN_FILES.AUTH_CONFIG).
7
+ * Produces no files — exists solely so features can communicate auth details.
8
+ */ export class AuthConfigModel {
9
+ config;
10
+ key = WELL_KNOWN_FILES.AUTH_CONFIG;
11
+ constructor(config){
12
+ this.config = config;
13
+ }
14
+ setAdminUser(adminUser) {
15
+ this.config.adminUser = adminUser;
16
+ }
17
+ files() {
18
+ return [];
19
+ }
20
+ }
@@ -0,0 +1,181 @@
1
+ import { WELL_KNOWN_FILES } from "../../models/const.js";
2
+ /** Tracks named imports from a single module path, split into values and types. */ class ImportGroup {
3
+ _values = [];
4
+ _types = [];
5
+ add(name, typeOnly = false) {
6
+ if (typeOnly) {
7
+ // Value import already present — value wins, skip
8
+ if (!this._values.includes(name) && !this._types.includes(name)) {
9
+ this._types.push(name);
10
+ }
11
+ } else {
12
+ // Promote: remove from types if it was previously type-only
13
+ const typeIdx = this._types.indexOf(name);
14
+ if (typeIdx !== -1) this._types.splice(typeIdx, 1);
15
+ if (!this._values.includes(name)) this._values.push(name);
16
+ }
17
+ return this;
18
+ }
19
+ render(from) {
20
+ const values = [
21
+ ...this._values
22
+ ].sort();
23
+ const types = [
24
+ ...this._types
25
+ ].sort().map((n)=>`type ${n}`);
26
+ return `import { ${[
27
+ ...values,
28
+ ...types
29
+ ].join(", ")} } from "${from}";`;
30
+ }
31
+ }
32
+ /**
33
+ * A controller method. Features call `addDecorator()` to prepend decorators
34
+ * before the HTTP verb decorator (e.g. `@AllowAnonymous` before `@Get()`).
35
+ */ export class ControllerMethod {
36
+ name;
37
+ _httpDecorator;
38
+ _params;
39
+ _extraDecorators = [];
40
+ _body;
41
+ _isAsync;
42
+ constructor(name, _httpDecorator, _params, isAsync, body){
43
+ this.name = name;
44
+ this._httpDecorator = _httpDecorator;
45
+ this._params = _params;
46
+ this._isAsync = isAsync;
47
+ this._body = body;
48
+ }
49
+ addDecorator(decorator) {
50
+ this._extraDecorators.push(decorator);
51
+ return this;
52
+ }
53
+ /** Replace the method body lines (used by features that extend generated behaviour). */ setBody(body) {
54
+ this._body = body;
55
+ return this;
56
+ }
57
+ setAsync(isAsync = true) {
58
+ this._isAsync = isAsync;
59
+ return this;
60
+ }
61
+ render() {
62
+ const lines = [];
63
+ for (const d of this._extraDecorators){
64
+ lines.push(`\t${d}`);
65
+ }
66
+ lines.push(`\t${this._httpDecorator}`);
67
+ const asyncKw = this._isAsync ? "async " : "";
68
+ lines.push(`\tpublic ${asyncKw}${this.name}(${this._params}) {`);
69
+ for (const line of this._body){
70
+ lines.push(`\t\t${line}`);
71
+ }
72
+ lines.push("\t}");
73
+ return lines;
74
+ }
75
+ }
76
+ /**
77
+ * Model for `projects/api/src/controllers/hello.controller.ts`.
78
+ *
79
+ * Pre-initialised with all routes and their HTTP decorators. Features mutate
80
+ * the model via `getMethod(name).addDecorator(...)`, `addDecorator(...)` (class
81
+ * level), `addCortexImport(...)`, `addImport(...)`, and `addConstructorParam(...)`.
82
+ */ export class ControllerSourceFile {
83
+ key = WELL_KNOWN_FILES.CONTROLLER_TS;
84
+ _cortexImports = new ImportGroup().add("Controller").add("Delete").add("Field").add("Get").add("Param").add("Post").add("Put").add("Query");
85
+ _extraImportGroups = new Map([
86
+ [
87
+ "@decohere/crucible/decorators",
88
+ new ImportGroup().add("inject")
89
+ ]
90
+ ]);
91
+ _classDecorators = [];
92
+ /** Explicit @inject(SampleService): using the class as a value keeps its import, which SWC can drop when the class is only a type (design:paramtypes then collapses to Object). */ _ctorParams = [
93
+ "@inject(SampleService) private readonly sample: SampleService"
94
+ ];
95
+ _methods = new Map([
96
+ [
97
+ "getAll",
98
+ new ControllerMethod("getAll", "@Get()", '@Query("filter") filter: string', true, [
99
+ "const greeting = await this.sample.getGreeting();",
100
+ "return { filter, message: greeting };"
101
+ ])
102
+ ],
103
+ [
104
+ "getOne",
105
+ new ControllerMethod("getOne", '@Get("/:id")', '@Param("id") id: string', false, [
106
+ "return { id, message: `Hello ${id}!` };"
107
+ ])
108
+ ],
109
+ [
110
+ "create",
111
+ new ControllerMethod("create", "@Post()", "body: HelloBody", false, [
112
+ "return { message: body.message };"
113
+ ])
114
+ ],
115
+ [
116
+ "update",
117
+ new ControllerMethod("update", '@Put("/:id")', '@Param("id") id: string, body: HelloBody', false, [
118
+ "return { id, message: body.message };"
119
+ ])
120
+ ],
121
+ [
122
+ "remove",
123
+ new ControllerMethod("remove", '@Delete("/:id")', '@Param("id") id: string', false, [
124
+ "return { id, deleted: true };"
125
+ ])
126
+ ]
127
+ ]);
128
+ /** Retrieve a method by name to mutate its decorators. */ getMethod(name) {
129
+ const m = this._methods.get(name);
130
+ if (m == null) throw new Error(`No method '${name}' on ControllerSourceFile`);
131
+ return m;
132
+ }
133
+ /** Add a class-level decorator (e.g. `@Authorize`). */ addDecorator(decorator) {
134
+ this._classDecorators.push(decorator);
135
+ return this;
136
+ }
137
+ /** Add a named import from `@decohere/http`. */ addCortexImport(name, typeOnly = false) {
138
+ this._cortexImports.add(name, typeOnly);
139
+ return this;
140
+ }
141
+ /** Add a named import from any other module. */ addImport(name, from, typeOnly = false) {
142
+ const group = this._extraImportGroups.get(from) ?? new ImportGroup();
143
+ group.add(name, typeOnly);
144
+ this._extraImportGroups.set(from, group);
145
+ return this;
146
+ }
147
+ /** Add a constructor parameter (e.g. an injected dependency). */ addConstructorParam(param) {
148
+ this._ctorParams.push(param);
149
+ return this;
150
+ }
151
+ files() {
152
+ const lines = [
153
+ this._cortexImports.render("@decohere/http"),
154
+ 'import { SampleService } from "../services/sample.service.js";'
155
+ ];
156
+ for (const [from, group] of this._extraImportGroups){
157
+ lines.push(group.render(from));
158
+ }
159
+ lines.push("", "class HelloBody {", '\t@Field("body", "message")', "\tpublic message!: string;", "}", "");
160
+ for (const d of this._classDecorators){
161
+ lines.push(d);
162
+ }
163
+ lines.push('@Controller({ prefix: "/hello" })', "export class HelloController {", "");
164
+ lines.push("\tpublic constructor(");
165
+ for (const p of this._ctorParams){
166
+ lines.push(`\t\t${p},`);
167
+ }
168
+ lines.push("\t) {}");
169
+ lines.push("");
170
+ for (const method of this._methods.values()){
171
+ lines.push(...method.render(), "");
172
+ }
173
+ lines.push("}", "");
174
+ return [
175
+ {
176
+ filePath: "src/controllers/hello.controller.ts",
177
+ content: lines.join("\n")
178
+ }
179
+ ];
180
+ }
181
+ }
@@ -1,18 +1,14 @@
1
1
  import { WELL_KNOWN_FILES } from "../../models/const.js";
2
- import type { KeyedModel, SourceFile } from "../../models/source-file.js";
3
-
4
2
  /**
5
3
  * Minimal GitHub Actions for scaffolded apps: lint ∥ test, build gate on default branch,
6
4
  * npm publish after CI (skips private packages). No default forge docker --push.
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
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
16
12
  description: Install pnpm and Node, configure npm registry auth, install deps
17
13
  inputs:
18
14
  node-version:
@@ -57,11 +53,11 @@ runs:
57
53
  else
58
54
  pnpm install
59
55
  fi
60
- `,
61
- },
62
- {
63
- filePath: ".github/workflows/ci.yml",
64
- content: `name: CI
56
+ `
57
+ },
58
+ {
59
+ filePath: ".github/workflows/ci.yml",
60
+ content: `name: CI
65
61
  on:
66
62
  push:
67
63
  branches:
@@ -119,9 +115,9 @@ jobs:
119
115
  - name: Test
120
116
  run: pnpm test
121
117
 
122
- - name: Build (default branch)
118
+ - name: Build and package (default branch)
123
119
  if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
124
- run: pnpm run build
120
+ run: pnpm run package
125
121
 
126
122
  ci:
127
123
  needs: [lint, test]
@@ -136,11 +132,11 @@ jobs:
136
132
  exit 1
137
133
  fi
138
134
  echo "CI passed"
139
- `,
140
- },
141
- {
142
- filePath: ".github/workflows/publish.yml",
143
- content: `name: Publish
135
+ `
136
+ },
137
+ {
138
+ filePath: ".github/workflows/publish.yml",
139
+ content: `name: Publish
144
140
 
145
141
  on:
146
142
  workflow_run:
@@ -179,10 +175,12 @@ jobs:
179
175
  - name: Publish packages
180
176
  run: pnpm exec forge publish --configuration release
181
177
  # Skips packages with "private": true (e.g. the webapi app).
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
- }
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
+ }
188
186
  }