@zaaxch/tailframe 2.2.0 → 3.0.0
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.
- package/bin/tailframe.mjs +30 -12
- package/package.json +1 -1
- package/src/architecture.mjs +15 -10
- package/src/config.mjs +59 -0
- package/src/conventions.mjs +22 -5
- package/src/flutter.mjs +111 -0
- package/src/generate.mjs +3 -2
- package/src/new.mjs +207 -52
- package/src/owned-guidance.mjs +15 -0
- package/src/owned-sources.mjs +479 -0
- package/src/service-templates.mjs +222 -19
- package/src/sync.mjs +57 -0
- package/src/ui-templates.mjs +180 -1
- package/src/validate.mjs +44 -0
package/src/new.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { authAppStoreSource, GenerateError } from "./generate.mjs";
|
|
4
|
+
import { configSource } from "./config.mjs";
|
|
5
|
+
import { ownedSources } from "./owned-sources.mjs";
|
|
6
|
+
import { runSync } from "./sync.mjs";
|
|
4
7
|
import {
|
|
5
8
|
appErrorSource,
|
|
6
9
|
applicationRoutesSource,
|
|
@@ -16,6 +19,7 @@ import {
|
|
|
16
19
|
healthSchemasSource,
|
|
17
20
|
httpErrorsSource,
|
|
18
21
|
mongoReadinessProbeSource,
|
|
22
|
+
postgresReadinessProbeSource,
|
|
19
23
|
readinessProbeSource,
|
|
20
24
|
redisReadinessProbeSource,
|
|
21
25
|
redisSource,
|
|
@@ -42,13 +46,14 @@ function parseArgs(argv) {
|
|
|
42
46
|
const result = { db: "mongo", auth: "none", ui: false, redis: false, worker: false };
|
|
43
47
|
for (let i = 0; i < argv.length; i += 1) {
|
|
44
48
|
const arg = argv[i];
|
|
45
|
-
if (["--name", "--path", "--auth"].includes(arg)) result[arg.slice(2)] = argv[++i];
|
|
49
|
+
if (["--name", "--path", "--auth", "--db"].includes(arg)) result[arg.slice(2)] = argv[++i];
|
|
46
50
|
else if (["--ui", "--redis", "--worker"].includes(arg)) result[arg.slice(2)] = true;
|
|
47
51
|
else fail(`Unknown argument: ${arg}`);
|
|
48
52
|
}
|
|
49
53
|
if (!result.name || !/^[a-z][a-z0-9-]*$/.test(result.name)) fail("--name must use lowercase letters, digits, and hyphens and start with a letter");
|
|
50
54
|
if (!result.path) fail("--path is required");
|
|
51
55
|
if (!["none", "firebase"].includes(result.auth)) fail("--auth must be none or firebase");
|
|
56
|
+
if (!["mongo", "postgres"].includes(result.db)) fail("--db must be mongo or postgres");
|
|
52
57
|
if (result.worker) result.redis = true;
|
|
53
58
|
return result;
|
|
54
59
|
}
|
|
@@ -64,6 +69,7 @@ const title = options.name.split("-").map((part) => part[0].toUpperCase() + part
|
|
|
64
69
|
const svc = `${options.name}-svc`;
|
|
65
70
|
const ui = `${options.name}-ui`;
|
|
66
71
|
const databaseName = options.name.replaceAll("-", "_");
|
|
72
|
+
const postgres = options.db === "postgres";
|
|
67
73
|
const mongoApplicationUser = `${options.name}-app`;
|
|
68
74
|
const mongoBackupUser = `${options.name}-backup`;
|
|
69
75
|
const files = {};
|
|
@@ -71,6 +77,13 @@ const add = (relative, content) => { files[relative] = content.endsWith("\n") ?
|
|
|
71
77
|
// Generated repositories depend on the published toolkit rather than copying a validator, and they
|
|
72
78
|
// pin the exact contract version they were generated against.
|
|
73
79
|
const contractVersion = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
80
|
+
const serviceProfiles = [
|
|
81
|
+
...(options.auth === "firebase" ? ["firebase"] : []),
|
|
82
|
+
options.db,
|
|
83
|
+
...(options.redis ? ["redis", "rate-limit"] : []),
|
|
84
|
+
...(options.worker ? ["worker"] : []),
|
|
85
|
+
...(options.ui ? ["ui-host"] : [])
|
|
86
|
+
];
|
|
74
87
|
const prettierConfig = {
|
|
75
88
|
useTabs: true,
|
|
76
89
|
tabWidth: 4,
|
|
@@ -169,13 +182,13 @@ Use this workflow for any new domain capability or operation. Add only the layer
|
|
|
169
182
|
2. Read the root and every applicable child \`AGENTS.md\`.
|
|
170
183
|
3. Inspect the nearest working module and tests.
|
|
171
184
|
4. Create architecture files only with the tailframe CLI: \`tailframe generate\` creates service modules, use cases, http files, ports, adapters, identifiers, integrations, UI modules, and the closed UI shell catalog on canonical paths with canonical names. Service module generation also registers the new use case and mounts or extends its single route factory through deterministic composition-file edits. Hand-creating architecture files or editing generated shell-catalog implementations is a conformance violation; inspect the generated diff and complete only the remaining behavior-specific checklist.
|
|
172
|
-
5. Colocate product capability behavior under \`src/modules/<module>\` in both the service and UI. Never substitute \`features\` or global \`apis\`, \`views\`, \`services\`, \`stores\`, or \`types\` directories. UI \`app/stores\` is closed to generated \`auth.store.ts\` and \`theme.store.ts\`; \`app/public\` is closed to generated \`ThemeToggle.vue\`. Never create \`src/modules/theme\` or \`src/modules/
|
|
185
|
+
5. Colocate product capability behavior under \`src/modules/<module>\` in both the service and UI. Never substitute \`features\` or global \`apis\`, \`views\`, \`services\`, \`stores\`, or \`types\` directories. UI \`app/stores\` is closed to generated \`auth.store.ts\`, \`notification.store.ts\`, and \`theme.store.ts\`; \`app/public\` is closed to generated \`ThemeToggle.vue\`. Never create \`src/modules/theme\`, \`src/modules/auth\`, or \`src/modules/notification\`. Product records, current-user domain records, selections, filters, and workflows stay in their owning module. Start backend modules with \`use-cases/\`, \`http/\`, and tests; add \`domain/\` or \`persistence/\` only when required. Add only the UI module directories earned by the capability.
|
|
173
186
|
6. Define use cases as \`execute(context, input)\`. Keep trusted \`RequestContext\` separate from client input.
|
|
174
187
|
7. Put ownership, RBAC, entitlements, and domain policies in use-case or domain code, not client bodies or route metadata.
|
|
175
188
|
8. Define repository contracts in the owning module and implement them in persistence adapters.
|
|
176
189
|
9. For persisted capabilities, prefer branded, domain-owned string IDs at domain and repository-port boundaries; generate the module's identifier file with \`tailframe generate identifiers <module> <NameId...>\`. Keep MongoDB \`ObjectId\` conversion inside MongoDB persistence adapters, using persistence-only document types rather than \`any\` to bypass the boundary.
|
|
177
190
|
10. Let HTTP routes, jobs, workers, scheduled tasks, and CLIs invoke use cases directly; do not call one entry point from another.
|
|
178
|
-
11. Keep operation names, payloads, authentication, response contracts, and client types synchronized. A service module may import another only through a named file directly under the provider's use-cases directory. A UI module may import another only through a named file directly under the provider's public directory. Both graphs must remain acyclic. UI modules may import only the generated auth/theme app stores and \`ThemeToggle\` from app; every other app path is private.
|
|
191
|
+
11. Keep operation names, payloads, authentication, response contracts, and client types synchronized. A service module may import another only through a named file directly under the provider's use-cases directory. A UI module may import another only through a named file directly under the provider's public directory. Both graphs must remain acyclic. UI modules may import only the generated auth/notification/theme app stores and \`ThemeToggle\` from app; every other app path is private.
|
|
179
192
|
12. Verify generated container and route wiring, then add only dependency construction, client, navigation, worker, or process registration earned by real behavior. Route module views directly from \`app/router.ts\`. App views and shell components consume product modules only through named module \`public/\` entries; they never import module views or other internals.
|
|
180
193
|
13. Run npm run validate:architecture in every affected repository, then run focused type checks and tests. Report repositories, operations, entry points, checks, placement decisions, and gaps.
|
|
181
194
|
|
|
@@ -189,12 +202,13 @@ const svcDeps = {
|
|
|
189
202
|
joi: "^17.13.3",
|
|
190
203
|
"reflect-metadata": "^0.2.2",
|
|
191
204
|
tsyringe: "^4.9.1",
|
|
192
|
-
mongodb: "^6.17.0",
|
|
205
|
+
...(postgres ? { pg: "^8.16.3" } : { mongodb: "^6.17.0" }),
|
|
193
206
|
...(options.auth === "firebase" ? { "firebase-admin": "^13.0.0" } : {}),
|
|
194
|
-
...(options.redis ? { redis: "^5.10.0" } : {})
|
|
207
|
+
...(options.redis ? { "rate-limiter-flexible": "^11.2.0", redis: "^5.10.0" } : {})
|
|
195
208
|
};
|
|
196
209
|
const svcDevDeps = {
|
|
197
210
|
"@types/cors": "^2.8.17", "@types/express": "^5.0.1", "@types/jest": "^30.0.0", "@types/node": "^22.13.14", "@types/supertest": "^7.2.1",
|
|
211
|
+
...(postgres ? { "@types/pg": "^8.15.5" } : {}),
|
|
198
212
|
"@zaaxch/tailframe": contractVersion,
|
|
199
213
|
jest: "^30.0.0", nodemon: "^3.1.9", prettier: "3.5.3", "ts-jest": "^29.4.11", "ts-node": "^10.9.2",
|
|
200
214
|
"tsc-alias": "^1.8.16", "tsconfig-paths": "^4.2.0", typescript: "^5.8.2", supertest: "^7.2.2"
|
|
@@ -207,13 +221,15 @@ add(`${svc}/package.json`, JSON.stringify({
|
|
|
207
221
|
...(options.worker ? { "dev:worker": "nodemon --legacy-watch --exec ts-node -r tsconfig-paths/register src/worker.ts" } : {}),
|
|
208
222
|
build: "tsc && tsc-alias", start: "node dist/server.js",
|
|
209
223
|
...(options.worker ? { worker: "node dist/worker.js" } : {}),
|
|
210
|
-
test: "npm run test:unit && npm run test:integration", "test:unit": "jest --selectProjects unit --runInBand", "test:integration": "jest --selectProjects integration --runInBand", "validate:architecture": "tailframe validate --
|
|
224
|
+
test: "npm run test:unit && npm run test:integration", "test:unit": "jest --selectProjects unit --runInBand", "test:integration": "jest --selectProjects integration --runInBand", "validate:architecture": "tailframe validate .", "sync:architecture": "tailframe sync --check .", format: "prettier --write src/", "format:check": "prettier --check src/",
|
|
225
|
+
"schema:apply": "node dist/app/cli/applySchema.js", "schema:apply:dev": "ts-node -r tsconfig-paths/register src/app/cli/applySchema.ts",
|
|
211
226
|
"test:db:up": "docker compose -f docker-compose.test.yml up -d", "test:db:down": "docker compose -f docker-compose.test.yml down -v",
|
|
212
227
|
"docker:dev": "docker compose -f docker-compose.dev.yml up",
|
|
213
228
|
"docker:prod": "docker compose -f docker-compose.yml up -d",
|
|
214
229
|
"docker:push": "bash scripts/build_and_push.sh"
|
|
215
230
|
}, dependencies: svcDeps, devDependencies: svcDevDeps
|
|
216
231
|
}, null, "\t"));
|
|
232
|
+
add(`${svc}/tailframe.json`, configSource({ kind: "service", profiles: serviceProfiles, contractVersion }));
|
|
217
233
|
add(`${svc}/tsconfig.json`, JSON.stringify({
|
|
218
234
|
compilerOptions: { target: "ES2022", module: "commonjs", rootDir: "src", outDir: "dist", strict: true, esModuleInterop: true, experimentalDecorators: true, emitDecoratorMetadata: true, baseUrl: ".", paths: { "@/*": ["src/*"] }, skipLibCheck: true },
|
|
219
235
|
include: ["src/**/*.ts"]
|
|
@@ -228,17 +244,23 @@ add(`${svc}/.gitattributes`, gitAttributes);
|
|
|
228
244
|
add(`${svc}/.gitignore`, `node_modules/\ndist/\n.env*\n!.env.example\n!.env.infrastructure.example\n*.log\nfirebase-service-account*.json\n`);
|
|
229
245
|
add(`${svc}/.dockerignore`, `node_modules\ndist\n.git\n.env*\n!.env.example\n*.log\ndata\nfirebase-service-account*.json\n`);
|
|
230
246
|
if (options.ui) add(".dockerignore", `**/node_modules\n**/dist\n**/.git\n**/.env*\n**/*.log\n**/data\n**/firebase-service-account*.json\n`);
|
|
231
|
-
const dbEnv =
|
|
247
|
+
const dbEnv = postgres
|
|
248
|
+
? `POSTGRES_URI=postgresql://${options.name}-app:development@localhost:5432/${databaseName}`
|
|
249
|
+
: "MONGODB_URI=mongodb://localhost:27017/?replicaSet=rs0&directConnection=true\nMONGODB_DB_NAME=" + databaseName;
|
|
232
250
|
add(`${svc}/.env.example`, `NODE_ENV=development\nPORT=3000\nCORS_ORIGIN=https://localhost:5173\n${dbEnv}\n${options.auth === "firebase" ? "FIREBASE_PROJECT_ID=\n" : ""}${options.redis ? "REDIS_URL=redis://localhost:6379\n" : ""}`);
|
|
233
251
|
add(`${svc}/.env.infrastructure.example`, `ECR_IMAGE=<account>.dkr.ecr.<region>.amazonaws.com/${options.name}
|
|
234
252
|
IMAGE_TAG=<service-sha>${options.ui ? "_<ui-sha>" : ""}
|
|
235
253
|
APP_ENV_FILE=/opt/${options.name}/shared/.env.production
|
|
236
|
-
|
|
254
|
+
${postgres ? `POSTGRES_DATA_DIR=/opt/${options.name}/shared/postgres/data
|
|
255
|
+
POSTGRES_ROOT_PASSWORD=<uri-safe-random-value>
|
|
256
|
+
POSTGRES_APP_PASSWORD=<different-uri-safe-random-value>
|
|
257
|
+
` : `MONGO_DATA_DIR=/opt/${options.name}/shared/mongo/data
|
|
237
258
|
MONGO_KEYFILE=/opt/${options.name}/shared/mongo/config/keyfile
|
|
238
259
|
MONGO_INIT_SCRIPT=/opt/${options.name}/current/deploy/mongo/10-create-users.js
|
|
239
260
|
MONGODB_ROOT_PASSWORD=<uri-safe-random-value>
|
|
240
261
|
MONGODB_APP_PASSWORD=<different-uri-safe-random-value>
|
|
241
262
|
MONGODB_BACKUP_PASSWORD=<different-uri-safe-random-value>
|
|
263
|
+
`}
|
|
242
264
|
${options.auth === "firebase" ? `SECRETS_DIR=/opt/${options.name}/shared/secrets
|
|
243
265
|
` : ""}${options.redis ? `REDIS_DATA_DIR=/opt/${options.name}/shared/redis/data
|
|
244
266
|
REDIS_PASSWORD=<different-uri-safe-random-value>
|
|
@@ -248,12 +270,25 @@ export const env = {
|
|
|
248
270
|
nodeEnv: process.env.NODE_ENV ?? "development",
|
|
249
271
|
port: Number(process.env.PORT ?? 3000),
|
|
250
272
|
corsOrigin: process.env.CORS_ORIGIN ?? "https://localhost:5173",
|
|
251
|
-
${options.auth === "firebase" ? `firebaseProjectId: process.env.FIREBASE_PROJECT_ID ?? ""
|
|
252
|
-
|
|
253
|
-
|
|
273
|
+
${options.auth === "firebase" ? `firebaseProjectId: process.env.FIREBASE_PROJECT_ID ?? "",
|
|
274
|
+
firebaseServiceAccountPath: process.env.FIREBASE_SERVICE_ACCOUNT_PATH,` : ""}
|
|
275
|
+
${postgres ? `postgresUri: process.env.POSTGRES_URI ?? "postgresql://${options.name}-app:development@localhost:5432/${databaseName}",` : `mongodbUri: process.env.MONGODB_URI ?? "mongodb://localhost:27017/?replicaSet=rs0&directConnection=true",
|
|
276
|
+
mongodbDbName: process.env.MONGODB_DB_NAME ?? "${databaseName}",`}
|
|
254
277
|
${options.redis ? `redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379",` : ""}
|
|
255
278
|
};`);
|
|
256
|
-
const databaseSource = `import {
|
|
279
|
+
const databaseSource = postgres ? `import { Pool } from "pg";
|
|
280
|
+
import { env } from "@/platform/config/env";
|
|
281
|
+
|
|
282
|
+
export const postgres = new Pool({ connectionString: env.postgresUri });
|
|
283
|
+
|
|
284
|
+
export async function connectDatabase() {
|
|
285
|
+
await postgres.query("SELECT 1");
|
|
286
|
+
return postgres;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function closeDatabase() {
|
|
290
|
+
await postgres.end();
|
|
291
|
+
}` : `import { MongoClient } from "mongodb";
|
|
257
292
|
import { env } from "@/platform/config/env";
|
|
258
293
|
|
|
259
294
|
export const mongo = new MongoClient(env.mongodbUri);
|
|
@@ -267,6 +302,15 @@ export async function closeDatabase() {
|
|
|
267
302
|
await mongo.close();
|
|
268
303
|
}`;
|
|
269
304
|
add(`${svc}/src/platform/database/index.ts`, databaseSource);
|
|
305
|
+
add(`${svc}/src/app/schema.ts`, `import type { SchemaLifecycle } from "@/core/SchemaLifecycle";
|
|
306
|
+
import { closeDatabase, connectDatabase } from "@/platform/database";
|
|
307
|
+
|
|
308
|
+
export const schemaLifecycle: SchemaLifecycle = {
|
|
309
|
+
async apply() {
|
|
310
|
+
await connectDatabase();
|
|
311
|
+
},
|
|
312
|
+
close: closeDatabase
|
|
313
|
+
};`);
|
|
270
314
|
if (options.redis) add(`${svc}/src/platform/redis/index.ts`, redisSource);
|
|
271
315
|
add(`${svc}/src/core/RequestContext.ts`, requestContextSource);
|
|
272
316
|
add(`${svc}/src/core/UseCase.ts`, useCaseSource);
|
|
@@ -283,9 +327,10 @@ add(`${svc}/src/modules/health/use-cases/GetReadiness.ts`, getReadinessSource);
|
|
|
283
327
|
add(`${svc}/src/modules/health/use-cases/ports/ReadinessProbe.ts`, readinessProbeSource);
|
|
284
328
|
add(`${svc}/src/modules/health/http/health.schemas.ts`, healthSchemasSource);
|
|
285
329
|
add(`${svc}/src/modules/health/http/health.routes.ts`, healthRoutesSource);
|
|
286
|
-
add(`${svc}/src/platform/integrations/
|
|
330
|
+
if (postgres) add(`${svc}/src/platform/integrations/postgres/PostgresReadinessProbe.ts`, postgresReadinessProbeSource);
|
|
331
|
+
else add(`${svc}/src/platform/integrations/mongodb/MongoReadinessProbe.ts`, mongoReadinessProbeSource);
|
|
287
332
|
if (options.redis) add(`${svc}/src/platform/integrations/redis/RedisReadinessProbe.ts`, redisReadinessProbeSource);
|
|
288
|
-
add(`${svc}/src/app/container.ts`, containerSource({ redis: options.redis }));
|
|
333
|
+
add(`${svc}/src/app/container.ts`, containerSource({ database: options.db, redis: options.redis }));
|
|
289
334
|
add(`${svc}/src/app/routes.ts`, applicationRoutesSource);
|
|
290
335
|
add(`${svc}/src/app/server.ts`, serverSource({ ui: options.ui, redis: options.redis, csrf }));
|
|
291
336
|
add(`${svc}/src/server.ts`, `import "reflect-metadata";\nimport { startServer } from "@/app/server";\nvoid startServer();\n`);
|
|
@@ -293,13 +338,34 @@ if (options.worker) add(`${svc}/src/app/workers/startWorker.ts`, workerSource())
|
|
|
293
338
|
if (options.worker) add(`${svc}/src/worker.ts`, `import "reflect-metadata";\nimport { startWorker } from "@/app/workers/startWorker";\nvoid startWorker();\n`);
|
|
294
339
|
add(`${svc}/src/modules/health/__tests__/GetHealth.test.ts`, getHealthTestSource);
|
|
295
340
|
add(`${svc}/src/modules/health/__tests__/GetReadiness.test.ts`, getReadinessTestSource);
|
|
296
|
-
add(`${svc}/src/__tests__/testDb.ts`, `import {
|
|
341
|
+
add(`${svc}/src/__tests__/testDb.ts`, postgres ? `import { Pool } from "pg";
|
|
342
|
+
const pool = new Pool({ connectionString: process.env.TEST_POSTGRES_URI ?? "postgresql://postgres:postgres@localhost:5433/${databaseName}_test" });
|
|
343
|
+
export async function openTestDatabase() { await pool.query("SELECT 1"); return pool; }
|
|
344
|
+
export async function resetTestDatabase() { await pool.query("DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public"); return pool; }
|
|
345
|
+
export async function closeTestDatabase() { await pool.end(); }
|
|
346
|
+
` : `import { MongoClient } from "mongodb";
|
|
297
347
|
const client = new MongoClient(process.env.TEST_MONGODB_URI ?? "mongodb://localhost:27018/?replicaSet=rs0&directConnection=true");
|
|
298
348
|
export async function openTestDatabase() { await client.connect(); return client.db(process.env.TEST_MONGODB_DB_NAME ?? "${options.name.replaceAll("-", "_")}_test"); }
|
|
299
349
|
export async function resetTestDatabase() { const database = await openTestDatabase(); await database.dropDatabase(); return database; }
|
|
300
350
|
export async function closeTestDatabase() { await client.close(); }
|
|
301
351
|
`);
|
|
302
|
-
add(`${svc}/docker-compose.test.yml`, `name: ${options.name}-test
|
|
352
|
+
add(`${svc}/docker-compose.test.yml`, postgres ? `name: ${options.name}-test
|
|
353
|
+
services:
|
|
354
|
+
postgres:
|
|
355
|
+
image: postgres:17
|
|
356
|
+
ports:
|
|
357
|
+
- "5433:5432"
|
|
358
|
+
environment:
|
|
359
|
+
POSTGRES_DB: ${databaseName}_test
|
|
360
|
+
POSTGRES_PASSWORD: postgres
|
|
361
|
+
tmpfs:
|
|
362
|
+
- /var/lib/postgresql/data
|
|
363
|
+
healthcheck:
|
|
364
|
+
test: ["CMD-SHELL", "pg_isready -U postgres -d ${databaseName}_test"]
|
|
365
|
+
interval: 2s
|
|
366
|
+
timeout: 2s
|
|
367
|
+
retries: 30
|
|
368
|
+
` : `name: ${options.name}-test
|
|
303
369
|
services:
|
|
304
370
|
mongo:
|
|
305
371
|
image: mongo:8
|
|
@@ -476,7 +542,12 @@ Run \`npm run validate:architecture\` and \`npm run format:check\` after changin
|
|
|
476
542
|
if (options.ui) {
|
|
477
543
|
const uiDeps = { "@tailwindcss/vite": "^4.1.18", "@vueuse/core": "^14.3.0", axios: "^1.9.0", pinia: "^3.0.1", primevue: "^4.2.5", vue: "^3.5.13", "vue-router": "^4.5.0", ...(options.auth === "firebase" ? { firebase: "^11.0.0" } : {}) };
|
|
478
544
|
const uiDevDeps = { "@tsconfig/node22": "^22.0.1", "@zaaxch/tailframe": contractVersion, "@types/node": "^22.13.14", "@vitejs/plugin-vue": "^5.2.3", "@vue/test-utils": "^2.4.10", "@vue/tsconfig": "^0.7.0", jsdom: "^29.1.1", "npm-run-all2": "^7.0.2", prettier: "3.5.3", typescript: "~5.8.0", vite: "^6.2.4", vitest: "^4.1.7", "vue-tsc": "^2.2.8" };
|
|
479
|
-
add(`${ui}/package.json`, JSON.stringify({ name: ui, version: "0.1.0", private: true, type: "module", scripts: { dev: "vite --host 0.0.0.0", build: "run-p type-check \"build-only {@}\" --", "build-only": "vite build", "type-check": "vue-tsc --build", test: "vitest run", "test:watch": "vitest", "validate:architecture": "tailframe validate --
|
|
545
|
+
add(`${ui}/package.json`, JSON.stringify({ name: ui, version: "0.1.0", private: true, type: "module", scripts: { dev: "vite --host 0.0.0.0", build: "run-p type-check \"build-only {@}\" --", "build-only": "vite build", "type-check": "vue-tsc --build", test: "vitest run", "test:watch": "vitest", "validate:architecture": "tailframe validate .", "sync:architecture": "tailframe sync --check .", format: "prettier --write src/", "format:check": "prettier --check src/" }, dependencies: uiDeps, devDependencies: uiDevDeps }, null, "\t"));
|
|
546
|
+
add(`${ui}/tailframe.json`, configSource({
|
|
547
|
+
kind: "ui",
|
|
548
|
+
profiles: [...(options.auth === "firebase" ? ["firebase"] : []), "notifications"],
|
|
549
|
+
contractVersion
|
|
550
|
+
}));
|
|
480
551
|
add(`${ui}/.prettierrc.json`, JSON.stringify(prettierConfig, null, "\t"));
|
|
481
552
|
add(`${ui}/.gitattributes`, gitAttributes);
|
|
482
553
|
add(`${ui}/tsconfig.json`, JSON.stringify({ files: [], references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }] }, null, "\t"));
|
|
@@ -515,6 +586,7 @@ export default createRouter({
|
|
|
515
586
|
});`);
|
|
516
587
|
add(`${ui}/src/platform/http.ts`, uiHttpSource);
|
|
517
588
|
if (options.auth === "firebase") add(`${ui}/src/app/configureHttp.ts`, configureHttpSource);
|
|
589
|
+
add(`${ui}/src/platform/config.ts`, `export const apiBaseUrl = \`\${window.location.origin}/api/v1\`;`);
|
|
518
590
|
add(`${ui}/src/modules/health/api/health.api.ts`, healthApiSource);
|
|
519
591
|
add(`${ui}/src/modules/health/views/HealthView.vue`, `<script setup lang="ts">
|
|
520
592
|
import { ref } from "vue";
|
|
@@ -556,23 +628,31 @@ async function checkReadiness() {
|
|
|
556
628
|
<p v-if="error" role="alert">{{ error }}</p>
|
|
557
629
|
</main>
|
|
558
630
|
</template>`);
|
|
559
|
-
add(`${ui}/src/app/App.vue`, `<template
|
|
631
|
+
add(`${ui}/src/app/App.vue`, `<template>
|
|
632
|
+
<RouterView />
|
|
633
|
+
<NotificationHost />
|
|
634
|
+
</template>
|
|
635
|
+
|
|
636
|
+
<script setup lang="ts">
|
|
637
|
+
import { RouterView } from "vue-router";
|
|
638
|
+
import NotificationHost from "@/app/components/NotificationHost.vue";
|
|
639
|
+
</script>`);
|
|
560
640
|
add(`${ui}/src/app/startApplication.ts`, `import { createApp } from "vue";\nimport { createPinia } from "pinia";\nimport PrimeVue from "primevue/config";\nimport App from "@/app/App.vue";\n${options.auth === "firebase" ? 'import { configureHttp } from "@/app/configureHttp";\n' : ""}import router from "@/app/router";\nimport "@/app/styles.css";\nexport function startApplication() {\n\tconst app = createApp(App);\n\tconst pinia = createPinia();\n\tapp.use(pinia).use(PrimeVue).use(router);\n\t${options.auth === "firebase" ? "configureHttp(pinia);\n\t" : ""}app.mount("#app");\n}\n`);
|
|
561
641
|
add(`${ui}/src/main.ts`, `import { startApplication } from "@/app/startApplication";\nstartApplication();\n`);
|
|
562
642
|
add(`${ui}/src/app/styles.css`, `@import "tailwindcss";\n:root { font-family: Inter, ui-sans-serif, system-ui; color: #17221c; background: #f5f3ec; }\nbody { margin: 0; }`);
|
|
563
|
-
add(`${ui}/src/app/__tests__/App.spec.ts`, `import { mount } from "@vue/test-utils";\nimport App from "@/app/App.vue";\ndescribe("App", () => { it("renders a router view", () => { expect(mount(App, { global: { stubs: ["RouterView"] } }).exists()).toBe(true); }); });`);
|
|
643
|
+
add(`${ui}/src/app/__tests__/App.spec.ts`, `import { mount } from "@vue/test-utils";\nimport App from "@/app/App.vue";\ndescribe("App", () => { it("renders a router view", () => { expect(mount(App, { global: { stubs: ["RouterView", "NotificationHost"] } }).exists()).toBe(true); }); });`);
|
|
564
644
|
add(`${ui}/AGENTS.md`, `# ${title} UI guidance
|
|
565
645
|
|
|
566
646
|
## Scope
|
|
567
647
|
These conventions apply to \`${ui}/src\`. Inspect the nearest working module before adding files.
|
|
568
648
|
|
|
569
649
|
## Architecture vocabulary
|
|
570
|
-
- Use \`src/app\` for Vue bootstrap, the application shell, routing, global styles, and the closed Tailframe auth/theme catalog.
|
|
650
|
+
- Use \`src/app\` for Vue bootstrap, the application shell, routing, global styles, and the closed Tailframe auth/notification/theme catalog.
|
|
571
651
|
- Use \`src/core\` only for small technology-neutral client contracts.
|
|
572
652
|
- Use \`src/platform\` for Axios, Firebase, browser APIs, and other technical adapters.
|
|
573
653
|
- Use \`src/modules/<module>\` for every product capability. Never substitute \`src/features\` or global \`apis\`, \`views\`, \`services\`, \`stores\`, or \`types\` directories.
|
|
574
|
-
- These are the only UI architecture roots. Keep \`src/core\` flat. Shared capability code belongs to the module that owns it. \`src/app/stores\` is closed to generated \`auth.store.ts\` and \`theme.store.ts\`; \`src/app/public\` is closed to generated \`ThemeToggle.vue\`. Do not edit their implementations; only the generated theme storage key varies. Internal shell components remain private under \`src/app/components\`.
|
|
575
|
-
- Never create \`src/modules/theme\` or \`src/modules/
|
|
654
|
+
- These are the only UI architecture roots. Keep \`src/core\` flat. Shared capability code belongs to the module that owns it. \`src/app/stores\` is closed to generated \`auth.store.ts\`, \`notification.store.ts\`, and \`theme.store.ts\`; \`src/app/public\` is closed to generated \`ThemeToggle.vue\`. Do not edit their implementations; only the generated theme storage key varies. Internal shell components remain private under \`src/app/components\`.
|
|
655
|
+
- Never create \`src/modules/theme\`, \`src/modules/auth\`, or \`src/modules/notification\`. Product records, current-user domain records, selections, filters, workflows, and module API calls always belong to the owning product module.
|
|
576
656
|
|
|
577
657
|
## Change placement
|
|
578
658
|
| Change | Canonical owner |
|
|
@@ -580,14 +660,14 @@ These conventions apply to \`${ui}/src\`. Inspect the nearest working module bef
|
|
|
580
660
|
| Capability API, component, composable, state, type, or view | \`src/modules/<module>\` |
|
|
581
661
|
| Capability intentionally consumed by another module | Named file directly under provider \`src/modules/<module>/public\` |
|
|
582
662
|
| Axios, Firebase, browser, or vendor mechanism | \`src/platform\` |
|
|
583
|
-
| Firebase identity/readiness or theme preference | Generated \`src/app/stores/auth.store.ts\` or \`theme.store.ts\` |
|
|
663
|
+
| Firebase identity/readiness, notification queue, or theme preference | Generated \`src/app/stores/auth.store.ts\`, \`notification.store.ts\`, or \`theme.store.ts\` |
|
|
584
664
|
| Canonical theme control consumed by modules | Generated \`src/app/public/ThemeToggle.vue\` |
|
|
585
665
|
| Any other state or reusable presentation | Owning \`src/modules/<module>\` |
|
|
586
666
|
| Shell, router, global styles, or cross-capability composition | \`src/app\` |
|
|
587
667
|
| Technology-neutral client contract | Flat \`src/core\` |
|
|
588
668
|
| Root executable | Bootstrap of \`src/app\` only |
|
|
589
669
|
|
|
590
|
-
UI modules may import a sibling only through a named file directly under the provider's \`public/\` directory; keep the graph acyclic. From app, modules may import only generated \`auth.store.ts\`, \`theme.store.ts\`, and \`ThemeToggle.vue\`; every other app path is private. App views and shell components consume modules only through named module \`public/\` entries; route module views directly from \`app/router.ts\`. Use \`@/\` or relative internal imports; do not add another source alias. Platform imports neither app nor modules.
|
|
670
|
+
UI modules may import a sibling only through a named file directly under the provider's \`public/\` directory; keep the graph acyclic. From app, modules may import only generated \`auth.store.ts\`, \`notification.store.ts\`, \`theme.store.ts\`, and \`ThemeToggle.vue\`; every other app path is private. App views and shell components consume modules only through named module \`public/\` entries; route module views directly from \`app/router.ts\`. Use \`@/\` or relative internal imports; do not add another source alias. Platform imports neither app nor modules.
|
|
591
671
|
|
|
592
672
|
## Module shape
|
|
593
673
|
Under \`src/modules/<module>\`, add only needed \`api/\`, \`components/\`, \`composables/\`, \`public/\`, \`routes/\`, \`stores/\`, \`types/\`, \`views/\`, and \`__tests__/\` directories. Public entries are named direct files, never nested directories or barrels. Do not invent alternative layer names or generate speculative CRUD. Create module and public shell files only with \`tailframe generate\`.
|
|
@@ -602,7 +682,7 @@ Use named routes.${options.auth === "firebase" ? " Preserve Firebase authenticat
|
|
|
602
682
|
|
|
603
683
|
## State management
|
|
604
684
|
Keep local state local. Use Pinia only for state shared across routes or unrelated components.
|
|
605
|
-
Keep all product state in the owning module. The auth/theme stores and ThemeToggle are Tailframe-owned generated sources shared byte-for-byte across projects apart from the explicit theme storage key.
|
|
685
|
+
Keep all product state in the owning module. The auth/notification/theme stores, NotificationHost, and ThemeToggle are Tailframe-owned generated sources shared byte-for-byte across projects apart from the explicit theme storage key.
|
|
606
686
|
|
|
607
687
|
## UI behavior
|
|
608
688
|
Reuse Vue, PrimeVue, Tailwind, loading, error, and accessibility conventions. Provide explicit loading, empty, error, and authorization states.
|
|
@@ -852,24 +932,19 @@ docker compose --env-file "\${INFRA_ENV}" --file "\${COMPOSE_FILE}" pull
|
|
|
852
932
|
docker compose --env-file "\${INFRA_ENV}" --file "\${COMPOSE_FILE}" up -d --wait
|
|
853
933
|
`);
|
|
854
934
|
|
|
855
|
-
add(`${svc}/deploy/
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
if (!applicationPassword || !backupPassword) {
|
|
859
|
-
throw new Error("MONGODB_APP_PASSWORD and MONGODB_BACKUP_PASSWORD are required");
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
db.getSiblingDB("${databaseName}").createUser({
|
|
863
|
-
user: "${mongoApplicationUser}",
|
|
864
|
-
pwd: applicationPassword,
|
|
865
|
-
roles: [{ role: "readWrite", db: "${databaseName}" }]
|
|
866
|
-
});
|
|
935
|
+
if (postgres) add(`${svc}/deploy/postgres/10-create-application-user.sh`, `#!/usr/bin/env bash
|
|
936
|
+
set -euo pipefail
|
|
867
937
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
}
|
|
938
|
+
psql --set ON_ERROR_STOP=1 --username postgres --dbname "${databaseName}" --set app_password="\${POSTGRES_APP_PASSWORD}" <<'SQL'
|
|
939
|
+
SELECT 'CREATE ROLE "${options.name}-app" LOGIN PASSWORD ' || quote_literal(:'app_password')
|
|
940
|
+
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${options.name}-app')\gexec
|
|
941
|
+
GRANT CONNECT ON DATABASE "${databaseName}" TO "${options.name}-app";
|
|
942
|
+
GRANT USAGE ON SCHEMA public TO "${options.name}-app";
|
|
943
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "${options.name}-app";
|
|
944
|
+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO "${options.name}-app";
|
|
945
|
+
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "${options.name}-app";
|
|
946
|
+
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO "${options.name}-app";
|
|
947
|
+
SQL
|
|
873
948
|
`);
|
|
874
949
|
|
|
875
950
|
add(`${svc}/docs/production-deployment.md`, `# Production deployment
|
|
@@ -964,24 +1039,30 @@ const devVolumes = [
|
|
|
964
1039
|
...(options.auth === "firebase" ? [" - ./firebase-service-account.development.json:/app/firebase-service-account.development.json:ro"] : [])
|
|
965
1040
|
];
|
|
966
1041
|
const productionDependencies = [
|
|
967
|
-
["
|
|
1042
|
+
["schema-init", "service_completed_successfully"],
|
|
968
1043
|
...(options.redis ? [["redis", "service_healthy"]] : [])
|
|
969
1044
|
];
|
|
970
1045
|
const developmentDependencies = [
|
|
971
|
-
["
|
|
1046
|
+
["schema-init", "service_completed_successfully"],
|
|
972
1047
|
...(options.redis ? [["redis", "service_started"]] : [])
|
|
973
1048
|
];
|
|
974
1049
|
const dependsBlock = `\n depends_on:\n${productionDependencies.map(([dependency, condition]) => ` ${dependency}:\n condition: ${condition}`).join("\n")}`;
|
|
975
1050
|
const devDependsBlock = `\n depends_on:\n${developmentDependencies.map(([dependency, condition]) => ` ${dependency}:\n condition: ${condition}`).join("\n")}`;
|
|
976
1051
|
const productionEnvironment = [
|
|
977
1052
|
" NODE_ENV: production",
|
|
978
|
-
|
|
979
|
-
|
|
1053
|
+
...(postgres
|
|
1054
|
+
? [` POSTGRES_URI: "postgresql://${options.name}-app:\${POSTGRES_APP_PASSWORD:?Set POSTGRES_APP_PASSWORD}@postgres:5432/${databaseName}"`]
|
|
1055
|
+
: [
|
|
1056
|
+
` MONGODB_URI: "mongodb://${mongoApplicationUser}:\${MONGODB_APP_PASSWORD:?Set MONGODB_APP_PASSWORD}@mongodb:27017/${databaseName}?replicaSet=rs0&directConnection=true&authSource=${databaseName}"`,
|
|
1057
|
+
` MONGODB_DB_NAME: "${databaseName}"`
|
|
1058
|
+
]),
|
|
980
1059
|
...(options.auth === "firebase" ? [" GOOGLE_APPLICATION_CREDENTIALS: /app/secrets/firebase-service-account.json"] : []),
|
|
981
1060
|
...(options.redis ? [" REDIS_URL: \"redis://:\${REDIS_PASSWORD:?Set REDIS_PASSWORD}@redis:6379\""] : [])
|
|
982
1061
|
];
|
|
983
1062
|
const productionEnvironmentBlock = `\n environment:\n${productionEnvironment.join("\n")}`;
|
|
984
|
-
const databaseEnvironmentBlock =
|
|
1063
|
+
const databaseEnvironmentBlock = postgres
|
|
1064
|
+
? `\n environment:\n POSTGRES_URI: "postgresql://${options.name}-app:development@postgres:5432/${databaseName}"`
|
|
1065
|
+
: `\n environment:\n MONGODB_URI: "mongodb://mongodb:27017/?replicaSet=rs0&directConnection=true"`;
|
|
985
1066
|
const volumeBlock = serviceVolumes.length ? `\n volumes:\n${serviceVolumes.join("\n")}` : "";
|
|
986
1067
|
const workerService = options.worker ? `\n worker:
|
|
987
1068
|
image: \${ECR_IMAGE:?Set ECR_IMAGE}:\${IMAGE_TAG:?Set IMAGE_TAG}
|
|
@@ -998,7 +1079,25 @@ const workerService = options.worker ? `\n worker:
|
|
|
998
1079
|
networks:
|
|
999
1080
|
- backend
|
|
1000
1081
|
restart: unless-stopped` : "";
|
|
1001
|
-
const databaseService = `\n
|
|
1082
|
+
const databaseService = postgres ? `\n postgres:
|
|
1083
|
+
image: postgres:17
|
|
1084
|
+
environment:
|
|
1085
|
+
POSTGRES_DB: "${databaseName}"
|
|
1086
|
+
POSTGRES_USER: postgres
|
|
1087
|
+
POSTGRES_PASSWORD: \${POSTGRES_ROOT_PASSWORD:?Set POSTGRES_ROOT_PASSWORD}
|
|
1088
|
+
POSTGRES_APP_PASSWORD: \${POSTGRES_APP_PASSWORD:?Set POSTGRES_APP_PASSWORD}
|
|
1089
|
+
volumes:
|
|
1090
|
+
- \${POSTGRES_DATA_DIR:?Set POSTGRES_DATA_DIR}:/var/lib/postgresql/data
|
|
1091
|
+
- ./deploy/postgres/10-create-application-user.sh:/docker-entrypoint-initdb.d/10-create-application-user.sh:ro
|
|
1092
|
+
healthcheck:
|
|
1093
|
+
test: ["CMD-SHELL", "pg_isready -U postgres -d ${databaseName}"]
|
|
1094
|
+
interval: 5s
|
|
1095
|
+
timeout: 5s
|
|
1096
|
+
retries: 30
|
|
1097
|
+
start_period: 20s
|
|
1098
|
+
networks:
|
|
1099
|
+
- backend
|
|
1100
|
+
restart: unless-stopped` : `\n mongodb:
|
|
1002
1101
|
image: mongo:8
|
|
1003
1102
|
command:
|
|
1004
1103
|
- mongod
|
|
@@ -1011,8 +1110,12 @@ const databaseService = `\n mongodb:
|
|
|
1011
1110
|
MONGO_INITDB_DATABASE: "${databaseName}"
|
|
1012
1111
|
MONGO_INITDB_ROOT_USERNAME: "${options.name}-root"
|
|
1013
1112
|
MONGO_INITDB_ROOT_PASSWORD: \${MONGODB_ROOT_PASSWORD:?Set MONGODB_ROOT_PASSWORD}
|
|
1113
|
+
MONGODB_DB_NAME: "${databaseName}"
|
|
1114
|
+
MONGODB_APP_USERNAME: "${mongoApplicationUser}"
|
|
1014
1115
|
MONGODB_APP_PASSWORD: \${MONGODB_APP_PASSWORD:?Set MONGODB_APP_PASSWORD}
|
|
1116
|
+
MONGODB_BACKUP_USERNAME: "${mongoBackupUser}"
|
|
1015
1117
|
MONGODB_BACKUP_PASSWORD: \${MONGODB_BACKUP_PASSWORD:?Set MONGODB_BACKUP_PASSWORD}
|
|
1118
|
+
MONGODB_APP_COLLECTIONS: "_tailframe_reserved"
|
|
1016
1119
|
volumes:
|
|
1017
1120
|
- \${MONGO_DATA_DIR:?Set MONGO_DATA_DIR}:/data/db
|
|
1018
1121
|
- \${MONGO_KEYFILE:?Set MONGO_KEYFILE}:/etc/mongo-keyfile/keyfile:ro
|
|
@@ -1059,6 +1162,17 @@ const databaseService = `\n mongodb:
|
|
|
1059
1162
|
}"
|
|
1060
1163
|
networks:
|
|
1061
1164
|
- backend`;
|
|
1165
|
+
const schemaInitDependencies = postgres ? [["postgres", "service_healthy"]] : [["mongo-init", "service_completed_successfully"]];
|
|
1166
|
+
const schemaInitDependsBlock = `\n depends_on:\n${schemaInitDependencies.map(([dependency, condition]) => ` ${dependency}:\n condition: ${condition}`).join("\n")}`;
|
|
1167
|
+
const schemaInitEnvironment = postgres
|
|
1168
|
+
? `\n environment:\n POSTGRES_URI: "postgresql://postgres:\${POSTGRES_ROOT_PASSWORD:?Set POSTGRES_ROOT_PASSWORD}@postgres:5432/${databaseName}"`
|
|
1169
|
+
: `\n environment:\n MONGODB_URI: "mongodb://${options.name}-root:\${MONGODB_ROOT_PASSWORD:?Set MONGODB_ROOT_PASSWORD}@mongodb:27017/${databaseName}?replicaSet=rs0&directConnection=true&authSource=admin"\n MONGODB_DB_NAME: "${databaseName}"`;
|
|
1170
|
+
const schemaInitService = `\n schema-init:
|
|
1171
|
+
image: \${ECR_IMAGE:?Set ECR_IMAGE}:\${IMAGE_TAG:?Set IMAGE_TAG}
|
|
1172
|
+
command: ["node", "dist/app/cli/applySchema.js"]${schemaInitEnvironment}${schemaInitDependsBlock}
|
|
1173
|
+
networks:
|
|
1174
|
+
- backend
|
|
1175
|
+
restart: "no"`;
|
|
1062
1176
|
const redisService = options.redis ? `\n redis:
|
|
1063
1177
|
image: redis:7-alpine
|
|
1064
1178
|
command:
|
|
@@ -1109,7 +1223,7 @@ services:
|
|
|
1109
1223
|
stop_grace_period: 2m
|
|
1110
1224
|
networks:
|
|
1111
1225
|
- backend
|
|
1112
|
-
restart: unless-stopped${workerService}${databaseService}${redisService}
|
|
1226
|
+
restart: unless-stopped${workerService}${schemaInitService}${databaseService}${redisService}
|
|
1113
1227
|
networks:
|
|
1114
1228
|
backend:
|
|
1115
1229
|
driver: bridge
|
|
@@ -1124,7 +1238,21 @@ const devWorker = options.worker ? `\n worker:
|
|
|
1124
1238
|
command: ["npm", "run", "dev:worker"]${databaseEnvironmentBlock}
|
|
1125
1239
|
volumes:
|
|
1126
1240
|
${devVolumes.join("\n")}${devDependsBlock}` : "";
|
|
1127
|
-
const devDatabase = `\n
|
|
1241
|
+
const devDatabase = postgres ? `\n postgres:
|
|
1242
|
+
image: postgres:17
|
|
1243
|
+
ports:
|
|
1244
|
+
- "5432:5432"
|
|
1245
|
+
environment:
|
|
1246
|
+
POSTGRES_DB: ${databaseName}
|
|
1247
|
+
POSTGRES_USER: ${options.name}-app
|
|
1248
|
+
POSTGRES_PASSWORD: development
|
|
1249
|
+
volumes:
|
|
1250
|
+
- postgres-dev-data:/var/lib/postgresql/data
|
|
1251
|
+
healthcheck:
|
|
1252
|
+
test: ["CMD-SHELL", "pg_isready -U ${options.name}-app -d ${databaseName}"]
|
|
1253
|
+
interval: 2s
|
|
1254
|
+
timeout: 2s
|
|
1255
|
+
retries: 30` : `\n mongodb:
|
|
1128
1256
|
image: mongo:8
|
|
1129
1257
|
command: mongod --replSet rs0 --bind_ip_all
|
|
1130
1258
|
ports:
|
|
@@ -1160,11 +1288,23 @@ const devRedis = options.redis ? `\n redis:
|
|
|
1160
1288
|
volumes:
|
|
1161
1289
|
- redis-dev-data:/data` : "";
|
|
1162
1290
|
const devNamedVolumes = [
|
|
1163
|
-
" mongodb-dev-data:",
|
|
1291
|
+
postgres ? " postgres-dev-data:" : " mongodb-dev-data:",
|
|
1164
1292
|
...(options.redis ? [" redis-dev-data:"] : [])
|
|
1165
1293
|
];
|
|
1166
1294
|
add(`${svc}/docker-compose.dev.yml`, `name: ${options.name}-dev
|
|
1167
1295
|
services:
|
|
1296
|
+
schema-init:
|
|
1297
|
+
build:
|
|
1298
|
+
context: .
|
|
1299
|
+
dockerfile: Dockerfile.dev
|
|
1300
|
+
env_file:
|
|
1301
|
+
- .env.development
|
|
1302
|
+
command: ["npm", "run", "schema:apply:dev"]${databaseEnvironmentBlock}
|
|
1303
|
+
volumes:
|
|
1304
|
+
${devVolumes.join("\n")}
|
|
1305
|
+
depends_on:
|
|
1306
|
+
${postgres ? "postgres" : "mongo-init"}:
|
|
1307
|
+
condition: ${postgres ? "service_healthy" : "service_completed_successfully"}
|
|
1168
1308
|
${options.name}:
|
|
1169
1309
|
build:
|
|
1170
1310
|
context: .
|
|
@@ -1179,6 +1319,16 @@ volumes:
|
|
|
1179
1319
|
${devNamedVolumes.join("\n")}
|
|
1180
1320
|
`);
|
|
1181
1321
|
|
|
1322
|
+
for (const [relative, content] of Object.entries(ownedSources({ kind: "service", profiles: serviceProfiles }))) {
|
|
1323
|
+
add(`${svc}/${relative}`, content);
|
|
1324
|
+
}
|
|
1325
|
+
if (options.ui) {
|
|
1326
|
+
const uiProfiles = [...(options.auth === "firebase" ? ["firebase"] : []), "notifications"];
|
|
1327
|
+
for (const [relative, content] of Object.entries(ownedSources({ kind: "ui", profiles: uiProfiles }))) {
|
|
1328
|
+
add(`${ui}/${relative}`, content);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1182
1332
|
for (const [relative, content] of Object.entries(files)) {
|
|
1183
1333
|
const destination = path.join(root, relative);
|
|
1184
1334
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
@@ -1186,5 +1336,10 @@ for (const [relative, content] of Object.entries(files)) {
|
|
|
1186
1336
|
if (relative.endsWith(".sh")) fs.chmodSync(destination, 0o755);
|
|
1187
1337
|
}
|
|
1188
1338
|
|
|
1189
|
-
|
|
1339
|
+
for (const repository of [svc, ...(options.ui ? [ui] : [])]) {
|
|
1340
|
+
const result = runSync(path.join(root, repository), "write", contractVersion);
|
|
1341
|
+
if (result.errors.length) fail(result.errors.join("\n"));
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
return { root, name: options.name, database: options.db, authentication: options.auth, ui: options.ui, redis: options.redis, worker: options.worker, repositories: [svc, ...(options.ui ? [ui] : [])], files: Object.keys(files).length };
|
|
1190
1345
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const OWNED_GUIDANCE_START = "<!-- tailframe:owned:start -->";
|
|
2
|
+
export const OWNED_GUIDANCE_END = "<!-- tailframe:owned:end -->";
|
|
3
|
+
|
|
4
|
+
export function ownedGuidance(config) {
|
|
5
|
+
const profileList = config.profiles.length ? config.profiles.join(", ") : "none";
|
|
6
|
+
const client = config.kind !== "service";
|
|
7
|
+
return `${OWNED_GUIDANCE_START}
|
|
8
|
+
## Tailframe-owned contract
|
|
9
|
+
|
|
10
|
+
- Contract: \`${config.contractVersion}\`; kind: \`${config.kind}\`; profiles: ${profileList}.
|
|
11
|
+
- Run \`npm run sync:architecture\` and \`npm run validate:architecture\` after architectural changes.
|
|
12
|
+
- Files written by \`tailframe sync --write\` are generated sources. Change their canonical templates in Tailframe, not in this product.
|
|
13
|
+
- Keep product capabilities under ${client ? "`src/modules/<module>` (or `lib/modules/<module>` for Flutter)" : "`src/modules/<module>`"}; keep application assembly, technology-neutral contracts, and provider adapters in their canonical app/core/platform roots.
|
|
14
|
+
${client ? "- Notifications use the Tailframe-owned application-shell queue and host. Product copy remains in the owning capability module.\n" : "- Every use case exposes `execute(context, input)`. Entry points construct a request or system context and pass an explicit input object.\n- Resident processes perform no DDL. `schema:apply` is the only schema lifecycle entry point.\n- RPC operations use `<owning-module>.<operation>` with no compatibility aliases.\n"}${OWNED_GUIDANCE_END}`;
|
|
15
|
+
}
|