cyberia 3.2.12 → 3.2.70
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/.env.example +127 -68
- package/.github/workflows/cyberia-client.cd.yml +40 -0
- package/.github/workflows/cyberia-server.cd.yml +40 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +49 -0
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +48 -0
- package/.github/workflows/docker-image.cyberia-server.ci.yml +69 -0
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +69 -0
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +52 -0
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +52 -0
- package/.github/workflows/engine-cyberia.cd.yml +34 -24
- package/.github/workflows/engine-cyberia.ci.yml +27 -5
- package/.github/workflows/ghpkg.ci.yml +89 -1
- package/.github/workflows/gitlab.ci.yml +1 -1
- package/.github/workflows/hardhat.ci.yml +1 -1
- package/.github/workflows/npmpkg.ci.yml +19 -12
- package/.github/workflows/publish.ci.yml +2 -2
- package/.github/workflows/publish.cyberia.ci.yml +5 -16
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -8
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/CHANGELOG.md +451 -1
- package/CLI-HELP.md +1040 -1130
- package/Dockerfile +141 -43
- package/Dockerfile.dev +143 -0
- package/Dockerfile.test +165 -0
- package/README.md +1 -1
- package/baremetal/commission-workflows.json +1 -0
- package/bin/build.js +128 -136
- package/bin/build.template.js +25 -179
- package/bin/cyberia.js +1089 -188
- package/bin/deploy.js +6 -3
- package/bin/index.js +1089 -188
- package/bump.config.js +1 -0
- package/compose.env +131 -0
- package/conf.js +19 -1
- package/deployment.yaml +74 -2
- package/docker-compose.yml +316 -0
- package/hardhat/hardhat.config.js +2 -2
- package/hardhat/package-lock.json +620 -2041
- package/hardhat/package.json +7 -5
- package/hardhat/scripts/deployObjectLayerToken.js +18 -18
- package/hardhat/test/ObjectLayerToken.js +378 -274
- package/ipfs/configure-ipfs.sh +13 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +74 -2
- package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
- package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/mongodb/entrypoint.sh +76 -0
- package/nginx.conf +87 -0
- package/package.json +34 -22
- package/pv-pvc.yaml +32 -0
- package/scripts/disk-clean.sh +85 -60
- package/scripts/kubeadm-node-setup.sh +317 -0
- package/scripts/link-local-underpost-cli.sh +6 -0
- package/scripts/rocky-kickstart.sh +877 -185
- package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
- package/scripts/test-monitor.sh +248 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
- package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
- package/src/api/cyberia-action/cyberia-action.model.js +21 -29
- package/src/api/cyberia-action/cyberia-action.router.js +42 -7
- package/src/api/cyberia-action/cyberia-action.service.js +20 -4
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
- package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
- package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
- package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
- package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
- package/src/api/cyberia-map/cyberia-map.router.js +21 -6
- package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
- package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
- package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
- package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
- package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
- package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
- package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -122
- package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
- package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
- package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
- package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
- package/src/api/ipfs/ipfs.service.js +28 -15
- package/src/api/object-layer/object-layer.router.js +25 -15
- package/src/api/object-layer/object-layer.service.js +15 -20
- package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
- package/src/cli/baremetal.js +717 -16
- package/src/cli/cluster.js +80 -5
- package/src/cli/deploy.js +326 -292
- package/src/cli/docker-compose.js +648 -0
- package/src/cli/env.js +12 -6
- package/src/cli/fs.js +75 -30
- package/src/cli/image.js +187 -55
- package/src/cli/index.js +157 -6
- package/src/cli/kickstart.js +142 -20
- package/src/cli/monitor.js +387 -6
- package/src/cli/release.js +72 -15
- package/src/cli/repository.js +339 -40
- package/src/cli/run.js +678 -186
- package/src/cli/secrets.js +82 -48
- package/src/cli/ssh.js +234 -0
- package/src/cli/static.js +2 -2
- package/src/client/components/core/PanelForm.js +44 -44
- package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
- package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
- package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
- package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
- package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +92 -20
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +272 -50
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
- package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
- package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +28 -12
- package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
- package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +27 -5
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
- package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
- package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
- package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
- package/src/client/services/object-layer/object-layer.management.js +6 -6
- package/src/{server → client-builder}/client-build-docs.js +15 -5
- package/src/{server → client-builder}/client-build-live.js +3 -3
- package/src/{server → client-builder}/client-build.js +25 -22
- package/src/{server → client-builder}/client-dev-server.js +3 -3
- package/src/{server → client-builder}/client-icons.js +2 -2
- package/src/{server → client-builder}/ssr.js +5 -5
- package/src/client.build.js +1 -3
- package/src/client.dev.js +1 -1
- package/src/db/mongo/MongoBootstrap.js +12 -12
- package/src/db/mongo/MongooseDB.js +2 -1
- package/src/grpc/cyberia/grpc-server.js +255 -70
- package/src/index.js +12 -1
- package/src/mailer/EmailRender.js +1 -1
- package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
- package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
- package/src/projects/cyberia/catalog-cyberia.js +81 -0
- package/src/projects/cyberia/gemini-client.js +175 -0
- package/src/projects/cyberia/generate-saga.js +2107 -0
- package/src/{server → projects/cyberia}/ipfs-client.js +6 -4
- package/src/{server → projects/cyberia}/object-layer.js +12 -108
- package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
- package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
- package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
- package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
- package/src/{server → projects/cyberia}/shape-generator.js +2 -2
- package/src/projects/underpost/catalog-underpost.js +60 -0
- package/src/runtime/cyberia-client/Dockerfile +31 -83
- package/src/runtime/cyberia-client/Dockerfile.dev +23 -30
- package/src/runtime/cyberia-server/Dockerfile +23 -42
- package/src/runtime/cyberia-server/Dockerfile.dev +20 -35
- package/src/runtime/engine-cyberia/Dockerfile +143 -0
- package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
- package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
- package/src/runtime/engine-cyberia/compose.env +131 -0
- package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
- package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
- package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
- package/src/runtime/engine-cyberia/nginx.conf +87 -0
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/nginx/Nginx.js +250 -0
- package/src/runtime/wp/Dockerfile +3 -3
- package/src/server/catalog.js +72 -0
- package/src/server/conf.js +415 -60
- package/src/server/runtime-status.js +252 -0
- package/src/server/start.js +42 -11
- package/src/server.js +6 -2
- package/test/cyberia-instance-conf-defaults.test.js +140 -0
- package/test/deploy-monitor.test.js +267 -0
- package/test/shape-generator.test.js +7 -1
- package/typedoc.dd-cyberia.json +3 -1
- package/typedoc.json +3 -1
- package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
- package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
- /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
- /package/src/{server → client-builder}/client-formatted.js +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime status contract and the in-pod internal status endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the Underpost runtime readiness signal (Phase 2 of
|
|
5
|
+
* the two-phase deployment monitor). The runtime publishes its lifecycle here;
|
|
6
|
+
* the CD-side monitor (`src/cli/monitor.js`) reads it over HTTP via
|
|
7
|
+
* `kubectl port-forward`. Kubernetes pod readiness (Phase 1) is owned by kubelet
|
|
8
|
+
* and is intentionally not modeled in this module.
|
|
9
|
+
*
|
|
10
|
+
* Cross-process contract:
|
|
11
|
+
* - In-pod, the canonical value lives in the underpost root env key
|
|
12
|
+
* `container-status`, written by `start.js`. For non-error phases it carries
|
|
13
|
+
* the namespaced form `<deployId>-<env>-<phase>`; a fatal fault collapses to
|
|
14
|
+
* the bare value `error`.
|
|
15
|
+
* - The internal HTTP server exposes that value (normalized to the bare
|
|
16
|
+
* contract phase) and never exposes secrets, env dumps, or configuration.
|
|
17
|
+
*
|
|
18
|
+
* @module src/server/runtime-status.js
|
|
19
|
+
* @namespace RuntimeStatus
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import http from 'node:http';
|
|
23
|
+
import fs from 'fs-extra';
|
|
24
|
+
import dotenv from 'dotenv';
|
|
25
|
+
import Underpost from '../index.js';
|
|
26
|
+
import { loggerFactory } from './logger.js';
|
|
27
|
+
|
|
28
|
+
const logger = loggerFactory(import.meta);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Allowed runtime status contract values. These are the only Phase-2 signals
|
|
32
|
+
* the monitor reasons about.
|
|
33
|
+
* @memberof RuntimeStatus
|
|
34
|
+
*/
|
|
35
|
+
const RUNTIME_STATUS = {
|
|
36
|
+
BUILD: 'build-deployment',
|
|
37
|
+
INIT: 'initializing-deployment',
|
|
38
|
+
RUNNING: 'running-deployment',
|
|
39
|
+
ERROR: 'error',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const CONTAINER_STATUS_KEY = 'container-status';
|
|
43
|
+
const START_CONTAINER_STATUS_KEY = 'start-container-status';
|
|
44
|
+
const INTERNAL_STATUS_PATH = '/_internal/status';
|
|
45
|
+
const INTERNAL_READY_PATH = '/_internal/ready';
|
|
46
|
+
const INTERNAL_HEALTH_PATH = '/_internal/health';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the internal status port. Defaults to the deployment base `PORT`
|
|
50
|
+
* (app instances bind `PORT + 1` upward, so the base port is free inside the
|
|
51
|
+
* pod). An explicit `UNDERPOST_INTERNAL_PORT` override wins.
|
|
52
|
+
* @memberof RuntimeStatus
|
|
53
|
+
* @returns {number|undefined}
|
|
54
|
+
*/
|
|
55
|
+
const resolveInternalStatusPort = () => {
|
|
56
|
+
const raw = process.env.UNDERPOST_INTERNAL_PORT || process.env.PORT;
|
|
57
|
+
const port = parseInt(raw);
|
|
58
|
+
return Number.isNaN(port) ? undefined : port;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Single source of truth for the internal status port of a specific deployment,
|
|
63
|
+
* used identically by the in-pod server bind (`start.js`) and the CD-side
|
|
64
|
+
* monitor target (`monitor.js`) so the two can never disagree.
|
|
65
|
+
*
|
|
66
|
+
* Resolution order: `UNDERPOST_INTERNAL_PORT` override → the deployment's
|
|
67
|
+
* `.env.<env>` `PORT` → the ambient `PORT`.
|
|
68
|
+
*
|
|
69
|
+
* @memberof RuntimeStatus
|
|
70
|
+
* @param {string} deployId
|
|
71
|
+
* @param {string} env
|
|
72
|
+
* @returns {number|undefined}
|
|
73
|
+
*/
|
|
74
|
+
const deployStatusPort = (deployId, env) => {
|
|
75
|
+
const override = parseInt(process.env.UNDERPOST_INTERNAL_PORT);
|
|
76
|
+
if (!Number.isNaN(override)) return override;
|
|
77
|
+
try {
|
|
78
|
+
const envPath = `./engine-private/conf/${deployId}/.env.${env}`;
|
|
79
|
+
if (fs.existsSync(envPath)) {
|
|
80
|
+
const port = parseInt(dotenv.parse(fs.readFileSync(envPath, 'utf8')).PORT);
|
|
81
|
+
if (!Number.isNaN(port)) return port;
|
|
82
|
+
}
|
|
83
|
+
} catch (_) {
|
|
84
|
+
/* fall through to ambient resolution */
|
|
85
|
+
}
|
|
86
|
+
return resolveInternalStatusPort();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builds the `container-status` env value for a lifecycle phase.
|
|
91
|
+
* @memberof RuntimeStatus
|
|
92
|
+
*/
|
|
93
|
+
const containerStatusValue = (deployId, env, phase) =>
|
|
94
|
+
phase === RUNTIME_STATUS.ERROR ? RUNTIME_STATUS.ERROR : `${deployId}-${env}-${phase}`;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Normalizes a raw `container-status` value to a bare contract phase.
|
|
98
|
+
* Strips the `<deployId>-<env>-` prefix; `error` and unknown/empty values are
|
|
99
|
+
* passed through (empty → undefined).
|
|
100
|
+
* @memberof RuntimeStatus
|
|
101
|
+
* @param {string} raw
|
|
102
|
+
* @returns {string|undefined}
|
|
103
|
+
*/
|
|
104
|
+
const normalizeContainerStatus = (raw) => {
|
|
105
|
+
if (!raw || typeof raw !== 'string') return undefined;
|
|
106
|
+
const value = raw.trim();
|
|
107
|
+
if (!value || value === 'undefined' || value.toLowerCase().includes('empty')) return undefined;
|
|
108
|
+
if (value === RUNTIME_STATUS.ERROR) return RUNTIME_STATUS.ERROR;
|
|
109
|
+
for (const phase of [RUNTIME_STATUS.BUILD, RUNTIME_STATUS.INIT, RUNTIME_STATUS.RUNNING])
|
|
110
|
+
if (value.endsWith(`-${phase}`)) return phase;
|
|
111
|
+
return value;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Reads the current normalized runtime status from the env file.
|
|
116
|
+
* @memberof RuntimeStatus
|
|
117
|
+
* @returns {string|undefined}
|
|
118
|
+
*/
|
|
119
|
+
const getRuntimeStatus = () =>
|
|
120
|
+
normalizeContainerStatus(Underpost.env.get(CONTAINER_STATUS_KEY, undefined, { disableLog: true }));
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reads the start-container-status env key — an insulated marker set by the
|
|
124
|
+
* start pipeline after it completes the running phase. Unlike container-status
|
|
125
|
+
* (which external scripts / backup failures may clobber), this key is written
|
|
126
|
+
* once and survives globalSecretClean. Used exclusively by the readinessProbe
|
|
127
|
+
* endpoint so K8s pod readiness is never derailed by lifecycle noise.
|
|
128
|
+
* @memberof RuntimeStatus
|
|
129
|
+
* @returns {string|undefined}
|
|
130
|
+
*/
|
|
131
|
+
const getStartContainerStatus = () => {
|
|
132
|
+
const raw = Underpost.env.get(START_CONTAINER_STATUS_KEY, undefined, { disableLog: true });
|
|
133
|
+
return raw && typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Minimal, secret-free payload served by the internal status endpoint and used
|
|
138
|
+
* by the monitor for failure classification and observability.
|
|
139
|
+
* @memberof RuntimeStatus
|
|
140
|
+
* @returns {{status: (string|null), deployId: (string|null), env: (string|null)}}
|
|
141
|
+
*/
|
|
142
|
+
const runtimeStatusPayload = () => ({
|
|
143
|
+
status: getRuntimeStatus() ?? null,
|
|
144
|
+
deployId: process.env.DEPLOY_ID ?? null,
|
|
145
|
+
env: process.env.NODE_ENV ?? null,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Emits a structured, secret-free deployment transition event.
|
|
150
|
+
* @memberof RuntimeStatus
|
|
151
|
+
*/
|
|
152
|
+
const emitRuntimeEvent = ({ deployId, env, phase }) => {
|
|
153
|
+
logger.info('runtime-status', {
|
|
154
|
+
deployId,
|
|
155
|
+
env,
|
|
156
|
+
phase: 'runtime',
|
|
157
|
+
status: phase,
|
|
158
|
+
timestamp: new Date().toISOString(),
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Publishes a runtime lifecycle phase to the cross-process contract.
|
|
164
|
+
* @memberof RuntimeStatus
|
|
165
|
+
* @param {string} deployId
|
|
166
|
+
* @param {string} env
|
|
167
|
+
* @param {string} phase - One of {@link RUNTIME_STATUS}.
|
|
168
|
+
*/
|
|
169
|
+
const setRuntimeStatus = (deployId, env, phase) => {
|
|
170
|
+
Underpost.env.set(CONTAINER_STATUS_KEY, containerStatusValue(deployId, env, phase));
|
|
171
|
+
emitRuntimeEvent({ deployId, env, phase });
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
let internalServer;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Starts the in-pod internal status server. Idempotent: repeated calls return
|
|
178
|
+
* the already-listening server. Exposes only the three internal endpoints and
|
|
179
|
+
* never serves secrets or configuration.
|
|
180
|
+
*
|
|
181
|
+
* GET /_internal/status → 200, `{status, deployId, env}` (monitor transport)
|
|
182
|
+
* GET /_internal/ready → 200 iff running-deployment, else 503 (readinessProbe)
|
|
183
|
+
* GET /_internal/health → 200 while the process is alive (livenessProbe)
|
|
184
|
+
*
|
|
185
|
+
* @memberof RuntimeStatus
|
|
186
|
+
* @param {number} [port]
|
|
187
|
+
* @returns {import('node:http').Server|undefined}
|
|
188
|
+
*/
|
|
189
|
+
const startInternalStatusServer = (port = resolveInternalStatusPort()) => {
|
|
190
|
+
if (internalServer) return internalServer;
|
|
191
|
+
if (!port) {
|
|
192
|
+
logger.warn('Internal status server not started: no resolvable port');
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
const server = http.createServer((req, res) => {
|
|
196
|
+
const url = (req.url || '').split('?')[0];
|
|
197
|
+
const sendJson = (code, body) => {
|
|
198
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
199
|
+
res.end(JSON.stringify(body));
|
|
200
|
+
};
|
|
201
|
+
if (req.method !== 'GET') return sendJson(405, { error: 'method_not_allowed' });
|
|
202
|
+
switch (url) {
|
|
203
|
+
case INTERNAL_HEALTH_PATH:
|
|
204
|
+
return sendJson(200, { status: 'ok' });
|
|
205
|
+
case INTERNAL_READY_PATH:
|
|
206
|
+
return getStartContainerStatus()
|
|
207
|
+
? sendJson(200, { status: RUNTIME_STATUS.RUNNING })
|
|
208
|
+
: sendJson(503, { status: getRuntimeStatus() ?? null });
|
|
209
|
+
case INTERNAL_STATUS_PATH:
|
|
210
|
+
return sendJson(200, runtimeStatusPayload());
|
|
211
|
+
default:
|
|
212
|
+
return sendJson(404, { error: 'not_found' });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
server.on('error', (error) => logger.error('internal status server error', error?.message ?? error));
|
|
216
|
+
server.listen(port, () => logger.info(`Internal status endpoint listening on :${port}${INTERNAL_STATUS_PATH}`));
|
|
217
|
+
internalServer = server;
|
|
218
|
+
return internalServer;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Stops the internal status server if running. Returns a promise that resolves
|
|
223
|
+
* once the listener is closed. Primarily a test/teardown hook.
|
|
224
|
+
* @memberof RuntimeStatus
|
|
225
|
+
* @returns {Promise<void>}
|
|
226
|
+
*/
|
|
227
|
+
const stopInternalStatusServer = () =>
|
|
228
|
+
new Promise((resolve) => {
|
|
229
|
+
if (!internalServer) return resolve();
|
|
230
|
+
const server = internalServer;
|
|
231
|
+
internalServer = undefined;
|
|
232
|
+
server.close(() => resolve());
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
export {
|
|
236
|
+
RUNTIME_STATUS,
|
|
237
|
+
CONTAINER_STATUS_KEY,
|
|
238
|
+
START_CONTAINER_STATUS_KEY,
|
|
239
|
+
INTERNAL_STATUS_PATH,
|
|
240
|
+
INTERNAL_READY_PATH,
|
|
241
|
+
INTERNAL_HEALTH_PATH,
|
|
242
|
+
resolveInternalStatusPort,
|
|
243
|
+
deployStatusPort,
|
|
244
|
+
containerStatusValue,
|
|
245
|
+
normalizeContainerStatus,
|
|
246
|
+
getRuntimeStatus,
|
|
247
|
+
getStartContainerStatus,
|
|
248
|
+
runtimeStatusPayload,
|
|
249
|
+
setRuntimeStatus,
|
|
250
|
+
startInternalStatusServer,
|
|
251
|
+
stopInternalStatusServer,
|
|
252
|
+
};
|
package/src/server/start.js
CHANGED
|
@@ -8,6 +8,14 @@ import fs from 'fs-extra';
|
|
|
8
8
|
import { awaitDeployMonitor } from './conf.js';
|
|
9
9
|
import { actionInitLog, loggerFactory } from './logger.js';
|
|
10
10
|
import { shellCd, shellExec } from './process.js';
|
|
11
|
+
import {
|
|
12
|
+
RUNTIME_STATUS,
|
|
13
|
+
START_CONTAINER_STATUS_KEY,
|
|
14
|
+
setRuntimeStatus,
|
|
15
|
+
startInternalStatusServer,
|
|
16
|
+
deployStatusPort,
|
|
17
|
+
containerStatusValue,
|
|
18
|
+
} from './runtime-status.js';
|
|
11
19
|
import Underpost from '../index.js';
|
|
12
20
|
const logger = loggerFactory(import.meta);
|
|
13
21
|
|
|
@@ -147,10 +155,20 @@ class UnderpostStartUp {
|
|
|
147
155
|
pullBundle: false,
|
|
148
156
|
},
|
|
149
157
|
) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
// Bring the internal status endpoint up first so Phase-2 readiness is
|
|
159
|
+
// observable through every lifecycle phase, including build and init. Bind
|
|
160
|
+
// the deployment-resolved port so it always matches the monitor's target.
|
|
161
|
+
startInternalStatusServer(deployStatusPort(deployId, env));
|
|
162
|
+
try {
|
|
163
|
+
setRuntimeStatus(deployId, env, RUNTIME_STATUS.BUILD);
|
|
164
|
+
if (options.build === true) await Underpost.start.build(deployId, env, options);
|
|
165
|
+
setRuntimeStatus(deployId, env, RUNTIME_STATUS.INIT);
|
|
166
|
+
if (options.run === true) await Underpost.start.run(deployId, env, options);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.error('Deployment build/init failed', { deployId, env, message: error?.message });
|
|
169
|
+
setRuntimeStatus(deployId, env, RUNTIME_STATUS.ERROR);
|
|
170
|
+
if (!Underpost.env.isInsideContainer()) throw error;
|
|
171
|
+
}
|
|
154
172
|
},
|
|
155
173
|
/**
|
|
156
174
|
* Run itc-scripts and builds client bundle.
|
|
@@ -162,6 +180,8 @@ class UnderpostStartUp {
|
|
|
162
180
|
* @param {boolean} options.skipFullBuild - Whether to skip building the full client bundle.
|
|
163
181
|
* @param {boolean} options.pullBundle - When true, download pre-built client bundle from Cloudinary via pull-bundle (must be pushed first with push-bundle).
|
|
164
182
|
* This flag is independent of skipFullBuild: it can be combined with skipFullBuild or used alone.
|
|
183
|
+
* @param {boolean} options.privateTestRepo - When true, clone `engine-test-<id>` (the private test source repo
|
|
184
|
+
* published by `node bin/build <deployId> --update-private`) instead of the production `engine-<id>` repo.
|
|
165
185
|
* @memberof UnderpostStartUp
|
|
166
186
|
*/
|
|
167
187
|
async build(
|
|
@@ -170,7 +190,11 @@ class UnderpostStartUp {
|
|
|
170
190
|
options = { underpostQuicklyInstall: false, skipPullBase: false, skipFullBuild: false, pullBundle: false },
|
|
171
191
|
) {
|
|
172
192
|
const buildBasePath = `/home/dd`;
|
|
173
|
-
|
|
193
|
+
// `--private-test-repo` clones the isolated test source repo published by
|
|
194
|
+
// `node bin/build <deployId> --update-private`, instead of the production one.
|
|
195
|
+
const repoName = options?.privateTestRepo
|
|
196
|
+
? `engine-test-${deployId.split('-')[1]}`
|
|
197
|
+
: `engine-${deployId.split('-')[1]}`;
|
|
174
198
|
if (!options.skipPullBase) {
|
|
175
199
|
shellExec(`cd ${buildBasePath} && underpost clone ${process.env.GITHUB_USERNAME}/${repoName}`);
|
|
176
200
|
shellExec(`mkdir -p ${buildBasePath}/engine`);
|
|
@@ -201,7 +225,7 @@ class UnderpostStartUp {
|
|
|
201
225
|
const makeDeployCallback = (cmd) => (code, out, msg) => {
|
|
202
226
|
if (code !== 0) {
|
|
203
227
|
logger.error(`Deployment process exited with code ${code}`, { cmd, msg });
|
|
204
|
-
|
|
228
|
+
setRuntimeStatus(deployId, env, RUNTIME_STATUS.ERROR);
|
|
205
229
|
}
|
|
206
230
|
};
|
|
207
231
|
if (fs.existsSync(`./engine-private/replica`)) {
|
|
@@ -211,18 +235,25 @@ class UnderpostStartUp {
|
|
|
211
235
|
shellExec(`node bin env ${replica} ${env}`);
|
|
212
236
|
const replicaCmd = `npm ${runCmd} ${replica}`;
|
|
213
237
|
shellExec(replicaCmd, { async: true, callback: makeDeployCallback(replicaCmd) });
|
|
214
|
-
await awaitDeployMonitor();
|
|
238
|
+
const result = await awaitDeployMonitor();
|
|
239
|
+
if (result !== true) {
|
|
240
|
+
setRuntimeStatus(deployId, env, RUNTIME_STATUS.ERROR);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
215
243
|
}
|
|
216
244
|
}
|
|
217
245
|
shellExec(`node bin env ${deployId} ${env}`);
|
|
218
246
|
const deployCmd = `npm ${runCmd} ${deployId}`;
|
|
219
247
|
shellExec(deployCmd, { async: true, callback: makeDeployCallback(deployCmd) });
|
|
220
|
-
await awaitDeployMonitor(true);
|
|
221
|
-
if (
|
|
248
|
+
const result = await awaitDeployMonitor(true);
|
|
249
|
+
if (result === true) {
|
|
222
250
|
if (env === 'production' && Underpost.env.isInsideContainer()) Underpost.secret.globalSecretClean();
|
|
223
|
-
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
setRuntimeStatus(deployId, env, RUNTIME_STATUS.RUNNING);
|
|
253
|
+
Underpost.env.set(START_CONTAINER_STATUS_KEY, containerStatusValue(deployId, env, RUNTIME_STATUS.RUNNING));
|
|
254
|
+
});
|
|
224
255
|
} else {
|
|
225
|
-
|
|
256
|
+
setRuntimeStatus(deployId, env, RUNTIME_STATUS.ERROR);
|
|
226
257
|
}
|
|
227
258
|
},
|
|
228
259
|
};
|
package/src/server.js
CHANGED
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
// https://nodejs.org/api
|
|
4
4
|
// https://expressjs.com/en/4x/api.html
|
|
5
5
|
|
|
6
|
+
import dotenv from 'dotenv';
|
|
6
7
|
import { loggerFactory } from './server/logger.js';
|
|
7
|
-
import { buildClient } from './
|
|
8
|
+
import { buildClient } from './client-builder/client-build.js';
|
|
8
9
|
import { buildRuntime } from './server/runtime.js';
|
|
9
10
|
import { ProcessController } from './server/process.js';
|
|
10
11
|
import { Config } from './server/conf.js';
|
|
12
|
+
|
|
13
|
+
dotenv.config();
|
|
14
|
+
|
|
11
15
|
await Config.build();
|
|
12
16
|
|
|
13
17
|
const logger = loggerFactory(import.meta);
|
|
14
18
|
|
|
15
19
|
await logger.setUpInfo();
|
|
16
20
|
|
|
17
|
-
await buildClient();
|
|
21
|
+
if (process.env.NODE_ENV === 'development') await buildClient();
|
|
18
22
|
|
|
19
23
|
await buildRuntime();
|
|
20
24
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import {
|
|
5
|
+
fillInstanceConfDefaults,
|
|
6
|
+
CYBERIA_INSTANCE_CONF_DEFAULTS,
|
|
7
|
+
} from '../src/api/cyberia-server-defaults/cyberia-server-defaults.js';
|
|
8
|
+
|
|
9
|
+
// The CyberiaInstanceConfSchema fields that must always be present after a
|
|
10
|
+
// backfill. Mirrors cyberia-instance-conf.model.js (minus instanceCode, which
|
|
11
|
+
// is the lookup key and is only present when the source doc carries it).
|
|
12
|
+
const SCHEMA_FIELDS = [
|
|
13
|
+
'tickRate',
|
|
14
|
+
'snapshotRate',
|
|
15
|
+
'aoiRadius',
|
|
16
|
+
'portalHoldTimeMs',
|
|
17
|
+
'portalSpawnRadius',
|
|
18
|
+
'entityBaseSpeed',
|
|
19
|
+
'entityBaseMaxLife',
|
|
20
|
+
'entityBaseActionCooldownMs',
|
|
21
|
+
'entityBaseMinActionCooldownMs',
|
|
22
|
+
'botAggroRange',
|
|
23
|
+
'defaultPlayerWidth',
|
|
24
|
+
'defaultPlayerHeight',
|
|
25
|
+
'playerBaseLifeRegenMin',
|
|
26
|
+
'playerBaseLifeRegenMax',
|
|
27
|
+
'sumStatsLimit',
|
|
28
|
+
'maxActiveLayers',
|
|
29
|
+
'initialLifeFraction',
|
|
30
|
+
'respawnDurationMs',
|
|
31
|
+
'collisionLifeLoss',
|
|
32
|
+
'economyRules',
|
|
33
|
+
'lifeRegenChance',
|
|
34
|
+
'maxChance',
|
|
35
|
+
'entityDefaults',
|
|
36
|
+
'statusIcons',
|
|
37
|
+
'skillConfig',
|
|
38
|
+
'skillRules',
|
|
39
|
+
'equipmentRules',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
describe('fillInstanceConfDefaults', () => {
|
|
43
|
+
it('fills every schema field from canonical defaults for an empty doc', () => {
|
|
44
|
+
const out = fillInstanceConfDefaults({});
|
|
45
|
+
for (const field of SCHEMA_FIELDS) {
|
|
46
|
+
expect(out, `missing field: ${field}`).to.have.property(field);
|
|
47
|
+
expect(out[field], `null/undefined field: ${field}`).to.not.equal(undefined);
|
|
48
|
+
expect(out[field], `null field: ${field}`).to.not.equal(null);
|
|
49
|
+
}
|
|
50
|
+
expect(out.tickRate).to.equal(CYBERIA_INSTANCE_CONF_DEFAULTS.tickRate);
|
|
51
|
+
expect(out.economyRules).to.deep.equal(CYBERIA_INSTANCE_CONF_DEFAULTS.economyRules);
|
|
52
|
+
expect(out.skillRules).to.deep.equal(CYBERIA_INSTANCE_CONF_DEFAULTS.skillRules);
|
|
53
|
+
expect(out.equipmentRules).to.deep.equal(CYBERIA_INSTANCE_CONF_DEFAULTS.equipmentRules);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('handles undefined / non-object input as an empty doc', () => {
|
|
57
|
+
for (const bad of [undefined, null, 42, 'x']) {
|
|
58
|
+
const out = fillInstanceConfDefaults(bad);
|
|
59
|
+
for (const field of SCHEMA_FIELDS) expect(out).to.have.property(field);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('preserves author-set scalar values, including falsy 0 / false / empty string', () => {
|
|
64
|
+
const out = fillInstanceConfDefaults({
|
|
65
|
+
tickRate: 30,
|
|
66
|
+
collisionLifeLoss: 0,
|
|
67
|
+
portalFee: 0,
|
|
68
|
+
});
|
|
69
|
+
expect(out.tickRate).to.equal(30);
|
|
70
|
+
expect(out.collisionLifeLoss).to.equal(0);
|
|
71
|
+
// Fields not overridden still come from defaults.
|
|
72
|
+
expect(out.snapshotRate).to.equal(CYBERIA_INSTANCE_CONF_DEFAULTS.snapshotRate);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('backfills a missing nested sub-document (economyRules)', () => {
|
|
76
|
+
const out = fillInstanceConfDefaults({ tickRate: 60 });
|
|
77
|
+
expect(out.economyRules).to.deep.equal(CYBERIA_INSTANCE_CONF_DEFAULTS.economyRules);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('fills only the missing keys of a partial nested sub-document', () => {
|
|
81
|
+
const out = fillInstanceConfDefaults({
|
|
82
|
+
economyRules: { botSpawnCoins: 999 },
|
|
83
|
+
});
|
|
84
|
+
expect(out.economyRules.botSpawnCoins).to.equal(999);
|
|
85
|
+
expect(out.economyRules.playerSpawnCoins).to.equal(
|
|
86
|
+
CYBERIA_INSTANCE_CONF_DEFAULTS.economyRules.playerSpawnCoins,
|
|
87
|
+
);
|
|
88
|
+
expect(out.economyRules.portalFee).to.equal(CYBERIA_INSTANCE_CONF_DEFAULTS.economyRules.portalFee);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('keeps a present non-empty array and does not merge defaults into it', () => {
|
|
92
|
+
const custom = [{ entityType: 'player', liveItemIds: ['anon'], deadItemIds: [], dropItemIds: [] }];
|
|
93
|
+
const out = fillInstanceConfDefaults({ entityDefaults: custom });
|
|
94
|
+
expect(out.entityDefaults).to.deep.equal(custom);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('populates an empty skillConfig from defaults, normalised to schema shape (no `skills`)', () => {
|
|
98
|
+
for (const input of [{}, { skillConfig: [] }, { skillConfig: null }]) {
|
|
99
|
+
const out = fillInstanceConfDefaults(input);
|
|
100
|
+
expect(out.skillConfig.length, JSON.stringify(input)).to.be.greaterThan(0);
|
|
101
|
+
for (const entry of out.skillConfig) {
|
|
102
|
+
expect(entry).to.have.property('triggerItemId');
|
|
103
|
+
expect(entry).to.have.property('logicEventIds');
|
|
104
|
+
expect(entry).to.not.have.property('skills');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('keeps an author-set non-empty skillConfig verbatim', () => {
|
|
110
|
+
const custom = [{ triggerItemId: 'custom-weapon', logicEventIds: ['projectile'] }];
|
|
111
|
+
const out = fillInstanceConfDefaults({ skillConfig: custom });
|
|
112
|
+
expect(out.skillConfig).to.deep.equal(custom);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('fills an empty config array (entityDefaults) from defaults', () => {
|
|
116
|
+
const out = fillInstanceConfDefaults({ entityDefaults: [] });
|
|
117
|
+
expect(out.entityDefaults.length).to.be.greaterThan(0);
|
|
118
|
+
expect(out.entityDefaults).to.deep.equal(CYBERIA_INSTANCE_CONF_DEFAULTS.entityDefaults);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('preserves DB metadata (_id, instanceCode, timestamps)', () => {
|
|
122
|
+
const now = new Date().toISOString();
|
|
123
|
+
const out = fillInstanceConfDefaults({
|
|
124
|
+
_id: 'abc123',
|
|
125
|
+
instanceCode: 'amethyst-strata-expansion',
|
|
126
|
+
createdAt: now,
|
|
127
|
+
updatedAt: now,
|
|
128
|
+
});
|
|
129
|
+
expect(out._id).to.equal('abc123');
|
|
130
|
+
expect(out.instanceCode).to.equal('amethyst-strata-expansion');
|
|
131
|
+
expect(out.createdAt).to.equal(now);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('does not mutate the input document', () => {
|
|
135
|
+
const input = { economyRules: { botSpawnCoins: 5 } };
|
|
136
|
+
const snapshot = JSON.parse(JSON.stringify(input));
|
|
137
|
+
fillInstanceConfDefaults(input);
|
|
138
|
+
expect(input).to.deep.equal(snapshot);
|
|
139
|
+
});
|
|
140
|
+
});
|