cyberia 3.2.12 → 3.2.22
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/.github/workflows/engine-cyberia.cd.yml +1 -0
- package/.github/workflows/engine-cyberia.ci.yml +14 -2
- package/.github/workflows/ghpkg.ci.yml +1 -0
- package/.github/workflows/npmpkg.ci.yml +9 -5
- package/CHANGELOG.md +151 -1
- package/CLI-HELP.md +975 -1130
- package/bin/build.js +97 -136
- package/bin/build.template.js +25 -179
- package/bin/cyberia.js +11 -6
- package/bin/deploy.js +4 -1
- package/bin/index.js +11 -6
- package/conf.js +1 -0
- package/deployment.yaml +74 -2
- package/hardhat/package-lock.json +4 -4
- package/hardhat/package.json +1 -1
- 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-default-development/deployment.yaml +2 -2
- package/package.json +7 -7
- package/scripts/link-local-underpost-cli.sh +6 -0
- package/scripts/test-monitor.sh +250 -0
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +7 -0
- package/src/cli/deploy.js +200 -282
- package/src/cli/env.js +1 -4
- package/src/cli/image.js +58 -4
- package/src/cli/index.js +47 -0
- package/src/cli/monitor.js +387 -6
- package/src/cli/release.js +26 -11
- package/src/cli/repository.js +101 -7
- package/src/cli/run.js +159 -73
- package/src/client/components/core/PanelForm.js +44 -44
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +1 -1
- package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +55 -1
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +272 -50
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +20 -11
- package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +23 -1
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/db/mongo/MongooseDB.js +2 -1
- package/src/index.js +1 -1
- package/src/runtime/cyberia-client/Dockerfile +4 -22
- package/src/runtime/cyberia-client/Dockerfile.dev +3 -18
- package/src/runtime/cyberia-server/Dockerfile +3 -23
- package/src/runtime/cyberia-server/Dockerfile.dev +3 -27
- package/src/runtime/wp/Dockerfile +3 -3
- package/src/server/catalog-underpost.js +61 -0
- package/src/server/catalog.js +77 -0
- package/src/server/conf.js +414 -56
- package/src/server/ipfs-client.js +5 -3
- package/src/server/runtime-status.js +235 -0
- package/src/server/start.js +32 -11
- package/test/deploy-monitor.test.js +251 -0
- package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
- package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
package/src/cli/run.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getNpmRootPath,
|
|
16
16
|
isDeployRunnerContext,
|
|
17
17
|
loadConfServerJson,
|
|
18
|
+
loadReplicas,
|
|
18
19
|
writeEnv,
|
|
19
20
|
} from '../server/conf.js';
|
|
20
21
|
import { actionInitLog, loggerFactory } from '../server/logger.js';
|
|
@@ -119,6 +120,7 @@ const logger = loggerFactory(import.meta);
|
|
|
119
120
|
* @property {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment (supported by: sync, template-deploy).
|
|
120
121
|
* @property {boolean} pullBundle - Whether to pull the bundle before running. Use together with --skip-full-build to skip the local build entirely (supported by: sync, template-deploy).
|
|
121
122
|
* @property {boolean} remove - Whether to remove/teardown resources instead of creating them (e.g. delete-expose for k3s proxy devices in dev-cluster).
|
|
123
|
+
* @property {boolean} test - Whether to enable test/generic-purpose mode (e.g. use self-signed TLS instead of cert-manager).
|
|
122
124
|
* @memberof UnderpostRun
|
|
123
125
|
*/
|
|
124
126
|
const DEFAULT_OPTION = {
|
|
@@ -187,6 +189,7 @@ const DEFAULT_OPTION = {
|
|
|
187
189
|
skipFullBuild: false,
|
|
188
190
|
pullBundle: false,
|
|
189
191
|
remove: false,
|
|
192
|
+
test: false,
|
|
190
193
|
};
|
|
191
194
|
|
|
192
195
|
/**
|
|
@@ -222,7 +225,7 @@ class UnderpostRun {
|
|
|
222
225
|
shellExec(`${baseCommand} cluster${options.dev ? ' --dev' : ''}`);
|
|
223
226
|
|
|
224
227
|
shellExec(
|
|
225
|
-
`${baseCommand} cluster${options.dev ? ' --dev' : ''} --
|
|
228
|
+
`${baseCommand} cluster${options.dev ? ' --dev' : ''} --mongodb --service-host ${mongoHosts.join(
|
|
226
229
|
',',
|
|
227
230
|
)} --pull-image`,
|
|
228
231
|
);
|
|
@@ -264,6 +267,16 @@ class UnderpostRun {
|
|
|
264
267
|
logger.info(hostListenResult.renderHosts);
|
|
265
268
|
},
|
|
266
269
|
|
|
270
|
+
/**
|
|
271
|
+
* @method etc-hosts
|
|
272
|
+
* @description Modifies the `/etc/hosts` file to add entries for local access to services,
|
|
273
|
+
* based on the provided path input.
|
|
274
|
+
* @param {string} path - The input value, identifier, or path for the operation (used to specify the entries to add to /etc/hosts).
|
|
275
|
+
*/
|
|
276
|
+
'etc-hosts': (path = '', options = DEFAULT_OPTION) => {
|
|
277
|
+
etcHostFactory(path.split(','));
|
|
278
|
+
},
|
|
279
|
+
|
|
267
280
|
/**
|
|
268
281
|
* @method ipfs-expose
|
|
269
282
|
* @description Exposes IPFS Cluster services on specified ports for local access.
|
|
@@ -422,6 +435,7 @@ class UnderpostRun {
|
|
|
422
435
|
return;
|
|
423
436
|
}
|
|
424
437
|
shellExec(`${baseCommand} run pull`);
|
|
438
|
+
shellExec(`${baseCommand} run shared-dir`);
|
|
425
439
|
|
|
426
440
|
// Capture last N commit messages for propagation.
|
|
427
441
|
// When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
|
|
@@ -502,6 +516,7 @@ class UnderpostRun {
|
|
|
502
516
|
return;
|
|
503
517
|
}
|
|
504
518
|
shellExec(`${baseCommand} run pull`);
|
|
519
|
+
shellExec(`${baseCommand} run shared-dir`);
|
|
505
520
|
|
|
506
521
|
// Capture last N commit messages from the engine repo.
|
|
507
522
|
// When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
|
|
@@ -713,6 +728,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
713
728
|
let targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'green';
|
|
714
729
|
if (targetTraffic) versions = versions ? versions : targetTraffic;
|
|
715
730
|
|
|
731
|
+
const ignorePods =
|
|
732
|
+
isDeployRunnerContext(path, options) && targetTraffic
|
|
733
|
+
? Underpost.kubectl.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace).map((p) => p.NAME)
|
|
734
|
+
: [];
|
|
735
|
+
|
|
716
736
|
const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
|
|
717
737
|
const cmdString = options.cmd
|
|
718
738
|
? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
|
|
@@ -743,7 +763,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
743
763
|
);
|
|
744
764
|
if (!targetTraffic)
|
|
745
765
|
targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
|
|
746
|
-
await Underpost.
|
|
766
|
+
await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
|
|
747
767
|
Underpost.deploy.switchTraffic(deployId, env, targetTraffic, replicas, options.namespace, options);
|
|
748
768
|
} else
|
|
749
769
|
logger.info('current traffic', Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }));
|
|
@@ -989,8 +1009,19 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
989
1009
|
// pathRewritePolicy,
|
|
990
1010
|
});
|
|
991
1011
|
if (options.tls) {
|
|
992
|
-
|
|
993
|
-
|
|
1012
|
+
if (options.test) {
|
|
1013
|
+
const sslDir = `./engine-private/ssl/${_host}`;
|
|
1014
|
+
const nameSafe = _host.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
1015
|
+
fs.mkdirpSync(sslDir);
|
|
1016
|
+
shellExec(`bash ./scripts/ssl.sh "${sslDir}" "${_host}"`);
|
|
1017
|
+
shellExec(`kubectl delete secret ${_host} -n ${options.namespace} --ignore-not-found`);
|
|
1018
|
+
shellExec(
|
|
1019
|
+
`kubectl create secret tls ${_host} --cert="${sslDir}/${nameSafe}.pem" --key="${sslDir}/${nameSafe}-key.pem" -n ${options.namespace}`,
|
|
1020
|
+
);
|
|
1021
|
+
} else {
|
|
1022
|
+
shellExec(`sudo kubectl delete Certificate ${_host} -n ${options.namespace} --ignore-not-found`);
|
|
1023
|
+
proxyYaml += Underpost.deploy.buildCertManagerCertificate({ ...options, host: _host });
|
|
1024
|
+
}
|
|
994
1025
|
}
|
|
995
1026
|
// console.log(proxyYaml);
|
|
996
1027
|
shellExec(`kubectl delete HTTPProxy ${_host} --namespace ${options.namespace} --ignore-not-found`);
|
|
@@ -1059,6 +1090,7 @@ EOF
|
|
|
1059
1090
|
// Examples images:
|
|
1060
1091
|
// `underpost/underpost-engine:${Underpost.version}`
|
|
1061
1092
|
// `localhost/rockylinux9-underpost:${Underpost.version}`
|
|
1093
|
+
if (options.imageName) _image = options.imageName;
|
|
1062
1094
|
if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
|
|
1063
1095
|
|
|
1064
1096
|
if (_image && !_image.startsWith('localhost'))
|
|
@@ -1154,12 +1186,16 @@ EOF
|
|
|
1154
1186
|
`,
|
|
1155
1187
|
{ disableLog: true },
|
|
1156
1188
|
);
|
|
1157
|
-
|
|
1189
|
+
// Custom instances run a bare binary (no `underpost start` / internal
|
|
1190
|
+
// HTTP endpoint): Kubernetes readiness is the running signal and
|
|
1191
|
+
// container-status is read via exec. See `Deploy custom instance to K8S.md`.
|
|
1192
|
+
const { ready, readyPods } = await Underpost.monitor.monitorReadyRunner(
|
|
1158
1193
|
_deployId,
|
|
1159
1194
|
env,
|
|
1160
1195
|
targetTraffic,
|
|
1161
1196
|
ignorePods,
|
|
1162
1197
|
options.namespace,
|
|
1198
|
+
{ readyGate: 'kubernetes', statusTransport: 'exec' },
|
|
1163
1199
|
);
|
|
1164
1200
|
|
|
1165
1201
|
if (!ready) {
|
|
@@ -1169,7 +1205,7 @@ EOF
|
|
|
1169
1205
|
shellExec(
|
|
1170
1206
|
`${baseCommand} run${baseClusterCommand} --namespace ${options.namespace}` +
|
|
1171
1207
|
`${options.nodeName ? ` --node-name ${options.nodeName}` : ''}` +
|
|
1172
|
-
`${options.tls ? ` --tls` : ''}` +
|
|
1208
|
+
`${options.tls ? ` --tls ${options.test ? '--test' : ''}` : ''}` +
|
|
1173
1209
|
` instance-promote '${path}'`,
|
|
1174
1210
|
);
|
|
1175
1211
|
}
|
|
@@ -1436,8 +1472,8 @@ EOF`);
|
|
|
1436
1472
|
const baseClusterCommand = options.dev ? ' --dev' : '';
|
|
1437
1473
|
const currentImage = options.imageName
|
|
1438
1474
|
? options.imageName
|
|
1439
|
-
: Underpost.
|
|
1440
|
-
.
|
|
1475
|
+
: Underpost.image
|
|
1476
|
+
.getCurrentLoaded(options.nodeName ? options.nodeName : 'kind-worker', false)
|
|
1441
1477
|
.find((o) => o.IMAGE.match('underpost'));
|
|
1442
1478
|
const podName = options.podName || `underpost-dev-container`;
|
|
1443
1479
|
const volumeHostPath = options.claimName || '/home/dd';
|
|
@@ -1714,20 +1750,13 @@ EOF`);
|
|
|
1714
1750
|
const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
|
|
1715
1751
|
const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
|
|
1716
1752
|
const env = options.dev ? 'development' : 'production';
|
|
1717
|
-
const ignorePods = Underpost.
|
|
1753
|
+
const ignorePods = Underpost.kubectl
|
|
1718
1754
|
.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace)
|
|
1719
1755
|
.map((p) => p.NAME);
|
|
1720
1756
|
|
|
1721
1757
|
shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${options.namespace}`);
|
|
1722
1758
|
|
|
1723
|
-
await Underpost.
|
|
1724
|
-
deployId,
|
|
1725
|
-
env,
|
|
1726
|
-
targetTraffic,
|
|
1727
|
-
ignorePods,
|
|
1728
|
-
options.namespace,
|
|
1729
|
-
'underpost',
|
|
1730
|
-
);
|
|
1759
|
+
await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
|
|
1731
1760
|
|
|
1732
1761
|
Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, options.namespace, options);
|
|
1733
1762
|
},
|
|
@@ -1819,7 +1848,7 @@ EOF`);
|
|
|
1819
1848
|
}`;
|
|
1820
1849
|
shellExec(cmd, { async: true });
|
|
1821
1850
|
}
|
|
1822
|
-
await awaitDeployMonitor();
|
|
1851
|
+
if ((await awaitDeployMonitor()) !== true) return;
|
|
1823
1852
|
{
|
|
1824
1853
|
const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
|
|
1825
1854
|
|
|
@@ -1827,7 +1856,7 @@ EOF`);
|
|
|
1827
1856
|
async: true,
|
|
1828
1857
|
});
|
|
1829
1858
|
}
|
|
1830
|
-
await awaitDeployMonitor();
|
|
1859
|
+
if ((await awaitDeployMonitor()) !== true) return;
|
|
1831
1860
|
shellExec(
|
|
1832
1861
|
`NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
|
|
1833
1862
|
);
|
|
@@ -2504,7 +2533,8 @@ EOF`;
|
|
|
2504
2533
|
/**
|
|
2505
2534
|
* @method push-bundle
|
|
2506
2535
|
* @description Builds the client zip for the specified deployment, splits it into parts, and uploads to file storage.
|
|
2507
|
-
* Steps: set env, build+split zip,
|
|
2536
|
+
* Steps: set env, build+split zip, upload only the zip parts belonging to the deploy-id's hosts (from conf.server.json).
|
|
2537
|
+
* Only files matching `<host>-<route>.zip.part*` or `<host>-<route>.zip` for each non-skipped route are uploaded.
|
|
2508
2538
|
* @param {string} path - Optional `fsPath.splitOption` string.
|
|
2509
2539
|
* Examples: `build` (default split 8), `build.16` (split 16 MB), `build.none-split` (no split flag).
|
|
2510
2540
|
* @param {Object} options - The default underpost runner options for customizing workflow.
|
|
@@ -2513,7 +2543,7 @@ EOF`;
|
|
|
2513
2543
|
* @memberof UnderpostRun
|
|
2514
2544
|
*/
|
|
2515
2545
|
'push-bundle': (path = '', options = DEFAULT_OPTION) => {
|
|
2516
|
-
const baseCommand = options.dev ? 'node bin' : 'underpost';
|
|
2546
|
+
const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
|
|
2517
2547
|
const env = options.dev ? 'development' : 'production';
|
|
2518
2548
|
const deployId = options.deployId || 'dd-default';
|
|
2519
2549
|
const pathParts = (path || '').split('.');
|
|
@@ -2537,11 +2567,54 @@ EOF`;
|
|
|
2537
2567
|
}
|
|
2538
2568
|
}
|
|
2539
2569
|
|
|
2570
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
2571
|
+
const confServer = fs.existsSync(confServerPath)
|
|
2572
|
+
? loadReplicas(deployId, loadConfServerJson(confServerPath))
|
|
2573
|
+
: {};
|
|
2574
|
+
const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
|
|
2575
|
+
|
|
2540
2576
|
shellExec(`${baseCommand} env ${deployId} ${env}`);
|
|
2541
2577
|
shellExec(`${baseCommand} client ${deployId} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2578
|
+
|
|
2579
|
+
const pushBundleFiles = (host, routePath) => {
|
|
2580
|
+
const buildId = `${host}-${routePath.replaceAll('/', '')}`;
|
|
2581
|
+
const buildDir = `./${fsPath}`;
|
|
2582
|
+
if (!fs.existsSync(buildDir)) return;
|
|
2583
|
+
const partFiles = fs
|
|
2584
|
+
.readdirSync(buildDir)
|
|
2585
|
+
.filter(
|
|
2586
|
+
(name) =>
|
|
2587
|
+
name.startsWith(`${buildId}.zip.part`) ||
|
|
2588
|
+
name.startsWith(`${buildId}.zip-part`) ||
|
|
2589
|
+
name === `${buildId}.zip`,
|
|
2590
|
+
)
|
|
2591
|
+
.map((name) => `${fsPath}/${name}`);
|
|
2592
|
+
if (partFiles.length === 0) {
|
|
2593
|
+
logger.warn(`push-bundle: no bundle files found for '${host}${routePath}'`, { buildId });
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
for (const partFile of partFiles) {
|
|
2597
|
+
shellExec(
|
|
2598
|
+
`${baseCommand} fs ${partFile} --deploy-id ${deployId} --storage-file-path ${storageFilePath} --force`,
|
|
2599
|
+
);
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
|
|
2603
|
+
for (const host of Object.keys(confServer)) {
|
|
2604
|
+
for (const routePath of Object.keys(confServer[host])) {
|
|
2605
|
+
const routeConf = confServer[host][routePath] || {};
|
|
2606
|
+
if (routeConf.redirect || routeConf.disabledRebuild) continue;
|
|
2607
|
+
if (routeConf.singleReplica) {
|
|
2608
|
+
if (routeConf.replicas) {
|
|
2609
|
+
for (const replica of routeConf.replicas) {
|
|
2610
|
+
pushBundleFiles(host, replica);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
continue;
|
|
2614
|
+
}
|
|
2615
|
+
pushBundleFiles(host, routePath);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2545
2618
|
},
|
|
2546
2619
|
|
|
2547
2620
|
/**
|
|
@@ -2558,11 +2631,13 @@ EOF`;
|
|
|
2558
2631
|
* @memberof UnderpostRun
|
|
2559
2632
|
*/
|
|
2560
2633
|
'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
|
|
2561
|
-
const baseCommand = options.dev ? 'node bin' : 'underpost';
|
|
2634
|
+
const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
|
|
2562
2635
|
const env = options.dev ? 'development' : 'production';
|
|
2563
2636
|
const deployId = options.deployId || 'dd-default';
|
|
2564
2637
|
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
2565
|
-
const confServer = fs.existsSync(confServerPath)
|
|
2638
|
+
const confServer = fs.existsSync(confServerPath)
|
|
2639
|
+
? loadReplicas(deployId, loadConfServerJson(confServerPath))
|
|
2640
|
+
: {};
|
|
2566
2641
|
const hostsArg = path
|
|
2567
2642
|
? path
|
|
2568
2643
|
.split(',')
|
|
@@ -2581,64 +2656,75 @@ EOF`;
|
|
|
2581
2656
|
`${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
|
|
2582
2657
|
);
|
|
2583
2658
|
|
|
2659
|
+
const pullBundleRoute = (host, routePath) => {
|
|
2660
|
+
const buildId = `${host}-${routePath.replaceAll('/', '')}`;
|
|
2661
|
+
const zipPath = `build/${buildId}.zip`;
|
|
2662
|
+
const buildDir = './build';
|
|
2663
|
+
const hasZip = fs.existsSync(zipPath);
|
|
2664
|
+
const hasParts =
|
|
2665
|
+
fs.existsSync(buildDir) &&
|
|
2666
|
+
fs
|
|
2667
|
+
.readdirSync(buildDir)
|
|
2668
|
+
.some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
|
|
2669
|
+
|
|
2670
|
+
if (!hasZip && !hasParts) {
|
|
2671
|
+
logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
|
|
2676
|
+
shellExec(`${baseCommand} client --unzip ${zipPath}`);
|
|
2677
|
+
shellExec(`sudo rm -rf ${zipPath}`);
|
|
2678
|
+
|
|
2679
|
+
if (fs.existsSync(buildDir)) {
|
|
2680
|
+
fs.readdirSync(buildDir)
|
|
2681
|
+
.filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
|
|
2682
|
+
.forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
const extractedDir = `build/${buildId.replace(/-$/, '')}`;
|
|
2686
|
+
if (!fs.existsSync(extractedDir)) {
|
|
2687
|
+
logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
|
|
2692
|
+
if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
|
|
2693
|
+
if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
|
|
2694
|
+
fs.copySync(`${extractedDir}`, `${publicDestPath}`);
|
|
2695
|
+
};
|
|
2696
|
+
|
|
2584
2697
|
for (const host of hostsArg) {
|
|
2585
|
-
// Gather all routes for this host; fall back to root '/' when host is not in confServer
|
|
2586
|
-
// (e.g. when hosts were provided explicitly via the path argument).
|
|
2587
2698
|
const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
|
|
2588
2699
|
|
|
2589
2700
|
for (const routePath of routePaths) {
|
|
2590
2701
|
const routeConf = confServer[host] ? confServer[host][routePath] || {} : {};
|
|
2591
|
-
|
|
2592
|
-
if (routeConf.singleReplica
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
const zipPath = `build/${buildId}.zip`;
|
|
2599
|
-
const buildDir = './build';
|
|
2600
|
-
const hasZip = fs.existsSync(zipPath);
|
|
2601
|
-
const hasParts =
|
|
2602
|
-
fs.existsSync(buildDir) &&
|
|
2603
|
-
fs
|
|
2604
|
-
.readdirSync(buildDir)
|
|
2605
|
-
.some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
|
|
2606
|
-
|
|
2607
|
-
if (!hasZip && !hasParts) {
|
|
2608
|
-
logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
|
|
2609
|
-
continue;
|
|
2610
|
-
}
|
|
2611
|
-
|
|
2612
|
-
if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
|
|
2613
|
-
shellExec(`${baseCommand} client --unzip ${zipPath}`);
|
|
2614
|
-
shellExec(`sudo rm -rf ${zipPath}`);
|
|
2615
|
-
|
|
2616
|
-
// Clean up downloaded part wrapper zips left by --omit-unzip pull
|
|
2617
|
-
if (fs.existsSync(buildDir)) {
|
|
2618
|
-
fs.readdirSync(buildDir)
|
|
2619
|
-
.filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
|
|
2620
|
-
.forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
// unzipClientBuild extracts to buildId with trailing '-' stripped
|
|
2624
|
-
// e.g. "build/underpost.net-" → "build/underpost.net"
|
|
2625
|
-
// e.g. "build/app.net-admin" → "build/app.net-admin" (no trailing dash, no change)
|
|
2626
|
-
const extractedDir = `build/${buildId.replace(/-$/, '')}`;
|
|
2627
|
-
if (!fs.existsSync(extractedDir)) {
|
|
2628
|
-
logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
|
|
2702
|
+
if (routeConf.redirect || routeConf.disabledRebuild) continue;
|
|
2703
|
+
if (routeConf.singleReplica) {
|
|
2704
|
+
if (routeConf.replicas) {
|
|
2705
|
+
for (const replica of routeConf.replicas) {
|
|
2706
|
+
pullBundleRoute(host, replica);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2629
2709
|
continue;
|
|
2630
2710
|
}
|
|
2631
|
-
|
|
2632
|
-
// Destination mirrors the public directory layout used by the server
|
|
2633
|
-
const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
|
|
2634
|
-
if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
|
|
2635
|
-
// Ensure parent directory exists for sub-paths
|
|
2636
|
-
if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
|
|
2637
|
-
shellExec(`sudo mv ${extractedDir} ${publicDestPath}`);
|
|
2711
|
+
pullBundleRoute(host, routePath);
|
|
2638
2712
|
}
|
|
2639
2713
|
}
|
|
2640
2714
|
},
|
|
2641
2715
|
|
|
2716
|
+
/**
|
|
2717
|
+
* @method build-cluster-deployment-manifests
|
|
2718
|
+
* @description Builds deployment manifests for both production and development environments using `node bin deploy --build-manifest`, syncing them, and setting replicas to 1 for the `dd` deployment.
|
|
2719
|
+
* @param {string} path - Unused.
|
|
2720
|
+
* @param {Object} options - The default underpost runner options for customizing workflow.
|
|
2721
|
+
* @memberof UnderpostRun
|
|
2722
|
+
*/
|
|
2723
|
+
'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
|
|
2724
|
+
shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
|
|
2725
|
+
shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production --cert`);
|
|
2726
|
+
},
|
|
2727
|
+
|
|
2642
2728
|
/**
|
|
2643
2729
|
* @method monitor-ui
|
|
2644
2730
|
* @description Installs and enables the Cockpit KVM Dashboard (cockpit, cockpit-machines, libvirt)
|
|
@@ -73,7 +73,7 @@ class PanelForm {
|
|
|
73
73
|
parentIdModal: undefined,
|
|
74
74
|
route: 'home',
|
|
75
75
|
htmlFormHeader: async () => '',
|
|
76
|
-
firsUpdateEvent: async () => {
|
|
76
|
+
firsUpdateEvent: async () => {},
|
|
77
77
|
share: {
|
|
78
78
|
copyLink: false,
|
|
79
79
|
copySourceMd: false,
|
|
@@ -196,12 +196,12 @@ class PanelForm {
|
|
|
196
196
|
<img
|
|
197
197
|
class="abs center"
|
|
198
198
|
style="${renderCssAttr({
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
style: {
|
|
200
|
+
width: '100px',
|
|
201
|
+
height: '100px',
|
|
202
|
+
opacity: 0.2,
|
|
203
|
+
},
|
|
204
|
+
})}"
|
|
205
205
|
src="${defaultUrlImage}"
|
|
206
206
|
/>
|
|
207
207
|
`,
|
|
@@ -382,15 +382,15 @@ class PanelForm {
|
|
|
382
382
|
// It will be filtered from the tags array to keep visibility control separate from content tags
|
|
383
383
|
const tags = data.tags
|
|
384
384
|
? uniqueArray(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
385
|
+
data.tags
|
|
386
|
+
.replaceAll('/', ',')
|
|
387
|
+
.replaceAll('-', ',')
|
|
388
|
+
.replaceAll(' ', ',')
|
|
389
|
+
.split(',')
|
|
390
|
+
.map((t) => t.trim())
|
|
391
|
+
.filter((t) => t)
|
|
392
|
+
.concat(prefixTags),
|
|
393
|
+
)
|
|
394
394
|
: prefixTags;
|
|
395
395
|
let originObj, originFileObj, indexOriginObj;
|
|
396
396
|
if (editId) {
|
|
@@ -432,8 +432,8 @@ class PanelForm {
|
|
|
432
432
|
// In edit mode, null means user cleared the file - we need to tell server to remove it
|
|
433
433
|
const isFileCleared = data.fileId === null && editId;
|
|
434
434
|
await (async () => {
|
|
435
|
-
// When file is null
|
|
436
|
-
if (!file && !isFileCleared) return;
|
|
435
|
+
// When file is null, no markdown content, and not clearing a file, skip upload
|
|
436
|
+
if (!file && !isFileCleared && !hasMdContent) return;
|
|
437
437
|
// When user cleared file in edit mode, set fileId=null so server removes the reference
|
|
438
438
|
if (isFileCleared) {
|
|
439
439
|
fileId = null;
|
|
@@ -489,8 +489,8 @@ class PanelForm {
|
|
|
489
489
|
message: documentMessage,
|
|
490
490
|
data: documentData,
|
|
491
491
|
} = originObj && indexFormDoc === 0
|
|
492
|
-
|
|
493
|
-
|
|
492
|
+
? await DocumentService.put({ id: originObj._id, body })
|
|
493
|
+
: await DocumentService.post({
|
|
494
494
|
body,
|
|
495
495
|
});
|
|
496
496
|
const newDoc = {
|
|
@@ -518,12 +518,12 @@ class PanelForm {
|
|
|
518
518
|
fileId: {
|
|
519
519
|
fileBlob: file
|
|
520
520
|
? {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
521
|
+
data: {
|
|
522
|
+
data: await getDataFromInputFile(file),
|
|
523
|
+
},
|
|
524
|
+
mimetype: file.type,
|
|
525
|
+
name: file.name,
|
|
526
|
+
}
|
|
527
527
|
: undefined,
|
|
528
528
|
filePlain: undefined,
|
|
529
529
|
},
|
|
@@ -742,36 +742,36 @@ class PanelForm {
|
|
|
742
742
|
<div
|
|
743
743
|
class="in fll ssr-shimmer-search-box"
|
|
744
744
|
style="${renderCssAttr({
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
745
|
+
style: {
|
|
746
|
+
width: '80%',
|
|
747
|
+
height: '30px',
|
|
748
|
+
top: '-13px',
|
|
749
|
+
left: '10px',
|
|
750
|
+
},
|
|
751
|
+
})}"
|
|
752
752
|
></div>
|
|
753
753
|
</div>`,
|
|
754
754
|
createdAt: html`<div class="fl">
|
|
755
755
|
<div
|
|
756
756
|
class="in fll ssr-shimmer-search-box"
|
|
757
757
|
style="${renderCssAttr({
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
758
|
+
style: {
|
|
759
|
+
width: '50%',
|
|
760
|
+
height: '30px',
|
|
761
|
+
left: '-5px',
|
|
762
|
+
},
|
|
763
|
+
})}"
|
|
764
764
|
></div>
|
|
765
765
|
</div>`,
|
|
766
766
|
mdFileId: html`<div class="fl section-mp">
|
|
767
767
|
<div
|
|
768
768
|
class="in fll ssr-shimmer-search-box"
|
|
769
769
|
style="${renderCssAttr({
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
770
|
+
style: {
|
|
771
|
+
width: '80%',
|
|
772
|
+
height: '30px',
|
|
773
|
+
},
|
|
774
|
+
})}"
|
|
775
775
|
></div>
|
|
776
776
|
</div>`.repeat(random(2, 4)),
|
|
777
777
|
ssr: true,
|
|
@@ -109,7 +109,7 @@ export const ENTITY_TYPE_TO_ITEM_TYPES = Object.freeze({
|
|
|
109
109
|
export const QUEST_STEPS_TYPES = Object.freeze(['collect', 'talk', 'kill']);
|
|
110
110
|
|
|
111
111
|
/** Action categories accepted by the cyberia-action engine. */
|
|
112
|
-
export const CYBERIA_ACTION_TYPES = Object.freeze(['craft', 'shop', 'storage', 'quest-talk']);
|
|
112
|
+
export const CYBERIA_ACTION_TYPES = Object.freeze(['craft', 'shop', 'storage', 'talk', 'quest-talk']);
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
115
|
* Canonical (itemId → itemType) registry shipped with the engine. Used
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
The Action System defines how NPC entities interact with players. An **Action** is a spatial, typed payload attached to a map entity that the player activates by tapping the NPC. Actions drive dialogue, shops, crafting, storage, and quest grant events.
|
|
10
10
|
|
|
11
|
-
> **Implementation status —
|
|
11
|
+
> **Implementation status — Alpha (talk / quest-talk):** The CyberiaAction and CyberiaDialogue MongoDB schemas and Engine REST API (`src/api/cyberia-action`, `src/api/cyberia-dialogue`) are defined. The `talk` and `quest-talk` paths are wired end-to-end: the Go server binds actions to entities at instance init, validates dialogue completion, grants quests, and advances `talk` objectives (see **Dialogue Interaction Protocol** below). Shop / craft / storage transaction processing remains planned for a later Alpha increment. The `freeze_start`/`freeze_end` WS messages for modal protection are implemented; dialogue freeze now rides on the `dlg_*` frames.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -154,6 +154,60 @@ sequenceDiagram
|
|
|
154
154
|
|
|
155
155
|
---
|
|
156
156
|
|
|
157
|
+
## Dialogue Interaction Protocol (talk / quest-talk)
|
|
158
|
+
|
|
159
|
+
Tapping an interaction bubble opens the Raylib-native **`modal_interact`** modal
|
|
160
|
+
first — the general-purpose entry point. Its `[Talk]` tab (shown only when the
|
|
161
|
+
entity has dialogue) opens `modal_dialogue`; its `[Chat / Profile]` tab opens the
|
|
162
|
+
JS overlay with no freeze.
|
|
163
|
+
|
|
164
|
+
The client is identical for `talk` and `quest-talk`; the **server** branches after
|
|
165
|
+
`dlg_complete`. The client never declares the action type, quest code, or quest
|
|
166
|
+
dialogue codes — the server resolves the bound action from its own
|
|
167
|
+
`entityId → CyberiaAction` cache (bound at instance init).
|
|
168
|
+
|
|
169
|
+
### Wire messages
|
|
170
|
+
|
|
171
|
+
| Direction | Message | When | Payload |
|
|
172
|
+
| --------- | -------------- | ------------------------------------- | ------------------------------------ |
|
|
173
|
+
| C → S | `dlg_start` | `modal_dialogue` opens | `{ entityId, itemId }` |
|
|
174
|
+
| C → S | `dlg_complete` | player reads all lines, closes | `{ entityId, itemId, dialogCode }` |
|
|
175
|
+
| C → S | `dlg_cancel` | player dismisses early (✕ / outside) | `{ entityId, itemId }` |
|
|
176
|
+
| S → C | `dlg_ack` | after `dlg_complete` is processed | `{ questGranted, objectivesDone, quests[] }` |
|
|
177
|
+
|
|
178
|
+
Binary uplink opcodes: `dlg_start` `0x17`, `dlg_complete` `0x18`, `dlg_cancel`
|
|
179
|
+
`0x19` (JSON aliases of the same names are also accepted).
|
|
180
|
+
|
|
181
|
+
`dlg_ack` is notify-only — it carries the affected quest snapshot entries the
|
|
182
|
+
client upserts into its local `quest_store` (Quest Journal); it never gates
|
|
183
|
+
simulation state.
|
|
184
|
+
|
|
185
|
+
### Server `dlg_complete` handling
|
|
186
|
+
|
|
187
|
+
1. Validate `player.activeDialogueEntityID == msg.entityId`; drop otherwise.
|
|
188
|
+
2. Clear the dialogue context and thaw the player (modal protection off).
|
|
189
|
+
3. Resolve the action from `actionCache[entityId]`. `talk` → ack only.
|
|
190
|
+
4. `quest-talk`: on first contact grant `grantQuestCode`; then for every active
|
|
191
|
+
quest whose **current step** has a `{ type: 'talk', itemId == provideItemId }`
|
|
192
|
+
objective, increment it — **only** when `msg.dialogCode` is in the action's
|
|
193
|
+
`questDialogueCodes`. On quest completion, deliver rewards (FCT) and unlock
|
|
194
|
+
successors.
|
|
195
|
+
|
|
196
|
+
> **Dialogue-code contract.** The C client fetches dialogue groups at
|
|
197
|
+
> `/api/cyberia-dialogue/code/default-<itemId>`, so the code it reports on
|
|
198
|
+
> `dlg_complete` is `default-<provideItemId>`. For a `quest-talk` objective to
|
|
199
|
+
> advance, the action's `questDialogueCodes` must contain that code.
|
|
200
|
+
|
|
201
|
+
### Freeze semantics
|
|
202
|
+
|
|
203
|
+
| Event | Player state |
|
|
204
|
+
| ---------------------------------- | ------------------------- |
|
|
205
|
+
| `modal_interact` open | Active (no freeze) |
|
|
206
|
+
| `dlg_start` sent | Frozen — immune to damage |
|
|
207
|
+
| `dlg_complete` / `dlg_cancel` sent | Unfrozen |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
157
211
|
## Spatial Binding and Instance Init
|
|
158
212
|
|
|
159
213
|
`sourceMapCode + sourceCellX + sourceCellY` links an Action to a specific entity cell in a specific map. During instance initialization:
|