@guanjinke/artifact-client 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +2 -7
  2. package/dist/cli/environment.d.ts +1 -10
  3. package/dist/cli/environment.d.ts.map +1 -1
  4. package/dist/cli/environment.js +0 -24
  5. package/dist/cli/environment.js.map +1 -1
  6. package/dist/cli/setup.js +5 -8
  7. package/dist/cli/setup.js.map +1 -1
  8. package/dist/environment/builder.d.ts.map +1 -1
  9. package/dist/environment/builder.js +83 -23
  10. package/dist/environment/builder.js.map +1 -1
  11. package/dist/environment/catalog.d.ts +36 -3
  12. package/dist/environment/catalog.d.ts.map +1 -1
  13. package/dist/environment/catalog.js +50 -0
  14. package/dist/environment/catalog.js.map +1 -1
  15. package/dist/environment/plan.d.ts +31 -5
  16. package/dist/environment/plan.d.ts.map +1 -1
  17. package/dist/environment/plan.js +104 -16
  18. package/dist/environment/plan.js.map +1 -1
  19. package/dist/environment/types.d.ts +49 -12
  20. package/dist/environment/types.d.ts.map +1 -1
  21. package/dist/environment/update.d.ts +6 -26
  22. package/dist/environment/update.d.ts.map +1 -1
  23. package/dist/environment/update.js +120 -74
  24. package/dist/environment/update.js.map +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/package.json +2 -3
  28. package/resources/3DScene/capeHill.hdr +0 -0
  29. package/resources/3DScene/defaultGround.jpg +0 -0
  30. package/resources/3DScene/environmentSpecular.env +0 -0
  31. package/resources/3DScene/flare.png +0 -0
  32. package/resources/3DScene/geometry1.png +0 -0
  33. package/resources/3DScene/geometry10.png +0 -0
  34. package/resources/3DScene/geometry11.png +0 -0
  35. package/resources/3DScene/geometry12.png +0 -0
  36. package/resources/3DScene/geometry13.png +0 -0
  37. package/resources/3DScene/geometry2.png +0 -0
  38. package/resources/3DScene/geometry3.png +0 -0
  39. package/resources/3DScene/geometry4.png +0 -0
  40. package/resources/3DScene/geometry5.png +0 -0
  41. package/resources/3DScene/geometry6.png +0 -0
  42. package/resources/3DScene/geometry7.png +0 -0
  43. package/resources/3DScene/geometry8.png +0 -0
  44. package/resources/3DScene/geometry9.png +0 -0
  45. package/resources/3DScene/lilienstein.hdr +0 -0
  46. package/resources/3DScene/sky.hdr +0 -0
  47. package/resources/3DScene/studio.env +0 -0
  48. package/resources/3DScene/wastelandCloudsPureSky.hdr +0 -0
@@ -48,7 +48,6 @@ const registry_1 = require("./registry");
48
48
  const validate_1 = require("./validate");
49
49
  const process_1 = require("./process");
50
50
  const posix_process_1 = require("./posix-process");
51
- const package_resource_1 = require("./package-resource");
52
51
  const atomic_1 = require("../utils/atomic");
53
52
  const local_path_1 = require("../utils/local-path");
54
53
  /** Same parser options as the creation path, so a rewrite round-trips identically. */
@@ -57,8 +56,13 @@ const builderOptions = { ignoreAttributes: false, format: true, suppressEmptyNod
57
56
  const windowsPath = (value, platform) => (0, local_path_1.nativeSeparators)(value, platform);
58
57
  /** The separator the recorded entries use on the target platform. */
59
58
  const separatorOf = (platform) => (platform === 'win32' ? '\\' : '/');
60
- /** Every catalog module can be updated remotely; local overrides are excluded per environment. */
61
- exports.updatableModules = Object.keys(catalog_1.catalog).sort();
59
+ /**
60
+ * Every catalog module an update may refresh. The create-only modules are
61
+ * excluded: they seed data the product imports once, when the Server starts for
62
+ * the first time, so an update never refreshes, preserves or re-deploys them —
63
+ * and the web update picker reads this same list.
64
+ */
65
+ exports.updatableModules = Object.keys(catalog_1.catalog).filter(module => !catalog_1.catalog[module].createOnly).sort();
62
66
  /**
63
67
  * Server-root settings the environment owns.
64
68
  *
@@ -95,10 +99,21 @@ const excludedCopyNames = (mapping) => mapping.role === 'server' ? [...serverOwn
95
99
  */
96
100
  const selectableModules = () => new Set([...new Set(Object.values(plan_1.optionalModulesByEdition).flat())]);
97
101
  const installationDirectory = (root, destination) => path.join(root, ...destination.split('/'));
102
+ /**
103
+ * The edition an environment deploys. The registry records a free string and
104
+ * only `en` is special everywhere else in this module, so anything else counts
105
+ * as `cn`, exactly like the edition gate below does.
106
+ */
107
+ const editionOf = (entry) => entry.edition === 'en' ? 'en' : 'cn';
98
108
  function validateUpdatableModule(module) {
99
- if (Object.prototype.hasOwnProperty.call(catalog_1.catalog, module))
100
- return;
101
- throw new Error(`Unsupported module "${module}": supported modules are ${Object.keys(catalog_1.catalog).sort().join(', ')}.`);
109
+ // `hasOwnProperty` keeps prototype names (`constructor`) out of the catalog.
110
+ if (!Object.prototype.hasOwnProperty.call(catalog_1.catalog, module))
111
+ throw new Error(`Unsupported module "${module}": supported modules are ${exports.updatableModules.join(', ')}.`);
112
+ // A create-only module is refused before anything else happens — before the
113
+ // registry, the manifest or the configuration is read — because no update can
114
+ // produce it: it is deployed only while the environment is created.
115
+ if (catalog_1.catalog[module].createOnly)
116
+ throw new Error(`Module ${module} is create-only: it is deployed only when the environment is created and is never refreshed, preserved or re-deployed by update-env. Recreate the environment with create-env to get it again.`);
102
117
  }
103
118
  /** `all` is the only allowed pseudo-module: every configured module from remote. */
104
119
  const allModules = 'all';
@@ -382,10 +397,9 @@ async function readManifest(root) {
382
397
  }
383
398
  if (manifest.selectedModules !== undefined && (!Array.isArray(manifest.selectedModules) || manifest.selectedModules.some(module => typeof module !== 'string')))
384
399
  throw new Error('Environment manifest is unreadable.');
385
- // Only the entry name is required: the resource list is written by this package
386
- // for the package's own trees, and unknown extra fields are preserved as-is.
387
- if (manifest.resources !== undefined && (!Array.isArray(manifest.resources) || manifest.resources.some(resource => !resource || typeof resource !== 'object' || Array.isArray(resource) || typeof resource.name !== 'string')))
388
- throw new Error('Environment manifest is unreadable.');
400
+ // A stale `resources` key written by an older version is ignored here and
401
+ // dropped when the manifest is rewritten: the key belongs to the removed
402
+ // packaged-resource feature, not to the environment's configuration.
389
403
  if (manifest.moduleSources !== undefined && (!manifest.moduleSources || typeof manifest.moduleSources !== 'object' || Array.isArray(manifest.moduleSources)))
390
404
  throw new Error('Environment manifest is unreadable.');
391
405
  for (const source of Object.values(manifest.moduleSources ?? {})) {
@@ -444,7 +458,7 @@ async function resolveRemoteArtifact(client, entry, spec) {
444
458
  catch {
445
459
  throw new Error(`Unable to resolve artifacts for ${source.plugin}/${spec.version ?? entry.major}.`);
446
460
  }
447
- const name = source.filename(version, entry.edition);
461
+ const name = (0, catalog_1.archiveName)(source, version, entry.edition);
448
462
  const candidates = files.filter(file => file.name === name);
449
463
  if (candidates.length !== 1)
450
464
  throw new Error(`${source.plugin}/${version}: expected exactly one ${name}, found ${candidates.length}.`);
@@ -454,7 +468,7 @@ async function resolveRemoteArtifact(client, entry, spec) {
454
468
  return { module, plugin: source.plugin, version, name, path: remotePath, mappings: source.mappings.map(mapping => ({ ...mapping, assemblies: [...mapping.assemblies] })) };
455
469
  }
456
470
  /** Download, hash, extract and verify one resolved artifact inside the staging workspace. */
457
- async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
471
+ async function stageRemoteArtifact(client, artifact, edition, workspace, onProgress) {
458
472
  const label = `[${artifact.module}] ${artifact.plugin}/${artifact.version}/${artifact.name}`;
459
473
  const download = path.join(workspace, 'download', artifact.module);
460
474
  const expanded = path.join(workspace, `expanded-${artifact.module}`);
@@ -469,10 +483,16 @@ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
469
483
  onProgress?.(`${label}: Extracting and validating ZIP`);
470
484
  await (0, archive_1.extractArchive)(archive, expanded);
471
485
  const assemblies = new Map();
472
- for (const mapping of artifact.mappings) {
486
+ // A mapping of the other edition is never required: its source directory
487
+ // does not even have to be in the archive.
488
+ for (const mapping of (0, catalog_1.editionMappings)(artifact.mappings, edition)) {
473
489
  const source = path.join(expanded, mapping.source);
474
490
  if (!(await fs.lstat(source)).isDirectory())
475
491
  throw new Error('Required source directory missing.');
492
+ // A `resource` mapping is a static tree with no assembly to verify and no
493
+ // runtimeconfig to read; the Server only serves it.
494
+ if (mapping.role === 'resource')
495
+ continue;
476
496
  if (mapping.role === 'service' && mapping.assemblies.length === 0)
477
497
  continue;
478
498
  const present = [];
@@ -616,6 +636,12 @@ function mappingForAssembly(module, assembly) {
616
636
  * explicitly, for a build whose file name matches no assembly at all.
617
637
  */
618
638
  async function resolveLocalSource(entry, module, assembly, input) {
639
+ // The built-in resource module is never a developer-build target: it ships a
640
+ // static asset tree that no local DLL could stand in for, so a local source is
641
+ // refused on its own terms before any path is even looked at. Remote specs
642
+ // (latest, pinned and `all`) keep working.
643
+ if (plan_1.trailingModules.includes(module))
644
+ throw new Error(`Module ${module} is built in and has no local source: it ships the 3D scene and map assets and is always deployed from its published artifact. Refresh it from the cloud (\`${module}\`, \`${module}:<version>\` or \`all\`).`);
619
645
  const mappings = localMappings(module);
620
646
  // The Server, Monitor and collaboration deployments are whole services, not a
621
647
  // plugin the Server loads: a developer build has no place to point the
@@ -705,14 +731,12 @@ class EnvironmentUpdater {
705
731
  registry;
706
732
  processes;
707
733
  platform;
708
- bundledResources;
709
734
  onProgress;
710
735
  constructor(dependencies = {}) {
711
736
  this.dependencies = dependencies;
712
737
  this.registry = dependencies.registry ?? new registry_1.EnvironmentRegistry();
713
738
  this.processes = dependencies.processes ?? (process.platform === 'win32' ? new process_1.WindowsProcessBackend() : new posix_process_1.PosixProcessBackend());
714
739
  this.platform = dependencies.platform ?? process.platform;
715
- this.bundledResources = dependencies.bundledResourcesDirectory ?? (0, package_resource_1.bundledResourcesDirectory)();
716
740
  this.onProgress = dependencies.onProgress;
717
741
  }
718
742
  async update(name, specs, clientFactory, options = {}) {
@@ -720,9 +744,8 @@ class EnvironmentUpdater {
720
744
  const onProgress = options.onProgress ?? this.onProgress;
721
745
  onProgress?.('Validating module specs');
722
746
  let parsed = normalizeModuleSpecs(specs);
723
- // No module spec is a valid request: it (re)lays out the bundled resources
724
- // alone, which is how an environment created before they were bundled gets
725
- // them without touching a single module.
747
+ if (!parsed.length)
748
+ throw new Error('At least one module spec is required: pass `all` or one or more module names (module, module:<version> or module=<local path>).');
726
749
  const entry = await this.registry.get(name);
727
750
  onProgress?.(`Validating environment ${entry.name} at ${entry.root}`);
728
751
  await (0, validate_1.validateEnvironmentRoot)(entry, this.registry.directory);
@@ -766,6 +789,23 @@ class EnvironmentUpdater {
766
789
  localOverrides.set(spec.module, targets);
767
790
  }
768
791
  const remote = parsed.filter(spec => spec.localPath === undefined);
792
+ // The built-in resource module deploys its trees *inside* the Server
793
+ // directory, and a Server refresh replaces that whole directory. An
794
+ // environment that has it therefore re-deploys it from its own artifact
795
+ // whenever the Server is refreshed in the same call: a remote-only module has
796
+ // no local copy to restore the trees from. A create-only trailing module is
797
+ // never re-deployed: the Server refresh simply drops what it had seeded.
798
+ const autoRefreshed = [];
799
+ if (remote.some(spec => spec.module === 'server')) {
800
+ for (const module of plan_1.trailingModules) {
801
+ if (catalog_1.catalog[module].createOnly || !installed.has(module) || parsed.some(spec => spec.module === module))
802
+ continue;
803
+ parsed.push({ module });
804
+ remote.push({ module });
805
+ autoRefreshed.push(module);
806
+ onProgress?.(`${module} is refreshed with the Server package: its trees deploy inside the Server directory`);
807
+ }
808
+ }
769
809
  // Smart documents cannot run without the collaboration service, which
770
810
  // create-env deploys and declares in the Server configuration alongside
771
811
  // documents. Installing documents into an environment with no collaboration
@@ -779,26 +819,6 @@ class EnvironmentUpdater {
779
819
  throw new Error(`Cannot update modules from a remote source while ${entry.name} is ${state}: deployed files may be in use. Run \`afm env stop ${entry.name}\` first.`);
780
820
  if (!remote.length && state !== 'ready' && state !== 'stopped' && state !== 'running')
781
821
  throw new Error(`Cannot update modules while ${entry.name} is ${state}: the runtime identity must be verified before its files are changed. Run \`afm env stop ${entry.name}\` first.`);
782
- // The packaged 3D scene assets belong to every environment, not only to one
783
- // that create-env assembled: the plan below finds the files an environment is
784
- // still missing (or that differ) and the apply writes them with the rest of
785
- // the update. Planning touches nothing and runs before any download or write,
786
- // so an incomplete installation fails loudly with the path it expected and
787
- // leaves the environment exactly as it was.
788
- //
789
- // The assets live at `Server/wwwroot/3DScene`, so refreshing the server
790
- // module removes the whole tree they are in. Plan every packaged file as
791
- // missing in that case: comparing against the content that is about to be
792
- // deleted would report every file identical and lay nothing out at all.
793
- const refreshesServer = remote.some(spec => spec.module === 'server');
794
- const resourceSource = (0, package_resource_1.bundledResourceDirectory)(package_resource_1.sceneResourceName, this.bundledResources);
795
- const resourceDestination = path.join(entry.root, ...package_resource_1.sceneResourceDestination.split('/'));
796
- onProgress?.(`Checking bundled 3D scene resources in ${package_resource_1.sceneResourceDestination}`);
797
- const resourcePlan = await (0, package_resource_1.planBundledResource)(resourceSource, resourceDestination, refreshesServer);
798
- const resourceWrites = resourcePlan.added + resourcePlan.updated;
799
- const resource = { name: package_resource_1.sceneResourceName, source: resourceSource, destination: package_resource_1.sceneResourceDestination, files: resourcePlan.files.length, added: resourcePlan.added, updated: resourcePlan.updated };
800
- const resourceSummary = `${resourcePlan.files.length - resourceWrites} identical, ${resourcePlan.added} missing, ${resourcePlan.updated} different`;
801
- onProgress?.(`Bundled 3D scene resources in ${package_resource_1.sceneResourceDestination}: ${dryRun ? 'dry run; ' : ''}${resourceSummary}`);
802
822
  const downloads = [...(manifest.downloads ?? [])];
803
823
  const selectedModules = stringListOrUndefined(manifest.selectedModules);
804
824
  const autoAddedModules = stringListOrUndefined(manifest.autoAddedModules);
@@ -811,7 +831,7 @@ class EnvironmentUpdater {
811
831
  // runtimeconfig files, so an update replaces the entries of the modules it
812
832
  // refreshes and leaves every other module's alone.
813
833
  let requirements = Array.isArray(manifest.requirements) ? manifest.requirements.filter((line) => typeof line === 'string') : [];
814
- const workspace = !dryRun && (remote.length > 0 || resourceWrites > 0) ? await fs.mkdtemp(path.join(path.dirname(entry.root), '.afm-env-')) : undefined;
834
+ const workspace = !dryRun && remote.length > 0 ? await fs.mkdtemp(path.join(path.dirname(entry.root), '.afm-env-')) : undefined;
815
835
  const warnings = [];
816
836
  try {
817
837
  const resolved = new Map();
@@ -824,7 +844,7 @@ class EnvironmentUpdater {
824
844
  }
825
845
  if (!dryRun)
826
846
  for (const spec of remote)
827
- staged.set(spec.module, await stageRemoteArtifact(client, resolved.get(spec.module), workspace, onProgress));
847
+ staged.set(spec.module, await stageRemoteArtifact(client, resolved.get(spec.module), editionOf(entry), workspace, onProgress));
828
848
  }
829
849
  const configured = await fs.readFile(path.join(entry.root, 'conf', 'Wyn.conf'), 'utf8');
830
850
  // A Server or Monitor package is only usable when it still serves the
@@ -846,7 +866,12 @@ class EnvironmentUpdater {
846
866
  }
847
867
  const plans = [];
848
868
  // A module may carry several local specs: plan it once, for all its targets.
849
- for (const module of [...new Set(parsed.map(spec => spec.module))]) {
869
+ // The built-in trailing modules are planned last, in the same order
870
+ // `create-env` assembles: their trees live inside other modules'
871
+ // directories, so a Server replacement must not wipe them again.
872
+ const planOrder = [...new Set(parsed.map(spec => spec.module))]
873
+ .sort((left, right) => Number(plan_1.trailingModules.includes(left)) - Number(plan_1.trailingModules.includes(right)));
874
+ for (const module of planOrder) {
850
875
  const artifact = artifacts.find(candidate => candidate.module === module);
851
876
  const previous = moduleSources[module] ? { local: moduleSources[module].path } : { version: artifact?.version };
852
877
  const override = localOverrides.get(module);
@@ -885,8 +910,12 @@ class EnvironmentUpdater {
885
910
  const entries = [];
886
911
  let configurationAssets;
887
912
  let configurationLink;
888
- for (const mapping of remoteArtifact.mappings) {
913
+ // Only the mappings this environment's edition deploys: the other
914
+ // edition's directory is never removed, copied or referenced.
915
+ for (const mapping of (0, catalog_1.editionMappings)(remoteArtifact.mappings, editionOf(entry))) {
889
916
  const destination = installationDirectory(entry.root, mapping.destination);
917
+ // A `resource` directory the module already deployed is replaced, never
918
+ // merged, and it contributes no plugin, worker, service or runtime entry.
890
919
  if (await exists(destination)) {
891
920
  removals.push(destination);
892
921
  removedDirectories.push(mapping.destination);
@@ -903,6 +932,8 @@ class EnvironmentUpdater {
903
932
  if (stagedArtifact)
904
933
  copies.push({ source: path.join(stagedArtifact.expanded, mapping.source), destination, exclude: excludedCopyNames(mapping) });
905
934
  copiedDirectories.push(mapping.destination);
935
+ if (mapping.role === 'resource')
936
+ continue;
906
937
  // A native out-of-process service has no assembly and is declared as an
907
938
  // empty node: the Server loads it from that entry and supplies the rest.
908
939
  if (mapping.role === 'service') {
@@ -975,9 +1006,10 @@ class EnvironmentUpdater {
975
1006
  nextDownloads[downloadIndex] = download;
976
1007
  // The requirements the old artifact needed are gone with it: re-read
977
1008
  // the ones the new package actually declares, per deployed mapping.
1009
+ // A resource mapping is a static tree: it declares none.
978
1010
  requirements = requirements.filter(line => !line.endsWith(` (${plan.module})`));
979
- for (const mapping of plan.artifact.mappings) {
980
- if (mapping.role === 'service')
1011
+ for (const mapping of (0, catalog_1.editionMappings)(plan.artifact.mappings, editionOf(entry))) {
1012
+ if (mapping.role === 'service' || mapping.role === 'resource')
981
1013
  continue;
982
1014
  for (const framework of await (0, builder_1.runtimeRequirements)(path.join(plan.staged.expanded, mapping.source))) {
983
1015
  requirements.push(`${framework.name} ${framework.version} (${plan.module})`);
@@ -1010,6 +1042,32 @@ class EnvironmentUpdater {
1010
1042
  autoAddedModules.length = 0;
1011
1043
  autoAddedModules.push(...unique);
1012
1044
  }
1045
+ // A create-only module deploys its payload inside the Server directory and is
1046
+ // never refreshed, so a Server refresh replaces the tree it owns: the module
1047
+ // is gone from the environment, and its record goes with it — the manifest
1048
+ // and the registry would otherwise keep claiming a deployment that is not
1049
+ // there any more.
1050
+ const removedCreateOnly = [];
1051
+ if (plans.some(plan => plan.module === 'server')) {
1052
+ for (const module of Object.keys(catalog_1.catalog)) {
1053
+ if (catalog_1.catalog[module].createOnly !== true || !installed.has(module) || plans.some(plan => plan.module === module))
1054
+ continue;
1055
+ const plugin = catalog_1.catalog[module].plugin;
1056
+ for (let index = nextArtifacts.length - 1; index >= 0; index -= 1)
1057
+ if (nextArtifacts[index].module === module)
1058
+ nextArtifacts.splice(index, 1);
1059
+ for (let index = nextDownloads.length - 1; index >= 0; index -= 1)
1060
+ if (nextDownloads[index].plugin === plugin)
1061
+ nextDownloads.splice(index, 1);
1062
+ for (let index = requirements.length - 1; index >= 0; index -= 1)
1063
+ if (requirements[index].endsWith(` (${module})`))
1064
+ requirements.splice(index, 1);
1065
+ if (autoAddedModules)
1066
+ autoAddedModules.splice(0, autoAddedModules.length, ...autoAddedModules.filter(name => name !== module));
1067
+ delete moduleSources[module];
1068
+ removedCreateOnly.push(module);
1069
+ }
1070
+ }
1013
1071
  const nextManifest = { ...manifest, artifacts: nextArtifacts, downloads: nextDownloads, requirements: [...new Set(requirements)] };
1014
1072
  if (autoAddedModules)
1015
1073
  nextManifest.autoAddedModules = autoAddedModules;
@@ -1019,19 +1077,10 @@ class EnvironmentUpdater {
1019
1077
  delete nextManifest.moduleSources;
1020
1078
  if (selectedModules)
1021
1079
  nextManifest.selectedModules = selectedModules;
1022
- // The resource is recorded in place, in exactly the shape create-env writes:
1023
- // a second update finds the entry and refreshes it instead of adding a
1024
- // duplicate. The result carries the added/updated counts; the manifest keeps
1025
- // the durable four-field description.
1026
- const resources = [...(manifest.resources ?? [])];
1027
- const recorded = resources.findIndex(candidate => candidate.name === package_resource_1.sceneResourceName);
1028
- const record = { name: package_resource_1.sceneResourceName, source: resource.source, destination: resource.destination, files: resource.files };
1029
- if (recorded < 0)
1030
- resources.push(record);
1031
- else
1032
- resources[recorded] = { ...resources[recorded], ...record };
1033
- nextManifest.resources = resources;
1034
- const modules = [...new Set([...entry.modules, ...plans.filter(plan => !installed.has(plan.module) && selectable.has(plan.module)).map(plan => plan.module)])].sort();
1080
+ // A stale `resources` key from an older manifest is dropped here: the
1081
+ // packaged-resource feature is gone and nothing records it any more.
1082
+ delete nextManifest.resources;
1083
+ const modules = [...new Set([...entry.modules, ...plans.filter(plan => !installed.has(plan.module) && selectable.has(plan.module)).map(plan => plan.module)])].filter(module => !removedCreateOnly.includes(module)).sort();
1035
1084
  const localModules = Object.keys(moduleSources).sort();
1036
1085
  if (state === 'running')
1037
1086
  warnings.push(`${entry.name} is running; restart it to load the updated modules.`);
@@ -1046,6 +1095,10 @@ class EnvironmentUpdater {
1046
1095
  if (parsed.some(spec => spec.module === 'documents') && !parsed.some(spec => spec.module === 'collaboration')) {
1047
1096
  warnings.push('documents was refreshed without collaboration; the smart documents plugin may expect the deployed collaboration service version, so refresh collaboration too.');
1048
1097
  }
1098
+ if (autoRefreshed.length)
1099
+ warnings.push(`${autoRefreshed.join(', ')} was refreshed with the Server package: its trees live inside the Server directory and would otherwise be lost.`);
1100
+ if (removedCreateOnly.length)
1101
+ warnings.push(`${removedCreateOnly.join(', ')} was removed with the Server package: it is deployed only when an environment is created.`);
1049
1102
  if (!dryRun) {
1050
1103
  for (const plan of plans) {
1051
1104
  // Take the environment-owned settings aside first, and drop the Monitor
@@ -1075,20 +1128,13 @@ class EnvironmentUpdater {
1075
1128
  if (plan.configurationLink)
1076
1129
  await fs.symlink(path.join(entry.root, 'conf'), plan.configurationLink, 'junction');
1077
1130
  }
1078
- // The staged resources are written with the module changes, before the
1079
- // manifest that records them; a failure rolls the layout back.
1080
- if (resourceWrites) {
1081
- if (!workspace)
1082
- throw new Error('Missing staging workspace for the bundled resource layout.');
1083
- onProgress?.(`Laying out bundled 3D scene resources into ${package_resource_1.sceneResourceDestination}`);
1084
- const applied = await (0, package_resource_1.applyBundledResource)(resourceSource, resourceDestination, resourcePlan, path.join(workspace, 'replaced-resources'));
1085
- if (applied.added.length !== resourcePlan.added || applied.updated.length !== resourcePlan.updated)
1086
- throw new Error('Bundled resource layout did not match its plan.');
1087
- }
1088
- // A resource-only update (no module spec) leaves the Server configuration
1089
- // exactly as it is: the assets need no plugin, worker or service entry.
1090
- onProgress?.(plans.length ? 'Writing configuration, manifest and registry record' : 'Writing manifest and registry record');
1091
- if (plans.length)
1131
+ // Only a plan that really contributes something to the configuration
1132
+ // rewrites it: the built-in dashboard-resource module adds no plugin,
1133
+ // worker or service entry, so refreshing it alone leaves `conf/Wyn.conf`
1134
+ // exactly as it is instead of round-tripping it through the XML builder.
1135
+ const configurationChanged = plans.some(plan => plan.entries.length > 0 || plan.configurationAssets !== undefined || plan.configurationLink !== undefined);
1136
+ onProgress?.(configurationChanged ? 'Writing configuration, manifest and registry record' : 'Writing manifest and registry record');
1137
+ if (configurationChanged)
1092
1138
  await writeAtomic(path.join(entry.root, 'conf', 'Wyn.conf'), configuration);
1093
1139
  await writeJsonAtomic(path.join(entry.root, 'environment.lock.json'), nextManifest);
1094
1140
  await this.registry.update(entries => {
@@ -1104,10 +1150,10 @@ class EnvironmentUpdater {
1104
1150
  delete entries[index].localModules;
1105
1151
  });
1106
1152
  }
1107
- const restartRequired = !dryRun && (changes.length > 0 || resourceWrites > 0);
1153
+ const restartRequired = !dryRun && changes.length > 0;
1108
1154
  if (restartRequired)
1109
- onProgress?.(`${entry.name}: ${changes.length ? 'modules updated' : 'bundled resources laid out'}; restart the environment to load the change (no service was started).`);
1110
- return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings, localTargets: plans.flatMap(plan => plan.local ?? []), resource };
1155
+ onProgress?.(`${entry.name}: modules updated; restart the environment to load the change (no service was started).`);
1156
+ return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings, localTargets: plans.flatMap(plan => plan.local ?? []) };
1111
1157
  }
1112
1158
  finally {
1113
1159
  if (workspace)