@guanjinke/artifact-client 1.2.0 → 1.4.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 (84) hide show
  1. package/README.md +146 -1
  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 +6 -0
  7. package/dist/cli/environment.d.ts.map +1 -0
  8. package/dist/cli/environment.js +134 -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 +200 -18
  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 +85 -0
  56. package/dist/environment/update.d.ts.map +1 -0
  57. package/dist/environment/update.js +644 -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/providers/azure-provider.d.ts +1 -1
  68. package/dist/providers/azure-provider.d.ts.map +1 -1
  69. package/dist/providers/azure-provider.js +7 -6
  70. package/dist/providers/azure-provider.js.map +1 -1
  71. package/dist/providers/oss-provider.d.ts +1 -1
  72. package/dist/providers/oss-provider.d.ts.map +1 -1
  73. package/dist/providers/oss-provider.js +7 -6
  74. package/dist/providers/oss-provider.js.map +1 -1
  75. package/dist/providers/storage-provider.d.ts +5 -1
  76. package/dist/providers/storage-provider.d.ts.map +1 -1
  77. package/dist/providers/storage-provider.js.map +1 -1
  78. package/dist/types.d.ts +6 -0
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/utils/local-path.d.ts +10 -0
  81. package/dist/utils/local-path.d.ts.map +1 -0
  82. package/dist/utils/local-path.js +51 -0
  83. package/dist/utils/local-path.js.map +1 -0
  84. package/package.json +9 -3
@@ -0,0 +1,644 @@
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 module = value.slice(0, equals).trim().toLowerCase();
71
+ const localPath = value.slice(equals + 1).trim();
72
+ if (!module)
73
+ throw new Error(`Invalid module spec "${value}": missing module name.`);
74
+ if (module === allModules)
75
+ throw new Error(`Invalid module spec "${value}": all selects every configured module from remote and cannot point at a local source.`);
76
+ // The module token itself may not carry a pinned version: `=` and `:` select
77
+ // different sources. A Windows drive letter inside the path is not that.
78
+ if (module.includes(':'))
79
+ throw new Error(`Invalid module spec "${value}": a local source (=) cannot also pin a version (:).`);
80
+ if (!localPath)
81
+ throw new Error(`Invalid module spec "${value}": missing local DLL path.`);
82
+ return { module, localPath };
83
+ }
84
+ const colon = value.indexOf(':');
85
+ if (colon >= 0) {
86
+ const module = value.slice(0, colon).trim().toLowerCase();
87
+ const version = value.slice(colon + 1).trim();
88
+ if (!module)
89
+ throw new Error(`Invalid module spec "${value}": missing module name.`);
90
+ if (module === allModules)
91
+ throw new Error(`Invalid module spec "${value}": all selects every configured module from remote and cannot pin a version.`);
92
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(version))
93
+ throw new Error(`Invalid module spec "${value}": invalid pinned version.`);
94
+ return { module, version };
95
+ }
96
+ const module = value.trim().toLowerCase();
97
+ if (!module)
98
+ throw new Error(`Invalid module spec "${value}": missing module name.`);
99
+ if (module === allModules)
100
+ return { module: allModules };
101
+ return { module };
102
+ }
103
+ /** Record one parsed spec, enforcing "all alone" and duplicate rules. */
104
+ function pushSpec(specs, seen, spec) {
105
+ if (spec.module === allModules) {
106
+ if (seen.has(allModules))
107
+ throw new Error('Duplicate module in one update: all.');
108
+ if (seen.size > 0)
109
+ throw new Error('Module spec all cannot be combined with individual modules.');
110
+ }
111
+ else {
112
+ validateUpdatableModule(spec.module);
113
+ if (seen.has(allModules))
114
+ throw new Error('Module spec all cannot be combined with individual modules.');
115
+ if (seen.has(spec.module))
116
+ throw new Error(`Duplicate module in one update: ${spec.module}.`);
117
+ }
118
+ seen.add(spec.module);
119
+ specs.push(spec);
120
+ }
121
+ /**
122
+ * Parse `--modules` values into module specs.
123
+ *
124
+ * Values that carry a local source (`module=path`) are split on commas only, so
125
+ * a Windows path may contain spaces; every other value accepts space- and
126
+ * comma-separated specs, which is how the CLI passes `--modules a b,c`.
127
+ * `all` selects every configured module from remote and must be used alone.
128
+ */
129
+ function parseModuleSpecs(values) {
130
+ if (!Array.isArray(values))
131
+ throw new Error('Module specs must be an array of strings.');
132
+ const specs = [];
133
+ const seen = new Set();
134
+ for (const value of values) {
135
+ const text = String(value ?? '');
136
+ for (const fragment of text.includes('=') ? text.split(',') : text.split(/[\s,]+/)) {
137
+ const entry = fragment.trim();
138
+ if (!entry)
139
+ continue;
140
+ pushSpec(specs, seen, parseModuleSpec(entry));
141
+ }
142
+ }
143
+ return specs;
144
+ }
145
+ function normalizeModuleSpecs(values) {
146
+ if (!Array.isArray(values))
147
+ throw new Error('Module specs must be an array.');
148
+ const specs = [];
149
+ const seen = new Set();
150
+ const push = (spec) => pushSpec(specs, seen, spec);
151
+ for (const value of values) {
152
+ if (typeof value === 'string') {
153
+ for (const spec of parseModuleSpecs([value]))
154
+ push(spec);
155
+ continue;
156
+ }
157
+ if (!value || typeof value !== 'object')
158
+ throw new Error('Invalid module spec.');
159
+ const module = typeof value.module === 'string' ? value.module.trim().toLowerCase() : '';
160
+ const version = value.version === undefined ? undefined : String(value.version);
161
+ const localPath = value.localPath === undefined ? undefined : String(value.localPath);
162
+ if (!module)
163
+ throw new Error('Invalid module spec: missing module name.');
164
+ if (module === allModules) {
165
+ if (version !== undefined || localPath !== undefined)
166
+ throw new Error('Module spec all cannot pin a version or point at a local source.');
167
+ push({ module: allModules });
168
+ continue;
169
+ }
170
+ if (version !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(version))
171
+ throw new Error(`Invalid version for module ${module}.`);
172
+ if (version !== undefined && localPath !== undefined)
173
+ throw new Error(`Module ${module} cannot combine a local path (=) with a pinned version (:).`);
174
+ if (version === undefined && localPath === undefined) {
175
+ push({ module });
176
+ continue;
177
+ }
178
+ push({ module, ...(version !== undefined ? { version } : {}), ...(localPath !== undefined ? { localPath } : {}) });
179
+ }
180
+ return specs;
181
+ }
182
+ /**
183
+ * Rewrite the `<Server><Plugins>` list of a Wyn configuration document.
184
+ *
185
+ * The document is parsed with the creation path's parser options, only the
186
+ * `sys:string` plugin list is mutated, and the result is rebuilt with the same
187
+ * builder options and re-validated before it is returned. Dashboard stays the
188
+ * first entry; unknown existing entries keep their order.
189
+ */
190
+ function patchEnvironmentConfiguration(xml, entries) {
191
+ if (typeof xml !== 'string' || /<!DOCTYPE|<!ENTITY/i.test(xml) || fast_xml_parser_1.XMLValidator.validate(xml) !== true)
192
+ throw new Error('Invalid environment configuration.');
193
+ const document = new fast_xml_parser_1.XMLParser(parserOptions).parse(xml);
194
+ const server = document?.SystemConfig?.Services?.Server;
195
+ if (!server || typeof server !== 'object' || Array.isArray(server))
196
+ throw new Error('Environment configuration has no Server service.');
197
+ const plugins = server.Plugins ?? {};
198
+ if (typeof plugins !== 'object' || Array.isArray(plugins))
199
+ throw new Error('Invalid environment configuration.');
200
+ const raw = plugins['sys:string'];
201
+ const list = raw === undefined ? [] : Array.isArray(raw) ? [...raw] : [raw];
202
+ if (list.some(value => typeof value !== 'string'))
203
+ throw new Error('Invalid environment configuration.');
204
+ const strings = list.map(value => value.trim());
205
+ const used = new Set();
206
+ const appended = [];
207
+ for (const entry of entries) {
208
+ const prefix = windowsPath(entry.destination).toLowerCase() + '\\';
209
+ const assemblies = new Set(entry.assemblies.map(name => name.toLowerCase()));
210
+ const index = strings.findIndex((value, candidate) => {
211
+ if (used.has(candidate))
212
+ return false;
213
+ const folded = windowsPath(value).toLowerCase();
214
+ return folded.startsWith(prefix) || assemblies.has(path.win32.basename(folded));
215
+ });
216
+ if (index < 0)
217
+ appended.push(windowsPath(entry.path));
218
+ else {
219
+ used.add(index);
220
+ strings[index] = windowsPath(entry.path);
221
+ }
222
+ }
223
+ const combined = [...strings, ...appended];
224
+ // Keep Dashboard first even when it is a local build path rather than a deployed one.
225
+ const dashboard = combined.findIndex(value => {
226
+ const folded = windowsPath(value).toLowerCase();
227
+ return folded.startsWith('plugins\\dashboard\\') || path.win32.basename(folded) === 'wyn.dashboards.plugin.dll';
228
+ });
229
+ if (dashboard > 0)
230
+ combined.unshift(combined.splice(dashboard, 1)[0]);
231
+ plugins['sys:string'] = combined;
232
+ server.Plugins = plugins;
233
+ const output = new fast_xml_parser_1.XMLBuilder(builderOptions).build(document);
234
+ if (fast_xml_parser_1.XMLValidator.validate(output) !== true)
235
+ throw new Error('Invalid environment configuration.');
236
+ return output;
237
+ }
238
+ async function exists(target) {
239
+ try {
240
+ await fs.lstat(target);
241
+ return true;
242
+ }
243
+ catch (error) {
244
+ if (error.code === 'ENOENT')
245
+ return false;
246
+ throw error;
247
+ }
248
+ }
249
+ async function readManifest(root) {
250
+ const file = path.join(root, 'environment.lock.json');
251
+ const stat = await fs.lstat(file);
252
+ if (!stat.isFile() || stat.isSymbolicLink())
253
+ throw new Error('Environment manifest is not a regular file.');
254
+ let document;
255
+ try {
256
+ document = JSON.parse(await fs.readFile(file, 'utf8'));
257
+ }
258
+ catch {
259
+ throw new Error('Environment manifest is unreadable.');
260
+ }
261
+ if (!document || typeof document !== 'object' || Array.isArray(document))
262
+ throw new Error('Environment manifest is unreadable.');
263
+ const manifest = document;
264
+ for (const field of ['artifacts', 'downloads']) {
265
+ const value = manifest[field];
266
+ if (value !== undefined && (!Array.isArray(value) || value.some(item => !item || typeof item !== 'object')))
267
+ throw new Error('Environment manifest is unreadable.');
268
+ }
269
+ if (manifest.selectedModules !== undefined && (!Array.isArray(manifest.selectedModules) || manifest.selectedModules.some(module => typeof module !== 'string')))
270
+ throw new Error('Environment manifest is unreadable.');
271
+ if (manifest.moduleSources !== undefined && (!manifest.moduleSources || typeof manifest.moduleSources !== 'object' || Array.isArray(manifest.moduleSources)))
272
+ throw new Error('Environment manifest is unreadable.');
273
+ return manifest;
274
+ }
275
+ /** Write JSON (or any text) through `<file>.afm-tmp` and an atomic rename. */
276
+ async function writeAtomic(file, content) {
277
+ const temporary = `${file}.afm-tmp`;
278
+ await fs.writeFile(temporary, content, 'utf8');
279
+ await fs.rename(temporary, file);
280
+ }
281
+ const writeJsonAtomic = (file, value) => writeAtomic(file, JSON.stringify(value, null, 2) + '\n');
282
+ /**
283
+ * Resolve one remote spec against the record's major and the module's exact
284
+ * artifact name; version `undefined` means "latest for the record's major".
285
+ */
286
+ async function resolveRemoteArtifact(client, entry, spec) {
287
+ const module = spec.module;
288
+ const source = catalog_1.catalog[module];
289
+ let version;
290
+ let files;
291
+ try {
292
+ version = spec.version ?? (await client.getLatestVersion(source.plugin, entry.major)).version;
293
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(version))
294
+ throw new Error('Invalid resolved version');
295
+ files = await client.listArtifacts(source.plugin, version);
296
+ }
297
+ catch {
298
+ throw new Error(`Unable to resolve artifacts for ${source.plugin}/${spec.version ?? entry.major}.`);
299
+ }
300
+ const name = source.filename(version, entry.edition);
301
+ const candidates = files.filter(file => file.name === name);
302
+ if (candidates.length !== 1)
303
+ throw new Error(`${source.plugin}/${version}: expected exactly one ${name}, found ${candidates.length}.`);
304
+ const remotePath = candidates[0].path;
305
+ if (!remotePath || /[?:\\\x00-\x1f]/.test(remotePath) || remotePath.startsWith('/') || remotePath.split('/').includes('..'))
306
+ throw new Error(`${source.plugin}/${version}: invalid artifact path.`);
307
+ return { module, plugin: source.plugin, version, name, path: remotePath, mappings: source.mappings.map(mapping => ({ ...mapping, assemblies: [...mapping.assemblies] })) };
308
+ }
309
+ /** Download, hash, extract and verify one resolved artifact inside the staging workspace. */
310
+ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
311
+ const label = `[${artifact.module}] ${artifact.plugin}/${artifact.version}/${artifact.name}`;
312
+ const download = path.join(workspace, 'download', artifact.module);
313
+ const expanded = path.join(workspace, `expanded-${artifact.module}`);
314
+ try {
315
+ await fs.mkdir(download, { recursive: true });
316
+ onProgress?.(`${label}: Downloading`);
317
+ await client.downloadArtifact(artifact.plugin, artifact.version, artifact.name, download);
318
+ const archive = path.join(download, artifact.name);
319
+ await (0, builder_1.regularFile)(archive);
320
+ onProgress?.(`${label}: Download complete; calculating SHA256`);
321
+ const sha256 = await (0, builder_1.hashFile)(archive);
322
+ onProgress?.(`${label}: Extracting and validating ZIP`);
323
+ await (0, archive_1.extractArchive)(archive, expanded);
324
+ const assemblies = new Map();
325
+ for (const mapping of artifact.mappings) {
326
+ const source = path.join(expanded, mapping.source);
327
+ if (!(await fs.lstat(source)).isDirectory())
328
+ throw new Error('Required source directory missing.');
329
+ const present = [];
330
+ for (const name of mapping.assemblies) {
331
+ try {
332
+ await (0, builder_1.regularFile)(path.join(source, name));
333
+ present.push(name);
334
+ }
335
+ catch (error) {
336
+ if (error.code !== 'ENOENT')
337
+ throw error;
338
+ }
339
+ }
340
+ if (present.length !== 1)
341
+ throw new Error(`Expected one runtime assembly in ${mapping.destination || 'root'}.`);
342
+ assemblies.set(mapping.destination, present[0]);
343
+ }
344
+ onProgress?.(`${label}: Archive and layout checks complete`);
345
+ return { ...artifact, sha256, expanded, assemblies };
346
+ }
347
+ catch (error) {
348
+ throw new Error(`Module update failed for ${artifact.plugin}/${artifact.version}/${artifact.name} (download, archive or layout): ${error.message}`);
349
+ }
350
+ }
351
+ /** Resolve and validate a local plugin source for one module. */
352
+ async function resolveLocalSource(entry, module, mappings, input) {
353
+ const plugins = mappings.filter(mapping => mapping.role === 'plugin');
354
+ if (plugins.length !== 1)
355
+ throw new Error(`Local sources are not supported for module ${module}: it deploys ${plugins.length} plugin directories (${plugins.map(mapping => mapping.destination).join(', ')}). Use a remote update instead.`);
356
+ const mapping = plugins[0];
357
+ const target = path.resolve(input);
358
+ const stat = await fs.lstat(target).catch((error) => { if (error.code === 'ENOENT')
359
+ return undefined; throw error; });
360
+ if (!stat)
361
+ throw new Error(`Local source not found: ${target}.`);
362
+ let dll;
363
+ if (stat.isSymbolicLink())
364
+ throw new Error(`Local source must be a regular file or directory: ${target}.`);
365
+ if (stat.isFile()) {
366
+ if (path.extname(target).toLowerCase() !== '.dll')
367
+ throw new Error(`Local source must be a .dll file: ${target}.`);
368
+ dll = target;
369
+ }
370
+ else if (stat.isDirectory()) {
371
+ const found = [];
372
+ for (const assembly of mapping.assemblies) {
373
+ const candidate = path.join(target, assembly);
374
+ const candidateStat = await fs.lstat(candidate).catch((error) => { if (error.code === 'ENOENT')
375
+ return undefined; throw error; });
376
+ if (candidateStat?.isFile() && !candidateStat.isSymbolicLink())
377
+ found.push(candidate);
378
+ }
379
+ if (found.length !== 1)
380
+ throw new Error(`Local source directory ${target} must contain exactly one of ${mapping.assemblies.join(', ')}; found ${found.length}.`);
381
+ dll = found[0];
382
+ }
383
+ else
384
+ throw new Error(`Local source must be a regular .dll file or a directory: ${target}.`);
385
+ const deployed = installationDirectory(entry.root, mapping.destination);
386
+ const relative = path.relative(deployed.toLowerCase(), dll.toLowerCase());
387
+ if (relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative))
388
+ throw new Error(`Local source ${dll} is inside the deployed plugin directory ${deployed}; point at the developer build output outside the environment.`);
389
+ return { dll, pluginDirectories: [mapping.destination] };
390
+ }
391
+ /**
392
+ * Update selected modules of a registered environment.
393
+ *
394
+ * Remote specs replace the deployed module directories with a freshly verified
395
+ * artifact; local specs copy nothing, delete the deployed plugin directory and
396
+ * point the Server configuration at the absolute local DLL so the developer
397
+ * build is loaded directly. Files are staged in a sibling temporary workspace
398
+ * that is always removed, and JSON files are written through `<file>.afm-tmp`.
399
+ */
400
+ class EnvironmentUpdater {
401
+ dependencies;
402
+ registry;
403
+ processes;
404
+ platform;
405
+ onProgress;
406
+ constructor(dependencies = {}) {
407
+ this.dependencies = dependencies;
408
+ this.registry = dependencies.registry ?? new registry_1.EnvironmentRegistry();
409
+ this.processes = dependencies.processes ?? new process_1.WindowsProcessBackend();
410
+ this.platform = dependencies.platform ?? process.platform;
411
+ this.onProgress = dependencies.onProgress;
412
+ }
413
+ async update(name, specs, clientFactory, options = {}) {
414
+ const dryRun = options.dryRun === true;
415
+ const onProgress = options.onProgress ?? this.onProgress;
416
+ if (!dryRun && this.platform !== 'win32')
417
+ throw new Error('Environment execution is Windows-only. Use dry-run on other platforms.');
418
+ onProgress?.('Validating module specs');
419
+ let parsed = normalizeModuleSpecs(specs);
420
+ if (!parsed.length)
421
+ throw new Error('At least one module spec is required: use --modules all, dashboard, reporting or dashboard=D:\\path\\Wyn.Dashboards.Plugin.dll.');
422
+ const entry = await this.registry.get(name);
423
+ onProgress?.(`Validating environment ${entry.name} at ${entry.root}`);
424
+ await (0, validate_1.validateEnvironmentRoot)(entry, this.registry.directory);
425
+ await (0, validate_1.validateEnvironmentOwnership)(entry, this.registry.directory);
426
+ await (0, validate_1.validateEnvironmentTree)(entry.root);
427
+ const manifest = await readManifest(entry.root);
428
+ const artifacts = [...(manifest.artifacts ?? [])];
429
+ const skipped = [];
430
+ if (parsed.some(spec => spec.module === 'all')) {
431
+ // `all` means every configured module from remote; modules that already
432
+ // point at a local developer build are left exactly as they are.
433
+ const localModules = new Set([...Object.keys(manifest.moduleSources ?? {}), ...(entry.localModules ?? [])]);
434
+ const configured = [...new Set(artifacts.map(artifact => artifact.module))].filter(module => updatableModules.includes(module)).sort();
435
+ for (const module of configured)
436
+ if (localModules.has(module))
437
+ skipped.push({ module, reason: 'local' });
438
+ const selected = configured.filter(module => !localModules.has(module));
439
+ if (!selected.length)
440
+ 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)'}.`);
441
+ parsed = selected.map(module => ({ module }));
442
+ onProgress?.(`Updating all configured modules from remote: ${selected.join(', ')}${skipped.length ? ` (skipping local: ${skipped.map(item => item.module).join(', ')})` : ''}`);
443
+ }
444
+ const localOverrides = new Map();
445
+ for (const spec of parsed) {
446
+ if (spec.module === 'sheets' && entry.edition !== 'en')
447
+ throw new Error(`Module sheets requires an EN edition environment; ${entry.name} is ${entry.edition}.`);
448
+ if (spec.localPath === undefined)
449
+ continue;
450
+ localOverrides.set(spec.module, await resolveLocalSource(entry, spec.module, catalog_1.catalog[spec.module].mappings, spec.localPath));
451
+ }
452
+ const remote = parsed.filter(spec => spec.localPath === undefined);
453
+ const state = await (0, validate_1.observeRuntimeState)(entry, this.processes);
454
+ if (remote.length && state !== 'ready' && state !== 'stopped')
455
+ 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.`);
456
+ if (!remote.length && state !== 'ready' && state !== 'stopped' && state !== 'running')
457
+ 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.`);
458
+ const downloads = [...(manifest.downloads ?? [])];
459
+ const selectedModules = manifest.selectedModules === undefined ? undefined : [...manifest.selectedModules];
460
+ const moduleSources = { ...(manifest.moduleSources ?? {}) };
461
+ const installed = new Set(artifacts.map(artifact => artifact.module));
462
+ const workspace = remote.length && !dryRun ? await fs.mkdtemp(path.join(path.dirname(entry.root), '.afm-env-')) : undefined;
463
+ const warnings = [];
464
+ try {
465
+ const resolved = new Map();
466
+ const staged = new Map();
467
+ if (remote.length) {
468
+ const client = clientFactory();
469
+ for (const spec of remote) {
470
+ onProgress?.(`Resolving ${catalog_1.catalog[spec.module].plugin}`);
471
+ resolved.set(spec.module, await resolveRemoteArtifact(client, entry, spec));
472
+ }
473
+ if (!dryRun)
474
+ for (const spec of remote)
475
+ staged.set(spec.module, await stageRemoteArtifact(client, resolved.get(spec.module), workspace, onProgress));
476
+ }
477
+ const configured = await fs.readFile(path.join(entry.root, 'conf', 'Wyn.conf'), 'utf8');
478
+ const plans = [];
479
+ for (const spec of parsed) {
480
+ const module = spec.module;
481
+ const artifact = artifacts.find(candidate => candidate.module === module);
482
+ const previous = moduleSources[module] ? { local: moduleSources[module].path } : { version: artifact?.version };
483
+ const pluginMappings = catalog_1.catalog[module].mappings.filter(mapping => mapping.role === 'plugin');
484
+ const override = localOverrides.get(module);
485
+ if (override) {
486
+ const removals = [];
487
+ const removedDirectories = [];
488
+ for (const mapping of pluginMappings) {
489
+ const destination = installationDirectory(entry.root, mapping.destination);
490
+ if (!await exists(destination))
491
+ continue;
492
+ removals.push(destination);
493
+ removedDirectories.push(mapping.destination);
494
+ }
495
+ plans.push({
496
+ module, previous, next: { local: override.dll }, local: override,
497
+ entries: pluginMappings.map(mapping => ({ destination: mapping.destination, assemblies: mapping.assemblies, path: override.dll })),
498
+ copies: [], removals, copiedDirectories: [], removedDirectories,
499
+ });
500
+ continue;
501
+ }
502
+ const remoteArtifact = resolved.get(module);
503
+ const stagedArtifact = staged.get(module);
504
+ const copies = [];
505
+ const removals = [];
506
+ const copiedDirectories = [];
507
+ const removedDirectories = [];
508
+ const entries = [];
509
+ for (const mapping of remoteArtifact.mappings) {
510
+ const destination = installationDirectory(entry.root, mapping.destination);
511
+ if (await exists(destination)) {
512
+ removals.push(destination);
513
+ removedDirectories.push(mapping.destination);
514
+ }
515
+ if (stagedArtifact)
516
+ copies.push({ source: path.join(stagedArtifact.expanded, mapping.source), destination });
517
+ copiedDirectories.push(mapping.destination);
518
+ const assembly = stagedArtifact?.assemblies.get(mapping.destination) ?? knownAssembly(configured, mapping);
519
+ if (mapping.role === 'plugin')
520
+ entries.push({ destination: mapping.destination, assemblies: mapping.assemblies, path: path.posix.join(mapping.destination, assembly) });
521
+ }
522
+ if (module === 'dashboard' && stagedArtifact) {
523
+ const contents = path.join(stagedArtifact.expanded, 'Web Contents');
524
+ if ((await fs.lstat(contents).catch(() => undefined))?.isDirectory()) {
525
+ const destination = path.join(entry.root, 'Web Contents');
526
+ if (await exists(destination)) {
527
+ removals.push(destination);
528
+ removedDirectories.push('Web Contents');
529
+ }
530
+ copies.push({ source: contents, destination });
531
+ copiedDirectories.push('Web Contents');
532
+ }
533
+ }
534
+ plans.push({
535
+ module, previous, next: { version: remoteArtifact.version }, artifact: remoteArtifact, staged: stagedArtifact,
536
+ entries, copies, removals, copiedDirectories, removedDirectories,
537
+ });
538
+ }
539
+ const changes = plans.map(plan => ({ module: plan.module, previous: plan.previous, next: plan.next, copiedDirectories: plan.copiedDirectories, removedDirectories: plan.removedDirectories }));
540
+ const configuration = patchEnvironmentConfiguration(configured, plans.flatMap(plan => plan.entries));
541
+ const nextArtifacts = [...artifacts];
542
+ const nextDownloads = [...downloads];
543
+ for (const plan of plans) {
544
+ if (plan.artifact) {
545
+ const replacement = {
546
+ module: plan.module, plugin: plan.artifact.plugin, version: plan.artifact.version, name: plan.artifact.name, path: plan.artifact.path,
547
+ mappings: catalog_1.catalog[plan.module].mappings.map(mapping => ({ ...mapping, assemblies: [...mapping.assemblies] })),
548
+ };
549
+ const index = nextArtifacts.findIndex(candidate => candidate.module === plan.module);
550
+ if (index < 0)
551
+ nextArtifacts.push(replacement);
552
+ else
553
+ nextArtifacts[index] = { ...nextArtifacts[index], ...replacement };
554
+ if (plan.staged) {
555
+ const download = { plugin: plan.artifact.plugin, version: plan.artifact.version, name: plan.artifact.name, sha256: plan.staged.sha256 };
556
+ const downloadIndex = nextDownloads.findIndex(candidate => candidate.plugin === plan.artifact.plugin);
557
+ if (downloadIndex < 0)
558
+ nextDownloads.push(download);
559
+ else
560
+ nextDownloads[downloadIndex] = download;
561
+ }
562
+ delete moduleSources[plan.module];
563
+ }
564
+ if (plan.local)
565
+ moduleSources[plan.module] = { type: 'local', path: plan.local.dll, pluginDirectories: plan.local.pluginDirectories };
566
+ if (!installed.has(plan.module) && plan.module !== 'common' && selectedModules)
567
+ selectedModules.push(plan.module);
568
+ }
569
+ if (selectedModules) {
570
+ const unique = [...new Set(selectedModules)].sort();
571
+ selectedModules.length = 0;
572
+ selectedModules.push(...unique);
573
+ }
574
+ const nextManifest = { ...manifest, artifacts: nextArtifacts, downloads: nextDownloads };
575
+ if (Object.keys(moduleSources).length)
576
+ nextManifest.moduleSources = moduleSources;
577
+ else
578
+ delete nextManifest.moduleSources;
579
+ if (selectedModules)
580
+ nextManifest.selectedModules = selectedModules;
581
+ const modules = [...new Set([...entry.modules, ...plans.filter(plan => !installed.has(plan.module)).map(plan => plan.module)])].sort();
582
+ const localModules = Object.keys(moduleSources).sort();
583
+ if (state === 'running')
584
+ warnings.push(`${entry.name} is running; restart it to load the updated modules.`);
585
+ for (const item of skipped)
586
+ warnings.push(`${item.module} has a local source and was not updated from artifacts.`);
587
+ if (!dryRun) {
588
+ for (const plan of plans) {
589
+ for (const removal of plan.removals)
590
+ await fs.rm(removal, { recursive: true, force: true });
591
+ for (const copy of plan.copies)
592
+ await (0, builder_1.copyTree)(copy.source, copy.destination);
593
+ }
594
+ onProgress?.('Writing configuration, manifest and registry record');
595
+ await writeAtomic(path.join(entry.root, 'conf', 'Wyn.conf'), configuration);
596
+ await writeJsonAtomic(path.join(entry.root, 'environment.lock.json'), nextManifest);
597
+ await this.registry.update(entries => {
598
+ const index = entries.findIndex(candidate => candidate.id === entry.id);
599
+ if (index < 0)
600
+ throw new Error('Environment record disappeared during update.');
601
+ if (entries[index].root !== entry.root)
602
+ throw new Error('Environment changed during update.');
603
+ entries[index].modules = modules;
604
+ if (localModules.length)
605
+ entries[index].localModules = localModules;
606
+ else
607
+ delete entries[index].localModules;
608
+ });
609
+ }
610
+ const restartRequired = !dryRun && changes.length > 0;
611
+ if (restartRequired)
612
+ onProgress?.(`${entry.name}: modules updated; restart the environment to load the change (no service was started).`);
613
+ return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings };
614
+ }
615
+ finally {
616
+ if (workspace)
617
+ await fs.rm(workspace, { recursive: true, force: true });
618
+ }
619
+ }
620
+ }
621
+ exports.EnvironmentUpdater = EnvironmentUpdater;
622
+ /** Best-effort preview assembly for dry-run, where nothing can be extracted. */
623
+ function knownAssembly(xml, mapping) {
624
+ try {
625
+ const document = new fast_xml_parser_1.XMLParser(parserOptions).parse(xml);
626
+ const raw = document?.SystemConfig?.Services?.Server?.Plugins?.['sys:string'];
627
+ const list = raw === undefined ? [] : Array.isArray(raw) ? raw : [raw];
628
+ const prefix = windowsPath(mapping.destination).toLowerCase() + '\\';
629
+ for (const value of list) {
630
+ if (typeof value !== 'string')
631
+ continue;
632
+ const folded = windowsPath(value).toLowerCase();
633
+ if (!folded.startsWith(prefix))
634
+ continue;
635
+ const name = path.win32.basename(folded);
636
+ const match = mapping.assemblies.find(assembly => assembly.toLowerCase() === name);
637
+ if (match)
638
+ return match;
639
+ }
640
+ }
641
+ catch { /* Fall through to the first known assembly name. */ }
642
+ return mapping.assemblies[0];
643
+ }
644
+ //# sourceMappingURL=update.js.map