@ultimat3/cli 21.0.0 → 22.1.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/CLAUDE.md +84 -1650
- package/README.md +40 -2
- package/package.json +30 -30
- package/src/api-registration.ts +124 -0
- package/src/app-artifacts.ts +39 -0
- package/src/app-boundaries.ts +5 -10
- package/src/app-load.ts +37 -5
- package/src/app-openapi.ts +27 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-root.ts +1 -1
- package/src/async-pages.ts +32 -0
- package/src/bin.ts +7 -1
- package/src/boundary-findings.ts +36 -0
- package/src/browser-launcher-fake-html.ts +80 -0
- package/src/browser-launcher-fake.ts +165 -0
- package/src/browser-launcher-port.ts +159 -0
- package/src/browser-launcher.ts +26 -133
- package/src/budgets.ts +11 -1
- package/src/cdp-shot-a11y.ts +77 -0
- package/src/cdp-shot-clock.ts +14 -0
- package/src/cdp-shot-driver.ts +150 -0
- package/src/cdp-shot-element.ts +147 -0
- package/src/cdp-shot-errors.ts +62 -0
- package/src/cdp-shot-keys.ts +152 -0
- package/src/cdp-shot-page.ts +230 -0
- package/src/cdp-shot-watch.ts +241 -0
- package/src/cmd-affected-spec.ts +24 -0
- package/src/cmd-affected.ts +3 -19
- package/src/cmd-build-spec.ts +16 -0
- package/src/cmd-build.ts +46 -20
- package/src/cmd-ci-spec.ts +24 -0
- package/src/cmd-ci.ts +17 -21
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db-spec.ts +99 -0
- package/src/cmd-db.ts +15 -94
- package/src/cmd-deploy-helm.ts +136 -0
- package/src/cmd-deploy-spec.ts +40 -0
- package/src/cmd-deploy.ts +111 -43
- package/src/cmd-dev-spec.ts +25 -0
- package/src/cmd-dev.ts +55 -36
- package/src/cmd-docs-spec.ts +16 -0
- package/src/cmd-docs.ts +2 -11
- package/src/cmd-doctor-spec.ts +21 -0
- package/src/cmd-doctor.ts +4 -18
- package/src/cmd-env-spec.ts +18 -0
- package/src/cmd-env.ts +4 -13
- package/src/cmd-errors-spec.ts +23 -0
- package/src/cmd-errors.ts +3 -17
- package/src/cmd-fix-spec.ts +18 -0
- package/src/cmd-fix.ts +4 -13
- package/src/cmd-generate-spec.ts +31 -0
- package/src/cmd-generate.ts +46 -42
- package/src/cmd-i18n-spec.ts +16 -0
- package/src/cmd-i18n.ts +7 -14
- package/src/cmd-jobs-spec.ts +56 -0
- package/src/cmd-jobs.ts +3 -50
- package/src/cmd-manifest-spec.ts +15 -0
- package/src/cmd-manifest.ts +19 -20
- package/src/cmd-mcp-spec.ts +21 -0
- package/src/cmd-mcp.ts +2 -15
- package/src/cmd-new-spec.ts +33 -0
- package/src/cmd-new.ts +38 -33
- package/src/cmd-policy-spec.ts +13 -0
- package/src/cmd-policy.ts +2 -9
- package/src/cmd-pr-spec.ts +39 -0
- package/src/cmd-pr.ts +5 -65
- package/src/cmd-registries-spec.ts +32 -0
- package/src/cmd-registries.ts +4 -25
- package/src/cmd-routes-spec.ts +12 -0
- package/src/cmd-routes.ts +2 -7
- package/src/cmd-secrets-spec.ts +19 -0
- package/src/cmd-secrets.ts +22 -34
- package/src/cmd-shot-island.ts +12 -13
- package/src/cmd-shot-spec.ts +53 -0
- package/src/cmd-shot.ts +11 -60
- package/src/cmd-tasks-spec.ts +21 -0
- package/src/cmd-tasks.ts +2 -16
- package/src/cmd-test-spec.ts +54 -0
- package/src/cmd-test.ts +4 -49
- package/src/cmd-verify-spec.ts +28 -0
- package/src/cmd-verify.ts +3 -23
- package/src/db-seed.ts +4 -2
- package/src/dev-dashboard.ts +4 -4
- package/src/dev-lock.ts +18 -2
- package/src/dev-port.ts +21 -0
- package/src/dev-route-table.ts +3 -3
- package/src/dispatch.ts +12 -5
- package/src/document-styles.ts +1 -1
- package/src/drift.ts +4 -18
- package/src/error-catalog.ts +16 -9
- package/src/error-codes.ts +28 -30
- package/src/error-pages.ts +19 -7
- package/src/errors.ts +14 -0
- package/src/favicon.ts +2 -2
- package/src/fix-imports.ts +1 -1
- package/src/fix-scan.ts +2 -9
- package/src/flag-reads.ts +1 -2
- package/src/foreign-text.ts +36 -0
- package/src/framework-schema.ts +5 -6
- package/src/generate-feature.ts +42 -0
- package/src/generate-files.ts +7 -3
- package/src/generate-grants.ts +83 -0
- package/src/generate-kinds.ts +56 -4
- package/src/i18n-index.ts +59 -10
- package/src/icon-assets.ts +1 -1
- package/src/image-prepare.ts +14 -0
- package/src/import-scan.ts +63 -0
- package/src/index.ts +67 -348
- package/src/invocation-flags.ts +26 -0
- package/src/island-bundle.ts +6 -1
- package/src/island-capture.ts +6 -6
- package/src/island-harness-script.ts +6 -2
- package/src/island-realtime.ts +15 -8
- package/src/island-shot.ts +5 -0
- package/src/island-store.ts +131 -0
- package/src/island-verdict.ts +1 -1
- package/src/job-registration.ts +42 -0
- package/src/jobs-driver.ts +2 -2
- package/src/load-findings.ts +51 -0
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +28 -27
- package/src/mcp-host.ts +27 -12
- package/src/mcp-ui-diff.ts +27 -0
- package/src/mcp-ui-inspect.ts +4 -4
- package/src/mcp-ui-interact.ts +21 -15
- package/src/mcp-ui.ts +23 -17
- package/src/measure-database.ts +73 -0
- package/src/measure-paths.ts +74 -0
- package/src/measure-scope.ts +74 -0
- package/src/messages.ts +1 -3
- package/src/metrics-endpoint.ts +1 -1
- package/src/otlp-export.ts +1 -1
- package/src/output.ts +6 -0
- package/src/permission-grants.ts +86 -0
- package/src/prerender-out.ts +25 -0
- package/src/prerender.ts +121 -102
- package/src/pwa-artifacts.ts +3 -3
- package/src/reexport-manifest.ts +2 -1
- package/src/registry.ts +80 -56
- package/src/role-realtime.ts +36 -0
- package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
- package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
- package/src/role-start-types.ts +112 -0
- package/src/{dev-roles.ts → role-start.ts} +42 -115
- package/src/{dev-sync.ts → role-sync.ts} +22 -8
- package/src/root-env.ts +67 -0
- package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
- package/src/{dev-services.ts → runtime-bindings.ts} +39 -16
- package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
- package/src/runtime-jobs.ts +87 -0
- package/src/{dev-live-feed.ts → runtime-live-feed.ts} +18 -5
- package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
- package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
- package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
- package/src/runtime-realtime.ts +55 -0
- package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
- package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
- package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
- package/src/scaffold-fixture.ts +28 -6
- package/src/scaffold-typecheck.ts +6 -3
- package/src/schema-drift.ts +7 -1
- package/src/script-csp.ts +5 -2
- package/src/secrets-rotation.ts +59 -0
- package/src/serve-boot.ts +192 -0
- package/src/serve-drain.ts +24 -0
- package/src/serve-entry.ts +6 -0
- package/src/serve-env.ts +116 -0
- package/src/serve-types.ts +55 -0
- package/src/serve.ts +44 -347
- package/src/shot-server.ts +2 -2
- package/src/shot-settle.ts +10 -1
- package/src/shot-theme.ts +3 -3
- package/src/shot-verdict.ts +16 -7
- package/src/signal-shred.ts +27 -0
- package/src/solid-loader.ts +26 -2
- package/src/static-report.ts +8 -1
- package/src/sync-url.ts +21 -1
- package/src/templates/action.ts +30 -16
- package/src/templates/entity.ts +12 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/job.ts +10 -7
- package/src/templates/policy.ts +20 -2
- package/src/templates/resource-create.ts +127 -0
- package/src/templates/resource-form-island.ts +49 -12
- package/src/templates/resource.ts +10 -4
- package/src/templates/scaffold-app.ts +4 -1
- package/src/templates/scaffold-auth.ts +3 -1
- package/src/templates/scaffold-container-compose.ts +184 -0
- package/src/templates/scaffold-container.ts +24 -159
- package/src/templates/scaffold-dashboard-example.ts +2 -2
- package/src/templates/scaffold-db-package.ts +16 -3
- package/src/templates/scaffold-demo-org.ts +41 -0
- package/src/templates/scaffold-entries.ts +1 -1
- package/src/templates/scaffold-env.ts +6 -0
- package/src/templates/scaffold-helm-templates.ts +66 -7
- package/src/templates/scaffold-helm.ts +27 -5
- package/src/templates/scaffold-i18n.ts +15 -10
- package/src/templates/scaffold-repo.ts +13 -10
- package/src/templates/scaffold-roles.ts +38 -10
- package/src/templates/slice-foundation.ts +1 -1
- package/src/templates/wrap.ts +4 -1
- package/src/test-passes.ts +2 -1
- package/src/test-workers.ts +26 -0
- package/src/ts-scan.ts +3 -6
- package/src/tsconfig-references.ts +1 -2
- package/src/verify-checks.ts +41 -39
- package/src/verify-e2e.ts +10 -7
- package/src/verify-run.ts +11 -1
- package/src/verify-step.ts +3 -3
- package/src/verify-tests.ts +1 -26
- package/src/verify-typecheck.ts +28 -0
- package/src/web-binding.ts +2 -2
- package/src/workspace-graph.ts +10 -33
- package/src/cdp-browser.ts +0 -94
- package/src/cdp-connection.ts +0 -247
- package/src/cdp-e2e-page.ts +0 -180
- package/src/cdp-e2e-session.ts +0 -199
- package/src/cdp-errors.ts +0 -56
- package/src/cdp-launch.ts +0 -193
- package/src/cdp-offline-script.ts +0 -73
- package/src/cdp-pipe.ts +0 -77
- package/src/e2e-app.ts +0 -103
- package/src/e2e-browser-handle.ts +0 -55
- package/src/e2e-dom-fixture.ts +0 -117
- package/src/e2e-driver.ts +0 -117
- package/src/e2e-errors.ts +0 -117
- package/src/e2e-evaluate.ts +0 -156
- package/src/e2e-locator.ts +0 -86
- package/src/e2e-page.ts +0 -153
- package/src/e2e-preload.ts +0 -64
- package/src/e2e-probe.ts +0 -23
- package/src/e2e-selection.ts +0 -182
- package/src/e2e-spawn.ts +0 -169
- package/src/measurement-actor.ts +0 -26
- /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
- /package/src/{dev-render.ts → runtime-render.ts} +0 -0
|
@@ -35,6 +35,7 @@ const devActor = (
|
|
|
35
35
|
// Everything downstream — pages, policies, live subscribers, MCP tools — reads what this returns.
|
|
36
36
|
import { type Actor, logger, tryResolveEnvironment } from '@ultimat3/core';
|
|
37
37
|
import { configureAuthenticator, readCookie } from '@ultimat3/http';
|
|
38
|
+
import { DEMO_ORG_ID } from '../../shared/demo-org';
|
|
38
39
|
|
|
39
40
|
/** Set it to a role from \`apps/web/shared/roles.ts\` to browse as that role. */
|
|
40
41
|
export const DEV_ROLE_COOKIE = '${app.kebab}_dev_role';
|
|
@@ -66,7 +67,8 @@ export const devRoleFrom = (cookieHeader: string | null): DevRole => {
|
|
|
66
67
|
export const devActorFor = (role: DevRole): Actor => ({
|
|
67
68
|
kind: 'user',
|
|
68
69
|
id: 'dev-actor',
|
|
69
|
-
|
|
70
|
+
// The seed's org, never an invented string: a generated tenant policy compares it with a uuid.
|
|
71
|
+
orgId: DEMO_ORG_ID,
|
|
70
72
|
roles: [role],
|
|
71
73
|
// Both required, and both deliberately empty: \`scopes\` is the framework's own escape hatch
|
|
72
74
|
// (\`tenancy:cross\`) and \`permissions\` is a DIRECT grant that bypasses the role map — a
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// The production topology `x new` writes: one compose service per role, one image. Split from
|
|
2
|
+
// scaffold-container.ts, which holds the image and the page that explains it; this file is what
|
|
3
|
+
// `x deploy --method compose` runs, and the single-node rung of the scale ladder.
|
|
4
|
+
|
|
5
|
+
import type { NameSet } from './naming';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The production env file, relative to the app root. Named once because two readers must agree:
|
|
9
|
+
* the compose file's \`env_file:\` (what the containers see) and \`x deploy\`'s \`--env-file\` (what
|
|
10
|
+
* compose interpolates \`\${VAR:?…}\` from). Two spellings would let them drift apart silently.
|
|
11
|
+
*/
|
|
12
|
+
export const PROD_ENV_FILE = '.env.production';
|
|
13
|
+
|
|
14
|
+
/** The production topology `x new` writes at `docker/docker-compose.prod.yml`. */
|
|
15
|
+
export const composeProdFile = (
|
|
16
|
+
app: NameSet,
|
|
17
|
+
): string => `# The production topology: one service per role, one image, differing only by ROLE and replicas.
|
|
18
|
+
# What \`x deploy --method compose\` runs. migrate runs to completion before anything serves.
|
|
19
|
+
#
|
|
20
|
+
# IMAGE=ghcr.io/you/${app.kebab}:1.2.3 x deploy --image ghcr.io/you/${app.kebab}:1.2.3
|
|
21
|
+
#
|
|
22
|
+
# By hand, always with \`--env-file ${PROD_ENV_FILE}\`: Compose fills \`\${VAR:?…}\` below from the shell
|
|
23
|
+
# and \`--env-file\` only, NEVER from \`env_file:\`, so without it a value set only in that file is
|
|
24
|
+
# "missing" and the parse fails. \`x deploy\` passes it on every step.
|
|
25
|
+
#
|
|
26
|
+
# docker compose --env-file ${PROD_ENV_FILE} -f docker/docker-compose.prod.yml up -d
|
|
27
|
+
#
|
|
28
|
+
# A published host port has exactly one binder, so \`web\` and \`sync\` run at 1 here. Compose is one
|
|
29
|
+
# box; horizontal scaling of those two belongs to an orchestrator — \`docker/helm\`, beside this
|
|
30
|
+
# file, is the chart \`x deploy --method helm\` installs. To scale them on one box anyway, drop
|
|
31
|
+
# \`ports:\` and put your own proxy on this network — the service name resolves to every replica
|
|
32
|
+
# over the compose DNS round robin.
|
|
33
|
+
name: ${app.kebab}
|
|
34
|
+
|
|
35
|
+
# Every service's logs, rotated. Docker's default json-file driver never rotates, so a chatty role
|
|
36
|
+
# on a long-lived box fills the disk the database lives on; 3 x 10 MB per container is the ceiling.
|
|
37
|
+
x-logging: &logging
|
|
38
|
+
driver: json-file
|
|
39
|
+
options: { max-size: 10m, max-file: '3' }
|
|
40
|
+
|
|
41
|
+
x-image: &image
|
|
42
|
+
image: \${IMAGE:-${app.kebab}:dev}
|
|
43
|
+
env_file: [../${PROD_ENV_FILE}]
|
|
44
|
+
restart: unless-stopped
|
|
45
|
+
logging: *logging
|
|
46
|
+
# SIGTERM → /readyz answers 503 for the readiness grace (5s outside local environments), then the
|
|
47
|
+
# drain of in-flight requests, jobs and sockets (25s). 40s leaves 10s before Docker SIGKILLs.
|
|
48
|
+
stop_grace_period: 40s
|
|
49
|
+
# What the chart's securityContext applies, on the rung that had none: a read-only root
|
|
50
|
+
# filesystem, no Linux capabilities (nothing here binds below 1024), no setuid escalation, and a
|
|
51
|
+
# memory ceiling so one runaway role takes down its container rather than the box. /tmp and .x/
|
|
52
|
+
# are tmpfs — .x/ is where a still-embedded binding keeps state, which production never relies on.
|
|
53
|
+
read_only: true
|
|
54
|
+
tmpfs: [/tmp, /app/.x]
|
|
55
|
+
cap_drop: [ALL]
|
|
56
|
+
security_opt: ['no-new-privileges:true']
|
|
57
|
+
mem_limit: 1g
|
|
58
|
+
depends_on:
|
|
59
|
+
db: { condition: service_healthy }
|
|
60
|
+
|
|
61
|
+
# The image's own HEALTHCHECK fetches \`/readyz\` on $PORT, and only \`web\` and \`sync\` open an HTTP
|
|
62
|
+
# socket — every other role gets the scrape listener and nothing else. A service that inherits that
|
|
63
|
+
# probe is fetching a port it never binds: it reports \`unhealthy\` for its whole life and anything
|
|
64
|
+
# gated on it never starts. Probes follow the role here, exactly as they do in \`docker/helm\`.
|
|
65
|
+
x-metrics-probe: &metrics-probe
|
|
66
|
+
test: ['CMD', 'bun', '--eval', "fetch('http://127.0.0.1:'+(process.env.METRICS_PORT||9090)+'/metrics').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
|
67
|
+
interval: 10s
|
|
68
|
+
timeout: 3s
|
|
69
|
+
start_period: 30s
|
|
70
|
+
retries: 3
|
|
71
|
+
|
|
72
|
+
# Run-once services exit. A probe against an exited container reports \`unhealthy\` forever, and
|
|
73
|
+
# nothing waits on their health — \`service_completed_successfully\` is what the others gate on.
|
|
74
|
+
x-run-once-probe: &run-once-probe
|
|
75
|
+
disable: true
|
|
76
|
+
|
|
77
|
+
services:
|
|
78
|
+
db:
|
|
79
|
+
image: postgres:17-alpine
|
|
80
|
+
environment:
|
|
81
|
+
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
|
|
82
|
+
POSTGRES_DB: ${app.kebab}
|
|
83
|
+
volumes: ['pgdata:/var/lib/postgresql/data']
|
|
84
|
+
healthcheck:
|
|
85
|
+
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
|
86
|
+
interval: 5s
|
|
87
|
+
restart: unless-stopped
|
|
88
|
+
logging: *logging
|
|
89
|
+
|
|
90
|
+
# The release phase. Applies pending migrations under an advisory lock and exits; every serving
|
|
91
|
+
# role waits for it to complete, so no replica ever serves against a schema it does not ship.
|
|
92
|
+
migrate:
|
|
93
|
+
<<: *image
|
|
94
|
+
environment: [ROLE=migrate]
|
|
95
|
+
restart: 'no'
|
|
96
|
+
healthcheck: *run-once-probe
|
|
97
|
+
|
|
98
|
+
# Run-once, AFTER the new version serves. Deliberately NOT part of the release gate: a slow
|
|
99
|
+
# UPDATE there holds the deploy open against a database still serving the previous version.
|
|
100
|
+
# Dry run is the default, so \`--write\` is explicit.
|
|
101
|
+
backfill:
|
|
102
|
+
<<: *image
|
|
103
|
+
# The image's ENTRYPOINT is \`bun apps/web/server.ts\`, and that entry reads ROLE and PORT and
|
|
104
|
+
# NOTHING ELSE — argv never reaches a parser. A bare \`command:\` is appended to it and silently
|
|
105
|
+
# discarded, so this service used to serve HTTP as ROLE=web under a name that said otherwise.
|
|
106
|
+
# Overriding the entrypoint is what makes the words below a command. The file path, not
|
|
107
|
+
# \`node_modules/.bin/x\`: it needs no bin symlink and no executable bit inside the image.
|
|
108
|
+
entrypoint: ['bun', 'node_modules/@ultimat3/cli/src/bin.ts']
|
|
109
|
+
command: ['db', 'backfill', '--all', '--write', '--json']
|
|
110
|
+
depends_on:
|
|
111
|
+
db: { condition: service_healthy }
|
|
112
|
+
migrate: { condition: service_completed_successfully }
|
|
113
|
+
# The barrier, not the ordering. \`docker compose up -d\` returns when a container STARTS, so
|
|
114
|
+
# listing this last would only look like "after". The image's HEALTHCHECK is what makes it true.
|
|
115
|
+
web: { condition: service_healthy }
|
|
116
|
+
restart: 'no'
|
|
117
|
+
healthcheck: *run-once-probe
|
|
118
|
+
|
|
119
|
+
web:
|
|
120
|
+
<<: *image
|
|
121
|
+
# The page dials SYNC_URL; unset, it dials /_x/sync on :3000, which web does not serve here.
|
|
122
|
+
# Set it in ${PROD_ENV_FILE}: \`--env-file\` (header) is what lets this line read it there.
|
|
123
|
+
environment: [ROLE=web, 'SYNC_URL=\${SYNC_URL:?set SYNC_URL=ws://<host>:3001/_x/sync, see wiki/Deployment.md}']
|
|
124
|
+
depends_on:
|
|
125
|
+
db: { condition: service_healthy }
|
|
126
|
+
migrate: { condition: service_completed_successfully }
|
|
127
|
+
deploy: { replicas: 1 } # stateless, scales on RPS — pinned by the published port
|
|
128
|
+
ports: ['3000:3000']
|
|
129
|
+
|
|
130
|
+
sync:
|
|
131
|
+
<<: *image
|
|
132
|
+
environment: [ROLE=sync]
|
|
133
|
+
depends_on:
|
|
134
|
+
db: { condition: service_healthy }
|
|
135
|
+
migrate: { condition: service_completed_successfully }
|
|
136
|
+
# OFF by default (zero replicas). A sync node on this database hears committed changes only
|
|
137
|
+
# from a replicator it can reach, and this file runs none: booted anyway it is refused
|
|
138
|
+
# (X_REALTIME_TOPOLOGY). To turn live queries and channels on: declare realtime: { enabled:
|
|
139
|
+
# true, transport: 'nats', urlEnv: 'NATS_URL' } in app.config.ts, add a NATS service and set the
|
|
140
|
+
# SAME NATS_URL for web, sync and a replicator service (ROLE=replicator, exactly one), start
|
|
141
|
+
# Postgres with wal_level=logical and a REPLICATION role on a cluster dedicated to this app (the replicator creates its publication at boot), then set replicas: 1.
|
|
142
|
+
# A NATS_URL under transport 'memory' refuses the boot (X_CONFIG_INVALID), and so does 'nats'
|
|
143
|
+
# without one.
|
|
144
|
+
deploy: { replicas: 0 } # when on: scales on concurrent websockets — pinned to 1 by the port
|
|
145
|
+
# The sync role binds PORT + 1. PORT is unset here, so it is 3000 and this listens on 3001.
|
|
146
|
+
ports: ['3001:3001']
|
|
147
|
+
# ...which is why the image's own HEALTHCHECK cannot be inherited here. It fetches $PORT —
|
|
148
|
+
# 3000 — and this role never binds it, so the container reports \`unhealthy\` from
|
|
149
|
+
# \`start_period\` onward and never recovers, and anything gated on \`sync: service_healthy\`
|
|
150
|
+
# would never start. Literal 3001 rather than an expression, for the same reason \`ports:\`
|
|
151
|
+
# above is literal: PORT is unset in this file, and two ways of saying one number drift.
|
|
152
|
+
# \`docker/helm\` states the same rule as \`PORT = .port - 1\`.
|
|
153
|
+
healthcheck:
|
|
154
|
+
test: ['CMD', 'bun', '--eval', "fetch('http://127.0.0.1:3001/readyz').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
|
155
|
+
interval: 10s
|
|
156
|
+
timeout: 3s
|
|
157
|
+
start_period: 30s
|
|
158
|
+
retries: 3
|
|
159
|
+
|
|
160
|
+
worker:
|
|
161
|
+
<<: *image
|
|
162
|
+
environment: [ROLE=worker]
|
|
163
|
+
depends_on:
|
|
164
|
+
db: { condition: service_healthy }
|
|
165
|
+
migrate: { condition: service_completed_successfully }
|
|
166
|
+
healthcheck: *metrics-probe
|
|
167
|
+
deploy: { replicas: 1 } # scales on queue depth
|
|
168
|
+
|
|
169
|
+
scheduler:
|
|
170
|
+
<<: *image
|
|
171
|
+
environment: [ROLE=scheduler]
|
|
172
|
+
depends_on:
|
|
173
|
+
db: { condition: service_healthy }
|
|
174
|
+
migrate: { condition: service_completed_successfully }
|
|
175
|
+
# Fixed 1. Leadership is an EXPIRING LEASE ROW in \`x_scheduler_leader\` (role-start.ts,
|
|
176
|
+
# driver-pg-ddl.ts), NOT an advisory lock: that grant belongs to the session, not to the
|
|
177
|
+
# process — it outlives every transaction and no pooled node can renew it or prove it still
|
|
178
|
+
# holds one. A second instance is harmless but idle.
|
|
179
|
+
healthcheck: *metrics-probe
|
|
180
|
+
deploy: { replicas: 1 }
|
|
181
|
+
|
|
182
|
+
volumes:
|
|
183
|
+
pgdata:
|
|
184
|
+
`;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { SECRETS_KEY_FILE } from '@ultimat3/core';
|
|
10
10
|
import type { GeneratedFile, NameSet } from './naming';
|
|
11
|
+
import { composeProdFile, PROD_ENV_FILE } from './scaffold-container-compose';
|
|
11
12
|
import { helmFiles } from './scaffold-helm';
|
|
12
13
|
|
|
13
14
|
const dockerfile = (
|
|
@@ -21,13 +22,24 @@ const dockerfile = (
|
|
|
21
22
|
#
|
|
22
23
|
# syntax=docker/dockerfile:1
|
|
23
24
|
|
|
24
|
-
# ----------
|
|
25
|
+
# ---------- manifests: every workspace package.json, and nothing else ----------
|
|
26
|
+
# The install below must be keyed on the MANIFESTS, not on the source: copying \`apps\` and
|
|
27
|
+
# \`packages\` ahead of \`bun install\` made every edit to a page a full reinstall. The set is derived
|
|
28
|
+
# from the context rather than listed, so a workspace glob added later is not a lockfile the frozen
|
|
29
|
+
# install refuses. \`bunfig.toml\` survives because its \`[install]\` section is input to the install,
|
|
30
|
+
# and so does a \`.tgz\`: a \`file:\` tarball dependency (a vendored or private package) is resolved
|
|
31
|
+
# from the file itself, and without it the frozen install cannot find what the lockfile names.
|
|
32
|
+
FROM oven/bun:1.4-alpine AS manifests
|
|
33
|
+
WORKDIR /app
|
|
34
|
+
COPY . .
|
|
35
|
+
RUN find . ! -type d ! -name package.json ! -name bunfig.toml ! -name '*.tgz' -delete \\
|
|
36
|
+
&& find . -type d -empty -delete
|
|
37
|
+
|
|
38
|
+
# ---------- deps: runtime dependencies only, cached on the lockfile and the manifests ----------
|
|
25
39
|
FROM oven/bun:1.4-alpine AS deps
|
|
26
40
|
WORKDIR /app
|
|
27
|
-
COPY
|
|
28
|
-
|
|
29
|
-
COPY apps ./apps
|
|
30
|
-
COPY packages ./packages
|
|
41
|
+
COPY --from=manifests /app ./
|
|
42
|
+
COPY bun.lock ./
|
|
31
43
|
RUN bun install --frozen-lockfile --production
|
|
32
44
|
|
|
33
45
|
# ---------- runtime ----------
|
|
@@ -35,9 +47,13 @@ RUN bun install --frozen-lockfile --production
|
|
|
35
47
|
# before it ever calls \`docker build\`. Re-running typecheck and lint here would need the
|
|
36
48
|
# devDependencies the \`--production\` install above deliberately leaves out — which is exactly how
|
|
37
49
|
# a build stage came to run \`tsc\` and \`biome\` against a tree that had neither.
|
|
38
|
-
|
|
50
|
+
#
|
|
51
|
+
# Built ON the install stage, so the WHOLE installed tree comes with it — Bun's isolated linker
|
|
52
|
+
# links a workspace's dependencies under that workspace's own \`node_modules\`, and copying only the
|
|
53
|
+
# root one left \`apps/web\`'s behind — and the source lands on top (the ignore file drops every host
|
|
54
|
+
# \`node_modules\`, so the two merge). The layer cache is the same as a COPY from it would be.
|
|
55
|
+
FROM deps AS runtime
|
|
39
56
|
WORKDIR /app
|
|
40
|
-
COPY --from=deps /app/node_modules ./node_modules
|
|
41
57
|
COPY . .
|
|
42
58
|
|
|
43
59
|
# The immutable content hash this image serves. Stamped by CI (\`--build-arg BUILD_ID=$(git rev-parse HEAD)\`);
|
|
@@ -112,157 +128,6 @@ coverage
|
|
|
112
128
|
**/playwright-report
|
|
113
129
|
`;
|
|
114
130
|
|
|
115
|
-
/**
|
|
116
|
-
* The production env file, relative to the app root. Named once because two readers must agree:
|
|
117
|
-
* the compose file's \`env_file:\` (what the containers see) and \`x deploy\`'s \`--env-file\` (what
|
|
118
|
-
* compose interpolates \`\${VAR:?…}\` from). Two spellings would let them drift apart silently.
|
|
119
|
-
*/
|
|
120
|
-
export const PROD_ENV_FILE = '.env.production';
|
|
121
|
-
|
|
122
|
-
const composeProd = (
|
|
123
|
-
app: NameSet,
|
|
124
|
-
): string => `# The production topology: one service per role, one image, differing only by ROLE and replicas.
|
|
125
|
-
# What \`x deploy --method compose\` runs. migrate runs to completion before anything serves.
|
|
126
|
-
#
|
|
127
|
-
# IMAGE=ghcr.io/you/${app.kebab}:1.2.3 x deploy --image ghcr.io/you/${app.kebab}:1.2.3
|
|
128
|
-
#
|
|
129
|
-
# By hand, always with \`--env-file ${PROD_ENV_FILE}\`: Compose fills \`\${VAR:?…}\` below from the shell
|
|
130
|
-
# and \`--env-file\` only, NEVER from \`env_file:\`, so without it a value set only in that file is
|
|
131
|
-
# "missing" and the parse fails. \`x deploy\` passes it on every step.
|
|
132
|
-
#
|
|
133
|
-
# docker compose --env-file ${PROD_ENV_FILE} -f docker/docker-compose.prod.yml up -d
|
|
134
|
-
#
|
|
135
|
-
# A published host port has exactly one binder, so \`web\` and \`sync\` run at 1 here. Compose is one
|
|
136
|
-
# box; horizontal scaling of those two belongs to an orchestrator — \`docker/helm\`, beside this
|
|
137
|
-
# file, is the chart \`x deploy --method helm\` installs. To scale them on one box anyway, drop
|
|
138
|
-
# \`ports:\` and put your own proxy on this network — the service name resolves to every replica
|
|
139
|
-
# over the compose DNS round robin.
|
|
140
|
-
name: ${app.kebab}
|
|
141
|
-
|
|
142
|
-
x-image: &image
|
|
143
|
-
image: \${IMAGE:-${app.kebab}:dev}
|
|
144
|
-
env_file: [../${PROD_ENV_FILE}]
|
|
145
|
-
restart: unless-stopped
|
|
146
|
-
stop_grace_period: 30s # SIGTERM → drain in-flight requests, jobs and sockets
|
|
147
|
-
depends_on:
|
|
148
|
-
db: { condition: service_healthy }
|
|
149
|
-
|
|
150
|
-
# The image's own HEALTHCHECK fetches \`/readyz\` on $PORT, and only \`web\` and \`sync\` open an HTTP
|
|
151
|
-
# socket — every other role gets the scrape listener and nothing else. A service that inherits that
|
|
152
|
-
# probe is fetching a port it never binds: it reports \`unhealthy\` for its whole life and anything
|
|
153
|
-
# gated on it never starts. Probes follow the role here, exactly as they do in \`docker/helm\`.
|
|
154
|
-
x-metrics-probe: &metrics-probe
|
|
155
|
-
test: ['CMD', 'bun', '--eval', "fetch('http://127.0.0.1:'+(process.env.METRICS_PORT||9090)+'/metrics').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
|
156
|
-
interval: 10s
|
|
157
|
-
timeout: 3s
|
|
158
|
-
start_period: 30s
|
|
159
|
-
retries: 3
|
|
160
|
-
|
|
161
|
-
# Run-once services exit. A probe against an exited container reports \`unhealthy\` forever, and
|
|
162
|
-
# nothing waits on their health — \`service_completed_successfully\` is what the others gate on.
|
|
163
|
-
x-run-once-probe: &run-once-probe
|
|
164
|
-
disable: true
|
|
165
|
-
|
|
166
|
-
services:
|
|
167
|
-
db:
|
|
168
|
-
image: postgres:17-alpine
|
|
169
|
-
environment:
|
|
170
|
-
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
|
|
171
|
-
POSTGRES_DB: ${app.kebab}
|
|
172
|
-
volumes: ['pgdata:/var/lib/postgresql/data']
|
|
173
|
-
healthcheck:
|
|
174
|
-
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
|
175
|
-
interval: 5s
|
|
176
|
-
restart: unless-stopped
|
|
177
|
-
|
|
178
|
-
# The release phase. Applies pending migrations under an advisory lock and exits; every serving
|
|
179
|
-
# role waits for it to complete, so no replica ever serves against a schema it does not ship.
|
|
180
|
-
migrate:
|
|
181
|
-
<<: *image
|
|
182
|
-
environment: [ROLE=migrate]
|
|
183
|
-
restart: 'no'
|
|
184
|
-
healthcheck: *run-once-probe
|
|
185
|
-
|
|
186
|
-
# Run-once, AFTER the new version serves. Deliberately NOT part of the release gate: a slow
|
|
187
|
-
# UPDATE there holds the deploy open against a database still serving the previous version.
|
|
188
|
-
# Dry run is the default, so \`--write\` is explicit.
|
|
189
|
-
backfill:
|
|
190
|
-
<<: *image
|
|
191
|
-
# The image's ENTRYPOINT is \`bun apps/web/server.ts\`, and that entry reads ROLE and PORT and
|
|
192
|
-
# NOTHING ELSE — argv never reaches a parser. A bare \`command:\` is appended to it and silently
|
|
193
|
-
# discarded, so this service used to serve HTTP as ROLE=web under a name that said otherwise.
|
|
194
|
-
# Overriding the entrypoint is what makes the words below a command. The file path, not
|
|
195
|
-
# \`node_modules/.bin/x\`: it needs no bin symlink and no executable bit inside the image.
|
|
196
|
-
entrypoint: ['bun', 'node_modules/@ultimat3/cli/src/bin.ts']
|
|
197
|
-
command: ['db', 'backfill', '--all', '--write', '--json']
|
|
198
|
-
depends_on:
|
|
199
|
-
db: { condition: service_healthy }
|
|
200
|
-
migrate: { condition: service_completed_successfully }
|
|
201
|
-
# The barrier, not the ordering. \`docker compose up -d\` returns when a container STARTS, so
|
|
202
|
-
# listing this last would only look like "after". The image's HEALTHCHECK is what makes it true.
|
|
203
|
-
web: { condition: service_healthy }
|
|
204
|
-
restart: 'no'
|
|
205
|
-
healthcheck: *run-once-probe
|
|
206
|
-
|
|
207
|
-
web:
|
|
208
|
-
<<: *image
|
|
209
|
-
# The page dials SYNC_URL; unset, it dials /_x/sync on :3000, which web does not serve here.
|
|
210
|
-
# Set it in ${PROD_ENV_FILE}: \`--env-file\` (header) is what lets this line read it there.
|
|
211
|
-
environment: [ROLE=web, 'SYNC_URL=\${SYNC_URL:?set SYNC_URL=ws://<host>:3001/_x/sync, see wiki/Deployment.md}']
|
|
212
|
-
depends_on:
|
|
213
|
-
db: { condition: service_healthy }
|
|
214
|
-
migrate: { condition: service_completed_successfully }
|
|
215
|
-
deploy: { replicas: 1 } # stateless, scales on RPS — pinned by the published port
|
|
216
|
-
ports: ['3000:3000']
|
|
217
|
-
|
|
218
|
-
sync:
|
|
219
|
-
<<: *image
|
|
220
|
-
environment: [ROLE=sync]
|
|
221
|
-
depends_on:
|
|
222
|
-
db: { condition: service_healthy }
|
|
223
|
-
migrate: { condition: service_completed_successfully }
|
|
224
|
-
deploy: { replicas: 1 } # scales on concurrent websockets, no sticky sessions — pinned by the port
|
|
225
|
-
# The sync role binds PORT + 1. PORT is unset here, so it is 3000 and this listens on 3001.
|
|
226
|
-
ports: ['3001:3001']
|
|
227
|
-
# ...which is why the image's own HEALTHCHECK cannot be inherited here. It fetches $PORT —
|
|
228
|
-
# 3000 — and this role never binds it, so the container reports \`unhealthy\` from
|
|
229
|
-
# \`start_period\` onward and never recovers, and anything gated on \`sync: service_healthy\`
|
|
230
|
-
# would never start. Literal 3001 rather than an expression, for the same reason \`ports:\`
|
|
231
|
-
# above is literal: PORT is unset in this file, and two ways of saying one number drift.
|
|
232
|
-
# \`docker/helm\` states the same rule as \`PORT = .port - 1\`.
|
|
233
|
-
healthcheck:
|
|
234
|
-
test: ['CMD', 'bun', '--eval', "fetch('http://127.0.0.1:3001/readyz').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
|
235
|
-
interval: 10s
|
|
236
|
-
timeout: 3s
|
|
237
|
-
start_period: 30s
|
|
238
|
-
retries: 3
|
|
239
|
-
|
|
240
|
-
worker:
|
|
241
|
-
<<: *image
|
|
242
|
-
environment: [ROLE=worker]
|
|
243
|
-
depends_on:
|
|
244
|
-
db: { condition: service_healthy }
|
|
245
|
-
migrate: { condition: service_completed_successfully }
|
|
246
|
-
healthcheck: *metrics-probe
|
|
247
|
-
deploy: { replicas: 1 } # scales on queue depth
|
|
248
|
-
|
|
249
|
-
scheduler:
|
|
250
|
-
<<: *image
|
|
251
|
-
environment: [ROLE=scheduler]
|
|
252
|
-
depends_on:
|
|
253
|
-
db: { condition: service_healthy }
|
|
254
|
-
migrate: { condition: service_completed_successfully }
|
|
255
|
-
# Fixed 1. Leadership is an EXPIRING LEASE ROW in \`x_scheduler_leader\` (dev-roles.ts,
|
|
256
|
-
# driver-pg-ddl.ts), NOT an advisory lock: that grant belongs to the session, not to the
|
|
257
|
-
# process — it outlives every transaction and no pooled node can renew it or prove it still
|
|
258
|
-
# holds one. A second instance is harmless but idle.
|
|
259
|
-
healthcheck: *metrics-probe
|
|
260
|
-
deploy: { replicas: 1 }
|
|
261
|
-
|
|
262
|
-
volumes:
|
|
263
|
-
pgdata:
|
|
264
|
-
`;
|
|
265
|
-
|
|
266
131
|
const readme = (app: NameSet): string => `# docker
|
|
267
132
|
|
|
268
133
|
One image, every role. \`ROLE\` selects behaviour at start, so there is one artifact to promote and
|
|
@@ -398,7 +263,7 @@ export function containerFiles(app: NameSet): readonly GeneratedFile[] {
|
|
|
398
263
|
return [
|
|
399
264
|
{ path: 'docker/Dockerfile', contents: dockerfile(app) },
|
|
400
265
|
{ path: 'docker/Dockerfile.dockerignore', contents: dockerignore() },
|
|
401
|
-
{ path: 'docker/docker-compose.prod.yml', contents:
|
|
266
|
+
{ path: 'docker/docker-compose.prod.yml', contents: composeProdFile(app) },
|
|
402
267
|
{ path: 'docker/README.md', contents: readme(app) },
|
|
403
268
|
// The other deploy method's topology, on the same terms as the compose file above: `x deploy`
|
|
404
269
|
// is one `helm upgrade --install` against this directory, and a chart that shipped in no npm
|
|
@@ -19,7 +19,6 @@ const examplePage = (
|
|
|
19
19
|
${sortedImports([
|
|
20
20
|
`import { useT } from '@${app.kebab}/i18n';`,
|
|
21
21
|
"import { isUltimateError } from '@ultimat3/core';",
|
|
22
|
-
"import { seedId } from '@ultimat3/entity';",
|
|
23
22
|
"import { currentLocale } from '@ultimat3/i18n';",
|
|
24
23
|
"import { defineRoute, island } from '@ultimat3/render';",
|
|
25
24
|
[
|
|
@@ -35,6 +34,7 @@ ${sortedImports([
|
|
|
35
34
|
"} from '@ultimat3/ui';",
|
|
36
35
|
].join('\n'),
|
|
37
36
|
])}
|
|
37
|
+
import { DEMO_ORG_ID } from '../../shared/demo-org';
|
|
38
38
|
import { Shell } from '../../shared/shell';
|
|
39
39
|
import * as repo from '../post/repo';
|
|
40
40
|
import {
|
|
@@ -53,7 +53,7 @@ import styles from './page.module.scss';
|
|
|
53
53
|
* one \`packages/db/src/seed.ts\` writes. The day sessions exist, this becomes the actor's org and
|
|
54
54
|
* the read becomes \`postList.as(actor, …)\` — the query already declares the tenancy rule.
|
|
55
55
|
*/
|
|
56
|
-
const DEMO_ORG =
|
|
56
|
+
const DEMO_ORG = DEMO_ORG_ID;
|
|
57
57
|
|
|
58
58
|
/** The stat row counts what it can see. Past this many posts, write an aggregate query. */
|
|
59
59
|
const ROW_LIMIT = 500;
|
|
@@ -94,6 +94,19 @@ export {};
|
|
|
94
94
|
*
|
|
95
95
|
* `x db seed` owns the connection, the tier and the per-seed transaction. One runner, one answer.
|
|
96
96
|
*/
|
|
97
|
+
/**
|
|
98
|
+
* The seed's two package imports in the order Biome sorts them: the app's own scope can sort on
|
|
99
|
+
* either side of \`@ultimat3\`, and a fixed order is a lint failure for half of all app names.
|
|
100
|
+
*/
|
|
101
|
+
const seedImports = (app: NameSet): string =>
|
|
102
|
+
[
|
|
103
|
+
[`@${app.kebab}/web/shared/demo-org`, 'DEMO_ORG_LABEL'],
|
|
104
|
+
['@ultimat3/entity', 'defineSeed'],
|
|
105
|
+
]
|
|
106
|
+
.sort(([a = ''], [b = '']) => (a < b ? -1 : a > b ? 1 : 0))
|
|
107
|
+
.map(([from, name]) => `import { ${name} } from '${from}';`)
|
|
108
|
+
.join('\n');
|
|
109
|
+
|
|
97
110
|
const dbSeed = (app: NameSet, example: boolean): string =>
|
|
98
111
|
example
|
|
99
112
|
? `// Deterministic fixtures: the same rows every time, so a test, a demo and a branch database
|
|
@@ -104,7 +117,7 @@ const dbSeed = (app: NameSet, example: boolean): string =>
|
|
|
104
117
|
// included), and wraps each seed in its own transaction. Never a plain \`bun run\` script: that
|
|
105
118
|
// reaches the database through \`db()\`, which needs a \`postgres:\` \`DATABASE_URL\` and so cannot
|
|
106
119
|
// see the embedded database at all.
|
|
107
|
-
|
|
120
|
+
${seedImports(app)}
|
|
108
121
|
import { post } from './schema';
|
|
109
122
|
|
|
110
123
|
/** Stable across runs: \`id('post:hello')\` is a UUID v5 of the label, not a random one. */
|
|
@@ -112,13 +125,13 @@ export const ${app.camel}Seed = defineSeed('${app.kebab}', async ({ insert, id }
|
|
|
112
125
|
await insert(post, [
|
|
113
126
|
{
|
|
114
127
|
id: id('post:hello'),
|
|
115
|
-
orgId: id(
|
|
128
|
+
orgId: id(DEMO_ORG_LABEL),
|
|
116
129
|
title: 'Hello ${app.pascal}',
|
|
117
130
|
price: { minor: 0, currency: 'USD' },
|
|
118
131
|
},
|
|
119
132
|
{
|
|
120
133
|
id: id('post:second'),
|
|
121
|
-
orgId: id(
|
|
134
|
+
orgId: id(DEMO_ORG_LABEL),
|
|
122
135
|
title: 'Second post',
|
|
123
136
|
price: { minor: 1900, currency: 'USD' },
|
|
124
137
|
},
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// `apps/web/shared/demo-org.ts`: the ONE org this app has until it issues sessions. The dev actor
|
|
2
|
+
// named `'dev-org'`, the seed wrote `id('org:demo')` and the dashboard computed a third copy — so a
|
|
3
|
+
// generated tenant policy (`actor.orgId === input.orgId`, a `t.uuid`) could never pass for the dev
|
|
4
|
+
// actor, and every generated write answered 403 under a green gate.
|
|
5
|
+
|
|
6
|
+
import type { GeneratedFile } from './naming';
|
|
7
|
+
|
|
8
|
+
const demoOrg =
|
|
9
|
+
(): string => `// The org every development-only fact belongs to: the dev actor's \`orgId\`, the org the seed
|
|
10
|
+
// writes its rows under, and the org the dashboard aggregates. One constant, so the three cannot
|
|
11
|
+
// disagree — a policy deciding \`actor.orgId === input.orgId\` passes for the dev actor on the rows
|
|
12
|
+
// the seed wrote. Replace its readers when this app issues real sessions.
|
|
13
|
+
import { seedId } from '@ultimat3/entity';
|
|
14
|
+
|
|
15
|
+
/** The label \`defineSeed\`'s \`id()\` derives the uuid from — the same uuid on every run. */
|
|
16
|
+
export const DEMO_ORG_LABEL = 'org:demo';
|
|
17
|
+
|
|
18
|
+
/** \`seedId(DEMO_ORG_LABEL)\`: a uuid v5, exactly what the seed's \`id(DEMO_ORG_LABEL)\` answers. */
|
|
19
|
+
export const DEMO_ORG_ID = seedId(DEMO_ORG_LABEL);
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const demoOrgTest =
|
|
23
|
+
(): string => `// The dev actor, the seed and the dashboard all read DEMO_ORG_ID. If it stops being the uuid the
|
|
24
|
+
// seed's \`id()\` derives, a generated tenant policy denies the dev actor again.
|
|
25
|
+
import { seedId } from '@ultimat3/entity';
|
|
26
|
+
import { expect, unitTest } from '@ultimat3/testing';
|
|
27
|
+
import { DEMO_ORG_ID, DEMO_ORG_LABEL } from './demo-org';
|
|
28
|
+
|
|
29
|
+
const UUID_V5 = /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
30
|
+
|
|
31
|
+
unitTest('the demo org is the uuid the seed writes', () => {
|
|
32
|
+
expect(DEMO_ORG_ID).toBe(seedId(DEMO_ORG_LABEL));
|
|
33
|
+
expect(DEMO_ORG_ID).toMatch(UUID_V5);
|
|
34
|
+
});
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
/** Written by `x new`, with or without the example slice: the dev actor always reads it. */
|
|
38
|
+
export const demoOrgFiles = (): readonly GeneratedFile[] => [
|
|
39
|
+
{ path: 'apps/web/shared/demo-org.ts', contents: demoOrg() },
|
|
40
|
+
{ path: 'apps/web/shared/demo-org.test.ts', contents: demoOrgTest() },
|
|
41
|
+
];
|
|
@@ -15,7 +15,7 @@ const server =
|
|
|
15
15
|
// shares the host's network namespace (\`--network host\`), or through a sidecar and \`ssh -L\`.
|
|
16
16
|
|
|
17
17
|
import { join } from 'node:path';
|
|
18
|
-
import { runRole } from '@ultimat3/cli';
|
|
18
|
+
import { runRole } from '@ultimat3/cli/serve';
|
|
19
19
|
|
|
20
20
|
// MORE THAN ONE REPLICA? Add these two lines, above \`runRole\`:
|
|
21
21
|
//
|
|
@@ -29,6 +29,12 @@ export const SCAFFOLD_ENV_SCHEMA = {
|
|
|
29
29
|
role: 'sync',
|
|
30
30
|
description: 'Realtime fan-out cluster. Only the sync role is asked for it.',
|
|
31
31
|
},
|
|
32
|
+
APP_URL: {
|
|
33
|
+
type: 'url',
|
|
34
|
+
required: false,
|
|
35
|
+
role: ['web', 'sync'],
|
|
36
|
+
description: 'Public origin of the pages. Sync admits sockets from it.',
|
|
37
|
+
},
|
|
32
38
|
SYNC_URL: {
|
|
33
39
|
type: 'string',
|
|
34
40
|
required: false,
|