@underpostnet/cyberia 3.3.73 → 3.3.76
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 +17 -5
- package/.github/workflows/coverall.cyberia.ci.yml +5 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
- package/.github/workflows/publish.cyberia.ci.yml +44 -0
- package/AGENTS.md +61 -12
- package/CHANGELOG.md +126 -41
- package/CLI-HELP.md +19 -12
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +2 -2
- package/README.md +28 -28
- package/bin/build.js +37 -17
- package/bin/cyberia.js +1247 -1373
- package/bin/index.js +1247 -1373
- package/compose.env +17 -5
- package/conf.js +7 -3
- package/deploy/cyberia-client/deploy.sh +10 -11
- package/deploy/cyberia-client/package.sh +0 -1
- package/deploy/cyberia-client/state.sh +3 -2
- package/deploy/cyberia-server/deploy.sh +10 -11
- package/deploy/cyberia-server/package.sh +0 -1
- package/deploy/cyberia-server/state.sh +3 -2
- package/deploy/dd-cyberia/deploy.sh +5 -5
- package/deploy/dd-cyberia/init.sh +15 -14
- package/deploy/dd-cyberia/package.sh +0 -1
- package/deploy/dd-cyberia/state.sh +2 -2
- package/deploy/dd-cyberia/sync-deploy.sh +139 -75
- package/deploy/lib/config.sh +29 -0
- package/deploy/lib/github-actions-logging.sh +92 -76
- package/deploy/lib/host.sh +27 -18
- package/deploy/lib/state.sh +4 -1
- package/deploy/pwa-microservices-template/deploy.sh +10 -8
- package/deploy/release/deploy.sh +0 -2
- package/deployment.yaml +1 -1
- package/docker-compose.yml +23 -17
- package/hardhat/package-lock.json +2 -2
- package/hardhat/package.json +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/mongo-express/deployment.yaml +46 -27
- package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
- package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
- package/manifests/deployment/mongo-express/service.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
- package/package.json +19 -17
- package/scripts/test-monitor.sh +3 -3
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
- package/src/api/cyberia-action/cyberia-action.router.js +2 -2
- package/src/api/cyberia-action/cyberia-action.service.js +3 -4
- package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
- package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
- package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
- package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
- package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
- package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
- package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
- package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
- package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
- package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
- package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
- package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
- package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
- package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
- package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
- package/src/api/cyberia-map/cyberia-map.service.js +6 -2
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
- package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
- package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
- package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
- package/src/api/file/file.ref.js +153 -0
- package/src/api/file/file.ref.json +11 -2
- package/src/api/object-layer/object-layer.model.js +47 -12
- package/src/api/object-layer/object-layer.router.js +10 -3
- package/src/api/object-layer/object-layer.service.js +113 -120
- package/src/cli/app.js +12 -1
- package/src/cli/client.js +2 -2
- package/src/cli/cluster.js +7 -0
- package/src/cli/deploy.js +4 -4
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/fs-selection.js +115 -0
- package/src/cli/fs.js +158 -342
- package/src/cli/index.js +22 -10
- package/src/cli/repository.js +28 -0
- package/src/cli/run.js +71 -197
- package/src/cli/test.js +4 -0
- package/src/cli/wireguard.js +32 -31
- package/src/client/components/core/Docs.js +77 -100
- package/src/client/components/core/Modal.js +56 -1
- package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
- package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
- package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
- package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
- package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
- package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
- package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
- package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
- package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
- package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
- package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
- package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
- package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
- package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
- package/src/client/services/object-layer/object-layer.management.js +6 -10
- package/src/client/services/object-layer/object-layer.service.js +3 -1
- package/src/client-builder/client-build-docs.js +52 -52
- package/src/client-builder/client-build.js +67 -44
- package/src/client-builder/client-bundle.js +347 -0
- package/src/db/mongo/MongoExpress.js +187 -0
- package/src/grpc/cyberia/grpc-server.js +5 -7
- package/src/index.js +1 -1
- package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
- package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
- package/src/projects/cyberia/besu-genesis-generator.js +5 -14
- package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
- package/src/projects/cyberia/catalog-cyberia.js +3 -2
- package/src/projects/cyberia/gemini-client.js +3 -3
- package/src/projects/cyberia/generate-saga.js +4 -11
- package/src/projects/cyberia/hot-reload-trigger.js +17 -28
- package/src/projects/cyberia/instance-backup.js +210 -0
- package/src/projects/cyberia/instance-data.js +177 -257
- package/src/projects/cyberia/map-preview-generator.js +32 -68
- package/src/projects/cyberia/object-layer.js +152 -319
- package/src/projects/cyberia/seed-audio.js +139 -0
- package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
- package/src/projects/cyberia/semantic-layer-generator.js +2 -18
- package/src/projects/cyberia/stat-balance.js +81 -0
- package/src/projects/cyberia/stat-commands.js +23 -0
- package/src/projects/cyberia/stat-contract-generator.js +101 -0
- package/src/runtime/cyberia-client/Dockerfile +8 -4
- package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
- package/src/runtime/cyberia-server/Dockerfile +9 -2
- package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
- package/src/runtime/engine-cyberia/Dockerfile +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
- package/src/runtime/engine-cyberia/compose.env +17 -5
- package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
- package/src/server/build/coverage.js +113 -42
- package/src/server/build/package.js +55 -12
- package/src/server/build/testing.js +59 -9
- package/src/server/network/middlewares.js +8 -2
- package/src/server/ops/logger.js +70 -35
- package/src/server/runtime/conf.js +4 -2
- package/src/server/storage/data-query.js +16 -0
- package/src/server/storage/downloader.js +62 -14
- package/src/server/storage/zip.js +153 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
- package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
- package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
- package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
- package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
- package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
- package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
- package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
- package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
- package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
- package/test/integration/app/cyberia/seed-audio.test.js +275 -0
- package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
- package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
- package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
- package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
- package/test/unit/client-build-docs.test.js +97 -70
- package/test/unit/client-bundle.test.js +313 -0
- package/test/unit/conf-resolution.test.js +17 -0
- package/test/unit/coverage-artifact.test.js +120 -29
- package/test/unit/cyberia/instance-backup.test.js +183 -0
- package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
- package/test/unit/cyberia/publish-workflow.test.js +22 -0
- package/test/unit/cyberia/stat-balance.test.js +69 -0
- package/test/unit/deploy-log-table.test.js +76 -5
- package/test/unit/downloader-integrity.test.js +98 -0
- package/test/unit/file-reference-registry.test.js +141 -0
- package/test/unit/fs-storage-paths.test.js +596 -0
- package/test/unit/logger-redaction.test.js +29 -0
- package/test/unit/package.test.js +70 -16
- package/test/unit/prepare-host.test.js +84 -1
- package/test/unit/test-tiers.test.js +27 -0
- package/test/unit/zip-archive.test.js +142 -0
- package/vitest.config.js +11 -3
- package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
- package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
|
@@ -15,13 +15,18 @@ import { loggerFactory } from '../../server/ops/logger.js';
|
|
|
15
15
|
import { ObjectLayerRenderFramesDto } from '../object-layer-render-frames/object-layer-render-frames.model.js';
|
|
16
16
|
import { FileFactory } from '../file/file.service.js';
|
|
17
17
|
import fs from 'fs-extra';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import sharp from 'sharp';
|
|
21
|
+
import { validateStats } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
|
|
18
22
|
import { ObjectLayerDto } from './object-layer.model.js';
|
|
19
23
|
import { ObjectLayerEngine } from '../../projects/cyberia/object-layer.js';
|
|
20
24
|
import { shellExec } from '../../server/runtime/process.js';
|
|
21
25
|
import { DataQuery } from '../../server/storage/data-query.js';
|
|
22
26
|
import { AtlasSpriteSheetService } from '../atlas-sprite-sheet/atlas-sprite-sheet.service.js';
|
|
27
|
+
import { AtlasSpriteSheetStore } from '../../projects/cyberia/atlas-sprite-sheet-store.js';
|
|
23
28
|
import { IpfsClient } from '../../projects/cyberia/ipfs-client.js';
|
|
24
|
-
import {
|
|
29
|
+
import { removePinRecordsAndUnpin } from '../ipfs/ipfs.service.js';
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* Logger instance for this module.
|
|
@@ -45,8 +50,8 @@ class ObjectLayerService {
|
|
|
45
50
|
* - `/metadata/:itemType/:itemId` — Create an object layer from uploaded frames and metadata.
|
|
46
51
|
* - Default — Create an object layer directly from the request body.
|
|
47
52
|
*
|
|
48
|
-
* The `/metadata` and default routes delegate to {@link ObjectLayerEngine.
|
|
49
|
-
* for document
|
|
53
|
+
* The `/metadata` and default routes delegate to {@link ObjectLayerEngine.persistObjectLayerDocuments}
|
|
54
|
+
* for the document write, atlas generation, SHA-256 computation, and IPFS pinning.
|
|
50
55
|
*
|
|
51
56
|
* @async
|
|
52
57
|
* @function post
|
|
@@ -160,22 +165,12 @@ class ObjectLayerService {
|
|
|
160
165
|
metadataOverride: req.body,
|
|
161
166
|
});
|
|
162
167
|
|
|
163
|
-
const { objectLayer } = await ObjectLayerEngine.
|
|
168
|
+
const { objectLayer } = await ObjectLayerEngine.persistObjectLayerDocuments({
|
|
164
169
|
ObjectLayer,
|
|
165
170
|
ObjectLayerRenderFrames,
|
|
166
171
|
objectLayerRenderFramesData,
|
|
167
172
|
objectLayerData,
|
|
168
|
-
|
|
169
|
-
generateAtlas: true,
|
|
170
|
-
atlasServiceContext: {
|
|
171
|
-
req,
|
|
172
|
-
res,
|
|
173
|
-
options,
|
|
174
|
-
AtlasSpriteSheetService,
|
|
175
|
-
IpfsClient,
|
|
176
|
-
createPinRecord,
|
|
177
|
-
},
|
|
178
|
-
},
|
|
173
|
+
persistOptions: { options },
|
|
179
174
|
});
|
|
180
175
|
|
|
181
176
|
return objectLayer;
|
|
@@ -191,44 +186,32 @@ class ObjectLayerService {
|
|
|
191
186
|
bodyData.data.render.cid = '';
|
|
192
187
|
bodyData.data.render.metadataCid = '';
|
|
193
188
|
|
|
194
|
-
//
|
|
189
|
+
// Stage the atlas CIDs before the write, so the document is never stored without them.
|
|
195
190
|
if (bodyData.objectLayerRenderFramesId) {
|
|
196
191
|
const renderFramesDoc = await ObjectLayerRenderFrames.findById(bodyData.objectLayerRenderFramesId);
|
|
197
192
|
if (renderFramesDoc) {
|
|
198
193
|
try {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
};
|
|
203
|
-
const result = await AtlasSpriteSheetService.generate(
|
|
204
|
-
{ objectLayer: stagingOL, auth: req.auth },
|
|
205
|
-
res,
|
|
194
|
+
const { atlasDoc, atlasCid, atlasMetadataCid } = await AtlasSpriteSheetStore.persist({
|
|
195
|
+
itemKey: bodyData.data.item.id,
|
|
196
|
+
objectLayerRenderFrames: renderFramesDoc,
|
|
206
197
|
options,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
bodyData.data.render.
|
|
210
|
-
bodyData.
|
|
211
|
-
bodyData.atlasSpriteSheetId = result.atlasDoc._id;
|
|
198
|
+
});
|
|
199
|
+
bodyData.data.render.cid = atlasCid;
|
|
200
|
+
bodyData.data.render.metadataCid = atlasMetadataCid;
|
|
201
|
+
bodyData.atlasSpriteSheetId = atlasDoc._id;
|
|
212
202
|
} catch (atlasError) {
|
|
213
203
|
logger.error('Failed to auto-generate atlas for new ObjectLayer:', atlasError);
|
|
214
204
|
}
|
|
215
205
|
}
|
|
216
206
|
}
|
|
217
207
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
bodyData.cid = ipfsResult.cid;
|
|
225
|
-
await createPinRecord({ cid: ipfsResult.cid, resourceType: 'object-layer-data', mfsPath, options });
|
|
226
|
-
}
|
|
227
|
-
} catch (ipfsError) {
|
|
228
|
-
logger.warn('Failed to pin data JSON to IPFS:', ipfsError.message);
|
|
229
|
-
}
|
|
208
|
+
const dataCid = await ObjectLayerEngine.pinObjectLayerData({
|
|
209
|
+
data: bodyData.data,
|
|
210
|
+
itemId: bodyData.data.item.id,
|
|
211
|
+
options,
|
|
212
|
+
});
|
|
213
|
+
if (dataCid) bodyData.cid = dataCid;
|
|
230
214
|
|
|
231
|
-
// Atomic create/replace – ObjectLayer is fully populated with all CIDs
|
|
232
215
|
return await (await ObjectLayer.upsertByItemId(bodyData)).populate('objectLayerRenderFramesId');
|
|
233
216
|
};
|
|
234
217
|
|
|
@@ -257,25 +240,33 @@ class ObjectLayerService {
|
|
|
257
240
|
/** @type {import('./object-layer.model.js').ObjectLayerModel} */
|
|
258
241
|
const ObjectLayer = DataBaseProviderService.getModel('ObjectLayer', options);
|
|
259
242
|
|
|
260
|
-
//
|
|
243
|
+
// Object layers are addressed by item id, the key the editor navigates by.
|
|
244
|
+
const keyFilter = (key) => DataQuery.naturalKeyFilter('data.item.id', key);
|
|
245
|
+
|
|
246
|
+
// GET /search-item-ids - item identity, by prefix (`q`, for type-ahead) or by exact ids
|
|
247
|
+
// (`ids`, comma separated). Carries the item's type because that is what addresses its
|
|
248
|
+
// sprite directory; an id alone cannot be previewed.
|
|
261
249
|
if (req.path.startsWith('/search-item-ids')) {
|
|
262
250
|
const q = (req.query.q || '').trim();
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
251
|
+
const ids = String(req.query.ids || '')
|
|
252
|
+
.split(',')
|
|
253
|
+
.map((id) => id.trim())
|
|
254
|
+
.filter(Boolean);
|
|
255
|
+
if (!q && ids.length === 0) return { items: [] };
|
|
256
|
+
const query = ids.length
|
|
257
|
+
? { 'data.item.id': { $in: ids } }
|
|
258
|
+
: // Escape regex special characters for safe partial matching
|
|
259
|
+
{ 'data.item.id': { $regex: q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' } };
|
|
260
|
+
const results = await ObjectLayer.find(query, { 'data.item.id': 1, 'data.item.type': 1, _id: 0 })
|
|
261
|
+
.limit(ids.length ? ids.length : 20)
|
|
271
262
|
.lean();
|
|
272
|
-
const
|
|
273
|
-
return {
|
|
263
|
+
const byId = new Map(results.map((r) => [r.data.item.id, r.data.item.type || '']));
|
|
264
|
+
return { items: [...byId].map(([id, type]) => ({ id, type })) };
|
|
274
265
|
}
|
|
275
266
|
|
|
276
267
|
// GET /frame-counts/:id - Get frame counts for each direction using numeric codes
|
|
277
268
|
if (req.path.startsWith('/frame-counts/')) {
|
|
278
|
-
const objectLayer = await ObjectLayer.
|
|
269
|
+
const objectLayer = await ObjectLayer.findOne(keyFilter(req.params.id))
|
|
279
270
|
.select(ObjectLayerDto.select.getMetadata())
|
|
280
271
|
.populate('objectLayerRenderFramesId');
|
|
281
272
|
if (!objectLayer) {
|
|
@@ -318,7 +309,7 @@ class ObjectLayerService {
|
|
|
318
309
|
|
|
319
310
|
// GET /render/:id - Get only render data for specific object layer
|
|
320
311
|
if (req.path.startsWith('/render/')) {
|
|
321
|
-
const objectLayer = await ObjectLayer.
|
|
312
|
+
const objectLayer = await ObjectLayer.findOne(keyFilter(req.params.id))
|
|
322
313
|
.select(ObjectLayerDto.select.getRender())
|
|
323
314
|
.populate('objectLayerRenderFramesId');
|
|
324
315
|
if (!objectLayer) {
|
|
@@ -333,7 +324,7 @@ class ObjectLayerService {
|
|
|
333
324
|
|
|
334
325
|
// GET /metadata/:id - Get only metadata (no render frames/colors) for specific object layer
|
|
335
326
|
if (req.path.startsWith('/metadata/')) {
|
|
336
|
-
const objectLayer = await ObjectLayer.
|
|
327
|
+
const objectLayer = await ObjectLayer.findOne(keyFilter(req.params.id))
|
|
337
328
|
.select(ObjectLayerDto.select.getMetadata())
|
|
338
329
|
.populate('objectLayerRenderFramesId', ObjectLayerRenderFramesDto.select.get());
|
|
339
330
|
if (!objectLayer) {
|
|
@@ -385,22 +376,14 @@ class ObjectLayerService {
|
|
|
385
376
|
}
|
|
386
377
|
|
|
387
378
|
// GET / - Get paginated list of object layers
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (objectLayer) {
|
|
397
|
-
logger.info(`ObjectLayerService.get - found record by id: ${id}`);
|
|
398
|
-
return { data: [objectLayer], total: 1, page: 1, totalPages: 1 };
|
|
399
|
-
}
|
|
400
|
-
logger.warn(`ObjectLayerService.get - record NOT found for id: ${id}`);
|
|
401
|
-
} catch (e) {
|
|
402
|
-
logger.warn(`Invalid ID format or not found: ${id}`);
|
|
403
|
-
}
|
|
379
|
+
const key = req.params.id || req.query.id;
|
|
380
|
+
if (key && key !== 'undefined' && !['render', 'metadata', 'frame-counts'].includes(key)) {
|
|
381
|
+
const objectLayer = await ObjectLayer.findOne(keyFilter(key))
|
|
382
|
+
.select(ObjectLayerDto.select.get())
|
|
383
|
+
.populate('atlasSpriteSheetId', 'cid')
|
|
384
|
+
.populate('objectLayerRenderFramesId', ObjectLayerRenderFramesDto.select.get());
|
|
385
|
+
if (objectLayer) return { data: [objectLayer], total: 1, page: 1, totalPages: 1 };
|
|
386
|
+
logger.warn(`ObjectLayerService.get - no object layer for key: ${key}`);
|
|
404
387
|
}
|
|
405
388
|
|
|
406
389
|
const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
|
|
@@ -493,9 +476,14 @@ class ObjectLayerService {
|
|
|
493
476
|
// Create temporary output file path
|
|
494
477
|
const tempOutputPath = `${framesFolder}/${tmpOutputFileName}`;
|
|
495
478
|
|
|
479
|
+
// img2webp takes one frame size, and frames of one direction may differ: the game
|
|
480
|
+
// stretches each to the entity's cell box, so the preview stretches them to the
|
|
481
|
+
// largest one — nearest-neighbour, since a scaled frame is still pixel art.
|
|
482
|
+
const stagedFolder = await ObjectLayerService.stageUniformFrames(framesFolder, pngFiles);
|
|
483
|
+
|
|
496
484
|
try {
|
|
497
485
|
// Change to the frames directory and execute img2webp command
|
|
498
|
-
const cmd = `cd "${framesFolder}" && img2webp -d ${frameDuration} -loop 0 *.png -o ${
|
|
486
|
+
const cmd = `cd "${stagedFolder ?? framesFolder}" && img2webp -d ${frameDuration} -loop 0 *.png -o "${path.resolve(tempOutputPath)}"`;
|
|
499
487
|
|
|
500
488
|
logger.info(`Executing command: ${cmd}`);
|
|
501
489
|
|
|
@@ -546,9 +534,41 @@ class ObjectLayerService {
|
|
|
546
534
|
}
|
|
547
535
|
|
|
548
536
|
throw error;
|
|
537
|
+
} finally {
|
|
538
|
+
if (stagedFolder) await fs.remove(stagedFolder);
|
|
549
539
|
}
|
|
550
540
|
};
|
|
551
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Stages copies of the frames resized to one common size when they differ, or returns null
|
|
544
|
+
* when they already share one and can be encoded in place.
|
|
545
|
+
* @function stageUniformFrames
|
|
546
|
+
* @memberof ObjectLayerService
|
|
547
|
+
* @param {string} framesFolder - Directory holding the direction's PNG frames.
|
|
548
|
+
* @param {string[]} pngFiles - Frame file names, in animation order.
|
|
549
|
+
* @returns {Promise<string|null>} Temporary directory with the resized frames, or null.
|
|
550
|
+
*/
|
|
551
|
+
static stageUniformFrames = async (framesFolder, pngFiles) => {
|
|
552
|
+
const sizes = await Promise.all(
|
|
553
|
+
pngFiles.map(async (file) => {
|
|
554
|
+
const { width, height } = await sharp(`${framesFolder}/${file}`).metadata();
|
|
555
|
+
return { file, width, height };
|
|
556
|
+
}),
|
|
557
|
+
);
|
|
558
|
+
const width = Math.max(...sizes.map((size) => size.width));
|
|
559
|
+
const height = Math.max(...sizes.map((size) => size.height));
|
|
560
|
+
if (sizes.every((size) => size.width === width && size.height === height)) return null;
|
|
561
|
+
|
|
562
|
+
const stagedFolder = await fs.mkdtemp(path.join(os.tmpdir(), 'ol-webp-'));
|
|
563
|
+
for (const { file } of sizes) {
|
|
564
|
+
await sharp(`${framesFolder}/${file}`)
|
|
565
|
+
.resize(width, height, { fit: 'fill', kernel: 'nearest' })
|
|
566
|
+
.toFile(`${stagedFolder}/${file}`);
|
|
567
|
+
}
|
|
568
|
+
logger.info(`Staged ${pngFiles.length} frame(s) at ${width}x${height} for webp encoding`);
|
|
569
|
+
return stagedFolder;
|
|
570
|
+
};
|
|
571
|
+
|
|
552
572
|
/**
|
|
553
573
|
* PUT handler for updating object layers, their frame images, and metadata.
|
|
554
574
|
*
|
|
@@ -557,8 +577,8 @@ class ObjectLayerService {
|
|
|
557
577
|
* - `/:id/metadata/:itemType/:itemId` — Update metadata and reprocess all frames.
|
|
558
578
|
* - `/:id` — Standard update from request body.
|
|
559
579
|
*
|
|
560
|
-
* The `/metadata` route delegates to {@link ObjectLayerEngine.
|
|
561
|
-
* for document
|
|
580
|
+
* The `/metadata` route delegates to {@link ObjectLayerEngine.persistObjectLayerDocuments}
|
|
581
|
+
* for the document write, atlas regeneration, SHA-256 computation, and IPFS pinning.
|
|
562
582
|
*
|
|
563
583
|
* @async
|
|
564
584
|
* @function put
|
|
@@ -652,7 +672,6 @@ class ObjectLayerService {
|
|
|
652
672
|
|
|
653
673
|
// PUT /:id/metadata/:itemType/:itemId - Update object layer metadata and reprocess all frames
|
|
654
674
|
if (req.path.includes('/metadata/')) {
|
|
655
|
-
const objectLayerId = req.params.id;
|
|
656
675
|
const itemType = req.params.itemType;
|
|
657
676
|
const itemId = req.params.itemId;
|
|
658
677
|
|
|
@@ -683,24 +702,12 @@ class ObjectLayerService {
|
|
|
683
702
|
metadataOverride: req.body,
|
|
684
703
|
});
|
|
685
704
|
|
|
686
|
-
|
|
687
|
-
const { objectLayer } = await ObjectLayerEngine.updateObjectLayerDocuments({
|
|
688
|
-
objectLayerId,
|
|
705
|
+
const { objectLayer } = await ObjectLayerEngine.persistObjectLayerDocuments({
|
|
689
706
|
ObjectLayer,
|
|
690
707
|
ObjectLayerRenderFrames,
|
|
691
708
|
objectLayerRenderFramesData,
|
|
692
709
|
objectLayerData,
|
|
693
|
-
|
|
694
|
-
generateAtlas: true,
|
|
695
|
-
atlasServiceContext: {
|
|
696
|
-
req,
|
|
697
|
-
res,
|
|
698
|
-
options,
|
|
699
|
-
AtlasSpriteSheetService,
|
|
700
|
-
IpfsClient,
|
|
701
|
-
createPinRecord,
|
|
702
|
-
},
|
|
703
|
-
},
|
|
710
|
+
persistOptions: { options },
|
|
704
711
|
});
|
|
705
712
|
|
|
706
713
|
return objectLayer;
|
|
@@ -715,25 +722,19 @@ class ObjectLayerService {
|
|
|
715
722
|
// Apply body updates to staging data in memory
|
|
716
723
|
const updateData = { ...req.body };
|
|
717
724
|
const stagingData = updateData.data || existingOL.data.toObject();
|
|
725
|
+
stagingData.stats = validateStats(stagingData.stats);
|
|
718
726
|
if (!stagingData.render) stagingData.render = {};
|
|
719
727
|
|
|
720
|
-
|
|
721
|
-
const renderFramesData = existingOL.objectLayerRenderFramesId;
|
|
722
|
-
if (renderFramesData) {
|
|
728
|
+
if (existingOL.objectLayerRenderFramesId) {
|
|
723
729
|
try {
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
};
|
|
728
|
-
const result = await AtlasSpriteSheetService.generate(
|
|
729
|
-
{ objectLayer: stagingOL, auth: req.auth },
|
|
730
|
-
res,
|
|
730
|
+
const { atlasDoc, atlasCid, atlasMetadataCid } = await AtlasSpriteSheetStore.persist({
|
|
731
|
+
itemKey: stagingData.item.id,
|
|
732
|
+
objectLayerRenderFrames: existingOL.objectLayerRenderFramesId,
|
|
731
733
|
options,
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
stagingData.render.
|
|
735
|
-
|
|
736
|
-
updateData.atlasSpriteSheetId = result.atlasDoc._id;
|
|
734
|
+
});
|
|
735
|
+
stagingData.render.cid = atlasCid;
|
|
736
|
+
stagingData.render.metadataCid = atlasMetadataCid;
|
|
737
|
+
updateData.atlasSpriteSheetId = atlasDoc._id;
|
|
737
738
|
} catch (atlasError) {
|
|
738
739
|
logger.error('Failed to auto-update atlas for ObjectLayer:', atlasError);
|
|
739
740
|
}
|
|
@@ -742,25 +743,17 @@ class ObjectLayerService {
|
|
|
742
743
|
updateData.data = stagingData;
|
|
743
744
|
updateData.sha256 = ObjectLayerEngine.computeSha256(stagingData);
|
|
744
745
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
updateData.cid = ipfsResult.cid;
|
|
752
|
-
await createPinRecord({ cid: ipfsResult.cid, resourceType: 'object-layer-data', mfsPath, options });
|
|
753
|
-
}
|
|
754
|
-
} catch (ipfsError) {
|
|
755
|
-
logger.warn('Failed to pin data JSON to IPFS:', ipfsError.message);
|
|
756
|
-
}
|
|
746
|
+
const dataCid = await ObjectLayerEngine.pinObjectLayerData({
|
|
747
|
+
data: stagingData,
|
|
748
|
+
itemId: stagingData.item.id,
|
|
749
|
+
options,
|
|
750
|
+
});
|
|
751
|
+
if (dataCid) updateData.cid = dataCid;
|
|
757
752
|
|
|
758
|
-
|
|
759
|
-
let updatedObjectLayer = await ObjectLayer.findByIdAndUpdate(req.params.id, updateData, {
|
|
753
|
+
return await ObjectLayer.findByIdAndUpdate(req.params.id, updateData, {
|
|
760
754
|
returnDocument: 'after',
|
|
755
|
+
runValidators: true,
|
|
761
756
|
}).populate('objectLayerRenderFramesId');
|
|
762
|
-
|
|
763
|
-
return updatedObjectLayer;
|
|
764
757
|
};
|
|
765
758
|
|
|
766
759
|
/**
|
package/src/cli/app.js
CHANGED
|
@@ -205,14 +205,25 @@ class UnderpostApp {
|
|
|
205
205
|
// The resolution loadConf performed, not `ociEnv()`, which forces the overlay on: a
|
|
206
206
|
// skipped overlay and an applied one were indistinguishable in the pod log.
|
|
207
207
|
const resolved = deployEnvContentFactory(deployId, context.env, `${context.args?.['sub-conf'] ?? ''}`.trim());
|
|
208
|
+
const inContainer = Underpost.state.isInsideContainer();
|
|
208
209
|
logger.info('Deployment environment loaded', {
|
|
209
210
|
deployId,
|
|
210
211
|
instanceId: instanceId || null,
|
|
211
212
|
source: envPath,
|
|
212
|
-
inContainer
|
|
213
|
+
inContainer,
|
|
213
214
|
ociOverlay: resolved.overlay,
|
|
214
215
|
keys: Object.keys(values).length,
|
|
215
216
|
});
|
|
217
|
+
// A container that resolves no overlay runs on the host-scoped endpoints of the base file —
|
|
218
|
+
// a loopback database, a local cache — and fails minutes later at the first connection,
|
|
219
|
+
// pointing at the endpoint rather than at the conf checkout that is missing the overlay.
|
|
220
|
+
// Named rather than thrown: a deployment is allowed to have no overlay for an environment.
|
|
221
|
+
if (inContainer && !instanceId && !resolved.overlay)
|
|
222
|
+
logger.warn('No OCI env overlay resolved; container is running on host-scoped values', {
|
|
223
|
+
deployId,
|
|
224
|
+
env: context.env,
|
|
225
|
+
expected: deployOciEnvFilePath(deployId, context.env),
|
|
226
|
+
});
|
|
216
227
|
return { source: envPath, keys: Object.keys(values).length };
|
|
217
228
|
},
|
|
218
229
|
|
package/src/cli/client.js
CHANGED
|
@@ -83,7 +83,7 @@ class UnderpostClient {
|
|
|
83
83
|
const env = deployEnvFactory(options, process.env.NODE_ENV || 'development');
|
|
84
84
|
process.env.NODE_ENV = env;
|
|
85
85
|
if (options.mergeZip) {
|
|
86
|
-
mergeClientBuildZip({
|
|
86
|
+
await mergeClientBuildZip({
|
|
87
87
|
buildPrefix: options.mergeZip,
|
|
88
88
|
logger,
|
|
89
89
|
});
|
|
@@ -91,7 +91,7 @@ class UnderpostClient {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
if (options.unzip) {
|
|
94
|
-
unzipClientBuild({
|
|
94
|
+
await unzipClientBuild({
|
|
95
95
|
buildPrefix: options.unzip,
|
|
96
96
|
logger,
|
|
97
97
|
});
|
package/src/cli/cluster.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from '../server/network/underpost-ingress.js';
|
|
32
32
|
import { MONGODB_DEFAULT_REPLICA_COUNT } from '../db/mongo/MongooseDB.js';
|
|
33
33
|
import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
|
|
34
|
+
import { MongoExpress } from '../db/mongo/MongoExpress.js';
|
|
34
35
|
import os from 'os';
|
|
35
36
|
import fs from 'fs-extra';
|
|
36
37
|
import Underpost from '../index.js';
|
|
@@ -82,6 +83,7 @@ class UnderpostCluster {
|
|
|
82
83
|
* @param {object} [options] - Configuration options for cluster initialization.
|
|
83
84
|
* @param {boolean} [options.mongodb=false] - Deploy MongoDB.
|
|
84
85
|
* @param {boolean} [options.mongodb4=false] - Deploy MongoDB 4.4.
|
|
86
|
+
* @param {boolean} [options.mongoExpress=false] - Deploy the mongo-express web client for inspecting the MongoDB StatefulSet collections.
|
|
85
87
|
* @param {string} [options.serviceHost=''] - Set a custom host/IP for exposed MongoDB and Valkey clients.
|
|
86
88
|
* @param {boolean} [options.mariadb=false] - Deploy MariaDB.
|
|
87
89
|
* @param {boolean} [options.mysql=false] - Deploy MySQL.
|
|
@@ -129,6 +131,7 @@ class UnderpostCluster {
|
|
|
129
131
|
options = {
|
|
130
132
|
mongodb: false,
|
|
131
133
|
mongodb4: false,
|
|
134
|
+
mongoExpress: false,
|
|
132
135
|
serviceHost: '',
|
|
133
136
|
mariadb: false,
|
|
134
137
|
mysql: false,
|
|
@@ -509,6 +512,10 @@ class UnderpostCluster {
|
|
|
509
512
|
});
|
|
510
513
|
}
|
|
511
514
|
|
|
515
|
+
// After both StatefulSet branches, so one invocation can bring up MongoDB and its inspector
|
|
516
|
+
// and the client reads the auth mode from the set this run just applied.
|
|
517
|
+
if (options.mongoExpress) await MongoExpress.deploy({ namespace: options.namespace, underpostRoot, options });
|
|
518
|
+
|
|
512
519
|
// Installing one stack must never break the other. Whichever is already
|
|
513
520
|
// present decides whether this install takes the node's 80/443 for itself
|
|
514
521
|
// or joins the underpost ingress, so it is read before anything is applied.
|
package/src/cli/deploy.js
CHANGED
|
@@ -147,10 +147,10 @@ const GATEWAY_DURATION_UNITS = [
|
|
|
147
147
|
];
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
* Converts an HTTPProxy duration (`
|
|
150
|
+
* Converts an HTTPProxy duration (`10000ms`, `10s`, `infinity`) into a Gateway
|
|
151
151
|
* API Duration. The Gateway API grammar allows at most 5 digits per component,
|
|
152
152
|
* so a value that overflows in one unit is re-expressed in a coarser one
|
|
153
|
-
* (`
|
|
153
|
+
* (`10000ms` → `5m`); `infinity` maps to `0s`, which disables the timeout.
|
|
154
154
|
* @param {string|number} value - Source duration.
|
|
155
155
|
* @returns {string|null} Gateway API Duration, or null when unset/unparsable.
|
|
156
156
|
*/
|
|
@@ -617,7 +617,7 @@ spec:
|
|
|
617
617
|
* @param {string} options.namespace - Kubernetes namespace for the deployment (defaults to "default").
|
|
618
618
|
* @param {string} [options.versions] - Comma-separated list of versions to deploy.
|
|
619
619
|
* @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
|
|
620
|
-
* @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "
|
|
620
|
+
* @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "10000ms", "infinity").
|
|
621
621
|
* @param {string} [options.timeoutIdle] - HTTPProxy per-route idle timeout (e.g. "10s", "infinity").
|
|
622
622
|
* @param {string} [options.retryCount] - HTTPProxy per-route retry count (e.g. 3).
|
|
623
623
|
* @param {string} [options.retryPerTryTimeout] - HTTPProxy per-route per-try timeout (e.g. "150ms").
|
|
@@ -2185,7 +2185,7 @@ ${rules}`;
|
|
|
2185
2185
|
* @param {boolean} options.disableUpdateVolume - Whether to disable volume updates.
|
|
2186
2186
|
* @param {boolean} options.disableUpdateUnderpostConfig - Whether to disable Underpost config updates.
|
|
2187
2187
|
* @param {string} [options.namespace] - Kubernetes namespace for the deployment (defaults to "default").
|
|
2188
|
-
* @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "
|
|
2188
|
+
* @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "10000ms", "infinity").
|
|
2189
2189
|
* @param {string} [options.timeoutIdle] - HTTPProxy per-route idle timeout (e.g. "10s", "infinity").
|
|
2190
2190
|
* @param {string} [options.retryCount] - HTTPProxy per-route retry count (e.g. 3).
|
|
2191
2191
|
* @param {string} [options.retryPerTryTimeout] - HTTPProxy per-route per-try timeout (e.g. "150ms").
|
|
@@ -192,7 +192,7 @@ services:
|
|
|
192
192
|
MONGO_IMAGE=mongo:latest
|
|
193
193
|
VALKEY_IMAGE=valkey/valkey:latest
|
|
194
194
|
APP_IMAGE=underpost/underpost-engine
|
|
195
|
-
APP_TAG=v3.3.
|
|
195
|
+
APP_TAG=v3.3.76
|
|
196
196
|
PROXY_IMAGE=nginx:stable-alpine
|
|
197
197
|
PROMETHEUS_IMAGE=prom/prometheus:latest
|
|
198
198
|
GRAFANA_IMAGE=grafana/grafana:latest
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import UnderpostRepository from './repository.js';
|
|
4
|
+
|
|
5
|
+
const manifestFilterNames = ['key', 'fromKey', 'toKey', 'keyRegex'];
|
|
6
|
+
|
|
7
|
+
const normalizeStoragePath = (value) => {
|
|
8
|
+
if (value === undefined || value === '') return '';
|
|
9
|
+
const absolute = path.resolve(value);
|
|
10
|
+
const relative = path.relative(process.cwd(), absolute);
|
|
11
|
+
return (
|
|
12
|
+
path.isAbsolute(value) && (relative === '..' || relative.startsWith(`..${path.sep}`)) ? absolute : relative || '.'
|
|
13
|
+
)
|
|
14
|
+
.split(path.sep)
|
|
15
|
+
.join('/');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const isWithinScope = (key, scope) => {
|
|
19
|
+
if (scope === undefined || scope === '') return true;
|
|
20
|
+
const relative = path.relative(path.resolve(scope), path.resolve(key));
|
|
21
|
+
return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const validateSelectionOptions = (scope, options) => {
|
|
25
|
+
if (options.git && options.tracked) throw new Error('Use either --git or --tracked.');
|
|
26
|
+
if (!options.tracked && !scope) throw new Error('Provide a filesystem path, or use --tracked.');
|
|
27
|
+
if (options.rm && options.pull) throw new Error('Use either --rm or --pull.');
|
|
28
|
+
if (options.omitUnzip && !options.pull) throw new Error('--omit-unzip requires --pull.');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const filterManifestEntries = (entries, options = {}, scope) => {
|
|
32
|
+
const keys = entries.map(([key]) => key);
|
|
33
|
+
const keyIndex = (name) => {
|
|
34
|
+
const index = keys.indexOf(options[name]);
|
|
35
|
+
if (index < 0) throw new Error(`Manifest key not found for ${name}: ${options[name]}`);
|
|
36
|
+
return index;
|
|
37
|
+
};
|
|
38
|
+
const start = options.fromKey === undefined ? 0 : keyIndex('fromKey');
|
|
39
|
+
const end = options.toKey === undefined ? keys.length - 1 : keyIndex('toKey');
|
|
40
|
+
if (start > end && (options.fromKey !== undefined || options.toKey !== undefined))
|
|
41
|
+
throw new Error('--from-key must precede or equal --to-key.');
|
|
42
|
+
|
|
43
|
+
let regex;
|
|
44
|
+
if (options.keyRegex !== undefined) {
|
|
45
|
+
try {
|
|
46
|
+
regex = new RegExp(options.keyRegex);
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error(`Invalid --key-regex: ${options.keyRegex}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const matches = (key) => isWithinScope(key, scope) && (!regex || regex.test(key));
|
|
52
|
+
if (options.key !== undefined) {
|
|
53
|
+
const index = keyIndex('key');
|
|
54
|
+
if (index < start || index > end || !matches(options.key))
|
|
55
|
+
throw new Error('--key conflicts with the manifest range, path scope, or regex.');
|
|
56
|
+
}
|
|
57
|
+
return entries
|
|
58
|
+
.slice(start, end + 1)
|
|
59
|
+
.filter(([key]) => matches(key) && (options.key === undefined || key === options.key));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const resolveFilesystemSelection = (scope) => {
|
|
63
|
+
const files = [];
|
|
64
|
+
const visit = (target) => {
|
|
65
|
+
const stats = fs.statSync(target);
|
|
66
|
+
if (stats.isFile()) files.push(normalizeStoragePath(target));
|
|
67
|
+
else if (stats.isDirectory()) {
|
|
68
|
+
for (const entry of fs
|
|
69
|
+
.readdirSync(target, { withFileTypes: true })
|
|
70
|
+
.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
71
|
+
const child = path.join(target, entry.name);
|
|
72
|
+
// Do not follow directory links during traversal.
|
|
73
|
+
if (entry.isSymbolicLink()) {
|
|
74
|
+
if (fs.statSync(child, { throwIfNoEntry: false })?.isFile()) files.push(normalizeStoragePath(child));
|
|
75
|
+
} else visit(child);
|
|
76
|
+
}
|
|
77
|
+
} else throw new Error(`Storage requires a regular file or directory: ${target}`);
|
|
78
|
+
};
|
|
79
|
+
visit(scope);
|
|
80
|
+
return files;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const resolveGitSelection = (files, scope) => {
|
|
84
|
+
const context = fs.statSync(scope).isDirectory() ? scope : path.dirname(scope);
|
|
85
|
+
const tracked = new Set(UnderpostRepository.API.getTrackedFiles(context));
|
|
86
|
+
return files.filter((file) => tracked.has(path.resolve(file)));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const resolveTrackedSelection = (entries) => entries.map(([key]) => key);
|
|
90
|
+
|
|
91
|
+
const resolveSelection = (scope, manifest, options = {}) => {
|
|
92
|
+
validateSelectionOptions(scope, options);
|
|
93
|
+
const entries = Object.entries(manifest);
|
|
94
|
+
const filtered = filterManifestEntries(entries, options, scope);
|
|
95
|
+
if (options.tracked) return resolveTrackedSelection(filtered);
|
|
96
|
+
|
|
97
|
+
let files = resolveFilesystemSelection(scope);
|
|
98
|
+
if (options.git) files = resolveGitSelection(files, scope);
|
|
99
|
+
const manifestKeys = new Map(entries.map(([key]) => [normalizeStoragePath(key), key]));
|
|
100
|
+
files = files.map((file) => manifestKeys.get(file) ?? file);
|
|
101
|
+
if (manifestFilterNames.some((name) => options[name] !== undefined)) {
|
|
102
|
+
const selected = new Set(resolveTrackedSelection(filtered));
|
|
103
|
+
files = files.filter((file) => selected.has(file));
|
|
104
|
+
}
|
|
105
|
+
return files;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
normalizeStoragePath,
|
|
110
|
+
filterManifestEntries,
|
|
111
|
+
resolveFilesystemSelection,
|
|
112
|
+
resolveGitSelection,
|
|
113
|
+
resolveTrackedSelection,
|
|
114
|
+
resolveSelection,
|
|
115
|
+
};
|