@guanjinke/artifact-client 1.13.0 → 1.14.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,217 +1,219 @@
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: `extras/X.dll` is written to `<out>/extras/X.dll`.
66
-
67
- ## Local Wyn environments (Windows)
68
-
69
- The artifacts and the tool are cross-platform: runtime packages ship the RID assets for every platform they support (`runtimes/{linux*, osx, unix, win*}`, with `libSkiaSharp.so`/`.dylib`/`.dll` per platform), and an assembled environment is configured for the host it is built on — `Wyn.conf` uses that platform's path separators, and `Monitor/conf` is a junction on Windows or a directory symlink elsewhere. Which `runtimes/<rid>/native` variant is loaded is decided by the .NET runtime from the whole copied directory; the tool never selects a CPU architecture and never targets 32-bit Windows (`win32` is Node's name for Windows on x64, x86 and arm64 alike — the architecture is `process.arch`). `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` is edition-scoped: **CN** offers `dashboard`, `reporting` and `data-monitoring`, **EN** additionally offers `sheets` and `documents` (smart documents, which pulls in the collaboration service it needs). Without `--port` the first free port in `51900`-`51980` is used; an explicit `--port` is used as given and fails on a conflict. `--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 installed 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. `--modules` is required — name `all` or one or more modules.
113
-
114
- **Every module can be refreshed, the core components included** — `server`, `monitoring`, `common`, `analysis`, `dashboard`, `reporting`, `data-monitoring`, `sheets`, `documents` and `collaboration` all go through the same path, and `all` covers them together (skipping the ones that have a local source). Refreshing `server` or `monitoring` is checked like a create: the package must ship the runtime assemblies and settings its service needs (the Server's required runtime files, and `Wyn.Configuration.dll` plus `appsettings.json` for the Monitor) and the `xsd/<schema>/Wyn.conf.xsd` for the configuration schema the environment already declares — a Monitor package without it is refused, a Server package without it only warns, because Wyn's configuration reader falls back to ordinary XML deserialization. Refresh the two in the same call when you can: they ship the schema assets for the same configuration, and a single-sided refresh is reported as a warning. The environment's own `Server\appsettings.json` is kept across the replacement (the Server package never deploys one), the Monitor's `conf` assets are merged into `conf\` and the `Monitor/conf` junction is recreated. A local source is refused for `server`, `monitoring` and `collaboration`: they are whole services, refreshed from artifacts only. `sheets` and `documents` still require an EN edition environment.
115
-
116
- A module can own several assemblies (`gces-common` = two plugins + one worker; `dashboard`/`reporting` = plugin + worker), so local sources resolve per assembly:
117
-
118
- | Spec | Selects |
119
- |---|---|
120
- | `dashboard=D:\build\Wyn.Dashboards.Plugin.dll` | the target whose file name matches |
121
- | `dashboard=D:\build\out` | every assembly of that module found in the directory |
122
- | `dashboard#Wyn.Dashboards.Worker.dll=D:\build\worker.dll` | that target explicitly |
123
- | repeated `common=` specs | one target each, as long as no target is selected twice |
124
-
125
- ```bash
126
- afm oss update-env demo --modules \
127
- common=D:\gces-common\src\Wyn.Dataset.Plugin\bin\release\net8.0\Wyn.Dataset.Plugin.dll \
128
- common=D:\gces-common\src\Wyn.Account.Plugin\bin\release\net8.0\Wyn.Account.Plugin.dll \
129
- common=D:\gces-common\src\Wyn.CotWorker\bin\release\net8.0\Wyn.CotWorker.dll
130
- ```
131
-
132
- 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.
133
-
134
- ## Apply a local build while starting
135
-
136
- ```bash
137
- afm env start demo --background --modules dashboard=D:\build\Wyn.Dashboards.Plugin.dll
138
- afm env restart demo --modules common=D:\build\common,common#Wyn.CotWorker.dll=D:\build\Wyn.CotWorker.dll
139
- ```
140
-
141
- 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.
142
-
143
- ## Local web interface
144
-
145
- ```bash
146
- afm web # 127.0.0.1, OS-assigned port, opens your browser
147
- afm web --port 4599 # fixed port
148
- afm web --token <secret> # reuse your own token instead of a generated one (>= 16 characters)
149
- afm web --no-open # print the URL only
150
- ```
151
-
152
- The page lists the registered environments — one row each, with its site link (`http://localhost:<port>`), a **复制** button for that address and a **打开目录** button that reveals the environment root in Explorer (Windows), Finder (macOS) or `xdg-open` (Linux) — and can create, start, stop, restart and delete them and update their modules. The create form and the update panel default to the **`oss`** provider, falling back to `az` only when OSS has no stored credentials, and the optional-module list follows the selected edition (CN: `dashboard`, `reporting`; EN: those plus `sheets` and `documents`). Creating and updating use the same code paths as `create-env`/`update-env`, including `--dry-run`. The 设置 dialog writes the same local profile the CLI uses, so a machine without a terminal session can still be prepared: the **environment root** (like `afm env root <path>`) and the **az/oss credentials** (like `afm az login` / `afm oss login`). Credentials are send-only — the page shows whether a provider is configured, never the SAS URL or the key.
153
-
154
- ```json
155
- {
156
- "host": "127.0.0.1",
157
- "port": 4599,
158
- "url": "http://127.0.0.1:4599/",
159
- "tokenUrl": "http://127.0.0.1:4599/#token=<token>",
160
- "token": "<token>"
161
- }
162
- ```
163
-
164
- * **Loopback only.** The listener binds `127.0.0.1`; there is no option to expose it.
165
- * **Token on every request.** The page reads the token from the URL fragment (which browsers never send to the server), keeps it in `sessionStorage` and sends it as `X-AFM-Token`. `Authorization: Bearer <token>` also works. Comparisons are constant-time.
166
- * **Host and origin checks.** A request whose `Host` header is not `127.0.0.1:<port>` or `localhost:<port>` is refused (DNS rebinding), and no CORS header is ever sent, so another page cannot read a reply or send the token header. The page is served with a strict `Content-Security-Policy`.
167
- * **Long operations are jobs.** Create/update/delete/start/stop/restart answer `202` with a job id; the page polls `GET /api/jobs/<id>`. Mutations of one environment are serialized (a second request waits as `queued`), and the first `Ctrl+C` waits for running jobs before stopping so an interrupted `create-env` does not leave a reservation behind.
168
- * **Start always uses background mode** from the page — a foreground monitor would own the terminal the web server runs in. Use the CLI when you want foreground output.
169
- * Deleting from the page requires typing the environment name; it stops the verified monitor and removes the directory and the registry record, exactly like `env delete`.
170
-
171
- ## Library
172
-
173
-
174
- ```ts
175
- import { ArtifactClient, buildEnvironmentPlan, executeEnvironmentPlan } from '@guanjinke/artifact-client';
176
-
177
- const client = new ArtifactClient('oss', { keyId: 'id', key: 'secret', bucket: 'my-bucket' });
178
-
179
- await client.listPlugins({ includeSize: true });
180
- await client.listMajorVersions('myPlugin', { sortOrder: 'asc' });
181
- await client.getLatestVersion('gces-server', '9.1');
182
- await client.listArtifacts('gces-server', '9.1.21042', { includeSize: true });
183
- await client.uploadArtifact('myPlugin', '1.0.6', 'bundle.zip', './dist/bundle.zip');
184
- await client.updateLatestVersion('myPlugin', '9.1-hotfix1', '1.0.6');
185
- await client.downloadArtifact('myPlugin', '1.0.5', 'bundle.zip', './downloads');
186
- await client.downloadArtifactByPath('plugin_artifacts/myPlugin/1.0.5/bundle.zip', './downloads');
187
- const files = await client.downloadVersion('myPlugin', '1.0.5', './downloads');
188
-
189
- // Plan resolutely (no downloads or writes), then assemble and register an environment
190
- const plan = await buildEnvironmentPlan({ root: 'D:/wyn/demo', major: '9.1', modules: ['dashboard'] }, client);
191
- const result = await executeEnvironmentPlan(plan, { client });
192
- ```
193
-
194
- | Method | Description |
195
- |---|---|
196
- | `listPlugins(opts?)` | List plugins (optionally with total size) |
197
- | `listMajorVersions(plugin, opts?)` | Majors with the version each one points at |
198
- | `getLatestVersion(plugin, major, opts?)` | Version a major currently points at |
199
- | `listMinorVersions(plugin, major, opts?)` | Versions under a major |
200
- | `listArtifacts(plugin, version, opts?)` | Files of a version (optionally with sizes) |
201
- | `uploadArtifact(plugin, version, name, localPath)` | Upload one file into a version directory |
202
- | `updateLatestVersion(plugin, major, version)` | Move a major's latest pointer |
203
- | `downloadArtifactByPath(fullPath, localDir)` | Download by full storage path |
204
- | `downloadArtifact(plugin, version, fileName, localDir)` | Download by name |
205
- | `downloadVersion(plugin, version, localDir)` | Download every file of a version |
206
-
207
- `client` is a small typed interface, so tests can inject a stub. Assembly, planning, listing and lifecycle all work on Windows, Linux and macOS: lifecycle uses the Windows backend on Windows and a POSIX backend (`/proc` on Linux, `ps` on macOS) elsewhere. Running an environment never needs administrator rights; only the process-identity check touches privileges, because Windows withholds the command line and executable path of a process owned by another account (a SYSTEM `WynService`, for instance). Such a process is attributed to an environment only when it carries that environment's recorded monitor PID, and it is reported in `afm env list` instead of blocking a start.
208
-
209
- ## Notes
210
-
211
- * Stopping is a forced process-tree termination, not a graceful HTTP shutdown.
212
- * A missing or unknown process identity blocks start/stop/delete instead of guessing.
213
- * Version resolution never selects a different archive by size: only the exact expected file name is used.
214
-
215
- Design details, safety semantics and development notes live in the repository.
216
-
217
- 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: `extras/X.dll` is written to `<out>/extras/X.dll`.
66
+
67
+ ## Local Wyn environments (Windows)
68
+
69
+ The artifacts and the tool are cross-platform: runtime packages ship the RID assets for every platform they support (`runtimes/{linux*, osx, unix, win*}`, with `libSkiaSharp.so`/`.dylib`/`.dll` per platform), and an assembled environment is configured for the host it is built on — `Wyn.conf` uses that platform's path separators, and `Monitor/conf` is a junction on Windows or a directory symlink elsewhere. Which `runtimes/<rid>/native` variant is loaded is decided by the .NET runtime from the whole copied directory; the tool never selects a CPU architecture and never targets 32-bit Windows (`win32` is Node's name for Windows on x64, x86 and arm64 alike — the architecture is `process.arch`). `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` is edition-scoped: **CN** offers `dashboard`, `reporting` and `data-monitoring`, **EN** additionally offers `sheets` and `documents` (smart documents, which pulls in the collaboration service it needs). Without `--port` the first free port in `51900`-`51980` is used; an explicit `--port` is used as given and fails on a conflict. `--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 installed 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** for plugins: 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. `--modules` is required — name `all` or one or more modules.
113
+
114
+ The **Server** is the exception: `--modules server=<build directory>` records a local Server build output, and `env start`/`env restart` copy it over the environment's `Server\` directory on every start (the environment's own `Server\appsettings.json` is kept). After the copy, the built-in payloads that live inside `Server\` are re-applied **from a local cache** — a start never downloads, and a missing cache entry is refused with the command that refreshes it. Because the copied build decides the runtime file versions, run `afm oss|az update-env <name> --modules all` once after a build whose dependencies changed: it refreshes the installed payloads from the recorded build directory and stores them in that cache. A remote `update-env <name> --modules server` deploys the published package again and clears the local source.
115
+
116
+ **Every module can be refreshed, the core components included** — `server`, `monitoring`, `common`, `analysis`, `dashboard`, `reporting`, `data-monitoring`, `sheets`, `documents` and `collaboration` all go through the same path, and `all` covers them together (skipping the ones that have a local source). Refreshing `server` or `monitoring` is checked like a create: the package must ship the runtime assemblies and settings its service needs (the Server's required runtime files, and `Wyn.Configuration.dll` plus `appsettings.json` for the Monitor) and the `xsd/<schema>/Wyn.conf.xsd` for the configuration schema the environment already declares — a Monitor package without it is refused, a Server package without it only warns, because Wyn's configuration reader falls back to ordinary XML deserialization. Refresh the two in the same call when you can: they ship the schema assets for the same configuration, and a single-sided refresh is reported as a warning. The environment's own `Server\appsettings.json` is kept across the replacement (the Server package never deploys one), the Monitor's `conf` assets are merged into `conf\` and the `Monitor/conf` junction is recreated. A local source is refused for `monitoring` and `collaboration`: they are whole services, refreshed from artifacts only. `sheets` and `documents` still require an EN edition environment.
117
+
118
+ A module can own several assemblies (`gces-common` = two plugins + one worker; `dashboard`/`reporting` = plugin + worker), so local sources resolve per assembly:
119
+
120
+ | Spec | Selects |
121
+ |---|---|
122
+ | `dashboard=D:\build\Wyn.Dashboards.Plugin.dll` | the target whose file name matches |
123
+ | `dashboard=D:\build\out` | every assembly of that module found in the directory |
124
+ | `dashboard#Wyn.Dashboards.Worker.dll=D:\build\worker.dll` | that target explicitly |
125
+ | repeated `common=` specs | one target each, as long as no target is selected twice |
126
+
127
+ ```bash
128
+ afm oss update-env demo --modules \
129
+ common=D:\gces-common\src\Wyn.Dataset.Plugin\bin\release\net8.0\Wyn.Dataset.Plugin.dll \
130
+ common=D:\gces-common\src\Wyn.Account.Plugin\bin\release\net8.0\Wyn.Account.Plugin.dll \
131
+ common=D:\gces-common\src\Wyn.CotWorker\bin\release\net8.0\Wyn.CotWorker.dll
132
+ ```
133
+
134
+ 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.
135
+
136
+ ## Apply a local build while starting
137
+
138
+ ```bash
139
+ afm env start demo --background --modules dashboard=D:\build\Wyn.Dashboards.Plugin.dll
140
+ afm env restart demo --modules common=D:\build\common,common#Wyn.CotWorker.dll=D:\build\Wyn.CotWorker.dll
141
+ ```
142
+
143
+ 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.
144
+
145
+ ## Local web interface
146
+
147
+ ```bash
148
+ afm web # 127.0.0.1, OS-assigned port, opens your browser
149
+ afm web --port 4599 # fixed port
150
+ afm web --token <secret> # reuse your own token instead of a generated one (>= 16 characters)
151
+ afm web --no-open # print the URL only
152
+ ```
153
+
154
+ The page lists the registered environments — one row each, with its site link (`http://localhost:<port>`), a **复制** button for that address and a **打开目录** button that reveals the environment root in Explorer (Windows), Finder (macOS) or `xdg-open` (Linux) — and can create, start, stop, restart and delete them and update their modules. The create form and the update panel default to the **`oss`** provider, falling back to `az` only when OSS has no stored credentials, and the optional-module list follows the selected edition (CN: `dashboard`, `reporting`; EN: those plus `sheets` and `documents`). Creating and updating use the same code paths as `create-env`/`update-env`, including `--dry-run`. The 设置 dialog writes the same local profile the CLI uses, so a machine without a terminal session can still be prepared: the **environment root** (like `afm env root <path>`) and the **az/oss credentials** (like `afm az login` / `afm oss login`). Credentials are send-only — the page shows whether a provider is configured, never the SAS URL or the key.
155
+
156
+ ```json
157
+ {
158
+ "host": "127.0.0.1",
159
+ "port": 4599,
160
+ "url": "http://127.0.0.1:4599/",
161
+ "tokenUrl": "http://127.0.0.1:4599/#token=<token>",
162
+ "token": "<token>"
163
+ }
164
+ ```
165
+
166
+ * **Loopback only.** The listener binds `127.0.0.1`; there is no option to expose it.
167
+ * **Token on every request.** The page reads the token from the URL fragment (which browsers never send to the server), keeps it in `sessionStorage` and sends it as `X-AFM-Token`. `Authorization: Bearer <token>` also works. Comparisons are constant-time.
168
+ * **Host and origin checks.** A request whose `Host` header is not `127.0.0.1:<port>` or `localhost:<port>` is refused (DNS rebinding), and no CORS header is ever sent, so another page cannot read a reply or send the token header. The page is served with a strict `Content-Security-Policy`.
169
+ * **Long operations are jobs.** Create/update/delete/start/stop/restart answer `202` with a job id; the page polls `GET /api/jobs/<id>`. Mutations of one environment are serialized (a second request waits as `queued`), and the first `Ctrl+C` waits for running jobs before stopping so an interrupted `create-env` does not leave a reservation behind.
170
+ * **Start always uses background mode** from the page — a foreground monitor would own the terminal the web server runs in. Use the CLI when you want foreground output.
171
+ * Deleting from the page requires typing the environment name; it stops the verified monitor and removes the directory and the registry record, exactly like `env delete`.
172
+
173
+ ## Library
174
+
175
+
176
+ ```ts
177
+ import { ArtifactClient, buildEnvironmentPlan, executeEnvironmentPlan } from '@guanjinke/artifact-client';
178
+
179
+ const client = new ArtifactClient('oss', { keyId: 'id', key: 'secret', bucket: 'my-bucket' });
180
+
181
+ await client.listPlugins({ includeSize: true });
182
+ await client.listMajorVersions('myPlugin', { sortOrder: 'asc' });
183
+ await client.getLatestVersion('gces-server', '9.1');
184
+ await client.listArtifacts('gces-server', '9.1.21042', { includeSize: true });
185
+ await client.uploadArtifact('myPlugin', '1.0.6', 'bundle.zip', './dist/bundle.zip');
186
+ await client.updateLatestVersion('myPlugin', '9.1-hotfix1', '1.0.6');
187
+ await client.downloadArtifact('myPlugin', '1.0.5', 'bundle.zip', './downloads');
188
+ await client.downloadArtifactByPath('plugin_artifacts/myPlugin/1.0.5/bundle.zip', './downloads');
189
+ const files = await client.downloadVersion('myPlugin', '1.0.5', './downloads');
190
+
191
+ // Plan resolutely (no downloads or writes), then assemble and register an environment
192
+ const plan = await buildEnvironmentPlan({ root: 'D:/wyn/demo', major: '9.1', modules: ['dashboard'] }, client);
193
+ const result = await executeEnvironmentPlan(plan, { client });
194
+ ```
195
+
196
+ | Method | Description |
197
+ |---|---|
198
+ | `listPlugins(opts?)` | List plugins (optionally with total size) |
199
+ | `listMajorVersions(plugin, opts?)` | Majors with the version each one points at |
200
+ | `getLatestVersion(plugin, major, opts?)` | Version a major currently points at |
201
+ | `listMinorVersions(plugin, major, opts?)` | Versions under a major |
202
+ | `listArtifacts(plugin, version, opts?)` | Files of a version (optionally with sizes) |
203
+ | `uploadArtifact(plugin, version, name, localPath)` | Upload one file into a version directory |
204
+ | `updateLatestVersion(plugin, major, version)` | Move a major's latest pointer |
205
+ | `downloadArtifactByPath(fullPath, localDir)` | Download by full storage path |
206
+ | `downloadArtifact(plugin, version, fileName, localDir)` | Download by name |
207
+ | `downloadVersion(plugin, version, localDir)` | Download every file of a version |
208
+
209
+ `client` is a small typed interface, so tests can inject a stub. Assembly, planning, listing and lifecycle all work on Windows, Linux and macOS: lifecycle uses the Windows backend on Windows and a POSIX backend (`/proc` on Linux, `ps` on macOS) elsewhere. Running an environment never needs administrator rights; only the process-identity check touches privileges, because Windows withholds the command line and executable path of a process owned by another account (a SYSTEM `WynService`, for instance). Such a process is attributed to an environment only when it carries that environment's recorded monitor PID, and it is reported in `afm env list` instead of blocking a start.
210
+
211
+ ## Notes
212
+
213
+ * Stopping is a forced process-tree termination, not a graceful HTTP shutdown.
214
+ * A missing or unknown process identity blocks start/stop/delete instead of guessing.
215
+ * Version resolution never selects a different archive by size: only the exact expected file name is used.
216
+
217
+ Design details, safety semantics and development notes live in the repository.
218
+
219
+ MIT
package/dist/cli.js CHANGED
File without changes
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Local cache of downloaded artifacts.
3
+ *
4
+ * A local Server build replaces `Server/` on every start, which throws away the
5
+ * built-in resource tree and the patched file. Re-applying them must not touch the
6
+ * network: the `env` command group has no cloud provider, and a start should not
7
+ * depend on the cloud being reachable. Create and update therefore store every
8
+ * artifact they download here, keyed by the provider-side identity, and the start
9
+ * path reads only from this cache.
10
+ *
11
+ * Layout: `<config directory>/cache/artifacts/<plugin>/<version>/<object name>`
12
+ * (object names may contain sub-directories, e.g. `patched/X.dll`), where the
13
+ * config directory is `AFM_CONFIG_DIR` or `~/.afm`. The provider and version are
14
+ * internal: nothing here is echoed to the user.
15
+ */
16
+ export declare function artifactCacheDirectory(): string;
17
+ /** Where one cached object lives; exported for tests and diagnostics. */
18
+ export declare function cachedArtifactPath(plugin: string, version: string, name: string): string;
19
+ /**
20
+ * A cached object, or `undefined` when it is absent, not a regular file, or does
21
+ * not match the recorded SHA256. A mismatching file is treated as absent: the next
22
+ * create or update overwrites it, and a start refuses instead of deploying bytes
23
+ * nobody verified.
24
+ */
25
+ export declare function cachedArtifact(plugin: string, version: string, name: string, sha256?: string): Promise<string | undefined>;
26
+ /**
27
+ * Store one downloaded artifact under its provider-side identity.
28
+ *
29
+ * Copies through a uniquely named `.afm-tmp` sibling and an atomic rename, so a
30
+ * concurrent reader never sees a partial object and a failed write leaves no
31
+ * temporary file behind. An existing object with the same hash is kept as it is.
32
+ */
33
+ export declare function storeArtifact(plugin: string, version: string, name: string, sourceFile: string, sha256: string): Promise<void>;
34
+ /** Best-effort variant: caching must never fail a create or an update. */
35
+ export declare function rememberArtifact(plugin: string, version: string, name: string, sourceFile: string, sha256: string): Promise<void>;
36
+ //# sourceMappingURL=artifact-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-cache.d.ts","sourceRoot":"","sources":["../../src/environment/artifact-cache.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAqBD,yEAAyE;AACzE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAExF;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAMhI;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYpI;AAED,0EAA0E;AAC1E,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvI"}