@zaaxch/tailframe 4.0.8 → 4.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaaxch/tailframe",
3
- "version": "4.0.8",
3
+ "version": "4.0.10",
4
4
  "description": "Tailframe architecture toolkit: validates the Tailframe structure, import-boundary, and file-convention contracts. The package version is the contract version.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/generate.mjs CHANGED
@@ -404,35 +404,14 @@ export function themeToggleSource() {
404
404
  :title="\`Switch to \${themeStore.isDark ? 'light' : 'dark'} mode\`"
405
405
  @click="themeStore.toggleTheme"
406
406
  >
407
- <svg
408
- v-if="themeStore.isDark"
409
- aria-hidden="true"
410
- class="size-5"
411
- viewBox="0 0 24 24"
412
- fill="none"
413
- stroke="currentColor"
414
- stroke-width="1.8"
415
- >
416
- <circle cx="12" cy="12" r="4" />
417
- <path
418
- d="M12 2v2m0 16v2M4.93 4.93l1.42 1.42m11.3 11.3 1.42 1.42M2 12h2m16 0h2M4.93 19.07l1.42-1.42m11.3-11.3 1.42-1.42"
419
- />
420
- </svg>
421
- <svg
422
- v-else
423
- aria-hidden="true"
424
- class="size-5"
425
- viewBox="0 0 24 24"
426
- fill="none"
427
- stroke="currentColor"
428
- stroke-width="1.8"
429
- >
430
- <path d="M20.5 15.2A8.5 8.5 0 0 1 8.8 3.5a8.5 8.5 0 1 0 11.7 11.7Z" />
431
- </svg>
407
+ <FontAwesomeIcon v-if="themeStore.isDark" :icon="faSun" aria-hidden="true" class="size-5" />
408
+ <FontAwesomeIcon v-else :icon="faMoon" aria-hidden="true" class="size-5" />
432
409
  </button>
433
410
  </template>
434
411
 
435
412
  <script setup lang="ts">
413
+ import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons";
414
+ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
436
415
  import { useThemeStore } from "@/app/stores/theme.store";
437
416
 
438
417
  const themeStore = useThemeStore();
package/src/new.mjs CHANGED
@@ -202,7 +202,7 @@ Use this workflow for any new domain capability or operation. Add only the layer
202
202
  2. Read the root and every applicable child \`AGENTS.md\`.
203
203
  3. Inspect the nearest working module and tests.
204
204
  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.
205
- 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.
205
+ 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. Font Awesome's Vue component and free solid icon set are the standard icon system for shell and product UI.
206
206
  6. Define use cases as \`execute(context, input)\`. Keep trusted \`RequestContext\` separate from client input.
207
207
  7. Put ownership, RBAC, entitlements, and domain policies in use-case or domain code, not client bodies or route metadata.
208
208
  8. Define repository contracts in the owning module and implement them in persistence adapters.
@@ -595,7 +595,7 @@ Run \`pnpm validate:architecture\` and \`pnpm format:check\` after changing file
595
595
  `);
596
596
 
597
597
  if (options.ui) {
598
- 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" } : {}) };
598
+ const uiDeps = { "@fortawesome/fontawesome-svg-core": "^7.3.1", "@fortawesome/free-solid-svg-icons": "^7.3.1", "@fortawesome/vue-fontawesome": "^3.3.3", "@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" } : {}) };
599
599
  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" };
600
600
  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"));
601
601
 
@@ -604,7 +604,24 @@ add(`${ui}/.gitattributes`, gitAttributes);
604
604
  add(`${ui}/tsconfig.json`, JSON.stringify({ files: [], references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }] }, null, "\t"));
605
605
  add(`${ui}/tsconfig.app.json`, JSON.stringify({ extends: "@vue/tsconfig/tsconfig.dom.json", include: ["env.d.ts", "src/**/*", "src/**/*.vue"], compilerOptions: { composite: true, noEmit: true, tsBuildInfoFile: "./.tmp/tsconfig.app.tsbuildinfo", types: ["vitest/globals"], baseUrl: ".", paths: { "@/*": ["./src/*"] } } }, null, "\t"));
606
606
  add(`${ui}/tsconfig.node.json`, JSON.stringify({ extends: "@tsconfig/node22/tsconfig.json", include: ["vite.config.ts"], compilerOptions: { composite: true, noEmit: true, tsBuildInfoFile: "./.tmp/tsconfig.node.tsbuildinfo", types: ["node"] } }, null, "\t"));
607
- add(`${ui}/vite.config.ts`, `import { fileURLToPath, URL } from "node:url";\nimport { defineConfig } from "vitest/config";\nimport vue from "@vitejs/plugin-vue";\nimport tailwindcss from "@tailwindcss/vite";\nexport default defineConfig({ plugins: [vue(), tailwindcss()], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } }, server: { proxy: { "/api": { target: "http://localhost:3000", changeOrigin: true } } }, test: { environment: "jsdom", globals: true } });`);
607
+ add(`${ui}/vite.config.ts`, `import { fileURLToPath, URL } from "node:url";
608
+ import { defineConfig } from "vitest/config";
609
+ import vue from "@vitejs/plugin-vue";
610
+ import tailwindcss from "@tailwindcss/vite";
611
+
612
+ export default defineConfig({
613
+ plugins: [vue(), tailwindcss()],
614
+ resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
615
+ server: {
616
+ proxy: {
617
+ "/api": {
618
+ target: process.env.VITE_API_PROXY_TARGET ?? "http://localhost:3000",
619
+ changeOrigin: true
620
+ }
621
+ }
622
+ },
623
+ test: { environment: "jsdom", globals: true }
624
+ });`);
608
625
  add(`${ui}/env.d.ts`, `/// <reference types="vite/client" />`);
609
626
  add(`${ui}/index.html`, `<!doctype html>
610
627
  <html lang="en">
@@ -717,7 +734,7 @@ These conventions apply to \`${ui}/src\`. Inspect the nearest working module bef
717
734
  - Use \`src/core\` only for small technology-neutral client contracts.
718
735
  - Use \`src/platform\` for Axios, Firebase, browser APIs, and other technical adapters.
719
736
  - Use \`src/modules/<module>\` for every product capability. Never substitute \`src/features\` or global \`apis\`, \`views\`, \`services\`, \`stores\`, or \`types\` directories.
720
- - 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\`.
737
+ - 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\`. Font Awesome's Vue component and free solid icon set are the standard icon system for shell and product UI.
721
738
  - 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.
722
739
 
723
740
  ## Change placement
@@ -748,7 +765,7 @@ Use named routes.${options.auth === "firebase" ? " Preserve Firebase authenticat
748
765
 
749
766
  ## State management
750
767
  Keep local state local. Use Pinia only for state shared across routes or unrelated components.
751
- 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.
768
+ 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. ThemeToggle uses Tailframe's Font Awesome Vue integration.
752
769
 
753
770
  ## UI behavior
754
771
  - 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.
@@ -760,7 +777,7 @@ Keep all product state in the owning module. The auth/notification/theme stores,
760
777
  - Keep \`src/app/styles.css\` limited to Tailwind imports, theme tokens, and true global base behavior.
761
778
 
762
779
  ## Production packaging
763
- The UI runs through Vite in development but has no production container. The service's multi-stage Dockerfile builds this application and copies its output into \`dist/public\`; Express serves it with SPA fallback. AWS ECR stores the combined \`linux/amd64\` production image under the default immutable \`<product-sha>\` tag, and production Compose requires that exact tag.${options.auth === "firebase" ? ` Provide \`${ui}/.env.production\` before a production build; the service build mounts it as a required BuildKit secret only while Vite compiles the browser bundle and does not copy it into the final image.` : ""}
780
+ Development Compose runs Vite from the UI's development-only container on the product's private network; its proxy target is configurable and all development ports remain internal-only so multiple products can run concurrently. The UI has no independent production container. The service's multi-stage Dockerfile builds this application and copies its output into \`dist/public\`; Express serves it with SPA fallback. AWS ECR stores the combined \`linux/amd64\` production image under the default immutable \`<product-sha>\` tag, and production Compose requires that exact tag.${options.auth === "firebase" ? ` Provide \`${ui}/.env.production\` before a production build; the service build mounts it as a required BuildKit secret only while Vite compiles the browser bundle and does not copy it into the final image.` : ""}
764
781
 
765
782
  ## Product language
766
783
  The product domain is undefined. Do not invent entities, workflows, roles, claims, navigation, or customer-facing promises.
@@ -799,6 +816,18 @@ WORKDIR /workspace/apps/service
799
816
  EXPOSE 3000
800
817
  CMD ["pnpm", "dev"]
801
818
  `);
819
+ if (options.ui) add(`${ui}/Dockerfile.dev`, `FROM node:22.13-alpine
820
+ RUN npm install --global corepack@0.34.5 && corepack enable
821
+ WORKDIR /workspace
822
+ COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
823
+ COPY apps/service/package.json apps/service/package.json
824
+ COPY apps/ui/package.json apps/ui/package.json
825
+ RUN pnpm install --frozen-lockfile --filter ${uiPackage}...
826
+ COPY apps/ui apps/ui
827
+ WORKDIR /workspace/apps/ui
828
+ EXPOSE 5173
829
+ CMD ["pnpm", "dev"]
830
+ `);
802
831
  add(`${svc}/scripts/build_and_push.sh`, `#!/usr/bin/env bash
803
832
  set -euo pipefail
804
833
 
@@ -1552,17 +1581,17 @@ networks:
1552
1581
 
1553
1582
  const devWorker = options.worker ? `\n worker:
1554
1583
  build:
1555
- context: .
1556
- dockerfile: Dockerfile.dev
1584
+ context: ../..
1585
+ dockerfile: apps/service/Dockerfile.dev
1557
1586
  env_file:
1558
1587
  - .env.development
1559
- command: ["npm", "run", "dev:worker"]${developmentEnvironmentBlock}
1588
+ command: ["pnpm", "dev:worker"]${developmentEnvironmentBlock}
1560
1589
  volumes:
1561
1590
  ${devVolumes.join("\n")}${devDependsBlock}` : "";
1562
1591
  const devDatabase = postgres ? `\n postgres:
1563
1592
  image: postgres:17
1564
- ports:
1565
- - "5432:5432"
1593
+ expose:
1594
+ - "5432"
1566
1595
  environment:
1567
1596
  POSTGRES_DB: ${databaseName}
1568
1597
  POSTGRES_USER: ${options.name}-app
@@ -1576,8 +1605,8 @@ const devDatabase = postgres ? `\n postgres:
1576
1605
  retries: 30` : `\n mongodb:
1577
1606
  image: mongo:8
1578
1607
  command: mongod --replSet rs0 --bind_ip_all
1579
- ports:
1580
- - "27017:27017"
1608
+ expose:
1609
+ - "27017"
1581
1610
  volumes:
1582
1611
  - mongodb-dev-data:/data/db
1583
1612
  healthcheck:
@@ -1604,10 +1633,22 @@ const devDatabase = postgres ? `\n postgres:
1604
1633
  '`;
1605
1634
  const devRedis = options.redis ? `\n redis:
1606
1635
  image: redis:7-alpine
1607
- ports:
1608
- - "6379:6379"
1636
+ expose:
1637
+ - "6379"
1609
1638
  volumes:
1610
1639
  - redis-dev-data:/data` : "";
1640
+ const devUi = options.ui ? `\n ui:
1641
+ build:
1642
+ context: ../..
1643
+ dockerfile: apps/ui/Dockerfile.dev
1644
+ ${options.auth === "firebase" ? " env_file:\n - ../ui/.env.development\n" : ""} environment:
1645
+ VITE_API_PROXY_TARGET: http://${options.name}:3000
1646
+ volumes:
1647
+ - ../ui:/workspace/apps/ui
1648
+ - /workspace/node_modules
1649
+ - /workspace/apps/ui/node_modules
1650
+ expose:
1651
+ - "5173"` : "";
1611
1652
  const devNamedVolumes = [
1612
1653
  postgres ? " postgres-dev-data:" : " mongodb-dev-data:",
1613
1654
  ...(options.redis ? [" redis-dev-data:"] : [])
@@ -1616,11 +1657,11 @@ add(`${svc}/docker-compose.dev.yml`, `name: ${options.name}-dev
1616
1657
  services:
1617
1658
  schema-init:
1618
1659
  build:
1619
- context: .
1620
- dockerfile: Dockerfile.dev
1660
+ context: ../..
1661
+ dockerfile: apps/service/Dockerfile.dev
1621
1662
  env_file:
1622
1663
  - .env.development
1623
- command: ["npm", "run", "schema:apply:dev"]${developmentEnvironmentBlock}
1664
+ command: ["pnpm", "schema:apply:dev"]${developmentEnvironmentBlock}
1624
1665
  volumes:
1625
1666
  ${devVolumes.join("\n")}
1626
1667
  depends_on:
@@ -1628,14 +1669,14 @@ ${devVolumes.join("\n")}
1628
1669
  condition: ${postgres ? "service_healthy" : "service_completed_successfully"}
1629
1670
  ${options.name}:
1630
1671
  build:
1631
- context: .
1632
- dockerfile: Dockerfile.dev
1672
+ context: ../..
1673
+ dockerfile: apps/service/Dockerfile.dev
1633
1674
  env_file:
1634
1675
  - .env.development
1635
- ports:
1636
- - "3000:3000"${developmentEnvironmentBlock}
1676
+ expose:
1677
+ - "3000"${developmentEnvironmentBlock}
1637
1678
  volumes:
1638
- ${devVolumes.join("\n")}${devDependsBlock}${devWorker}${devDatabase}${devRedis}
1679
+ ${devVolumes.join("\n")}${devDependsBlock}${devUi}${devWorker}${devDatabase}${devRedis}
1639
1680
  volumes:
1640
1681
  ${devNamedVolumes.join("\n")}
1641
1682
  `);
package/src/sync.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { appRoot, loadConfig } from "./config.mjs";
4
+ import { themeToggleSource } from "./generate.mjs";
4
5
  import { appGuidance, OWNED_GUIDANCE_END, OWNED_GUIDANCE_START, productGuidance } from "./owned-guidance.mjs";
5
6
  import { ownedSources } from "./owned-sources.mjs";
6
7
 
@@ -47,7 +48,11 @@ export function runSync(rootArgument, mode, runningVersion, selectedKind) {
47
48
  continue;
48
49
  }
49
50
  syncGuidance(loaded.root, root, appGuidance(loaded.config, app), mode, changed, errors, app.path);
50
- for (const [relative, expected] of Object.entries(ownedSources(app))) {
51
+ const sources = ownedSources(app);
52
+ if (app.kind === "ui" && fs.existsSync(path.join(root, "src/app/public/ThemeToggle.vue"))) {
53
+ sources["src/app/public/ThemeToggle.vue"] = themeToggleSource();
54
+ }
55
+ for (const [relative, expected] of Object.entries(sources)) {
51
56
  const absolute = path.join(root, relative);
52
57
  const productRelative = path.join(app.path, relative);
53
58
  const actual = fs.existsSync(absolute) ? fs.readFileSync(absolute, "utf8") : undefined;
package/src/validate.mjs CHANGED
@@ -8,6 +8,11 @@ import { runSync } from "./sync.mjs";
8
8
 
9
9
  const NON_EXCEPTABLE_RULES = new Set(["S8", "S9", "U5", "U6", "U7"]);
10
10
  const PNPM_VERSION = "11.22.0";
11
+ const FONT_AWESOME_UI_DEPENDENCIES = {
12
+ "@fortawesome/fontawesome-svg-core": "^7.3.1",
13
+ "@fortawesome/free-solid-svg-icons": "^7.3.1",
14
+ "@fortawesome/vue-fontawesome": "^3.3.3"
15
+ };
11
16
 
12
17
  export function runValidate(root, kind) {
13
18
  const structural = validateArchitecture(root, kind);
@@ -61,7 +66,7 @@ function validateVersionMetadata(root, runningVersion) {
61
66
  return errors;
62
67
  }
63
68
 
64
- function validateAppMetadata(productRoot, app) {
69
+ function validateAppMetadata(productRoot, app, productConfig) {
65
70
  const errors = [];
66
71
  const root = appRoot(productRoot, app);
67
72
  if (!fs.existsSync(root)) return [`Missing configured application root: ${app.path}`];
@@ -71,6 +76,7 @@ function validateAppMetadata(productRoot, app) {
71
76
  else {
72
77
  const compose = fs.readFileSync(composeFile, "utf8");
73
78
  const generatedProcessCount = app.profiles.includes("worker") ? 3 : 2;
79
+ const hasUi = productConfig.apps.some((candidate) => candidate.kind === "ui");
74
80
  const count = (term) => compose.split(term).length - 1;
75
81
  for (const term of [
76
82
  " - .:/workspace/apps/service",
@@ -84,6 +90,14 @@ function validateAppMetadata(productRoot, app) {
84
90
  if (compose.includes(" - .:/app\n")) {
85
91
  errors.push(`${app.path}/docker-compose.dev.yml uses the obsolete /app service source mount`);
86
92
  }
93
+ if (compose.includes(" ports:")) {
94
+ errors.push(`${app.path}/docker-compose.dev.yml must keep generated development ports internal-only`);
95
+ }
96
+ if (hasUi) {
97
+ for (const term of [" ui:", "dockerfile: apps/ui/Dockerfile.dev", "VITE_API_PROXY_TARGET:", " - ../ui:/workspace/apps/ui", " - /workspace/apps/ui/node_modules"]) {
98
+ if (!compose.includes(term)) errors.push(`${app.path}/docker-compose.dev.yml is missing the development UI container contract: ${term.trim()}`);
99
+ }
100
+ }
87
101
  if (app.profiles.includes("firebase")) {
88
102
  for (const term of [
89
103
  " FIREBASE_SERVICE_ACCOUNT_PATH: /app/firebase-service-account.development.json",
@@ -97,6 +111,7 @@ function validateAppMetadata(productRoot, app) {
97
111
  }
98
112
  }
99
113
  if (app.kind === "ui") {
114
+ if (!fs.existsSync(path.join(root, "Dockerfile.dev"))) errors.push(`${app.path}/Dockerfile.dev is required`);
100
115
  const generatedArtifacts = fs.readdirSync(root, { withFileTypes: true })
101
116
  .filter(
102
117
  (entry) =>
@@ -123,6 +138,13 @@ function validateAppMetadata(productRoot, app) {
123
138
  if (manifest.devDependencies?.["@zaaxch/tailframe"] || manifest.dependencies?.["@zaaxch/tailframe"]) {
124
139
  errors.push(`${app.path}/package.json must not depend on @zaaxch/tailframe; pin it at the product root`);
125
140
  }
141
+ if (app.kind === "ui") {
142
+ for (const [dependency, version] of Object.entries(FONT_AWESOME_UI_DEPENDENCIES)) {
143
+ if (manifest.dependencies?.[dependency] !== version) {
144
+ errors.push(`${app.path}/package.json must pin Tailframe's ${dependency} dependency to ${version}`);
145
+ }
146
+ }
147
+ }
126
148
  } catch {
127
149
  errors.push(`${app.path}/package.json is not valid JSON`);
128
150
  }
@@ -140,7 +162,7 @@ export function runConfiguredValidate(rootArgument, runningVersion, selectedKind
140
162
  errors.push(`tailframe.json does not declare a ${selectedKind} app`);
141
163
  }
142
164
  errors.push(...validateVersionMetadata(loaded.root, runningVersion));
143
- for (const app of loaded.config.apps) errors.push(...validateAppMetadata(loaded.root, app));
165
+ for (const app of loaded.config.apps) errors.push(...validateAppMetadata(loaded.root, app, loaded.config));
144
166
  if (errors.length) return errors;
145
167
  const apps = selectedKind ? loaded.config.apps.filter((app) => app.kind === selectedKind) : loaded.config.apps;
146
168
  for (const app of apps) {