@snaptrude/plugin-client 0.7.1 → 0.9.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/CHANGELOG.md +22 -0
- package/dist/api/index.d.ts +2 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/events.d.ts +28 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/handle-runtime.d.ts +27 -0
- package/dist/handle-runtime.d.ts.map +1 -0
- package/dist/host-api.d.ts.map +1 -1
- package/dist/index.cjs +201 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +171 -3
- package/dist/index.js.map +1 -1
- package/dist/plugin-worker.d.ts +32 -0
- package/dist/plugin-worker.d.ts.map +1 -1
- package/dist/rpc-proxy.d.ts.map +1 -1
- package/package.json +10 -3
- package/upgrade-notes/0.4.0-to-0.5.0.md +53 -0
- package/upgrade-notes/0.5.0-to-0.6.0.md +38 -0
- package/upgrade-notes/0.6.0-to-0.7.0.md +38 -0
- package/upgrade-notes/0.7.0-to-0.7.1.md +28 -0
- package/upgrade-notes/0.7.1-to-0.8.0.md +44 -0
- package/upgrade-notes/0.8.0-to-0.9.0.md +61 -0
- package/upgrade-notes/index.json +42 -0
- package/AGENTS.md +0 -86
- package/CLAUDE.md +0 -11
- package/src/api/index.ts +0 -45
- package/src/host-api.ts +0 -87
- package/src/index.ts +0 -38
- package/src/plugin-worker.ts +0 -99
- package/src/rpc-proxy.ts +0 -56
- package/test/host-api-errors.test.mjs +0 -101
- package/tsconfig.json +0 -17
- package/tsup.config.ts +0 -12
package/dist/index.js
CHANGED
|
@@ -6,6 +6,117 @@ import {
|
|
|
6
6
|
// src/host-api.ts
|
|
7
7
|
import * as Comlink from "comlink";
|
|
8
8
|
import { PluginError, fromEnvelope, makeClientEnvelope } from "@snaptrude/plugin-core";
|
|
9
|
+
|
|
10
|
+
// src/handle-runtime.ts
|
|
11
|
+
import { Handle } from "@snaptrude/plugin-core";
|
|
12
|
+
var interned = /* @__PURE__ */ new Map();
|
|
13
|
+
var RELEASE_FLUSH_SIZE = 64;
|
|
14
|
+
var RELEASE_FLUSH_MS = 250;
|
|
15
|
+
var MAX_FLUSH_FAILURES = 5;
|
|
16
|
+
var releaseQueue = /* @__PURE__ */ new Set();
|
|
17
|
+
var flushTimer = null;
|
|
18
|
+
var consecutiveFlushFailures = 0;
|
|
19
|
+
var finalizer = new FinalizationRegistry((id) => {
|
|
20
|
+
if (interned.get(id)?.deref()) return;
|
|
21
|
+
interned.delete(id);
|
|
22
|
+
enqueueRelease(id);
|
|
23
|
+
});
|
|
24
|
+
function intern(id) {
|
|
25
|
+
releaseQueue.delete(id);
|
|
26
|
+
const existing = interned.get(id)?.deref();
|
|
27
|
+
if (existing) return existing;
|
|
28
|
+
const handle = new Handle(id);
|
|
29
|
+
handle[Symbol.asyncDispose] = async () => {
|
|
30
|
+
if (interned.get(id)?.deref() === handle) interned.delete(id);
|
|
31
|
+
enqueueRelease(id);
|
|
32
|
+
};
|
|
33
|
+
interned.set(id, new WeakRef(handle));
|
|
34
|
+
finalizer.register(handle, id);
|
|
35
|
+
return handle;
|
|
36
|
+
}
|
|
37
|
+
function enqueueRelease(id) {
|
|
38
|
+
releaseQueue.add(id);
|
|
39
|
+
if (releaseQueue.size >= RELEASE_FLUSH_SIZE) {
|
|
40
|
+
void flushReleaseQueue();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
flushTimer ?? (flushTimer = setTimeout(() => {
|
|
44
|
+
void flushReleaseQueue();
|
|
45
|
+
}, RELEASE_FLUSH_MS));
|
|
46
|
+
}
|
|
47
|
+
var releaseTransport = async (batch) => {
|
|
48
|
+
await getHostApi().call({
|
|
49
|
+
method: "core.handles.release",
|
|
50
|
+
args: [batch]
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
function __setReleaseTransport(transport) {
|
|
54
|
+
releaseTransport = transport ?? (async (batch) => {
|
|
55
|
+
await getHostApi().call({
|
|
56
|
+
method: "core.handles.release",
|
|
57
|
+
args: [batch]
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function flushReleaseQueue() {
|
|
62
|
+
if (flushTimer) {
|
|
63
|
+
clearTimeout(flushTimer);
|
|
64
|
+
flushTimer = null;
|
|
65
|
+
}
|
|
66
|
+
if (releaseQueue.size === 0) return;
|
|
67
|
+
const batch = [...releaseQueue];
|
|
68
|
+
releaseQueue.clear();
|
|
69
|
+
try {
|
|
70
|
+
await releaseTransport(batch);
|
|
71
|
+
consecutiveFlushFailures = 0;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
consecutiveFlushFailures += 1;
|
|
74
|
+
if (consecutiveFlushFailures >= MAX_FLUSH_FAILURES) {
|
|
75
|
+
console.warn(
|
|
76
|
+
`[snaptrude] dropping ${batch.length} handle release(s) after ${consecutiveFlushFailures} failed flushes`,
|
|
77
|
+
err
|
|
78
|
+
);
|
|
79
|
+
consecutiveFlushFailures = 0;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const id of batch) {
|
|
83
|
+
if (!interned.get(id)?.deref()) releaseQueue.add(id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function rewrapResult(value) {
|
|
88
|
+
if (Array.isArray(value)) return value.map(rewrapResult);
|
|
89
|
+
if (value && typeof value === "object") {
|
|
90
|
+
const record = value;
|
|
91
|
+
const keys = Object.keys(record);
|
|
92
|
+
if (keys.length === 1 && keys[0] === "__h" && typeof record.__h === "string") {
|
|
93
|
+
return intern(record.__h);
|
|
94
|
+
}
|
|
95
|
+
for (const key of keys) record[key] = rewrapResult(record[key]);
|
|
96
|
+
return record;
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function unwrapArgs(value, memo = /* @__PURE__ */ new Map()) {
|
|
101
|
+
if (value instanceof Handle) return value.id;
|
|
102
|
+
if (value === null || typeof value !== "object") return value;
|
|
103
|
+
const hit = memo.get(value);
|
|
104
|
+
if (hit !== void 0) return hit;
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
const out2 = [];
|
|
107
|
+
memo.set(value, out2);
|
|
108
|
+
for (const item of value) out2.push(unwrapArgs(item, memo));
|
|
109
|
+
return out2;
|
|
110
|
+
}
|
|
111
|
+
const proto = Object.getPrototypeOf(value);
|
|
112
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
113
|
+
const out = {};
|
|
114
|
+
memo.set(value, out);
|
|
115
|
+
for (const [key, item] of Object.entries(value)) out[key] = unwrapArgs(item, memo);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/host-api.ts
|
|
9
120
|
function createHostApi(endpoint) {
|
|
10
121
|
return Comlink.wrap(
|
|
11
122
|
endpoint ?? globalThis
|
|
@@ -38,7 +149,7 @@ function getHostApi() {
|
|
|
38
149
|
);
|
|
39
150
|
}
|
|
40
151
|
if (result.success) {
|
|
41
|
-
return result.data;
|
|
152
|
+
return rewrapResult(result.data);
|
|
42
153
|
}
|
|
43
154
|
throw rehydrate(
|
|
44
155
|
result.errorInfo ?? makeClientEnvelope("UNKNOWN", result.error ?? "Unknown host error", {
|
|
@@ -64,7 +175,8 @@ function createRpcNamespace(basePath) {
|
|
|
64
175
|
apply(_target, _thisArg, argArray) {
|
|
65
176
|
const payload = {
|
|
66
177
|
method: path,
|
|
67
|
-
|
|
178
|
+
// Handle instances anywhere in the tuple become their wire id strings.
|
|
179
|
+
args: argArray.map((arg) => unwrapArgs(arg))
|
|
68
180
|
};
|
|
69
181
|
return getHostApi().call(payload);
|
|
70
182
|
}
|
|
@@ -84,6 +196,7 @@ var ClientPluginApi = class _ClientPluginApi extends PluginApi {
|
|
|
84
196
|
this.program = createRpcNamespace("program");
|
|
85
197
|
this.presentation = createRpcNamespace("presentation");
|
|
86
198
|
this.analysis = createRpcNamespace("analysis");
|
|
199
|
+
this.workspace = createRpcNamespace("workspace");
|
|
87
200
|
}
|
|
88
201
|
static getInstance() {
|
|
89
202
|
if (!_ClientPluginApi.instance) {
|
|
@@ -105,6 +218,17 @@ var PluginWorker = class {
|
|
|
105
218
|
;
|
|
106
219
|
this.hostAPI.ui.sendToUI({ action, payload });
|
|
107
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Show a transient notification toast in the Snaptrude editor.
|
|
223
|
+
*
|
|
224
|
+
* @param message - Text to display.
|
|
225
|
+
* @param type - Severity; `"error"`/`"warning"` linger a little longer than
|
|
226
|
+
* `"info"`. Defaults to `"info"`.
|
|
227
|
+
*/
|
|
228
|
+
notify(message, type = "info") {
|
|
229
|
+
;
|
|
230
|
+
this.hostAPI.ui.showNotification(message, type);
|
|
231
|
+
}
|
|
108
232
|
/**
|
|
109
233
|
* Signal the host that this plugin has finished its work and should be
|
|
110
234
|
* stopped. Use this in headless (UI-less) plugins that run a task and
|
|
@@ -114,6 +238,35 @@ var PluginWorker = class {
|
|
|
114
238
|
;
|
|
115
239
|
this.hostAPI.lifecycle.complete();
|
|
116
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Subscribe to a host event (e.g. `"model:changed"`, fired debounced on any
|
|
243
|
+
* user- or plugin-initiated model edit). The callback runs in this worker
|
|
244
|
+
* each time the event fires; its argument is a small structured-clone-safe
|
|
245
|
+
* payload (see the event's payload type, e.g. `ModelChangedEvent`).
|
|
246
|
+
*
|
|
247
|
+
* Fire-and-forget: subscribe once (typically in `init()`). Subscriptions live
|
|
248
|
+
* for the plugin's lifetime and are cleared automatically when the plugin is
|
|
249
|
+
* stopped — there is no `unsubscribe` yet.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* import type { ModelChangedEvent } from "@snaptrude/plugin-client";
|
|
254
|
+
*
|
|
255
|
+
* async init() {
|
|
256
|
+
* this.subscribe("model:changed", (e) => {
|
|
257
|
+
* const { source } = e as ModelChangedEvent;
|
|
258
|
+
* console.log("model changed via", source);
|
|
259
|
+
* });
|
|
260
|
+
* }
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
subscribe(event, callback) {
|
|
264
|
+
;
|
|
265
|
+
this.hostAPI.events.on(
|
|
266
|
+
event,
|
|
267
|
+
Comlink2.proxy(callback)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
117
270
|
async init() {
|
|
118
271
|
console.log(this.pluginId, "init() called");
|
|
119
272
|
console.log(this.pluginId, "Initialization complete");
|
|
@@ -151,6 +304,15 @@ var PluginWorker = class {
|
|
|
151
304
|
}
|
|
152
305
|
};
|
|
153
306
|
|
|
307
|
+
// src/events.ts
|
|
308
|
+
var PLUGIN_EVENTS = [
|
|
309
|
+
"selection:changed",
|
|
310
|
+
"tool:activated",
|
|
311
|
+
"project:saved",
|
|
312
|
+
"view:changed",
|
|
313
|
+
"model:changed"
|
|
314
|
+
];
|
|
315
|
+
|
|
154
316
|
// src/index.ts
|
|
155
317
|
import {
|
|
156
318
|
PluginError as PluginError2,
|
|
@@ -175,6 +337,7 @@ export {
|
|
|
175
337
|
CODE_META,
|
|
176
338
|
ClientPluginApi,
|
|
177
339
|
PLUGIN_ERROR_CODES,
|
|
340
|
+
PLUGIN_EVENTS,
|
|
178
341
|
PluginError2 as PluginError,
|
|
179
342
|
PluginExecutionError,
|
|
180
343
|
PluginHandleError,
|
|
@@ -188,11 +351,16 @@ export {
|
|
|
188
351
|
PluginValidationError,
|
|
189
352
|
PluginWorker,
|
|
190
353
|
__setHostApiInstance,
|
|
354
|
+
__setReleaseTransport,
|
|
191
355
|
createHostApi,
|
|
356
|
+
flushReleaseQueue,
|
|
192
357
|
fromEnvelope2 as fromEnvelope,
|
|
193
358
|
getHostApi,
|
|
359
|
+
intern,
|
|
194
360
|
isErrorEnvelope,
|
|
195
361
|
isPluginErrorCode,
|
|
196
|
-
|
|
362
|
+
rewrapResult,
|
|
363
|
+
snaptrude,
|
|
364
|
+
unwrapArgs
|
|
197
365
|
};
|
|
198
366
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api/index.ts","../src/host-api.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts","../src/index.ts"],"sourcesContent":["import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n PluginAnalysisApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n public analysis: PluginAnalysisApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n this.analysis = createRpcNamespace<PluginAnalysisApi>(\"analysis\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\nimport { PluginError, fromEnvelope, makeClientEnvelope } from \"@snaptrude/plugin-core\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\n/** TEST SEAM ONLY: replace the Comlink host instance (pass `undefined` to reset). */\nexport function __setHostApiInstance(instance?: HostApi): void {\n _instance = instance ?? null\n}\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n\n let result: PluginApiCallWrappedResult<M>\n try {\n result = await _instance.call(payload)\n } catch (transportErr) {\n // Comlink-level rejection: port closed, worker terminated, clone\n // failure. Never a routed failure — the router always RETURNS its\n // envelope — so normalize to a typed transport error.\n if (PluginError.is(transportErr)) throw transportErr\n throw rehydrate(\n makeClientEnvelope(\n \"TRANSPORT_LOST\",\n transportErr instanceof Error ? transportErr.message : String(transportErr),\n { methodPath: payload.method }\n )\n )\n }\n\n if (result.success) {\n return result.data\n }\n\n // Structured envelope when the host provides one; legacy hosts (string\n // `error` only) degrade to UNKNOWN with the message preserved.\n throw rehydrate(\n result.errorInfo ??\n makeClientEnvelope(\"UNKNOWN\", result.error ?? \"Unknown host error\", {\n methodPath: payload.method,\n })\n )\n }\n }\n}\n\n/**\n * Envelope → typed `PluginError`, with the stack trimmed to the plugin's call\n * site (V8 only; harmless no-op elsewhere) instead of transport internals.\n */\nfunction rehydrate(envelope: Parameters<typeof fromEnvelope>[0]): PluginError {\n const error = fromEnvelope(envelope)\n ;(Error as { captureStackTrace?: (target: object, ctor: Function) => void })\n .captureStackTrace?.(error, rehydrate)\n return error\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n args: argArray,\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n","import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\n\n// Error surface — plugins branch on `PluginError.is(e)` + `e.code`.\nexport {\n PluginError,\n PluginValidationError,\n PluginNotFoundError,\n PluginPermissionError,\n PluginHandleError,\n PluginQuotaError,\n PluginTimeoutError,\n PluginTransportError,\n PluginLifecycleError,\n PluginExecutionError,\n PluginInternalError,\n fromEnvelope,\n isErrorEnvelope,\n isPluginErrorCode,\n PLUGIN_ERROR_CODES,\n CODE_META,\n} from \"@snaptrude/plugin-core\"\nexport type {\n ErrorEnvelope,\n PluginErrorCode,\n WirePluginErrorCode,\n PluginErrorCategory,\n} from \"@snaptrude/plugin-core\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n"],"mappings":"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","names":["Comlink","PluginError","fromEnvelope"]}
|
|
1
|
+
{"version":3,"sources":["../src/api/index.ts","../src/host-api.ts","../src/handle-runtime.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts","../src/events.ts","../src/index.ts"],"sourcesContent":["import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n PluginAnalysisApi,\n PluginWorkspaceApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n public analysis: PluginAnalysisApi\n public workspace: PluginWorkspaceApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n this.analysis = createRpcNamespace<PluginAnalysisApi>(\"analysis\")\n this.workspace = createRpcNamespace<PluginWorkspaceApi>(\"workspace\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\nimport { PluginError, fromEnvelope, makeClientEnvelope } from \"@snaptrude/plugin-core\"\nimport { rewrapResult } from \"./handle-runtime\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\n/** TEST SEAM ONLY: replace the Comlink host instance (pass `undefined` to reset). */\nexport function __setHostApiInstance(instance?: HostApi): void {\n _instance = instance ?? null\n}\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n\n let result: PluginApiCallWrappedResult<M>\n try {\n result = await _instance.call(payload)\n } catch (transportErr) {\n // Comlink-level rejection: port closed, worker terminated, clone\n // failure. Never a routed failure — the router always RETURNS its\n // envelope — so normalize to a typed transport error.\n if (PluginError.is(transportErr)) throw transportErr\n throw rehydrate(\n makeClientEnvelope(\n \"TRANSPORT_LOST\",\n transportErr instanceof Error ? transportErr.message : String(transportErr),\n { methodPath: payload.method }\n )\n )\n }\n\n if (result.success) {\n // Re-wrap tagged handles ({__h: id} → interned Handle instances).\n return rewrapResult(result.data) as PluginApiCallResult<M>\n }\n\n // Structured envelope when the host provides one; legacy hosts (string\n // `error` only) degrade to UNKNOWN with the message preserved.\n throw rehydrate(\n result.errorInfo ??\n makeClientEnvelope(\"UNKNOWN\", result.error ?? \"Unknown host error\", {\n methodPath: payload.method,\n })\n )\n }\n }\n}\n\n/**\n * Envelope → typed `PluginError`, with the stack trimmed to the plugin's call\n * site (V8 only; harmless no-op elsewhere) instead of transport internals.\n */\nfunction rehydrate(envelope: Parameters<typeof fromEnvelope>[0]): PluginError {\n const error = fromEnvelope(envelope)\n ;(Error as { captureStackTrace?: (target: object, ctor: Function) => void })\n .captureStackTrace?.(error, rehydrate)\n return error\n}\n","import { Handle } from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Worker-side handle lifecycle runtime (P1):\n *\n * - **Interning** — one live `Handle` instance per id, so `===`, `Set`, and\n * `Map` keys keep working exactly as they did when handles were raw strings\n * (the host identity-dedups resource/topology handles, so the same id\n * arrives repeatedly).\n * - **FinalizationRegistry backstop** — when the plugin drops every reference\n * to a handle, its host registry entry is eventually released without any\n * author action. Eventual, not timely: deterministic release\n * (`core.handles.release` / scopes / `await using`) remains the primary tool.\n * - **Batched release queue** — finalizer hits and `Symbol.asyncDispose` calls\n * collapse into one `core.handles.release([...])` RPC per flush.\n */\n\nconst interned = new Map<string, WeakRef<Handle<string>>>()\n\nconst RELEASE_FLUSH_SIZE = 64\nconst RELEASE_FLUSH_MS = 250\nconst MAX_FLUSH_FAILURES = 5\n\nconst releaseQueue = new Set<string>()\nlet flushTimer: ReturnType<typeof setTimeout> | null = null\nlet consecutiveFlushFailures = 0\n\nconst finalizer = new FinalizationRegistry<string>((id) => {\n // A NEW wrapper for the same id may have been interned after the collected\n // one died (deduped topology re-enumeration) — releasing then would free a\n // handle the plugin still holds. Only release when no live wrapper remains.\n if (interned.get(id)?.deref()) return\n interned.delete(id)\n enqueueRelease(id)\n})\n\n/**\n * Return THE `Handle` instance for this id — the existing live wrapper when\n * present, else a fresh one wired for auto-release.\n */\nexport function intern(id: string): Handle<string> {\n // The host just (re-)vended this id, so it is live host-side. Cancel any\n // PENDING auto-release for it — covers the finalizer-already-ran window\n // (old wrapper collected, id queued, same id re-vended before the flush;\n // without this the flush would release a handle the plugin holds live).\n releaseQueue.delete(id)\n\n const existing = interned.get(id)?.deref()\n if (existing) return existing\n\n const handle = new Handle<string>(id)\n // Instance-level override shadows the class's no-op placeholder. Explicit\n // dispose also UN-interns the wrapper: (a) the flush-failure retry filter\n // skips ids with a live interned wrapper, so a still-interned explicit\n // dispose would never be retried; (b) if the host re-vends the id later,\n // a fresh wrapper is minted instead of resurrecting the disposed one.\n ;(handle as { [Symbol.asyncDispose]?: () => Promise<void> })[Symbol.asyncDispose] =\n async () => {\n if (interned.get(id)?.deref() === handle) interned.delete(id)\n enqueueRelease(id)\n }\n interned.set(id, new WeakRef(handle))\n finalizer.register(handle, id)\n return handle\n}\n\nfunction enqueueRelease(id: string): void {\n releaseQueue.add(id)\n if (releaseQueue.size >= RELEASE_FLUSH_SIZE) {\n void flushReleaseQueue()\n return\n }\n flushTimer ??= setTimeout(() => {\n void flushReleaseQueue()\n }, RELEASE_FLUSH_MS)\n}\n\n/** How a release batch reaches the host. Swappable for tests (`__setReleaseTransport`). */\nlet releaseTransport = async (batch: string[]): Promise<void> => {\n await getHostApi().call({\n method: \"core.handles.release\",\n args: [batch],\n } as never)\n}\n\n/** TEST SEAM ONLY: replace the release transport (pass `undefined` to restore). */\nexport function __setReleaseTransport(\n transport?: (batch: string[]) => Promise<void>\n): void {\n releaseTransport =\n transport ??\n (async (batch: string[]) => {\n await getHostApi().call({\n method: \"core.handles.release\",\n args: [batch],\n } as never)\n })\n}\n\n/** Exported for tests and for an eager flush before a plugin self-completes. */\nexport async function flushReleaseQueue(): Promise<void> {\n if (flushTimer) {\n clearTimeout(flushTimer)\n flushTimer = null\n }\n if (releaseQueue.size === 0) return\n const batch = [...releaseQueue]\n releaseQueue.clear()\n try {\n await releaseTransport(batch)\n consecutiveFlushFailures = 0\n } catch (err) {\n // Transient failure (rate limit, timeout): re-queue so the entries are not\n // permanently leaked host-side; the next enqueue/flush retries. Ids the\n // plugin re-acquired in the meantime were already purged by intern() and\n // must not be re-added. If the host is simply gone, the plugin is stopping\n // and registry teardown reclaims everything anyway.\n consecutiveFlushFailures += 1\n if (consecutiveFlushFailures >= MAX_FLUSH_FAILURES) {\n // Persistent failure: retrying forever would just spin. Drop the batch\n // loudly — host teardown reclaims the entries when the plugin stops.\n console.warn(\n `[snaptrude] dropping ${batch.length} handle release(s) after ${consecutiveFlushFailures} failed flushes`,\n err\n )\n consecutiveFlushFailures = 0\n return\n }\n for (const id of batch) {\n if (!interned.get(id)?.deref()) releaseQueue.add(id)\n }\n }\n}\n\n/**\n * Recursively re-wrap a host result: `{ __h: \"<id>\" }` tags (produced by\n * `Handle.toJSON` through the host's JSON-round-trip serialization) become\n * interned `Handle` instances. Arrays and plain records are walked; all other\n * values pass through untouched.\n */\nexport function rewrapResult(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(rewrapResult)\n if (value && typeof value === \"object\") {\n const record = value as Record<string, unknown>\n const keys = Object.keys(record)\n if (keys.length === 1 && keys[0] === \"__h\" && typeof record.__h === \"string\") {\n return intern(record.__h)\n }\n for (const key of keys) record[key] = rewrapResult(record[key])\n return record\n }\n return value\n}\n\n/**\n * Recursively unwrap outgoing args: `Handle` instances become their wire id\n * strings. Uses a memo Map (not a bail-out set) so shared/diamond references\n * get the SAME converted subtree — a bail-out would leak un-unwrapped Handle\n * instances through the second reference. Cycles are handled by memoizing the\n * output object before recursing. Non-plain objects pass through untouched\n * (structured clone imposes plain-data args anyway).\n */\nexport function unwrapArgs(value: unknown, memo = new Map<object, unknown>()): unknown {\n if (value instanceof Handle) return value.id\n if (value === null || typeof value !== \"object\") return value\n\n const hit = memo.get(value)\n if (hit !== undefined) return hit\n\n if (Array.isArray(value)) {\n const out: unknown[] = []\n memo.set(value, out)\n for (const item of value) out.push(unwrapArgs(item, memo))\n return out\n }\n\n const proto = Object.getPrototypeOf(value)\n if (proto !== Object.prototype && proto !== null) return value\n\n const out: Record<string, unknown> = {}\n memo.set(value, out)\n for (const [key, item] of Object.entries(value)) out[key] = unwrapArgs(item, memo)\n return out\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\nimport { unwrapArgs } from \"./handle-runtime\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n // Handle instances anywhere in the tuple become their wire id strings.\n args: argArray.map((arg) => unwrapArgs(arg)),\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\nimport type { PluginEventName } from \"./events\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint,\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Show a transient notification toast in the Snaptrude editor.\n *\n * @param message - Text to display.\n * @param type - Severity; `\"error\"`/`\"warning\"` linger a little longer than\n * `\"info\"`. Defaults to `\"info\"`.\n */\n protected notify(\n message: string,\n type: \"info\" | \"warning\" | \"error\" = \"info\",\n ): void {\n ;(this.hostAPI as Record<string, any>).ui.showNotification(message, type)\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n /**\n * Subscribe to a host event (e.g. `\"model:changed\"`, fired debounced on any\n * user- or plugin-initiated model edit). The callback runs in this worker\n * each time the event fires; its argument is a small structured-clone-safe\n * payload (see the event's payload type, e.g. `ModelChangedEvent`).\n *\n * Fire-and-forget: subscribe once (typically in `init()`). Subscriptions live\n * for the plugin's lifetime and are cleared automatically when the plugin is\n * stopped — there is no `unsubscribe` yet.\n *\n * @example\n * ```ts\n * import type { ModelChangedEvent } from \"@snaptrude/plugin-client\";\n *\n * async init() {\n * this.subscribe(\"model:changed\", (e) => {\n * const { source } = e as ModelChangedEvent;\n * console.log(\"model changed via\", source);\n * });\n * }\n * ```\n */\n protected subscribe(\n event: PluginEventName,\n callback: (payload?: unknown) => void,\n ): void {\n ;(this.hostAPI as Record<string, any>).events.on(\n event,\n Comlink.proxy(callback),\n )\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint,\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n","/**\n * Snaptrude plugin events.\n *\n * Plugins can react to changes in the host app. Events are delivered through\n * the host event bus — a separate channel from the request/response\n * `snaptrude.*` API — and carry a small structured-clone-safe payload, never\n * live model data.\n *\n * Subscribe from a {@link PluginWorker} subclass with `this.subscribe(...)`.\n *\n * NOTE: events are a subscription surface, not `call()` RPC methods, so they do\n * NOT appear in the discovery manifest (which enumerates callable methods\n * only). This module is the typed, discoverable declaration of the surface.\n */\n\n/** Every event a plugin can subscribe to. */\nexport const PLUGIN_EVENTS = [\n \"selection:changed\",\n \"tool:activated\",\n \"project:saved\",\n \"view:changed\",\n \"model:changed\",\n] as const\n\n/** Union of subscribable event names. */\nexport type PluginEventName = (typeof PLUGIN_EVENTS)[number]\n\n/**\n * Payload for `model:changed` — fired (debounced) whenever the project model is\n * mutated, by a user edit OR a plugin edit. Deliberately minimal: it signals\n * *that* the model changed, never *what* changed. Re-query the API for details.\n */\nexport interface ModelChangedEvent {\n /** Origin of the change. Currently always `\"command\"` (the edit chokepoint). */\n source: string\n}\n","import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\nexport * from \"./events\"\nexport {\n flushReleaseQueue,\n intern,\n rewrapResult,\n unwrapArgs,\n __setReleaseTransport,\n} from \"./handle-runtime\"\n\n// Error surface — plugins branch on `PluginError.is(e)` + `e.code`.\nexport {\n PluginError,\n PluginValidationError,\n PluginNotFoundError,\n PluginPermissionError,\n PluginHandleError,\n PluginQuotaError,\n PluginTimeoutError,\n PluginTransportError,\n PluginLifecycleError,\n PluginExecutionError,\n PluginInternalError,\n fromEnvelope,\n isErrorEnvelope,\n isPluginErrorCode,\n PLUGIN_ERROR_CODES,\n CODE_META,\n} from \"@snaptrude/plugin-core\"\nexport type {\n ErrorEnvelope,\n PluginErrorCode,\n WirePluginErrorCode,\n PluginErrorCategory,\n} from \"@snaptrude/plugin-core\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n"],"mappings":"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","names":["out","Comlink","PluginError","fromEnvelope"]}
|
package/dist/plugin-worker.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PluginEventName } from "./events";
|
|
1
2
|
export interface UIMessage {
|
|
2
3
|
action: string;
|
|
3
4
|
payload: unknown;
|
|
@@ -31,12 +32,43 @@ export declare abstract class PluginWorker {
|
|
|
31
32
|
private hostAPI;
|
|
32
33
|
constructor();
|
|
33
34
|
protected sendToUI(action: string, payload: unknown): void;
|
|
35
|
+
/**
|
|
36
|
+
* Show a transient notification toast in the Snaptrude editor.
|
|
37
|
+
*
|
|
38
|
+
* @param message - Text to display.
|
|
39
|
+
* @param type - Severity; `"error"`/`"warning"` linger a little longer than
|
|
40
|
+
* `"info"`. Defaults to `"info"`.
|
|
41
|
+
*/
|
|
42
|
+
protected notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
34
43
|
/**
|
|
35
44
|
* Signal the host that this plugin has finished its work and should be
|
|
36
45
|
* stopped. Use this in headless (UI-less) plugins that run a task and
|
|
37
46
|
* self-terminate.
|
|
38
47
|
*/
|
|
39
48
|
protected complete(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Subscribe to a host event (e.g. `"model:changed"`, fired debounced on any
|
|
51
|
+
* user- or plugin-initiated model edit). The callback runs in this worker
|
|
52
|
+
* each time the event fires; its argument is a small structured-clone-safe
|
|
53
|
+
* payload (see the event's payload type, e.g. `ModelChangedEvent`).
|
|
54
|
+
*
|
|
55
|
+
* Fire-and-forget: subscribe once (typically in `init()`). Subscriptions live
|
|
56
|
+
* for the plugin's lifetime and are cleared automatically when the plugin is
|
|
57
|
+
* stopped — there is no `unsubscribe` yet.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import type { ModelChangedEvent } from "@snaptrude/plugin-client";
|
|
62
|
+
*
|
|
63
|
+
* async init() {
|
|
64
|
+
* this.subscribe("model:changed", (e) => {
|
|
65
|
+
* const { source } = e as ModelChangedEvent;
|
|
66
|
+
* console.log("model changed via", source);
|
|
67
|
+
* });
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
protected subscribe(event: PluginEventName, callback: (payload?: unknown) => void): void;
|
|
40
72
|
init(): Promise<void>;
|
|
41
73
|
destroy(): Promise<void>;
|
|
42
74
|
ping(): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-worker.d.ts","sourceRoot":"","sources":["../src/plugin-worker.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin-worker.d.ts","sourceRoot":"","sources":["../src/plugin-worker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAE/C,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;CACjB;AAMD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,8BAAsB,YAAY;IAChC,SAAS,CAAC,QAAQ,EAAG,MAAM,CAAA;IAC3B,OAAO,CAAC,OAAO,CAAyC;;IAQxD,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAI1D;;;;;;OAMG;IACH,SAAS,CAAC,MAAM,CACd,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,MAAM,GAAG,SAAS,GAAG,OAAgB,GAC1C,IAAI;IAIP;;;;OAIG;IACH,SAAS,CAAC,QAAQ,IAAI,IAAI;IAI1B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,SAAS,CAAC,SAAS,CACjB,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,GACpC,IAAI;IAOD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAKrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;IAIvB,WAAW,CAAC,QAAQ,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrD;;;;;;;OAOG;IACH,KAAK,IAAI,IAAI;CAed"}
|
package/dist/rpc-proxy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-proxy.d.ts","sourceRoot":"","sources":["../src/rpc-proxy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rpc-proxy.d.ts","sourceRoot":"","sources":["../src/rpc-proxy.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CAqBxE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snaptrude/plugin-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -16,9 +16,14 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"upgrade-notes",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
19
24
|
"dependencies": {
|
|
20
25
|
"comlink": "^4.4.2",
|
|
21
|
-
"@snaptrude/plugin-core": "0.
|
|
26
|
+
"@snaptrude/plugin-core": "0.9.0"
|
|
22
27
|
},
|
|
23
28
|
"devDependencies": {
|
|
24
29
|
"tsup": "^8.5.1",
|
|
@@ -28,6 +33,8 @@
|
|
|
28
33
|
"check-types": "tsc --noEmit",
|
|
29
34
|
"build": "tsup --clean",
|
|
30
35
|
"dev": "tsup --watch",
|
|
31
|
-
"clean-dist": "rm -rf dist"
|
|
36
|
+
"clean-dist": "rm -rf dist",
|
|
37
|
+
"check:upgrade-notes": "node scripts/check-upgrade-notes.mjs",
|
|
38
|
+
"test": "node scripts/check-upgrade-notes.mjs && node --test --expose-gc test/*.test.mjs"
|
|
32
39
|
}
|
|
33
40
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
from: 0.4.0
|
|
3
|
+
to: 0.5.0
|
|
4
|
+
breaking: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
Additive expansion, thin release record. 0.5.0 is the first published release of the reorganized, handle-based `snaptrude.*` surface introduced by the 0.4.0 expose initiative (plugin-client 0.4.0 itself was never published to npm), and its changelog entry is deliberately terse ("Exposed more snaptrude APIs"). The verified additions between the 0.4.0 and 0.5.0 version stamps are the `design.*` authoring/query families, the `core.geom` query/update kernel, and the `core` comment/history/project/units/zoom modules, enumerated below at family level. Every method listed exists in the 0.5.0 contract; use the API reference for exact signatures.
|
|
10
|
+
|
|
11
|
+
## Breaking changes
|
|
12
|
+
|
|
13
|
+
_None._
|
|
14
|
+
|
|
15
|
+
## New APIs
|
|
16
|
+
|
|
17
|
+
- `design.create.space(...)` — scene-entity authoring family; also `mass`, `slab`, `floor`, `roof`, `ceiling`, `column`, `beam`, `walls`, `staircase`, `referenceLines`, `furniture`; all return `ComponentHandle`(s) and are undoable.
|
|
18
|
+
- `design.query.listEntities(filters?)` — read-only enumeration/inspection family; also `listByType`, `listWalls`, `listSpaces`, `listMasses`, `listDoors`, `listWindows`, `listFloors`, `listSlabs`, `listRoofs`, `listColumns`, `listBeams`, `listCeilings`, `listStaircases`, `listFurniture`, `listReferenceLines`, `listMullions`, `getById`, `exists`, `getEntityType`, `getLabel`, `getEntityRef`, `listChildren`, `getHost`, `getProperties`, `measure`, `getBoundingBox`, plus `design.query.spaces.getFootprint` and `design.query.geometry.getBrep` / `getBottomContour`.
|
|
19
|
+
- `design.selection.get() / set(components) / add(components) / remove(components) / clear()` — read and mutate the live scene selection (mutators are not undoable).
|
|
20
|
+
- `design.transform.move(components, displacement) / rotate(components, angle) / mirror(...) / getPosition(component) / setPosition(component, position)` — rigid transforms on scene entities.
|
|
21
|
+
- `design.edit.offsetSplit(...)` — offset/split a component's top profile.
|
|
22
|
+
- `design.erase.listErasableEdges(...) / edge(...)` — plan-level adjacency-edge erase.
|
|
23
|
+
- `design.delete.entities(components)` — hard removal of scene BIM entities.
|
|
24
|
+
- `design.boolean.union(...) / subtract(...) / intersect(...)` — OpenCascade CSG on scene masses and spaces; one undoable step, throws on failure.
|
|
25
|
+
- `design.doors.getType / getFamily / getWidth / getHeight / getSupportFloor / getSwingDirection / mirror / setWidth / setHeight` — door-specific reads and edits.
|
|
26
|
+
- `design.windows.getType / getWidth / getHeight / getDimensions / setWidth / setHeight` — window-specific reads and dimension mutations.
|
|
27
|
+
- `design.furniture.listCatalog() / getCatalogItem(id) / exists(id)` — the placeable furniture catalog (library items, not scene entities).
|
|
28
|
+
- `design.materials.list / getInfo / apply / reset / create / getDefault / isDefault / isUniform / hasTexture` — project material library and assignment.
|
|
29
|
+
- `design.lock(components) / unlock(components) / isLocked(component) / listLocked()` — top-level lock verbs.
|
|
30
|
+
- `core.geom.create.line / arc / circle / profileFromLinePoints / profileFromCurves / contourFromProfile / contourFromProfiles` — construct transient geometry handles (points, curves, profiles, contours).
|
|
31
|
+
- `core.geom.delete.profile(handle) / contour(handle)` — release transient geometry handles (frees quota; never deletes engine-side geometry).
|
|
32
|
+
- `core.geom.query.curve.getLength(curve)` — curve read family (28 methods): `getStartPoint`, `getEndPoint`, `getMidPoint`, `getChordLength`, `getTangent`, `getNormal`, `getNearestPoint`, `getProjection`, `getPointAtDistance`, `getParameterAtPoint`, `getCurvature`, `getDistanceToPoint`, `getDistanceAlong`, `listPoints`, `listSubdivisions`, `isLinear`, `isOnCurve`, `hasPoint`, `isEqual`, `isOverlapping`, `getCommonPart`, `listIntersections`, `getMergedCurve`, `isContinuous`, `isParallel`, `getShortestGap`, `getDistanceBetween`.
|
|
33
|
+
- `core.geom.query.arc.getRadius(arc)` — arc reads; also `getCentre`, `getAxis`, `getSweepAngle`, `isValid`.
|
|
34
|
+
- `core.geom.query.circle.getRadius(circle)` — circle reads; also `getCentre`, `getAxis`, `getLength`, `isValid`, `getNearestPoint`, `hasPoint`, `getTangent`, `getNormal`, `getPointAtDistance`, `isEqual`, `listPoints`.
|
|
35
|
+
- `core.geom.query.profile.getArea(profile)` — profile reads; also `listPoints`, `listCurves`, `getPerimeter`, `getBoundingBox`, `getNormal`, `getNearestPoint`, `hasPoint`, `isOnBoundary`, `isClosed`, `isPlanar`, `isClockwise`, `isSelfIntersecting`, `isEmpty`, `isEqual`.
|
|
36
|
+
- `core.geom.query.contour.getOuterProfile(contour)` — contour reads; also `listHoles`, `getHoleCount`, `getArea`, `getPerimeter`, `getBoundingBox`, `getNormal`, `getNearestPoint`, `hasPoint`, `isOnBoundary`, `isClockwise`, `isPlanar`, `isEmpty`, `isSelfIntersecting`, `isOrientationValid`, `isEqual`.
|
|
37
|
+
- `core.geom.query.brep.listFaces(brep)` — BREP topology reads; also `listEdges`, `listVertices`, `listHalfEdges`, `getEdge`, `hasEdge`, `listEdgesBetween`, `getVertexPosition`, `listVertexPositions`, `getCentroid`, `getFaceCount`, `getEdgeCount`, `getVertexCount`, `getHalfEdgeCount`, `getBoundingBox`, `isEqual`; plus per-node reads on `core.geom.query.face`, `core.geom.query.edge`, `core.geom.query.halfedge` (`getNext`/`getPrev`/`getFlip` traversals), and `core.geom.query.vertex`.
|
|
38
|
+
- `core.geom.update.curve.extend / split / trim / offset / reverse` — derive new curves; inputs are never mutated.
|
|
39
|
+
- `core.geom.update.profile.offset / move / rotate / scale / mirror / add / remove / reverse / simplify` — derive new profiles.
|
|
40
|
+
- `core.geom.update.contour.offset / move / rotate / scale / mirror / addHole / removeHole` — derive new contours.
|
|
41
|
+
- `core.comment.create / update / resolve / reopen / isResolved / tag / delete / list` — scene/project comments anchored to components.
|
|
42
|
+
- `core.history.undo(steps?) / redo(steps?)` — step Snaptrude's command stack; reports steps actually applied.
|
|
43
|
+
- `core.project.getTolerance() / setTolerance(value)` — project settings family; also snap controls (dimension/angle/parallel/normal/magnetic enable/disable/get/set) and grid controls (`enable`, `disable`, `setValue`, `getValue`).
|
|
44
|
+
- `core.units.listTypes() / getType() / setType(type) / getBabylonType() / convert(value, from, to)` — unit types and conversion (babylon is the internal storage unit).
|
|
45
|
+
- `core.zoom.extents() / selection()` — frame the viewport camera on scene geometry.
|
|
46
|
+
|
|
47
|
+
## Behavior changes
|
|
48
|
+
|
|
49
|
+
- The transient in-repo `core.geom` shape (`geom.arc` / `geom.curve` / `geom.line` / `geom.profile` modules) was reorganized into the `create` / `query` / `update` / `delete` submodules before 0.5.0 shipped. No published plugin-client release ever exposed the old shape, so no migration applies.
|
|
50
|
+
|
|
51
|
+
## No action needed
|
|
52
|
+
|
|
53
|
+
If your plugin builds against the 0.4.0-era contract (positional arguments and handles), it compiles and runs unchanged on 0.5.0 — all changes are additive. Bump the `@snaptrude/plugin-client` pin to `0.5.0` and reinstall. Plugins older than 0.4.0 (the args-object, by-value surface) must first migrate through the 0.4.0 breaking rewrite documented in the plugin-core CHANGELOG.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
from: 0.5.0
|
|
3
|
+
to: 0.6.0
|
|
4
|
+
breaking: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
Additive release. 0.6.0 introduces the `analysis` namespace (sun and light studies), the `core.io` namespace (file import plus underlay/terrain management), bulk selection via `design.selection.setByFilter`, and geo reads on `program.site`. No existing API changed shape or behavior — upgrading requires only bumping the `@snaptrude/plugin-client` dependency to `0.6.0` and reinstalling.
|
|
10
|
+
|
|
11
|
+
## Breaking changes
|
|
12
|
+
|
|
13
|
+
_None._
|
|
14
|
+
|
|
15
|
+
## New APIs
|
|
16
|
+
|
|
17
|
+
- `analysis.sunpath.enable() / disable() / isActive()` — toggle the 3D sun-path diagram over the scene; `enable` throws without a geo-located site.
|
|
18
|
+
- `analysis.shadows.enable() / disable() / isEnabled()` — toggle real-time sun shadows; needs a geo-located site.
|
|
19
|
+
- `analysis.shadows.setDateTime(dateTime) / getDateTime()` — position the sun by local ISO date-time string; times snap down to the half-hour grid.
|
|
20
|
+
- `analysis.sunlightHours.compute(options) / get(job) / cancel(job) / reset()` — async sunlight-hours heatmap study over a date range; `compute` starts a backend job, `get` polls status/result; needs a space surface (Room or Department Mass).
|
|
21
|
+
- `analysis.illuminance.compute(options) / get(job) / cancel(job) / reset()` — async illuminance (lux) heatmap study; Pro-gated; needs BIM slab/roof objects and the 3D view.
|
|
22
|
+
- `analysis.heatmaps.renderSpaces(items, options) / renderGrid(samples, options) / reset() / isActive()` — render plugin-computed scalar data as a heatmap (flat colour per space top face, or a coloured grid mesh from world-coordinate samples); single active heatmap, values outside `[min, max]` clamp to the end colours, ephemeral (auto-cleared on scene-mutating edits, never persisted).
|
|
23
|
+
- `core.io.import.image(source, options) / pdf(source, options) / dwg(source, options) / cadJson(source, options) / model(source, options) / terrain(location, options)` — bring external files onto a storey; sources are `https://` or `data:` URLs; slow formats return an `ImportJobHandle` instead of blocking.
|
|
24
|
+
- `core.io.query.getPdfPageCount(source) / listCadLayers(source)` — pre-import source inspection.
|
|
25
|
+
- `core.io.job.getStatus(job) / isComplete(job) / getResult(job) / getError(job)` — poll long-running asynchronous imports; always poll with a timeout (a UI-cancelled job stays `processing`).
|
|
26
|
+
- `core.io.underlay.list() / getBounds(underlay) / getScale(underlay) / setScale(underlay, scale) / resetScale(underlay) / getOpacity(underlay) / setOpacity(underlay, opacity) / delete(underlay)` — inspect and edit placed image/PDF/CAD underlays (scale applies to image and PDF only).
|
|
27
|
+
- `core.io.terrain.exists() / get() / getDatum() / setDatum(shift) / getReport() / isElevationEnabled() / enableElevation() / disableElevation() / isSatelliteEnabled() / enableSatellite() / disableSatellite() / getOpacity() / setOpacity(opacity) / delete()` — manage the imported map terrain; `setDatum` is a relative datum shift, not an absolute elevation.
|
|
28
|
+
- `design.selection.setByFilter(filter)` — replace the selection with every visible entity of the active building matching `{ storeys?: number[], types?: PluginSelectionEntityType[] }` (fields AND, values within a field OR; at least one field required — use `clear()` to deselect); one-shot bulk select, not undoable, like the other selection mutators. Introduces the `PluginSelectionEntityType` vocabulary covering all 24 filter-menu kinds, including selection-only kinds the query surface cannot reach.
|
|
29
|
+
- `program.site.getLocation()` — the site's geo location (lat/lng).
|
|
30
|
+
- `program.site.getNorthAngle()` — the site's true-north angle.
|
|
31
|
+
|
|
32
|
+
## Behavior changes
|
|
33
|
+
|
|
34
|
+
_None._
|
|
35
|
+
|
|
36
|
+
## No action needed
|
|
37
|
+
|
|
38
|
+
Existing 0.5.0 plugin code compiles and runs unchanged against 0.6.0. The `analysis` namespace is mounted automatically by the client; no wiring changes are required. Bump the `@snaptrude/plugin-client` pin to `0.6.0`, reinstall, and rebuild.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
from: 0.6.0
|
|
3
|
+
to: 0.7.0
|
|
4
|
+
breaking: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
0.7.0 is a broad API wave: design options (`core.proposals`), async layout jobs (`program.layout`), door/window catalogs and placement, smart layout cloning, per-face materials, staircase params, footprint-area locks, storey duplication, presentation imports and view settings, and typed plugin API errors. Everything is additive except the error envelope — plugins that string-match host error messages need a small update.
|
|
10
|
+
|
|
11
|
+
## Breaking changes
|
|
12
|
+
|
|
13
|
+
- **Host errors are now typed envelopes.** Failures no longer arrive as flattened `Execution error: <msg>` strings: errors carry a stable `code` (shared plugin-core registry), the offending handle, an authored `hint`, and `retryAfterMs` / `didYouMean` where applicable. Code that string-matched on `Execution error:` prefixes must switch to matching `error.code`.
|
|
14
|
+
|
|
15
|
+
## New APIs
|
|
16
|
+
|
|
17
|
+
- `core.proposals.*` — design options: `list` / `get` / `getActive` / `listForComponent` / `isActive` (reads) plus `create` / `rename` / `setActive` / `delete` (non-undoable writes).
|
|
18
|
+
- `program.layout.*` — space solver as an async-job family: `arrange` / `pack` start a backend job and return immediately; `getState` (poll) and `cancel`.
|
|
19
|
+
- `design.doors.*` / `design.windows.*` — `listCatalogGroups` / `listCatalog` / `getCatalogItem` / `exists` (shared catalog DTOs); `design.create.door` / `design.create.window` place a catalog item into a host wall.
|
|
20
|
+
- `design.create.smartLayout` — clone an in-scene template cluster into target spaces in a single undo batch.
|
|
21
|
+
- `design.furniture.listCategories` + `category` filter on `listCatalog`; `thumbnailUrl` on catalog items.
|
|
22
|
+
- `design.transform.align` — snap components' bounding-box edges (left/right/center/top/bottom/middle), optionally to a reference.
|
|
23
|
+
- `design.materials.applyToFaces` / `resetFaces` / `getByFace` / `listByFace` — per-face materials (BREP faces by durable index, visual-only).
|
|
24
|
+
- `design.query.getStaircaseParams` / `design.update.staircase` — staircase parametric read/sparse write.
|
|
25
|
+
- `design.lockArea` / `unlockArea` / `isAreaLocked` / `listAreaLocked` — footprint-area lock for Room/Department spaces.
|
|
26
|
+
- `entity.story.duplicate` — duplicate a storey (or a subset) up/down, auto-creating the target storey.
|
|
27
|
+
- `presentation.import.svg` — import a guaranteed-vector SVG onto the Present sheet.
|
|
28
|
+
- `presentation.views.getSettings` / `updateSettings` — view display settings (background, color mode, axis, edges, labels).
|
|
29
|
+
- `design.update.setLabel` — set any component's panel Label (write pair of `design.query.getLabel`); undoable, lock-aware, validated (supersedes the cancelled 0.6.1 patch).
|
|
30
|
+
- `program.areas` — area members now report `areaClass` (`NET` / `GROSS` / `EXCLUDED`).
|
|
31
|
+
|
|
32
|
+
## Behavior changes
|
|
33
|
+
|
|
34
|
+
- Zod validation failures forward the first 10 issues as `{path, code, message}` (full count in `details.issueCount`); internal faults are sanitized and host-logged under an `errorId`.
|
|
35
|
+
|
|
36
|
+
## No action needed
|
|
37
|
+
|
|
38
|
+
Plugin code that does not string-match host error messages compiles and runs unchanged against 0.7.0 — the entire API surface above is additive. Bump the `@snaptrude/plugin-client` pin to `0.7.0`, reinstall, and rebuild; if you matched on `Execution error:` strings, switch those checks to `error.code`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
from: 0.7.0
|
|
3
|
+
to: 0.7.1
|
|
4
|
+
breaking: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
Patch release. 0.7.1 expands `presentation.aiInspiration` to the full AI Inspiration surface (18 methods) now that the host implementation has landed, and adds job-mode execution to the generate/refine family. No existing API changed shape or behavior.
|
|
10
|
+
|
|
11
|
+
## Breaking changes
|
|
12
|
+
|
|
13
|
+
_None._
|
|
14
|
+
|
|
15
|
+
## New APIs
|
|
16
|
+
|
|
17
|
+
- `presentation.aiInspiration.getModelCapabilities` / `getSelection` / `getPresetCatalog` — capability and context reads.
|
|
18
|
+
- `presentation.aiInspiration.getJob` / `cancelJob` / `listJobs` — job lifecycle for job-mode runs.
|
|
19
|
+
- `presentation.aiInspiration.getWorkflowForShape` / `getSelectedBranchRerunPlan` / `rerunSelectedBranch` — workflow inspection and branch reruns.
|
|
20
|
+
- `presentation.aiInspiration.extractRecipeFromShape` / `listRecipes` / `saveRecipe` / `deleteRecipe` / `runRecipe` — recipe management (plus job/workflow/recipe Zod schemas).
|
|
21
|
+
|
|
22
|
+
## Behavior changes
|
|
23
|
+
|
|
24
|
+
- `generate` / `refine` (and `rerunSelectedBranch` / `runRecipe`) accept `runMode: "blocking" | "job"`; `PluginAIInspirationRunResult` is accordingly loosened to `{ jobId?, output?, outputs?, metadata? }` — job-mode calls resolve with a `jobId` instead of outputs. Blocking-mode calls behave exactly as before.
|
|
25
|
+
|
|
26
|
+
## No action needed
|
|
27
|
+
|
|
28
|
+
Existing 0.7.0 plugin code compiles and runs unchanged against 0.7.1. Bump the `@snaptrude/plugin-client` pin to `0.7.1`, reinstall, and rebuild.
|