@decohere/scaffold 0.0.1-dev.100

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 (208) hide show
  1. package/commonjs/src/add-api.action.js +37 -0
  2. package/commonjs/src/add-api.command.js +39 -0
  3. package/commonjs/src/add.command.js +22 -0
  4. package/commonjs/src/add.module.js +24 -0
  5. package/commonjs/src/auth0-configure.action.js +28 -0
  6. package/commonjs/src/auth0-configure.command.js +24 -0
  7. package/commonjs/src/auth0-login.action.js +28 -0
  8. package/commonjs/src/auth0-login.command.js +23 -0
  9. package/commonjs/src/auth0.command.js +22 -0
  10. package/commonjs/src/auth0.module.js +28 -0
  11. package/commonjs/src/cli.js +7 -0
  12. package/commonjs/src/create.command.js +22 -0
  13. package/commonjs/src/create.module.js +24 -0
  14. package/commonjs/src/domain/prompt.js +62 -0
  15. package/commonjs/src/index.js +48 -0
  16. package/commonjs/src/models/compose.model.js +258 -0
  17. package/commonjs/src/models/config-yaml.model.js +86 -0
  18. package/commonjs/src/models/const.js +30 -0
  19. package/commonjs/src/models/dev-container.model.js +172 -0
  20. package/commonjs/src/models/docker-image.model.js +277 -0
  21. package/commonjs/src/models/env-file.model.js +48 -0
  22. package/commonjs/src/models/gitignore.model.js +48 -0
  23. package/commonjs/src/models/launch-json.model.js +70 -0
  24. package/commonjs/src/models/package-json.model.js +156 -0
  25. package/commonjs/src/models/project.model.js +132 -0
  26. package/commonjs/src/models/source-file.js +4 -0
  27. package/commonjs/src/models/tsconfig.model.js +32 -0
  28. package/commonjs/src/models/vitest-config.model.js +33 -0
  29. package/commonjs/src/models/vscode-settings.model.js +29 -0
  30. package/commonjs/src/program.js +64 -0
  31. package/commonjs/src/resolve-target-dir.js +17 -0
  32. package/commonjs/src/services/scaffold.service.js +60 -0
  33. package/commonjs/src/version.js +55 -0
  34. package/commonjs/src/webapi/api-name.js +93 -0
  35. package/commonjs/src/webapi/auth0-commands.js +250 -0
  36. package/commonjs/src/webapi/authorize-scopes.js +79 -0
  37. package/commonjs/src/webapi/cli-options.js +99 -0
  38. package/commonjs/src/webapi/const.js +54 -0
  39. package/commonjs/src/webapi/create-api.js +265 -0
  40. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.js +164 -0
  41. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
  42. package/commonjs/src/webapi/devcontainer/host-scripts.js +50 -0
  43. package/commonjs/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
  44. package/commonjs/src/webapi/devcontainer/templates/initialize.sh +169 -0
  45. package/commonjs/src/webapi/devcontainer/templates/post-create.sh +35 -0
  46. package/commonjs/src/webapi/devcontainer/templates/post-start.sh +65 -0
  47. package/commonjs/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
  48. package/commonjs/src/webapi/features/auth.feature.js +408 -0
  49. package/commonjs/src/webapi/features/auth0-cli.js +472 -0
  50. package/commonjs/src/webapi/features/biome-format.js +47 -0
  51. package/commonjs/src/webapi/features/database.feature.js +432 -0
  52. package/commonjs/src/webapi/features/finalize.feature.js +93 -0
  53. package/commonjs/src/webapi/features/postman.feature.js +322 -0
  54. package/commonjs/src/webapi/models/api-package-json.model.js +58 -0
  55. package/commonjs/src/webapi/models/auth-config.model.js +24 -0
  56. package/commonjs/src/webapi/models/controller-source-file.model.js +190 -0
  57. package/commonjs/src/webapi/models/github-ci.model.js +193 -0
  58. package/commonjs/src/webapi/models/main-source-file.model.js +165 -0
  59. package/commonjs/src/webapi/models/production-dockerfile.model.js +75 -0
  60. package/commonjs/src/webapi/models/sample-files.model.js +58 -0
  61. package/commonjs/src/webapi/models/webapi-image.model.js +25 -0
  62. package/commonjs/src/webapi/postman/hello-openapi.js +173 -0
  63. package/commonjs/src/webapi/postman/local-view.js +182 -0
  64. package/commonjs/src/webapi/postman/openapi-to-postman.js +200 -0
  65. package/commonjs/src/webapi/workspace-readme.js +109 -0
  66. package/commonjs/src/webapi.action.js +28 -0
  67. package/commonjs/src/webapi.command.js +43 -0
  68. package/es6/src/add-api.action.js +27 -0
  69. package/es6/src/add-api.command.js +29 -0
  70. package/es6/src/add.command.js +12 -0
  71. package/es6/src/add.module.js +14 -0
  72. package/es6/src/auth0-configure.action.js +18 -0
  73. package/es6/src/auth0-configure.command.js +14 -0
  74. package/es6/src/auth0-login.action.js +18 -0
  75. package/es6/src/auth0-login.command.js +13 -0
  76. package/es6/src/auth0.command.js +12 -0
  77. package/es6/src/auth0.module.js +18 -0
  78. package/es6/src/cli.js +3 -0
  79. package/es6/src/create.command.js +12 -0
  80. package/es6/src/create.module.js +14 -0
  81. package/es6/src/domain/prompt.js +41 -0
  82. package/es6/src/index.js +9 -0
  83. package/es6/src/models/compose.model.js +254 -0
  84. package/es6/src/models/config-yaml.model.js +74 -0
  85. package/es6/src/models/const.js +20 -0
  86. package/es6/src/models/dev-container.model.js +162 -0
  87. package/es6/src/models/docker-image.model.js +235 -0
  88. package/es6/src/models/env-file.model.js +44 -0
  89. package/es6/src/models/gitignore.model.js +38 -0
  90. package/es6/src/models/launch-json.model.js +60 -0
  91. package/es6/src/models/package-json.model.js +141 -0
  92. package/es6/src/models/project.model.js +117 -0
  93. package/es6/src/models/source-file.js +1 -0
  94. package/es6/src/models/tsconfig.model.js +22 -0
  95. package/es6/src/models/vitest-config.model.js +23 -0
  96. package/es6/src/models/vscode-settings.model.js +19 -0
  97. package/es6/src/program.js +46 -0
  98. package/es6/src/resolve-target-dir.js +7 -0
  99. package/es6/src/services/scaffold.service.js +50 -0
  100. package/es6/src/version.js +34 -0
  101. package/es6/src/webapi/api-name.js +57 -0
  102. package/es6/src/webapi/auth0-commands.js +185 -0
  103. package/es6/src/webapi/authorize-scopes.js +55 -0
  104. package/es6/src/webapi/cli-options.js +34 -0
  105. package/es6/src/webapi/const.js +21 -0
  106. package/es6/src/webapi/create-api.js +245 -0
  107. package/es6/src/webapi/devcontainer/codebase-rag-mcp.js +154 -0
  108. package/es6/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
  109. package/es6/src/webapi/devcontainer/host-scripts.js +29 -0
  110. package/es6/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
  111. package/es6/src/webapi/devcontainer/templates/initialize.sh +169 -0
  112. package/es6/src/webapi/devcontainer/templates/post-create.sh +35 -0
  113. package/es6/src/webapi/devcontainer/templates/post-start.sh +65 -0
  114. package/es6/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
  115. package/es6/src/webapi/features/auth.feature.js +357 -0
  116. package/es6/src/webapi/features/auth0-cli.js +406 -0
  117. package/es6/src/webapi/features/biome-format.js +40 -0
  118. package/es6/src/webapi/features/database.feature.js +422 -0
  119. package/es6/src/webapi/features/finalize.feature.js +83 -0
  120. package/es6/src/webapi/features/postman.feature.js +256 -0
  121. package/es6/src/webapi/models/api-package-json.model.js +53 -0
  122. package/es6/src/webapi/models/auth-config.model.js +20 -0
  123. package/es6/src/webapi/models/controller-source-file.model.js +181 -0
  124. package/es6/src/webapi/models/github-ci.model.js +186 -0
  125. package/es6/src/webapi/models/main-source-file.model.js +161 -0
  126. package/es6/src/webapi/models/production-dockerfile.model.js +66 -0
  127. package/es6/src/webapi/models/sample-files.model.js +40 -0
  128. package/es6/src/webapi/models/webapi-image.model.js +18 -0
  129. package/es6/src/webapi/postman/hello-openapi.js +166 -0
  130. package/es6/src/webapi/postman/local-view.js +167 -0
  131. package/es6/src/webapi/postman/openapi-to-postman.js +177 -0
  132. package/es6/src/webapi/workspace-readme.js +88 -0
  133. package/es6/src/webapi.action.js +18 -0
  134. package/es6/src/webapi.command.js +33 -0
  135. package/package.json +36 -0
  136. package/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
  137. package/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
  138. package/src/webapi/devcontainer/templates/initialize.sh +169 -0
  139. package/src/webapi/devcontainer/templates/post-create.sh +35 -0
  140. package/src/webapi/devcontainer/templates/post-start.sh +65 -0
  141. package/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
  142. package/types/src/add-api.action.d.ts +13 -0
  143. package/types/src/add-api.command.d.ts +7 -0
  144. package/types/src/add.command.d.ts +6 -0
  145. package/types/src/add.module.d.ts +14 -0
  146. package/types/src/auth0-configure.action.d.ts +7 -0
  147. package/types/src/auth0-configure.command.d.ts +6 -0
  148. package/types/src/auth0-login.action.d.ts +4 -0
  149. package/types/src/auth0-login.command.d.ts +6 -0
  150. package/types/src/auth0.command.d.ts +6 -0
  151. package/types/src/auth0.module.d.ts +18 -0
  152. package/types/src/cli.d.ts +2 -0
  153. package/types/src/create.command.d.ts +6 -0
  154. package/types/src/create.module.d.ts +14 -0
  155. package/types/src/domain/prompt.d.ts +4 -0
  156. package/types/src/index.d.ts +13 -0
  157. package/types/src/models/compose.model.d.ts +73 -0
  158. package/types/src/models/config-yaml.model.d.ts +27 -0
  159. package/types/src/models/const.d.ts +20 -0
  160. package/types/src/models/dev-container.model.d.ts +49 -0
  161. package/types/src/models/docker-image.model.d.ts +90 -0
  162. package/types/src/models/env-file.model.d.ts +16 -0
  163. package/types/src/models/gitignore.model.d.ts +9 -0
  164. package/types/src/models/launch-json.model.d.ts +7 -0
  165. package/types/src/models/package-json.model.d.ts +36 -0
  166. package/types/src/models/project.model.d.ts +60 -0
  167. package/types/src/models/source-file.d.ts +11 -0
  168. package/types/src/models/tsconfig.model.d.ts +5 -0
  169. package/types/src/models/vitest-config.model.d.ts +5 -0
  170. package/types/src/models/vscode-settings.model.d.ts +7 -0
  171. package/types/src/program.d.ts +3 -0
  172. package/types/src/resolve-target-dir.d.ts +5 -0
  173. package/types/src/services/scaffold.service.d.ts +22 -0
  174. package/types/src/version.d.ts +6 -0
  175. package/types/src/webapi/api-name.d.ts +14 -0
  176. package/types/src/webapi/auth0-commands.d.ts +11 -0
  177. package/types/src/webapi/authorize-scopes.d.ts +7 -0
  178. package/types/src/webapi/cli-options.d.ts +6 -0
  179. package/types/src/webapi/const.d.ts +19 -0
  180. package/types/src/webapi/create-api.d.ts +15 -0
  181. package/types/src/webapi/devcontainer/codebase-rag-mcp.d.ts +32 -0
  182. package/types/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
  183. package/types/src/webapi/devcontainer/host-scripts.d.ts +7 -0
  184. package/types/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
  185. package/types/src/webapi/devcontainer/templates/initialize.sh +169 -0
  186. package/types/src/webapi/devcontainer/templates/post-create.sh +35 -0
  187. package/types/src/webapi/devcontainer/templates/post-start.sh +65 -0
  188. package/types/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
  189. package/types/src/webapi/features/auth.feature.d.ts +10 -0
  190. package/types/src/webapi/features/auth0-cli.d.ts +84 -0
  191. package/types/src/webapi/features/biome-format.d.ts +5 -0
  192. package/types/src/webapi/features/database.feature.d.ts +7 -0
  193. package/types/src/webapi/features/finalize.feature.d.ts +2 -0
  194. package/types/src/webapi/features/postman.feature.d.ts +16 -0
  195. package/types/src/webapi/models/api-package-json.model.d.ts +18 -0
  196. package/types/src/webapi/models/auth-config.model.d.ts +26 -0
  197. package/types/src/webapi/models/controller-source-file.model.d.ts +46 -0
  198. package/types/src/webapi/models/github-ci.model.d.ts +9 -0
  199. package/types/src/webapi/models/main-source-file.model.d.ts +54 -0
  200. package/types/src/webapi/models/production-dockerfile.model.d.ts +18 -0
  201. package/types/src/webapi/models/sample-files.model.d.ts +7 -0
  202. package/types/src/webapi/models/webapi-image.model.d.ts +12 -0
  203. package/types/src/webapi/postman/hello-openapi.d.ts +19 -0
  204. package/types/src/webapi/postman/local-view.d.ts +17 -0
  205. package/types/src/webapi/postman/openapi-to-postman.d.ts +62 -0
  206. package/types/src/webapi/workspace-readme.d.ts +18 -0
  207. package/types/src/webapi.action.d.ts +14 -0
  208. package/types/src/webapi.command.d.ts +11 -0
@@ -0,0 +1,422 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { 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 { withPostgresqlClient } from "../../models/docker-image.model.js";
7
+ import { CREATE_VERSION } from "../../version.js";
8
+ import { apiEnvPrefix, apiNameFromProjectDir } from "../api-name.js";
9
+ /** Bind path relative to `.devcontainer/docker-compose.yml` (dies with the working tree). */ const POSTGRES_DATA_BIND = "./postgres-data:/var/lib/postgresql/data";
10
+ const POSTGRES_DATA_GITIGNORE = ".devcontainer/postgres-data";
11
+ const DB_RESET_SCRIPT = "rm -rf .devcontainer/postgres-data && docker compose -f .devcontainer/docker-compose.yml down && docker compose -f .devcontainer/docker-compose.yml up -d postgres";
12
+ /** Read a single KEY= from an existing target `.env` (if any). */ function readExistingEnvValue(workspace, key) {
13
+ try {
14
+ const raw = readFileSync(join(workspace.targetDir, ".env"), "utf8");
15
+ const line = raw.split("\n").find((l)=>l.startsWith(`${key}=`));
16
+ const value = line?.slice(key.length + 1).trim();
17
+ return value || undefined;
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ function randomPassword() {
23
+ return randomBytes(16).toString("hex");
24
+ }
25
+ /** Safe Postgres identifier fragment from the project name. */ function toSqlIdentFragment(name) {
26
+ const cleaned = name.replace(/[^A-Za-z0-9_]/g, "_").replace(/^([^A-Za-z_])/, "p_$1");
27
+ return cleaned.length > 0 ? cleaned.slice(0, 40) : "app";
28
+ }
29
+ function passwordOrExisting(workspace, key, fallbackExistingKeys = []) {
30
+ const existing = readExistingEnvValue(workspace, key);
31
+ if (existing) return existing;
32
+ for (const alt of fallbackExistingKeys){
33
+ const v = readExistingEnvValue(workspace, alt);
34
+ if (v) return v;
35
+ }
36
+ return randomPassword();
37
+ }
38
+ // Production: runtime app role only — no bootstrap, no admin/migrate secrets required.
39
+ function renderBaseDataSource(apiName) {
40
+ const p = apiEnvPrefix(apiName);
41
+ return [
42
+ ` - name: ${apiName}`,
43
+ ` provider: postgres`,
44
+ ` host: \${${p}_DB_HOST}`,
45
+ ` port: \${${p}_DB_PORT}`,
46
+ ` database: \${${p}_DB_NAME}`,
47
+ ` username: \${${p}_DB_USER}`,
48
+ ` password: \${${p}_DB_PASSWORD}`,
49
+ ` synchronize: false`,
50
+ ` seed: false`,
51
+ ` bootstrap: false`
52
+ ].join("\n");
53
+ }
54
+ // Dev overlay (merged by list index onto config.yaml). Enable bootstrap + migrate role.
55
+ function renderDevDataSource(apiName) {
56
+ const p = apiEnvPrefix(apiName);
57
+ return [
58
+ ` - name: ${apiName}`,
59
+ ` synchronize: true`,
60
+ ` seed: true`,
61
+ ` bootstrap: true`,
62
+ ` adminUser: \${DB_ADMIN_USER}`,
63
+ ` adminPassword: \${DB_ADMIN_PASSWORD}`,
64
+ ` migrateUsername: \${${p}_DB_MIGRATE_USER}`,
65
+ ` migratePassword: \${${p}_DB_MIGRATE_PASSWORD}`
66
+ ].join("\n");
67
+ }
68
+ function renderDatabaseSection(apiNames, items) {
69
+ return `database:\n dataSources:\n${apiNames.map(items).join("\n")}\n`;
70
+ }
71
+ /** Create-or-sync bootstrap role; runs on every Postgres container start (not only empty volume). */ const POSTGRES_ENSURE_BOOTSTRAP_SH = `#!/bin/bash
72
+ set -euo pipefail
73
+ # Superuser aligns decohere_bootstrap with DB_ADMIN_* from env_file on every start.
74
+ # Initdb.d alone is insufficient: passwords in .env can change while the volume persists.
75
+ : "\${POSTGRES_USER:?POSTGRES_USER is required}"
76
+ : "\${DB_ADMIN_USER:?DB_ADMIN_USER is required}"
77
+ : "\${DB_ADMIN_PASSWORD:?DB_ADMIN_PASSWORD is required}"
78
+
79
+ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname postgres \\
80
+ \t--set=bootstrap_user="$DB_ADMIN_USER" \\
81
+ \t--set=bootstrap_password="$DB_ADMIN_PASSWORD" <<'EOSQL'
82
+ SELECT format(
83
+ \t'CREATE ROLE %I LOGIN PASSWORD %L CREATEDB CREATEROLE NOSUPERUSER NOBYPASSRLS',
84
+ \t:'bootstrap_user',
85
+ \t:'bootstrap_password'
86
+ )
87
+ WHERE NOT EXISTS (
88
+ \tSELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'bootstrap_user'
89
+ )\\gexec
90
+
91
+ SELECT format(
92
+ \t'ALTER ROLE %I WITH LOGIN PASSWORD %L CREATEDB CREATEROLE NOSUPERUSER NOBYPASSRLS',
93
+ \t:'bootstrap_user',
94
+ \t:'bootstrap_password'
95
+ )\\gexec
96
+
97
+ GRANT CONNECT ON DATABASE postgres TO :"bootstrap_user";
98
+ EOSQL
99
+ `;
100
+ const POSTGRES_ENTRYPOINT_SH = `#!/bin/bash
101
+ set -euo pipefail
102
+ (
103
+ \tfor _ in $(seq 1 60); do
104
+ \t\tif pg_isready -U "\${POSTGRES_USER:-postgres}" >/dev/null 2>&1; then
105
+ \t\t\t/usr/local/bin/decohere-ensure-bootstrap.sh
106
+ \t\t\texit 0
107
+ \t\tfi
108
+ \t\tsleep 0.5
109
+ \tdone
110
+ \techo "decohere: timed out waiting for Postgres before ensuring bootstrap role" >&2
111
+ \texit 1
112
+ ) &
113
+ exec docker-entrypoint.sh "$@"
114
+ `;
115
+ const POSTGRES_IMAGE_DOCKERFILE = `FROM postgres:16
116
+ COPY ensure-bootstrap-role.sh /usr/local/bin/decohere-ensure-bootstrap.sh
117
+ COPY entrypoint.sh /usr/local/bin/decohere-postgres-entrypoint.sh
118
+ RUN chmod +x /usr/local/bin/decohere-ensure-bootstrap.sh /usr/local/bin/decohere-postgres-entrypoint.sh
119
+ ENTRYPOINT ["/usr/local/bin/decohere-postgres-entrypoint.sh"]
120
+ CMD ["postgres"]
121
+ `;
122
+ const SAMPLE_ENTITY_TS = `import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
123
+ import type { Entity as IEntity } from "@decohere/codex";
124
+
125
+ @Entity()
126
+ export class SampleEntity implements IEntity {
127
+ \t@PrimaryGeneratedColumn()
128
+ \tpublic id!: number;
129
+
130
+ \t@Column()
131
+ \tpublic name!: string;
132
+ }
133
+ `;
134
+ const SAMPLE_REPOSITORY_TS = `import { injectable } from "@decohere/crucible/decorators";
135
+ import { injectUnitOfWork } from "@decohere/codex";
136
+ import { TypeOrmRepository, TypeOrmUnitOfWork } from "@decohere/codex-typeorm";
137
+ import { SampleEntity } from "../entities/sample.entity.js";
138
+
139
+ @injectable()
140
+ export class SampleEntityRepository extends TypeOrmRepository<SampleEntity> {
141
+ \tpublic constructor(@injectUnitOfWork() unitOfWork: TypeOrmUnitOfWork) {
142
+ \t\tsuper(unitOfWork, SampleEntity);
143
+ \t}
144
+ }
145
+ `;
146
+ /** TypeORM CLI DataSource — connects as the migrate role; never enables synchronize. */ function renderTypeOrmDataSource(apiName) {
147
+ const p = apiEnvPrefix(apiName);
148
+ return `import { dirname, join } from "node:path";
149
+ import { fileURLToPath } from "node:url";
150
+ import { DataSource } from "typeorm";
151
+
152
+ const root = dirname(fileURLToPath(import.meta.url));
153
+ const required = (key: string): string => {
154
+ \tconst value = process.env[key];
155
+ \tif (value == null || value === "") {
156
+ \t\tthrow new Error(\`Missing env \${key} (load .env via --env-file or export it)\`);
157
+ \t}
158
+ \treturn value;
159
+ };
160
+
161
+ /**
162
+ * CLI-only DataSource for \`pnpm db:migrate\` / \`pnpm db:migrate:generate\`.
163
+ * Uses the migrate role (DDL). Runtime API pods use the app role with synchronize: false.
164
+ */
165
+ export default new DataSource({
166
+ \ttype: "postgres",
167
+ \thost: required("${p}_DB_HOST"),
168
+ \tport: Number(required("${p}_DB_PORT")),
169
+ \tdatabase: required("${p}_DB_NAME"),
170
+ \tusername: required("${p}_DB_MIGRATE_USER"),
171
+ \tpassword: required("${p}_DB_MIGRATE_PASSWORD"),
172
+ \tsynchronize: false,
173
+ \tentities: [join(root, "entities", "*.{ts,js}")],
174
+ \tmigrations: [join(root, "migrations", "*.{ts,js}")],
175
+ });
176
+ `;
177
+ }
178
+ const DATABASE_DOCS_MD = `# Database (Postgres / TypeORM)
179
+
180
+ ## Roles
181
+
182
+ | Role | Env | Purpose |
183
+ |---|---|---|
184
+ | App | \`{PREFIX}_DB_USER\` | Runtime API (DML only in production) |
185
+ | Migrate | \`{PREFIX}_DB_MIGRATE_USER\` | Schema changes / TypeORM migrations (CI/deploy) |
186
+ | Bootstrap | \`DB_ADMIN_USER\` | Create DB + roles when \`bootstrap: true\` (dev) |
187
+
188
+ \`{PREFIX}\` is the API slug in SCREAMING_SNAKE (\`api\` → \`API\`, \`billing\` → \`BILLING\`).
189
+
190
+ ## Development (fast iteration)
191
+
192
+ \`config/config.dev.yaml\` sets \`synchronize: true\` and \`seed: true\` so the API can create tables on startup. You can keep using sync while prototyping.
193
+
194
+ Alternatively, turn sync off in the overlay and use migrations the same way as production:
195
+
196
+ \`\`\`bash
197
+ pnpm db:up
198
+ pnpm db:migrate
199
+ pnpm db:migrate:generate -- ./projects/api/src/migrations/AddSomething
200
+ \`\`\`
201
+
202
+ Do **not** mix sync and migrations for the same schema change (pick one path per change).
203
+
204
+ ## Production (migrations only)
205
+
206
+ \`config/config.yaml\` keeps \`synchronize: false\` / \`seed: false\` / \`bootstrap: false\`. Deploy steps:
207
+
208
+ 1. Ensure the app database and roles already exist (provisioner / ops — not the API pod).
209
+ 2. Run migrations with the migrate role: \`pnpm db:migrate\` (or \`pnpm db:migrate:<api>\` for multi-API).
210
+ 3. Start the API with the app role only — no migrate/admin secrets on the pod.
211
+
212
+ ## Scripts
213
+
214
+ | Script | What it does |
215
+ |---|---|
216
+ | \`db:up\` / \`db:down\` / \`db:reset\` | Local Compose Postgres |
217
+ | \`db:migrate\` | Run pending migrations (\`projects/api\` by default) |
218
+ | \`db:migrate:generate\` | Generate a migration from entity drift (pass path after \`--\`) |
219
+ | \`db:migrate:<api>\` | Same for a named API under \`projects/<api>\` |
220
+
221
+ CLI entry: \`projects/<api>/src/typeorm.data-source.ts\` (migrate credentials, \`synchronize: false\`).
222
+
223
+ ## Browse the database
224
+
225
+ Prefer the Dev Container **SQLTools** extension (Postgres driver) or any desktop client against \`127.0.0.1:5433\` (see \`.env\` for credentials).
226
+
227
+ Adminer is **not** scaffolded. If you want a browser UI in Compose, add this under \`services:\` in \`.devcontainer/docker-compose.yml\` (profile \`tools\` so it stays off by default):
228
+
229
+ \`\`\`yaml
230
+ adminer:
231
+ image: adminer
232
+ profiles:
233
+ - tools
234
+ ports:
235
+ - "127.0.0.1:8081:8080"
236
+ networks:
237
+ - <compose-network-key> # same key as postgres / devcontainer
238
+ \`\`\`
239
+
240
+ Then: \`docker compose -f .devcontainer/docker-compose.yml --profile tools up -d adminer\`
241
+ `;
242
+ function typeormCli(args) {
243
+ return `node --import tsx --env-file=.env ./node_modules/typeorm/cli.js ${args}`;
244
+ }
245
+ function migrateRunScript(apiName) {
246
+ return typeormCli(`migration:run -d ./projects/${apiName}/src/typeorm.data-source.ts`);
247
+ }
248
+ function migrateGenerateScript(apiName) {
249
+ return typeormCli(`migration:generate --esm -d ./projects/${apiName}/src/typeorm.data-source.ts`);
250
+ }
251
+ async function getOptions(_workspace, cliOptions) {
252
+ if (cliOptions.databaseProvider != null) return {};
253
+ const choices = [
254
+ {
255
+ label: "none"
256
+ },
257
+ {
258
+ label: "postgres/typeorm"
259
+ }
260
+ ];
261
+ const selected = await pick("\n Database: ", choices, (c)=>c.label);
262
+ return {
263
+ databaseProvider: selected?.label ?? "none"
264
+ };
265
+ }
266
+ function scaffold(workspace, options) {
267
+ if (options.databaseProvider !== "postgres/typeorm") return;
268
+ const name = workspace.name;
269
+ const ident = toSqlIdentFragment(name);
270
+ const apiProjects = workspace.getProjects();
271
+ const apiNames = apiProjects.map((project)=>apiNameFromProjectDir(project.dir));
272
+ // Container init superuser — never reused as an API runtime role.
273
+ const postgresSuperuser = "postgres";
274
+ const postgresPassword = passwordOrExisting(workspace, "POSTGRES_PASSWORD");
275
+ const bootstrapUser = "decohere_bootstrap";
276
+ const bootstrapPassword = passwordOrExisting(workspace, "DB_ADMIN_PASSWORD", [
277
+ "POSTGRES_PASSWORD"
278
+ ]);
279
+ const devContainer = workspace.getModel(WELL_KNOWN_FILES.DEVCONTAINER_JSON);
280
+ const compose = workspace.getModel(WELL_KNOWN_FILES.DOCKER_COMPOSE);
281
+ const networkKey = name;
282
+ // Skip when the workspace already runs Postgres (`decohere add api` on a database workspace).
283
+ if (compose && !compose.hasService("postgres")) {
284
+ // One shared instance. Each API owns a database on it (created at bootstrap).
285
+ // Point `{PREFIX}_DB_HOST` / `{PREFIX}_DB_PORT` at another server to split later.
286
+ // Custom image: sync bootstrap role password on every start (initdb.d is once-only).
287
+ // Default data is a bind mount under the project so `rm -rf` the tree removes it.
288
+ compose.addHeaderComment("Postgres data defaults to ./postgres-data under this folder (bind mount; dies with the working tree).", `For data that outlives the checkout, replace the postgres volumes entry with a named volume`, `e.g. "${name}-postgres-data:/var/lib/postgresql/data" and declare it under top-level volumes:.`).addService("postgres", {
289
+ build: "./postgres",
290
+ container_name: `samples-${name}-postgres`,
291
+ restart: "unless-stopped",
292
+ ports: [
293
+ "127.0.0.1:5433:5432"
294
+ ],
295
+ volumes: [
296
+ POSTGRES_DATA_BIND
297
+ ],
298
+ networks: [
299
+ {
300
+ [networkKey]: {
301
+ aliases: [
302
+ "postgres"
303
+ ]
304
+ }
305
+ }
306
+ ],
307
+ healthcheck: {
308
+ // Ensure role/password match .env before reporting healthy.
309
+ test: [
310
+ "CMD-SHELL",
311
+ `pg_isready -U ${postgresSuperuser} && /usr/local/bin/decohere-ensure-bootstrap.sh`
312
+ ],
313
+ interval: "2s",
314
+ timeout: "5s",
315
+ retries: 15
316
+ }
317
+ }).addEnvFile("postgres", "../.env");
318
+ workspace.getModel(WELL_KNOWN_FILES.GITIGNORE)?.add(POSTGRES_DATA_GITIGNORE);
319
+ workspace.addFile({
320
+ files: ()=>[
321
+ {
322
+ filePath: ".devcontainer/postgres/Dockerfile",
323
+ content: POSTGRES_IMAGE_DOCKERFILE
324
+ },
325
+ {
326
+ filePath: ".devcontainer/postgres/entrypoint.sh",
327
+ content: POSTGRES_ENTRYPOINT_SH
328
+ },
329
+ {
330
+ filePath: ".devcontainer/postgres/ensure-bootstrap-role.sh",
331
+ content: POSTGRES_ENSURE_BOOTSTRAP_SH
332
+ }
333
+ ]
334
+ });
335
+ compose.addEnvFile("devcontainer", "../.env");
336
+ }
337
+ devContainer?.addExtension("mtxr.sqltools", "mtxr.sqltools-driver-pg");
338
+ workspace.getModel(WELL_KNOWN_FILES.DOCKERFILE)?.addLayer(withPostgresqlClient());
339
+ const pkg = workspace.getModel(WELL_KNOWN_FILES.PACKAGE_JSON)?.addDependency("@decohere/codex", CREATE_VERSION).addDependency("typeorm", "^0.3.28").addDependency("@decohere/codex-typeorm", CREATE_VERSION).addDependency("tsx", "^4.20.5").addScript("db:up", "docker compose -f .devcontainer/docker-compose.yml up -d postgres").addScript("db:down", "docker compose -f .devcontainer/docker-compose.yml down").addScript("db:reset", DB_RESET_SCRIPT);
340
+ const primaryApi = apiNames[0];
341
+ if (pkg != null) {
342
+ for (const apiName of apiNames){
343
+ pkg.addScript(`db:migrate:${apiName}`, migrateRunScript(apiName)).addScript(`db:migrate:generate:${apiName}`, migrateGenerateScript(apiName));
344
+ }
345
+ // Default aliases target the first API of a new workspace only — never overwrite on `webapi add`.
346
+ if (!options.add && primaryApi != null) {
347
+ pkg.addScript("db:migrate", migrateRunScript(primaryApi)).addScript("db:migrate:generate", migrateGenerateScript(primaryApi));
348
+ }
349
+ }
350
+ workspace.addFile({
351
+ files: ()=>[
352
+ {
353
+ filePath: "docs/database.md",
354
+ content: DATABASE_DOCS_MD
355
+ }
356
+ ]
357
+ });
358
+ writeDatabaseConfig(workspace, apiNames);
359
+ for (const project of apiProjects){
360
+ wireApiDatabase(project);
361
+ }
362
+ const env = workspace.getModel(WELL_KNOWN_FILES.ENV_FILE);
363
+ env?.addEntry("POSTGRES_DB", ident).addEntry("POSTGRES_USER", postgresSuperuser).addEntry("POSTGRES_PASSWORD", postgresPassword).addEntry("DB_ADMIN_USER", bootstrapUser).addEntry("DB_ADMIN_PASSWORD", bootstrapPassword);
364
+ for (const apiName of apiNames){
365
+ writeApiDatabaseEnv(workspace, ident, apiName);
366
+ }
367
+ }
368
+ function writeDatabaseConfig(workspace, apiNames) {
369
+ const config = workspace.getModel(WELL_KNOWN_FILES.CONFIG_YAML);
370
+ const devConfig = workspace.getModel(WELL_KNOWN_FILES.CONFIG_DEV_YAML);
371
+ const pending = apiNames.filter((apiName)=>!config?.hasDataSource(apiName));
372
+ if (pending.length === 0) return;
373
+ if (config?.hasDataSources()) {
374
+ for (const apiName of pending){
375
+ config.appendDataSource(renderBaseDataSource(apiName));
376
+ devConfig?.appendDataSource(renderDevDataSource(apiName));
377
+ }
378
+ return;
379
+ }
380
+ config?.addSection(renderDatabaseSection(pending, renderBaseDataSource));
381
+ devConfig?.addSection(renderDatabaseSection(pending, renderDevDataSource));
382
+ }
383
+ function writeApiDatabaseEnv(workspace, ident, apiName) {
384
+ const prefix = apiEnvPrefix(apiName);
385
+ const sqlApi = toSqlIdentFragment(apiName);
386
+ workspace.getModel(WELL_KNOWN_FILES.ENV_FILE)?.addEntry(`${prefix}_DB_HOST`, "postgres").addEntry(`${prefix}_DB_PORT`, "5432").addEntry(`${prefix}_DB_NAME`, `${ident}_${sqlApi}_dev`).addEntry(`${prefix}_DB_USER`, `${ident}_${sqlApi}_app`).addEntry(`${prefix}_DB_PASSWORD`, passwordOrExisting(workspace, `${prefix}_DB_PASSWORD`)).addEntry(`${prefix}_DB_MIGRATE_USER`, `${ident}_${sqlApi}_migrate`).addEntry(`${prefix}_DB_MIGRATE_PASSWORD`, passwordOrExisting(workspace, `${prefix}_DB_MIGRATE_PASSWORD`));
387
+ }
388
+ function wireApiDatabase(project) {
389
+ const apiName = apiNameFromProjectDir(project.dir);
390
+ project.getModel(WELL_KNOWN_FILES.MAIN_TS)?.setDataSourceName(apiName).addSideEffectImport("@decohere/codex-typeorm").addEntity({
391
+ className: "SampleEntity",
392
+ importPath: "./entities/sample.entity.js"
393
+ }).addRepository({
394
+ className: "SampleEntityRepository",
395
+ importPath: "./repositories/sample.repository.js"
396
+ });
397
+ project.getModel(WELL_KNOWN_FILES.API_PACKAGE_JSON)?.addDependency("@decohere/codex", CREATE_VERSION).addDependency("@decohere/codex-typeorm", CREATE_VERSION);
398
+ project.addFile({
399
+ files: ()=>[
400
+ {
401
+ filePath: "src/entities/sample.entity.ts",
402
+ content: SAMPLE_ENTITY_TS
403
+ },
404
+ {
405
+ filePath: "src/repositories/sample.repository.ts",
406
+ content: SAMPLE_REPOSITORY_TS
407
+ },
408
+ {
409
+ filePath: "src/typeorm.data-source.ts",
410
+ content: renderTypeOrmDataSource(apiName)
411
+ },
412
+ {
413
+ filePath: "src/migrations/.gitkeep",
414
+ content: ""
415
+ }
416
+ ]
417
+ });
418
+ }
419
+ export const DatabaseFeature = {
420
+ getOptions,
421
+ scaffold
422
+ };
@@ -0,0 +1,83 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { detectPackageManager, promptForPackageManager } from "@decohere/nexus/interop";
4
+ import { ask, closePrompt } from "../../domain/prompt.js";
5
+ import { WELL_KNOWN_FILES } from "../../models/const.js";
6
+ import { writeWorkspaceReadme } from "../workspace-readme.js";
7
+ import { formatScaffoldedFiles } from "./biome-format.js";
8
+ function readExistingEnvValue(workspace, key) {
9
+ try {
10
+ const raw = readFileSync(join(workspace.targetDir, ".env"), "utf8");
11
+ const line = raw.split("\n").find((l)=>l.startsWith(`${key}=`));
12
+ const value = line?.slice(key.length + 1).trim();
13
+ return value || undefined;
14
+ } catch {
15
+ return undefined;
16
+ }
17
+ }
18
+ // State resolved during scaffold(), consumed by finalize().
19
+ let _pm = null;
20
+ let _shouldInstall = true;
21
+ async function getOptions(_workspace, cliOptions) {
22
+ if (cliOptions.installDependencies != null) {
23
+ _shouldInstall = cliOptions.installDependencies === "yes";
24
+ }
25
+ return {};
26
+ }
27
+ async function scaffold(workspace, options) {
28
+ // Local scaffold default — production deploys must set NODE_ENV explicitly.
29
+ const nodeEnv = readExistingEnvValue(workspace, "NODE_ENV") ?? "dev";
30
+ workspace.getModel(WELL_KNOWN_FILES.ENV_FILE)?.addEntry("NODE_ENV", nodeEnv);
31
+ // Durable day-2 CLI examples (also refreshed on `webapi add`).
32
+ writeWorkspaceReadme(workspace, options);
33
+ if (process.env.DECOHERE_BUILDER === "1") {
34
+ _shouldInstall = false;
35
+ return;
36
+ }
37
+ _pm = detectPackageManager(workspace.targetDir) ?? await promptForPackageManager();
38
+ if (options.installDependencies != null) {
39
+ _shouldInstall = options.installDependencies === "yes";
40
+ return;
41
+ }
42
+ const answer = await ask("\nInstall dependencies now? [yes/no] (yes): ", "yes");
43
+ _shouldInstall = answer.toLowerCase() !== "no";
44
+ }
45
+ async function finalize(workspace, options) {
46
+ const formatPaths = options.add ? workspace.getProjects().map((project)=>project.dir) : [
47
+ "."
48
+ ];
49
+ formatScaffoldedFiles(workspace.targetDir, formatPaths.length > 0 ? formatPaths : [
50
+ "."
51
+ ]);
52
+ if (_shouldInstall) {
53
+ const pm = _pm;
54
+ console.log(`\nScaffolded ${workspace.name}. Installing dependencies...\n`);
55
+ await pm.install({
56
+ cwd: workspace.targetDir
57
+ });
58
+ }
59
+ const apiNames = workspace.getProjects().map((project)=>project.dir.replace(/^projects\//, ""));
60
+ console.log(`\nDone! ${_shouldInstall ? "Next" : "To get started:"} steps:\n`);
61
+ if (!options.add) {
62
+ console.log(` cd ${workspace.name}`);
63
+ }
64
+ if (!_shouldInstall && options.add) {
65
+ const pmName = _pm?.name ?? "pnpm";
66
+ console.log(` ${pmName} install`);
67
+ }
68
+ if (options.add && apiNames[0] != null) {
69
+ console.log(` pnpm run start:${apiNames[0]}`);
70
+ console.log(` pnpm run package:${apiNames[0]} && docker build projects/${apiNames[0]}`);
71
+ } else {
72
+ console.log(" code . # open in VS Code");
73
+ console.log(" # then: Reopen in Container (first create runs pnpm install)\n");
74
+ }
75
+ console.log(" See README.md for CLI examples.\n");
76
+ // Drop readline's stdin hold so the CLI / container can exit.
77
+ closePrompt();
78
+ }
79
+ export const FinalizeFeature = {
80
+ getOptions,
81
+ scaffold,
82
+ finalize
83
+ };