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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/package.json +9 -19
  2. package/src/add-api.action.ts +25 -0
  3. package/src/add-api.command.ts +31 -0
  4. package/src/add.command.ts +20 -0
  5. package/src/add.module.ts +11 -0
  6. package/src/cli.ts +4 -0
  7. package/src/create.command.ts +20 -0
  8. package/src/create.module.ts +11 -0
  9. package/src/domain/prompt.ts +50 -0
  10. package/{types/src/index.d.ts → src/index.ts} +0 -4
  11. package/src/models/compose.model.ts +205 -0
  12. package/src/models/config-yaml.model.ts +83 -0
  13. package/src/models/const.ts +20 -0
  14. package/src/models/dev-container.model.ts +136 -0
  15. package/src/models/docker-image.model.ts +343 -0
  16. package/src/models/env-file.model.ts +45 -0
  17. package/src/models/gitignore.model.ts +35 -0
  18. package/src/models/launch-json.model.ts +60 -0
  19. package/src/models/package-json.model.ts +169 -0
  20. package/src/models/project.model.ts +141 -0
  21. package/{types/src/models/source-file.d.ts → src/models/source-file.ts} +6 -4
  22. package/src/models/tsconfig.model.ts +29 -0
  23. package/src/models/vitest-config.model.ts +26 -0
  24. package/src/models/vscode-settings.model.ts +24 -0
  25. package/src/program.ts +63 -0
  26. package/{types/src/resolve-target-dir.d.ts → src/resolve-target-dir.ts} +4 -1
  27. package/src/services/scaffold.service.ts +89 -0
  28. package/src/version.ts +56 -0
  29. package/src/webapi/api-name.ts +73 -0
  30. package/src/webapi/cli-options.ts +56 -0
  31. package/{types/src/webapi/const.d.ts → src/webapi/const.ts} +21 -9
  32. package/src/webapi/create-api.ts +275 -0
  33. package/src/webapi/devcontainer/host-scripts.ts +35 -0
  34. package/src/webapi/features/auth.feature.ts +454 -0
  35. package/src/webapi/features/auth0-cli.ts +421 -0
  36. package/src/webapi/features/biome-format.ts +34 -0
  37. package/src/webapi/features/database.feature.ts +522 -0
  38. package/src/webapi/features/finalize.feature.ts +112 -0
  39. package/src/webapi/features/postman.feature.ts +287 -0
  40. package/src/webapi/models/api-package-json.model.ts +60 -0
  41. package/src/webapi/models/auth-config.model.ts +36 -0
  42. package/src/webapi/models/controller-source-file.model.ts +232 -0
  43. package/{es6/src/webapi/models/github-ci.model.js → src/webapi/models/github-ci.model.ts} +29 -27
  44. package/src/webapi/models/main-source-file.model.ts +208 -0
  45. package/src/webapi/models/production-dockerfile.model.ts +162 -0
  46. package/src/webapi/models/sample-files.model.ts +45 -0
  47. package/src/webapi/models/webapi-image.model.ts +21 -0
  48. package/src/webapi/postman/hello-openapi.ts +92 -0
  49. package/src/webapi/postman/local-view.ts +195 -0
  50. package/src/webapi/postman/openapi-to-postman.ts +208 -0
  51. package/src/webapi/workspace-readme.ts +192 -0
  52. package/src/webapi.action.ts +22 -0
  53. package/src/webapi.command.ts +41 -0
  54. package/tests/auth-postman.wiring.spec.ts +263 -0
  55. package/tests/auth0-cli.spec.ts +232 -0
  56. package/tests/biome-format.spec.ts +49 -0
  57. package/tests/cli.integration.spec.ts +658 -0
  58. package/tests/compose.model.spec.ts +51 -0
  59. package/tests/container-image.model.spec.ts +162 -0
  60. package/tests/dev-container.model.spec.ts +29 -0
  61. package/tests/devcontainer-base-image.spec.ts +32 -0
  62. package/tests/hello-di-inject.scaffold.spec.ts +18 -0
  63. package/tests/hello-route.contract.spec.ts +94 -0
  64. package/tests/package-json.order.spec.ts +59 -0
  65. package/tests/postman-from-openapi.spec.ts +197 -0
  66. package/tests/seed-devcontainer-templates.ts +40 -0
  67. package/tests/webapi/devcontainer/codebase-rag-mcp.spec.ts +96 -0
  68. package/LICENSE +0 -15
  69. package/commonjs/src/add-api.action.js +0 -37
  70. package/commonjs/src/add-api.command.js +0 -39
  71. package/commonjs/src/add.command.js +0 -22
  72. package/commonjs/src/add.module.js +0 -24
  73. package/commonjs/src/auth0-configure.action.js +0 -28
  74. package/commonjs/src/auth0-configure.command.js +0 -24
  75. package/commonjs/src/auth0-login.action.js +0 -28
  76. package/commonjs/src/auth0-login.command.js +0 -23
  77. package/commonjs/src/auth0.command.js +0 -22
  78. package/commonjs/src/auth0.module.js +0 -28
  79. package/commonjs/src/cli.js +0 -8
  80. package/commonjs/src/create.command.js +0 -22
  81. package/commonjs/src/create.module.js +0 -24
  82. package/commonjs/src/domain/prompt.js +0 -62
  83. package/commonjs/src/index.js +0 -48
  84. package/commonjs/src/models/compose.model.js +0 -258
  85. package/commonjs/src/models/config-yaml.model.js +0 -86
  86. package/commonjs/src/models/const.js +0 -30
  87. package/commonjs/src/models/dev-container.model.js +0 -172
  88. package/commonjs/src/models/docker-image.model.js +0 -277
  89. package/commonjs/src/models/env-file.model.js +0 -48
  90. package/commonjs/src/models/gitignore.model.js +0 -48
  91. package/commonjs/src/models/launch-json.model.js +0 -70
  92. package/commonjs/src/models/package-json.model.js +0 -156
  93. package/commonjs/src/models/project.model.js +0 -132
  94. package/commonjs/src/models/source-file.js +0 -4
  95. package/commonjs/src/models/tsconfig.model.js +0 -32
  96. package/commonjs/src/models/vitest-config.model.js +0 -33
  97. package/commonjs/src/models/vscode-settings.model.js +0 -29
  98. package/commonjs/src/program.js +0 -64
  99. package/commonjs/src/resolve-target-dir.js +0 -17
  100. package/commonjs/src/services/scaffold.service.js +0 -60
  101. package/commonjs/src/version.js +0 -55
  102. package/commonjs/src/webapi/api-name.js +0 -93
  103. package/commonjs/src/webapi/auth0-commands.js +0 -250
  104. package/commonjs/src/webapi/authorize-scopes.js +0 -79
  105. package/commonjs/src/webapi/cli-options.js +0 -99
  106. package/commonjs/src/webapi/const.js +0 -54
  107. package/commonjs/src/webapi/create-api.js +0 -265
  108. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.js +0 -164
  109. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.ts +0 -182
  110. package/commonjs/src/webapi/devcontainer/host-scripts.js +0 -50
  111. package/commonjs/src/webapi/devcontainer/templates/codebase-rag.env.example +0 -3
  112. package/commonjs/src/webapi/devcontainer/templates/initialize.sh +0 -169
  113. package/commonjs/src/webapi/devcontainer/templates/post-create.sh +0 -35
  114. package/commonjs/src/webapi/devcontainer/templates/post-start.sh +0 -65
  115. package/commonjs/src/webapi/devcontainer/templates/tty-prompt.sh +0 -38
  116. package/commonjs/src/webapi/features/auth.feature.js +0 -408
  117. package/commonjs/src/webapi/features/auth0-cli.js +0 -472
  118. package/commonjs/src/webapi/features/biome-format.js +0 -47
  119. package/commonjs/src/webapi/features/database.feature.js +0 -433
  120. package/commonjs/src/webapi/features/finalize.feature.js +0 -93
  121. package/commonjs/src/webapi/features/postman.feature.js +0 -322
  122. package/commonjs/src/webapi/models/api-package-json.model.js +0 -58
  123. package/commonjs/src/webapi/models/auth-config.model.js +0 -24
  124. package/commonjs/src/webapi/models/controller-source-file.model.js +0 -190
  125. package/commonjs/src/webapi/models/github-ci.model.js +0 -193
  126. package/commonjs/src/webapi/models/main-source-file.model.js +0 -165
  127. package/commonjs/src/webapi/models/production-dockerfile.model.js +0 -75
  128. package/commonjs/src/webapi/models/sample-files.model.js +0 -58
  129. package/commonjs/src/webapi/models/webapi-image.model.js +0 -25
  130. package/commonjs/src/webapi/postman/hello-openapi.js +0 -173
  131. package/commonjs/src/webapi/postman/local-view.js +0 -182
  132. package/commonjs/src/webapi/postman/openapi-to-postman.js +0 -238
  133. package/commonjs/src/webapi/workspace-readme.js +0 -109
  134. package/commonjs/src/webapi.action.js +0 -28
  135. package/commonjs/src/webapi.command.js +0 -43
  136. package/es6/src/add-api.action.js +0 -27
  137. package/es6/src/add-api.command.js +0 -29
  138. package/es6/src/add.command.js +0 -12
  139. package/es6/src/add.module.js +0 -14
  140. package/es6/src/auth0-configure.action.js +0 -18
  141. package/es6/src/auth0-configure.command.js +0 -14
  142. package/es6/src/auth0-login.action.js +0 -18
  143. package/es6/src/auth0-login.command.js +0 -13
  144. package/es6/src/auth0.command.js +0 -12
  145. package/es6/src/auth0.module.js +0 -18
  146. package/es6/src/cli.js +0 -4
  147. package/es6/src/create.command.js +0 -12
  148. package/es6/src/create.module.js +0 -14
  149. package/es6/src/domain/prompt.js +0 -41
  150. package/es6/src/index.js +0 -9
  151. package/es6/src/models/compose.model.js +0 -254
  152. package/es6/src/models/config-yaml.model.js +0 -74
  153. package/es6/src/models/const.js +0 -20
  154. package/es6/src/models/dev-container.model.js +0 -162
  155. package/es6/src/models/docker-image.model.js +0 -235
  156. package/es6/src/models/env-file.model.js +0 -44
  157. package/es6/src/models/gitignore.model.js +0 -38
  158. package/es6/src/models/launch-json.model.js +0 -60
  159. package/es6/src/models/package-json.model.js +0 -141
  160. package/es6/src/models/project.model.js +0 -117
  161. package/es6/src/models/source-file.js +0 -1
  162. package/es6/src/models/tsconfig.model.js +0 -22
  163. package/es6/src/models/vitest-config.model.js +0 -23
  164. package/es6/src/models/vscode-settings.model.js +0 -19
  165. package/es6/src/program.js +0 -50
  166. package/es6/src/resolve-target-dir.js +0 -7
  167. package/es6/src/services/scaffold.service.js +0 -50
  168. package/es6/src/version.js +0 -34
  169. package/es6/src/webapi/api-name.js +0 -57
  170. package/es6/src/webapi/auth0-commands.js +0 -185
  171. package/es6/src/webapi/authorize-scopes.js +0 -55
  172. package/es6/src/webapi/cli-options.js +0 -34
  173. package/es6/src/webapi/const.js +0 -21
  174. package/es6/src/webapi/create-api.js +0 -245
  175. package/es6/src/webapi/devcontainer/codebase-rag-mcp.js +0 -154
  176. package/es6/src/webapi/devcontainer/codebase-rag-mcp.ts +0 -182
  177. package/es6/src/webapi/devcontainer/host-scripts.js +0 -29
  178. package/es6/src/webapi/devcontainer/templates/codebase-rag.env.example +0 -3
  179. package/es6/src/webapi/devcontainer/templates/initialize.sh +0 -169
  180. package/es6/src/webapi/devcontainer/templates/post-create.sh +0 -35
  181. package/es6/src/webapi/devcontainer/templates/post-start.sh +0 -65
  182. package/es6/src/webapi/devcontainer/templates/tty-prompt.sh +0 -38
  183. package/es6/src/webapi/features/auth.feature.js +0 -357
  184. package/es6/src/webapi/features/auth0-cli.js +0 -406
  185. package/es6/src/webapi/features/biome-format.js +0 -40
  186. package/es6/src/webapi/features/database.feature.js +0 -423
  187. package/es6/src/webapi/features/finalize.feature.js +0 -83
  188. package/es6/src/webapi/features/postman.feature.js +0 -256
  189. package/es6/src/webapi/models/api-package-json.model.js +0 -53
  190. package/es6/src/webapi/models/auth-config.model.js +0 -20
  191. package/es6/src/webapi/models/controller-source-file.model.js +0 -181
  192. package/es6/src/webapi/models/main-source-file.model.js +0 -161
  193. package/es6/src/webapi/models/production-dockerfile.model.js +0 -66
  194. package/es6/src/webapi/models/sample-files.model.js +0 -40
  195. package/es6/src/webapi/models/webapi-image.model.js +0 -18
  196. package/es6/src/webapi/postman/hello-openapi.js +0 -166
  197. package/es6/src/webapi/postman/local-view.js +0 -167
  198. package/es6/src/webapi/postman/openapi-to-postman.js +0 -179
  199. package/es6/src/webapi/workspace-readme.js +0 -88
  200. package/es6/src/webapi.action.js +0 -18
  201. package/es6/src/webapi.command.js +0 -33
  202. package/types/src/add-api.action.d.ts +0 -13
  203. package/types/src/add-api.command.d.ts +0 -7
  204. package/types/src/add.command.d.ts +0 -6
  205. package/types/src/add.module.d.ts +0 -14
  206. package/types/src/auth0-configure.action.d.ts +0 -7
  207. package/types/src/auth0-configure.command.d.ts +0 -6
  208. package/types/src/auth0-login.action.d.ts +0 -4
  209. package/types/src/auth0-login.command.d.ts +0 -6
  210. package/types/src/auth0.command.d.ts +0 -6
  211. package/types/src/auth0.module.d.ts +0 -18
  212. package/types/src/cli.d.ts +0 -2
  213. package/types/src/create.command.d.ts +0 -6
  214. package/types/src/create.module.d.ts +0 -14
  215. package/types/src/domain/prompt.d.ts +0 -4
  216. package/types/src/models/compose.model.d.ts +0 -73
  217. package/types/src/models/config-yaml.model.d.ts +0 -27
  218. package/types/src/models/const.d.ts +0 -20
  219. package/types/src/models/dev-container.model.d.ts +0 -49
  220. package/types/src/models/docker-image.model.d.ts +0 -90
  221. package/types/src/models/env-file.model.d.ts +0 -16
  222. package/types/src/models/gitignore.model.d.ts +0 -9
  223. package/types/src/models/launch-json.model.d.ts +0 -7
  224. package/types/src/models/package-json.model.d.ts +0 -36
  225. package/types/src/models/project.model.d.ts +0 -60
  226. package/types/src/models/tsconfig.model.d.ts +0 -5
  227. package/types/src/models/vitest-config.model.d.ts +0 -5
  228. package/types/src/models/vscode-settings.model.d.ts +0 -7
  229. package/types/src/program.d.ts +0 -8
  230. package/types/src/services/scaffold.service.d.ts +0 -22
  231. package/types/src/version.d.ts +0 -6
  232. package/types/src/webapi/api-name.d.ts +0 -14
  233. package/types/src/webapi/auth0-commands.d.ts +0 -11
  234. package/types/src/webapi/authorize-scopes.d.ts +0 -7
  235. package/types/src/webapi/cli-options.d.ts +0 -6
  236. package/types/src/webapi/create-api.d.ts +0 -15
  237. package/types/src/webapi/devcontainer/codebase-rag-mcp.d.ts +0 -32
  238. package/types/src/webapi/devcontainer/codebase-rag-mcp.ts +0 -182
  239. package/types/src/webapi/devcontainer/host-scripts.d.ts +0 -7
  240. package/types/src/webapi/devcontainer/templates/codebase-rag.env.example +0 -3
  241. package/types/src/webapi/devcontainer/templates/initialize.sh +0 -169
  242. package/types/src/webapi/devcontainer/templates/post-create.sh +0 -35
  243. package/types/src/webapi/devcontainer/templates/post-start.sh +0 -65
  244. package/types/src/webapi/devcontainer/templates/tty-prompt.sh +0 -38
  245. package/types/src/webapi/features/auth.feature.d.ts +0 -10
  246. package/types/src/webapi/features/auth0-cli.d.ts +0 -84
  247. package/types/src/webapi/features/biome-format.d.ts +0 -5
  248. package/types/src/webapi/features/database.feature.d.ts +0 -7
  249. package/types/src/webapi/features/finalize.feature.d.ts +0 -2
  250. package/types/src/webapi/features/postman.feature.d.ts +0 -16
  251. package/types/src/webapi/models/api-package-json.model.d.ts +0 -18
  252. package/types/src/webapi/models/auth-config.model.d.ts +0 -26
  253. package/types/src/webapi/models/controller-source-file.model.d.ts +0 -46
  254. package/types/src/webapi/models/github-ci.model.d.ts +0 -9
  255. package/types/src/webapi/models/main-source-file.model.d.ts +0 -54
  256. package/types/src/webapi/models/production-dockerfile.model.d.ts +0 -18
  257. package/types/src/webapi/models/sample-files.model.d.ts +0 -7
  258. package/types/src/webapi/models/webapi-image.model.d.ts +0 -12
  259. package/types/src/webapi/postman/hello-openapi.d.ts +0 -19
  260. package/types/src/webapi/postman/local-view.d.ts +0 -17
  261. package/types/src/webapi/postman/openapi-to-postman.d.ts +0 -62
  262. package/types/src/webapi/workspace-readme.d.ts +0 -18
  263. package/types/src/webapi.action.d.ts +0 -14
  264. package/types/src/webapi.command.d.ts +0 -11
@@ -1,117 +0,0 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { basename, dirname, join } from "node:path";
3
- /**
4
- * A sub-unit of the monorepo (e.g. `projects/api`).
5
- *
6
- * Models and files registered here produce paths relative to `dir`. The parent
7
- * `MonoRepoScaffold` calls `files()` and prepends `dir/` to every path, so
8
- * models remain unaware of where they live in the repo.
9
- */ export class ProjectScaffold {
10
- dir;
11
- _models = new Map();
12
- _files = [];
13
- constructor(dir){
14
- this.dir = dir;
15
- }
16
- addModel(model) {
17
- this._models.set(model.key, model);
18
- return model;
19
- }
20
- addFile(provider) {
21
- this._files.push(...provider.files());
22
- return this;
23
- }
24
- getModel(key) {
25
- return this._models.get(key);
26
- }
27
- getModels(ctor) {
28
- return [
29
- ...this._models.values()
30
- ].filter((m)=>m instanceof ctor);
31
- }
32
- /** Returns all files with `dir/` prepended to each path. */ files() {
33
- const all = [
34
- ...this._files,
35
- ...[
36
- ...this._models.values()
37
- ].flatMap((m)=>m.files())
38
- ];
39
- return all.map((f)=>({
40
- filePath: `${this.dir}/${f.filePath}`,
41
- content: f.content
42
- }));
43
- }
44
- }
45
- export class WorkspaceScaffold {
46
- files = [];
47
- targetDir = process.cwd();
48
- _name;
49
- /** Project name — host directory basename, not the in-container mount path. */ get name() {
50
- return this._name ?? basename(this.targetDir);
51
- }
52
- setName(name) {
53
- this._name = name;
54
- return this;
55
- }
56
- /** Add an ad-hoc file provider whose output is included at render time. */ addFile(provider) {
57
- this.files.push(...provider.files());
58
- return this;
59
- }
60
- }
61
- export class MonoRepoScaffold extends WorkspaceScaffold {
62
- _models = new Map();
63
- _projects = [];
64
- /** Register a named project. */ addProject(project) {
65
- this._projects.push(project);
66
- return project;
67
- }
68
- /** Retrieve a previously-registered project by its directory path. */ getProject(dir) {
69
- return this._projects.find((p)=>p.dir === dir);
70
- }
71
- /** All registered projects, in registration order. */ getProjects() {
72
- return this._projects;
73
- }
74
- /** Register a named model at workspace level. */ addModel(model) {
75
- this._models.set(model.key, model);
76
- return model;
77
- }
78
- /**
79
- * Retrieve a model by key. Searches workspace models first, then all
80
- * registered projects in order.
81
- */ getModel(key) {
82
- const m = this._models.get(key);
83
- if (m !== undefined) return m;
84
- for (const project of this._projects){
85
- const pm = project.getModel(key);
86
- if (pm !== undefined) return pm;
87
- }
88
- return undefined;
89
- }
90
- /**
91
- * Retrieve all models of a given type. Searches workspace models and all
92
- * projects, so features can discover controllers across multiple projects.
93
- */ getModels(ctor) {
94
- return [
95
- ...[
96
- ...this._models.values()
97
- ].filter((m)=>m instanceof ctor),
98
- ...this._projects.flatMap((p)=>p.getModels(ctor))
99
- ];
100
- }
101
- /** Render all models and projects into files, then write everything to disk. */ async scaffold() {
102
- const allFiles = [
103
- ...this.files,
104
- ...[
105
- ...this._models.values()
106
- ].flatMap((m)=>m.files()),
107
- ...this._projects.flatMap((p)=>p.files())
108
- ];
109
- await Promise.all(allFiles.map(async (file)=>{
110
- const fullPath = join(this.targetDir, file.filePath);
111
- await mkdir(dirname(fullPath), {
112
- recursive: true
113
- });
114
- await writeFile(fullPath, file.content);
115
- }));
116
- }
117
- }
@@ -1 +0,0 @@
1
- export { };
@@ -1,22 +0,0 @@
1
- import { WELL_KNOWN_FILES } from "./const.js";
2
- export class TsConfig {
3
- key = WELL_KNOWN_FILES.TSCONFIG;
4
- files() {
5
- return [
6
- {
7
- filePath: "tsconfig.json",
8
- content: JSON.stringify({
9
- compilerOptions: {
10
- target: "ES2022",
11
- module: "NodeNext",
12
- moduleResolution: "NodeNext",
13
- strict: true,
14
- skipLibCheck: true,
15
- experimentalDecorators: true,
16
- emitDecoratorMetadata: true
17
- }
18
- }, null, 2)
19
- }
20
- ];
21
- }
22
- }
@@ -1,23 +0,0 @@
1
- import { WELL_KNOWN_FILES } from "./const.js";
2
- export class VitestConfig {
3
- key = WELL_KNOWN_FILES.VITEST_CONFIG;
4
- files() {
5
- return [
6
- {
7
- filePath: "vitest.config.ts",
8
- content: [
9
- 'import { forgeBuild } from "@decohere/forge/vitest";',
10
- 'import { defineConfig } from "vitest/config";',
11
- "",
12
- "export default defineConfig({",
13
- '\tplugins: [forgeBuild({ logLevel: "silent" })],',
14
- "\ttest: {",
15
- '\t\tinclude: ["projects/**/*.{test,spec}.?(c|m)[jt]s?(x)"],',
16
- "\t},",
17
- "});",
18
- ""
19
- ].join("\n")
20
- }
21
- ];
22
- }
23
- }
@@ -1,19 +0,0 @@
1
- import { WELL_KNOWN_FILES } from "./const.js";
2
- export class VSCodeSettings {
3
- title;
4
- key = WELL_KNOWN_FILES.VSCODE_SETTINGS;
5
- constructor(title){
6
- this.title = title;
7
- }
8
- files() {
9
- return [
10
- {
11
- filePath: ".vscode/settings.json",
12
- content: JSON.stringify({
13
- "window.title": `${this.title}\${separator}\${activeEditorShort}`,
14
- "window.titleSeparator": " | "
15
- }, null, 2)
16
- }
17
- ];
18
- }
19
- }
@@ -1,50 +0,0 @@
1
- import { Command, CommanderError } from "commander";
2
- import { CreateCliError } from "./webapi/api-name.js";
3
- import { runAuth0Configure, runAuth0Login } from "./webapi/auth0-commands.js";
4
- import { addWebapiFeatureOptions, collectName, runScaffoldApi, toApiOptions } from "./webapi/cli-options.js";
5
- export function createProgram() {
6
- const program = new Command();
7
- program.exitOverride();
8
- program.name("decohere").description("Decohere CLI").option("-d <directory>", "Target directory (default: current directory)", ".");
9
- const create = program.command("create").description("Create a new Decohere workspace");
10
- addWebapiFeatureOptions(create.command("webapi").argument("[directory]", "Target directory (default: -d or current directory)").option("--name <slug>", "API name under projects/ (repeatable; default: api)", collectName, [])).action(async (directory, _opts, cmd)=>{
11
- await runScaffoldApi(toApiOptions(directory, cmd.optsWithGlobals()));
12
- });
13
- addWebapiFeatureOptions(program.command("add").description("Add to an existing Decohere workspace").command("api").argument("<name>", "API name under projects/")).action(async (name, _opts, cmd)=>{
14
- await runScaffoldApi(toApiOptions(undefined, cmd.optsWithGlobals(), {
15
- names: [
16
- name
17
- ],
18
- add: true
19
- }));
20
- });
21
- const auth0 = program.command("auth0").description("Log in to Auth0 and update a scaffolded client");
22
- auth0.command("login").description("Authenticate the Auth0 CLI (device URL; tokens persist on the host)").action(()=>{
23
- runAuth0Login();
24
- });
25
- auth0.command("configure").description("Update Auth0 API permissions and Postman SPA from @Authorize in this workspace").action((_opts, cmd)=>{
26
- const d = cmd.optsWithGlobals().d;
27
- runAuth0Configure(typeof d === "string" ? d : ".");
28
- });
29
- return program;
30
- }
31
- /**
32
- * Runs the CLI and returns its exit code; the bin (`cli.ts`) exits with it. Returning instead
33
- * of calling `process.exit` here lets tests run it in process: a run that finished after its
34
- * test had timed out used to call `process.exit` in whichever test came next (#662).
35
- */ export async function runCli(argv = process.argv) {
36
- try {
37
- await createProgram().parseAsync(argv);
38
- } catch (err) {
39
- if (err instanceof CommanderError) {
40
- return err.exitCode;
41
- }
42
- if (err instanceof CreateCliError) {
43
- console.error(`\n ${err.message}\n`);
44
- return 1;
45
- }
46
- console.error(err);
47
- return 1;
48
- }
49
- return typeof process.exitCode === "number" ? process.exitCode : 0;
50
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Resolve scaffold destination: positional `webapi my-app` wins over `-d`.
3
- * `npm create … webapi -d my-app` drops `-d` (npm loglevel) and forwards `my-app`.
4
- */ export function resolveTargetDir(positional, flag) {
5
- const dir = positional?.trim() || flag?.trim() || ".";
6
- return dir;
7
- }
@@ -1,50 +0,0 @@
1
- async function scaffold(workspace, cliOptions, features) {
2
- const options = await getAllOptions(workspace, cliOptions, features);
3
- // Features mutate workspace models first, then workspace renders to disk.
4
- await scaffoldFeatures(workspace, options, features);
5
- await scaffoldWorkspace(workspace, options);
6
- // Post-write phase: install deps, print final messages, etc.
7
- await finalizeFeatures(workspace, options, features);
8
- }
9
- async function scaffoldWorkspace(workspace, _options) {
10
- await workspace.scaffold();
11
- }
12
- /**
13
- * Iterate features in order, calling `getOptions` on each.
14
- * Each feature receives the running merged options so later features can
15
- * branch on decisions made by earlier ones (e.g. database choice affects auth).
16
- * Results are shallow-merged, so features only need to return what they own.
17
- */ async function getAllOptions(workspace, cliOptions, features) {
18
- let options = {
19
- ...cliOptions
20
- };
21
- for (const feature of features){
22
- const featureOptions = await feature.getOptions(workspace, options);
23
- options = {
24
- ...options,
25
- ...featureOptions
26
- };
27
- }
28
- return options;
29
- }
30
- /**
31
- * Iterate features in order, calling `scaffold` with the fully-resolved options.
32
- * All option-gathering is complete before any scaffolding begins.
33
- */ async function scaffoldFeatures(workspace, options, features) {
34
- for (const feature of features){
35
- await feature.scaffold(workspace, options);
36
- }
37
- }
38
- /**
39
- * Iterate features in order, calling `finalize` on each (if defined).
40
- * All files are on disk by this point.
41
- */ async function finalizeFeatures(workspace, options, features) {
42
- for (const feature of features){
43
- if (feature.finalize) {
44
- await feature.finalize(workspace, options);
45
- }
46
- }
47
- }
48
- export const ScaffoldService = {
49
- scaffold
50
- };
@@ -1,34 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- const _require = createRequire(import.meta.url);
5
- const DEFAULT_CREATE_VERSION = "0.0.1-dev";
6
- const DEFAULT_CREATE_IMAGE_REPOSITORY = "ghcr.io/typesmiths/decohere/create";
7
- const DEFAULT_DEVCONTAINER_IMAGE_REPOSITORY = "ghcr.io/typesmiths/decohere/devcontainer";
8
- function resolveCreateMetadata() {
9
- let dir = dirname(fileURLToPath(import.meta.url));
10
- for(let i = 0; i < 5; i++){
11
- try {
12
- const pkg = _require(join(dir, "package.json"));
13
- if (pkg.name === "@decohere/scaffold") {
14
- return {
15
- version: pkg.version ?? DEFAULT_CREATE_VERSION,
16
- imageRepository: pkg.decohere?.dockerImageRepository ?? DEFAULT_CREATE_IMAGE_REPOSITORY,
17
- devcontainerImageRepository: pkg.decohere?.devcontainerImageRepository ?? DEFAULT_DEVCONTAINER_IMAGE_REPOSITORY
18
- };
19
- }
20
- } catch {}
21
- const parent = dirname(dir);
22
- if (parent === dir) break;
23
- dir = parent;
24
- }
25
- return {
26
- version: DEFAULT_CREATE_VERSION,
27
- imageRepository: DEFAULT_CREATE_IMAGE_REPOSITORY,
28
- devcontainerImageRepository: DEFAULT_DEVCONTAINER_IMAGE_REPOSITORY
29
- };
30
- }
31
- const createMetadata = resolveCreateMetadata();
32
- /** Version of @decohere/create, used to pin scaffolded @decohere/* dependencies. */ export const CREATE_VERSION = createMetadata.version;
33
- /** Canonical published Docker repository for the @decohere/create builder image. */ export const CREATE_IMAGE_REPOSITORY = createMetadata.imageRepository;
34
- /** Published app Dev Container base (`docker pull` updates shared layers). */ export const DEVCONTAINER_IMAGE_REPOSITORY = createMetadata.devcontainerImageRepository;
@@ -1,57 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { DEFAULT_WEBAPI_NAME } from "./const.js";
4
- /** Lowercase npm-safe directory slug: `billing`, `admin-api`. */ export const API_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
5
- export class CreateCliError extends Error {
6
- constructor(message){
7
- super(message);
8
- this.name = "CreateCliError";
9
- }
10
- }
11
- export function assertValidApiName(name) {
12
- if (!API_NAME_PATTERN.test(name)) {
13
- throw new CreateCliError(`Invalid API name "${name}". Use a lowercase slug starting with a letter, then letters, digits, or hyphens (e.g. billing).`);
14
- }
15
- }
16
- /** Default `["api"]` when the caller omitted `--name`. Rejects duplicates and invalid slugs. */ export function parseApiNames(raw) {
17
- const names = raw != null && raw.length > 0 ? [
18
- ...raw
19
- ] : [
20
- DEFAULT_WEBAPI_NAME
21
- ];
22
- const seen = new Set();
23
- for (const name of names){
24
- assertValidApiName(name);
25
- if (seen.has(name)) {
26
- throw new CreateCliError(`Duplicate API name "${name}".`);
27
- }
28
- seen.add(name);
29
- }
30
- return names;
31
- }
32
- export function isDecohereWorkspace(dir) {
33
- try {
34
- const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
35
- const build = pkg.scripts?.build;
36
- return typeof build === "string" && build.includes("forge");
37
- } catch {
38
- return false;
39
- }
40
- }
41
- /** Directory names under `projects/` that already have a `package.json`. */ export function listExistingApiNames(dir) {
42
- const projectsDir = join(dir, "projects");
43
- try {
44
- return readdirSync(projectsDir, {
45
- withFileTypes: true
46
- }).filter((entry)=>entry.isDirectory() && existsSync(join(projectsDir, entry.name, "package.json"))).map((entry)=>entry.name).sort();
47
- } catch {
48
- return [];
49
- }
50
- }
51
- /** `billing` → `BILLING`, `admin-api` → `ADMIN_API` — prefix for `{PREFIX}_DB_HOST`. */ export function apiEnvPrefix(apiName) {
52
- return apiName.replace(/-/g, "_").toUpperCase();
53
- }
54
- export function apiNameFromProjectDir(dir) {
55
- const slash = dir.lastIndexOf("/");
56
- return slash >= 0 ? dir.slice(slash + 1) : dir;
57
- }
@@ -1,185 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { basename, join, resolve } from "node:path";
3
- import { CreateCliError, isDecohereWorkspace } from "./api-name.js";
4
- import { discoverAuthorizeScopes, mergeApiScopes, postmanRequestedScopes } from "./authorize-scopes.js";
5
- import * as auth0Cli from "./features/auth0-cli.js";
6
- /** Same Postman OAuth callbacks as `PostmanFeature`. */ const POSTMAN_CALLBACKS = [
7
- "https://oauth.pstmn.io/v1/callback",
8
- "https://oauth.pstmn.io/v1/browser-callback"
9
- ];
10
- const SPA_SUFFIX = " Postman";
11
- export const AUTH0_CLI_INSTALL_MESSAGE = "The Auth0 CLI is required.\n" + " It is bundled in the Decohere create image (`npx @decohere/create auth0 login`).\n" + " Host install: brew install auth0/auth0-cli/auth0\n" + " Or visit: https://github.com/auth0/auth0-cli";
12
- function fail(message) {
13
- throw new CreateCliError(message);
14
- }
15
- /** Throw if the `auth0` binary is missing. */ export function requireInstalled() {
16
- if (auth0Cli.isInstalled()) return;
17
- fail(AUTH0_CLI_INSTALL_MESSAGE);
18
- }
19
- /**
20
- * Log in when there is no tenant session or the Management API token is expired.
21
- * Returns the active tenant.
22
- */ export function ensureLoggedIn() {
23
- requireInstalled();
24
- process.stdout.write("\n Checking Auth0 credentials...");
25
- let tenants = auth0Cli.listTenants();
26
- if (tenants.length === 0) {
27
- process.stdout.write(" not logged in.\n");
28
- console.log(" Logging in to Auth0 (open the URL in your browser)...");
29
- if (!auth0Cli.login()) fail("Auth0 login failed.");
30
- tenants = auth0Cli.listTenants();
31
- }
32
- try {
33
- auth0Cli.listApis();
34
- process.stdout.write(" ok.\n");
35
- } catch (err) {
36
- if (err instanceof auth0Cli.Auth0AuthError) {
37
- process.stdout.write(" token expired.\n");
38
- console.log(" Re-authenticating (open the URL in your browser)...");
39
- if (!auth0Cli.login()) fail("Auth0 login failed.");
40
- tenants = auth0Cli.listTenants();
41
- }
42
- }
43
- const active = tenants.find((t)=>t.active) ?? tenants[0];
44
- if (active == null) fail("No Auth0 tenant after login.");
45
- return active;
46
- }
47
- export function runAuth0Login() {
48
- requireInstalled();
49
- console.log(" Logging in to Auth0 (open the URL in your browser)...");
50
- if (!auth0Cli.login()) fail("Auth0 login failed.");
51
- const tenants = auth0Cli.listTenants();
52
- const active = tenants.find((t)=>t.active) ?? tenants[0];
53
- if (active == null) fail("No Auth0 tenant after login.");
54
- console.log(`\n ✓ Auth0 tenant: https://${active.name}`);
55
- console.log(" Tokens are stored in ~/.config/auth0 (mounted into the create image).");
56
- console.log(" Next: npx @decohere/create auth0 configure");
57
- }
58
- export function runAuth0Configure(directory = ".") {
59
- const targetDir = resolve(directory);
60
- if (!isDecohereWorkspace(targetDir)) {
61
- throw new CreateCliError(`Not a Decohere workspace: ${targetDir}. Scaffold one first with \`npx @decohere/create webapi\`.`);
62
- }
63
- const active = ensureLoggedIn();
64
- const issuerBaseUrl = `https://${active.name}`;
65
- const projectName = process.env.DECOHERE_PROJECT_NAME?.trim() || basename(targetDir);
66
- const envPath = join(targetDir, ".env");
67
- const existing = readEnvFile(envPath);
68
- const existingAudience = existing.AUTH0_AUDIENCE?.trim();
69
- const discovered = discoverAuthorizeScopes(targetDir);
70
- let existingScopes = [];
71
- if (existingAudience) {
72
- try {
73
- existingScopes = auth0Cli.listApiScopes(existingAudience);
74
- } catch (err) {
75
- if (err instanceof auth0Cli.Auth0AuthError) throw err;
76
- }
77
- }
78
- const apiScopes = resolveApiScopes(discovered, existingScopes);
79
- console.log(discovered.length > 0 ? ` ✓ Scanned @Authorize — ${discovered.length} scope(s) from controllers` : " ✓ No @Authorize in projects/*/src — keeping existing / Hello scopes");
80
- let audience = existingAudience;
81
- if (audience) {
82
- try {
83
- auth0Cli.updateApiScopes(audience, apiScopes);
84
- console.log(` ✓ API scopes ensured (${apiScopes.join(", ")})`);
85
- } catch (err) {
86
- console.error(` ✗ Failed to update API scopes: ${err instanceof Error ? err.message : err}`);
87
- }
88
- } else {
89
- const identifier = `https://api.${projectName}`;
90
- console.log(`\n Creating Auth0 API ${identifier}...`);
91
- audience = auth0Cli.createApi(`${projectName} API`, identifier, apiScopes).identifier;
92
- console.log(` ✓ API created: ${audience}`);
93
- }
94
- const appName = `${projectName}${SPA_SUFFIX}`;
95
- const app = auth0Cli.ensureSpaApp(appName, [
96
- ...POSTMAN_CALLBACKS
97
- ]);
98
- console.log(app.reused ? ` ✓ Reusing Auth0 SPA app (${app.client_id})` : ` ✓ Auth0 SPA app created (${app.client_id})`);
99
- try {
100
- auth0Cli.enableConnectionForClient(auth0Cli.DB_CONNECTION_NAME, app.client_id);
101
- console.log(` ✓ Enabled ${auth0Cli.DB_CONNECTION_NAME} on SPA`);
102
- } catch (err) {
103
- console.error(` ✗ Failed to enable DB connection on SPA: ${err instanceof Error ? err.message : err}`);
104
- }
105
- upsertEnv(envPath, {
106
- AUTH0_ISSUER_BASE_URL: issuerBaseUrl,
107
- AUTH0_AUDIENCE: audience
108
- });
109
- console.log(` ✓ Wrote AUTH0_ISSUER_BASE_URL and AUTH0_AUDIENCE to .env`);
110
- const adminSub = existing.AUTH0_ADMIN_USER_SUB?.trim();
111
- if (adminSub) {
112
- try {
113
- auth0Cli.assignUserPermissions(adminSub, audience, apiScopes);
114
- console.log(` ✓ Assigned ${apiScopes.length} API permission(s) to admin user`);
115
- } catch (err) {
116
- console.error(` ✗ Failed to assign admin permissions: ${err instanceof Error ? err.message : err}`);
117
- }
118
- }
119
- const domain = issuerBaseUrl.replace(/^https?:\/\//, "");
120
- patchPostmanAuth(targetDir, projectName, {
121
- domain,
122
- audience,
123
- clientId: app.client_id,
124
- scopes: postmanRequestedScopes(apiScopes)
125
- });
126
- }
127
- function readEnvFile(path) {
128
- if (!existsSync(path)) return {};
129
- const out = {};
130
- for (const line of readFileSync(path, "utf8").split("\n")){
131
- const trimmed = line.trim();
132
- if (trimmed === "" || trimmed.startsWith("#")) continue;
133
- const eq = line.indexOf("=");
134
- if (eq <= 0) continue;
135
- out[line.slice(0, eq).trim()] = line.slice(eq + 1);
136
- }
137
- return out;
138
- }
139
- function resolveApiScopes(discovered, existing) {
140
- const merged = mergeApiScopes(discovered, existing);
141
- return merged.length > 0 ? merged : [
142
- ...auth0Cli.HELLO_API_SCOPES
143
- ].sort();
144
- }
145
- function upsertEnv(path, entries) {
146
- let text = existsSync(path) ? readFileSync(path, "utf8") : "";
147
- for (const [key, value] of Object.entries(entries)){
148
- const re = new RegExp(`^${key}=.*$`, "m");
149
- if (re.test(text)) {
150
- text = text.replace(re, `${key}=${value}`);
151
- } else {
152
- text = `${text}${text === "" || text.endsWith("\n") ? "" : "\n"}${key}=${value}\n`;
153
- }
154
- }
155
- if (text !== "" && !text.endsWith("\n")) text = `${text}\n`;
156
- writeFileSync(path, text);
157
- }
158
- function patchPostmanAuth(targetDir, projectName, auth) {
159
- const envYaml = join(targetDir, "postman/environments", `${projectName}-Local.environment.yaml`);
160
- if (existsSync(envYaml)) {
161
- let yaml = readFileSync(envYaml, "utf8");
162
- yaml = replaceYamlKeyedValue(yaml, "auth0Domain", auth.domain);
163
- yaml = replaceYamlKeyedValue(yaml, "auth0Audience", auth.audience);
164
- yaml = replaceYamlKeyedValue(yaml, "auth0ClientId", auth.clientId);
165
- yaml = replaceYamlKeyedValue(yaml, "authUrl", `https://${auth.domain}/authorize?audience=${auth.audience}`);
166
- yaml = replaceYamlKeyedValue(yaml, "accessTokenUrl", `https://${auth.domain}/oauth/token`);
167
- yaml = replaceYamlKeyedValue(yaml, "scope", auth.scopes);
168
- writeFileSync(envYaml, yaml);
169
- console.log(` ✓ Updated Postman environment auth0ClientId (${auth.clientId})`);
170
- console.log(` ✓ Updated Postman token scopes`);
171
- }
172
- const collectionDef = join(targetDir, "postman/collections", `${projectName} WebAPI`, ".resources/definition.yaml");
173
- if (existsSync(collectionDef)) {
174
- let yaml = readFileSync(collectionDef, "utf8");
175
- yaml = yaml.replace(/^(\s*clientId:\s*).+$/m, `$1${JSON.stringify(auth.clientId)}`);
176
- yaml = yaml.replace(/^(\s*scope:\s*).+$/m, `$1${JSON.stringify(auth.scopes)}`);
177
- writeFileSync(collectionDef, yaml);
178
- }
179
- }
180
- /** Replace `key: <name>` / `value: …` pairs in Postman Local View environment YAML. */ function replaceYamlKeyedValue(yaml, key, value) {
181
- const quoted = JSON.stringify(value);
182
- const re = new RegExp(`(key:\\s*${key}\\s*\\n\\s*value:\\s*)[^\\n]+`, "m");
183
- if (!re.test(yaml)) return yaml;
184
- return yaml.replace(re, `$1${quoted}`);
185
- }
@@ -1,55 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { listExistingApiNames } from "./api-name.js";
4
- /** OIDC scopes requested on the Postman SPA token (not Auth0 API permissions). */ export const OIDC_SCOPES = [
5
- "openid",
6
- "profile",
7
- "email"
8
- ];
9
- const AUTHORIZE_RE = /@Authorize\(\s*(["'])([^"']+)\1(?:\s*,\s*(["'])([^"']+)\3)?\s*\)/g;
10
- function isAppSource(fileName) {
11
- return fileName.endsWith(".ts") && !fileName.endsWith(".spec.ts") && !fileName.endsWith(".test.ts") && !fileName.endsWith(".d.ts");
12
- }
13
- /** Collect action:Subject scopes from Authorize decorators in each API src tree. */ export function discoverAuthorizeScopes(rootDir) {
14
- const scopes = new Set();
15
- for (const apiName of listExistingApiNames(rootDir)){
16
- collectFromDir(join(rootDir, "projects", apiName, "src"), scopes);
17
- }
18
- return [
19
- ...scopes
20
- ].sort();
21
- }
22
- function collectFromDir(dir, scopes) {
23
- if (!existsSync(dir)) return;
24
- for (const entry of readdirSync(dir, {
25
- withFileTypes: true
26
- })){
27
- const path = join(dir, entry.name);
28
- if (entry.isDirectory()) {
29
- collectFromDir(path, scopes);
30
- continue;
31
- }
32
- if (!entry.isFile() || !isAppSource(entry.name)) continue;
33
- const text = readFileSync(path, "utf8");
34
- for (const match of text.matchAll(AUTHORIZE_RE)){
35
- const action = match[2];
36
- if (action == null || action === "") continue;
37
- const subject = match[4] ?? "all";
38
- scopes.add(`${action}:${subject}`);
39
- }
40
- }
41
- }
42
- /** Additive union so Auth0 CLI scope replace does not drop permissions already on the API. */ export function mergeApiScopes(discovered, existing) {
43
- return [
44
- ...new Set([
45
- ...existing,
46
- ...discovered
47
- ])
48
- ].sort();
49
- }
50
- export function postmanRequestedScopes(apiScopes) {
51
- return [
52
- ...OIDC_SCOPES,
53
- ...apiScopes
54
- ].join(" ");
55
- }
@@ -1,34 +0,0 @@
1
- import { resolveTargetDir } from "../resolve-target-dir.js";
2
- import { parseApiNames } from "./api-name.js";
3
- import { scaffoldApi } from "./create-api.js";
4
- export function collectName(value, previous) {
5
- previous.push(value);
6
- return previous;
7
- }
8
- export function addWebapiFeatureOptions(cmd) {
9
- return cmd.option("--database <provider>", "Database provider: none | postgres/typeorm").option("--auth <provider>", "Auth provider: none | auth0").option("--postman <provider>", "Postman provider: none | local | api").option("-y, --yes", "Install dependencies without prompting").option("--install <mode>", "Dependency install mode: ask | yes | no", "ask");
10
- }
11
- export function toApiOptions(directory, opts, extra = {}) {
12
- const installMode = String(opts.install ?? "ask").toLowerCase();
13
- const names = Array.isArray(opts.name) ? opts.name : typeof opts.name === "string" ? [
14
- opts.name
15
- ] : extra.names;
16
- return {
17
- d: resolveTargetDir(directory, typeof opts.d === "string" ? opts.d : undefined),
18
- databaseProvider: opts.database,
19
- authProvider: opts.auth,
20
- postmanProvider: opts.postman,
21
- installDependencies: opts.yes === true ? "yes" : installMode === "yes" || installMode === "no" ? installMode : undefined,
22
- names,
23
- ...extra
24
- };
25
- }
26
- export async function runScaffoldApi(normalized) {
27
- parseApiNames(normalized.names);
28
- const { closePrompt } = await import("../domain/prompt.js");
29
- try {
30
- await scaffoldApi(normalized);
31
- } finally{
32
- closePrompt();
33
- }
34
- }