@zaaxch/tailframe 4.0.7 → 4.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaaxch/tailframe",
3
- "version": "4.0.7",
3
+ "version": "4.0.9",
4
4
  "description": "Tailframe architecture toolkit: validates the Tailframe structure, import-boundary, and file-convention contracts. The package version is the contract version.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/new.mjs CHANGED
@@ -269,10 +269,6 @@ add(`${svc}/.env.example`, `NODE_ENV=development\nPORT=3000\nCORS_ORIGIN=https:/
269
269
  add(`${svc}/.env.infrastructure.example`, `ECR_IMAGE=<account>.dkr.ecr.<region>.amazonaws.com/${options.name}
270
270
  IMAGE_TAG=<product-sha>
271
271
  APP_ENV_FILE=/opt/${options.name}/shared/.env.production
272
- CADDY_ORIGIN_CERTIFICATE=/opt/${options.name}/shared/caddy/origin.pem
273
- CADDY_ORIGIN_KEY=/opt/${options.name}/shared/caddy/origin-key.pem
274
- CADDY_DATA_DIR=/opt/${options.name}/shared/caddy/data
275
- CADDY_CONFIG_DIR=/opt/${options.name}/shared/caddy/config
276
272
  ${postgres ? `POSTGRES_DATA_DIR=/opt/${options.name}/shared/postgres/data
277
273
  POSTGRES_ROOT_PASSWORD=<uri-safe-random-value>
278
274
  POSTGRES_APP_PASSWORD=<different-uri-safe-random-value>
@@ -286,7 +282,11 @@ MONGODB_BACKUP_PASSWORD=<different-uri-safe-random-value>
286
282
  ${options.auth === "firebase" ? `SECRETS_DIR=/opt/${options.name}/shared/secrets
287
283
  ` : ""}${options.redis ? `REDIS_DATA_DIR=/opt/${options.name}/shared/redis/data
288
284
  REDIS_PASSWORD=<different-uri-safe-random-value>
289
- ` : ""}`);
285
+ ` : ""}CADDY_ORIGIN_CERTIFICATE=/opt/${options.name}/shared/caddy/certs/origin.pem
286
+ CADDY_ORIGIN_KEY=/opt/${options.name}/shared/caddy/certs/origin-key.pem
287
+ CADDY_DATA_DIR=/opt/${options.name}/shared/caddy/data
288
+ CADDY_CONFIG_DIR=/opt/${options.name}/shared/caddy/config
289
+ `);
290
290
  add(`${svc}/src/platform/config/env.ts`, `import "@dotenvx/dotenvx/config";
291
291
  export const env = {
292
292
  nodeEnv: process.env.NODE_ENV ?? "development",
@@ -588,7 +588,7 @@ ${options.ui ? "- In production, serve the compiled UI from `dist/public` with a
588
588
  The ECR build targets \`linux/amd64\`. ${options.ui ? "Its default immutable tag is `<product-sha>` and production Compose requires that exact tag." : "Its default immutable tag is `<product-sha>` and production Compose requires that exact tag."} The runtime image contains production dependencies only and runs as the Node user.${options.ui && options.auth === "firebase" ? ` The build requires \`${ui}/.env.production\` and mounts it as a BuildKit secret only while Vite compiles the browser bundle; it is not copied into the final image.` : ""}
589
589
 
590
590
  ## Production deployment
591
- Pull requests into product \`main\` validate release candidates. The resulting \`main\` push validates again before GitHub Actions may publish an immutable image tagged with the full product commit SHA. Image publication and production deployment are separate workflows with distinct AWS roles: the release role publishes, while the pull-only deployment role manually deploys from \`main\` using an already-published full SHA from \`main\` history and that commit's deployment files. Build both role-trust subjects from GitHub's API-reported \`sub_claim_prefix\`; never infer the effective prefix from \`use_immutable_subject\`. Product data migrations must authenticate to the registry, pull and digest-verify that exact release image, and explicitly select the production datastore; never inherit rehearsal image or target defaults. GitHub Actions may replace files under \`/opt/${options.name}/current\` and MUST NOT replace persistent configuration, credentials, keyfiles, or data under \`/opt/${options.name}/shared\`. Keep Node runtime behavior in the shared service \`.env.production\`, Compose interpolation and infrastructure credentials in \`.env.infrastructure\`, and fixed topology wiring in Compose \`environment:\`. The optional UI build-time \`.env.production\` is a separate ephemeral file. Do not run deployment until the host and GitHub environments have been bootstrapped according to \`docs/production-deployment.md\`.
591
+ Pull requests into product \`main\` validate release candidates. The resulting \`main\` push validates again before GitHub Actions may publish an immutable image tagged with the full product commit SHA. Image publication and production deployment are separate workflows with distinct AWS roles: the release role publishes, while the pull-only deployment role manually deploys from \`main\` using an already-published full SHA from \`main\` history and that commit's deployment files. Build both role-trust subjects from GitHub's API-reported \`sub_claim_prefix\`; never infer the effective prefix from \`use_immutable_subject\`. Product data migrations must authenticate to the registry, pull and digest-verify that exact release image, and explicitly select the production datastore; never inherit rehearsal image or target defaults. GitHub Actions may replace files under \`/opt/${options.name}/current\` and MUST NOT replace persistent configuration, credentials, keyfiles, or data under \`/opt/${options.name}/shared\`. Caddy is the only production edge surface: deploy the matching commit's \`Caddyfile\`, keep its certificate and runtime state under \`shared\`, and force-recreate only the proxy after rollout so bind-mounted configuration changes take effect. Keep Node runtime behavior in the shared service \`.env.production\`, Compose interpolation and infrastructure credentials in \`.env.infrastructure\`, and fixed topology wiring in Compose \`environment:\`. The optional UI build-time \`.env.production\` is a separate ephemeral file. Do not run deployment until the host and GitHub environments have been bootstrapped according to \`docs/production-deployment.md\`.
592
592
 
593
593
  ## Tests and validation
594
594
  Run \`pnpm validate:architecture\` and \`pnpm format:check\` after changing files or imports. The generated service also includes unit tests and ${postgres ? "PostgreSQL-backed" : "Mongo-backed"} integration tests. Run focused service database tests through pnpm for the full service check. Test use cases, policies, ${postgres ? "PostgreSQL" : "Mongo"} repository adapters, and HTTP boundaries where behavior lives. Every public operation needs success and error-envelope coverage; authenticated operations need trusted-identity coverage; persisted capabilities need ${postgres ? "PostgreSQL" : "Mongo"} ownership/query coverage; UI-enabled services must verify that \`/api\` paths never fall through to the SPA. All AWS/ECR and Docker commands, including the release image script, must run outside the sandbox from the first attempt.
@@ -604,7 +604,24 @@ add(`${ui}/.gitattributes`, gitAttributes);
604
604
  add(`${ui}/tsconfig.json`, JSON.stringify({ files: [], references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }] }, null, "\t"));
605
605
  add(`${ui}/tsconfig.app.json`, JSON.stringify({ extends: "@vue/tsconfig/tsconfig.dom.json", include: ["env.d.ts", "src/**/*", "src/**/*.vue"], compilerOptions: { composite: true, noEmit: true, tsBuildInfoFile: "./.tmp/tsconfig.app.tsbuildinfo", types: ["vitest/globals"], baseUrl: ".", paths: { "@/*": ["./src/*"] } } }, null, "\t"));
606
606
  add(`${ui}/tsconfig.node.json`, JSON.stringify({ extends: "@tsconfig/node22/tsconfig.json", include: ["vite.config.ts"], compilerOptions: { composite: true, noEmit: true, tsBuildInfoFile: "./.tmp/tsconfig.node.tsbuildinfo", types: ["node"] } }, null, "\t"));
607
- add(`${ui}/vite.config.ts`, `import { fileURLToPath, URL } from "node:url";\nimport { defineConfig } from "vitest/config";\nimport vue from "@vitejs/plugin-vue";\nimport tailwindcss from "@tailwindcss/vite";\nexport default defineConfig({ plugins: [vue(), tailwindcss()], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } }, server: { proxy: { "/api": { target: "http://localhost:3000", changeOrigin: true } } }, test: { environment: "jsdom", globals: true } });`);
607
+ add(`${ui}/vite.config.ts`, `import { fileURLToPath, URL } from "node:url";
608
+ import { defineConfig } from "vitest/config";
609
+ import vue from "@vitejs/plugin-vue";
610
+ import tailwindcss from "@tailwindcss/vite";
611
+
612
+ export default defineConfig({
613
+ plugins: [vue(), tailwindcss()],
614
+ resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
615
+ server: {
616
+ proxy: {
617
+ "/api": {
618
+ target: process.env.VITE_API_PROXY_TARGET ?? "http://localhost:3000",
619
+ changeOrigin: true
620
+ }
621
+ }
622
+ },
623
+ test: { environment: "jsdom", globals: true }
624
+ });`);
608
625
  add(`${ui}/env.d.ts`, `/// <reference types="vite/client" />`);
609
626
  add(`${ui}/index.html`, `<!doctype html>
610
627
  <html lang="en">
@@ -760,7 +777,7 @@ Keep all product state in the owning module. The auth/notification/theme stores,
760
777
  - Keep \`src/app/styles.css\` limited to Tailwind imports, theme tokens, and true global base behavior.
761
778
 
762
779
  ## Production packaging
763
- The UI runs through Vite in development but has no production container. The service's multi-stage Dockerfile builds this application and copies its output into \`dist/public\`; Express serves it with SPA fallback. AWS ECR stores the combined \`linux/amd64\` production image under the default immutable \`<product-sha>\` tag, and production Compose requires that exact tag.${options.auth === "firebase" ? ` Provide \`${ui}/.env.production\` before a production build; the service build mounts it as a required BuildKit secret only while Vite compiles the browser bundle and does not copy it into the final image.` : ""}
780
+ Development Compose runs Vite from the UI's development-only container on the product's private network; its proxy target is configurable and all development ports remain internal-only so multiple products can run concurrently. The UI has no independent production container. The service's multi-stage Dockerfile builds this application and copies its output into \`dist/public\`; Express serves it with SPA fallback. AWS ECR stores the combined \`linux/amd64\` production image under the default immutable \`<product-sha>\` tag, and production Compose requires that exact tag.${options.auth === "firebase" ? ` Provide \`${ui}/.env.production\` before a production build; the service build mounts it as a required BuildKit secret only while Vite compiles the browser bundle and does not copy it into the final image.` : ""}
764
781
 
765
782
  ## Product language
766
783
  The product domain is undefined. Do not invent entities, workflows, roles, claims, navigation, or customer-facing promises.
@@ -799,6 +816,18 @@ WORKDIR /workspace/apps/service
799
816
  EXPOSE 3000
800
817
  CMD ["pnpm", "dev"]
801
818
  `);
819
+ if (options.ui) add(`${ui}/Dockerfile.dev`, `FROM node:22.13-alpine
820
+ RUN npm install --global corepack@0.34.5 && corepack enable
821
+ WORKDIR /workspace
822
+ COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
823
+ COPY apps/service/package.json apps/service/package.json
824
+ COPY apps/ui/package.json apps/ui/package.json
825
+ RUN pnpm install --frozen-lockfile --filter ${uiPackage}...
826
+ COPY apps/ui apps/ui
827
+ WORKDIR /workspace/apps/ui
828
+ EXPOSE 5173
829
+ CMD ["pnpm", "dev"]
830
+ `);
802
831
  add(`${svc}/scripts/build_and_push.sh`, `#!/usr/bin/env bash
803
832
  set -euo pipefail
804
833
 
@@ -1103,6 +1132,7 @@ jobs:
1103
1132
  "install -m 755 -d /opt/${options.name}/current/deploy/${deploymentArtifactDirectory}"
1104
1133
  scp -i ~/.ssh/${options.name}-deploy \\
1105
1134
  apps/service/docker-compose.yml \\
1135
+ apps/service/Caddyfile \\
1106
1136
  apps/service/scripts/deploy_remote.sh \\
1107
1137
  "\${DEPLOY_USER}@\${DEPLOY_HOST}:/opt/${options.name}/current/"
1108
1138
  scp -i ~/.ssh/${options.name}-deploy \\
@@ -1158,6 +1188,9 @@ trap - EXIT
1158
1188
  docker compose --env-file "\${INFRA_ENV}" --file "\${COMPOSE_FILE}" config --quiet
1159
1189
  docker compose --env-file "\${INFRA_ENV}" --file "\${COMPOSE_FILE}" pull
1160
1190
  docker compose --env-file "\${INFRA_ENV}" --file "\${COMPOSE_FILE}" up -d --wait
1191
+ # Compose does not detect changes inside the bind-mounted Caddyfile, so recreate only the proxy
1192
+ # after every deployment to make the files installed under current/ take effect.
1193
+ docker compose --env-file "\${INFRA_ENV}" --file "\${COMPOSE_FILE}" up -d --wait --no-deps --force-recreate caddy
1161
1194
  `);
1162
1195
 
1163
1196
  if (postgres) add(`${svc}/deploy/postgres/10-create-application-user.sh`, `#!/usr/bin/env bash
@@ -1194,14 +1227,16 @@ configured. No push or merge deploys production automatically.
1194
1227
  Configure the ECR repository with immutable image tags. The release-only role may publish; the distinct deployment
1195
1228
  role is pull-only. Record the full commit SHA and ECR digest; deployment and product migrations use that release image.
1196
1229
 
1197
- Production ingress is a required Caddy reverse proxy in every generated service deployment. The application container remains
1198
- internal; Caddy is the only edge surface and must own 80/443 exposure.
1230
+ Production ingress is a required Caddy reverse proxy for every generated Tailframe service. The app container is internal-only;
1231
+ Caddy is the only edge surface and must remain the only internet-facing host for production traffic.
1232
+ This document assumes 80/443 exposure through Caddy only.
1199
1233
 
1200
1234
  The host separates replaceable artifacts from persistent state:
1201
1235
 
1202
1236
  \`\`\`text
1203
1237
  /opt/${options.name}/
1204
1238
  ├── current/
1239
+ │ ├── Caddyfile
1205
1240
  │ ├── docker-compose.yml
1206
1241
  │ ├── deploy_remote.sh
1207
1242
  │ └── deploy/${deploymentArtifactDirectory}/${deploymentArtifactFile}
@@ -1228,8 +1263,7 @@ Actions may replace \`current/\` and must never replace \`shared/\`. The server
1228
1263
  - \`.env.production\` contains Node application behavior and provider credentials.
1229
1264
  - \`.env.infrastructure\` contains Compose paths, image identity, and infrastructure passwords. Start from
1230
1265
  \`.env.infrastructure.example\` and replace every placeholder.
1231
- - \`.env.infrastructure\` is also the host-owned control plane for Caddy certificate paths and runtime directories; keep TLS
1232
- key material under shared storage and rotate it independently from application images.
1266
+ - \`.env.infrastructure\` also owns Caddy certificate/runtime ownership boundaries.
1233
1267
  - Compose \`environment:\` owns fixed production wiring: \`NODE_ENV\`, internal database/cache URLs, and mounted
1234
1268
  credential paths.
1235
1269
  ${options.ui && options.auth === "firebase" ? `- The UI application's build-time \`.env.production\` comes from the GitHub \`UI_ENV_PRODUCTION\` secret and is unrelated to the service runtime file above.
@@ -1243,14 +1277,16 @@ ${postgres
1243
1277
  ? `2. Create \`/opt/${options.name}/current/deploy/postgres\`, \`/opt/${options.name}/shared/postgres/data\`,
1244
1278
  \`/opt/${options.name}/shared/caddy/certs\`, \`/opt/${options.name}/shared/caddy/data\`, \`/opt/${options.name}/shared/caddy/config\`${options.redis ? `, plus \`/opt/${options.name}/shared/redis/data\`` : ""}.`
1245
1279
  : `2. Create \`/opt/${options.name}/current/deploy/mongo\`, \`/opt/${options.name}/shared/mongo/config\`, and
1246
- \`/opt/${options.name}/shared/mongo/data\`, \`/opt/${options.name}/shared/caddy/certs\`,
1247
- \`/opt/${options.name}/shared/caddy/data\`, \`/opt/${options.name}/shared/caddy/config\`${options.redis ? `, plus \`/opt/${options.name}/shared/redis/data\`` : ""}.`}
1280
+ \`/opt/${options.name}/shared/mongo/data\`, \`/opt/${options.name}/shared/caddy/certs\`, \`/opt/${options.name}/shared/caddy/data\`,
1281
+ \`/opt/${options.name}/shared/caddy/config\`${options.redis ? `, plus \`/opt/${options.name}/shared/redis/data\`` : ""}.`}
1248
1282
  3. Install \`.env.production\` and a completed \`.env.infrastructure\` under \`shared/\`.
1249
- ${postgres ? "" : `4. Generate the MongoDB replica key with \`openssl rand -base64 756\`, make it owned by UID/GID \`999:999\`, and
1283
+ 4. Install the origin certificate at \`shared/caddy/certs/origin.pem\` and its private key at
1284
+ \`shared/caddy/certs/origin-key.pem\`; make both root-owned and mode \`0600\`.
1285
+ ${postgres ? "" : `5. Generate the MongoDB replica key with \`openssl rand -base64 756\`, make it owned by UID/GID \`999:999\`, and
1250
1286
  mode \`0400\`.
1251
- `}${options.auth === "firebase" ? `${postgres ? "4" : "5"}. Install the Firebase service account at \`shared/secrets/firebase-service-account.json\`, owned by \`root:1000\` and mode \`0640\`.
1252
- ` : ""}${postgres ? (options.auth === "firebase" ? "5" : "4") : (options.auth === "firebase" ? "6" : "5")}. Give the deployment user Docker access and write access to \`current/\`; keep \`shared/\` and its files protected.
1253
- ${postgres ? (options.auth === "firebase" ? "6" : "5") : (options.auth === "firebase" ? "7" : "6")}. Authorize the dedicated deployment SSH key and record the host key for GitHub Actions.
1287
+ `}${options.auth === "firebase" ? `${postgres ? "5" : "6"}. Install the Firebase service account at \`shared/secrets/firebase-service-account.json\`, owned by \`root:1000\` and mode \`0640\`.
1288
+ ` : ""}${postgres ? (options.auth === "firebase" ? "6" : "5") : (options.auth === "firebase" ? "7" : "6")}. Give the deployment user Docker access and write access to \`current/\`; keep \`shared/\` and its files protected.
1289
+ ${postgres ? (options.auth === "firebase" ? "7" : "6") : (options.auth === "firebase" ? "8" : "7")}. Authorize the dedicated deployment SSH key and record the host key for GitHub Actions.
1254
1290
 
1255
1291
  Use a fresh ${postgres ? "PostgreSQL" : "MongoDB"} data directory. Migrating an existing unauthenticated database requires a separately reviewed plan.
1256
1292
 
@@ -1297,6 +1333,10 @@ The remote script requires the provisioned infrastructure file, atomically repla
1297
1333
  full-SHA release and running the same workflow. Never use \`docker compose down -v\` in production; image rollback
1298
1334
  does not restore database state.
1299
1335
 
1336
+ The deployment installs the selected commit's \`Caddyfile\` and force-recreates only the proxy after the general
1337
+ rollout so bind-mounted configuration changes take effect. Preserve product-specific hostname restrictions when
1338
+ customizing the domain-neutral generated listener.
1339
+
1300
1340
  ## Product data-migration handoff
1301
1341
 
1302
1342
  Tailframe does not generate product-specific data-migration orchestration. Before a production data migration,
@@ -1305,8 +1345,8 @@ explicitly configure the production datastore target. Never inherit a rehearsal
1305
1345
 
1306
1346
  ${postgres ? "PostgreSQL" : "MongoDB"} and optional Redis expose no host ports and require authentication. Verify API readiness, container health,
1307
1347
  database/cache authentication, file permissions, and off-host backup/restore before treating the deployment as
1308
- production-ready. Product-specific TLS, capacity limits, migrations, and scheduled-work grace periods must be added
1309
- from measured application requirements rather than copied from another product.
1348
+ production-ready. Product-specific hostnames, certificate-provider procedures, capacity limits, migrations, and
1349
+ scheduled-work grace periods must be added from measured application requirements rather than copied from another product.
1310
1350
  `);
1311
1351
 
1312
1352
  const serviceVolumes = [
@@ -1350,7 +1390,7 @@ const developmentEnvironmentBlock = `\n environment:\n${developmentEnvironmen
1350
1390
  const volumeBlock = serviceVolumes.length ? `\n volumes:\n${serviceVolumes.join("\n")}` : "";
1351
1391
  const caddyService = `
1352
1392
  caddy:
1353
- image: caddy:2-alpine
1393
+ image: caddy:2.11.4-alpine
1354
1394
  ports:
1355
1395
  - "80:80"
1356
1396
  - "443:443"
@@ -1367,14 +1407,14 @@ const caddyService = `
1367
1407
  - backend
1368
1408
  restart: unless-stopped`;
1369
1409
  add(`${svc}/Caddyfile`, `{
1370
- auto_https off
1410
+ auto_https off
1371
1411
  }
1372
1412
  :80 {
1373
- redir https://{host}{uri} 308
1413
+ redir https://{host}{uri} 308
1374
1414
  }
1375
1415
  :443 {
1376
- tls /etc/caddy/certs/origin.pem /etc/caddy/certs/origin-key.pem
1377
- reverse_proxy ${options.name}:3000
1416
+ tls /etc/caddy/certs/origin.pem /etc/caddy/certs/origin-key.pem
1417
+ reverse_proxy ${options.name}:3000
1378
1418
  }`);
1379
1419
  const workerService = options.worker ? `\n worker:
1380
1420
  image: \${ECR_IMAGE:?Set ECR_IMAGE}:\${IMAGE_TAG:?Set IMAGE_TAG}
@@ -1517,8 +1557,7 @@ services:
1517
1557
  ${options.name}:
1518
1558
  image: \${ECR_IMAGE:?Set ECR_IMAGE}:\${IMAGE_TAG:?Set IMAGE_TAG}
1519
1559
  env_file:
1520
- - \${APP_ENV_FILE:-.env.production}
1521
- ${productionEnvironmentBlock}${volumeBlock}${dependsBlock}
1560
+ - \${APP_ENV_FILE:-.env.production}${productionEnvironmentBlock}${volumeBlock}${dependsBlock}
1522
1561
  healthcheck:
1523
1562
  test:
1524
1563
  - CMD-SHELL
@@ -1534,7 +1573,7 @@ ${productionEnvironmentBlock}${volumeBlock}${dependsBlock}
1534
1573
  stop_grace_period: 2m
1535
1574
  networks:
1536
1575
  - backend
1537
- restart: unless-stopped${workerService}${schemaInitService}${databaseService}${redisService}${caddyService}
1576
+ restart: unless-stopped${caddyService}${workerService}${schemaInitService}${databaseService}${redisService}
1538
1577
  networks:
1539
1578
  backend:
1540
1579
  driver: bridge
@@ -1542,17 +1581,17 @@ networks:
1542
1581
 
1543
1582
  const devWorker = options.worker ? `\n worker:
1544
1583
  build:
1545
- context: .
1546
- dockerfile: Dockerfile.dev
1584
+ context: ../..
1585
+ dockerfile: apps/service/Dockerfile.dev
1547
1586
  env_file:
1548
1587
  - .env.development
1549
- command: ["npm", "run", "dev:worker"]${developmentEnvironmentBlock}
1588
+ command: ["pnpm", "dev:worker"]${developmentEnvironmentBlock}
1550
1589
  volumes:
1551
1590
  ${devVolumes.join("\n")}${devDependsBlock}` : "";
1552
1591
  const devDatabase = postgres ? `\n postgres:
1553
1592
  image: postgres:17
1554
- ports:
1555
- - "5432:5432"
1593
+ expose:
1594
+ - "5432"
1556
1595
  environment:
1557
1596
  POSTGRES_DB: ${databaseName}
1558
1597
  POSTGRES_USER: ${options.name}-app
@@ -1566,8 +1605,8 @@ const devDatabase = postgres ? `\n postgres:
1566
1605
  retries: 30` : `\n mongodb:
1567
1606
  image: mongo:8
1568
1607
  command: mongod --replSet rs0 --bind_ip_all
1569
- ports:
1570
- - "27017:27017"
1608
+ expose:
1609
+ - "27017"
1571
1610
  volumes:
1572
1611
  - mongodb-dev-data:/data/db
1573
1612
  healthcheck:
@@ -1594,10 +1633,22 @@ const devDatabase = postgres ? `\n postgres:
1594
1633
  '`;
1595
1634
  const devRedis = options.redis ? `\n redis:
1596
1635
  image: redis:7-alpine
1597
- ports:
1598
- - "6379:6379"
1636
+ expose:
1637
+ - "6379"
1599
1638
  volumes:
1600
1639
  - redis-dev-data:/data` : "";
1640
+ const devUi = options.ui ? `\n ui:
1641
+ build:
1642
+ context: ../..
1643
+ dockerfile: apps/ui/Dockerfile.dev
1644
+ ${options.auth === "firebase" ? " env_file:\n - ../ui/.env.development\n" : ""} environment:
1645
+ VITE_API_PROXY_TARGET: http://${options.name}:3000
1646
+ volumes:
1647
+ - ../ui:/workspace/apps/ui
1648
+ - /workspace/node_modules
1649
+ - /workspace/apps/ui/node_modules
1650
+ expose:
1651
+ - "5173"` : "";
1601
1652
  const devNamedVolumes = [
1602
1653
  postgres ? " postgres-dev-data:" : " mongodb-dev-data:",
1603
1654
  ...(options.redis ? [" redis-dev-data:"] : [])
@@ -1606,11 +1657,11 @@ add(`${svc}/docker-compose.dev.yml`, `name: ${options.name}-dev
1606
1657
  services:
1607
1658
  schema-init:
1608
1659
  build:
1609
- context: .
1610
- dockerfile: Dockerfile.dev
1660
+ context: ../..
1661
+ dockerfile: apps/service/Dockerfile.dev
1611
1662
  env_file:
1612
1663
  - .env.development
1613
- command: ["npm", "run", "schema:apply:dev"]${developmentEnvironmentBlock}
1664
+ command: ["pnpm", "schema:apply:dev"]${developmentEnvironmentBlock}
1614
1665
  volumes:
1615
1666
  ${devVolumes.join("\n")}
1616
1667
  depends_on:
@@ -1618,14 +1669,14 @@ ${devVolumes.join("\n")}
1618
1669
  condition: ${postgres ? "service_healthy" : "service_completed_successfully"}
1619
1670
  ${options.name}:
1620
1671
  build:
1621
- context: .
1622
- dockerfile: Dockerfile.dev
1672
+ context: ../..
1673
+ dockerfile: apps/service/Dockerfile.dev
1623
1674
  env_file:
1624
1675
  - .env.development
1625
- ports:
1626
- - "3000:3000"${developmentEnvironmentBlock}
1676
+ expose:
1677
+ - "3000"${developmentEnvironmentBlock}
1627
1678
  volumes:
1628
- ${devVolumes.join("\n")}${devDependsBlock}${devWorker}${devDatabase}${devRedis}
1679
+ ${devVolumes.join("\n")}${devDependsBlock}${devUi}${devWorker}${devDatabase}${devRedis}
1629
1680
  volumes:
1630
1681
  ${devNamedVolumes.join("\n")}
1631
1682
  `);
package/src/validate.mjs CHANGED
@@ -61,7 +61,7 @@ function validateVersionMetadata(root, runningVersion) {
61
61
  return errors;
62
62
  }
63
63
 
64
- function validateAppMetadata(productRoot, app) {
64
+ function validateAppMetadata(productRoot, app, productConfig) {
65
65
  const errors = [];
66
66
  const root = appRoot(productRoot, app);
67
67
  if (!fs.existsSync(root)) return [`Missing configured application root: ${app.path}`];
@@ -71,6 +71,7 @@ function validateAppMetadata(productRoot, app) {
71
71
  else {
72
72
  const compose = fs.readFileSync(composeFile, "utf8");
73
73
  const generatedProcessCount = app.profiles.includes("worker") ? 3 : 2;
74
+ const hasUi = productConfig.apps.some((candidate) => candidate.kind === "ui");
74
75
  const count = (term) => compose.split(term).length - 1;
75
76
  for (const term of [
76
77
  " - .:/workspace/apps/service",
@@ -84,6 +85,14 @@ function validateAppMetadata(productRoot, app) {
84
85
  if (compose.includes(" - .:/app\n")) {
85
86
  errors.push(`${app.path}/docker-compose.dev.yml uses the obsolete /app service source mount`);
86
87
  }
88
+ if (compose.includes(" ports:")) {
89
+ errors.push(`${app.path}/docker-compose.dev.yml must keep generated development ports internal-only`);
90
+ }
91
+ if (hasUi) {
92
+ for (const term of [" ui:", "dockerfile: apps/ui/Dockerfile.dev", "VITE_API_PROXY_TARGET:", " - ../ui:/workspace/apps/ui", " - /workspace/apps/ui/node_modules"]) {
93
+ if (!compose.includes(term)) errors.push(`${app.path}/docker-compose.dev.yml is missing the development UI container contract: ${term.trim()}`);
94
+ }
95
+ }
87
96
  if (app.profiles.includes("firebase")) {
88
97
  for (const term of [
89
98
  " FIREBASE_SERVICE_ACCOUNT_PATH: /app/firebase-service-account.development.json",
@@ -97,6 +106,7 @@ function validateAppMetadata(productRoot, app) {
97
106
  }
98
107
  }
99
108
  if (app.kind === "ui") {
109
+ if (!fs.existsSync(path.join(root, "Dockerfile.dev"))) errors.push(`${app.path}/Dockerfile.dev is required`);
100
110
  const generatedArtifacts = fs.readdirSync(root, { withFileTypes: true })
101
111
  .filter(
102
112
  (entry) =>
@@ -140,7 +150,7 @@ export function runConfiguredValidate(rootArgument, runningVersion, selectedKind
140
150
  errors.push(`tailframe.json does not declare a ${selectedKind} app`);
141
151
  }
142
152
  errors.push(...validateVersionMetadata(loaded.root, runningVersion));
143
- for (const app of loaded.config.apps) errors.push(...validateAppMetadata(loaded.root, app));
153
+ for (const app of loaded.config.apps) errors.push(...validateAppMetadata(loaded.root, app, loaded.config));
144
154
  if (errors.length) return errors;
145
155
  const apps = selectedKind ? loaded.config.apps.filter((app) => app.kind === selectedKind) : loaded.config.apps;
146
156
  for (const app of apps) {