@guanjinke/artifact-client 1.3.0 → 1.5.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 (69) hide show
  1. package/README.md +186 -229
  2. package/dist/cli/config-manager.d.ts +15 -0
  3. package/dist/cli/config-manager.d.ts.map +1 -1
  4. package/dist/cli/config-manager.js +31 -0
  5. package/dist/cli/config-manager.js.map +1 -1
  6. package/dist/cli/environment.d.ts +7 -0
  7. package/dist/cli/environment.d.ts.map +1 -0
  8. package/dist/cli/environment.js +200 -0
  9. package/dist/cli/environment.js.map +1 -0
  10. package/dist/cli/setup.d.ts.map +1 -1
  11. package/dist/cli/setup.js +160 -2
  12. package/dist/cli/setup.js.map +1 -1
  13. package/dist/cli.js +4 -1
  14. package/dist/cli.js.map +1 -1
  15. package/dist/environment/archive.d.ts +7 -0
  16. package/dist/environment/archive.d.ts.map +1 -0
  17. package/dist/environment/archive.js +132 -0
  18. package/dist/environment/archive.js.map +1 -0
  19. package/dist/environment/builder.d.ts +6 -0
  20. package/dist/environment/builder.d.ts.map +1 -0
  21. package/dist/environment/builder.js +323 -0
  22. package/dist/environment/builder.js.map +1 -0
  23. package/dist/environment/catalog.d.ts +9 -0
  24. package/dist/environment/catalog.d.ts.map +1 -0
  25. package/dist/environment/catalog.js +35 -0
  26. package/dist/environment/catalog.js.map +1 -0
  27. package/dist/environment/config.d.ts +8 -0
  28. package/dist/environment/config.d.ts.map +1 -0
  29. package/dist/environment/config.js +60 -0
  30. package/dist/environment/config.js.map +1 -0
  31. package/dist/environment/manager.d.ts +43 -0
  32. package/dist/environment/manager.d.ts.map +1 -0
  33. package/dist/environment/manager.js +383 -0
  34. package/dist/environment/manager.js.map +1 -0
  35. package/dist/environment/plan.d.ts +10 -0
  36. package/dist/environment/plan.d.ts.map +1 -0
  37. package/dist/environment/plan.js +131 -0
  38. package/dist/environment/plan.js.map +1 -0
  39. package/dist/environment/process.d.ts +94 -0
  40. package/dist/environment/process.d.ts.map +1 -0
  41. package/dist/environment/process.js +381 -0
  42. package/dist/environment/process.js.map +1 -0
  43. package/dist/environment/registry.d.ts +45 -0
  44. package/dist/environment/registry.d.ts.map +1 -0
  45. package/dist/environment/registry.js +157 -0
  46. package/dist/environment/registry.js.map +1 -0
  47. package/dist/environment/root.d.ts +33 -0
  48. package/dist/environment/root.d.ts.map +1 -0
  49. package/dist/environment/root.js +119 -0
  50. package/dist/environment/root.js.map +1 -0
  51. package/dist/environment/types.d.ts +66 -0
  52. package/dist/environment/types.d.ts.map +1 -0
  53. package/dist/environment/types.js +3 -0
  54. package/dist/environment/types.js.map +1 -0
  55. package/dist/environment/update.d.ts +106 -0
  56. package/dist/environment/update.d.ts.map +1 -0
  57. package/dist/environment/update.js +810 -0
  58. package/dist/environment/update.js.map +1 -0
  59. package/dist/environment/validate.d.ts +36 -0
  60. package/dist/environment/validate.d.ts.map +1 -0
  61. package/dist/environment/validate.js +147 -0
  62. package/dist/environment/validate.js.map +1 -0
  63. package/dist/index.d.ts +13 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +29 -1
  66. package/dist/index.js.map +1 -1
  67. package/dist/types.d.ts +6 -0
  68. package/dist/types.d.ts.map +1 -1
  69. package/package.json +13 -4
@@ -0,0 +1,810 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.EnvironmentUpdater = void 0;
37
+ exports.parseModuleSpecs = parseModuleSpecs;
38
+ exports.patchEnvironmentConfiguration = patchEnvironmentConfiguration;
39
+ const fs = __importStar(require("fs/promises"));
40
+ const path = __importStar(require("path"));
41
+ const fast_xml_parser_1 = require("fast-xml-parser");
42
+ const catalog_1 = require("./catalog");
43
+ const archive_1 = require("./archive");
44
+ const builder_1 = require("./builder");
45
+ const registry_1 = require("./registry");
46
+ const validate_1 = require("./validate");
47
+ const process_1 = require("./process");
48
+ /** Same parser options as the creation path, so a rewrite round-trips identically. */
49
+ const parserOptions = { ignoreAttributes: false, parseTagValue: false, processEntities: false };
50
+ const builderOptions = { ignoreAttributes: false, format: true, suppressEmptyNode: false };
51
+ const windowsPath = (value) => value.replace(/\//g, '\\');
52
+ const updatableModules = ['dashboard', 'reporting', 'sheets', 'analysis', 'common'];
53
+ const installationDirectory = (root, destination) => path.join(root, ...destination.split('/'));
54
+ function validateUpdatableModule(module) {
55
+ if (updatableModules.includes(module))
56
+ return;
57
+ if (module === 'server')
58
+ throw new Error('Module server cannot be updated: it defines the environment runtime and configuration schema. Recreate the environment with create-env instead.');
59
+ if (module === 'monitoring')
60
+ throw new Error('Module monitoring cannot be updated: it defines the configuration schema and monitor runtime. Recreate the environment with create-env instead.');
61
+ if (module === 'documents')
62
+ throw new Error('Module documents cannot be updated: embedded Documents execution is not supported.');
63
+ throw new Error(`Unsupported module "${module}": updateable modules are dashboard, reporting, sheets, analysis and common.`);
64
+ }
65
+ /** `all` is the only allowed pseudo-module: every configured module from remote. */
66
+ const allModules = 'all';
67
+ function parseModuleSpec(value) {
68
+ const equals = value.indexOf('=');
69
+ if (equals >= 0) {
70
+ const token = value.slice(0, equals).trim();
71
+ const hash = token.indexOf('#');
72
+ const module = (hash >= 0 ? token.slice(0, hash) : token).trim().toLowerCase();
73
+ const assembly = hash >= 0 ? token.slice(hash + 1).trim() : undefined;
74
+ if (!module)
75
+ throw new Error(`Invalid module spec "${value}": missing module name.`);
76
+ if (assembly !== undefined && !assembly)
77
+ throw new Error(`Invalid module spec "${value}": missing assembly name after #.`);
78
+ if (assembly !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(assembly))
79
+ throw new Error(`Invalid module spec "${value}": invalid assembly file name.`);
80
+ if (module === allModules)
81
+ throw new Error(`Invalid module spec "${value}": all selects every configured module from remote and cannot point at a local source.`);
82
+ // The module token itself may not carry a pinned version: `=` and `:` select
83
+ // different sources. A Windows drive letter inside the path is not that.
84
+ if (module.includes(':'))
85
+ throw new Error(`Invalid module spec "${value}": a local source (=) cannot also pin a version (:).`);
86
+ const localPath = value.slice(equals + 1).trim();
87
+ if (!localPath)
88
+ throw new Error(`Invalid module spec "${value}": missing local DLL path.`);
89
+ return { module, localPath, ...(assembly !== undefined ? { assembly } : {}) };
90
+ }
91
+ const colon = value.indexOf(':');
92
+ if (colon >= 0) {
93
+ const module = value.slice(0, colon).trim().toLowerCase();
94
+ const version = value.slice(colon + 1).trim();
95
+ if (!module)
96
+ throw new Error(`Invalid module spec "${value}": missing module name.`);
97
+ if (module === allModules)
98
+ throw new Error(`Invalid module spec "${value}": all selects every configured module from remote and cannot pin a version.`);
99
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(version))
100
+ throw new Error(`Invalid module spec "${value}": invalid pinned version.`);
101
+ return { module, version };
102
+ }
103
+ const module = value.trim().toLowerCase();
104
+ if (!module)
105
+ throw new Error(`Invalid module spec "${value}": missing module name.`);
106
+ if (module === allModules)
107
+ return { module: allModules };
108
+ return { module };
109
+ }
110
+ /**
111
+ * Record one parsed spec, enforcing "all alone" and duplicate rules.
112
+ *
113
+ * A module may be named several times only when every one of its specs is a
114
+ * local source: each local spec resolves to one or more targets, and the update
115
+ * refuses a target that is selected twice. A remote spec replaces the whole
116
+ * module, so it may never share a module with another spec.
117
+ */
118
+ function pushSpec(specs, seen, spec) {
119
+ const kind = spec.localPath === undefined ? 'remote' : 'local';
120
+ if (spec.module === allModules) {
121
+ if (seen.has(allModules))
122
+ throw new Error('Duplicate module in one update: all.');
123
+ if (seen.size > 0)
124
+ throw new Error('Module spec all cannot be combined with individual modules.');
125
+ }
126
+ else {
127
+ validateUpdatableModule(spec.module);
128
+ if (seen.has(allModules))
129
+ throw new Error('Module spec all cannot be combined with individual modules.');
130
+ const previous = seen.get(spec.module);
131
+ if (previous !== undefined && (previous === 'remote' || kind === 'remote'))
132
+ throw new Error(`Duplicate module in one update: ${spec.module}.`);
133
+ }
134
+ seen.set(spec.module, kind);
135
+ specs.push(spec);
136
+ }
137
+ /**
138
+ * Parse `--modules` values into module specs.
139
+ *
140
+ * Values that carry a local source (`module=path`) are split on commas only, so
141
+ * a Windows path may contain spaces; every other value accepts space- and
142
+ * comma-separated specs, which is how the CLI passes `--modules a b,c`.
143
+ * `all` selects every configured module from remote and must be used alone.
144
+ */
145
+ function parseModuleSpecs(values) {
146
+ if (!Array.isArray(values))
147
+ throw new Error('Module specs must be an array of strings.');
148
+ const specs = [];
149
+ const seen = new Map();
150
+ for (const value of values) {
151
+ const text = String(value ?? '');
152
+ for (const fragment of text.includes('=') ? text.split(',') : text.split(/[\s,]+/)) {
153
+ const entry = fragment.trim();
154
+ if (!entry)
155
+ continue;
156
+ pushSpec(specs, seen, parseModuleSpec(entry));
157
+ }
158
+ }
159
+ return specs;
160
+ }
161
+ function normalizeModuleSpecs(values) {
162
+ if (!Array.isArray(values))
163
+ throw new Error('Module specs must be an array.');
164
+ const specs = [];
165
+ const seen = new Map();
166
+ const push = (spec) => pushSpec(specs, seen, spec);
167
+ for (const value of values) {
168
+ if (typeof value === 'string') {
169
+ for (const spec of parseModuleSpecs([value]))
170
+ push(spec);
171
+ continue;
172
+ }
173
+ if (!value || typeof value !== 'object')
174
+ throw new Error('Invalid module spec.');
175
+ const module = typeof value.module === 'string' ? value.module.trim().toLowerCase() : '';
176
+ const version = value.version === undefined ? undefined : String(value.version);
177
+ const localPath = value.localPath === undefined ? undefined : String(value.localPath);
178
+ const assembly = value.assembly === undefined ? undefined : String(value.assembly);
179
+ if (!module)
180
+ throw new Error('Invalid module spec: missing module name.');
181
+ if (module === allModules) {
182
+ if (version !== undefined || localPath !== undefined || assembly !== undefined)
183
+ throw new Error('Module spec all cannot pin a version or point at a local source.');
184
+ push({ module: allModules });
185
+ continue;
186
+ }
187
+ if (version !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(version))
188
+ throw new Error(`Invalid version for module ${module}.`);
189
+ if (version !== undefined && localPath !== undefined)
190
+ throw new Error(`Module ${module} cannot combine a local path (=) with a pinned version (:).`);
191
+ if (assembly !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(assembly))
192
+ throw new Error(`Invalid assembly file name for module ${module}.`);
193
+ if (assembly !== undefined && localPath === undefined)
194
+ throw new Error(`Module ${module} names an assembly (#) without a local path (=).`);
195
+ if (version === undefined && localPath === undefined) {
196
+ push({ module });
197
+ continue;
198
+ }
199
+ push({ module, ...(version !== undefined ? { version } : {}), ...(localPath !== undefined ? { localPath } : {}), ...(assembly !== undefined ? { assembly } : {}) });
200
+ }
201
+ return specs;
202
+ }
203
+ /** Read one `<sys:string>` list, accepting a single string instead of an array. */
204
+ function stringList(raw) {
205
+ const list = raw === undefined ? [] : Array.isArray(raw) ? [...raw] : [raw];
206
+ if (list.some(value => typeof value !== 'string'))
207
+ throw new Error('Invalid environment configuration.');
208
+ return list;
209
+ }
210
+ /** Replace or append one `<sys:string>` path list, matching by destination or assembly name. */
211
+ function rewritePathList(values, entries) {
212
+ const strings = values.map(value => value.trim());
213
+ const used = new Set();
214
+ const appended = [];
215
+ for (const entry of entries) {
216
+ const prefix = windowsPath(entry.destination).toLowerCase() + '\\';
217
+ const assemblies = new Set(entry.assemblies.map(name => name.toLowerCase()));
218
+ const index = strings.findIndex((value, candidate) => {
219
+ if (used.has(candidate))
220
+ return false;
221
+ const folded = windowsPath(value).toLowerCase();
222
+ return folded.startsWith(prefix) || assemblies.has(path.win32.basename(folded));
223
+ });
224
+ if (index < 0)
225
+ appended.push(windowsPath(entry.path));
226
+ else {
227
+ used.add(index);
228
+ strings[index] = windowsPath(entry.path);
229
+ }
230
+ }
231
+ return [...strings, ...appended];
232
+ }
233
+ /**
234
+ * Rewrite the `<Server><Plugins>`, `<Server><PluginWorkers>` and worker service
235
+ * entries of a Wyn configuration document.
236
+ *
237
+ * The document is parsed with the creation path's parser options, only the
238
+ * `sys:string` path lists and the matching worker service nodes are mutated, and
239
+ * the result is rebuilt with the same builder options and re-validated before it
240
+ * is returned. Dashboard stays the first `Plugins` entry; every other entry keeps
241
+ * its order. A worker service keeps all of its other fields, including
242
+ * `Disabled`; a configuration without that node is updated through
243
+ * `<PluginWorkers>` alone.
244
+ */
245
+ function patchEnvironmentConfiguration(xml, entries) {
246
+ if (typeof xml !== 'string' || /<!DOCTYPE|<!ENTITY/i.test(xml) || fast_xml_parser_1.XMLValidator.validate(xml) !== true)
247
+ throw new Error('Invalid environment configuration.');
248
+ const document = new fast_xml_parser_1.XMLParser(parserOptions).parse(xml);
249
+ const server = document?.SystemConfig?.Services?.Server;
250
+ if (!server || typeof server !== 'object' || Array.isArray(server))
251
+ throw new Error('Environment configuration has no Server service.');
252
+ const pluginEntries = entries.filter(entry => entry.role !== 'worker');
253
+ if (server.Plugins !== undefined || pluginEntries.length) {
254
+ const plugins = server.Plugins === undefined || server.Plugins === '' ? {} : server.Plugins;
255
+ if (typeof plugins !== 'object' || Array.isArray(plugins))
256
+ throw new Error('Invalid environment configuration.');
257
+ const combined = rewritePathList(stringList(plugins['sys:string']), pluginEntries);
258
+ // Keep Dashboard first even when it is a local build path rather than a deployed one.
259
+ const dashboard = combined.findIndex(value => {
260
+ const folded = windowsPath(value).toLowerCase();
261
+ return folded.startsWith('plugins\\dashboard\\') || path.win32.basename(folded) === 'wyn.dashboards.plugin.dll';
262
+ });
263
+ if (dashboard > 0)
264
+ combined.unshift(combined.splice(dashboard, 1)[0]);
265
+ plugins['sys:string'] = combined;
266
+ server.Plugins = plugins;
267
+ }
268
+ const workers = entries.filter(entry => entry.role === 'worker');
269
+ if (workers.length) {
270
+ const pluginWorkers = server.PluginWorkers === undefined || server.PluginWorkers === '' ? {} : server.PluginWorkers;
271
+ if (typeof pluginWorkers !== 'object' || Array.isArray(pluginWorkers))
272
+ throw new Error('Invalid environment configuration.');
273
+ pluginWorkers['sys:string'] = rewritePathList(stringList(pluginWorkers['sys:string']), workers);
274
+ server.PluginWorkers = pluginWorkers;
275
+ const services = document?.SystemConfig?.Services;
276
+ for (const entry of workers) {
277
+ if (!entry.service || !services || typeof services !== 'object' || Array.isArray(services))
278
+ continue;
279
+ const service = services[entry.service];
280
+ if (!service || typeof service !== 'object' || Array.isArray(service))
281
+ continue;
282
+ const node = service;
283
+ // The worker is loaded from the path list above and uses its own directory
284
+ // as its base directory: keep the service in step without touching Disabled.
285
+ node.WorkingDirectory = windowsPath(path.win32.dirname(entry.path));
286
+ node.Assembly = path.win32.basename(entry.path);
287
+ }
288
+ }
289
+ const output = new fast_xml_parser_1.XMLBuilder(builderOptions).build(document);
290
+ if (fast_xml_parser_1.XMLValidator.validate(output) !== true)
291
+ throw new Error('Invalid environment configuration.');
292
+ return output;
293
+ }
294
+ async function exists(target) {
295
+ try {
296
+ await fs.lstat(target);
297
+ return true;
298
+ }
299
+ catch (error) {
300
+ if (error.code === 'ENOENT')
301
+ return false;
302
+ throw error;
303
+ }
304
+ }
305
+ async function readManifest(root) {
306
+ const file = path.join(root, 'environment.lock.json');
307
+ const stat = await fs.lstat(file);
308
+ if (!stat.isFile() || stat.isSymbolicLink())
309
+ throw new Error('Environment manifest is not a regular file.');
310
+ let document;
311
+ try {
312
+ document = JSON.parse(await fs.readFile(file, 'utf8'));
313
+ }
314
+ catch {
315
+ throw new Error('Environment manifest is unreadable.');
316
+ }
317
+ if (!document || typeof document !== 'object' || Array.isArray(document))
318
+ throw new Error('Environment manifest is unreadable.');
319
+ const manifest = document;
320
+ for (const field of ['artifacts', 'downloads']) {
321
+ const value = manifest[field];
322
+ if (value !== undefined && (!Array.isArray(value) || value.some(item => !item || typeof item !== 'object')))
323
+ throw new Error('Environment manifest is unreadable.');
324
+ }
325
+ if (manifest.selectedModules !== undefined && (!Array.isArray(manifest.selectedModules) || manifest.selectedModules.some(module => typeof module !== 'string')))
326
+ throw new Error('Environment manifest is unreadable.');
327
+ if (manifest.moduleSources !== undefined && (!manifest.moduleSources || typeof manifest.moduleSources !== 'object' || Array.isArray(manifest.moduleSources)))
328
+ throw new Error('Environment manifest is unreadable.');
329
+ for (const source of Object.values(manifest.moduleSources ?? {})) {
330
+ if (!source || typeof source !== 'object' || Array.isArray(source) || source.type !== 'local' || typeof source.path !== 'string' || !Array.isArray(source.pluginDirectories) || source.pluginDirectories.some(directory => typeof directory !== 'string'))
331
+ throw new Error('Environment manifest is unreadable.');
332
+ if (source.targets !== undefined && (!Array.isArray(source.targets) || source.targets.some(target => !target || typeof target !== 'object' || target.kind !== 'plugin' && target.kind !== 'worker' || typeof target.destination !== 'string' || typeof target.assembly !== 'string' || typeof target.path !== 'string')))
333
+ throw new Error('Environment manifest is unreadable.');
334
+ }
335
+ return manifest;
336
+ }
337
+ /**
338
+ * Normalize a recorded module source into targets.
339
+ *
340
+ * The 1.4.0 shape (`path` plus `pluginDirectories`) is still written whenever it
341
+ * describes the state exactly; it always meant one plugin target per recorded
342
+ * directory.
343
+ */
344
+ function localTargetsOf(module, source) {
345
+ if (!source)
346
+ return [];
347
+ if (Array.isArray(source.targets) && source.targets.length)
348
+ return source.targets.map(target => ({ module, kind: target.kind, destination: target.destination, assembly: target.assembly, path: target.path }));
349
+ return (source.pluginDirectories ?? []).map(destination => ({ module, kind: 'plugin', destination, assembly: path.win32.basename(source.path), path: source.path }));
350
+ }
351
+ /** Record the local targets of one module, keeping earlier targets of that module. */
352
+ function manifestSource(module, targets, previous) {
353
+ const overridden = new Set(targets.map(target => target.destination.toLowerCase()));
354
+ const merged = [...localTargetsOf(module, previous).filter(target => !overridden.has(target.destination.toLowerCase())), ...targets];
355
+ const source = { type: 'local', path: merged[0].path, pluginDirectories: merged.filter(target => target.kind === 'plugin').map(target => target.destination) };
356
+ if (merged.length > 1 || merged[0].kind !== 'plugin')
357
+ source.targets = merged.map(({ kind, destination, assembly, path: file }) => ({ kind, destination, assembly, path: file }));
358
+ return source;
359
+ }
360
+ /** Write JSON (or any text) through `<file>.afm-tmp` and an atomic rename. */
361
+ async function writeAtomic(file, content) {
362
+ const temporary = `${file}.afm-tmp`;
363
+ await fs.writeFile(temporary, content, 'utf8');
364
+ await fs.rename(temporary, file);
365
+ }
366
+ const writeJsonAtomic = (file, value) => writeAtomic(file, JSON.stringify(value, null, 2) + '\n');
367
+ /**
368
+ * Resolve one remote spec against the record's major and the module's exact
369
+ * artifact name; version `undefined` means "latest for the record's major".
370
+ */
371
+ async function resolveRemoteArtifact(client, entry, spec) {
372
+ const module = spec.module;
373
+ const source = catalog_1.catalog[module];
374
+ let version;
375
+ let files;
376
+ try {
377
+ version = spec.version ?? (await client.getLatestVersion(source.plugin, entry.major)).version;
378
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(version))
379
+ throw new Error('Invalid resolved version');
380
+ files = await client.listArtifacts(source.plugin, version);
381
+ }
382
+ catch {
383
+ throw new Error(`Unable to resolve artifacts for ${source.plugin}/${spec.version ?? entry.major}.`);
384
+ }
385
+ const name = source.filename(version, entry.edition);
386
+ const candidates = files.filter(file => file.name === name);
387
+ if (candidates.length !== 1)
388
+ throw new Error(`${source.plugin}/${version}: expected exactly one ${name}, found ${candidates.length}.`);
389
+ const remotePath = candidates[0].path;
390
+ if (!remotePath || /[?:\\\x00-\x1f]/.test(remotePath) || remotePath.startsWith('/') || remotePath.split('/').includes('..'))
391
+ throw new Error(`${source.plugin}/${version}: invalid artifact path.`);
392
+ return { module, plugin: source.plugin, version, name, path: remotePath, mappings: source.mappings.map(mapping => ({ ...mapping, assemblies: [...mapping.assemblies] })) };
393
+ }
394
+ /** Download, hash, extract and verify one resolved artifact inside the staging workspace. */
395
+ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
396
+ const label = `[${artifact.module}] ${artifact.plugin}/${artifact.version}/${artifact.name}`;
397
+ const download = path.join(workspace, 'download', artifact.module);
398
+ const expanded = path.join(workspace, `expanded-${artifact.module}`);
399
+ try {
400
+ await fs.mkdir(download, { recursive: true });
401
+ onProgress?.(`${label}: Downloading`);
402
+ await client.downloadArtifact(artifact.plugin, artifact.version, artifact.name, download);
403
+ const archive = path.join(download, artifact.name);
404
+ await (0, builder_1.regularFile)(archive);
405
+ onProgress?.(`${label}: Download complete; calculating SHA256`);
406
+ const sha256 = await (0, builder_1.hashFile)(archive);
407
+ onProgress?.(`${label}: Extracting and validating ZIP`);
408
+ await (0, archive_1.extractArchive)(archive, expanded);
409
+ const assemblies = new Map();
410
+ for (const mapping of artifact.mappings) {
411
+ const source = path.join(expanded, mapping.source);
412
+ if (!(await fs.lstat(source)).isDirectory())
413
+ throw new Error('Required source directory missing.');
414
+ const present = [];
415
+ for (const name of mapping.assemblies) {
416
+ try {
417
+ await (0, builder_1.regularFile)(path.join(source, name));
418
+ present.push(name);
419
+ }
420
+ catch (error) {
421
+ if (error.code !== 'ENOENT')
422
+ throw error;
423
+ }
424
+ }
425
+ if (present.length !== 1)
426
+ throw new Error(`Expected one runtime assembly in ${mapping.destination || 'root'}.`);
427
+ assemblies.set(mapping.destination, present[0]);
428
+ }
429
+ onProgress?.(`${label}: Archive and layout checks complete`);
430
+ return { ...artifact, sha256, expanded, assemblies };
431
+ }
432
+ catch (error) {
433
+ throw new Error(`Module update failed for ${artifact.plugin}/${artifact.version}/${artifact.name} (download, archive or layout): ${error.message}`);
434
+ }
435
+ }
436
+ /** Mappings of one module that a local source may override. */
437
+ const localMappings = (module) => catalog_1.catalog[module].mappings.filter(mapping => mapping.role === 'plugin' || mapping.role === 'worker');
438
+ const expectedAssemblies = (module) => [...new Set(localMappings(module).flatMap(mapping => mapping.assemblies))];
439
+ const lstatOrUndefined = (target) => fs.lstat(target).catch((error) => { if (error.code === 'ENOENT')
440
+ return undefined; throw error; });
441
+ /** Locate the single mapping of `module` whose known assemblies include `assembly`. */
442
+ function mappingForAssembly(module, assembly) {
443
+ const wanted = assembly.toLowerCase();
444
+ const matches = localMappings(module).filter(mapping => mapping.assemblies.some(name => name.toLowerCase() === wanted));
445
+ if (matches.length !== 1)
446
+ throw new Error(`Local target ${assembly} is not one assembly of module ${module}. Expected one of ${expectedAssemblies(module).join(', ')}.`);
447
+ return { mapping: matches[0], assembly: matches[0].assemblies.find(name => name.toLowerCase() === wanted) };
448
+ }
449
+ /**
450
+ * Resolve one local source into the module targets it overrides.
451
+ *
452
+ * A DLL selects the mapping whose assembly file name it matches. A directory
453
+ * selects every mapping that holds exactly one of its assemblies, so a partial
454
+ * developer build only overrides what it really contains; the other mappings are
455
+ * reported as inactive and stay deployed. `module#<assembly>=` names the target
456
+ * explicitly, for a build whose file name matches no assembly at all.
457
+ */
458
+ async function resolveLocalSource(entry, module, assembly, input) {
459
+ const mappings = localMappings(module);
460
+ const source = path.resolve(input);
461
+ const stat = await lstatOrUndefined(source);
462
+ if (!stat)
463
+ throw new Error(`Local source not found: ${source}.`);
464
+ if (stat.isSymbolicLink())
465
+ throw new Error(`Local source must be a regular file or directory: ${source}.`);
466
+ const selected = [];
467
+ const inactive = [];
468
+ if (stat.isFile()) {
469
+ if (path.extname(source).toLowerCase() !== '.dll')
470
+ throw new Error(`Local source must be a .dll file: ${source}.`);
471
+ if (assembly === undefined) {
472
+ const name = path.win32.basename(source).toLowerCase();
473
+ const matches = mappings.filter(mapping => mapping.assemblies.some(known => known.toLowerCase() === name));
474
+ if (matches.length !== 1)
475
+ throw new Error(`Local source ${source} matches ${matches.length} targets of module ${module}. Expected one of ${expectedAssemblies(module).join(', ')}; select one explicitly with ${module}#<assembly>=<path>.`);
476
+ selected.push({ mapping: matches[0], assembly: matches[0].assemblies.find(known => known.toLowerCase() === name), path: source });
477
+ inactive.push(...mappings.filter(mapping => mapping !== matches[0]).map(mapping => mapping.destination));
478
+ }
479
+ else {
480
+ const named = mappingForAssembly(module, assembly);
481
+ // The explicit form exists for a build whose file name matches no assembly;
482
+ // a file that is another target's assembly is still a mistake.
483
+ const basename = path.win32.basename(source);
484
+ const conflicts = mappings.filter(mapping => mapping !== named.mapping && mapping.assemblies.some(known => known.toLowerCase() === basename.toLowerCase()));
485
+ if (conflicts.length)
486
+ throw new Error(`Local source ${source} is an assembly of ${conflicts.map(mapping => mapping.destination).join(', ')}, not of ${named.mapping.destination}: ${module}#${assembly}= selects ${named.mapping.destination}.`);
487
+ selected.push({ mapping: named.mapping, assembly: named.assembly, path: source });
488
+ inactive.push(...mappings.filter(mapping => mapping !== named.mapping).map(mapping => mapping.destination));
489
+ }
490
+ }
491
+ else if (stat.isDirectory()) {
492
+ const named = assembly === undefined ? undefined : mappingForAssembly(module, assembly);
493
+ for (const mapping of named ? [named.mapping] : mappings) {
494
+ const present = [];
495
+ for (const name of named ? [named.assembly] : mapping.assemblies) {
496
+ const candidate = path.join(source, name);
497
+ const candidateStat = await lstatOrUndefined(candidate);
498
+ if (candidateStat?.isFile() && !candidateStat.isSymbolicLink())
499
+ present.push({ assembly: name, path: candidate });
500
+ }
501
+ if (present.length === 1)
502
+ selected.push({ mapping, ...present[0] });
503
+ else
504
+ inactive.push(mapping.destination);
505
+ }
506
+ // An explicit selection leaves every other mapping of the module deployed.
507
+ if (named)
508
+ inactive.push(...mappings.filter(mapping => mapping !== named.mapping).map(mapping => mapping.destination));
509
+ if (!selected.length) {
510
+ if (named)
511
+ throw new Error(`Local source directory ${source} does not contain ${named.assembly}.`);
512
+ throw new Error(`Local source directory ${source} must contain exactly one of ${expectedAssemblies(module).join(', ')} for module ${module}; select one explicitly with ${module}#<assembly>=<path>.`);
513
+ }
514
+ }
515
+ else
516
+ throw new Error(`Local source must be a regular .dll file or a directory: ${source}.`);
517
+ const targets = selected.map(item => {
518
+ const deployed = installationDirectory(entry.root, item.mapping.destination);
519
+ const relative = path.relative(deployed.toLowerCase(), item.path.toLowerCase());
520
+ if (relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative))
521
+ throw new Error(`Local source ${item.path} is inside the deployed ${item.mapping.role === 'worker' ? 'worker' : 'plugin'} directory ${deployed}; point at the developer build output outside the environment.`);
522
+ const target = { module, kind: item.mapping.role, destination: item.mapping.destination, assembly: item.assembly, path: item.path };
523
+ if (item.mapping.service)
524
+ target.service = item.mapping.service;
525
+ return target;
526
+ });
527
+ return { targets, inactive, source };
528
+ }
529
+ /**
530
+ * Update selected modules of a registered environment.
531
+ *
532
+ * Remote specs replace the deployed module directories with a freshly verified
533
+ * artifact; local specs copy nothing, delete the deployed plugin directory and
534
+ * point the Server configuration at the absolute local DLL so the developer
535
+ * build is loaded directly. Files are staged in a sibling temporary workspace
536
+ * that is always removed, and JSON files are written through `<file>.afm-tmp`.
537
+ */
538
+ class EnvironmentUpdater {
539
+ dependencies;
540
+ registry;
541
+ processes;
542
+ platform;
543
+ onProgress;
544
+ constructor(dependencies = {}) {
545
+ this.dependencies = dependencies;
546
+ this.registry = dependencies.registry ?? new registry_1.EnvironmentRegistry();
547
+ this.processes = dependencies.processes ?? new process_1.WindowsProcessBackend();
548
+ this.platform = dependencies.platform ?? process.platform;
549
+ this.onProgress = dependencies.onProgress;
550
+ }
551
+ async update(name, specs, clientFactory, options = {}) {
552
+ const dryRun = options.dryRun === true;
553
+ const onProgress = options.onProgress ?? this.onProgress;
554
+ if (!dryRun && this.platform !== 'win32')
555
+ throw new Error('Environment execution is Windows-only. Use dry-run on other platforms.');
556
+ onProgress?.('Validating module specs');
557
+ let parsed = normalizeModuleSpecs(specs);
558
+ if (!parsed.length)
559
+ throw new Error('At least one module spec is required: use --modules all, dashboard, reporting or dashboard=D:\\path\\Wyn.Dashboards.Plugin.dll.');
560
+ const entry = await this.registry.get(name);
561
+ onProgress?.(`Validating environment ${entry.name} at ${entry.root}`);
562
+ await (0, validate_1.validateEnvironmentRoot)(entry, this.registry.directory);
563
+ await (0, validate_1.validateEnvironmentOwnership)(entry, this.registry.directory);
564
+ await (0, validate_1.validateEnvironmentTree)(entry.root);
565
+ const manifest = await readManifest(entry.root);
566
+ const artifacts = [...(manifest.artifacts ?? [])];
567
+ const skipped = [];
568
+ if (parsed.some(spec => spec.module === 'all')) {
569
+ // `all` means every configured module from remote; modules that already
570
+ // point at a local developer build are left exactly as they are.
571
+ const localModules = new Set([...Object.keys(manifest.moduleSources ?? {}), ...(entry.localModules ?? [])]);
572
+ const configured = [...new Set(artifacts.map(artifact => artifact.module))].filter(module => updatableModules.includes(module)).sort();
573
+ for (const module of configured)
574
+ if (localModules.has(module))
575
+ skipped.push({ module, reason: 'local' });
576
+ const selected = configured.filter(module => !localModules.has(module));
577
+ if (!selected.length)
578
+ throw new Error(`Module spec all matched no updatable modules: ${configured.length ? configured.map(module => `${module} has a local source`).join('; ') : 'no configured module is updateable (server and monitoring are never updated)'}.`);
579
+ parsed = selected.map(module => ({ module }));
580
+ onProgress?.(`Updating all configured modules from remote: ${selected.join(', ')}${skipped.length ? ` (skipping local: ${skipped.map(item => item.module).join(', ')})` : ''}`);
581
+ }
582
+ const localOverrides = new Map();
583
+ const selectedTargets = new Set();
584
+ for (const spec of parsed) {
585
+ if (spec.module === 'sheets' && entry.edition !== 'en')
586
+ throw new Error(`Module sheets requires an EN edition environment; ${entry.name} is ${entry.edition}.`);
587
+ if (spec.localPath === undefined)
588
+ continue;
589
+ const resolution = await resolveLocalSource(entry, spec.module, spec.assembly, spec.localPath);
590
+ for (const destination of resolution.inactive)
591
+ onProgress?.(`${spec.module}: ${destination} stays deployed (not selected by ${resolution.source})`);
592
+ const targets = localOverrides.get(spec.module) ?? [];
593
+ for (const target of resolution.targets) {
594
+ if (selectedTargets.has(target.destination.toLowerCase()))
595
+ throw new Error(`Local target ${target.assembly} of module ${target.module} is selected twice in one update; give one spec per assembly.`);
596
+ selectedTargets.add(target.destination.toLowerCase());
597
+ targets.push(target);
598
+ }
599
+ localOverrides.set(spec.module, targets);
600
+ }
601
+ const remote = parsed.filter(spec => spec.localPath === undefined);
602
+ const state = await (0, validate_1.observeRuntimeState)(entry, this.processes);
603
+ if (remote.length && state !== 'ready' && state !== 'stopped')
604
+ 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.`);
605
+ if (!remote.length && state !== 'ready' && state !== 'stopped' && state !== 'running')
606
+ 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.`);
607
+ const downloads = [...(manifest.downloads ?? [])];
608
+ const selectedModules = manifest.selectedModules === undefined ? undefined : [...manifest.selectedModules];
609
+ const moduleSources = { ...(manifest.moduleSources ?? {}) };
610
+ const installed = new Set(artifacts.map(artifact => artifact.module));
611
+ const workspace = remote.length && !dryRun ? await fs.mkdtemp(path.join(path.dirname(entry.root), '.afm-env-')) : undefined;
612
+ const warnings = [];
613
+ try {
614
+ const resolved = new Map();
615
+ const staged = new Map();
616
+ if (remote.length) {
617
+ const client = clientFactory();
618
+ for (const spec of remote) {
619
+ onProgress?.(`Resolving ${catalog_1.catalog[spec.module].plugin}`);
620
+ resolved.set(spec.module, await resolveRemoteArtifact(client, entry, spec));
621
+ }
622
+ if (!dryRun)
623
+ for (const spec of remote)
624
+ staged.set(spec.module, await stageRemoteArtifact(client, resolved.get(spec.module), workspace, onProgress));
625
+ }
626
+ const configured = await fs.readFile(path.join(entry.root, 'conf', 'Wyn.conf'), 'utf8');
627
+ const plans = [];
628
+ // A module may carry several local specs: plan it once, for all its targets.
629
+ for (const module of [...new Set(parsed.map(spec => spec.module))]) {
630
+ const artifact = artifacts.find(candidate => candidate.module === module);
631
+ const previous = moduleSources[module] ? { local: moduleSources[module].path } : { version: artifact?.version };
632
+ const override = localOverrides.get(module);
633
+ if (override) {
634
+ const removals = [];
635
+ const removedDirectories = [];
636
+ for (const target of override) {
637
+ const destination = installationDirectory(entry.root, target.destination);
638
+ if (!await exists(destination))
639
+ continue;
640
+ removals.push(destination);
641
+ removedDirectories.push(target.destination);
642
+ }
643
+ const entries = override.map(target => {
644
+ const entry = { destination: target.destination, assemblies: [target.assembly], path: target.path };
645
+ if (target.kind === 'worker') {
646
+ entry.role = 'worker';
647
+ if (target.service)
648
+ entry.service = target.service;
649
+ }
650
+ return entry;
651
+ });
652
+ plans.push({
653
+ module, previous, next: { local: override.map(target => target.path).join(', ') }, local: override,
654
+ entries, copies: [], removals, copiedDirectories: [], removedDirectories,
655
+ });
656
+ continue;
657
+ }
658
+ const remoteArtifact = resolved.get(module);
659
+ const stagedArtifact = staged.get(module);
660
+ const copies = [];
661
+ const removals = [];
662
+ const copiedDirectories = [];
663
+ const removedDirectories = [];
664
+ const entries = [];
665
+ for (const mapping of remoteArtifact.mappings) {
666
+ const destination = installationDirectory(entry.root, mapping.destination);
667
+ if (await exists(destination)) {
668
+ removals.push(destination);
669
+ removedDirectories.push(mapping.destination);
670
+ }
671
+ if (stagedArtifact)
672
+ copies.push({ source: path.join(stagedArtifact.expanded, mapping.source), destination });
673
+ copiedDirectories.push(mapping.destination);
674
+ const assembly = stagedArtifact?.assemblies.get(mapping.destination) ?? knownAssembly(configured, mapping);
675
+ // Plugins stay relative to the environment root; a worker entry does too,
676
+ // and its service keeps using the deployed destination as working directory.
677
+ if (mapping.role === 'plugin' || mapping.role === 'worker') {
678
+ const entry = { destination: mapping.destination, assemblies: mapping.assemblies, path: path.posix.join(mapping.destination, assembly) };
679
+ if (mapping.role === 'worker') {
680
+ entry.role = 'worker';
681
+ if (mapping.service)
682
+ entry.service = mapping.service;
683
+ }
684
+ entries.push(entry);
685
+ }
686
+ }
687
+ if (module === 'dashboard' && stagedArtifact) {
688
+ const contents = path.join(stagedArtifact.expanded, 'Web Contents');
689
+ if ((await fs.lstat(contents).catch(() => undefined))?.isDirectory()) {
690
+ const destination = path.join(entry.root, 'Web Contents');
691
+ if (await exists(destination)) {
692
+ removals.push(destination);
693
+ removedDirectories.push('Web Contents');
694
+ }
695
+ copies.push({ source: contents, destination });
696
+ copiedDirectories.push('Web Contents');
697
+ }
698
+ }
699
+ plans.push({
700
+ module, previous, next: { version: remoteArtifact.version }, artifact: remoteArtifact, staged: stagedArtifact,
701
+ entries, copies, removals, copiedDirectories, removedDirectories,
702
+ });
703
+ }
704
+ const changes = plans.map(plan => ({ module: plan.module, previous: plan.previous, next: plan.next, copiedDirectories: plan.copiedDirectories, removedDirectories: plan.removedDirectories }));
705
+ const configuration = patchEnvironmentConfiguration(configured, plans.flatMap(plan => plan.entries));
706
+ const nextArtifacts = [...artifacts];
707
+ const nextDownloads = [...downloads];
708
+ for (const plan of plans) {
709
+ if (plan.artifact) {
710
+ const replacement = {
711
+ module: plan.module, plugin: plan.artifact.plugin, version: plan.artifact.version, name: plan.artifact.name, path: plan.artifact.path,
712
+ mappings: catalog_1.catalog[plan.module].mappings.map(mapping => ({ ...mapping, assemblies: [...mapping.assemblies] })),
713
+ };
714
+ const index = nextArtifacts.findIndex(candidate => candidate.module === plan.module);
715
+ if (index < 0)
716
+ nextArtifacts.push(replacement);
717
+ else
718
+ nextArtifacts[index] = { ...nextArtifacts[index], ...replacement };
719
+ if (plan.staged) {
720
+ const download = { plugin: plan.artifact.plugin, version: plan.artifact.version, name: plan.artifact.name, sha256: plan.staged.sha256 };
721
+ const downloadIndex = nextDownloads.findIndex(candidate => candidate.plugin === plan.artifact.plugin);
722
+ if (downloadIndex < 0)
723
+ nextDownloads.push(download);
724
+ else
725
+ nextDownloads[downloadIndex] = download;
726
+ }
727
+ delete moduleSources[plan.module];
728
+ }
729
+ if (plan.local)
730
+ moduleSources[plan.module] = manifestSource(plan.module, plan.local, moduleSources[plan.module]);
731
+ if (!installed.has(plan.module) && plan.module !== 'common' && selectedModules)
732
+ selectedModules.push(plan.module);
733
+ }
734
+ if (selectedModules) {
735
+ const unique = [...new Set(selectedModules)].sort();
736
+ selectedModules.length = 0;
737
+ selectedModules.push(...unique);
738
+ }
739
+ const nextManifest = { ...manifest, artifacts: nextArtifacts, downloads: nextDownloads };
740
+ if (Object.keys(moduleSources).length)
741
+ nextManifest.moduleSources = moduleSources;
742
+ else
743
+ delete nextManifest.moduleSources;
744
+ if (selectedModules)
745
+ nextManifest.selectedModules = selectedModules;
746
+ const modules = [...new Set([...entry.modules, ...plans.filter(plan => !installed.has(plan.module)).map(plan => plan.module)])].sort();
747
+ const localModules = Object.keys(moduleSources).sort();
748
+ if (state === 'running')
749
+ warnings.push(`${entry.name} is running; restart it to load the updated modules.`);
750
+ for (const item of skipped)
751
+ warnings.push(`${item.module} has a local source and was not updated from artifacts.`);
752
+ if (!dryRun) {
753
+ for (const plan of plans) {
754
+ for (const removal of plan.removals)
755
+ await fs.rm(removal, { recursive: true, force: true });
756
+ for (const copy of plan.copies)
757
+ await (0, builder_1.copyTree)(copy.source, copy.destination);
758
+ }
759
+ onProgress?.('Writing configuration, manifest and registry record');
760
+ await writeAtomic(path.join(entry.root, 'conf', 'Wyn.conf'), configuration);
761
+ await writeJsonAtomic(path.join(entry.root, 'environment.lock.json'), nextManifest);
762
+ await this.registry.update(entries => {
763
+ const index = entries.findIndex(candidate => candidate.id === entry.id);
764
+ if (index < 0)
765
+ throw new Error('Environment record disappeared during update.');
766
+ if (entries[index].root !== entry.root)
767
+ throw new Error('Environment changed during update.');
768
+ entries[index].modules = modules;
769
+ if (localModules.length)
770
+ entries[index].localModules = localModules;
771
+ else
772
+ delete entries[index].localModules;
773
+ });
774
+ }
775
+ const restartRequired = !dryRun && changes.length > 0;
776
+ if (restartRequired)
777
+ onProgress?.(`${entry.name}: modules updated; restart the environment to load the change (no service was started).`);
778
+ return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings, localTargets: plans.flatMap(plan => plan.local ?? []) };
779
+ }
780
+ finally {
781
+ if (workspace)
782
+ await fs.rm(workspace, { recursive: true, force: true });
783
+ }
784
+ }
785
+ }
786
+ exports.EnvironmentUpdater = EnvironmentUpdater;
787
+ /** Best-effort preview assembly for dry-run, where nothing can be extracted. */
788
+ function knownAssembly(xml, mapping) {
789
+ try {
790
+ const document = new fast_xml_parser_1.XMLParser(parserOptions).parse(xml);
791
+ const server = document?.SystemConfig?.Services?.Server;
792
+ const raw = mapping.role === 'worker' ? server?.PluginWorkers?.['sys:string'] : server?.Plugins?.['sys:string'];
793
+ const list = raw === undefined ? [] : Array.isArray(raw) ? raw : [raw];
794
+ const prefix = windowsPath(mapping.destination).toLowerCase() + '\\';
795
+ for (const value of list) {
796
+ if (typeof value !== 'string')
797
+ continue;
798
+ const folded = windowsPath(value).toLowerCase();
799
+ if (!folded.startsWith(prefix))
800
+ continue;
801
+ const name = path.win32.basename(folded);
802
+ const match = mapping.assemblies.find(assembly => assembly.toLowerCase() === name);
803
+ if (match)
804
+ return match;
805
+ }
806
+ }
807
+ catch { /* Fall through to the first known assembly name. */ }
808
+ return mapping.assemblies[0];
809
+ }
810
+ //# sourceMappingURL=update.js.map