@zhin.js/gateway 0.0.1-bootstrap.0 → 1.1.1

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 (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/lib/config/ai-config-loader.d.ts +22 -0
  4. package/lib/config/ai-config-loader.js +96 -0
  5. package/lib/config/configuration-chat-policy.d.ts +57 -0
  6. package/lib/config/configuration-chat-policy.js +423 -0
  7. package/lib/config/configuration-patch.d.ts +16 -0
  8. package/lib/config/configuration-patch.js +105 -0
  9. package/lib/config/configuration-path.d.ts +6 -0
  10. package/lib/config/configuration-path.js +40 -0
  11. package/lib/config/configuration-projection.d.ts +6 -0
  12. package/lib/config/configuration-projection.js +28 -0
  13. package/lib/config/configuration-redaction.d.ts +24 -0
  14. package/lib/config/configuration-redaction.js +128 -0
  15. package/lib/config/configuration.d.ts +81 -0
  16. package/lib/config/configuration.js +686 -0
  17. package/lib/config/console-schema.d.ts +11 -0
  18. package/lib/config/console-schema.js +222 -0
  19. package/lib/config/environment-files.d.ts +2 -0
  20. package/lib/config/environment-files.js +21 -0
  21. package/lib/config/plugin-package-map.d.ts +2 -0
  22. package/lib/config/plugin-package-map.js +14 -0
  23. package/lib/config/plugin-schema-catalog.d.ts +22 -0
  24. package/lib/config/plugin-schema-catalog.js +262 -0
  25. package/lib/config.d.ts +12 -0
  26. package/lib/config.js +12 -0
  27. package/lib/database/config.d.ts +22 -0
  28. package/lib/database/config.js +159 -0
  29. package/lib/database/index.d.ts +2 -0
  30. package/lib/database/index.js +2 -0
  31. package/lib/database/project-database.d.ts +31 -0
  32. package/lib/database/project-database.js +130 -0
  33. package/lib/database.d.ts +2 -0
  34. package/lib/database.js +2 -0
  35. package/lib/http/console-rpc.d.ts +19 -0
  36. package/lib/http/console-rpc.js +51 -0
  37. package/lib/http/gateway-console.d.ts +63 -0
  38. package/lib/http/gateway-console.js +309 -0
  39. package/lib/http/gateway-events.d.ts +35 -0
  40. package/lib/http/gateway-events.js +172 -0
  41. package/lib/http/gateway-http-server.d.ts +95 -0
  42. package/lib/http/gateway-http-server.js +518 -0
  43. package/lib/http/local-principal.d.ts +2 -0
  44. package/lib/http/local-principal.js +16 -0
  45. package/lib/http/run-gateway.d.ts +43 -0
  46. package/lib/http/run-gateway.js +268 -0
  47. package/lib/http/start-gateway.d.ts +114 -0
  48. package/lib/http/start-gateway.js +220 -0
  49. package/lib/http-response.d.ts +6 -0
  50. package/lib/http-response.js +25 -0
  51. package/lib/http.d.ts +6 -0
  52. package/lib/http.js +6 -0
  53. package/lib/iam/crypto.d.ts +12 -0
  54. package/lib/iam/crypto.js +58 -0
  55. package/lib/iam/index.d.ts +8 -0
  56. package/lib/iam/index.js +8 -0
  57. package/lib/iam/install.d.ts +36 -0
  58. package/lib/iam/install.js +74 -0
  59. package/lib/iam/local-admin.d.ts +27 -0
  60. package/lib/iam/local-admin.js +102 -0
  61. package/lib/iam/rate-limit.d.ts +15 -0
  62. package/lib/iam/rate-limit.js +52 -0
  63. package/lib/iam/routes.d.ts +23 -0
  64. package/lib/iam/routes.js +157 -0
  65. package/lib/iam/rpc.d.ts +14 -0
  66. package/lib/iam/rpc.js +100 -0
  67. package/lib/iam/service.d.ts +203 -0
  68. package/lib/iam/service.js +784 -0
  69. package/lib/iam/setup-service.d.ts +83 -0
  70. package/lib/iam/setup-service.js +206 -0
  71. package/lib/iam/tables.d.ts +20 -0
  72. package/lib/iam/tables.js +82 -0
  73. package/lib/iam.d.ts +2 -0
  74. package/lib/iam.js +2 -0
  75. package/lib/index.d.ts +22 -0
  76. package/lib/index.js +22 -0
  77. package/lib/local/client.d.ts +71 -0
  78. package/lib/local/client.js +296 -0
  79. package/lib/local/handler.d.ts +30 -0
  80. package/lib/local/handler.js +86 -0
  81. package/lib/local/local-socket.d.ts +55 -0
  82. package/lib/local/local-socket.js +232 -0
  83. package/lib/local/paths.d.ts +34 -0
  84. package/lib/local/paths.js +58 -0
  85. package/lib/local/protocol.d.ts +140 -0
  86. package/lib/local/protocol.js +90 -0
  87. package/lib/local/state-file.d.ts +8 -0
  88. package/lib/local/state-file.js +45 -0
  89. package/lib/local.d.ts +10 -0
  90. package/lib/local.js +10 -0
  91. package/lib/onboarding/allowlist.d.ts +54 -0
  92. package/lib/onboarding/allowlist.js +106 -0
  93. package/lib/onboarding/errors.d.ts +7 -0
  94. package/lib/onboarding/errors.js +17 -0
  95. package/lib/onboarding/index.d.ts +6 -0
  96. package/lib/onboarding/index.js +6 -0
  97. package/lib/onboarding/install.d.ts +99 -0
  98. package/lib/onboarding/install.js +132 -0
  99. package/lib/onboarding/package-manager.d.ts +47 -0
  100. package/lib/onboarding/package-manager.js +179 -0
  101. package/lib/onboarding/service.d.ts +53 -0
  102. package/lib/onboarding/service.js +144 -0
  103. package/lib/onboarding/state-store.d.ts +26 -0
  104. package/lib/onboarding/state-store.js +97 -0
  105. package/lib/onboarding.d.ts +2 -0
  106. package/lib/onboarding.js +2 -0
  107. package/lib/packages/authorization.d.ts +23 -0
  108. package/lib/packages/authorization.js +109 -0
  109. package/lib/packages/errors.d.ts +11 -0
  110. package/lib/packages/errors.js +26 -0
  111. package/lib/packages/index.d.ts +5 -0
  112. package/lib/packages/index.js +5 -0
  113. package/lib/packages/rpc.d.ts +30 -0
  114. package/lib/packages/rpc.js +155 -0
  115. package/lib/packages/service.d.ts +121 -0
  116. package/lib/packages/service.js +441 -0
  117. package/lib/packages/snapshot.d.ts +14 -0
  118. package/lib/packages/snapshot.js +73 -0
  119. package/lib/packages.d.ts +2 -0
  120. package/lib/packages.js +2 -0
  121. package/lib/plugin-management/activity.d.ts +6 -0
  122. package/lib/plugin-management/activity.js +14 -0
  123. package/lib/plugin-management/config-explanation.d.ts +6 -0
  124. package/lib/plugin-management/config-explanation.js +113 -0
  125. package/lib/plugin-management/use-cases.d.ts +15 -0
  126. package/lib/plugin-management/use-cases.js +120 -0
  127. package/lib/plugin-management/workflow.d.ts +92 -0
  128. package/lib/plugin-management/workflow.js +772 -0
  129. package/lib/plugin-management.d.ts +5 -0
  130. package/lib/plugin-management.js +5 -0
  131. package/lib/project/config-file.d.ts +6 -0
  132. package/lib/project/config-file.js +58 -0
  133. package/lib/project/environment.d.ts +13 -0
  134. package/lib/project/environment.js +52 -0
  135. package/lib/project/instance-declarations.d.ts +55 -0
  136. package/lib/project/instance-declarations.js +542 -0
  137. package/lib/project/legacy-config.d.ts +25 -0
  138. package/lib/project/legacy-config.js +70 -0
  139. package/lib/project/node-requirements.d.ts +7 -0
  140. package/lib/project/node-requirements.js +20 -0
  141. package/lib/project/tsx-module-loader.d.ts +7 -0
  142. package/lib/project/tsx-module-loader.js +29 -0
  143. package/lib/project.d.ts +7 -0
  144. package/lib/project.js +7 -0
  145. package/lib/server.d.ts +11 -0
  146. package/lib/server.js +11 -0
  147. package/lib/supervisor/instance-logs.d.ts +39 -0
  148. package/lib/supervisor/instance-logs.js +152 -0
  149. package/lib/supervisor/instance-supervisor.d.ts +140 -0
  150. package/lib/supervisor/instance-supervisor.js +408 -0
  151. package/lib/supervisor/run-instance.d.ts +35 -0
  152. package/lib/supervisor/run-instance.js +161 -0
  153. package/lib/supervisor/terminal-state.d.ts +30 -0
  154. package/lib/supervisor/terminal-state.js +57 -0
  155. package/lib/supervisor.d.ts +4 -0
  156. package/lib/supervisor.js +4 -0
  157. package/lib/terminal/backends.d.ts +38 -0
  158. package/lib/terminal/backends.js +193 -0
  159. package/lib/terminal/config.d.ts +63 -0
  160. package/lib/terminal/config.js +237 -0
  161. package/lib/terminal/host.d.ts +26 -0
  162. package/lib/terminal/host.js +76 -0
  163. package/lib/terminal/index.d.ts +60 -0
  164. package/lib/terminal/index.js +135 -0
  165. package/lib/terminal/modules.d.ts +87 -0
  166. package/lib/terminal/modules.js +44 -0
  167. package/lib/terminal/service.d.ts +101 -0
  168. package/lib/terminal/service.js +455 -0
  169. package/lib/terminal/tickets.d.ts +46 -0
  170. package/lib/terminal/tickets.js +76 -0
  171. package/lib/terminal.d.ts +3 -0
  172. package/lib/terminal.js +3 -0
  173. package/lib/usage/aggregate.d.ts +87 -0
  174. package/lib/usage/aggregate.js +212 -0
  175. package/lib/usage.d.ts +2 -0
  176. package/lib/usage.js +2 -0
  177. package/package.json +117 -4
@@ -0,0 +1,772 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { mkdir, open, readFile, unlink, access } from 'node:fs/promises';
4
+ import { join, resolve } from 'node:path';
5
+ import { promisify, isDeepStrictEqual } from 'node:util';
6
+ import { loadProjectConfig, packageToInstanceKey } from '@zhin.js/scaffold-wizard';
7
+ import { createConfigDocument, HOST_CONFIG_KEYS, JsonConfigDocument, NodePackageResolver, PackageResolutionError, ProjectGraphService, primaryConfigToken, parsePluginConfigSchema, envStoreToken, runtimeEnvironmentToken, readApplicationPluginDeclaration } from '@zhin.js/runtime';
8
+ import { featureId, pluginVerificationToken, rootPluginId, readPluginConfigurationMap, } from '@zhin.js/plugin-runtime';
9
+ import { ConsoleConfigurationStore } from '../config/configuration.js';
10
+ import { explainConfigFields } from './config-explanation.js';
11
+ import { resolveCommand } from '../onboarding/package-manager.js';
12
+ import { readPluginLifecycleState, resolvePluginLifecycleFile } from '@zhin.js/runtime';
13
+ const execute = promisify(execFile);
14
+ // Narrow local view of the `@zhin.js/adapter` index projection: the gateway must not import IM layers (ADR 0054 §0).
15
+ const adapterFeatureId = featureId('zhin.adapter');
16
+ function isAdapterIndex(value) {
17
+ return !!value && typeof value === 'object'
18
+ && value.$projection === 'zhin.adapter-index/1';
19
+ }
20
+ /** A declaration refused before anything was written (invalid request or configuration). */
21
+ export class PluginDeclarationError extends Error {
22
+ errors;
23
+ missingEnv;
24
+ constructor(message, errors = [], missingEnv = []) {
25
+ super(message);
26
+ this.name = 'PluginDeclarationError';
27
+ this.errors = Object.freeze(errors.map(error => Object.freeze({ ...error })));
28
+ this.missingEnv = Object.freeze([...missingEnv]);
29
+ }
30
+ }
31
+ /** One installation authority for CLI and Console. No generation or package parser of its own. */
32
+ export class PluginManagementWorkflow {
33
+ #root;
34
+ #options;
35
+ #verificationIncarnation = randomUUID();
36
+ constructor(projectRoot, options = {}) {
37
+ this.#root = resolve(projectRoot);
38
+ this.#options = options;
39
+ }
40
+ async #project() {
41
+ const document = new JsonConfigDocument(join(this.#root, 'package.json'));
42
+ const source = await document.read();
43
+ return { document, source, project: source.document };
44
+ }
45
+ #configuration(snapshot) {
46
+ const loaded = loadProjectConfig(this.#root);
47
+ if (loaded.status === 'missing')
48
+ return undefined;
49
+ if (loaded.status !== 'loaded' || !loaded.configPath)
50
+ throw new Error(loaded.message ?? 'Root configuration must use YAML or JSON');
51
+ const document = createConfigDocument(loaded.configPath);
52
+ return { document, store: this.#configurationStore(document, snapshot) };
53
+ }
54
+ #configurationStore(document, snapshot) {
55
+ // inspect already holds this lease. Other callers continue to resolve a fresh
56
+ // generation per operation, without maintaining a parallel runtime view.
57
+ const acquire = () => snapshot ? { value: snapshot, release() { } } : this.#options.snapshots?.acquire();
58
+ return new ConsoleConfigurationStore({ projectRoot: this.#root, document,
59
+ environmentName: () => {
60
+ const lease = acquire();
61
+ try {
62
+ return lease?.value.resources.get(rootPluginId())?.get(runtimeEnvironmentToken.id)?.name ?? (process.env.NODE_ENV === 'production' ? 'production' : 'development');
63
+ }
64
+ finally {
65
+ lease?.release();
66
+ }
67
+ },
68
+ environmentForPlugin: instanceKey => {
69
+ const lease = acquire();
70
+ try {
71
+ const node = [...(lease?.value.tree.values() ?? [])].find(item => item.parent === rootPluginId() && item.instanceKey === instanceKey);
72
+ return node ? lease?.value.resources.get(node.id)?.get(envStoreToken.id) : undefined;
73
+ }
74
+ finally {
75
+ lease?.release();
76
+ }
77
+ },
78
+ configDeclarations: async () => {
79
+ const lease = acquire();
80
+ try {
81
+ const nodes = [...(lease?.value.tree.values() ?? [])].filter(node => node.configSchema && node.packageVersion);
82
+ if (!nodes.length)
83
+ return new Map();
84
+ const resolver = await NodePackageResolver.create(this.#root);
85
+ const current = await Promise.all(nodes.map(async (node) => {
86
+ const installed = await resolver.loadPackage(node.packageRoot);
87
+ return installed.name === node.packageName && installed.packageJson.version === node.packageVersion
88
+ ? [[node.packageRoot, node.configSchema]] : [];
89
+ }));
90
+ return new Map(current.flat());
91
+ }
92
+ finally {
93
+ lease?.release();
94
+ }
95
+ },
96
+ });
97
+ }
98
+ async #declaredEntries() {
99
+ const configuration = this.#configuration();
100
+ const config = configuration ? (await configuration.document.read()).document : {};
101
+ const declaration = readApplicationPluginDeclaration(config);
102
+ if ((await this.#project()).project.zhin?.plugins?.length)
103
+ throw new Error('Legacy package.json zhin.plugins detected; run zhin migrate instances');
104
+ return declaration.instances;
105
+ }
106
+ async #resolveTarget(request, declaredEntries) {
107
+ const entries = declaredEntries ?? await this.#declaredEntries();
108
+ if (request.startsWith('root/'))
109
+ request = request.slice(5);
110
+ const direct = entries.find(entry => entry.instanceKey === request);
111
+ if (direct)
112
+ return { packageName: direct.package, instanceKey: direct.instanceKey };
113
+ const matches = entries.filter(entry => entry.package === request);
114
+ if (matches.length > 1)
115
+ throw new Error('此包存在多个插件实例,请使用精确 instanceKey');
116
+ if (matches.length === 1)
117
+ return { packageName: matches[0].package, instanceKey: matches[0].instanceKey };
118
+ const spec = npmSpec(request);
119
+ return { packageName: spec.name, instanceKey: packageToInstanceKey(spec.name) };
120
+ }
121
+ async #installed(packageName) {
122
+ let pkg;
123
+ let resolutionError;
124
+ try {
125
+ const resolver = await NodePackageResolver.create(this.#root);
126
+ const root = await resolver.root(this.#root);
127
+ pkg = await resolver.resolve(packageName, root);
128
+ }
129
+ catch (error) {
130
+ resolutionError = error;
131
+ }
132
+ if (pkg) {
133
+ try {
134
+ if (pkg.packageJson.zhin.type !== 'plugin')
135
+ throw new Error(`${packageName} is a Feature package, not a Plugin`);
136
+ await access(join(pkg.root, pkg.packageJson.zhin.entry));
137
+ return { installed: true, plugin: true, version: pkg.packageJson.version ?? null, error: undefined, pkg };
138
+ }
139
+ catch (error) {
140
+ return { installed: true, plugin: false, version: pkg.packageJson.version ?? null, error: errorMessage(error), pkg };
141
+ }
142
+ }
143
+ // Undeclared npm packages/ordinary dependencies still need installation evidence.
144
+ // Never guess a local/workspace directory: those are owned solely by the Runtime resolver.
145
+ if (packageName.startsWith('./'))
146
+ return { installed: false, plugin: false, version: null, error: errorMessage(resolutionError) };
147
+ const directory = join(this.#root, 'node_modules', ...packageName.split('/'));
148
+ let raw;
149
+ try {
150
+ raw = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
151
+ }
152
+ catch (error) {
153
+ if (notFound(error))
154
+ return { installed: false, plugin: false, version: null, error: undefined };
155
+ throw error;
156
+ }
157
+ if (raw.name !== packageName)
158
+ throw new Error(`Installed package name does not match ${packageName}`);
159
+ const version = typeof raw.version === 'string' ? raw.version : null;
160
+ if (!raw.zhin)
161
+ return { installed: true, plugin: false, version, error: undefined };
162
+ try {
163
+ const resolver = await NodePackageResolver.create(this.#root);
164
+ const installedPackage = await resolver.loadPackage(directory);
165
+ if (installedPackage.packageJson.zhin.type !== 'plugin')
166
+ throw new Error(`${packageName} is a Feature package, not a Plugin`);
167
+ await access(join(installedPackage.root, installedPackage.packageJson.zhin.entry));
168
+ const declarationMissing = resolutionError instanceof PackageResolutionError && resolutionError.message.includes('does not declare');
169
+ return { installed: true, plugin: true, version, error: declarationMissing ? errorMessage(resolutionError) : undefined, pkg: installedPackage };
170
+ }
171
+ catch (error) {
172
+ return { installed: true, plugin: false, version, error: errorMessage(error) };
173
+ }
174
+ }
175
+ async #checkDependencies(candidate, capturedDocument) {
176
+ const resolver = await NodePackageResolver.create(this.#root);
177
+ const root = await resolver.root(this.#root);
178
+ if (root.packageJson.zhin.type !== 'plugin')
179
+ throw new Error('Project root is not a Plugin');
180
+ const configuration = capturedDocument ? undefined : this.#configuration();
181
+ const document = capturedDocument ?? (configuration ? (await configuration.document.read()).document : {});
182
+ readApplicationPluginDeclaration(document);
183
+ const next = candidate ? { ...document, plugins: { ...document.plugins, [candidate.instanceKey]: candidate.packageName } } : document;
184
+ readApplicationPluginDeclaration(next);
185
+ await new ProjectGraphService(resolver).inspect(this.#root, next);
186
+ }
187
+ async #revisions() {
188
+ const { source } = await this.#project();
189
+ const config = this.#configuration();
190
+ const revision = config ? (await config.document.read()).revision : null;
191
+ return { revision, projectRevision: createHash('sha256').update(JSON.stringify([source.revision, revision])).digest('hex') };
192
+ }
193
+ async inspect(request) {
194
+ const lease = this.#options.snapshots?.acquire();
195
+ try {
196
+ return await this.#inspect(request, lease?.value);
197
+ }
198
+ finally {
199
+ lease?.release();
200
+ }
201
+ }
202
+ async #inspect(request, snapshot) {
203
+ const { configuration, source, document, declaration, projectRevision } = await this.#configurationObservation(snapshot);
204
+ const entries = declaration.instances;
205
+ const target = await this.#resolveTarget(request, entries);
206
+ const installed = await this.#installed(target.packageName);
207
+ const declared = entries.some(entry => entry.package === target.packageName && entry.instanceKey === target.instanceKey);
208
+ const config = readPluginConfigurationMap(document);
209
+ const revision = source.revision;
210
+ const issues = installed.error ? [installed.error] : [];
211
+ if (declared && !installed.installed)
212
+ issues.push('Declared plugin package is not installed');
213
+ let dependencies = 'unchecked';
214
+ let validation = null;
215
+ const legacy = await readPluginLifecycleState(this.#options.lifecycleFile ?? resolvePluginLifecycleFile(this.#root));
216
+ if (legacy.disabled.length)
217
+ throw new Error('Legacy plugin lifecycle state detected; run zhin migrate instances');
218
+ const disabled = declaration.disabled.includes(target.instanceKey);
219
+ if (installed.plugin && declared && !disabled) {
220
+ try {
221
+ await this.#checkDependencies(undefined, document);
222
+ dependencies = 'passed';
223
+ }
224
+ catch (error) {
225
+ dependencies = 'failed';
226
+ issues.push(errorMessage(error));
227
+ }
228
+ validation = await configuration.store.validatePluginConfig(target.instanceKey, config[target.instanceKey] ?? {}, document);
229
+ }
230
+ let node;
231
+ let generation = null;
232
+ let published = false;
233
+ let readiness = 'unknown';
234
+ let verification = 'not-configured';
235
+ {
236
+ generation = snapshot?.generation ?? null;
237
+ node = snapshot && [...snapshot.tree.values()].find(item => item.parent === rootPluginId() && item.instanceKey === target.instanceKey && item.packageName === (installed.pkg?.name ?? target.packageName));
238
+ if (node && snapshot) {
239
+ const owner = node.id;
240
+ const primary = snapshot.resources.get(rootPluginId())?.get(primaryConfigToken.id);
241
+ const applied = primary ? readPluginConfigurationMap(primary.document) : undefined;
242
+ const candidate = node.configSchema ? parsePluginConfigSchema(node.configSchema, config[target.instanceKey]).value : config[target.instanceKey] ?? {};
243
+ published = Boolean(installed.version && node.packageVersion === installed.version && applied && isDeepStrictEqual(applied[target.instanceKey] ?? {}, candidate));
244
+ if (published) {
245
+ const hasAdapters = [...snapshot.capabilities.values()].some(slot => slot.owner === owner && String(slot.feature) === 'zhin.adapter');
246
+ if (!hasAdapters)
247
+ readiness = 'ready';
248
+ else {
249
+ const adapters = snapshot.projections.get(adapterFeatureId);
250
+ if (isAdapterIndex(adapters)) {
251
+ const endpoints = adapters.describe().filter(endpoint => endpoint.owner === owner);
252
+ if (endpoints.length)
253
+ readiness = endpoints.every(endpoint => endpoint.connected && endpoint.admitted)
254
+ ? 'ready' : endpoints.some(endpoint => endpoint.transportState === 'closed' || endpoint.transportState === 'stopped')
255
+ ? 'degraded' : endpoints.some(endpoint => endpoint.transportState === 'unknown') ? 'unknown' : 'pending';
256
+ }
257
+ }
258
+ }
259
+ if (snapshot.resources.get(node.id)?.has(pluginVerificationToken.id))
260
+ verification = 'available';
261
+ }
262
+ }
263
+ // Publication compares this file's default-completed semantics with the
264
+ // generation authority; it does not attest an applied raw-file revision.
265
+ const missingConfig = declared && (validation?.valid === false || Boolean(validation?.missingEnv.length));
266
+ const restartRequired = declared && (disabled ? Boolean(node) : !published || dependencies !== 'passed');
267
+ const status = issues.length ? 'degraded'
268
+ : missingConfig ? 'missing-config'
269
+ : published && !disabled ? 'enabled'
270
+ : restartRequired ? 'pending-restart'
271
+ : declared ? 'declared' : installed.installed ? 'installed' : 'not-installed';
272
+ await this.#assertConfigurationObservation(projectRevision, snapshot);
273
+ return Object.freeze({ ...target, revision, projectRevision, installed: installed.installed, plugin: installed.plugin, version: installed.version, declared, status, dependencies, validation, generation, published, readiness, restartRequired, disabled, issues: Object.freeze(issues), verification });
274
+ }
275
+ async #configurationObservation(snapshot) {
276
+ const project = await this.#project();
277
+ const configuration = this.#configuration(snapshot);
278
+ if (!configuration)
279
+ throw new Error('Missing configVersion: 2 configuration; run zhin setup');
280
+ const source = await configuration.document.read();
281
+ const document = source.document;
282
+ const declaration = readApplicationPluginDeclaration(document);
283
+ if (project.project.zhin?.plugins?.length)
284
+ throw new Error('Legacy package.json zhin.plugins detected; run zhin migrate instances');
285
+ const projectRevision = createHash('sha256').update(JSON.stringify([project.source.revision, source.revision])).digest('hex');
286
+ return { project, configuration, source, document, declaration, projectRevision };
287
+ }
288
+ async #assertConfigurationObservation(projectRevision, snapshot) {
289
+ if ((await this.#revisions()).projectRevision !== projectRevision)
290
+ throw new Error('配置或项目版本在读取期间变化,请重新读取应用证据');
291
+ const current = this.#options.snapshots?.acquire();
292
+ try {
293
+ if (current?.value !== snapshot)
294
+ throw new Error('运行代在读取期间变化,请重新读取应用证据');
295
+ }
296
+ finally {
297
+ current?.release();
298
+ }
299
+ }
300
+ /** Full-scope, metadata-only projection over the same file and generation observation as inspect. */
301
+ async explainConfiguration(instanceKey, expectedRevision) {
302
+ if (typeof instanceKey !== 'string' || instanceKey.length > 128 || !/^[a-z0-9][a-z0-9-]*$/u.test(instanceKey)
303
+ || typeof expectedRevision !== 'string' || !/^[a-f0-9]{64}$/u.test(expectedRevision))
304
+ throw new Error('请提供精确实例键及已读取的文件版本');
305
+ const lease = this.#options.snapshots?.acquire();
306
+ try {
307
+ const snapshot = lease?.value;
308
+ const { source, document, declaration, projectRevision } = await this.#configurationObservation(snapshot);
309
+ if (source.revision !== expectedRevision)
310
+ throw new Error('配置文件版本已变化,请刷新后重新读取解释');
311
+ const base = { instanceKey, revision: source.revision, projectRevision, generation: snapshot?.generation ?? null };
312
+ const unsupported = (reason) => ({ ...base, projection: 'unsupported', reason, fields: [] });
313
+ const entry = declaration.instances.find(item => item.instanceKey === instanceKey);
314
+ const node = snapshot && [...snapshot.tree.values()].find(item => item.parent === snapshot.root && item.instanceKey === instanceKey);
315
+ let result;
316
+ if (!entry || !node || declaration.disabled.includes(instanceKey))
317
+ result = unsupported('instance-unavailable');
318
+ else {
319
+ const installed = await this.#installed(entry.package);
320
+ const primary = snapshot?.resources.get(snapshot.root)?.get(primaryConfigToken.id);
321
+ if (!installed.plugin || !installed.pkg || installed.pkg.root !== node.packageRoot || installed.pkg.name !== node.packageName
322
+ || !installed.version || installed.version !== node.packageVersion)
323
+ result = unsupported('schema-drift');
324
+ else if (!node.configSchema)
325
+ result = unsupported('schema-unavailable');
326
+ else if (primary?.compositionMode !== 'document')
327
+ result = unsupported('custom-resolver');
328
+ else
329
+ result = { ...base, ...explainConfigFields(node.configSchema, readPluginConfigurationMap(document)[instanceKey], snapshot?.config.get(node.id), readPluginConfigurationMap(primary.document)[instanceKey]) };
330
+ }
331
+ await this.#assertConfigurationObservation(projectRevision, snapshot);
332
+ return Object.freeze(result);
333
+ }
334
+ catch {
335
+ throw new Error('配置解释未确认,请重新读取文件版本与当前运行状态;原配置未改动。');
336
+ }
337
+ finally {
338
+ lease?.release();
339
+ }
340
+ }
341
+ async planInstall(request, options = {}) {
342
+ const spec = npmSpec(request);
343
+ const state = await this.inspect(spec.name);
344
+ if (state.declared && state.packageName !== spec.name)
345
+ throw new Error(`Instance ${state.instanceKey} already references ${state.packageName}; install by the package name or edit its declaration explicitly`);
346
+ const config = this.#configuration();
347
+ const document = config ? await config.store.readDocument() : {};
348
+ const enable = options.enable !== false;
349
+ const raw = config ? (await config.document.read()).document : {};
350
+ const v2 = readApplicationPluginDeclaration(raw).version === 2;
351
+ if (v2 && enable)
352
+ readApplicationPluginDeclaration({ ...raw, plugins: { ...raw.plugins, [state.instanceKey]: spec.name } });
353
+ return Object.freeze({ packageName: spec.name, instanceKey: state.instanceKey, projectRevision: state.projectRevision, configurationRevision: state.revision, alreadyInstalled: state.installed, alreadyDeclared: state.declared, restartRequired: enable ? !state.declared || state.restartRequired : state.declared && state.restartRequired, changes: Object.freeze({ packageManifest: v2 || !enable || state.declared ? 'unchanged' : 'add-plugin', config: !enable ? 'unchanged' : !config ? 'schema-required' : Object.hasOwn(document, state.instanceKey) ? 'unchanged' : 'create-entry' }), warnings: Object.freeze([...state.issues, ...(enable && !config ? ['Root YAML/JSON 配置文件缺失,请先运行 zhin setup'] : []), '安装/声明不表示业务验证通过;配置就绪后显式 apply 和 verify']) });
354
+ }
355
+ async #assertRevision(expectedRevision, expectedProjectRevision) {
356
+ const current = await this.#revisions();
357
+ if (expectedRevision !== undefined && current.revision !== expectedRevision || expectedProjectRevision !== undefined && current.projectRevision !== expectedProjectRevision)
358
+ throw new Error('项目或配置已被其他会话修改,请刷新后重试');
359
+ return current;
360
+ }
361
+ async #mutate(operation, request) {
362
+ return this.#options.activity
363
+ ? this.#options.activity.run(() => this.#mutateLocked(operation, request))
364
+ : this.#mutateLocked(operation, request);
365
+ }
366
+ async #mutateLocked(operation, request) {
367
+ const directory = join(this.#root, '.zhin');
368
+ await mkdir(directory, { recursive: true });
369
+ const file = join(directory, 'plugin-management.lock');
370
+ let handle;
371
+ try {
372
+ handle = await open(file, 'wx', 0o600);
373
+ }
374
+ catch (error) {
375
+ if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST'))
376
+ throw error;
377
+ let detail = '另一进程正在管理插件';
378
+ try {
379
+ const previous = JSON.parse(await readFile(file, 'utf8'));
380
+ try {
381
+ process.kill(previous.pid, 0);
382
+ }
383
+ catch (probe) {
384
+ if (probe instanceof Error && 'code' in probe && probe.code === 'ESRCH')
385
+ detail = `上一次插件管理进程 ${previous.pid} 已退出;确认没有安装进程后删除 ${file} 再重试`;
386
+ }
387
+ }
388
+ catch { /* An incomplete lock is not safe to steal from its writer. */ }
389
+ throw new Error(detail, { cause: error });
390
+ }
391
+ const nonce = randomUUID();
392
+ const completed = [];
393
+ try {
394
+ await handle.writeFile(JSON.stringify({ pid: process.pid, nonce }));
395
+ return await operation(completed);
396
+ }
397
+ catch (error) {
398
+ const state = request ? await this.inspect(request).catch(() => undefined) : undefined;
399
+ throw new PluginManagementOperationError(error, completed, state);
400
+ }
401
+ finally {
402
+ await handle.close();
403
+ const current = await readFile(file, 'utf8').catch(() => '');
404
+ if (current === JSON.stringify({ pid: process.pid, nonce }))
405
+ await unlink(file);
406
+ }
407
+ }
408
+ async #pnpm(args) {
409
+ const workspace = await access(join(this.#root, 'pnpm-workspace.yaml')).then(() => true, error => { if (notFound(error))
410
+ return false; throw error; });
411
+ const invocation = [args[0], ...(workspace ? ['-w'] : []), ...args.slice(1)];
412
+ if (this.#options.installDependencies)
413
+ await this.#options.installDependencies(invocation, this.#root);
414
+ else {
415
+ // No shell: on Windows `pnpm.cmd` is resolved and run through `cmd.exe /d /s /c` with quoted arguments.
416
+ const resolved = resolveCommand('pnpm', invocation);
417
+ await execute(resolved.command, [...resolved.args], { cwd: this.#root, maxBuffer: 10 * 1024 * 1024, windowsHide: true,
418
+ ...(resolved.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}) });
419
+ }
420
+ }
421
+ async install(request, expectedRevision, expectedProjectRevision, options = {}) {
422
+ if (options.saveDev && options.enable !== false)
423
+ throw new Error('--save-dev cannot enable a Runtime plugin; use --no-enable or install as a production dependency');
424
+ const spec = npmSpec(request);
425
+ return this.#mutate(async (completed) => {
426
+ const previous = await this.#assertRevision(expectedRevision, expectedProjectRevision);
427
+ const plan = await this.planInstall(request, options);
428
+ const existing = await this.#installed(spec.name);
429
+ const { project: installedProject } = await this.#project();
430
+ // An exact installed artifact can finish declaration/configuration after
431
+ // a lost receipt without running the package manager again. Tags/ranges,
432
+ // alternate sources and dependency-tier changes still need installation.
433
+ // The declared range must already be the requested exact pin; otherwise
434
+ // skipping would leave package.json at an older/looser range.
435
+ if (options.source || options.saveDev || installedProject.dependencies?.[spec.name] !== spec.version
436
+ || !spec.version || !existing.installed
437
+ || existing.error || existing.version !== spec.version) {
438
+ await this.#pnpm(['add', ...(options.saveDev ? ['-D'] : []), options.source ?? spec.spec]);
439
+ }
440
+ completed.push('dependency-install');
441
+ const installed = await this.#installed(spec.name);
442
+ if (!installed.installed)
443
+ throw new Error('Package manager completed but the installed package is unavailable');
444
+ if (installed.error)
445
+ throw new Error(installed.error);
446
+ let needsConfiguration;
447
+ if (options.enable !== false && installed.plugin) {
448
+ if (options.saveDev)
449
+ throw new Error('Runtime plugins must be production dependencies; use installation without --save-dev');
450
+ if ((await this.#revisions()).revision !== previous.revision)
451
+ throw new Error('Root config changed during installation');
452
+ const target = { packageName: spec.name, instanceKey: plan.instanceKey };
453
+ await this.#checkDependencies(target);
454
+ const configuration = this.#configuration();
455
+ if (!configuration)
456
+ throw new Error('Missing configVersion: 2 configuration; run zhin setup');
457
+ const currentConfig = await configuration.document.read();
458
+ readApplicationPluginDeclaration(currentConfig.document);
459
+ if (previous.revision && currentConfig.revision !== previous.revision)
460
+ throw new Error('Root config changed during installation');
461
+ const entries = currentConfig.document.plugins;
462
+ if (Object.hasOwn(entries, target.instanceKey) && entries[target.instanceKey] !== target.packageName)
463
+ throw new Error(`Plugin instanceKey collision: ${target.instanceKey}`);
464
+ if (!Object.hasOwn(entries, target.instanceKey)) {
465
+ // An undeclared instance has no section yet (the declaration reader refuses one): the
466
+ // instance would load `{}` completed by the schema defaults. Required fields without
467
+ // defaults keep the plugin undeclared until it is declared with its configuration.
468
+ const candidate = { ...currentConfig.document, plugins: { ...entries, [target.instanceKey]: target.packageName } };
469
+ const validation = await configuration.store.validatePluginConfig(target.instanceKey, {}, candidate);
470
+ if (!validation.valid || validation.missingEnv.length) {
471
+ needsConfiguration = Object.freeze({ name: target.packageName, instanceKey: target.instanceKey,
472
+ missing: Object.freeze([...new Set(validation.errors.map(error => relativeConfigPath(error.path)))]),
473
+ missingEnv: Object.freeze([...validation.missingEnv]) });
474
+ }
475
+ }
476
+ if (needsConfiguration)
477
+ completed.push('configuration-required');
478
+ else {
479
+ const patches = [];
480
+ if (!Object.hasOwn(entries, target.instanceKey))
481
+ patches.push({ op: 'set', path: ['plugins', target.instanceKey], value: target.packageName });
482
+ // Defaults stay owned by the package schema (completed at load), not pinned in the file.
483
+ if (!Object.hasOwn(currentConfig.document, target.instanceKey))
484
+ patches.push({ op: 'set', path: [target.instanceKey], value: {} });
485
+ if (patches.length)
486
+ await (await configuration.document.prepare(currentConfig, patches)).commit();
487
+ completed.push('application-declaration', 'minimum-config');
488
+ }
489
+ }
490
+ const state = await this.inspect(spec.name);
491
+ const result = { plan, restartRequired: state.restartRequired, state, ...(needsConfiguration ? { needsConfiguration } : {}) };
492
+ return Object.freeze(result);
493
+ }, spec.name);
494
+ }
495
+ /**
496
+ * Validates a declaration without writing: the package must be an installed production plugin,
497
+ * the instance key free, and `config` must satisfy the plugin schema (defaults applied).
498
+ */
499
+ async planDeclaration(request, config = {}, options = {}) {
500
+ const spec = npmSpec(request);
501
+ if (spec.version !== undefined)
502
+ throw new PluginDeclarationError('声明插件时不能指定版本');
503
+ const instanceKey = options.instanceKey ?? packageToInstanceKey(spec.name);
504
+ if (typeof instanceKey !== 'string' || instanceKey.length > 128 || !/^[a-z0-9][a-z0-9-]*$/u.test(instanceKey))
505
+ throw new PluginDeclarationError('instanceKey 只能包含小写字母、数字和连字符');
506
+ if (HOST_CONFIG_KEYS.includes(instanceKey) || ['configVersion', 'plugins', 'disable_plugins', 'plugin', 'root'].includes(instanceKey))
507
+ throw new PluginDeclarationError(`instanceKey ${instanceKey} 是保留配置键`);
508
+ if (!config || typeof config !== 'object' || Array.isArray(config))
509
+ throw new PluginDeclarationError('config 必须是对象');
510
+ const configuration = this.#configuration();
511
+ if (!configuration)
512
+ throw new PluginDeclarationError('Missing configVersion: 2 configuration; run zhin setup');
513
+ const current = await configuration.document.read();
514
+ const declaration = readApplicationPluginDeclaration(current.document);
515
+ if (declaration.version !== 2)
516
+ throw new PluginDeclarationError('Root configuration must use configVersion: 2');
517
+ const entries = (current.document.plugins ?? {});
518
+ if (Object.hasOwn(entries, instanceKey)) {
519
+ throw new PluginDeclarationError(entries[instanceKey] === spec.name
520
+ ? `插件实例 ${instanceKey} 已声明;修改其配置请使用 config:patch-project`
521
+ : `Plugin instanceKey collision: ${instanceKey}`);
522
+ }
523
+ const { project } = await this.#project();
524
+ if (project.zhin?.plugins?.length)
525
+ throw new PluginDeclarationError('Legacy package.json zhin.plugins detected; run zhin migrate instances');
526
+ if (!project.dependencies?.[spec.name] && !project.optionalDependencies?.[spec.name])
527
+ throw new PluginDeclarationError(`${spec.name} 不是项目的生产依赖,请先安装`);
528
+ const installed = await this.#installed(spec.name);
529
+ if (!installed.installed || !installed.plugin || installed.error)
530
+ throw new PluginDeclarationError(installed.error ?? `${spec.name} 未安装或不是 Plugin 包`);
531
+ const target = { packageName: spec.name, instanceKey };
532
+ const candidate = { ...current.document, plugins: { ...entries, [instanceKey]: spec.name } };
533
+ await this.#checkDependencies(target, candidate);
534
+ const validation = await configuration.store.validatePluginConfig(instanceKey, config, candidate);
535
+ if (!validation.valid || validation.missingEnv.length) {
536
+ throw new PluginDeclarationError(`配置未通过插件 Schema 校验,未声明:${[
537
+ ...validation.errors.map(error => `${error.path} ${error.message}`),
538
+ ...validation.missingEnv.map(name => `缺少环境变量 ${name}`)
539
+ ].join(';')}`, validation.errors, validation.missingEnv);
540
+ }
541
+ return Object.freeze({ ...target, revision: current.revision, schemaAvailable: validation.schemaAvailable });
542
+ }
543
+ /** Declares `plugins.<instanceKey>` and writes its section in one atomic config write. */
544
+ async declare(request, config = {}, options = {}) {
545
+ return this.#mutate(async (completed) => {
546
+ const plan = await this.planDeclaration(request, config, options);
547
+ if (options.expectedRevision !== undefined && plan.revision !== options.expectedRevision)
548
+ throw new PluginDeclarationError('项目或配置已被其他会话修改,请刷新后重试');
549
+ const configuration = this.#configuration();
550
+ if (!configuration)
551
+ throw new PluginDeclarationError('Missing configVersion: 2 configuration; run zhin setup');
552
+ const current = await configuration.document.read();
553
+ if (current.revision !== plan.revision)
554
+ throw new PluginDeclarationError('配置在校验期间变化,请刷新后重试');
555
+ await (await configuration.document.prepare(current, [
556
+ { op: 'set', path: ['plugins', plan.instanceKey], value: plan.packageName },
557
+ { op: 'set', path: [plan.instanceKey], value: structuredClone(config) },
558
+ ])).commit();
559
+ completed.push('application-declaration', 'config');
560
+ const state = await this.inspect(plan.instanceKey);
561
+ return Object.freeze({ packageName: plan.packageName, instanceKey: plan.instanceKey, restartRequired: state.restartRequired, state });
562
+ }, request);
563
+ }
564
+ async planUninstall(request) {
565
+ const state = await this.inspect(request);
566
+ const configuration = this.#configuration();
567
+ const config = configuration ? await configuration.store.readDocument() : {};
568
+ return { packageName: state.packageName, instanceKey: state.instanceKey, projectRevision: state.projectRevision, configurationRevision: state.revision, installed: state.installed, declared: state.declared, hasConfig: Object.hasOwn(config, state.instanceKey), restartRequired: state.declared || state.published };
569
+ }
570
+ async uninstall(request, expectedRevision, expectedProjectRevision, options = {}) {
571
+ return this.#mutate(async (completed) => {
572
+ const previous = await this.#assertRevision(expectedRevision, expectedProjectRevision);
573
+ const plan = await this.planUninstall(request);
574
+ const otherInstances = (await this.#declaredEntries()).filter(entry => entry.package === plan.packageName && entry.instanceKey !== plan.instanceKey);
575
+ const keepDependency = options.removeDependency === false || otherInstances.length > 0 || await this.#hasOtherPackageReference(plan.packageName, plan.instanceKey);
576
+ if (plan.installed && !keepDependency && !plan.packageName.startsWith('./'))
577
+ await this.#pnpm(['remove', plan.packageName]);
578
+ if (plan.installed && !keepDependency && !plan.packageName.startsWith('./'))
579
+ completed.push('dependency-remove');
580
+ if ((await this.#revisions()).revision !== previous.revision)
581
+ throw new Error('Root config changed during uninstall');
582
+ const configuration = this.#configuration();
583
+ const config = configuration ? await configuration.document.read() : undefined;
584
+ if (!config || !configuration)
585
+ throw new Error('Missing configVersion: 2 configuration');
586
+ const declaration = readApplicationPluginDeclaration(config.document);
587
+ const patches = [];
588
+ if (plan.declared)
589
+ patches.push({ op: 'remove', path: ['plugins', plan.instanceKey] });
590
+ if (plan.hasConfig)
591
+ patches.push({ op: 'remove', path: [plan.instanceKey] });
592
+ if (declaration.disabled.includes(plan.instanceKey))
593
+ patches.push({ op: 'set', path: ['disable_plugins'], value: declaration.disabled.filter(key => key !== plan.instanceKey) });
594
+ if (patches.length)
595
+ await (await configuration.document.prepare(config, patches)).commit();
596
+ completed.push('application-unmount', 'config-remove');
597
+ return { plan, restartRequired: plan.restartRequired };
598
+ }, request);
599
+ }
600
+ async #hasOtherPackageReference(packageName, removedKey) {
601
+ const resolver = await NodePackageResolver.create(this.#root);
602
+ const root = await resolver.root(this.#root);
603
+ const seen = new Set();
604
+ const visit = async (pkg, isRoot = false) => {
605
+ if (seen.has(pkg.root))
606
+ return false;
607
+ seen.add(pkg.root);
608
+ if (pkg.packageJson.zhin.type !== 'plugin')
609
+ return false;
610
+ if (pkg.packageJson.zhin.features.some(item => item.package === packageName))
611
+ return true;
612
+ if (!isRoot && [pkg.packageJson.dependencies, pkg.packageJson.optionalDependencies, pkg.packageJson.peerDependencies].some(deps => deps?.[packageName]))
613
+ return true;
614
+ const entries = isRoot ? await this.#declaredEntries() : [];
615
+ for (const entry of entries) {
616
+ if (isRoot && entry.instanceKey === removedKey)
617
+ continue;
618
+ if (entry.package === packageName)
619
+ return true;
620
+ try {
621
+ if (await visit(await resolver.resolve(entry.package, pkg)))
622
+ return true;
623
+ }
624
+ catch {
625
+ return true;
626
+ } // Unresolved references cannot prove dependency removal is safe.
627
+ }
628
+ return false;
629
+ };
630
+ return visit(root, true);
631
+ }
632
+ async planUpdate(request, targetVersion) {
633
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(targetVersion))
634
+ throw new Error('targetVersion must be an exact semver version');
635
+ const state = await this.inspect(request);
636
+ if (state.packageName.startsWith('./'))
637
+ throw new Error('Local plugins are updated through their source files, not npm version changes');
638
+ return { packageName: state.packageName, instanceKey: state.instanceKey, currentVersion: state.version, targetVersion, projectRevision: state.projectRevision, configurationRevision: state.revision, installed: state.installed, declared: state.declared, alreadyCurrent: state.version === targetVersion, restartRequired: state.version !== targetVersion };
639
+ }
640
+ async update(request, targetVersion, expectedRevision, expectedProjectRevision) {
641
+ return this.#mutate(async (completed) => {
642
+ const previous = await this.#assertRevision(expectedRevision, expectedProjectRevision);
643
+ const plan = await this.planUpdate(request, targetVersion);
644
+ if (!plan.installed || !plan.declared)
645
+ throw new Error('插件必须先安装并声明才能更新');
646
+ if (!plan.alreadyCurrent)
647
+ await this.#pnpm(['add', `${plan.packageName}@${plan.targetVersion}`]);
648
+ if (!plan.alreadyCurrent)
649
+ completed.push('dependency-update');
650
+ if ((await this.#revisions()).revision !== previous.revision)
651
+ throw new Error('Root config changed during update');
652
+ const state = await this.inspect(plan.instanceKey);
653
+ if (state.version !== plan.targetVersion)
654
+ throw new Error('Package manager did not install the requested exact version');
655
+ if (!state.installed || !state.plugin || state.dependencies !== 'passed' || state.issues.length)
656
+ throw new Error('Updated package failed plugin/dependency verification');
657
+ return { plan, installedVersion: state.version, restartRequired: !plan.alreadyCurrent };
658
+ }, request);
659
+ }
660
+ async apply(request, expectedProjectRevision) {
661
+ return this.#mutate(async () => {
662
+ await this.#assertRevision(undefined, expectedProjectRevision);
663
+ const state = await this.inspect(request);
664
+ if (!state.declared || !state.disabled && (!state.installed || !state.plugin || state.status === 'missing-config' || state.status === 'degraded'))
665
+ throw new Error('插件未通过安装、依赖和配置检查,不能应用');
666
+ if (!this.#options.requestRestart)
667
+ return { requested: false, state };
668
+ if (!state.restartRequired)
669
+ return { requested: false, state };
670
+ // The caller receives pending-restart; only a subsequent generation can prove publication.
671
+ await this.#options.requestRestart();
672
+ return { requested: true, state };
673
+ });
674
+ }
675
+ async verificationPlan(request) {
676
+ const target = await this.#resolveTarget(request);
677
+ const installed = await this.#installed(target.packageName);
678
+ const revisions = await this.#revisions();
679
+ const lease = this.#options.snapshots?.acquire();
680
+ try {
681
+ const node = lease && [...lease.value.tree.values()].find(item => item.parent === rootPluginId() && item.instanceKey === target.instanceKey && item.packageName === (installed.pkg?.name ?? target.packageName));
682
+ const host = node && lease?.value.resources.get(node.id)?.get(pluginVerificationToken.id);
683
+ const checks = verificationChecks(host);
684
+ return Object.freeze({ ...target, generation: lease?.value.generation ?? null, projectRevision: revisions.projectRevision, checkRevision: checksRevision(this.#verificationIncarnation, node?.id, checks), checks });
685
+ }
686
+ finally {
687
+ lease?.release();
688
+ }
689
+ }
690
+ async verify(request, checkId, expectedGeneration, expectedProjectRevision, expectedCheckRevision) {
691
+ const target = await this.#resolveTarget(request);
692
+ const state = await this.inspect(request);
693
+ const installed = await this.#installed(target.packageName);
694
+ const lease = this.#options.snapshots?.acquire();
695
+ try {
696
+ const node = lease && [...lease.value.tree.values()].find(item => item.parent === rootPluginId() && item.instanceKey === target.instanceKey && item.packageName === (installed.pkg?.name ?? target.packageName));
697
+ const host = node && lease?.value.resources.get(node.id)?.get(pluginVerificationToken.id);
698
+ const checks = verificationChecks(host);
699
+ if (expectedGeneration !== (lease?.value.generation ?? null) || typeof expectedProjectRevision !== 'string' || (await this.#revisions()).projectRevision !== expectedProjectRevision || checksRevision(this.#verificationIncarnation, node?.id, checks) !== expectedCheckRevision)
700
+ throw new Error('Verification plan changed; refresh the check and its effects before approving again');
701
+ const check = checks.find(item => item.id === checkId);
702
+ if (!host || !check || !lease || state.status !== 'enabled' || state.generation !== lease.value.generation)
703
+ return { ...target, checkId, generation: lease?.value.generation ?? null, current: false, status: 'not-configured', scope: null, summary: 'No applied current plugin business verification is configured', evidence: [] };
704
+ const outcome = await host.run(checkId);
705
+ const summaryI18n = verificationTranslations(outcome.summaryI18n, false);
706
+ const evidenceI18n = verificationTranslations(outcome.evidenceI18n, true, outcome.evidence.length);
707
+ const after = await this.inspect(request);
708
+ const current = after.generation === lease.value.generation && after.status === 'enabled';
709
+ return Object.freeze({ ...target, ...outcome,
710
+ ...(summaryI18n ? { summaryI18n } : {}), ...(evidenceI18n ? { evidenceI18n } : {}),
711
+ checkId, generation: lease.value.generation, current, scope: check.scope });
712
+ }
713
+ finally {
714
+ lease?.release();
715
+ }
716
+ }
717
+ }
718
+ /** Errors retain completed-stage evidence instead of pretending an unsafe rollback succeeded. */
719
+ export class PluginManagementOperationError extends Error {
720
+ completedStages;
721
+ state;
722
+ constructor(cause, completed, state) {
723
+ super(`${errorMessage(cause)}。已完成步骤: ${completed.join(', ') || '无已确认步骤'};未回滚文件,请检查 packages status 后继续。`, { cause });
724
+ this.name = 'PluginManagementOperationError';
725
+ this.completedStages = Object.freeze([...completed]);
726
+ this.state = state;
727
+ }
728
+ }
729
+ function verificationChecks(host) {
730
+ const checks = (host?.checks() ?? []).map(check => {
731
+ if (!check.id || !check.title || !Array.isArray(check.effects) || !check.effects.every(effect => typeof effect === 'string') || !['local', 'platform'].includes(check.scope))
732
+ throw new Error('Invalid plugin verification descriptor');
733
+ const titleI18n = verificationTranslations(check.titleI18n, false);
734
+ const effectsI18n = verificationTranslations(check.effectsI18n, true, check.effects.length);
735
+ return Object.freeze({ id: check.id, title: check.title, effects: Object.freeze([...check.effects]), scope: check.scope,
736
+ ...(titleI18n ? { titleI18n } : {}), ...(effectsI18n ? { effectsI18n } : {}) });
737
+ });
738
+ if (new Set(checks.map(check => check.id)).size !== checks.length)
739
+ throw new Error('Duplicate plugin verification check id');
740
+ return Object.freeze(checks);
741
+ }
742
+ function verificationTranslations(value, list, expectedLength) {
743
+ if (value === undefined)
744
+ return undefined;
745
+ if (!value || typeof value !== 'object' || Array.isArray(value))
746
+ throw new Error('Invalid plugin verification translations');
747
+ const result = {};
748
+ for (const locale of ['zh-CN', 'en']) {
749
+ const entry = value[locale];
750
+ if (entry === undefined)
751
+ continue;
752
+ const nonempty = (item) => typeof item === 'string' && item.trim().length > 0;
753
+ if (list ? !Array.isArray(entry) || entry.length !== expectedLength || !entry.every(nonempty) : !nonempty(entry))
754
+ throw new Error('Invalid plugin verification translations');
755
+ result[locale] = Array.isArray(entry) ? Object.freeze([...entry]) : entry;
756
+ }
757
+ return Object.freeze(result);
758
+ }
759
+ function checksRevision(incarnation, owner, checks) {
760
+ return createHash('sha256').update(JSON.stringify([incarnation, owner, checks])).digest('hex');
761
+ }
762
+ function npmSpec(value) {
763
+ const match = /^(?<name>(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*)(?:@(?<version>[^\s]+))?$/u.exec(value.trim());
764
+ if (!match?.groups)
765
+ throw new Error('Plugin management accepts npm package names with an optional version/tag');
766
+ const name = match.groups.name;
767
+ return { name, version: match.groups.version, spec: match.groups.version ? `${name}@${match.groups.version}` : name };
768
+ }
769
+ /** `$.endpoints.0.id` → `endpoints.0.id` (validation paths are rooted at the instance section). */
770
+ function relativeConfigPath(path) { return path.replace(/^\$\.?/u, ''); }
771
+ function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
772
+ function notFound(error) { return error instanceof Error && 'code' in error && error.code === 'ENOENT'; }