@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/bin/tailframe.mjs +18 -6
- package/package.json +1 -1
- package/src/architecture.mjs +3 -3
- package/src/config.mjs +57 -23
- package/src/conventions.mjs +3 -6
- package/src/exceptions.mjs +16 -6
- package/src/generate.mjs +13 -4
- package/src/new.mjs +133 -165
- package/src/owned-guidance.mjs +20 -8
- package/src/owned-sources.mjs +9 -266
- package/src/sync.mjs +48 -41
- package/src/ui-templates.mjs +0 -45
- package/src/validate.mjs +66 -20
- package/src/flutter.mjs +0 -111
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 =
|
|
70
|
-
const 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
|
|
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}
|
|
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
|
-
-
|
|
118
|
-
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
## Cross-application changes
|
|
129
163
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
141
|
-
-
|
|
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
|
|
148
|
-
-
|
|
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
|
|
162
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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: "
|
|
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=<
|
|
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
|
-
|
|
|
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
|
|
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
|
-
|
|
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 `<
|
|
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 \`
|
|
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", "@
|
|
545
|
-
add(`${ui}/package.json`, JSON.stringify({ name:
|
|
546
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
|
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 \`
|
|
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`,
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
COPY
|
|
711
|
-
RUN
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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=
|
|
729
|
-
COPY --from=
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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 ["
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
$
|
|
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
|
|
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="\${
|
|
782
|
-
${options.ui && options.auth === "firebase" ? `UI_ENV_FILE="\${UI_ENV_FILE:-\${
|
|
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}" "\${
|
|
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" >
|
|
780
|
+
printf '%s\\n' "$UI_ENV_PRODUCTION" > apps/ui/.env.production
|
|
808
781
|
` : "";
|
|
809
|
-
|
|
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
|
|
818
|
+
- name: Checkout product
|
|
844
819
|
uses: actions/checkout@v6
|
|
845
|
-
|
|
846
|
-
path: ${svc}
|
|
847
|
-
${workflowUiCheckout}${workflowUiEnvironment}
|
|
820
|
+
${workflowUiEnvironment}
|
|
848
821
|
- name: Resolve immutable image
|
|
849
822
|
id: image
|
|
850
823
|
run: |
|
|
851
|
-
|
|
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:
|
|
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
|
-
|
|
889
|
-
|
|
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
|
-
|
|
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
|
|
955
|
-
non-root production image, tags it
|
|
956
|
-
|
|
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
|
|
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 ? `- \`
|
|
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
|
-
|
|
1340
|
-
|
|
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,
|
|
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
|
}
|