@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,162 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { WELL_KNOWN_FILES } from "./const.js";
3
+ export class DevContainer {
4
+ key = WELL_KNOWN_FILES.DEVCONTAINER_JSON;
5
+ _extensions = [];
6
+ _ports = [];
7
+ _portAttributes = new Map();
8
+ _mounts;
9
+ _workspaceFolder = "/workspace";
10
+ _postCreateCommand;
11
+ _updateContentCommand;
12
+ _postStartCommand;
13
+ _initializeCommand;
14
+ _remoteUser = "node";
15
+ _useCompose = false;
16
+ _existing;
17
+ /**
18
+ * Load an existing devcontainer.json so `decohere add api` can merge into it.
19
+ * Only extensions, forwarded ports and port attributes are merged; everything else is kept.
20
+ */ static fromFile(absolutePath) {
21
+ const devContainer = new DevContainer();
22
+ try {
23
+ devContainer._existing = JSON.parse(readFileSync(absolutePath, "utf8"));
24
+ } catch (error) {
25
+ throw new Error(`Can't update ${absolutePath}: it isn't plain JSON (${error.message}). Remove any comments or trailing commas and try again.`);
26
+ }
27
+ return devContainer;
28
+ }
29
+ constructor(){
30
+ this._mounts = [
31
+ this.bindMount(this._workspaceFolder)
32
+ ];
33
+ }
34
+ /** In-container path Cursor/VS Code opens (Agents window uses the basename). */ setWorkspaceFolder(folder) {
35
+ this._workspaceFolder = folder;
36
+ this._mounts = [
37
+ this.bindMount(folder)
38
+ ];
39
+ return this;
40
+ }
41
+ bindMount(target) {
42
+ return `source=..,target=${target},type=bind,consistency=cached`;
43
+ }
44
+ addExtension(...ids) {
45
+ this._extensions.push(...ids);
46
+ return this;
47
+ }
48
+ forwardPort(...ports) {
49
+ this._ports.push(...ports);
50
+ return this;
51
+ }
52
+ setPortAttributes(port, attrs) {
53
+ this._portAttributes.set(port, attrs);
54
+ return this;
55
+ }
56
+ setPostCreateCommand(cmd) {
57
+ this._postCreateCommand = cmd;
58
+ return this;
59
+ }
60
+ setUpdateContentCommand(cmd) {
61
+ this._updateContentCommand = cmd;
62
+ return this;
63
+ }
64
+ setPostStartCommand(cmd) {
65
+ this._postStartCommand = cmd;
66
+ return this;
67
+ }
68
+ setInitializeCommand(cmd) {
69
+ this._initializeCommand = cmd;
70
+ return this;
71
+ }
72
+ setRemoteUser(user) {
73
+ this._remoteUser = user;
74
+ return this;
75
+ }
76
+ useCompose() {
77
+ this._useCompose = true;
78
+ return this;
79
+ }
80
+ files() {
81
+ const config = this._existing ? this.mergeIntoExisting(this._existing) : this.build();
82
+ return [
83
+ {
84
+ filePath: ".devcontainer/devcontainer.json",
85
+ content: JSON.stringify(config, null, 2)
86
+ }
87
+ ];
88
+ }
89
+ mergeIntoExisting(existing) {
90
+ const config = structuredClone(existing);
91
+ if (this._extensions.length > 0) {
92
+ const customizations = objectAt(config, "customizations");
93
+ const vscode = objectAt(customizations, "vscode");
94
+ vscode.extensions = union(vscode.extensions, this._extensions);
95
+ }
96
+ if (this._ports.length > 0) {
97
+ config.forwardPorts = union(config.forwardPorts, this._ports);
98
+ }
99
+ if (this._portAttributes.size > 0) {
100
+ const attributes = objectAt(config, "portsAttributes");
101
+ for (const [port, attrs] of this._portAttributes){
102
+ attributes[String(port)] ??= attrs;
103
+ }
104
+ }
105
+ return config;
106
+ }
107
+ build() {
108
+ const config = {};
109
+ if (this._useCompose) {
110
+ config.dockerComposeFile = "docker-compose.yml";
111
+ config.service = "devcontainer";
112
+ config.workspaceFolder = this._workspaceFolder;
113
+ }
114
+ if (this._initializeCommand) {
115
+ config.initializeCommand = this._initializeCommand;
116
+ }
117
+ if (this._mounts) {
118
+ config.mounts = this._mounts;
119
+ }
120
+ if (this._extensions.length > 0) {
121
+ config.customizations = {
122
+ vscode: {
123
+ extensions: this._extensions
124
+ }
125
+ };
126
+ }
127
+ if (this._ports.length > 0) {
128
+ config.forwardPorts = this._ports;
129
+ }
130
+ if (this._portAttributes.size > 0) {
131
+ const portsAttributes = {};
132
+ for (const [port, attrs] of this._portAttributes){
133
+ portsAttributes[String(port)] = attrs;
134
+ }
135
+ config.portsAttributes = portsAttributes;
136
+ }
137
+ if (this._postCreateCommand) {
138
+ config.postCreateCommand = this._postCreateCommand;
139
+ }
140
+ if (this._updateContentCommand) {
141
+ config.updateContentCommand = this._updateContentCommand;
142
+ }
143
+ if (this._postStartCommand) {
144
+ config.postStartCommand = this._postStartCommand;
145
+ }
146
+ if (this._remoteUser) {
147
+ config.remoteUser = this._remoteUser;
148
+ }
149
+ return config;
150
+ }
151
+ }
152
+ /** The object at `parent[key]`, created when missing. */ function objectAt(parent, key) {
153
+ parent[key] ??= {};
154
+ return parent[key];
155
+ }
156
+ function union(existing, additions) {
157
+ const items = Array.isArray(existing) ? [
158
+ ...existing
159
+ ] : [];
160
+ for (const item of additions)if (!items.includes(item)) items.push(item);
161
+ return items;
162
+ }
@@ -0,0 +1,235 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { WELL_KNOWN_FILES } from "./const.js";
3
+ export class DockerStep {
4
+ key;
5
+ content;
6
+ order;
7
+ constructor(key, content, order = 0){
8
+ this.key = key;
9
+ this.content = content;
10
+ this.order = order;
11
+ }
12
+ static run(key, command, order = 0) {
13
+ return new DockerStep(key, `RUN ${command}`, order);
14
+ }
15
+ static raw(key, content, order = 0) {
16
+ return new DockerStep(key, content, order);
17
+ }
18
+ static entrypoint(key, value, order = 0) {
19
+ const content = typeof value === "string" ? `ENTRYPOINT ${value}` : `ENTRYPOINT ${JSON.stringify(value)}`;
20
+ return new DockerStep(key, content, order);
21
+ }
22
+ }
23
+ const StepOrder = {
24
+ base: 1_000,
25
+ features: 5_000,
26
+ user: 9_000,
27
+ entrypoint: 10_000
28
+ };
29
+ const VSCODE_BOOTLOADER_FIX_BASH_SNIPPET = `if [[ -n "\${NODE_OPTIONS:-}" && "\${NODE_OPTIONS}" == *"bootloader.js"* ]]; then
30
+ _bootloader_path="$(printf '%s\\n' "$NODE_OPTIONS" | sed -n 's/.*--require[= ]\\([^[:space:]]*bootloader\\.js\\).*/\\1/p')"
31
+ if [[ -n "\${_bootloader_path}" && ! -f "\${_bootloader_path}" ]]; then
32
+ _stable_bootloader="$(find /vscode/vscode-server /home/node/.vscode-server/extensions -name "bootloader.js" -path "*/ms-vscode.js-debug/*" 2>/dev/null | head -1)"
33
+ if [[ -n "\${_stable_bootloader}" ]]; then
34
+ NODE_OPTIONS="\${NODE_OPTIONS//\${_bootloader_path}/\${_stable_bootloader}}"
35
+ else
36
+ NODE_OPTIONS="$(printf '%s\\n' "$NODE_OPTIONS" | sed -E 's#(^| )--require(=| )[[:graph:]]*bootloader\\.js( |$)# #g; s# +# #g; s#^ ##; s# $##')"
37
+ unset VSCODE_INSPECTOR_OPTIONS
38
+ fi
39
+ export NODE_OPTIONS
40
+ if [[ -z "$NODE_OPTIONS" ]]; then
41
+ unset NODE_OPTIONS
42
+ fi
43
+ fi
44
+ unset _bootloader_path _stable_bootloader
45
+ fi`;
46
+ /** Named volume + paths for interactive bash history in app Dev Containers. */ export const commandHistory = {
47
+ volumeName: "bashhistory",
48
+ dir: "/commandhistory",
49
+ fileName: ".bash_history",
50
+ histFile: "/commandhistory/.bash_history"
51
+ };
52
+ export function withCommandHistory(options) {
53
+ const historyDir = options?.historyDir ?? commandHistory.dir;
54
+ const historyFileName = options?.historyFileName ?? commandHistory.fileName;
55
+ const username = options?.username ?? "node";
56
+ const order = options?.order ?? StepOrder.base;
57
+ const historyFile = `${historyDir}/${historyFileName}`;
58
+ return (dockerfile)=>{
59
+ dockerfile.addStep(DockerStep.run("history-dir", `mkdir -p ${historyDir} && touch ${historyFile} && chown -R ${username} ${historyDir}`, order)).addStep(DockerStep.raw("history-env", `ENV HISTFILE=${historyFile}`, order + 1)).addStep(DockerStep.raw("history-bashrc", `RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=${historyFile}" \\\n\t&& echo "$SNIPPET" >> "/home/${username}/.bashrc"`, order + 2));
60
+ };
61
+ }
62
+ export function withNodeUser(options) {
63
+ const username = options?.username ?? "node";
64
+ const includeUserInstruction = options?.includeUserInstruction ?? true;
65
+ const order = options?.order ?? StepOrder.features;
66
+ return (dockerfile)=>{
67
+ dockerfile.addStep(DockerStep.raw("arg-username", `ARG USERNAME=${username}`, order));
68
+ if (includeUserInstruction) {
69
+ dockerfile.addStep(DockerStep.raw("user", "USER ${USERNAME}", StepOrder.user));
70
+ }
71
+ };
72
+ }
73
+ export function withPnpm(options) {
74
+ const version = options?.version ?? "10";
75
+ const usernameExpr = options?.usernameExpr ?? "${USERNAME}";
76
+ const order = options?.order ?? StepOrder.features + 100;
77
+ return (dockerfile)=>{
78
+ dockerfile.addStep(DockerStep.run("pnpm-install", `npm install --global npm@latest && npm add --global pnpm@${version}`, order)).addStep(DockerStep.run("pnpm-config", `mkdir -p /home/${usernameExpr}/.config/pnpm /home/${usernameExpr}/.pnpm-store && chown -R ${usernameExpr}:${usernameExpr} /home/${usernameExpr}/.config /home/${usernameExpr}/.pnpm-store && printf "store-dir=/home/${usernameExpr}/.pnpm-store\\n" > /home/${usernameExpr}/.config/pnpm/rc`, order + 1));
79
+ };
80
+ }
81
+ /**
82
+ * Client-only (`psql`, etc.). Escalates to root so this also works when layered on
83
+ * a published base image that already switched to `USER node`.
84
+ */ export function withPostgresqlClient(options) {
85
+ const order = options?.order ?? StepOrder.features + 150;
86
+ const username = options?.username ?? "node";
87
+ return (dockerfile)=>{
88
+ dockerfile.addStep(DockerStep.raw("postgresql-client-root", "USER root", order)).addStep(DockerStep.run("postgresql-client", "apt-get update && apt-get install -y --no-install-recommends postgresql-client && rm -rf /var/lib/apt/lists/*", order + 1)).addStep(DockerStep.raw("postgresql-client-user", `USER ${username}`, order + 2));
89
+ };
90
+ }
91
+ /** GitHub CLI — install before USER switches away from root. */ export function withGithubCli(options) {
92
+ const order = options?.order ?? StepOrder.features + 140;
93
+ return (dockerfile)=>{
94
+ dockerfile.addStep(DockerStep.run("github-cli", "apt-get update && apt-get install -y --no-install-recommends gh && rm -rf /var/lib/apt/lists/*", order));
95
+ };
96
+ }
97
+ export function withWorkspaceNodeModulesOwnership(options) {
98
+ const workspaceDir = options?.workspaceDir ?? "/workspace";
99
+ const usernameExpr = options?.usernameExpr ?? "${USERNAME}";
100
+ const order = options?.order ?? StepOrder.features + 200;
101
+ return (dockerfile)=>{
102
+ dockerfile.addStep(DockerStep.raw("workspace-node-modules", `RUN mkdir -p ${workspaceDir}/node_modules && chown ${usernameExpr}:${usernameExpr} ${workspaceDir}/node_modules`, order));
103
+ };
104
+ }
105
+ export function withVsCodeJsDebugBootloaderFix(options) {
106
+ const bashrcPath = options?.bashrcPath ?? "/home/${USERNAME}/.bashrc";
107
+ const order = options?.order ?? StepOrder.features + 300;
108
+ return (dockerfile)=>{
109
+ dockerfile.addStep(DockerStep.raw("vscode-bootloader-fix", [
110
+ "# VS Code / Cursor js-debug Auto Attach injects --require …/bootloader.js into",
111
+ "# NODE_OPTIONS. On a fresh Dev Container that path often does not exist yet",
112
+ "# (workspaceStorage file is only created after toggling Auto Attach), which crashes",
113
+ "# every Node command. If the injected path is missing, prefer the stable",
114
+ "# bootloader from the server extension install; otherwise strip the bad require.",
115
+ `RUN cat <<'EOF' >> "${bashrcPath}"`,
116
+ VSCODE_BOOTLOADER_FIX_BASH_SNIPPET,
117
+ "EOF"
118
+ ].join("\n"), order));
119
+ };
120
+ }
121
+ export class DockerfileModel {
122
+ key = WELL_KNOWN_FILES.DOCKERFILE;
123
+ _steps = new Map();
124
+ _layerWriters = [];
125
+ _fromImage;
126
+ _layerCounter = 0;
127
+ _existing;
128
+ constructor(baseImage){
129
+ this._fromImage = baseImage;
130
+ this.registerBaseLayers();
131
+ }
132
+ /**
133
+ * Load an existing Dockerfile so `decohere add api` can append feature layers.
134
+ * The new steps are appended as one block, unless the file already contains it.
135
+ */ static fromFile(absolutePath, filePath) {
136
+ const dockerfile = new DockerfileModel("");
137
+ dockerfile._existing = {
138
+ content: readFileSync(absolutePath, "utf8"),
139
+ filePath
140
+ };
141
+ return dockerfile;
142
+ }
143
+ from(baseImage) {
144
+ this._fromImage = baseImage;
145
+ return this;
146
+ }
147
+ addLayer(layer, order = StepOrder.features) {
148
+ if (typeof layer === "string") {
149
+ const idx = ++this._layerCounter;
150
+ const key = `layer-run-${idx}`;
151
+ this._layerWriters.push((dockerfile)=>{
152
+ dockerfile.addStep(DockerStep.run(key, layer, order + idx));
153
+ });
154
+ return this;
155
+ }
156
+ this._layerWriters.push(layer);
157
+ return this;
158
+ }
159
+ addStep(step) {
160
+ this._steps.set(step.key, step);
161
+ return this;
162
+ }
163
+ removeStep(key) {
164
+ this._steps.delete(key);
165
+ return this;
166
+ }
167
+ entrypoint(value, order = StepOrder.entrypoint) {
168
+ this.addStep(DockerStep.entrypoint("entrypoint", value, order));
169
+ return this;
170
+ }
171
+ addRunStep(cmd) {
172
+ return this.addLayer(cmd);
173
+ }
174
+ build() {
175
+ for (const layerWriter of this._layerWriters){
176
+ layerWriter(this);
177
+ }
178
+ const sortedSteps = [
179
+ ...this._steps.values()
180
+ ].sort((left, right)=>{
181
+ if (left.order !== right.order) {
182
+ return left.order - right.order;
183
+ }
184
+ return left.key.localeCompare(right.key, "en");
185
+ });
186
+ if (this._existing) {
187
+ // All or nothing: skipping one step of a layer (e.g. its `USER root`) would break the rest.
188
+ const { content } = this._existing;
189
+ const block = sortedSteps.map((step)=>step.content).join("\n\n");
190
+ if (block === "" || content.includes(block)) return content;
191
+ return `${content.replace(/\n*$/, "\n")}\n${block}\n`;
192
+ }
193
+ const lines = [
194
+ `FROM ${this._fromImage}`
195
+ ];
196
+ for (const step of sortedSteps){
197
+ lines.push("", step.content);
198
+ }
199
+ return `${lines.join("\n")}\n`;
200
+ }
201
+ registerBaseLayers() {
202
+ // Generic base model intentionally has no default layers.
203
+ }
204
+ files() {
205
+ return [
206
+ {
207
+ filePath: this._existing?.filePath ?? "./Dockerfile",
208
+ content: this.build()
209
+ }
210
+ ];
211
+ }
212
+ }
213
+ /**
214
+ * Shared layers for the published `ghcr.io/typesmiths/decohere/devcontainer` image.
215
+ * Checked in at `projects/devcontainer/Dockerfile` — keep in sync via the unit test.
216
+ */ export class AppDevContainerBaseImage extends DockerfileModel {
217
+ registerBaseLayers() {
218
+ super.registerBaseLayers();
219
+ this.addLayer(withCommandHistory()).addLayer(withNodeUser()).addLayer(withPnpm()).addLayer(withGithubCli()).addLayer(withWorkspaceNodeModulesOwnership()).addLayer(withVsCodeJsDebugBootloaderFix());
220
+ }
221
+ }
222
+ /** Generic Dev Container image helpers (history + bootloader) without webapi defaults. */ export class DevContainerImageModel extends DockerfileModel {
223
+ registerBaseLayers() {
224
+ super.registerBaseLayers();
225
+ this.addLayer(withCommandHistory()).addLayer(withVsCodeJsDebugBootloaderFix());
226
+ }
227
+ files() {
228
+ return [
229
+ {
230
+ filePath: ".devcontainer/Dockerfile",
231
+ content: this.build()
232
+ }
233
+ ];
234
+ }
235
+ }
@@ -0,0 +1,44 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { WELL_KNOWN_FILES } from "./const.js";
3
+ /**
4
+ * Accumulates key=value pairs and renders them as a `.env` file.
5
+ *
6
+ * Register once in the workspace; features call `addEntry` during their
7
+ * scaffold phase to contribute environment variables. The final file is
8
+ * written at render time when `workspace.scaffold()` is called.
9
+ */ export class EnvFile {
10
+ key = WELL_KNOWN_FILES.ENV_FILE;
11
+ _entries = new Map();
12
+ addEntry(key, value) {
13
+ this._entries.set(key, value);
14
+ return this;
15
+ }
16
+ hasEntry(key) {
17
+ return this._entries.has(key);
18
+ }
19
+ static fromFile(absolutePath) {
20
+ const env = new EnvFile();
21
+ const raw = readFileSync(absolutePath, "utf8");
22
+ for (const line of raw.split("\n")){
23
+ const trimmed = line.trim();
24
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
25
+ const eq = line.indexOf("=");
26
+ if (eq <= 0) continue;
27
+ env.addEntry(line.slice(0, eq).trim(), line.slice(eq + 1));
28
+ }
29
+ return env;
30
+ }
31
+ files() {
32
+ if (this._entries.size === 0) return [];
33
+ const body = [
34
+ ...this._entries.entries()
35
+ ].map(([k, v])=>`${k}=${v}`).join("\n");
36
+ const content = `${body}\n`;
37
+ return [
38
+ {
39
+ filePath: ".env",
40
+ content
41
+ }
42
+ ];
43
+ }
44
+ }
@@ -0,0 +1,38 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { WELL_KNOWN_FILES } from "./const.js";
3
+ export class Gitignore {
4
+ key = WELL_KNOWN_FILES.GITIGNORE;
5
+ ignoreList = [
6
+ "node_modules",
7
+ "dist",
8
+ ".package",
9
+ ".pnpm-store",
10
+ ".env",
11
+ ".devcontainer/gh.env",
12
+ ".cursor/codebase-rag.env",
13
+ ".cursor/mcp.json",
14
+ ".DS_Store",
15
+ "logs"
16
+ ];
17
+ /** Load an existing .gitignore so `decohere add api` appends instead of replacing. */ static fromFile(absolutePath) {
18
+ const gitignore = new Gitignore();
19
+ gitignore.ignoreList = readFileSync(absolutePath, "utf8").replace(/\n+$/, "").split("\n");
20
+ return gitignore;
21
+ }
22
+ add(...patterns) {
23
+ for (const pattern of patterns){
24
+ if (!this.ignoreList.includes(pattern)) {
25
+ this.ignoreList.push(pattern);
26
+ }
27
+ }
28
+ return this;
29
+ }
30
+ files() {
31
+ return [
32
+ {
33
+ filePath: ".gitignore",
34
+ content: `${this.ignoreList.join("\n")}\n`
35
+ }
36
+ ];
37
+ }
38
+ }
@@ -0,0 +1,60 @@
1
+ import { WELL_KNOWN_FILES } from "./const.js";
2
+ export class LaunchJson {
3
+ apiNames;
4
+ key = WELL_KNOWN_FILES.LAUNCH_JSON;
5
+ constructor(apiNames = [
6
+ "api"
7
+ ]){
8
+ this.apiNames = apiNames;
9
+ }
10
+ files() {
11
+ const configurations = [
12
+ {
13
+ name: "Build",
14
+ type: "node",
15
+ request: "launch",
16
+ runtimeExecutable: "pnpm",
17
+ runtimeArgs: [
18
+ "build"
19
+ ],
20
+ console: "integratedTerminal",
21
+ cwd: "${workspaceFolder}"
22
+ }
23
+ ];
24
+ const first = this.apiNames[0] ?? "api";
25
+ const defaultDevArgs = first === "api" && this.apiNames.length === 1 ? [
26
+ "dev"
27
+ ] : [
28
+ "run",
29
+ `dev:${first}`
30
+ ];
31
+ configurations.push(serveConfig("Serve / Dev", defaultDevArgs));
32
+ for (const apiName of this.apiNames){
33
+ if (apiName === first && this.apiNames.length === 1 && apiName === "api") continue;
34
+ configurations.push(serveConfig(`Serve / Dev (${apiName})`, [
35
+ "run",
36
+ `dev:${apiName}`
37
+ ]));
38
+ }
39
+ return [
40
+ {
41
+ filePath: ".vscode/launch.json",
42
+ content: JSON.stringify({
43
+ version: "0.2.0",
44
+ configurations
45
+ }, null, 2)
46
+ }
47
+ ];
48
+ }
49
+ }
50
+ function serveConfig(name, runtimeArgs) {
51
+ return {
52
+ name,
53
+ type: "node",
54
+ request: "launch",
55
+ runtimeExecutable: "pnpm",
56
+ runtimeArgs,
57
+ console: "integratedTerminal",
58
+ cwd: "${workspaceFolder}"
59
+ };
60
+ }
@@ -0,0 +1,142 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { CREATE_VERSION } from "../version.js";
3
+ import { DEFAULT_WEBAPI_NAME, webapiPackageName, webapiPort, webapiReleaseEntry } from "../webapi/const.js";
4
+ import { WELL_KNOWN_FILES } from "./const.js";
5
+ /**
6
+ * Builder for the root `package.json`.
7
+ *
8
+ * Starts with the core API dependencies. Features call `addDependency` /
9
+ * `addDevDependency` during their scaffold phase to inject their own deps
10
+ * before `files()` is called at render time.
11
+ */ export class PackageJson {
12
+ name;
13
+ key = WELL_KNOWN_FILES.PACKAGE_JSON;
14
+ _dependencies;
15
+ _devDependencies;
16
+ _scripts;
17
+ _version = "0.0.1";
18
+ _type = "module";
19
+ _rest = {};
20
+ constructor(name, options){
21
+ this.name = name;
22
+ const apiNames = options?.apiNames ?? [
23
+ DEFAULT_WEBAPI_NAME
24
+ ];
25
+ const first = apiNames[0] ?? DEFAULT_WEBAPI_NAME;
26
+ const startEntry = options?.startEntry ?? webapiReleaseEntry(first);
27
+ const workspaceName = options?.workspaceName ?? stripApiSuffix(name);
28
+ const namedOrMany = apiNames.length > 1 || first !== DEFAULT_WEBAPI_NAME;
29
+ this._scripts = {
30
+ build: "forge build --configuration release",
31
+ "build:watch": "forge build --watch",
32
+ // Release build, then `.package/` in every API (what its Dockerfile copies).
33
+ package: "pnpm run build && forge package",
34
+ start: `node ${startEntry}`,
35
+ dev: namedOrMany ? fluxDevScript(workspaceName, first, webapiPort(0)) : "flux serve --env-file-if-exists .env",
36
+ "lint:check": "biome check .",
37
+ typecheck: "forge typecheck",
38
+ test: "vitest run --passWithNoTests"
39
+ };
40
+ for (const [index, apiName] of apiNames.entries()){
41
+ this._scripts[`start:${apiName}`] = `node ${webapiReleaseEntry(apiName)}`;
42
+ this._scripts[`package:${apiName}`] = packageScript(workspaceName, apiName);
43
+ this._scripts[`dev:${apiName}`] = fluxDevScript(workspaceName, apiName, webapiPort(index));
44
+ }
45
+ this._dependencies = {
46
+ "@decohere/http": CREATE_VERSION,
47
+ "@decohere/crucible": CREATE_VERSION,
48
+ "@decohere/core": CREATE_VERSION,
49
+ "@decohere/config": CREATE_VERSION,
50
+ "@decohere/scribe": CREATE_VERSION,
51
+ "@decohere/scribe-pino": CREATE_VERSION,
52
+ "pino-pretty": "^13.0.0",
53
+ "pino-roll": "^4.0.0"
54
+ };
55
+ this._devDependencies = {
56
+ "@decohere/forge": CREATE_VERSION,
57
+ "@decohere/cli": CREATE_VERSION,
58
+ "@types/node": "^24.0.0",
59
+ typescript: "^6.0.3",
60
+ "@biomejs/biome": "^2.0.0",
61
+ vitest: "^3.2.4"
62
+ };
63
+ }
64
+ /** Load an existing root package.json so `webapi add` can merge scripts/deps. */ static fromFile(filePath) {
65
+ const raw = JSON.parse(readFileSync(filePath, "utf8"));
66
+ const pkg = new PackageJson(typeof raw.name === "string" ? raw.name : "app", {
67
+ apiNames: []
68
+ });
69
+ pkg._version = typeof raw.version === "string" ? raw.version : "0.0.1";
70
+ pkg._type = typeof raw.type === "string" ? raw.type : "module";
71
+ pkg._scripts = {
72
+ ...asStringMap(raw.scripts)
73
+ };
74
+ pkg._dependencies = {
75
+ ...asStringMap(raw.dependencies)
76
+ };
77
+ pkg._devDependencies = {
78
+ ...asStringMap(raw.devDependencies)
79
+ };
80
+ const reserved = new Set([
81
+ "name",
82
+ "version",
83
+ "type",
84
+ "scripts",
85
+ "dependencies",
86
+ "devDependencies"
87
+ ]);
88
+ pkg._rest = Object.fromEntries(Object.entries(raw).filter(([key])=>!reserved.has(key)));
89
+ return pkg;
90
+ }
91
+ addDependency(name, version) {
92
+ this._dependencies[name] = version;
93
+ return this;
94
+ }
95
+ addDevDependency(name, version) {
96
+ this._devDependencies[name] = version;
97
+ return this;
98
+ }
99
+ addScript(name, command) {
100
+ this._scripts[name] = command;
101
+ return this;
102
+ }
103
+ hasScript(name) {
104
+ return typeof this._scripts[name] === "string";
105
+ }
106
+ files() {
107
+ return [
108
+ {
109
+ filePath: "package.json",
110
+ content: JSON.stringify({
111
+ name: this.name,
112
+ version: this._version,
113
+ type: this._type,
114
+ ...this._rest,
115
+ scripts: this._scripts,
116
+ dependencies: sortStringMap(this._dependencies),
117
+ devDependencies: sortStringMap(this._devDependencies)
118
+ }, null, 2)
119
+ }
120
+ ];
121
+ }
122
+ }
123
+ function stripApiSuffix(name) {
124
+ return name.endsWith(".api") ? name.slice(0, -".api".length) : name;
125
+ }
126
+ function asStringMap(value) {
127
+ if (value == null || typeof value !== "object") return {};
128
+ const result = {};
129
+ for (const [key, entry] of Object.entries(value)){
130
+ if (typeof entry === "string") result[key] = entry;
131
+ }
132
+ return result;
133
+ }
134
+ /** Stable A→Z key order for package.json dependency maps. */ export function sortStringMap(map) {
135
+ return Object.fromEntries(Object.entries(map).sort(([left], [right])=>left.localeCompare(right, "en")));
136
+ }
137
+ export function packageScript(workspaceName, apiName) {
138
+ return `pnpm run build && forge package ${webapiPackageName(workspaceName, apiName)}`;
139
+ }
140
+ function fluxDevScript(workspaceName, apiName, port) {
141
+ return `PORT=${port} flux serve --env-file-if-exists .env --project ${webapiPackageName(workspaceName, apiName)}`;
142
+ }