@guanjinke/artifact-client 1.18.0 → 1.19.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 (74) hide show
  1. package/dist/artifact-client.d.ts +3 -1
  2. package/dist/artifact-client.d.ts.map +1 -1
  3. package/dist/artifact-client.js +6 -1
  4. package/dist/artifact-client.js.map +1 -1
  5. package/dist/cli/config-manager.d.ts +3 -0
  6. package/dist/cli/config-manager.d.ts.map +1 -1
  7. package/dist/cli/config-manager.js +44 -0
  8. package/dist/cli/config-manager.js.map +1 -1
  9. package/dist/cli/environment.d.ts.map +1 -1
  10. package/dist/cli/environment.js +116 -0
  11. package/dist/cli/environment.js.map +1 -1
  12. package/dist/cli/jdbc-service-client.d.ts +9 -0
  13. package/dist/cli/jdbc-service-client.d.ts.map +1 -0
  14. package/dist/cli/jdbc-service-client.js +19 -0
  15. package/dist/cli/jdbc-service-client.js.map +1 -0
  16. package/dist/cli/setup.d.ts.map +1 -1
  17. package/dist/cli/setup.js +43 -0
  18. package/dist/cli/setup.js.map +1 -1
  19. package/dist/environment/driver-discovery.d.ts +3 -0
  20. package/dist/environment/driver-discovery.d.ts.map +1 -0
  21. package/dist/environment/driver-discovery.js +163 -0
  22. package/dist/environment/driver-discovery.js.map +1 -0
  23. package/dist/environment/driver-oss.d.ts +14 -0
  24. package/dist/environment/driver-oss.d.ts.map +1 -0
  25. package/dist/environment/driver-oss.js +144 -0
  26. package/dist/environment/driver-oss.js.map +1 -0
  27. package/dist/environment/drivers.d.ts +33 -0
  28. package/dist/environment/drivers.d.ts.map +1 -0
  29. package/dist/environment/drivers.js +274 -0
  30. package/dist/environment/drivers.js.map +1 -0
  31. package/dist/environment/hana-archive.d.ts +9 -0
  32. package/dist/environment/hana-archive.d.ts.map +1 -0
  33. package/dist/environment/hana-archive.js +181 -0
  34. package/dist/environment/hana-archive.js.map +1 -0
  35. package/dist/environment/hana.d.ts +58 -0
  36. package/dist/environment/hana.d.ts.map +1 -0
  37. package/dist/environment/hana.js +238 -0
  38. package/dist/environment/hana.js.map +1 -0
  39. package/dist/environment/jdbc-connection-test.d.ts +25 -0
  40. package/dist/environment/jdbc-connection-test.d.ts.map +1 -0
  41. package/dist/environment/jdbc-connection-test.js +146 -0
  42. package/dist/environment/jdbc-connection-test.js.map +1 -0
  43. package/dist/environment/jdbc-diagnostics.d.ts +13 -0
  44. package/dist/environment/jdbc-diagnostics.d.ts.map +1 -0
  45. package/dist/environment/jdbc-diagnostics.js +108 -0
  46. package/dist/environment/jdbc-diagnostics.js.map +1 -0
  47. package/dist/environment/jdbc-service.d.ts +91 -0
  48. package/dist/environment/jdbc-service.d.ts.map +1 -0
  49. package/dist/environment/jdbc-service.js +375 -0
  50. package/dist/environment/jdbc-service.js.map +1 -0
  51. package/dist/environment/jdbc.d.ts +28 -0
  52. package/dist/environment/jdbc.d.ts.map +1 -0
  53. package/dist/environment/jdbc.js +164 -0
  54. package/dist/environment/jdbc.js.map +1 -0
  55. package/dist/environment/manager.d.ts +10 -0
  56. package/dist/environment/manager.d.ts.map +1 -1
  57. package/dist/environment/manager.js +32 -0
  58. package/dist/environment/manager.js.map +1 -1
  59. package/dist/environment/registry.d.ts +14 -0
  60. package/dist/environment/registry.d.ts.map +1 -1
  61. package/dist/environment/registry.js +7 -0
  62. package/dist/environment/registry.js.map +1 -1
  63. package/dist/environment/site-init.d.ts +16 -0
  64. package/dist/environment/site-init.d.ts.map +1 -0
  65. package/dist/environment/site-init.js +108 -0
  66. package/dist/environment/site-init.js.map +1 -0
  67. package/dist/environment/update.d.ts.map +1 -1
  68. package/dist/environment/update.js +607 -602
  69. package/dist/environment/update.js.map +1 -1
  70. package/dist/providers/oss-provider.d.ts +6 -2
  71. package/dist/providers/oss-provider.d.ts.map +1 -1
  72. package/dist/providers/oss-provider.js +37 -7
  73. package/dist/providers/oss-provider.js.map +1 -1
  74. package/package.json +1 -1
@@ -949,650 +949,655 @@ class EnvironmentUpdater {
949
949
  const foreignStore = options.provider === undefined ? [] : parsed.filter(spec => spec.localPath === undefined && !(0, catalog_1.providerAllows)(spec.module, options.provider));
950
950
  if (foreignStore.length)
951
951
  throw new Error((0, catalog_1.foreignStoreError)(foreignStore[0].module, options.provider));
952
- const entry = await this.registry.get(name);
953
- onProgress?.(`Validating environment ${entry.name} at ${entry.root}`);
954
- await (0, validate_1.validateEnvironmentRoot)(entry, this.registry.directory);
955
- await (0, validate_1.validateEnvironmentOwnership)(entry, this.registry.directory);
956
- await (0, validate_1.validateEnvironmentTree)(entry.root);
957
- const manifest = await (0, manifest_1.readManifest)(entry.root);
958
- const artifacts = [...(manifest.artifacts ?? [])];
959
- const installed = new Set(artifacts.map(artifact => artifact.module));
960
- const skipped = [];
961
- if (parsed.some(spec => spec.module === 'all')) {
962
- // `all` means every configured module from remote; modules that already
963
- // point at a local developer build are left exactly as they are.
964
- const localModules = new Set([...Object.keys(manifest.moduleSources ?? {}), ...(entry.localModules ?? [])]);
965
- const configured = [...new Set(artifacts.map(artifact => artifact.module))].filter(module => exports.updatableModules.includes(module)).sort();
966
- for (const module of configured) {
967
- if (localModules.has(module))
968
- skipped.push({ module, reason: 'local' });
969
- else if (!(0, catalog_1.providerAllows)(module, options.provider))
970
- skipped.push({ module, reason: 'provider' });
971
- }
972
- const selected = configured.filter(module => !localModules.has(module) && (0, catalog_1.providerAllows)(module, options.provider));
973
- if (!selected.length)
974
- throw new Error(`Module spec all matched no updatable modules: ${configured.length ? skipped.map(item => item.reason === 'local' ? `${item.module} has a local source` : `${item.module} is not published in this store`).join('; ') : 'this environment has no installed module to refresh'}.`);
975
- parsed = selected.map(module => ({ module }));
976
- const localSkipped = skipped.filter(item => item.reason === 'local').map(item => item.module);
977
- const foreignSkipped = skipped.filter(item => item.reason === 'provider').map(item => item.module);
978
- onProgress?.(`Updating all configured modules from remote: ${selected.join(', ')}${localSkipped.length ? ` (skipping local: ${localSkipped.join(', ')})` : ''}${foreignSkipped.length ? ` (not published in this store: ${foreignSkipped.join(', ')})` : ''}`);
979
- }
980
- const localOverrides = new Map();
981
- const selectedTargets = new Set();
982
- for (const spec of parsed) {
983
- if (['sheets', 'documents'].includes(spec.module) && entry.edition !== 'en')
984
- throw new Error(`Module ${spec.module} requires an EN edition environment; ${entry.name} is ${entry.edition}.`);
985
- if (spec.module === 'reportsheet' && entry.edition !== 'cn')
986
- throw new Error(`Module reportsheet requires a CN edition environment; ${entry.name} is ${entry.edition}.`);
987
- // A module the environment's major does not publish is refused the same way,
988
- // before a local or remote resolution touches anything.
989
- if (!(0, catalog_1.majorAllows)(spec.module, entry.major))
990
- throw new Error((0, catalog_1.foreignMajorError)(spec.module, entry.major));
991
- if (spec.localPath === undefined)
992
- continue;
993
- const resolution = await resolveLocalSource(entry, spec.module, spec.assembly, spec.localPath);
994
- for (const destination of resolution.inactive)
995
- onProgress?.(`${spec.module}: ${destination} stays deployed (not selected by ${resolution.source})`);
996
- const targets = localOverrides.get(spec.module) ?? [];
997
- for (const target of resolution.targets) {
998
- if (selectedTargets.has(target.destination.toLowerCase()))
999
- throw new Error(`Local target ${target.assembly} of module ${target.module} is selected twice in one update; give one spec per assembly.`);
1000
- selectedTargets.add(target.destination.toLowerCase());
1001
- targets.push(target);
1002
- }
1003
- localOverrides.set(spec.module, targets);
1004
- }
1005
- const remote = parsed.filter(spec => spec.localPath === undefined);
1006
- // Internal modules are never named by a spec (they are refused while the
1007
- // module specs are parsed) and `all` never selects them, but the updater
1008
- // still deploys them on its own. `dashboard-resource` ships the 3D scene and
1009
- // map trees *inside* the Server directory, and a Server refresh replaces that
1010
- // whole directory: an environment that has it therefore re-deploys it from
1011
- // its own artifact whenever the Server is refreshed in the same call, because
1012
- // there is no other copy to restore the trees from. A create-only trailing
1013
- // module is different: it seeds data the product imported once, so a Server
1014
- // refresh simply drops what it had seeded.
1015
- const autoRefreshed = [];
1016
- if (remote.some(spec => spec.module === 'server')) {
1017
- for (const module of plan_1.trailingModules) {
1018
- if (catalog_1.catalog[module].createOnly || !installed.has(module) || parsed.some(spec => spec.module === module))
1019
- continue;
1020
- parsed.push({ module });
1021
- remote.push({ module });
1022
- autoRefreshed.push(module);
1023
- onProgress?.(`${module} is refreshed with the Server package: its trees deploy inside the Server directory`);
952
+ const original = await this.registry.get(name);
953
+ return this.registry.locked(`env-${original.id}`, async () => {
954
+ const entry = await this.registry.get(name);
955
+ if (entry.id !== original.id || entry.root !== original.root)
956
+ throw new Error('Environment changed during operation.');
957
+ onProgress?.(`Validating environment ${entry.name} at ${entry.root}`);
958
+ await (0, validate_1.validateEnvironmentRoot)(entry, this.registry.directory);
959
+ await (0, validate_1.validateEnvironmentOwnership)(entry, this.registry.directory);
960
+ await (0, validate_1.validateEnvironmentTree)(entry.root);
961
+ const manifest = await (0, manifest_1.readManifest)(entry.root);
962
+ const artifacts = [...(manifest.artifacts ?? [])];
963
+ const installed = new Set(artifacts.map(artifact => artifact.module));
964
+ const skipped = [];
965
+ if (parsed.some(spec => spec.module === 'all')) {
966
+ // `all` means every configured module from remote; modules that already
967
+ // point at a local developer build are left exactly as they are.
968
+ const localModules = new Set([...Object.keys(manifest.moduleSources ?? {}), ...(entry.localModules ?? [])]);
969
+ const configured = [...new Set(artifacts.map(artifact => artifact.module))].filter(module => exports.updatableModules.includes(module)).sort();
970
+ for (const module of configured) {
971
+ if (localModules.has(module))
972
+ skipped.push({ module, reason: 'local' });
973
+ else if (!(0, catalog_1.providerAllows)(module, options.provider))
974
+ skipped.push({ module, reason: 'provider' });
975
+ }
976
+ const selected = configured.filter(module => !localModules.has(module) && (0, catalog_1.providerAllows)(module, options.provider));
977
+ if (!selected.length)
978
+ throw new Error(`Module spec all matched no updatable modules: ${configured.length ? skipped.map(item => item.reason === 'local' ? `${item.module} has a local source` : `${item.module} is not published in this store`).join('; ') : 'this environment has no installed module to refresh'}.`);
979
+ parsed = selected.map(module => ({ module }));
980
+ const localSkipped = skipped.filter(item => item.reason === 'local').map(item => item.module);
981
+ const foreignSkipped = skipped.filter(item => item.reason === 'provider').map(item => item.module);
982
+ onProgress?.(`Updating all configured modules from remote: ${selected.join(', ')}${localSkipped.length ? ` (skipping local: ${localSkipped.join(', ')})` : ''}${foreignSkipped.length ? ` (not published in this store: ${foreignSkipped.join(', ')})` : ''}`);
1024
983
  }
1025
- }
1026
- // A Server refresh restores the unpatched file the Server package ships, so
1027
- // an installed deferred module is re-applied in the same call. Its version is
1028
- // resolved from the *new* package below, never from the old tree. Without a
1029
- // Server refresh it is only applied when the caller names it explicitly.
1030
- const autoReplaced = [];
1031
- if (remote.some(spec => spec.module === 'server')) {
1032
- for (const module of Object.keys(catalog_1.catalog)) {
1033
- if (!catalog_1.catalog[module].deferred || !installed.has(module) || parsed.some(spec => spec.module === module))
984
+ const localOverrides = new Map();
985
+ const selectedTargets = new Set();
986
+ for (const spec of parsed) {
987
+ if (['sheets', 'documents'].includes(spec.module) && entry.edition !== 'en')
988
+ throw new Error(`Module ${spec.module} requires an EN edition environment; ${entry.name} is ${entry.edition}.`);
989
+ if (spec.module === 'reportsheet' && entry.edition !== 'cn')
990
+ throw new Error(`Module reportsheet requires a CN edition environment; ${entry.name} is ${entry.edition}.`);
991
+ // A module the environment's major does not publish is refused the same way,
992
+ // before a local or remote resolution touches anything.
993
+ if (!(0, catalog_1.majorAllows)(spec.module, entry.major))
994
+ throw new Error((0, catalog_1.foreignMajorError)(spec.module, entry.major));
995
+ if (spec.localPath === undefined)
1034
996
  continue;
1035
- parsed.push({ module });
1036
- remote.push({ module });
1037
- autoReplaced.push(module);
1038
- onProgress?.(`${module} is re-applied with the Server package: the new package replaces the file it patches`);
1039
- }
1040
- }
1041
- // Smart documents cannot run without the collaboration service, which
1042
- // create-env deploys and declares in the Server configuration alongside
1043
- // documents. Installing documents into an environment with no collaboration
1044
- // deployment would produce a broken environment, so refuse — after the
1045
- // edition gate above, so a CN environment still reports the EN requirement.
1046
- if (parsed.some(spec => spec.module === 'documents') && !installed.has('collaboration')) {
1047
- throw new Error(`Module documents requires the collaboration service, which ${entry.name} has not deployed. Recreate the environment with create-env: selecting documents deploys collaboration automatically.`);
1048
- }
1049
- const state = await (0, validate_1.observeRuntimeState)(entry, this.processes);
1050
- if (remote.length && state !== 'ready' && state !== 'stopped')
1051
- 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.`);
1052
- if (!remote.length && state !== 'ready' && state !== 'stopped' && state !== 'running')
1053
- 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.`);
1054
- const downloads = [...(manifest.downloads ?? [])];
1055
- const selectedModules = stringListOrUndefined(manifest.selectedModules);
1056
- const autoAddedModules = stringListOrUndefined(manifest.autoAddedModules);
1057
- const moduleSources = { ...(manifest.moduleSources ?? {}) };
1058
- // What a fresh environment would have selected for itself. An update records
1059
- // a first install the way `create-env` does: a selectable module joins the
1060
- // selection, a core component or an automatic dependency is added instead.
1061
- const selectable = selectableModules();
1062
- // The manifest records the .NET requirements read from each artifact's
1063
- // runtimeconfig files, so an update replaces the entries of the modules it
1064
- // refreshes and leaves every other module's alone.
1065
- let requirements = Array.isArray(manifest.requirements) ? manifest.requirements.filter((line) => typeof line === 'string') : [];
1066
- // Read once: the up-to-date check below inspects the same configuration the
1067
- // patch would rewrite later, and an environment without it is not current.
1068
- const configured = await fs.readFile(path.join(entry.root, 'conf', 'Wyn.conf'), 'utf8');
1069
- const workspace = !dryRun && remote.length > 0 ? await fs.mkdtemp(path.join(path.dirname(entry.root), '.afm-env-')) : undefined;
1070
- const warnings = [];
1071
- try {
1072
- const resolved = new Map();
1073
- const staged = new Map();
1074
- const replacements = new Map();
1075
- // Modules already at the resolved version with a complete deployment: they
1076
- // are reported as skipped and never downloaded, staged or written.
1077
- const current = new Map();
1078
- if (remote.length) {
1079
- const client = clientFactory();
1080
- const direct = remote.filter(spec => !catalog_1.catalog[spec.module].deferred);
1081
- for (const spec of direct) {
1082
- onProgress?.(`Resolving ${catalog_1.catalog[spec.module].plugin}`);
1083
- resolved.set(spec.module, await resolveRemoteArtifact(client, entry, spec));
997
+ const resolution = await resolveLocalSource(entry, spec.module, spec.assembly, spec.localPath);
998
+ for (const destination of resolution.inactive)
999
+ onProgress?.(`${spec.module}: ${destination} stays deployed (not selected by ${resolution.source})`);
1000
+ const targets = localOverrides.get(spec.module) ?? [];
1001
+ for (const target of resolution.targets) {
1002
+ if (selectedTargets.has(target.destination.toLowerCase()))
1003
+ throw new Error(`Local target ${target.assembly} of module ${target.module} is selected twice in one update; give one spec per assembly.`);
1004
+ selectedTargets.add(target.destination.toLowerCase());
1005
+ targets.push(target);
1084
1006
  }
1085
- // A module the caller pinned is always deployed: the pin is the explicit
1086
- // request for that build. Modules the same call re-deploys because of a
1087
- // Server refresh are never current either — their files are about to be
1088
- // replaced. Everything else is compared with the deployed record and its
1089
- // actual files and configuration entries before anything is downloaded.
1090
- for (const spec of direct) {
1091
- if (spec.version !== undefined || autoRefreshed.includes(spec.module) || autoReplaced.includes(spec.module))
1007
+ localOverrides.set(spec.module, targets);
1008
+ }
1009
+ const remote = parsed.filter(spec => spec.localPath === undefined);
1010
+ // Internal modules are never named by a spec (they are refused while the
1011
+ // module specs are parsed) and `all` never selects them, but the updater
1012
+ // still deploys them on its own. `dashboard-resource` ships the 3D scene and
1013
+ // map trees *inside* the Server directory, and a Server refresh replaces that
1014
+ // whole directory: an environment that has it therefore re-deploys it from
1015
+ // its own artifact whenever the Server is refreshed in the same call, because
1016
+ // there is no other copy to restore the trees from. A create-only trailing
1017
+ // module is different: it seeds data the product imported once, so a Server
1018
+ // refresh simply drops what it had seeded.
1019
+ const autoRefreshed = [];
1020
+ if (remote.some(spec => spec.module === 'server')) {
1021
+ for (const module of plan_1.trailingModules) {
1022
+ if (catalog_1.catalog[module].createOnly || !installed.has(module) || parsed.some(spec => spec.module === module))
1092
1023
  continue;
1093
- const artifact = resolved.get(spec.module);
1094
- if (await moduleIsCurrent(entry, manifest, artifact, editionOf(entry), configured, this.platform)) {
1095
- current.set(spec.module, artifact.version);
1096
- onProgress?.(`${artifact.module}/${artifact.version}: already deployed; nothing downloaded`);
1097
- }
1024
+ parsed.push({ module });
1025
+ remote.push({ module });
1026
+ autoRefreshed.push(module);
1027
+ onProgress?.(`${module} is refreshed with the Server package: its trees deploy inside the Server directory`);
1098
1028
  }
1099
- if (!dryRun)
1100
- for (const spec of direct)
1101
- if (!current.has(spec.module))
1102
- staged.set(spec.module, await stageRemoteArtifact(client, resolved.get(spec.module), editionOf(entry), workspace, onProgress));
1103
- // A deferred module resolves after the direct artifacts are staged, so a
1104
- // Server refresh contributes the *new* package's version. Without a
1105
- // Server refresh the already deployed tree is the authority. A dry-run
1106
- // that refreshes the Server cannot know that version without downloading
1107
- // the package, so it reports the deferred resolution and stops there.
1108
- for (const spec of remote.filter(candidate => catalog_1.catalog[candidate.module].deferred)) {
1109
- const module = spec.module;
1110
- const source = catalog_1.catalog[module];
1111
- const label = (0, catalog_1.moduleLabel)(source);
1112
- const server = staged.get('server');
1113
- const refreshesServer = remote.some(candidate => candidate.module === 'server');
1114
- const localServer = moduleSources.server?.type === 'local' ? moduleSources.server.path : undefined;
1115
- let version;
1116
- let resolvedFrom = 'the deployed Server package';
1117
- if (server) {
1118
- version = await (0, deferred_1.deferredVersionAt)(source, stagedServerFile(server, module));
1119
- resolvedFrom = 'the refreshed Server package';
1120
- // The replacement must overwrite an existing regular file in the new
1121
- // package; checking the staged tree catches a package that no longer
1122
- // ships it before anything has been written.
1123
- for (const mapping of (0, deferred_1.replacementMappings)(source)) {
1124
- const file = stagedServerPath(server, mapping.destination);
1125
- const stat = await lstatOrUndefined(file);
1126
- if (!stat)
1127
- throw new Error(`Module ${label} could not be applied: the Server package holds no file to replace.`);
1128
- if (!stat.isFile() || stat.isSymbolicLink())
1129
- throw new Error(`Module ${label} could not be applied: the Server package holds no regular file to replace.`);
1130
- }
1131
- }
1132
- else if (refreshesServer) {
1133
- onProgress?.(`${label}: version resolves from the refreshed Server package at execution time`);
1134
- replacements.set(module, { version: '' });
1135
- continue;
1136
- }
1137
- else if (localServer) {
1138
- // A local Server build is copied over the deployed tree on every start,
1139
- // so the version that must be cached now is the one that build
1140
- // declares — not the version of the currently deployed package.
1141
- version = await (0, deferred_1.deferredVersionAt)(source, localServerPath(localServer, source.deferred.versionFile));
1142
- resolvedFrom = 'the local Server build directory';
1143
- }
1144
- else
1145
- version = await (0, deferred_1.deferredVersion)(source, entry.root);
1146
- // Already applied and untouched: the recorded download still hashes to
1147
- // the deployed file, so there is nothing to download or copy. A call
1148
- // that refreshes the Server always re-applies it (autoReplaced), and a
1149
- // local Server build resolves a version this tree does not hold.
1150
- if (!refreshesServer && !localServer && spec.version === undefined && !autoReplaced.includes(module)
1151
- && await replacementIsCurrent(entry, manifest, module, version)) {
1152
- current.set(module, version);
1153
- onProgress?.(`${label}/${version}: already applied; nothing downloaded`);
1154
- continue;
1155
- }
1156
- const payloads = await (0, deferred_1.deferredPayload)(source, version, client);
1157
- if (dryRun) {
1158
- onProgress?.(`${label}/${version}: resolved from ${resolvedFrom}; nothing downloaded`);
1159
- replacements.set(module, { version });
1029
+ }
1030
+ // A Server refresh restores the unpatched file the Server package ships, so
1031
+ // an installed deferred module is re-applied in the same call. Its version is
1032
+ // resolved from the *new* package below, never from the old tree. Without a
1033
+ // Server refresh it is only applied when the caller names it explicitly.
1034
+ const autoReplaced = [];
1035
+ if (remote.some(spec => spec.module === 'server')) {
1036
+ for (const module of Object.keys(catalog_1.catalog)) {
1037
+ if (!catalog_1.catalog[module].deferred || !installed.has(module) || parsed.some(spec => spec.module === module))
1160
1038
  continue;
1161
- }
1162
- replacements.set(module, await stageReplacement(client, module, version, payloads, workspace, onProgress));
1039
+ parsed.push({ module });
1040
+ remote.push({ module });
1041
+ autoReplaced.push(module);
1042
+ onProgress?.(`${module} is re-applied with the Server package: the new package replaces the file it patches`);
1163
1043
  }
1164
1044
  }
1165
- // A Server or Monitor package is only usable when it still serves the
1166
- // configuration schema this environment runs, so both are checked against
1167
- // the deployed schema before anything is planned around them.
1168
- const schema = remote.some(spec => spec.module === 'server' || spec.module === 'monitoring')
1169
- ? environmentSchema(manifest, configured)
1170
- : undefined;
1171
- if (remote.some(spec => spec.module === 'server' || spec.module === 'monitoring') && !schema) {
1172
- throw new Error(`Module update for ${remote.filter(spec => spec.module === 'server' || spec.module === 'monitoring').map(spec => spec.module).join(' and ')} needs the configuration schema of ${entry.name}, which neither its manifest nor its conf/Wyn.conf records. Recreate the environment with create-env.`);
1173
- }
1174
- // A planned deferred module validates the exact Server file it replaces,
1175
- // with a message that names only the module; the Server's own required-file
1176
- // check would otherwise report the same missing file by its internal name.
1177
- const deferredPlanned = remote.some(spec => catalog_1.catalog[spec.module].deferred);
1178
- for (const spec of remote) {
1179
- const stagedArtifact = staged.get(spec.module);
1180
- if (!stagedArtifact || !schema || !(spec.module in requiredPackageFiles))
1181
- continue;
1182
- const required = spec.module === 'server' && deferredPlanned ? [] : requiredPackageFiles[spec.module];
1183
- const warning = await verifyStagedPackage(spec.module, stagedArtifact, schema, required);
1184
- if (warning)
1185
- warnings.push(warning);
1045
+ // Smart documents cannot run without the collaboration service, which
1046
+ // create-env deploys and declares in the Server configuration alongside
1047
+ // documents. Installing documents into an environment with no collaboration
1048
+ // deployment would produce a broken environment, so refuse — after the
1049
+ // edition gate above, so a CN environment still reports the EN requirement.
1050
+ if (parsed.some(spec => spec.module === 'documents') && !installed.has('collaboration')) {
1051
+ throw new Error(`Module documents requires the collaboration service, which ${entry.name} has not deployed. Recreate the environment with create-env: selecting documents deploys collaboration automatically.`);
1186
1052
  }
1187
- const plans = [];
1188
- // A module may carry several local specs: plan it once, for all its targets.
1189
- // The built-in trailing modules are planned last, in the same order
1190
- // `create-env` assembles: their trees live inside other modules'
1191
- // directories, so a Server replacement must not wipe them again. A deferred
1192
- // module runs after `server` even when the caller wrote it first: the Server
1193
- // directory must hold the new package before it is patched.
1194
- const rank = (module) => plan_1.trailingModules.includes(module) ? 2 : catalog_1.catalog[module].deferred ? 1 : 0;
1195
- const planOrder = [...new Set(parsed.map(spec => spec.module))].sort((left, right) => rank(left) - rank(right));
1196
- for (const module of planOrder) {
1197
- const artifact = artifacts.find(candidate => candidate.module === module);
1198
- const previous = moduleSources[module] ? { local: moduleSources[module].path } : { version: artifact?.version };
1199
- // Already deployed by the check above: report it and keep every file, the
1200
- // configuration and the manifest exactly as they are.
1201
- if (current.has(module)) {
1202
- skipped.push({ module, reason: 'current' });
1203
- onProgress?.(`${module}/${current.get(module)}: already deployed; skipped`);
1204
- continue;
1205
- }
1206
- const override = localOverrides.get(module);
1207
- if (override && module === 'server') {
1208
- // A local Server build is recorded, not laid out: the build directory is
1209
- // copied over `Server/` on every start (see `prepareLocalServer`), so an
1210
- // update only records it and never touches the Server tree or the
1211
- // configuration.
1212
- if (override.length !== 1)
1213
- throw new Error('Module server takes exactly one local build directory.');
1214
- onProgress?.('server: the local build is copied to Server on every start');
1215
- plans.push({
1216
- module, previous, next: { local: override[0].path }, local: override,
1217
- entries: [], copies: [], removals: [], preserved: [], copiedDirectories: [], removedDirectories: [],
1218
- });
1219
- continue;
1220
- }
1221
- if (override) {
1222
- const removals = [];
1223
- const removedDirectories = [];
1224
- for (const target of override) {
1225
- const destination = installationDirectory(entry.root, target.destination);
1226
- if (!await exists(destination))
1053
+ const state = await (0, validate_1.observeRuntimeState)(entry, this.processes);
1054
+ if (remote.length && state !== 'ready' && state !== 'stopped')
1055
+ 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.`);
1056
+ if (!remote.length && state !== 'ready' && state !== 'stopped' && state !== 'running')
1057
+ 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.`);
1058
+ const downloads = [...(manifest.downloads ?? [])];
1059
+ const selectedModules = stringListOrUndefined(manifest.selectedModules);
1060
+ const autoAddedModules = stringListOrUndefined(manifest.autoAddedModules);
1061
+ const moduleSources = { ...(manifest.moduleSources ?? {}) };
1062
+ // What a fresh environment would have selected for itself. An update records
1063
+ // a first install the way `create-env` does: a selectable module joins the
1064
+ // selection, a core component or an automatic dependency is added instead.
1065
+ const selectable = selectableModules();
1066
+ // The manifest records the .NET requirements read from each artifact's
1067
+ // runtimeconfig files, so an update replaces the entries of the modules it
1068
+ // refreshes and leaves every other module's alone.
1069
+ let requirements = Array.isArray(manifest.requirements) ? manifest.requirements.filter((line) => typeof line === 'string') : [];
1070
+ // Read once: the up-to-date check below inspects the same configuration the
1071
+ // patch would rewrite later, and an environment without it is not current.
1072
+ const configured = await fs.readFile(path.join(entry.root, 'conf', 'Wyn.conf'), 'utf8');
1073
+ const workspace = !dryRun && remote.length > 0 ? await fs.mkdtemp(path.join(path.dirname(entry.root), '.afm-env-')) : undefined;
1074
+ const warnings = [];
1075
+ try {
1076
+ const resolved = new Map();
1077
+ const staged = new Map();
1078
+ const replacements = new Map();
1079
+ // Modules already at the resolved version with a complete deployment: they
1080
+ // are reported as skipped and never downloaded, staged or written.
1081
+ const current = new Map();
1082
+ if (remote.length) {
1083
+ const client = clientFactory();
1084
+ const direct = remote.filter(spec => !catalog_1.catalog[spec.module].deferred);
1085
+ for (const spec of direct) {
1086
+ onProgress?.(`Resolving ${catalog_1.catalog[spec.module].plugin}`);
1087
+ resolved.set(spec.module, await resolveRemoteArtifact(client, entry, spec));
1088
+ }
1089
+ // A module the caller pinned is always deployed: the pin is the explicit
1090
+ // request for that build. Modules the same call re-deploys because of a
1091
+ // Server refresh are never current either — their files are about to be
1092
+ // replaced. Everything else is compared with the deployed record and its
1093
+ // actual files and configuration entries before anything is downloaded.
1094
+ for (const spec of direct) {
1095
+ if (spec.version !== undefined || autoRefreshed.includes(spec.module) || autoReplaced.includes(spec.module))
1227
1096
  continue;
1228
- removals.push(destination);
1229
- removedDirectories.push(target.destination);
1097
+ const artifact = resolved.get(spec.module);
1098
+ if (await moduleIsCurrent(entry, manifest, artifact, editionOf(entry), configured, this.platform)) {
1099
+ current.set(spec.module, artifact.version);
1100
+ onProgress?.(`${artifact.module}/${artifact.version}: already deployed; nothing downloaded`);
1101
+ }
1230
1102
  }
1231
- const entries = override.map(target => {
1232
- const entry = { destination: target.destination, assemblies: [target.assembly], path: target.path };
1233
- if (target.kind === 'worker') {
1234
- entry.role = 'worker';
1235
- if (target.service)
1236
- entry.service = target.service;
1103
+ if (!dryRun)
1104
+ for (const spec of direct)
1105
+ if (!current.has(spec.module))
1106
+ staged.set(spec.module, await stageRemoteArtifact(client, resolved.get(spec.module), editionOf(entry), workspace, onProgress));
1107
+ // A deferred module resolves after the direct artifacts are staged, so a
1108
+ // Server refresh contributes the *new* package's version. Without a
1109
+ // Server refresh the already deployed tree is the authority. A dry-run
1110
+ // that refreshes the Server cannot know that version without downloading
1111
+ // the package, so it reports the deferred resolution and stops there.
1112
+ for (const spec of remote.filter(candidate => catalog_1.catalog[candidate.module].deferred)) {
1113
+ const module = spec.module;
1114
+ const source = catalog_1.catalog[module];
1115
+ const label = (0, catalog_1.moduleLabel)(source);
1116
+ const server = staged.get('server');
1117
+ const refreshesServer = remote.some(candidate => candidate.module === 'server');
1118
+ const localServer = moduleSources.server?.type === 'local' ? moduleSources.server.path : undefined;
1119
+ let version;
1120
+ let resolvedFrom = 'the deployed Server package';
1121
+ if (server) {
1122
+ version = await (0, deferred_1.deferredVersionAt)(source, stagedServerFile(server, module));
1123
+ resolvedFrom = 'the refreshed Server package';
1124
+ // The replacement must overwrite an existing regular file in the new
1125
+ // package; checking the staged tree catches a package that no longer
1126
+ // ships it before anything has been written.
1127
+ for (const mapping of (0, deferred_1.replacementMappings)(source)) {
1128
+ const file = stagedServerPath(server, mapping.destination);
1129
+ const stat = await lstatOrUndefined(file);
1130
+ if (!stat)
1131
+ throw new Error(`Module ${label} could not be applied: the Server package holds no file to replace.`);
1132
+ if (!stat.isFile() || stat.isSymbolicLink())
1133
+ throw new Error(`Module ${label} could not be applied: the Server package holds no regular file to replace.`);
1134
+ }
1237
1135
  }
1238
- return entry;
1239
- });
1240
- plans.push({
1241
- module, previous, next: { local: override.map(target => target.path).join(', ') }, local: override,
1242
- entries, copies: [], removals, preserved: [], copiedDirectories: [], removedDirectories,
1243
- });
1244
- continue;
1245
- }
1246
- // A deferred module replaces one existing regular file and contributes
1247
- // no directory, plugin, worker, service or runtime requirement. Its
1248
- // version and payload were resolved above; nothing about the provider is
1249
- // recorded, and the target is checked before any write.
1250
- if (catalog_1.catalog[module].deferred) {
1251
- const replacement = replacements.get(module);
1252
- const label = (0, catalog_1.moduleLabel)(catalog_1.catalog[module]);
1253
- const version = replacement?.version || undefined;
1254
- const files = [];
1255
- if (replacement?.files?.length) {
1256
- const mappings = (0, deferred_1.replacementMappings)(catalog_1.catalog[module]);
1257
- for (const file of replacement.files) {
1258
- const matches = mappings.filter(mapping => mapping.source === file.name);
1259
- if (matches.length !== 1)
1260
- throw new Error(`Module ${label} could not be applied: the replacement target is not configured.`);
1261
- const destination = installationDirectory(entry.root, matches[0].destination);
1262
- const stat = await lstatOrUndefined(destination);
1263
- if (!stat)
1264
- throw new Error(`Module ${label} could not be applied: the Server tree holds no file to replace.`);
1265
- if (!stat.isFile() || stat.isSymbolicLink())
1266
- throw new Error(`Module ${label} could not be applied: the Server tree holds no regular file to replace.`);
1267
- files.push({ source: file.source, destination, sha256: file.sha256 });
1136
+ else if (refreshesServer) {
1137
+ onProgress?.(`${label}: version resolves from the refreshed Server package at execution time`);
1138
+ replacements.set(module, { version: '' });
1139
+ continue;
1268
1140
  }
1269
- }
1270
- plans.push({
1271
- module, previous, next: version ? { version } : {},
1272
- ...(files.length ? { replacement: { version: version, files } } : {}),
1273
- entries: [], copies: [], removals: [], preserved: [], copiedDirectories: [], removedDirectories: [],
1274
- });
1275
- continue;
1276
- }
1277
- const remoteArtifact = resolved.get(module);
1278
- const stagedArtifact = staged.get(module);
1279
- const copies = [];
1280
- const removals = [];
1281
- const preserved = [];
1282
- const copiedDirectories = [];
1283
- const removedDirectories = [];
1284
- const entries = [];
1285
- let configurationAssets;
1286
- let configurationLink;
1287
- // Only the mappings this environment's edition deploys: the other
1288
- // edition's directory is never removed, copied or referenced.
1289
- for (const mapping of stagedArtifact?.layout ?? (0, catalog_1.editionMappings)(remoteArtifact.mappings, editionOf(entry))) {
1290
- const destination = installationDirectory(entry.root, mapping.destination);
1291
- // A `resource` directory the module already deployed is replaced, never
1292
- // merged, and it contributes no plugin, worker, service or runtime entry.
1293
- if (await exists(destination)) {
1294
- removals.push(destination);
1295
- removedDirectories.push(mapping.destination);
1296
- }
1297
- // The Server settings the environment owns are never taken from the
1298
- // archive and must survive the replacement, so carry each one that is
1299
- // actually deployed across the directory swap.
1300
- if (stagedArtifact && mapping.role === 'server' && workspace) {
1301
- for (const name of excludedCopyNames(mapping)) {
1302
- if (await exists(path.join(destination, name)))
1303
- preserved.push({ directory: destination, name, source: path.join(workspace, 'preserved', module, name) });
1141
+ else if (localServer) {
1142
+ // A local Server build is copied over the deployed tree on every start,
1143
+ // so the version that must be cached now is the one that build
1144
+ // declares — not the version of the currently deployed package.
1145
+ version = await (0, deferred_1.deferredVersionAt)(source, localServerPath(localServer, source.deferred.versionFile));
1146
+ resolvedFrom = 'the local Server build directory';
1147
+ }
1148
+ else
1149
+ version = await (0, deferred_1.deferredVersion)(source, entry.root);
1150
+ // Already applied and untouched: the recorded download still hashes to
1151
+ // the deployed file, so there is nothing to download or copy. A call
1152
+ // that refreshes the Server always re-applies it (autoReplaced), and a
1153
+ // local Server build resolves a version this tree does not hold.
1154
+ if (!refreshesServer && !localServer && spec.version === undefined && !autoReplaced.includes(module)
1155
+ && await replacementIsCurrent(entry, manifest, module, version)) {
1156
+ current.set(module, version);
1157
+ onProgress?.(`${label}/${version}: already applied; nothing downloaded`);
1158
+ continue;
1304
1159
  }
1160
+ const payloads = await (0, deferred_1.deferredPayload)(source, version, client);
1161
+ if (dryRun) {
1162
+ onProgress?.(`${label}/${version}: resolved from ${resolvedFrom}; nothing downloaded`);
1163
+ replacements.set(module, { version });
1164
+ continue;
1165
+ }
1166
+ replacements.set(module, await stageReplacement(client, module, version, payloads, workspace, onProgress));
1305
1167
  }
1306
- if (stagedArtifact)
1307
- copies.push({ source: path.join(stagedArtifact.expanded, ...mapping.source.split('/')), destination, exclude: [...excludedCopyNames(mapping), ...(mapping.exclude ?? []).map(name => name.toLowerCase())] });
1308
- copiedDirectories.push(mapping.destination);
1309
- if (mapping.role === 'resource')
1168
+ }
1169
+ // A Server or Monitor package is only usable when it still serves the
1170
+ // configuration schema this environment runs, so both are checked against
1171
+ // the deployed schema before anything is planned around them.
1172
+ const schema = remote.some(spec => spec.module === 'server' || spec.module === 'monitoring')
1173
+ ? environmentSchema(manifest, configured)
1174
+ : undefined;
1175
+ if (remote.some(spec => spec.module === 'server' || spec.module === 'monitoring') && !schema) {
1176
+ throw new Error(`Module update for ${remote.filter(spec => spec.module === 'server' || spec.module === 'monitoring').map(spec => spec.module).join(' and ')} needs the configuration schema of ${entry.name}, which neither its manifest nor its conf/Wyn.conf records. Recreate the environment with create-env.`);
1177
+ }
1178
+ // A planned deferred module validates the exact Server file it replaces,
1179
+ // with a message that names only the module; the Server's own required-file
1180
+ // check would otherwise report the same missing file by its internal name.
1181
+ const deferredPlanned = remote.some(spec => catalog_1.catalog[spec.module].deferred);
1182
+ for (const spec of remote) {
1183
+ const stagedArtifact = staged.get(spec.module);
1184
+ if (!stagedArtifact || !schema || !(spec.module in requiredPackageFiles))
1310
1185
  continue;
1311
- // A native out-of-process service has no assembly and is declared as an
1312
- // empty node: the Server loads it from that entry and supplies the rest.
1313
- if (mapping.role === 'service') {
1314
- if (mapping.service)
1315
- entries.push({ destination: mapping.destination, assemblies: [], path: mapping.destination, role: 'service', service: mapping.service });
1186
+ const required = spec.module === 'server' && deferredPlanned ? [] : requiredPackageFiles[spec.module];
1187
+ const warning = await verifyStagedPackage(spec.module, stagedArtifact, schema, required);
1188
+ if (warning)
1189
+ warnings.push(warning);
1190
+ }
1191
+ const plans = [];
1192
+ // A module may carry several local specs: plan it once, for all its targets.
1193
+ // The built-in trailing modules are planned last, in the same order
1194
+ // `create-env` assembles: their trees live inside other modules'
1195
+ // directories, so a Server replacement must not wipe them again. A deferred
1196
+ // module runs after `server` even when the caller wrote it first: the Server
1197
+ // directory must hold the new package before it is patched.
1198
+ const rank = (module) => plan_1.trailingModules.includes(module) ? 2 : catalog_1.catalog[module].deferred ? 1 : 0;
1199
+ const planOrder = [...new Set(parsed.map(spec => spec.module))].sort((left, right) => rank(left) - rank(right));
1200
+ for (const module of planOrder) {
1201
+ const artifact = artifacts.find(candidate => candidate.module === module);
1202
+ const previous = moduleSources[module] ? { local: moduleSources[module].path } : { version: artifact?.version };
1203
+ // Already deployed by the check above: report it and keep every file, the
1204
+ // configuration and the manifest exactly as they are.
1205
+ if (current.has(module)) {
1206
+ skipped.push({ module, reason: 'current' });
1207
+ onProgress?.(`${module}/${current.get(module)}: already deployed; skipped`);
1316
1208
  continue;
1317
1209
  }
1318
- const assembly = stagedArtifact?.assemblies.get(mapping.destination) ?? knownAssembly(configured, mapping, this.platform);
1319
- // Plugins stay relative to the environment root; a worker entry does too,
1320
- // and its service keeps using the deployed destination as working directory.
1321
- // The Server service the Monitor starts is kept in step the same way.
1322
- if (mapping.role === 'plugin' || mapping.role === 'worker' || mapping.role === 'server') {
1323
- const entry = { destination: mapping.destination, assemblies: mapping.assemblies, path: path.posix.join(mapping.destination, assembly) };
1324
- if (mapping.role === 'worker' || mapping.role === 'server') {
1325
- entry.role = mapping.role;
1326
- if (mapping.service)
1327
- entry.service = mapping.service;
1210
+ const override = localOverrides.get(module);
1211
+ if (override && module === 'server') {
1212
+ // A local Server build is recorded, not laid out: the build directory is
1213
+ // copied over `Server/` on every start (see `prepareLocalServer`), so an
1214
+ // update only records it and never touches the Server tree or the
1215
+ // configuration.
1216
+ if (override.length !== 1)
1217
+ throw new Error('Module server takes exactly one local build directory.');
1218
+ onProgress?.('server: the local build is copied to Server on every start');
1219
+ plans.push({
1220
+ module, previous, next: { local: override[0].path }, local: override,
1221
+ entries: [], copies: [], removals: [], preserved: [], copiedDirectories: [], removedDirectories: [],
1222
+ });
1223
+ continue;
1224
+ }
1225
+ if (override) {
1226
+ const removals = [];
1227
+ const removedDirectories = [];
1228
+ for (const target of override) {
1229
+ const destination = installationDirectory(entry.root, target.destination);
1230
+ if (!await exists(destination))
1231
+ continue;
1232
+ removals.push(destination);
1233
+ removedDirectories.push(target.destination);
1328
1234
  }
1329
- entries.push(entry);
1235
+ const entries = override.map(target => {
1236
+ const entry = { destination: target.destination, assemblies: [target.assembly], path: target.path };
1237
+ if (target.kind === 'worker') {
1238
+ entry.role = 'worker';
1239
+ if (target.service)
1240
+ entry.service = target.service;
1241
+ }
1242
+ return entry;
1243
+ });
1244
+ plans.push({
1245
+ module, previous, next: { local: override.map(target => target.path).join(', ') }, local: override,
1246
+ entries, copies: [], removals, preserved: [], copiedDirectories: [], removedDirectories,
1247
+ });
1248
+ continue;
1330
1249
  }
1331
- // The Monitor resolves its configuration from its own working directory,
1332
- // so its package assets belong in the canonical `conf` and the directory
1333
- // itself has to point back at it once the Monitor tree is replaced.
1334
- if (mapping.role === 'monitor' && stagedArtifact) {
1335
- const assets = path.join(stagedArtifact.expanded, 'conf');
1336
- if ((await fs.lstat(assets).catch(() => undefined))?.isDirectory())
1337
- configurationAssets = { source: assets, destination: path.join(entry.root, 'conf') };
1338
- configurationLink = path.join(destination, 'conf');
1250
+ // A deferred module replaces one existing regular file and contributes
1251
+ // no directory, plugin, worker, service or runtime requirement. Its
1252
+ // version and payload were resolved above; nothing about the provider is
1253
+ // recorded, and the target is checked before any write.
1254
+ if (catalog_1.catalog[module].deferred) {
1255
+ const replacement = replacements.get(module);
1256
+ const label = (0, catalog_1.moduleLabel)(catalog_1.catalog[module]);
1257
+ const version = replacement?.version || undefined;
1258
+ const files = [];
1259
+ if (replacement?.files?.length) {
1260
+ const mappings = (0, deferred_1.replacementMappings)(catalog_1.catalog[module]);
1261
+ for (const file of replacement.files) {
1262
+ const matches = mappings.filter(mapping => mapping.source === file.name);
1263
+ if (matches.length !== 1)
1264
+ throw new Error(`Module ${label} could not be applied: the replacement target is not configured.`);
1265
+ const destination = installationDirectory(entry.root, matches[0].destination);
1266
+ const stat = await lstatOrUndefined(destination);
1267
+ if (!stat)
1268
+ throw new Error(`Module ${label} could not be applied: the Server tree holds no file to replace.`);
1269
+ if (!stat.isFile() || stat.isSymbolicLink())
1270
+ throw new Error(`Module ${label} could not be applied: the Server tree holds no regular file to replace.`);
1271
+ files.push({ source: file.source, destination, sha256: file.sha256 });
1272
+ }
1273
+ }
1274
+ plans.push({
1275
+ module, previous, next: version ? { version } : {},
1276
+ ...(files.length ? { replacement: { version: version, files } } : {}),
1277
+ entries: [], copies: [], removals: [], preserved: [], copiedDirectories: [], removedDirectories: [],
1278
+ });
1279
+ continue;
1339
1280
  }
1340
- }
1341
- if (module === 'dashboard' && stagedArtifact) {
1342
- const contents = path.join(stagedArtifact.expanded, 'Web Contents');
1343
- if ((await fs.lstat(contents).catch(() => undefined))?.isDirectory()) {
1344
- const destination = path.join(entry.root, 'Web Contents');
1281
+ const remoteArtifact = resolved.get(module);
1282
+ const stagedArtifact = staged.get(module);
1283
+ const copies = [];
1284
+ const removals = [];
1285
+ const preserved = [];
1286
+ const copiedDirectories = [];
1287
+ const removedDirectories = [];
1288
+ const entries = [];
1289
+ let configurationAssets;
1290
+ let configurationLink;
1291
+ // Only the mappings this environment's edition deploys: the other
1292
+ // edition's directory is never removed, copied or referenced.
1293
+ for (const mapping of stagedArtifact?.layout ?? (0, catalog_1.editionMappings)(remoteArtifact.mappings, editionOf(entry))) {
1294
+ const destination = installationDirectory(entry.root, mapping.destination);
1295
+ // A `resource` directory the module already deployed is replaced, never
1296
+ // merged, and it contributes no plugin, worker, service or runtime entry.
1345
1297
  if (await exists(destination)) {
1346
1298
  removals.push(destination);
1347
- removedDirectories.push('Web Contents');
1299
+ removedDirectories.push(mapping.destination);
1300
+ }
1301
+ // The Server settings the environment owns are never taken from the
1302
+ // archive and must survive the replacement, so carry each one that is
1303
+ // actually deployed across the directory swap.
1304
+ if (stagedArtifact && mapping.role === 'server' && workspace) {
1305
+ for (const name of excludedCopyNames(mapping)) {
1306
+ if (await exists(path.join(destination, name)))
1307
+ preserved.push({ directory: destination, name, source: path.join(workspace, 'preserved', module, name) });
1308
+ }
1309
+ }
1310
+ if (stagedArtifact)
1311
+ copies.push({ source: path.join(stagedArtifact.expanded, ...mapping.source.split('/')), destination, exclude: [...excludedCopyNames(mapping), ...(mapping.exclude ?? []).map(name => name.toLowerCase())] });
1312
+ copiedDirectories.push(mapping.destination);
1313
+ if (mapping.role === 'resource')
1314
+ continue;
1315
+ // A native out-of-process service has no assembly and is declared as an
1316
+ // empty node: the Server loads it from that entry and supplies the rest.
1317
+ if (mapping.role === 'service') {
1318
+ if (mapping.service)
1319
+ entries.push({ destination: mapping.destination, assemblies: [], path: mapping.destination, role: 'service', service: mapping.service });
1320
+ continue;
1321
+ }
1322
+ const assembly = stagedArtifact?.assemblies.get(mapping.destination) ?? knownAssembly(configured, mapping, this.platform);
1323
+ // Plugins stay relative to the environment root; a worker entry does too,
1324
+ // and its service keeps using the deployed destination as working directory.
1325
+ // The Server service the Monitor starts is kept in step the same way.
1326
+ if (mapping.role === 'plugin' || mapping.role === 'worker' || mapping.role === 'server') {
1327
+ const entry = { destination: mapping.destination, assemblies: mapping.assemblies, path: path.posix.join(mapping.destination, assembly) };
1328
+ if (mapping.role === 'worker' || mapping.role === 'server') {
1329
+ entry.role = mapping.role;
1330
+ if (mapping.service)
1331
+ entry.service = mapping.service;
1332
+ }
1333
+ entries.push(entry);
1334
+ }
1335
+ // The Monitor resolves its configuration from its own working directory,
1336
+ // so its package assets belong in the canonical `conf` and the directory
1337
+ // itself has to point back at it once the Monitor tree is replaced.
1338
+ if (mapping.role === 'monitor' && stagedArtifact) {
1339
+ const assets = path.join(stagedArtifact.expanded, 'conf');
1340
+ if ((await fs.lstat(assets).catch(() => undefined))?.isDirectory())
1341
+ configurationAssets = { source: assets, destination: path.join(entry.root, 'conf') };
1342
+ configurationLink = path.join(destination, 'conf');
1348
1343
  }
1349
- copies.push({ source: contents, destination, exclude: [] });
1350
- copiedDirectories.push('Web Contents');
1351
1344
  }
1345
+ if (module === 'dashboard' && stagedArtifact) {
1346
+ const contents = path.join(stagedArtifact.expanded, 'Web Contents');
1347
+ if ((await fs.lstat(contents).catch(() => undefined))?.isDirectory()) {
1348
+ const destination = path.join(entry.root, 'Web Contents');
1349
+ if (await exists(destination)) {
1350
+ removals.push(destination);
1351
+ removedDirectories.push('Web Contents');
1352
+ }
1353
+ copies.push({ source: contents, destination, exclude: [] });
1354
+ copiedDirectories.push('Web Contents');
1355
+ }
1356
+ }
1357
+ plans.push({
1358
+ module, previous, next: { version: remoteArtifact.version }, artifact: remoteArtifact, staged: stagedArtifact,
1359
+ entries, copies, removals, preserved, ...(configurationAssets ? { configurationAssets } : {}), ...(configurationLink ? { configurationLink } : {}),
1360
+ copiedDirectories, removedDirectories,
1361
+ });
1352
1362
  }
1353
- plans.push({
1354
- module, previous, next: { version: remoteArtifact.version }, artifact: remoteArtifact, staged: stagedArtifact,
1355
- entries, copies, removals, preserved, ...(configurationAssets ? { configurationAssets } : {}), ...(configurationLink ? { configurationLink } : {}),
1356
- copiedDirectories, removedDirectories,
1357
- });
1358
- }
1359
- const changes = plans.map(plan => ({ module: plan.module, previous: plan.previous, next: plan.next, copiedDirectories: plan.copiedDirectories, removedDirectories: plan.removedDirectories }));
1360
- const configuration = patchEnvironmentConfiguration(configured, plans.flatMap(plan => plan.entries), this.platform);
1361
- const nextArtifacts = [...artifacts];
1362
- const nextDownloads = [...downloads];
1363
- for (const plan of plans) {
1364
- if (plan.artifact) {
1365
- const replacement = {
1366
- module: plan.module, plugin: plan.artifact.plugin, version: plan.artifact.version, name: plan.artifact.name, path: plan.artifact.path,
1367
- mappings: plan.artifact.mappings.map(mapping => ({ ...mapping, assemblies: [...mapping.assemblies] })),
1368
- };
1369
- const index = nextArtifacts.findIndex(candidate => candidate.module === plan.module);
1370
- if (index < 0)
1371
- nextArtifacts.push(replacement);
1372
- else
1373
- nextArtifacts[index] = { ...nextArtifacts[index], ...replacement };
1374
- if (plan.staged) {
1375
- const download = { plugin: plan.artifact.plugin, version: plan.artifact.version, name: plan.artifact.name, sha256: plan.staged.sha256 };
1376
- const downloadIndex = nextDownloads.findIndex(candidate => candidate.plugin === plan.artifact.plugin);
1377
- if (downloadIndex < 0)
1378
- nextDownloads.push(download);
1363
+ const changes = plans.map(plan => ({ module: plan.module, previous: plan.previous, next: plan.next, copiedDirectories: plan.copiedDirectories, removedDirectories: plan.removedDirectories }));
1364
+ const configuration = patchEnvironmentConfiguration(configured, plans.flatMap(plan => plan.entries), this.platform);
1365
+ const nextArtifacts = [...artifacts];
1366
+ const nextDownloads = [...downloads];
1367
+ for (const plan of plans) {
1368
+ if (plan.artifact) {
1369
+ const replacement = {
1370
+ module: plan.module, plugin: plan.artifact.plugin, version: plan.artifact.version, name: plan.artifact.name, path: plan.artifact.path,
1371
+ mappings: plan.artifact.mappings.map(mapping => ({ ...mapping, assemblies: [...mapping.assemblies] })),
1372
+ };
1373
+ const index = nextArtifacts.findIndex(candidate => candidate.module === plan.module);
1374
+ if (index < 0)
1375
+ nextArtifacts.push(replacement);
1379
1376
  else
1380
- nextDownloads[downloadIndex] = download;
1381
- // The requirements the old artifact needed are gone with it: re-read
1382
- // the ones the new package actually declares, per deployed mapping.
1383
- // A resource mapping is a static tree: it declares none.
1384
- requirements = requirements.filter(line => !line.endsWith(` (${plan.module})`));
1385
- for (const mapping of plan.staged.layout) {
1386
- if (mapping.role === 'service' || mapping.role === 'resource')
1387
- continue;
1388
- for (const framework of await (0, builder_1.runtimeRequirements)(path.join(plan.staged.expanded, ...mapping.source.split('/')))) {
1389
- requirements.push(`${framework.name} ${framework.version} (${plan.module})`);
1377
+ nextArtifacts[index] = { ...nextArtifacts[index], ...replacement };
1378
+ if (plan.staged) {
1379
+ const download = { plugin: plan.artifact.plugin, version: plan.artifact.version, name: plan.artifact.name, sha256: plan.staged.sha256 };
1380
+ const downloadIndex = nextDownloads.findIndex(candidate => candidate.plugin === plan.artifact.plugin);
1381
+ if (downloadIndex < 0)
1382
+ nextDownloads.push(download);
1383
+ else
1384
+ nextDownloads[downloadIndex] = download;
1385
+ // The requirements the old artifact needed are gone with it: re-read
1386
+ // the ones the new package actually declares, per deployed mapping.
1387
+ // A resource mapping is a static tree: it declares none.
1388
+ requirements = requirements.filter(line => !line.endsWith(` (${plan.module})`));
1389
+ for (const mapping of plan.staged.layout) {
1390
+ if (mapping.role === 'service' || mapping.role === 'resource')
1391
+ continue;
1392
+ for (const framework of await (0, builder_1.runtimeRequirements)(path.join(plan.staged.expanded, ...mapping.source.split('/')))) {
1393
+ requirements.push(`${framework.name} ${framework.version} (${plan.module})`);
1394
+ }
1390
1395
  }
1391
1396
  }
1397
+ delete moduleSources[plan.module];
1392
1398
  }
1393
- delete moduleSources[plan.module];
1394
- }
1395
- if (plan.replacement) {
1396
- // The record keeps the public label, the resolved version and the
1397
- // payload's SHA256. No remote path and no mapping are written: both
1398
- // would expose a name the module hides.
1399
- const label = (0, catalog_1.moduleLabel)(catalog_1.catalog[plan.module]);
1400
- const replacement = { module: plan.module, plugin: label, version: plan.replacement.version, name: label };
1401
- const index = nextArtifacts.findIndex(candidate => candidate.module === plan.module);
1402
- if (index < 0)
1403
- nextArtifacts.push(replacement);
1404
- else
1405
- nextArtifacts[index] = { ...nextArtifacts[index], ...replacement };
1406
- for (const file of plan.replacement.files) {
1407
- const download = { plugin: label, version: plan.replacement.version, name: label, sha256: file.sha256 };
1408
- const downloadIndex = nextDownloads.findIndex(candidate => candidate.plugin === label);
1409
- if (downloadIndex < 0)
1410
- nextDownloads.push(download);
1399
+ if (plan.replacement) {
1400
+ // The record keeps the public label, the resolved version and the
1401
+ // payload's SHA256. No remote path and no mapping are written: both
1402
+ // would expose a name the module hides.
1403
+ const label = (0, catalog_1.moduleLabel)(catalog_1.catalog[plan.module]);
1404
+ const replacement = { module: plan.module, plugin: label, version: plan.replacement.version, name: label };
1405
+ const index = nextArtifacts.findIndex(candidate => candidate.module === plan.module);
1406
+ if (index < 0)
1407
+ nextArtifacts.push(replacement);
1411
1408
  else
1412
- nextDownloads[downloadIndex] = download;
1409
+ nextArtifacts[index] = { ...nextArtifacts[index], ...replacement };
1410
+ for (const file of plan.replacement.files) {
1411
+ const download = { plugin: label, version: plan.replacement.version, name: label, sha256: file.sha256 };
1412
+ const downloadIndex = nextDownloads.findIndex(candidate => candidate.plugin === label);
1413
+ if (downloadIndex < 0)
1414
+ nextDownloads.push(download);
1415
+ else
1416
+ nextDownloads[downloadIndex] = download;
1417
+ }
1418
+ requirements = requirements.filter(line => !line.endsWith(` (${plan.module})`));
1419
+ delete moduleSources[plan.module];
1413
1420
  }
1414
- requirements = requirements.filter(line => !line.endsWith(` (${plan.module})`));
1415
- delete moduleSources[plan.module];
1416
- }
1417
- if (plan.local)
1418
- moduleSources[plan.module] = manifestSource(plan.module, plan.local, moduleSources[plan.module]);
1419
- // First installing a module records it the way `create-env` would: a
1420
- // selectable one joins the selection, a core component or an automatic
1421
- // dependency joins the automatically added list instead.
1422
- if (!installed.has(plan.module)) {
1423
- if (selectable.has(plan.module)) {
1424
- if (selectedModules)
1425
- selectedModules.push(plan.module);
1421
+ if (plan.local)
1422
+ moduleSources[plan.module] = manifestSource(plan.module, plan.local, moduleSources[plan.module]);
1423
+ // First installing a module records it the way `create-env` would: a
1424
+ // selectable one joins the selection, a core component or an automatic
1425
+ // dependency joins the automatically added list instead.
1426
+ if (!installed.has(plan.module)) {
1427
+ if (selectable.has(plan.module)) {
1428
+ if (selectedModules)
1429
+ selectedModules.push(plan.module);
1430
+ }
1431
+ else if (autoAddedModules)
1432
+ autoAddedModules.push(plan.module);
1426
1433
  }
1427
- else if (autoAddedModules)
1428
- autoAddedModules.push(plan.module);
1429
1434
  }
1430
- }
1431
- if (selectedModules) {
1432
- const unique = [...new Set(selectedModules)].sort();
1433
- selectedModules.length = 0;
1434
- selectedModules.push(...unique);
1435
- }
1436
- if (autoAddedModules) {
1437
- const unique = [...new Set(autoAddedModules)].sort();
1438
- autoAddedModules.length = 0;
1439
- autoAddedModules.push(...unique);
1440
- }
1441
- // A create-only module deploys its payload inside the Server directory and is
1442
- // never refreshed, so a Server refresh replaces the tree it owns: the module
1443
- // is gone from the environment, and its record goes with it — the manifest
1444
- // and the registry would otherwise keep claiming a deployment that is not
1445
- // there any more.
1446
- const removedCreateOnly = [];
1447
- if (plans.some(plan => plan.module === 'server')) {
1448
- for (const module of Object.keys(catalog_1.catalog)) {
1449
- if (catalog_1.catalog[module].createOnly !== true || !installed.has(module) || plans.some(plan => plan.module === module))
1450
- continue;
1451
- const plugin = catalog_1.catalog[module].plugin;
1452
- for (let index = nextArtifacts.length - 1; index >= 0; index -= 1)
1453
- if (nextArtifacts[index].module === module)
1454
- nextArtifacts.splice(index, 1);
1455
- for (let index = nextDownloads.length - 1; index >= 0; index -= 1)
1456
- if (nextDownloads[index].plugin === plugin)
1457
- nextDownloads.splice(index, 1);
1458
- for (let index = requirements.length - 1; index >= 0; index -= 1)
1459
- if (requirements[index].endsWith(` (${module})`))
1460
- requirements.splice(index, 1);
1461
- if (autoAddedModules)
1462
- autoAddedModules.splice(0, autoAddedModules.length, ...autoAddedModules.filter(name => name !== module));
1463
- delete moduleSources[module];
1464
- removedCreateOnly.push(module);
1435
+ if (selectedModules) {
1436
+ const unique = [...new Set(selectedModules)].sort();
1437
+ selectedModules.length = 0;
1438
+ selectedModules.push(...unique);
1465
1439
  }
1466
- }
1467
- const nextManifest = { ...manifest, artifacts: nextArtifacts, downloads: nextDownloads, requirements: [...new Set(requirements)] };
1468
- if (autoAddedModules)
1469
- nextManifest.autoAddedModules = autoAddedModules;
1470
- if (Object.keys(moduleSources).length)
1471
- nextManifest.moduleSources = moduleSources;
1472
- else
1473
- delete nextManifest.moduleSources;
1474
- if (selectedModules)
1475
- nextManifest.selectedModules = selectedModules;
1476
- // A stale `resources` key from an older manifest is dropped here: the
1477
- // packaged-resource feature is gone and nothing records it any more.
1478
- delete nextManifest.resources;
1479
- 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();
1480
- const localModules = Object.keys(moduleSources).sort();
1481
- if (state === 'running')
1482
- warnings.push(`${entry.name} is running; restart it to load the updated modules.`);
1483
- for (const item of skipped) {
1484
- if (item.reason === 'local')
1485
- warnings.push(`${item.module} has a local source and was not updated from artifacts.`);
1486
- else if (item.reason === 'provider')
1487
- warnings.push(`${item.module} is published only on ${(0, catalog_1.providerLabel)('oss')} and was not updated from ${(0, catalog_1.providerLabel)('az')}.`);
1488
- }
1489
- const currentSkipped = skipped.filter(item => item.reason === 'current');
1490
- if (currentSkipped.length)
1491
- warnings.push(`Already at the resolved version (nothing updated): ${currentSkipped.map(item => `${item.module} ${current.get(item.module)}`).join(', ')}.`);
1492
- // The Server and Monitor packages each ship the XSD assets for a
1493
- // configuration schema, so replacing one of them alone leaves the
1494
- // environment mixing two package generations for the same configuration.
1495
- // Only a plan that really deployed a package pairs with the other package;
1496
- // recording a local Server build directory touches no package at all.
1497
- const runtime = plans.filter(plan => (plan.module === 'server' || plan.module === 'monitoring') && plan.copiedDirectories.length > 0).map(plan => plan.module).sort();
1498
- if (runtime.length === 1)
1499
- warnings.push(`Only ${runtime[0]} was refreshed; refresh ${runtime[0] === 'server' ? 'monitoring' : 'server'} in the same call to keep both packages on one configuration schema.`);
1500
- if (parsed.some(spec => spec.module === 'documents') && !parsed.some(spec => spec.module === 'collaboration')) {
1501
- warnings.push('documents was refreshed without collaboration; the smart documents plugin may expect the deployed collaboration service version, so refresh collaboration too.');
1502
- }
1503
- if (autoRefreshed.length)
1504
- warnings.push(`${autoRefreshed.join(', ')} was refreshed with the Server package: its trees live inside the Server directory and would otherwise be lost.`);
1505
- if (autoReplaced.length)
1506
- warnings.push(`${autoReplaced.join(', ')} was re-applied with the Server package: the new package ships the file it replaces, so it was applied again.`);
1507
- if (removedCreateOnly.length)
1508
- warnings.push(`${removedCreateOnly.join(', ')} was removed with the Server package: it is deployed only when an environment is created.`);
1509
- // Nothing to deploy: every requested module is already at the resolved
1510
- // version with a complete deployment. Do not rewrite the manifest, the
1511
- // registry or the configuration just to say so.
1512
- if (!plans.length) {
1513
- onProgress?.(`${entry.name}: nothing to update; every requested module is already at its resolved version`);
1514
- return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired: false, warnings, localTargets: [] };
1515
- }
1516
- if (!dryRun) {
1517
- for (const plan of plans) {
1518
- // Take the environment-owned settings aside first, and drop the Monitor
1519
- // configuration junction before the directory holding it: a recursive
1520
- // delete must never reach the canonical `conf` the junction points at.
1521
- for (const file of plan.preserved) {
1522
- await fs.mkdir(path.dirname(file.source), { recursive: true });
1523
- await fs.copyFile(path.join(file.directory, file.name), file.source);
1524
- }
1525
- if (plan.configurationLink) {
1526
- const existing = await fs.lstat(plan.configurationLink).catch(() => undefined);
1527
- if (existing?.isSymbolicLink())
1528
- await fs.unlink(plan.configurationLink);
1529
- else
1530
- await fs.rm(plan.configurationLink, { recursive: true, force: true });
1440
+ if (autoAddedModules) {
1441
+ const unique = [...new Set(autoAddedModules)].sort();
1442
+ autoAddedModules.length = 0;
1443
+ autoAddedModules.push(...unique);
1444
+ }
1445
+ // A create-only module deploys its payload inside the Server directory and is
1446
+ // never refreshed, so a Server refresh replaces the tree it owns: the module
1447
+ // is gone from the environment, and its record goes with it — the manifest
1448
+ // and the registry would otherwise keep claiming a deployment that is not
1449
+ // there any more.
1450
+ const removedCreateOnly = [];
1451
+ if (plans.some(plan => plan.module === 'server')) {
1452
+ for (const module of Object.keys(catalog_1.catalog)) {
1453
+ if (catalog_1.catalog[module].createOnly !== true || !installed.has(module) || plans.some(plan => plan.module === module))
1454
+ continue;
1455
+ const plugin = catalog_1.catalog[module].plugin;
1456
+ for (let index = nextArtifacts.length - 1; index >= 0; index -= 1)
1457
+ if (nextArtifacts[index].module === module)
1458
+ nextArtifacts.splice(index, 1);
1459
+ for (let index = nextDownloads.length - 1; index >= 0; index -= 1)
1460
+ if (nextDownloads[index].plugin === plugin)
1461
+ nextDownloads.splice(index, 1);
1462
+ for (let index = requirements.length - 1; index >= 0; index -= 1)
1463
+ if (requirements[index].endsWith(` (${module})`))
1464
+ requirements.splice(index, 1);
1465
+ if (autoAddedModules)
1466
+ autoAddedModules.splice(0, autoAddedModules.length, ...autoAddedModules.filter(name => name !== module));
1467
+ delete moduleSources[module];
1468
+ removedCreateOnly.push(module);
1531
1469
  }
1532
- for (const removal of plan.removals)
1533
- await fs.rm(removal, { recursive: true, force: true });
1534
- for (const copy of plan.copies)
1535
- await (0, builder_1.copyTree)(copy.source, copy.destination, copy.exclude);
1536
- for (const file of plan.preserved)
1537
- await fs.copyFile(file.source, path.join(file.directory, file.name));
1538
- // The replacement is applied after the directory it lives in has been
1539
- // copied, so a Server refresh cannot overwrite it again. The target is
1540
- // re-checked here and every error names the module only.
1541
- if (plan.replacement) {
1542
- const label = (0, catalog_1.moduleLabel)(catalog_1.catalog[plan.module]);
1543
- try {
1544
- for (const file of plan.replacement.files) {
1545
- const stat = await fs.lstat(file.destination).catch(() => undefined);
1546
- if (!stat)
1547
- throw new Error('missing');
1548
- if (!stat.isFile() || stat.isSymbolicLink())
1549
- throw new Error('not a regular file');
1550
- await fs.copyFile(file.source, file.destination);
1470
+ }
1471
+ const nextManifest = { ...manifest, artifacts: nextArtifacts, downloads: nextDownloads, requirements: [...new Set(requirements)] };
1472
+ if (autoAddedModules)
1473
+ nextManifest.autoAddedModules = autoAddedModules;
1474
+ if (Object.keys(moduleSources).length)
1475
+ nextManifest.moduleSources = moduleSources;
1476
+ else
1477
+ delete nextManifest.moduleSources;
1478
+ if (selectedModules)
1479
+ nextManifest.selectedModules = selectedModules;
1480
+ // A stale `resources` key from an older manifest is dropped here: the
1481
+ // packaged-resource feature is gone and nothing records it any more.
1482
+ delete nextManifest.resources;
1483
+ 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();
1484
+ const localModules = Object.keys(moduleSources).sort();
1485
+ if (state === 'running')
1486
+ warnings.push(`${entry.name} is running; restart it to load the updated modules.`);
1487
+ for (const item of skipped) {
1488
+ if (item.reason === 'local')
1489
+ warnings.push(`${item.module} has a local source and was not updated from artifacts.`);
1490
+ else if (item.reason === 'provider')
1491
+ warnings.push(`${item.module} is published only on ${(0, catalog_1.providerLabel)('oss')} and was not updated from ${(0, catalog_1.providerLabel)('az')}.`);
1492
+ }
1493
+ const currentSkipped = skipped.filter(item => item.reason === 'current');
1494
+ if (currentSkipped.length)
1495
+ warnings.push(`Already at the resolved version (nothing updated): ${currentSkipped.map(item => `${item.module} ${current.get(item.module)}`).join(', ')}.`);
1496
+ // The Server and Monitor packages each ship the XSD assets for a
1497
+ // configuration schema, so replacing one of them alone leaves the
1498
+ // environment mixing two package generations for the same configuration.
1499
+ // Only a plan that really deployed a package pairs with the other package;
1500
+ // recording a local Server build directory touches no package at all.
1501
+ const runtime = plans.filter(plan => (plan.module === 'server' || plan.module === 'monitoring') && plan.copiedDirectories.length > 0).map(plan => plan.module).sort();
1502
+ if (runtime.length === 1)
1503
+ warnings.push(`Only ${runtime[0]} was refreshed; refresh ${runtime[0] === 'server' ? 'monitoring' : 'server'} in the same call to keep both packages on one configuration schema.`);
1504
+ if (parsed.some(spec => spec.module === 'documents') && !parsed.some(spec => spec.module === 'collaboration')) {
1505
+ warnings.push('documents was refreshed without collaboration; the smart documents plugin may expect the deployed collaboration service version, so refresh collaboration too.');
1506
+ }
1507
+ if (autoRefreshed.length)
1508
+ warnings.push(`${autoRefreshed.join(', ')} was refreshed with the Server package: its trees live inside the Server directory and would otherwise be lost.`);
1509
+ if (autoReplaced.length)
1510
+ warnings.push(`${autoReplaced.join(', ')} was re-applied with the Server package: the new package ships the file it replaces, so it was applied again.`);
1511
+ if (removedCreateOnly.length)
1512
+ warnings.push(`${removedCreateOnly.join(', ')} was removed with the Server package: it is deployed only when an environment is created.`);
1513
+ // Nothing to deploy: every requested module is already at the resolved
1514
+ // version with a complete deployment. Do not rewrite the manifest, the
1515
+ // registry or the configuration just to say so.
1516
+ if (!plans.length) {
1517
+ onProgress?.(`${entry.name}: nothing to update; every requested module is already at its resolved version`);
1518
+ return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired: false, warnings, localTargets: [] };
1519
+ }
1520
+ if (!dryRun) {
1521
+ for (const plan of plans) {
1522
+ // Take the environment-owned settings aside first, and drop the Monitor
1523
+ // configuration junction before the directory holding it: a recursive
1524
+ // delete must never reach the canonical `conf` the junction points at.
1525
+ for (const file of plan.preserved) {
1526
+ await fs.mkdir(path.dirname(file.source), { recursive: true });
1527
+ await fs.copyFile(path.join(file.directory, file.name), file.source);
1528
+ }
1529
+ if (plan.configurationLink) {
1530
+ const existing = await fs.lstat(plan.configurationLink).catch(() => undefined);
1531
+ if (existing?.isSymbolicLink())
1532
+ await fs.unlink(plan.configurationLink);
1533
+ else
1534
+ await fs.rm(plan.configurationLink, { recursive: true, force: true });
1535
+ }
1536
+ for (const removal of plan.removals)
1537
+ await fs.rm(removal, { recursive: true, force: true });
1538
+ for (const copy of plan.copies)
1539
+ await (0, builder_1.copyTree)(copy.source, copy.destination, copy.exclude);
1540
+ for (const file of plan.preserved)
1541
+ await fs.copyFile(file.source, path.join(file.directory, file.name));
1542
+ // The replacement is applied after the directory it lives in has been
1543
+ // copied, so a Server refresh cannot overwrite it again. The target is
1544
+ // re-checked here and every error names the module only.
1545
+ if (plan.replacement) {
1546
+ const label = (0, catalog_1.moduleLabel)(catalog_1.catalog[plan.module]);
1547
+ try {
1548
+ for (const file of plan.replacement.files) {
1549
+ const stat = await fs.lstat(file.destination).catch(() => undefined);
1550
+ if (!stat)
1551
+ throw new Error('missing');
1552
+ if (!stat.isFile() || stat.isSymbolicLink())
1553
+ throw new Error('not a regular file');
1554
+ await fs.copyFile(file.source, file.destination);
1555
+ }
1556
+ }
1557
+ catch {
1558
+ throw new Error(`Module ${label} could not be applied: the Server tree holds no regular file to replace.`);
1551
1559
  }
1552
1560
  }
1553
- catch {
1554
- throw new Error(`Module ${label} could not be applied: the Server tree holds no regular file to replace.`);
1561
+ if (plan.configurationAssets) {
1562
+ onProgress?.(`Refreshing Monitor configuration assets in ${path.relative(entry.root, plan.configurationAssets.destination)}`);
1563
+ await mergeConfigurationAssets(plan.configurationAssets.source, plan.configurationAssets.destination);
1555
1564
  }
1565
+ if (plan.configurationLink)
1566
+ await fs.symlink(path.join(entry.root, 'conf'), plan.configurationLink, 'junction');
1556
1567
  }
1557
- if (plan.configurationAssets) {
1558
- onProgress?.(`Refreshing Monitor configuration assets in ${path.relative(entry.root, plan.configurationAssets.destination)}`);
1559
- await mergeConfigurationAssets(plan.configurationAssets.source, plan.configurationAssets.destination);
1560
- }
1561
- if (plan.configurationLink)
1562
- await fs.symlink(path.join(entry.root, 'conf'), plan.configurationLink, 'junction');
1568
+ // Only a plan that really contributes something to the configuration
1569
+ // rewrites it: a module that adds no plugin, worker or service entry
1570
+ // leaves `conf/Wyn.conf` exactly as it is instead of round-tripping it
1571
+ // through the XML builder. (The built-in modules are create-only, so an
1572
+ // update never plans one.)
1573
+ const configurationChanged = plans.some(plan => plan.entries.length > 0 || plan.configurationAssets !== undefined || plan.configurationLink !== undefined);
1574
+ onProgress?.(configurationChanged ? 'Writing configuration, manifest and registry record' : 'Writing manifest and registry record');
1575
+ if (configurationChanged)
1576
+ await writeAtomic(path.join(entry.root, 'conf', 'Wyn.conf'), configuration);
1577
+ await writeJsonAtomic(path.join(entry.root, 'environment.lock.json'), nextManifest);
1578
+ await this.registry.update(entries => {
1579
+ const index = entries.findIndex(candidate => candidate.id === entry.id);
1580
+ if (index < 0)
1581
+ throw new Error('Environment record disappeared during update.');
1582
+ if (entries[index].root !== entry.root)
1583
+ throw new Error('Environment changed during update.');
1584
+ entries[index].modules = modules;
1585
+ if (localModules.length)
1586
+ entries[index].localModules = localModules;
1587
+ else
1588
+ delete entries[index].localModules;
1589
+ });
1563
1590
  }
1564
- // Only a plan that really contributes something to the configuration
1565
- // rewrites it: a module that adds no plugin, worker or service entry
1566
- // leaves `conf/Wyn.conf` exactly as it is instead of round-tripping it
1567
- // through the XML builder. (The built-in modules are create-only, so an
1568
- // update never plans one.)
1569
- const configurationChanged = plans.some(plan => plan.entries.length > 0 || plan.configurationAssets !== undefined || plan.configurationLink !== undefined);
1570
- onProgress?.(configurationChanged ? 'Writing configuration, manifest and registry record' : 'Writing manifest and registry record');
1571
- if (configurationChanged)
1572
- await writeAtomic(path.join(entry.root, 'conf', 'Wyn.conf'), configuration);
1573
- await writeJsonAtomic(path.join(entry.root, 'environment.lock.json'), nextManifest);
1574
- await this.registry.update(entries => {
1575
- const index = entries.findIndex(candidate => candidate.id === entry.id);
1576
- if (index < 0)
1577
- throw new Error('Environment record disappeared during update.');
1578
- if (entries[index].root !== entry.root)
1579
- throw new Error('Environment changed during update.');
1580
- entries[index].modules = modules;
1581
- if (localModules.length)
1582
- entries[index].localModules = localModules;
1583
- else
1584
- delete entries[index].localModules;
1585
- });
1591
+ const restartRequired = !dryRun && changes.length > 0;
1592
+ if (restartRequired)
1593
+ onProgress?.(`${entry.name}: modules updated; restart the environment to load the change (no service was started).`);
1594
+ return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings, localTargets: plans.flatMap(plan => plan.local ?? []) };
1586
1595
  }
1587
- const restartRequired = !dryRun && changes.length > 0;
1588
- if (restartRequired)
1589
- onProgress?.(`${entry.name}: modules updated; restart the environment to load the change (no service was started).`);
1590
- return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings, localTargets: plans.flatMap(plan => plan.local ?? []) };
1591
- }
1592
- finally {
1593
- if (workspace)
1594
- await fs.rm(workspace, { recursive: true, force: true });
1595
- }
1596
+ finally {
1597
+ if (workspace)
1598
+ await fs.rm(workspace, { recursive: true, force: true });
1599
+ }
1600
+ });
1596
1601
  }
1597
1602
  }
1598
1603
  exports.EnvironmentUpdater = EnvironmentUpdater;