@guanjinke/artifact-client 1.4.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.
package/README.md CHANGED
@@ -1,363 +1,186 @@
1
- # @guanjinke/artifact-client
2
-
3
- Artifact management client and CLI for Azure Blob Storage and Aliyun OSS.
4
-
5
- Manage plugin build artifacts stored in `plugin_artifacts/` — list plugins, versions, artifacts, upload, download, and record which version each product major version currently releases.
6
-
7
- ## Installation
8
-
9
- ```bash
10
- npm install @guanjinke/artifact-client
11
- ```
12
-
13
- Requires Node.js >= 20.
14
-
15
- ## Usage
16
-
17
- ### As a Library
18
-
19
- ```typescript
20
- import { ArtifactClient } from '@guanjinke/artifact-client';
21
-
22
- // Azure Storage
23
- const azClient = new ArtifactClient('az', {
24
- sasUrl: 'https://<account>.blob.core.windows.net/<container>?<SAS token>',
25
- });
26
-
27
- // Aliyun OSS
28
- const ossClient = new ArtifactClient('oss', {
29
- keyId: 'your-access-key-id',
30
- key: 'your-access-key-secret',
31
- bucket: 'your-bucket',
32
- region: 'oss-cn-hangzhou', // optional, defaults to oss-cn-hangzhou
33
- });
34
-
35
- // List all plugins
36
- const plugins = await client.listPlugins();
37
- // [{ name: 'myPlugin', path: 'plugin_artifacts/myPlugin/' }, ...]
38
-
39
- // List plugins with sizes
40
- const pluginsWithSize = await client.listPlugins({ includeSize: true });
41
-
42
- // List major versions of a plugin
43
- const majors = await client.listMajorVersions('myPlugin');
44
- // [{ majorVersion: '1.0', latestVersion: '1.0.5', minorCount: 3, path: '...' }, ...]
45
-
46
- // List major versions sorted ascending
47
- const majorsAsc = await client.listMajorVersions('myPlugin', { sortOrder: 'asc' });
48
-
49
- // Get the latest version for a major
50
- const latest = await client.getLatestVersion('myPlugin', '1.0');
51
- // { version: '1.0.5', path: 'plugin_artifacts/myPlugin/1.0.5/' }
52
-
53
- // List all minor versions under a major
54
- const minors = await client.listMinorVersions('myPlugin', '1.0');
55
- // [{ version: '1.0.5', path: '...' }, { version: '1.0.4', path: '...' }, ...]
56
-
57
- // List artifacts in a specific version
58
- const artifacts = await client.listArtifacts('myPlugin', '1.0.5');
59
- // [{ name: 'bundle.zip', path: '...' }, ...]
60
-
61
- // ... including each file's size
62
- const artifactsWithSize = await client.listArtifacts('myPlugin', '1.0.5', { includeSize: true });
63
- // [{ name: 'bundle.zip', path: '...', sizeMB: 12.5 }, ...]
64
-
65
- // Upload an artifact into a version directory (a plain write: no major version
66
- // pointer is touched)
67
- await client.uploadArtifact('myPlugin', '1.0.6', 'bundle.zip', './dist/bundle.zip');
68
-
69
- // Separately, record it as the latest version of a product major version
70
- await client.updateLatestVersion('myPlugin', '9.1-hotfix1', '1.0.6');
71
-
72
- // Download by full path
73
- await client.downloadArtifactByPath(
74
- 'plugin_artifacts/myPlugin/1.0.5/bundle.zip',
75
- './downloads'
76
- );
77
-
78
- // Download by name
79
- await client.downloadArtifact('myPlugin', '1.0.5', 'bundle.zip', './downloads');
80
-
81
- // Download all artifacts for a version
82
- const files = await client.downloadVersion('myPlugin', '1.0.5', './downloads');
83
- // ['bundle.zip', 'checksum.txt']
84
- ```
85
-
86
- ### As a CLI
87
-
88
- ```bash
89
- # Login (saves credentials to ~/.afm/config.json; set AFM_CONFIG_DIR to use another directory)
90
- afm az login --sasurl "https://..."
91
- afm oss login --keyid "your-id" --key "your-key" --bucket "my-bucket"
92
-
93
- # Or pass credentials inline
94
- afm az list-plugins --sasurl "https://..."
95
-
96
- # List plugins
97
- afm az list-plugins
98
- afm az list-plugins --size
99
-
100
- # List major versions
101
- afm az list-majors myPlugin
102
- afm az list-majors myPlugin --size --sort asc
103
-
104
- # Get latest version for a major
105
- afm az get-latest myPlugin 1.0
106
-
107
- # List minor versions
108
- afm az list-minors myPlugin 1.0
109
- afm az list-minors myPlugin 1.0 --sort asc
110
-
111
- # List artifacts in a version
112
- afm az list-artifacts myPlugin 1.0.5
113
- afm az list-artifacts myPlugin 1.0.5 --size
114
-
115
- # Upload
116
- afm az upload myPlugin 1.0.6 bundle.zip ./dist/bundle.zip
117
- afm az set-latest myPlugin 1.0 1.0.6
118
-
119
- # Download by full path
120
- afm az download --path "plugin_artifacts/myPlugin/1.0.5/bundle.zip" --out ./downloads
121
-
122
- # Download by name
123
- afm az download --plugin myPlugin --ver 1.0.5 --file bundle.zip --out ./downloads
124
-
125
- # Download all artifacts for a version
126
- afm az download --plugin myPlugin --ver 1.0.5 --all --out ./downloads
127
-
128
- # Download the latest version of a major version (all artifacts by default)
129
- afm az download --plugin myPlugin --major 1.0 --out ./downloads
130
-
131
- # ... or a single artifact from that major's latest version
132
- afm az download --plugin myPlugin --major 1.0 --file bundle.zip --out ./downloads
133
-
134
- # Manage config
135
- afm az config show
136
- afm az config clear
137
- ```
138
-
139
- #### OSS Commands
140
-
141
- Same as above, replace `az` with `oss`:
142
-
143
- ```bash
144
- afm oss list-plugins --keyid "id" --key "secret"
145
- afm oss upload myPlugin 1.0.6 bundle.zip ./dist/bundle.zip
146
- afm oss set-latest myPlugin 9.1-hotfix1 1.0.6
147
- ```
148
-
149
- ## Major Versions and Channels
150
-
151
- A **product major version** (主版本号) is a label rather than a semver:
152
-
153
- | Major version | Meaning |
154
- |---|---|
155
- | `10.0`, `9.0` | first product major of the year |
156
- | `8.1`, `9.1`, `10.1` | second product major of the year |
157
- | `9.1-oem` | major version for a specific OEM customer |
158
- | `9.1-hotfix1` | hotfix major version built on top of `9.1` |
159
- | `dev` | development major version, tracked in `versiondev.txt` |
160
-
161
- - Every major version owns a version file `version<major>.txt` recording its latest artifact
162
- version, and `get-latest <plugin> <major>` reads exactly that file — so
163
- `afm oss get-latest gces-server 9.1-hotfix2` needs no special casing.
164
- - Artifact versions are **independent** of the major version: they usually repeat the major
165
- version's first two segments, but a dev artifact may be `10.0.1` (identity artifacts stay
166
- compatible with the license control system). Uploading a file and moving a major version's latest
167
- version are therefore **separate steps** — `upload` only writes the file, while
168
- `set-latest <plugin> <major> <version>` records a version as that major version's latest (or
169
- `upload --set-latest --major <major>` does both at once). A hotfix upload can no longer move
170
- `version9.1.txt` by accident, and a dev build such as `10.0.1` never lands in `version10.0.txt`.
171
- - `list-majors` reports every major version it finds — directory-derived majors and file-declared
172
- channel majors — with the latest version each one points at. Channel majors (`minorCount: 0`)
173
- publish into plain version directories, so their history cannot be enumerated; `list-minors
174
- <channel>` reports the version that major currently points at.
175
- - A version file without a pointer is a reserved major version and is not listed.
176
- - `get-latest` checks that the version a major version points at actually holds artifacts and warns
177
- on stderr when it does not — production still contains pointers to pruned versions — while still
178
- printing the pointer. `--no-verify` skips that extra listing.
179
- - `set-latest` warns when the version holds no artifacts but records it anyway — a major version is
180
- often published before every file has landed — and `--no-verify` skips that check too.
181
- - `download --plugin <plugin> --major <major>` needs no `--ver`: it resolves the major's latest
182
- version first and then downloads all of that version's artifacts, so `--all` is implied.
183
- `--file` narrows it to a single artifact of that version. `--major` and `--ver` are mutually
184
- exclusive. Artifacts that live in a sub-directory keep it locally: `patched/X.dll` is written to
185
- `<out>/patched/X.dll`.
186
-
187
- ## Storage Layout
188
-
189
- ```
190
- <container/bucket>/
191
- └── plugin_artifacts/
192
- ├── pluginA/
193
- │ ├── version1.0.txt # Tracks latest 1.0.x version
194
- │ ├── version9.1-hotfix1.txt # Tracks latest version of the 9.1-hotfix1 major version
195
- │ ├── versiondev.txt # Tracks latest dev version
196
- │ ├── 1.0.5/
197
- │ │ ├── bundle.zip
198
- │ │ └── checksum.txt
199
- │ └── 1.0.6/
200
- │ └── bundle.zip
201
- └── pluginB/
202
- └── ...
203
- ```
204
-
205
- - **Version files** (`version<major>.txt`, `versiondev.txt`): track the latest full version per major version
206
- - The dev major version is usually the whole `0.x` range: the dev pipeline publishes a plugin-specific minor
207
- (`0.0`, `0.1`, `0.2` and `0.3` are all in use) and always tracks it in `versiondev.txt`, so
208
- read it with `get-latest <plugin> dev` (the numeric `0.x` spelling is accepted as an alias).
209
- Some plugins publish dev artifacts under a different core (the production `gcef-identity-service`
210
- currently points `dev` at `8.1.7-beta`), which is exactly why the version file is authoritative.
211
- - Azure: latest version stored in blob metadata (`latestversion`)
212
- - OSS: latest version stored in object tag (`latestversion`)
213
-
214
- ## API Reference
215
-
216
- ### `ArtifactClient`
217
-
218
- | Method | Description |
219
- |---|---|
220
- | `listPlugins(opts?)` | List all plugins |
221
- | `listMajorVersions(plugin, opts?)` | List major versions with latest version and count |
222
- | `getLatestVersion(plugin, major, opts?)` | Get latest minor version for a major |
223
- | `listMinorVersions(plugin, major, opts?)` | List all minor versions under a major |
224
- | `listArtifacts(plugin, version, opts?)` | List all artifact files in a version |
225
- | `uploadArtifact(plugin, version, name, localPath)` | Upload a file into a version directory; major version pointers are left untouched |
226
- | `updateLatestVersion(plugin, major, version)` | Record a version as a major version's latest (creates `version<major>.txt` when absent) |
227
- | `downloadArtifactByPath(fullPath, localDir)` | Download by full storage path |
228
- | `downloadArtifact(plugin, version, fileName, localDir)` | Download by name |
229
- | `downloadVersion(plugin, version, localDir)` | Download all artifacts for a version |
230
-
231
- ## Create a Wyn Environment
232
-
233
- `create-env` resolves and assembles a **fresh Windows embedded environment** without installing Windows services or starting anything. It supports product labels `9.1`, `9.1-*` and `dev`. Dry-run reports `schema: "pending"`; execution reads the monitoring bundle's XML template and accepts only verified V91/9.1 or V100/10.0, with matching Monitor XSD assets. Missing Server XSD assets produce a warning rather than preventing assembly. Numeric artifact versions never determine the schema. Other numeric product majors fail explicitly.
234
-
235
- A resolved dry-run is not proof that independently published packages are compatible. Some dev Server packages omit `xsd/V100/Wyn.conf.xsd`, while the Monitor template selects V100. Wyn's configuration reader falls back to ordinary XML deserialization when its XSD assets are absent. Assembly therefore records and returns a warning for missing Server XSD rather than rejecting that combination or copying schemas from another package. Monitor's matching XSD remains required. This does not verify the Server assembly's XML reader compatibility or successful runtime startup.
236
-
237
-
238
- ```bash
239
- # Core environment only; Server, Account, Dataset, AnalysisModel and CotWorker are automatic
240
- afm oss create-env D:/wyn-core --name wyn-core --major 9.1
241
-
242
- # Resolve exact per-module artifacts; no downloads or filesystem writes
243
- afm oss create-env D:/wyn-test --name wyn-test --major 9.1 --modules dashboard,reporting --dry-run
244
- afm az create-env D:/wyn-test --name wyn-test --major dev --modules analysis --edition en --dry-run
245
-
246
- # Assemble a fresh environment (Windows only)
247
- afm oss create-env D:/wyn-test --name wyn-test --major 9.1 --modules dashboard reporting --port 51980
248
-
249
- # With a configured environment root (afm env root D:/wyn-envs) the path is optional:
250
- afm oss create-env --name wyn-test --major 9.1
251
- ```
252
-
253
- The default edition is `cn`, profile is `embedded`, and Web/embedded identity port is `51980`. The core Wyn components—Server, Account, Dataset, AnalysisModel and CotWorker—are always included and do not need to be listed in `--modules`. Optional modules are `dashboard`, `reporting` and EN-only `sheets`. `documents` is deliberately rejected until its CollaborationService dependency and embedded configuration are verified. Monitoring is also always included to run the environment. Dashboard no longer requires a separate AnalysisModel selection. Module names may be space- or comma-separated. Each plugin resolves its major once, and only its exact runtime ZIP is selected: unrelated language, styles and support archives are never selected by size.
254
-
255
- Saved credentials and inline credential flags work as with other commands. This command honors saved `basePrefix` and `--base-prefix`; an explicit empty string selects the container/bucket root. Provider defaults are Azure `plugin_artifacts` and OSS root. Existing commands retain their current prefix behavior. The JSON plan includes exact remote object names and mappings, not credentials or a SAS URL.
256
-
257
- Console progress is written to stderr with elapsed time and artifact counts: version resolution, download, SHA256, ZIP validation/extraction, directory assembly, configuration generation and publication. Long stages emit a waiting message every 15 seconds; this is not a byte percentage. Stdout remains a single JSON plan/result, so redirecting stdout preserves machine-readable output. Library callers can opt into the same stage messages using the third `buildEnvironmentPlan` argument or `executeEnvironmentPlan`'s `onProgress` dependency; library calls remain silent by default.
258
-
259
- The target must be absent or empty, must not be a filesystem root or protected installed/system location, and must not have a linked/reparse-point ancestor. There is no overwrite or force option. Download, streaming ZIP extraction, assembly verification and configuration generation happen in owned sibling temporary directories. ZIP paths, case-insensitive conflicts, links, reserved Windows names and resource limits are checked for every entry, including unused support files. The defaults cap each archive at 100,000 entries and 20 GiB uncompressed. Final files are published exclusively without replacing racing files; failure removes only owned paths and preserves unrelated data.
260
-
261
- Server-root `appsettings.json` and `appsettings.Development.json` are excluded from deployment (case-insensitive) so bundled startup overrides cannot affect the generated environment. This does not remove Monitor settings, nested helper settings or other environment-specific settings, and does not modify the downloaded ZIP or existing environments.
262
-
263
- The canonical editable configuration is `<root>/conf/Wyn.conf`. `<root>/Monitor/conf` is a Windows directory junction to it because the monitoring process resolves configuration from its own working directory. Other monitoring-bundle `conf` files are preserved in the canonical directory. The environment contains explicit plugin/embedded worker lists, actual assembly names (including known reporting naming variants), SQLite server databases and DuckDB warehouse settings. Monitored worker services are disabled to avoid parallel launches while embedded Server loads the deployed worker assemblies.
264
-
265
- `environment.lock.json` records selections, independent resolved versions, SHA256 hashes of downloaded ZIPs and runtime framework requirements read from required `.runtimeconfig.json` files. No host license, database or sample data is copied. Optional theme recompilation/styles-cache, additional language archives, bootstrap/sample provisioning, standalone identity patches and service installation are outside this command. Successful assembly is **not** proof of license validity, database initialization or successful runtime startup. Review the generated requirements and provide the appropriate .NET runtimes and licensing before starting anything manually.
266
-
267
- ### Environment Library API
268
-
269
- ```typescript
270
- import { ArtifactClient, buildEnvironmentPlan, executeEnvironmentPlan } from '@guanjinke/artifact-client';
271
-
272
- const client = new ArtifactClient('oss', { keyId: 'id', key: 'secret' });
273
- const plan = await buildEnvironmentPlan({
274
- root: 'D:/wyn-test', major: '9.1', modules: ['dashboard'],
275
- }, client);
276
- // Planning never downloads or writes; execution uses these exact resolved versions.
277
- const result = await executeEnvironmentPlan(plan, { client });
278
- console.log(result.root, result.requirements);
279
- ```
280
-
281
- Both APIs accept the existing client through a small typed interface, enabling cloud-independent testing. Execution defaults to the host platform; the optional injected platform is intended for isolated layout tests, not a claim of cross-platform Wyn support.
282
-
283
- ## Update an Existing Environment
284
-
285
- `update-env` replaces selected modules of an already registered environment, or points a module at a local developer build without copying any files:
286
-
287
- ```bash
288
- afm oss update-env <name> --modules <spec...> [--dry-run]
289
- afm az update-env <name> --modules <spec...> [--dry-run]
290
- ```
291
-
292
- Specs are space- or comma-separated, and each entry has one of these forms:
293
-
294
- | Spec | Meaning |
295
- | --- | --- |
296
- | `all` | every configured module from remote, skipping modules that have a local source |
297
- | `dashboard` | latest artifact of that module for the environment's recorded major |
298
- | `dashboard:0.1.12345` | pinned version |
299
- | `dashboard=D:\build\Wyn.Dashboards.Plugin.dll` | local source (no download, no copy) |
300
-
301
- `all` must be used alone and covers exactly the modules that are both updateable and currently installed in that environment (`common`, `analysis`, plus any of `dashboard`/`reporting`/`sheets` it has). Modules with a local source are skipped and reported (manifest `moduleSources`, registry `localModules`, the result's `skipped` list and a stderr line); `server` and `monitoring` are never included. Because `all` only selects remote specs, it follows the remote state rule below and requires the environment to be `ready` or `stopped`. If nothing is left to update the command fails without writing.
302
-
303
- A spec may not combine a pinned version with a local source, duplicate modules in one call are refused, and the module name must be `dashboard`, `reporting`, `sheets`, `analysis` or `common`. `server` and `monitoring` are refused because they define the runtime and configuration schema (recreate the environment instead), `documents` stays unsupported, and `sheets` still requires an `en` edition environment. Updating a module that was never installed installs it: its plugin entry is added and the module is listed in the registry; a remote spec also adds the manifest artifact/download entries, while a local spec records only `moduleSources`/`localModules` (there is no downloaded artifact to describe).
304
-
305
- Remote updates resolve exactly one exact artifact name for the resolved version, download into a sibling staging workspace (`<dirname(root)>/.afm-env-*`), verify its SHA256, extract and validate it, check every mapping's source directory and that exactly one runtime assembly is present, and only then replace the module's deployed directories (`Plugins/<X>`, its worker directories, and `<root>/Web Contents` when the dashboard archive contains it). The plugin entry is rewritten as the normal relative `Plugins\<dest>\<assembly>` path and the deployment is recorded in the manifest and registry.
306
-
307
- Local source behavior is configuration-only:
308
-
309
- * Nothing is copied into or out of the environment.
310
- * The already deployed `Plugins/<destination>` directory is deleted so the stale artifact cannot be loaded twice; the worker directories are left alone.
311
- * `<root>/conf/Wyn.conf` `<Server><Plugins>` is rewritten to the **absolute** local DLL path (forward slashes are normalized) so the Server loads the developer build directly.
312
- * The override is recorded in `environment.lock.json` `moduleSources` and in the registry as `localModules` (shown by `env list` in a `local` column); `dashboard`, `reporting`, `analysis` and `sheets` are supported. `common` deploys two plugin directories and is rejected for local sources. A directory may be given instead of a DLL when it contains exactly one of the module's known plugin assemblies, and the DLL must be outside the deployed plugin directory.
313
- * Only configuration changed, so the environment must be **restarted** (`afm env restart <name>`) to load the build; the CLI never restarts it automatically.
314
- * Switching a module back to a remote spec deletes the override, restores the relative entry and copies the freshly downloaded artifact again.
315
-
316
- Configuration rewriting always parses `<root>/conf/Wyn.conf`, mutates only the `Plugins` `sys:string` list (a single string or an array are both accepted), rebuilds it with the same options the creation path uses, and validates the result with `XMLValidator` before writing. Dashboard stays the first `Plugins` entry and unrelated entries keep their order.
317
-
318
- State rules: a remote update requires the observed state `ready` or `stopped`; `running`/`unknown` are refused with a request to run `afm env stop <name>` first, because the deployed files may be in use. A local-only update may run while the environment is `running` (the CLI then reports that a restart is required), but `unknown` is always refused. `--dry-run` performs the spec, ownership, tree and state validation plus remote resolution, prints the planned changes and downloads or writes nothing. Progress and the restart reminder go to stderr; stdout remains a single JSON result. JSON and configuration writes go through `<file>.afm-tmp` followed by an atomic rename, and the staging workspace is always removed. Unlike `create`/`start`/`stop`/`restart`, `update-env` does not take the per-environment lifecycle lock — those state rules are its only guard, so do not run an update concurrently with a lifecycle command for the same environment.
319
-
320
- ## Managed Local Environments
321
-
322
- Create a named environment with `node dist/cli.js oss create-env D:/wyn/demo --name demo --major 9.1` (use `az` similarly). `--name` is required for managed creation and names are case-insensitively unique. The environment path is optional once a unified environment root is configured (see below). `--port` defaults to `51980`; reserved or occupied ports fail without automatic reassignment. `--dry-run` checks options, registry conflicts and TCP availability without reserving or writing files.
323
-
324
- ### Unified Environment Root
325
-
326
- Configure one directory for all environments so the path no longer has to be typed:
327
-
328
- ```bash
329
- afm env root D:/wyn-envs # validate, create if missing and remember the directory
330
- afm env root # print the configured directory (path on stdout)
331
- afm env root --json # script-friendly state: configured/root/exists/configPath
332
- afm env root --unset # forget it; <root> is required again
333
-
334
- # No path needed: creates D:/wyn-envs/demo
335
- afm oss create-env --name demo --major 9.1
336
- ```
337
-
338
- The setting is stored as an absolute path in `~/.afm/config.json` (`AFM_CONFIG_DIR` redirects the directory) alongside credentials, so it is per OS user/profile; credentials and other settings are preserved when it is set or cleared. Every new environment is created at `<environment root>/<name>`, and the name must be a single safe directory segment (`A-Za-z0-9`, `.`, `_`, `-`, max 64 characters), so it can never escape the root.
339
-
340
- The configured directory is re-validated on every use. It must not be a filesystem root, the user profile itself, the AFM configuration directory or one of its descendants, or a Windows/Program Files/SystemRoot location, and no existing ancestor may be a symlink or junction. `afm env root <path>` creates a missing directory and verifies it is a regular directory; `--dry-run` validates the configured root without creating anything. An explicit `<root>` argument still wins over the configured root; when it points outside the configured directory a note is printed to stderr and the environment is still registered normally.
341
-
342
- Registered environments live in `~/.afm/environments.json` (set `AFM_CONFIG_DIR` for an isolated profile); credentials and the optional unified environment root remain in `config.json`. Use `node dist/cli.js env list [--json]` (the table shows a `local` column listing modules with a local build override; the JSON rows carry `localModules`), `env root [<path>] [--unset] [--json]`, `env start <name> [--foreground|--background]`, `env stop <name>`, and `env restart <name>`. Background output is appended to `<root>/logs/server.stdout.log` and `server.stderr.log`; foreground Ctrl+C stops the verified monitor process tree before returning.
343
-
344
- Start defaults to foreground. Restart preserves the previous mode, falling back to foreground. A stopped environment keeps its reserved port until deletion. List includes only environments registered for the current user/profile; in table mode it also prints the configured environment root (if any) to stderr. Lifecycle stages, verified monitor PID/mode and background log paths are printed to stderr; stdout remains available for inherited foreground output or list JSON.
345
-
346
- Background mode starts the monitor in its own hidden console through a short launcher, then confirms the monitor by its exact command line (absolute DLL, `--RunAsMonitor`) before recording it; the recorded process is independent of the CLI, and its stdout/stderr are appended to the log files. This is required because `Wyn.Server` assigns `Console.Title` at the top of `Main` and throws `IOException` without an attached console, while a process that merely inherits the CLI's console does not outlive the CLI. Foreground mode inherits the terminal's console and stdio, so run it from a real console; if a start fails, the error includes the monitor's last log output and, when the log shows the `Console.Title` failure, points at that call. Command execution still uses the OS process query (`Get-CimInstance`); if that query is unavailable or fails twice, start/stop report an uncertain state instead of guessing.
347
-
348
- `env delete <name> --yes` stops the verified monitor, removes the owned environment directory and then removes its registry record. Without `--yes`, deletion requires a TTY confirmation. Existing directories created before managed registration are not adopted.
349
-
350
-
351
- Registered states are `creating`, `ready`, `running` and `stopped`. List checks live identity and also reports `unknown`, `identity-mismatch`, `missing` and `broken`; `running` means a verified monitor process, not HTTP readiness. Start/stop/restart are serialized per environment. Names, ports and overlapping roots are reserved atomically before cloud access; interrupted creation remains visible rather than silently releasing an active reservation. Creator command lines are stored as digests so inline credentials are not persisted.
352
-
353
- Startup requires the owned marker and manifest, Server DLL/runtimeconfig, and a configuration port matching the registry. Stop verifies PID, creation timestamp, executable and exact argument tokens, then uses forced Windows `taskkill /T /F` tree termination and confirms captured descendants exited. A final process identity recheck reduces PID reuse risk; Windows native process handles/Job Objects are not used, so hostile process reuse between inspection and `taskkill` is outside this guarantee. Unknown identity or failed tree confirmation retains the record and refuses deletion. Uncertain startup cleanup keeps a recovery identity in the registry and reports failure. If registry persistence also fails, it attempts to write `<root>/.afm-runtime-recovery.json`; any such file makes lifecycle operations uncertain and requires manual recovery. Durable identity storage cannot be guaranteed if both registry and environment filesystems are unwritable. A manual relative `dotnet Wyn.Server.dll` launch cannot be safely attributed from CIM metadata, so such an ambiguous candidate makes list/stop/delete report `unknown` and refuse; a start is still allowed because environments are only ever launched with an absolute DLL path, so that process cannot be the one being replaced.
354
-
355
- Deletion preserves the ownership marker and manifest while removing other payload entries. If payload removal fails, the registry and metadata remain for retry. If the final directory removal fails (for example an editor or terminal still has the folder open), the command retries briefly, then attempts exclusive metadata restoration and reports an actionable error that names the processes whose command line mentions the directory, states that metadata and the registry record were retained, and asks for the same `env delete <name>` command to be re-run after those holders are closed; it does not silently unregister a partial directory.
356
-
357
- Locks are exclusive and bounded, with no automatic stale-lock deletion. A terminated CLI can leave `registry.lock` or `env-<uuid>.lock` in the configuration directory. Recover manually only after positively establishing that its owning operation is no longer active; elapsed age alone is not sufficient. Reservation/process recovery may require manual inspection when Windows denies process metadata. No command automatically adopts old environments or scans arbitrary Wyn installs.
358
-
359
- The standalone `buildEnvironmentPlan`/`executeEnvironmentPlan` APIs retain their previous behavior without global registration; managed callers can use `EnvironmentManager` and inject registry/process/port adapters for isolated tests. Runtime management is Windows-only; metadata planning/listing can run elsewhere but unavailable process inspection is reported as uncertain.
360
-
361
- ## License
362
-
363
- MIT
1
+ # @guanjinke/artifact-client
2
+
3
+ Upload, browse and download build artifacts on **Azure Blob Storage** and **Aliyun OSS**, and build and manage local **Wyn environments** from those artifacts.
4
+
5
+ Requires Node.js >= 20. `az` commands talk to Azure, `oss` commands to Aliyun OSS; below they are interchangeable unless noted.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @guanjinke/artifact-client
11
+ afm --version
12
+ ```
13
+
14
+ ## Credentials
15
+
16
+ ```bash
17
+ afm az login --sasurl "https://<account>.blob.core.windows.net/<container>?<SAS>"
18
+ afm oss login --keyid <id> --key <secret> --bucket <bucket> [--region oss-cn-hangzhou]
19
+
20
+ # or pass them inline on any command: --sasurl, --keyid/--key/--bucket/--region
21
+ afm az list-plugins --sasurl "https://..."
22
+
23
+ afm az config show # print the saved credentials
24
+ afm az config clear # remove them
25
+ ```
26
+
27
+ Saved settings live in `~/.afm/config.json`; set `AFM_CONFIG_DIR` to use another directory.
28
+
29
+ ## Browse artifacts
30
+
31
+ ```bash
32
+ afm az list-plugins [--size]
33
+ afm az list-majors <plugin> [--size] [--sort asc|desc]
34
+ afm az get-latest <plugin> <major> [--size] [--no-verify]
35
+ afm az list-minors <plugin> <major> [--size] [--sort asc|desc]
36
+ afm az list-artifacts <plugin> <version> [--size]
37
+ ```
38
+
39
+ A **major** (`9.1`, `9.0`, `9.1-hotfix1`, `9.1-oem`, `dev`, ...) is a product label, not an artifact version. Each major owns a version file (`version9.1.txt`, `versiondev.txt`) that records the version it currently points at; `get-latest` reads it and warns on stderr when that version no longer holds artifacts (`--no-verify` skips the check).
40
+
41
+ ## Upload
42
+
43
+ ```bash
44
+ # Write the file into the version directory (major pointers untouched)
45
+ afm az upload <plugin> <version> <artifact-name> <local-file>
46
+
47
+ # ... and record it as the latest of a major in the same step
48
+ afm az upload <plugin> <version> <artifact-name> <local-file> --set-latest --major 9.1
49
+
50
+ # Or move a major separately
51
+ afm az set-latest <plugin> <major> <version> [--no-verify]
52
+ ```
53
+
54
+ Uploading and moving a major are deliberately separate: a hotfix upload cannot accidentally move `version9.1.txt`.
55
+
56
+ ## Download
57
+
58
+ ```bash
59
+ afm az download --path "<full/storage/path>" [--out ./dir]
60
+ afm az download --plugin <plugin> --ver <version> --file <name> [--out ./dir]
61
+ afm az download --plugin <plugin> --ver <version> --all [--out ./dir]
62
+ afm az download --plugin <plugin> --major <major> [--file <name>] [--out ./dir]
63
+ ```
64
+
65
+ `--major` resolves that major's latest version first, so `--all` is implied; `--major` and `--ver` are mutually exclusive. Artifacts keep their sub-directory: `patched/X.dll` is written to `<out>/patched/X.dll`.
66
+
67
+ ## Local Wyn environments (Windows)
68
+
69
+ Assembling and running environments is Windows-only. `oss`/`az` only decide where the artifacts come from.
70
+
71
+ ```bash
72
+ # Optionally pick one directory for every environment (stored in config.json)
73
+ afm env root D:\wyn-envs # validate, create if missing, remember
74
+ afm env root # print it
75
+ afm env root --json # { configured, root, exists, configPath }
76
+ afm env root --unset
77
+
78
+ # Assemble a fresh embedded environment (nothing is started or registered as a Windows service)
79
+ afm oss create-env D:\wyn-envs\demo --name demo --major 9.1 \
80
+ [--modules dashboard reporting] [--edition en] [--port 51980] [--dry-run]
81
+
82
+ # With a configured environment root the path can be omitted
83
+ afm oss create-env --name demo --major 9.1
84
+ ```
85
+
86
+ Server, Monitor, Account, Dataset, AnalysisModel and CotWorker are always included; `--modules` adds `dashboard`, `reporting` and (EN edition only) `sheets`. The default port is `51980`. `--dry-run` resolves everything and writes nothing. The result warns when the Server package ships no XSD for the detected configuration schema.
87
+
88
+ ```bash
89
+ afm env list [--json] # state, port, edition, major, root, local overrides, PID
90
+ afm env start <name> [--foreground|--background]
91
+ afm env stop <name>
92
+ afm env restart <name> [--foreground|--background]
93
+ afm env delete <name> [--yes]
94
+ ```
95
+
96
+ * Background output is appended to `<root>\logs\server.stdout.log` and `server.stderr.log`.
97
+ * `running` means the monitor process is alive — not that Wyn finished starting or that the port answers.
98
+ * A stopped environment keeps its reserved port and name; nothing is reassigned automatically.
99
+ * `delete` stops the environment, then removes its directory and its registry record; `--yes` is required when stdin is not a terminal.
100
+ * Environments are listed from `~/.afm/environments.json` (`AFM_CONFIG_DIR` for another profile).
101
+
102
+ ## Update the modules of an environment
103
+
104
+ ```bash
105
+ afm oss update-env <name> --modules dashboard # latest for the recorded major
106
+ afm oss update-env <name> --modules dashboard:0.1.12345 # pinned version
107
+ afm oss update-env <name> --modules dashboard=D:\build\Wyn.Dashboards.Plugin.dll
108
+ afm oss update-env <name> --modules all # every configured module except local ones
109
+ afm oss update-env <name> --modules dashboard --dry-run
110
+ ```
111
+
112
+ Remote updates download, verify (SHA256 + archive checks) and replace the deployed files; the environment must be observed `ready` or `stopped` (`running`/`unknown` is refused — stop it first). A **local source copies nothing**: the deployed `Plugins\<module>` directory is removed and `conf\Wyn.conf` is pointed straight at your build, so the Server loads it; restart the environment to load the change. Modules that were never installed are installed.
113
+
114
+ A module can own several assemblies (`gces-common` = two plugins + one worker; `dashboard`/`reporting` = plugin + worker), so local sources resolve per assembly:
115
+
116
+ | Spec | Selects |
117
+ |---|---|
118
+ | `dashboard=D:\build\Wyn.Dashboards.Plugin.dll` | the target whose file name matches |
119
+ | `dashboard=D:\build\out` | every assembly of that module found in the directory |
120
+ | `dashboard#Wyn.Dashboards.Worker.dll=D:\build\worker.dll` | that target explicitly |
121
+ | repeated `common=` specs | one target each, as long as no target is selected twice |
122
+
123
+ ```bash
124
+ afm oss update-env demo --modules \
125
+ common=D:\gces-common\src\Wyn.Dataset.Plugin\bin\release\net8.0\Wyn.Dataset.Plugin.dll \
126
+ common=D:\gces-common\src\Wyn.Account.Plugin\bin\release\net8.0\Wyn.Account.Plugin.dll \
127
+ common=D:\gces-common\src\Wyn.CotWorker\bin\release\net8.0\Wyn.CotWorker.dll
128
+ ```
129
+
130
+ The override is recorded in the environment manifest (`moduleSources`) and shown by `env list` in a `local` column; switching the module back to a remote spec deletes the override and restores the deployed files.
131
+
132
+ ## Apply a local build while starting
133
+
134
+ ```bash
135
+ afm env start demo --background --modules dashboard=D:\build\Wyn.Dashboards.Plugin.dll
136
+ afm env restart demo --modules common=D:\build\common,common#Wyn.CotWorker.dll=D:\build\Wyn.CotWorker.dll
137
+ ```
138
+
139
+ Same specs as `update-env`, but only local sources are accepted here (`env` has no cloud provider — use `update-env` for artifacts). `start` applies the override and then starts; `restart` stops, applies and starts again, which is the usual "rebuild, then reload" loop.
140
+
141
+ ## Library
142
+
143
+ ```ts
144
+ import { ArtifactClient, buildEnvironmentPlan, executeEnvironmentPlan } from '@guanjinke/artifact-client';
145
+
146
+ const client = new ArtifactClient('oss', { keyId: 'id', key: 'secret', bucket: 'my-bucket' });
147
+
148
+ await client.listPlugins({ includeSize: true });
149
+ await client.listMajorVersions('myPlugin', { sortOrder: 'asc' });
150
+ await client.getLatestVersion('gces-server', '9.1');
151
+ await client.listArtifacts('gces-server', '9.1.21042', { includeSize: true });
152
+ await client.uploadArtifact('myPlugin', '1.0.6', 'bundle.zip', './dist/bundle.zip');
153
+ await client.updateLatestVersion('myPlugin', '9.1-hotfix1', '1.0.6');
154
+ await client.downloadArtifact('myPlugin', '1.0.5', 'bundle.zip', './downloads');
155
+ await client.downloadArtifactByPath('plugin_artifacts/myPlugin/1.0.5/bundle.zip', './downloads');
156
+ const files = await client.downloadVersion('myPlugin', '1.0.5', './downloads');
157
+
158
+ // Plan resolutely (no downloads or writes), then assemble and register an environment
159
+ const plan = await buildEnvironmentPlan({ root: 'D:/wyn/demo', major: '9.1', modules: ['dashboard'] }, client);
160
+ const result = await executeEnvironmentPlan(plan, { client });
161
+ ```
162
+
163
+ | Method | Description |
164
+ |---|---|
165
+ | `listPlugins(opts?)` | List plugins (optionally with total size) |
166
+ | `listMajorVersions(plugin, opts?)` | Majors with the version each one points at |
167
+ | `getLatestVersion(plugin, major, opts?)` | Version a major currently points at |
168
+ | `listMinorVersions(plugin, major, opts?)` | Versions under a major |
169
+ | `listArtifacts(plugin, version, opts?)` | Files of a version (optionally with sizes) |
170
+ | `uploadArtifact(plugin, version, name, localPath)` | Upload one file into a version directory |
171
+ | `updateLatestVersion(plugin, major, version)` | Move a major's latest pointer |
172
+ | `downloadArtifactByPath(fullPath, localDir)` | Download by full storage path |
173
+ | `downloadArtifact(plugin, version, fileName, localDir)` | Download by name |
174
+ | `downloadVersion(plugin, version, localDir)` | Download every file of a version |
175
+
176
+ `client` is a small typed interface, so tests can inject a stub. Environment APIs are Windows-only for execution; planning and listing work anywhere.
177
+
178
+ ## Notes
179
+
180
+ * Stopping is a forced process-tree termination, not a graceful HTTP shutdown.
181
+ * A missing or unknown process identity blocks start/stop/delete instead of guessing.
182
+ * Version resolution never selects a different archive by size: only the exact expected file name is used.
183
+
184
+ Design details, safety semantics and development notes live in the repository.
185
+
186
+ MIT
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { EnvironmentManager } from '../environment/manager';
3
3
  import { EnvironmentRecord } from '../environment/registry';
4
+ import { EnvironmentUpdater } from '../environment/update';
4
5
  export declare function confirmDeletion(entry: EnvironmentRecord, yes: boolean): Promise<boolean>;
5
- export declare function addEnvironmentCommands(program: Command, factory?: () => EnvironmentManager): void;
6
+ export declare function addEnvironmentCommands(program: Command, factory?: () => EnvironmentManager, updaterFactory?: () => EnvironmentUpdater): void;
6
7
  //# sourceMappingURL=environment.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/cli/environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAW,MAAM,yBAAyB,CAAC;AAQrE,wBAAsB,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAO9F;AA0CD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,MAAM,kBAAoG,GAAG,IAAI,CAoBlL"}
1
+ {"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/cli/environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAW,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAoB,MAAM,uBAAuB,CAAC;AA+C7E,wBAAsB,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAO9F;AA0CD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,MAAM,kBAAoG,EAAE,cAAc,GAAE,MAAM,kBAAoG,GAAG,IAAI,CA4C9S"}