@zaaxch/tailframe 3.0.0 → 4.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/src/new.mjs CHANGED
@@ -66,15 +66,17 @@ if (!fs.existsSync(parent) || !fs.statSync(parent).isDirectory()) fail(`Parent d
66
66
  if (fs.existsSync(root) && fs.readdirSync(root).length) fail(`Refusing to overwrite non-empty directory: ${root}`);
67
67
 
68
68
  const title = options.name.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
69
- const svc = `${options.name}-svc`;
70
- const ui = `${options.name}-ui`;
69
+ const svc = "apps/service";
70
+ const ui = "apps/ui";
71
+ const servicePackage = `@${options.name}/service`;
72
+ const uiPackage = `@${options.name}/ui`;
71
73
  const databaseName = options.name.replaceAll("-", "_");
72
74
  const postgres = options.db === "postgres";
73
75
  const mongoApplicationUser = `${options.name}-app`;
74
76
  const mongoBackupUser = `${options.name}-backup`;
75
77
  const files = {};
76
78
  const add = (relative, content) => { files[relative] = content.endsWith("\n") ? content : `${content}\n`; };
77
- // Generated repositories depend on the published toolkit rather than copying a validator, and they
79
+ // Generated products depend on the published toolkit rather than copying a validator, and they
78
80
  // pin the exact contract version they were generated against.
79
81
  const contractVersion = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
80
82
  const serviceProfiles = [
@@ -84,6 +86,11 @@ const serviceProfiles = [
84
86
  ...(options.worker ? ["worker"] : []),
85
87
  ...(options.ui ? ["ui-host"] : [])
86
88
  ];
89
+ const uiProfiles = [...(options.auth === "firebase" ? ["firebase"] : []), "notifications"];
90
+ const apps = [
91
+ { kind: "service", path: svc, profiles: serviceProfiles },
92
+ ...(options.ui ? [{ kind: "ui", path: ui, profiles: uiProfiles }] : [])
93
+ ];
87
94
  const prettierConfig = {
88
95
  useTabs: true,
89
96
  tabWidth: 4,
@@ -105,92 +112,102 @@ const gitAttributes = `* text=auto eol=lf
105
112
 
106
113
  add(".prettierrc.json", JSON.stringify(prettierConfig, null, "\t"));
107
114
  add(".gitattributes", gitAttributes);
115
+ add("package.json", JSON.stringify({
116
+ name: options.name,
117
+ version: "0.1.0",
118
+ private: true,
119
+ packageManager: "pnpm@11.22.0",
120
+ scripts: {
121
+ dev: "pnpm -r --parallel --stream --if-present dev",
122
+ build: "pnpm -r --if-present build",
123
+ test: "pnpm -r --if-present test",
124
+ "type-check": "pnpm -r --if-present type-check",
125
+ "format:check": "pnpm -r --if-present format:check",
126
+ "validate:architecture": "tailframe validate .",
127
+ "sync:architecture": "tailframe sync --check ."
128
+ },
129
+ devDependencies: { "@zaaxch/tailframe": contractVersion }
130
+ }, null, "\t"));
131
+ add("pnpm-workspace.yaml", `packages:
132
+ - "apps/*"
133
+ injectWorkspacePackages: true
134
+ allowBuilds:
135
+ "@firebase/util": true
136
+ esbuild: true
137
+ protobufjs: true
138
+ unrs-resolver: true
139
+ `);
140
+ add("tailframe.json", configSource({ apps, contractVersion }));
108
141
 
109
142
  const repos = [[svc, "Express/TypeScript API and background runtime."]];
110
143
  if (options.ui) repos.push([ui, "Vue/Vite customer interface."]);
111
144
 
112
- add("AGENTS.md", `# ${title} repository guidance
145
+ add("AGENTS.md", `# ${title} product guidance
113
146
 
114
147
  ## Sources of truth
115
148
 
116
149
  - Treat implementation and tests as the source of truth for current behavior.
117
- - Product documentation may establish intent but is not proof of implemented behavior.
118
- - Do not claim functionality works merely because a type, route, UI control, configuration key, placeholder, or planning entry exists.
150
+ - Treat \`tailframe.json\` and the root pnpm workspace as the product architecture and dependency boundary.
151
+ - Read the nearest application \`AGENTS.md\` before changing files under \`apps/\`.
119
152
 
120
153
  ## Product boundary
121
154
 
122
- The product domain is not yet defined. Do not invent domain entities, workflows, claims, roles, or integrations. Add product-specific guidance only after an explicit decision or implementation establishes it.
155
+ The product domain is not yet defined. Do not invent domain entities, workflows, claims, roles, or integrations.
123
156
 
124
157
  ## Repository map
125
158
 
126
- This project root contains separate repositories. Do not assume that a convention from one repository applies to another.
159
+ - \`apps/service/\`  Express/TypeScript API and background runtime.
160
+ ${options.ui ? "- \`apps/ui/\`  Vue/Vite customer interface.\n" : ""}- The product root owns Git history, the pnpm lockfile, Tailframe metadata, orchestration, CI, and deployment.
127
161
 
128
- ${repos.map(([name, description]) => `- \`${name}/\` — ${description} Read \`${name}/AGENTS.md\` before editing it.`).join("\n")}
162
+ ## Cross-application changes
129
163
 
130
- ## Cross-repository changes
131
-
132
- 1. Read each applicable child \`AGENTS.md\`.
133
- 2. Inspect the nearest working implementation in each repository.
134
- 3. Keep shared contracts aligned across all affected clients and services.
135
- 4. Validate each repository with its own scripts.
136
- 5. Do not claim end-to-end behavior from changes or static checks in only one repository.
164
+ 1. Read the root and applicable application guidance.
165
+ 2. Keep shared wire contracts aligned across service and UI.
166
+ 3. Run focused checks while iterating and product-root checks before handoff.
167
+ 4. Do not claim end-to-end behavior from changes or static checks in only one application.
137
168
 
138
169
  ## Module workflow
139
170
 
140
- - Use the local \`new-module\` skill whenever creating or extending a domain module, public operation, persisted entity, or client capability module.
141
- - This applies when work affects one repository, multiple repositories, or one phase of a feature whose backend and clients will be implemented separately.
142
- - Do not wait for a task to become cross-repository before using the skill.
143
- - Create architecture files with \`tailframe generate\` in the affected repository; the skill orchestrates the work, the CLI creates the files.
171
+ - Use the local \`new-module\` skill whenever creating or extending a domain module, public operation, persisted entity, or client capability.
172
+ - Create architecture files with \`tailframe generate --app service|ui\` from the product root.
144
173
 
145
174
  ## Code-change guidelines
146
175
 
147
- - Keep changes small and focused and avoid unrelated refactoring.
148
- - Modify existing structures when they fit the requirement.
149
- - Preserve unrelated worktree changes.
150
- - Confirm every committed change belongs to the requested scope.
176
+ - Keep changes focused and preserve unrelated worktree changes.
177
+ - Do not create nested repositories or package-manager boundaries under \`apps/\`.
151
178
 
152
179
  ## GitHub workflow
153
180
 
154
181
  - Use \`gh\` for GitHub interactions when it supports the action.
155
- - Inspect existing issues and pull requests before proposing duplicates.
156
182
  - Never mutate GitHub state without explicit authorization.
157
- - Verify external mutations with a read-only follow-up.
158
183
 
159
184
  ## GitHub authentication and sandboxing
160
185
 
161
- - All \`gh\`, AWS/ECR, and Docker commands must run outside the sandbox. Request approval before the first attempt; do not try them inside the sandbox first.
162
- - Docker commands include \`docker\`, \`docker compose\`, \`docker exec\`, and package scripts whose purpose is to invoke Docker.
163
- - AWS/ECR commands include \`aws ecr\` authentication and package scripts that build or push ECR images.
164
- - Never print, log, commit, or include GitHub tokens in command output.
165
- - Keep mutation approvals separate from read-only GitHub access.
166
-
167
- ## Starting work from a GitHub issue
168
-
169
- Resolve and read the issue and comments, load applicable guidance, inspect worktree state and related work, determine scope and validation, preserve unrelated changes, and use an isolated branch for implementation. Starting implementation does not authorize changing issue metadata. Report the issue, repositories, branch, scope, checks, preserved changes, and blockers.
186
+ - All \`gh\`, AWS/ECR, and Docker commands must run outside the sandbox. Request approval before the first attempt.
187
+ - Never print, log, commit, or include credentials or tokens in command output.
170
188
  `);
171
-
172
189
  add(".agents/skills/new-module/SKILL.md", `---
173
190
  name: new-module
174
- description: Create or extend a domain-colocated module in one repository or across the optional service and UI repositories using explicit use cases, separate trusted request context, module-owned repository ports and persistence adapters, HTTP validation, and focused tests. Use for backend capabilities, HTTP operations, persisted entities, queries, commands, jobs, workers, frontend capability modules, or phased work where a client is implemented later.
191
+ description: Create or extend a domain-colocated module in the service and optional UI applications in one product repository using explicit use cases, separate trusted request context, module-owned repository ports and persistence adapters, HTTP validation, and focused tests. Use for backend capabilities, HTTP operations, persisted entities, queries, commands, jobs, workers, frontend capability modules, or phased work where a client is implemented later.
175
192
  ---
176
193
 
177
194
  # New module
178
195
 
179
196
  Use this workflow for any new domain capability or operation. Add only the layers earned by actual behavior.
180
197
 
181
- 1. Identify the module, requested operations, entry points, and repositories in scope. Classify every intended file with the applicable AGENTS.md placement table before writing. Do not assume full CRUD.
198
+ 1. Identify the module, requested operations, entry points, and applications in scope. Classify every intended file with the applicable AGENTS.md placement table before writing. Do not assume full CRUD.
182
199
  2. Read the root and every applicable child \`AGENTS.md\`.
183
200
  3. Inspect the nearest working module and tests.
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.
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.
201
+ 4. Create architecture files only with the tailframe CLI: \`tailframe generate --app <service|ui>\` creates canonical service and UI architecture files. Hand-creating architecture files or editing generated shell-catalog implementations is a conformance violation; inspect the generated diff and complete only the behavior-specific checklist.
202
+ 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; when the service exposes a user capability, its current-user record belongs to a UI \`user\` 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.
186
203
  6. Define use cases as \`execute(context, input)\`. Keep trusted \`RequestContext\` separate from client input.
187
204
  7. Put ownership, RBAC, entitlements, and domain policies in use-case or domain code, not client bodies or route metadata.
188
205
  8. Define repository contracts in the owning module and implement them in persistence adapters.
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.
206
+ 9. For persisted capabilities, prefer branded, domain-owned string IDs at domain and repository-port boundaries; generate identifiers with \`tailframe generate --app service identifiers <module> <NameId...>\`. Keep MongoDB \`ObjectId\` conversion inside MongoDB persistence adapters.
190
207
  10. Let HTTP routes, jobs, workers, scheduled tasks, and CLIs invoke use cases directly; do not call one entry point from another.
191
208
  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.
192
209
  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.
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.
210
+ 13. Run \`pnpm validate:architecture\` from the product root, then run focused type checks and tests. Report applications, operations, entry points, checks, placement decisions, and gaps.
194
211
 
195
212
  Do not create empty architectural layers, speculative operations, or mandatory controllers. Do not claim behavior from static files, weaken trusted context or persistence ownership, or change unrelated background behavior.
196
213
  `);
@@ -209,19 +226,18 @@ const svcDeps = {
209
226
  const svcDevDeps = {
210
227
  "@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
228
  ...(postgres ? { "@types/pg": "^8.15.5" } : {}),
212
- "@zaaxch/tailframe": contractVersion,
213
229
  jest: "^30.0.0", nodemon: "^3.1.9", prettier: "3.5.3", "ts-jest": "^29.4.11", "ts-node": "^10.9.2",
214
230
  "tsc-alias": "^1.8.16", "tsconfig-paths": "^4.2.0", typescript: "^5.8.2", supertest: "^7.2.2"
215
231
  };
216
232
  add(`${svc}/package.json`, JSON.stringify({
217
- name: svc, version: "0.1.0", private: true, main: "dist/server.js",
233
+ name: servicePackage, version: "0.1.0", private: true, main: "dist/server.js", files: ["dist"],
218
234
  engines: { node: ">=22.13.0" },
219
235
  scripts: {
220
236
  dev: "nodemon --legacy-watch --exec ts-node -r tsconfig-paths/register src/server.ts",
221
237
  ...(options.worker ? { "dev:worker": "nodemon --legacy-watch --exec ts-node -r tsconfig-paths/register src/worker.ts" } : {}),
222
238
  build: "tsc && tsc-alias", start: "node dist/server.js",
223
239
  ...(options.worker ? { worker: "node dist/worker.js" } : {}),
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/",
240
+ test: "pnpm test:unit && pnpm test:integration", "test:unit": "jest --selectProjects unit --runInBand", "test:integration": "jest --selectProjects integration --runInBand", "validate:architecture": "tailframe validate ../.. --app service", "sync:architecture": "tailframe sync --check ../..", format: "prettier --write src/", "format:check": "prettier --check src/",
225
241
  "schema:apply": "node dist/app/cli/applySchema.js", "schema:apply:dev": "ts-node -r tsconfig-paths/register src/app/cli/applySchema.ts",
226
242
  "test:db:up": "docker compose -f docker-compose.test.yml up -d", "test:db:down": "docker compose -f docker-compose.test.yml down -v",
227
243
  "docker:dev": "docker compose -f docker-compose.dev.yml up",
@@ -229,7 +245,6 @@ add(`${svc}/package.json`, JSON.stringify({
229
245
  "docker:push": "bash scripts/build_and_push.sh"
230
246
  }, dependencies: svcDeps, devDependencies: svcDevDeps
231
247
  }, null, "\t"));
232
- add(`${svc}/tailframe.json`, configSource({ kind: "service", profiles: serviceProfiles, contractVersion }));
233
248
  add(`${svc}/tsconfig.json`, JSON.stringify({
234
249
  compilerOptions: { target: "ES2022", module: "commonjs", rootDir: "src", outDir: "dist", strict: true, esModuleInterop: true, experimentalDecorators: true, emitDecoratorMetadata: true, baseUrl: ".", paths: { "@/*": ["src/*"] }, skipLibCheck: true },
235
250
  include: ["src/**/*.ts"]
@@ -249,7 +264,7 @@ const dbEnv = postgres
249
264
  : "MONGODB_URI=mongodb://localhost:27017/?replicaSet=rs0&directConnection=true\nMONGODB_DB_NAME=" + databaseName;
250
265
  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" : ""}`);
251
266
  add(`${svc}/.env.infrastructure.example`, `ECR_IMAGE=<account>.dkr.ecr.<region>.amazonaws.com/${options.name}
252
- IMAGE_TAG=<service-sha>${options.ui ? "_<ui-sha>" : ""}
267
+ IMAGE_TAG=<product-sha>
253
268
  APP_ENV_FILE=/opt/${options.name}/shared/.env.production
254
269
  ${postgres ? `POSTGRES_DATA_DIR=/opt/${options.name}/shared/postgres/data
255
270
  POSTGRES_ROOT_PASSWORD=<uri-safe-random-value>
@@ -476,7 +491,7 @@ These conventions apply to \`${svc}\`. This service is a modular monolith organi
476
491
  | --- | --- |
477
492
  | Product rule, query, command, or policy | \`src/modules/<module>/use-cases\` or \`domain\` |
478
493
  | Repository port | Owning module's \`use-cases/ports\` |
479
- | MongoDB implementation | Owning module's \`persistence\` |
494
+ | Database implementation | Owning module's \`persistence\` |
480
495
  | Express route or request schema | Owning module's \`http\` |
481
496
  | Vendor or external-system adapter | \`src/platform/integrations/<provider>\` |
482
497
  | Database, Redis, authentication, or HTTP mechanism | \`src/platform\` |
@@ -510,11 +525,13 @@ Product vocabulary stays in the module that owns its meaning; never move it into
510
525
  - Define repository contracts under the owning module's \`use-cases/ports\`.
511
526
  - Implement database-specific adapters under that module's \`persistence/\` directory.
512
527
  - For persisted capabilities, prefer branded, domain-owned string IDs in module types and repository ports. A technology-neutral \`Brand<Value, Tag>\` helper MAY be defined when a real module needs distinct ID types; do not create product entities or concrete ID types in the scaffold.
513
- - Repository ports and use cases MUST NOT expose database identifier types. Persistence adapters SHOULD define persistence-only document types and MUST NOT use \`any\` to bypass the domain/persistence distinction. MongoDB \`ObjectId\` conversion belongs inside the MongoDB persistence adapter.
528
+ - Repository ports and use cases MUST NOT expose database identifier types. Persistence adapters SHOULD define persistence-only storage types and MUST NOT use \`any\` to bypass the domain/persistence distinction. Conversion between domain and database representations belongs inside the database persistence adapter.
514
529
  - Add appropriate schema or validation, indexes, initialization or migrations, and focused tests.
515
530
 
516
531
  ## Project-specific persistence
517
- This service uses MongoDB. Module-owned MongoDB adapters own collection access, persistence-only document types, and conversion of branded module string IDs to and from ObjectId inside the adapter, along with queries, projections, validators, and indexes. Do not use any to bypass the domain/persistence distinction.
532
+ ${postgres
533
+ ? "This service uses PostgreSQL. Module-owned PostgreSQL adapters own table access, persistence-only row types, queries, projections, transactions, and conversion to domain types. Keep schema definitions and migrations with the owning module's persistence implementation, and never expose database types or use any to bypass repository ports."
534
+ : "This service uses MongoDB. Module-owned MongoDB adapters own collection access, persistence-only document types, and conversion of branded module string IDs to and from ObjectId inside the adapter, along with queries, projections, validators, and indexes. Do not use any to bypass the domain/persistence distinction."}
518
535
 
519
536
  ## Queries and commands
520
537
  - Commands enforce invariants and change state.
@@ -530,24 +547,20 @@ This service uses MongoDB. Module-owned MongoDB adapters own collection access,
530
547
  ${options.ui ? "- In production, serve the compiled UI from `dist/public` with a non-API SPA fallback. Never return the SPA for `/api` requests." : ""}
531
548
 
532
549
  ## Production image
533
- The ECR build targets \`linux/amd64\`. ${options.ui ? "Its default immutable tag is `<service-sha>_<ui-sha>` and production Compose requires that exact tag." : "Its default immutable tag is `<service-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.` : ""}
550
+ 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.` : ""}
534
551
 
535
552
  ## Production deployment
536
553
  Only service \`main\` pushes or manual workflow dispatch deploy; UI pushes never trigger deployment. 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 production environment have been bootstrapped according to \`docs/production-deployment.md\`.
537
554
 
538
555
  ## Tests and validation
539
- Run \`npm run validate:architecture\` and \`npm run format:check\` after changing files or imports. The generated service also includes unit tests and Mongo-backed integration tests. Run \`npm run test:db:up\`, \`npm test\`, and \`npm run test:db:down\` for the full service check. Test use cases, policies, 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 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 \`npm run docker:push\`, must run outside the sandbox from the first attempt.
556
+ 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 service \`docker:push\` script, must run outside the sandbox from the first attempt.
540
557
  `);
541
558
 
542
559
  if (options.ui) {
543
560
  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" } : {}) };
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" };
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
- }));
561
+ const uiDevDeps = { "@tsconfig/node22": "^22.0.1", "@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" };
562
+ add(`${ui}/package.json`, JSON.stringify({ name: uiPackage, 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 ../.. --app ui", "sync:architecture": "tailframe sync --check ../..", format: "prettier --write src/", "format:check": "prettier --check src/" }, dependencies: uiDeps, devDependencies: uiDevDeps }, null, "\t"));
563
+
551
564
  add(`${ui}/.prettierrc.json`, JSON.stringify(prettierConfig, null, "\t"));
552
565
  add(`${ui}/.gitattributes`, gitAttributes);
553
566
  add(`${ui}/tsconfig.json`, JSON.stringify({ files: [], references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }] }, null, "\t"));
@@ -589,6 +602,7 @@ if (options.auth === "firebase") add(`${ui}/src/app/configureHttp.ts`, configure
589
602
  add(`${ui}/src/platform/config.ts`, `export const apiBaseUrl = \`\${window.location.origin}/api/v1\`;`);
590
603
  add(`${ui}/src/modules/health/api/health.api.ts`, healthApiSource);
591
604
  add(`${ui}/src/modules/health/views/HealthView.vue`, `<script setup lang="ts">
605
+ import Button from "primevue/button";
592
606
  import { ref } from "vue";
593
607
  import { isServiceError } from "@/core/errors";
594
608
  import { getReadiness } from "@/modules/health/api/health.api";
@@ -616,14 +630,14 @@ async function checkReadiness() {
616
630
  <p class="font-bold uppercase tracking-[0.12em] text-emerald-800">Architecture reference</p>
617
631
  <h1 class="m-0 text-5xl font-bold sm:text-7xl">${title}</h1>
618
632
  <p>The product domain is intentionally undefined.</p>
619
- <button
633
+ <Button
620
634
  type="button"
635
+ :label="loading ? 'Checking…' : 'Check readiness'"
621
636
  class="self-start rounded-full bg-emerald-800 px-4 py-3 text-white disabled:opacity-60"
622
637
  :disabled="loading"
638
+ :loading="loading"
623
639
  @click="checkReadiness"
624
- >
625
- {{ loading ? "Checking…" : "Check readiness" }}
626
- </button>
640
+ />
627
641
  <p v-if="status" role="status">Service status: {{ status }}</p>
628
642
  <p v-if="error" role="alert">{{ error }}</p>
629
643
  </main>
@@ -652,7 +666,7 @@ These conventions apply to \`${ui}/src\`. Inspect the nearest working module bef
652
666
  - Use \`src/platform\` for Axios, Firebase, browser APIs, and other technical adapters.
653
667
  - Use \`src/modules/<module>\` for every product capability. Never substitute \`src/features\` or global \`apis\`, \`views\`, \`services\`, \`stores\`, or \`types\` directories.
654
668
  - 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.
669
+ - 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. When the service exposes a user capability, its current-user record belongs to a UI \`user\` module rather than whichever consumer happens to use fields from the response.
656
670
 
657
671
  ## Change placement
658
672
  | Change | Canonical owner |
@@ -685,7 +699,8 @@ Keep local state local. Use Pinia only for state shared across routes or unrelat
685
699
  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.
686
700
 
687
701
  ## UI behavior
688
- Reuse Vue, PrimeVue, Tailwind, loading, error, and accessibility conventions. Provide explicit loading, empty, error, and authorization states.
702
+ - Prefer PrimeVue for standard interactive controls and overlays when an appropriate component exists. Use native HTML for semantic structure, links, and genuinely custom interactions; use Tailwind for layout and composition.
703
+ - Reuse Vue, PrimeVue, Tailwind, loading, error, and accessibility conventions. Provide explicit loading, empty, error, and authorization states.
689
704
 
690
705
  ## Styling
691
706
  - Prefer Tailwind utility classes in Vue templates over custom selectors and component-scoped CSS.
@@ -693,74 +708,44 @@ Reuse Vue, PrimeVue, Tailwind, loading, error, and accessibility conventions. Pr
693
708
  - Keep \`src/app/styles.css\` limited to Tailwind imports, theme tokens, and true global base behavior.
694
709
 
695
710
  ## Production packaging
696
- The UI runs through Vite in development but has no production container. The service's multi-stage Dockerfile builds this repository 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 \`<service-sha>_<ui-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.` : ""}
711
+ 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.` : ""}
697
712
 
698
713
  ## Product language
699
714
  The product domain is undefined. Do not invent entities, workflows, roles, claims, navigation, or customer-facing promises.
700
715
 
701
716
  ## Validation
702
- Run \`npm run validate:architecture\` and \`npm run format:check\`, then add focused Vitest coverage and run type-check, build, and \`npm test\` as applicable. Use \`npm run test:watch\` only for interactive development.
717
+ Run \`pnpm validate:architecture\` and \`pnpm format:check\`, then add focused Vitest coverage and run type-check, build, and \`pnpm test\` as applicable. Use \`pnpm test:watch\` only for interactive development.
703
718
  `);
704
719
  }
705
720
 
706
- add(`${svc}/Dockerfile`, options.ui ? `FROM node:22.13-bookworm AS service-build
707
- WORKDIR /app/service
708
- COPY ${svc}/package*.json ./
709
- RUN npm ci
710
- COPY ${svc}/ ./
711
- RUN npm run build
712
-
713
- FROM node:22.13-bookworm AS service-production-dependencies
714
- WORKDIR /app/service
715
- COPY ${svc}/package*.json ./
716
- RUN npm ci --omit=dev && npm cache clean --force
717
-
718
- FROM node:22.13-bookworm AS ui-build
719
- WORKDIR /app/ui
720
- COPY ${ui}/package*.json ./
721
- RUN npm ci
722
- COPY ${ui}/ ./
723
- ${options.auth === "firebase" ? "RUN --mount=type=secret,id=ui_env,target=/app/ui/.env.production,required=true npm run build" : "RUN npm run build"}
721
+ add(`${svc}/Dockerfile`, `FROM node:22.13-bookworm AS build
722
+ RUN corepack enable
723
+ WORKDIR /workspace
724
+ COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
725
+ COPY apps/service/package.json apps/service/package.json
726
+ ${options.ui ? "COPY apps/ui/package.json apps/ui/package.json\n" : ""}RUN pnpm install --frozen-lockfile
727
+ COPY apps/service apps/service
728
+ ${options.ui ? "COPY apps/ui apps/ui\n" : ""}RUN pnpm --filter ${servicePackage} build
729
+ ${options.ui ? (options.auth === "firebase" ? `RUN --mount=type=secret,id=ui_env,target=/workspace/apps/ui/.env.production,required=true pnpm --filter ${uiPackage} build` : `RUN pnpm --filter ${uiPackage} build`) + "\n" : ""}RUN pnpm --filter ${servicePackage} --prod deploy /prod/service
724
730
 
725
731
  FROM node:22.13-bookworm-slim
726
732
  WORKDIR /app
727
733
  ENV NODE_ENV=production
728
- COPY --from=service-build /app/service/dist ./dist
729
- COPY --from=service-build /app/service/package*.json ./
730
- COPY --from=service-production-dependencies /app/service/node_modules ./node_modules
731
- COPY --from=ui-build /app/ui/dist ./dist/public
732
- USER node
733
- EXPOSE 3000
734
- CMD ["node", "dist/server.js"]
735
- ` : `FROM node:22.13-bookworm AS build
736
- WORKDIR /app
737
- COPY package*.json ./
738
- RUN npm ci
739
- COPY . .
740
- RUN npm run build
741
-
742
- FROM node:22.13-bookworm AS production-dependencies
743
- WORKDIR /app
744
- COPY package*.json ./
745
- RUN npm ci --omit=dev && npm cache clean --force
746
-
747
- FROM node:22.13-bookworm-slim
748
- WORKDIR /app
749
- ENV NODE_ENV=production
750
- COPY --from=build /app/dist ./dist
751
- COPY --from=build /app/package*.json ./
752
- COPY --from=production-dependencies /app/node_modules ./node_modules
753
- USER node
734
+ COPY --from=build /prod/service ./
735
+ ${options.ui ? "COPY --from=build /workspace/apps/ui/dist ./dist/public\n" : ""}USER node
754
736
  EXPOSE 3000
755
737
  CMD ["node", "dist/server.js"]
756
738
  `);
757
739
  add(`${svc}/Dockerfile.dev`, `FROM node:22.13-alpine
758
- WORKDIR /app
759
- COPY package*.json ./
760
- RUN npm install
761
- COPY . .
740
+ RUN corepack enable
741
+ WORKDIR /workspace
742
+ COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
743
+ COPY apps/service/package.json apps/service/package.json
744
+ RUN pnpm install --frozen-lockfile
745
+ COPY apps/service apps/service
746
+ WORKDIR /workspace/apps/service
762
747
  EXPOSE 3000
763
- CMD ["npm", "run", "dev"]
748
+ CMD ["pnpm", "dev"]
764
749
  `);
765
750
  add(`${svc}/scripts/build_and_push.sh`, `#!/usr/bin/env bash
766
751
  set -euo pipefail
@@ -768,54 +753,44 @@ set -euo pipefail
768
753
  : "\${AWS_ACCOUNT_ID:?Set AWS_ACCOUNT_ID}"
769
754
  : "\${AWS_REGION:?Set AWS_REGION}"
770
755
 
771
- SVC_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
772
- BUILD_CONTEXT="${options.ui ? `$(cd "\${SVC_DIR}/.." && pwd)` : `\${SVC_DIR}`}"
773
- ${options.ui ? `UI_DIR="\${BUILD_CONTEXT}/${ui}"
774
- SVC_SHA="$(git -C "\${SVC_DIR}" rev-parse --short HEAD)"
775
- UI_SHA="$(git -C "\${UI_DIR}" rev-parse --short HEAD)"` : `SVC_SHA="$(git -C "\${SVC_DIR}" rev-parse --short HEAD)"`}
776
-
756
+ SERVICE_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
757
+ PRODUCT_ROOT="$(cd "\${SERVICE_DIR}/../.." && pwd)"
758
+ PRODUCT_SHA="$(git -C "\${PRODUCT_ROOT}" rev-parse --short HEAD)"
777
759
  ECR_REPOSITORY="\${ECR_REPOSITORY:-${options.name}}"
778
- IMAGE_TAG="\${IMAGE_TAG:-${options.ui ? `\${SVC_SHA}_\${UI_SHA}` : `\${SVC_SHA}`}}"
760
+ IMAGE_TAG="\${IMAGE_TAG:-\${PRODUCT_SHA}}"
779
761
  REGISTRY="\${AWS_ACCOUNT_ID}.dkr.ecr.\${AWS_REGION}.amazonaws.com"
780
762
  IMAGE="\${REGISTRY}/\${ECR_REPOSITORY}:\${IMAGE_TAG}"
781
- DOCKERFILE="\${SVC_DIR}/Dockerfile"
782
- ${options.ui && options.auth === "firebase" ? `UI_ENV_FILE="\${UI_ENV_FILE:-\${UI_DIR}/.env.production}"` : ""}
763
+ DOCKERFILE="\${SERVICE_DIR}/Dockerfile"
764
+ ${options.ui && options.auth === "firebase" ? `UI_ENV_FILE="\${UI_ENV_FILE:-\${PRODUCT_ROOT}/apps/ui/.env.production}"` : ""}
783
765
 
784
766
  aws ecr get-login-password --region "\${AWS_REGION}" |
785
767
  docker login --username AWS --password-stdin "\${REGISTRY}"
786
768
 
787
- docker build --platform linux/amd64${options.ui && options.auth === "firebase" ? ` --secret "id=ui_env,src=\${UI_ENV_FILE}"` : ""} --file "\${DOCKERFILE}" --tag "\${IMAGE}" "\${BUILD_CONTEXT}"
769
+ docker build --platform linux/amd64${options.ui && options.auth === "firebase" ? ` --secret "id=ui_env,src=\${UI_ENV_FILE}"` : ""} --file "\${DOCKERFILE}" --tag "\${IMAGE}" "\${PRODUCT_ROOT}"
788
770
  docker push "\${IMAGE}"
789
771
 
790
772
  echo "Pushed \${IMAGE}"
791
773
  `);
792
-
793
- const workflowUiCheckout = options.ui ? `
794
- - name: Checkout UI dependency
795
- uses: actions/checkout@v6
796
- with:
797
- repository: \${{ github.repository_owner }}/${ui}
798
- ref: main
799
- ssh-key: \${{ secrets.UI_REPO_SSH_KEY }}
800
- path: ${ui}
801
- ` : "";
774
+ const workflowUiPath = options.ui ? ' - "apps/ui/**"\n' : "";
802
775
  const workflowUiEnvironment = options.ui && options.auth === "firebase" ? `
803
776
  - name: Write UI production environment
804
777
  env:
805
778
  UI_ENV_PRODUCTION: \${{ secrets.UI_ENV_PRODUCTION }}
806
779
  run: |
807
- printf '%s\\n' "$UI_ENV_PRODUCTION" > ${ui}/.env.production
780
+ printf '%s\\n' "$UI_ENV_PRODUCTION" > apps/ui/.env.production
808
781
  ` : "";
809
- const workflowTagResolution = options.ui ? `
810
- ui_sha="$(git -C ${ui} rev-parse --short HEAD)"
811
- echo "tag=\${svc_sha}_\${ui_sha}" >> "$GITHUB_OUTPUT"` : `
812
- echo "tag=\${svc_sha}" >> "$GITHUB_OUTPUT"`;
813
- add(`${svc}/.github/workflows/deploy.yml`, `name: Build and deploy
782
+ add(".github/workflows/deploy.yml", `name: Build and deploy
814
783
 
815
784
  on:
816
785
  push:
817
786
  branches:
818
787
  - main
788
+ paths:
789
+ - "apps/service/**"
790
+ ${workflowUiPath} - "package.json"
791
+ - "pnpm-workspace.yaml"
792
+ - "pnpm-lock.yaml"
793
+ - ".github/workflows/deploy.yml"
819
794
  workflow_dispatch:
820
795
 
821
796
  permissions:
@@ -840,15 +815,14 @@ jobs:
840
815
  DEPLOY_USER: \${{ vars.DEPLOY_USER }}
841
816
 
842
817
  steps:
843
- - name: Checkout service
818
+ - name: Checkout product
844
819
  uses: actions/checkout@v6
845
- with:
846
- path: ${svc}
847
- ${workflowUiCheckout}${workflowUiEnvironment}
820
+ ${workflowUiEnvironment}
848
821
  - name: Resolve immutable image
849
822
  id: image
850
823
  run: |
851
- svc_sha="$(git -C ${svc} rev-parse --short HEAD)"${workflowTagResolution}
824
+ product_sha="$(git rev-parse --short HEAD)"
825
+ echo "tag=\${product_sha}" >> "$GITHUB_OUTPUT"
852
826
  echo "registry=\${AWS_ACCOUNT_ID}.dkr.ecr.\${AWS_REGION}.amazonaws.com" >> "$GITHUB_OUTPUT"
853
827
  echo "image=\${AWS_ACCOUNT_ID}.dkr.ecr.\${AWS_REGION}.amazonaws.com/\${ECR_REPOSITORY}" >> "$GITHUB_OUTPUT"
854
828
 
@@ -859,10 +833,9 @@ ${workflowUiCheckout}${workflowUiEnvironment}
859
833
  aws-region: \${{ vars.AWS_REGION }}
860
834
 
861
835
  - name: Build and push image
862
- working-directory: ${svc}
863
836
  env:
864
837
  IMAGE_TAG: \${{ steps.image.outputs.tag }}
865
- run: ./scripts/build_and_push.sh
838
+ run: apps/service/scripts/build_and_push.sh
866
839
 
867
840
  - name: Configure deployment SSH
868
841
  env:
@@ -885,11 +858,11 @@ ${workflowUiCheckout}${workflowUiEnvironment}
885
858
  ssh -i ~/.ssh/${options.name}-deploy "\${DEPLOY_USER}@\${DEPLOY_HOST}" \\
886
859
  "install -m 755 -d /opt/${options.name}/current/deploy/mongo"
887
860
  scp -i ~/.ssh/${options.name}-deploy \\
888
- ${svc}/docker-compose.yml \\
889
- ${svc}/scripts/deploy_remote.sh \\
861
+ apps/service/docker-compose.yml \\
862
+ apps/service/scripts/deploy_remote.sh \\
890
863
  "\${DEPLOY_USER}@\${DEPLOY_HOST}:/opt/${options.name}/current/"
891
864
  scp -i ~/.ssh/${options.name}-deploy \\
892
- ${svc}/deploy/mongo/10-create-users.js \\
865
+ apps/service/deploy/mongo/10-create-users.js \\
893
866
  "\${DEPLOY_USER}@\${DEPLOY_HOST}:/opt/${options.name}/current/deploy/mongo/"
894
867
  ssh -i ~/.ssh/${options.name}-deploy "\${DEPLOY_USER}@\${DEPLOY_HOST}" \\
895
868
  "chmod 755 /opt/${options.name}/current/deploy_remote.sh"
@@ -899,7 +872,6 @@ ${workflowUiCheckout}${workflowUiEnvironment}
899
872
  ssh -i ~/.ssh/${options.name}-deploy "\${DEPLOY_USER}@\${DEPLOY_HOST}" \\
900
873
  "/opt/${options.name}/current/deploy_remote.sh '\${{ steps.image.outputs.tag }}' '\${{ steps.image.outputs.image }}'"
901
874
  `);
902
-
903
875
  add(`${svc}/scripts/deploy_remote.sh`, `#!/usr/bin/env bash
904
876
  set -euo pipefail
905
877
 
@@ -951,9 +923,9 @@ add(`${svc}/docs/production-deployment.md`, `# Production deployment
951
923
 
952
924
  ## Delivery model
953
925
 
954
- GitHub Actions checks out ${svc}${options.ui ? ` and ${ui}` : ""}${options.ui ? " as sibling build inputs" : ""}, builds one
955
- non-root production image, tags it ${options.ui ? "`<service-sha>_<ui-sha>`" : "`<service-sha>`"}, pushes it to ECR, and installs only deployment artifacts on the host.
956
- Only service \`main\` pushes and manual dispatch trigger this workflow; UI pushes never deploy.
926
+ GitHub Actions checks out the product once, builds one
927
+ non-root production image, tags it \`<product-sha>\`, pushes it to ECR, and installs only deployment artifacts on the host.
928
+ Changes to the service, optional UI, root workspace/lock configuration, or deployment files on product \`main\`, plus manual dispatch, trigger this workflow.
957
929
 
958
930
  The host separates replaceable artifacts from persistent state:
959
931
 
@@ -981,7 +953,7 @@ Actions may replace \`current/\` and must never replace \`shared/\`. The server
981
953
  \`.env.infrastructure.example\` and replace every placeholder.
982
954
  - Compose \`environment:\` owns fixed production wiring: \`NODE_ENV\`, internal database/cache URLs, and mounted
983
955
  credential paths.
984
- ${options.ui && options.auth === "firebase" ? `- The UI repository's build-time \`.env.production\` comes from the GitHub \`UI_ENV_PRODUCTION\` secret and is unrelated to the service runtime file above.
956
+ ${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.
985
957
  ` : ""}
986
958
  Do not inject \`.env.infrastructure\` into Node. Make both host environment files root-owned and mode \`0600\`.
987
959
 
@@ -1009,8 +981,7 @@ Configure these variables:
1009
981
 
1010
982
  Configure these secrets:
1011
983
 
1012
- ${options.ui ? `- \`UI_REPO_SSH_KEY\`: read-only deploy key for \`${ui}\`.
1013
- ` : ""}${options.ui && options.auth === "firebase" ? `- \`UI_ENV_PRODUCTION\`: Vite/Firebase build-time environment.
984
+ ${options.ui && options.auth === "firebase" ? `- \`UI_ENV_PRODUCTION\`: Vite/Firebase build-time environment.
1014
985
  ` : ""}- \`DEPLOY_SSH_PRIVATE_KEY\`: dedicated host deployment key.
1015
986
  - \`DEPLOY_SSH_KNOWN_HOSTS\`: pinned host key.
1016
987
 
@@ -1323,7 +1294,6 @@ for (const [relative, content] of Object.entries(ownedSources({ kind: "service",
1323
1294
  add(`${svc}/${relative}`, content);
1324
1295
  }
1325
1296
  if (options.ui) {
1326
- const uiProfiles = [...(options.auth === "firebase" ? ["firebase"] : []), "notifications"];
1327
1297
  for (const [relative, content] of Object.entries(ownedSources({ kind: "ui", profiles: uiProfiles }))) {
1328
1298
  add(`${ui}/${relative}`, content);
1329
1299
  }
@@ -1336,10 +1306,8 @@ for (const [relative, content] of Object.entries(files)) {
1336
1306
  if (relative.endsWith(".sh")) fs.chmodSync(destination, 0o755);
1337
1307
  }
1338
1308
 
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
- }
1309
+ const syncResult = runSync(root, "write", contractVersion);
1310
+ if (syncResult.errors.length) fail(syncResult.errors.join("\n"));
1343
1311
 
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 };
1312
+ return { root, name: options.name, database: options.db, authentication: options.auth, ui: options.ui, redis: options.redis, worker: options.worker, applications: [svc, ...(options.ui ? [ui] : [])], files: Object.keys(files).length };
1345
1313
  }