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