@vibecontrols/plugin-sdk 2026.509.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 (40) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +206 -0
  3. package/boilerplate/.github/workflows/release.template.yml +57 -0
  4. package/boilerplate/README.md +41 -0
  5. package/boilerplate/bunfig.toml +13 -0
  6. package/boilerplate/eslint.config.base.js +36 -0
  7. package/boilerplate/lefthook.base.yml +10 -0
  8. package/boilerplate/package.template.json +52 -0
  9. package/boilerplate/tsconfig.base.json +20 -0
  10. package/dist/audit/index.d.ts +17 -0
  11. package/dist/audit/index.js +19 -0
  12. package/dist/broadcast/index.d.ts +15 -0
  13. package/dist/broadcast/index.js +14 -0
  14. package/dist/cli/index.d.ts +75 -0
  15. package/dist/cli/index.js +104 -0
  16. package/dist/config/index.d.ts +34 -0
  17. package/dist/config/index.js +66 -0
  18. package/dist/contract/index.d.ts +118 -0
  19. package/dist/contract/index.js +16 -0
  20. package/dist/http/index.d.ts +35 -0
  21. package/dist/http/index.js +70 -0
  22. package/dist/index.d.ts +15 -0
  23. package/dist/index.js +595 -0
  24. package/dist/lifecycle/index.d.ts +31 -0
  25. package/dist/lifecycle/index.js +30 -0
  26. package/dist/log/index.d.ts +22 -0
  27. package/dist/log/index.js +25 -0
  28. package/dist/providers/index.d.ts +29 -0
  29. package/dist/providers/index.js +38 -0
  30. package/dist/routes/index.d.ts +37 -0
  31. package/dist/routes/index.js +77 -0
  32. package/dist/storage/index.d.ts +36 -0
  33. package/dist/storage/index.js +67 -0
  34. package/dist/subprocess/index.d.ts +37 -0
  35. package/dist/subprocess/index.js +69 -0
  36. package/dist/telemetry/index.d.ts +27 -0
  37. package/dist/telemetry/index.js +41 -0
  38. package/dist/testing/index.d.ts +31 -0
  39. package/dist/testing/index.js +97 -0
  40. package/package.json +159 -0
@@ -0,0 +1,29 @@
1
+ import { HostServices, ServiceRegistry } from '../contract/index.js';
2
+
3
+ /**
4
+ * @vibecontrols/plugin-sdk/providers
5
+ *
6
+ * Thin façade over `hostServices.serviceRegistry` — provider plugins call
7
+ * `register` and consumer plugins call `get` / `list`. Every method is a
8
+ * graceful no-op when the host has no registry.
9
+ */
10
+
11
+ interface CliContribution {
12
+ statusSections?: unknown[];
13
+ doctorChecks?: unknown[];
14
+ }
15
+ declare class ProviderRegistry {
16
+ private readonly hostServices?;
17
+ constructor(hostServices?: HostServices | undefined);
18
+ getServiceRegistry(): ServiceRegistry | undefined;
19
+ registerProvider<T>(type: string, name: string, provider: T): void;
20
+ getProvider<T>(type: string, name: string): T | undefined;
21
+ listProviders(type: string): string[];
22
+ /**
23
+ * Register a CLI contribution bundle (status sections + doctor checks).
24
+ * No-op when the host doesn't expose `cliContributors`.
25
+ */
26
+ withCliContribution(contribution: CliContribution): void;
27
+ }
28
+
29
+ export { type CliContribution, ProviderRegistry };
@@ -0,0 +1,38 @@
1
+ // src/providers/index.ts
2
+ var ProviderRegistry = class {
3
+ constructor(hostServices) {
4
+ this.hostServices = hostServices;
5
+ }
6
+ hostServices;
7
+ getServiceRegistry() {
8
+ return this.hostServices?.serviceRegistry;
9
+ }
10
+ registerProvider(type, name, provider) {
11
+ this.hostServices?.serviceRegistry?.registerService(type, name, provider);
12
+ }
13
+ getProvider(type, name) {
14
+ return this.hostServices?.serviceRegistry?.getService(type, name);
15
+ }
16
+ listProviders(type) {
17
+ const reg = this.hostServices?.serviceRegistry;
18
+ return reg?.listProvidersForType?.(type) ?? [];
19
+ }
20
+ /**
21
+ * Register a CLI contribution bundle (status sections + doctor checks).
22
+ * No-op when the host doesn't expose `cliContributors`.
23
+ */
24
+ withCliContribution(contribution) {
25
+ const contributors = this.hostServices?.cliContributors;
26
+ if (!contributors) return;
27
+ for (const section of contribution.statusSections ?? []) {
28
+ contributors.addStatusSection?.(section);
29
+ }
30
+ for (const check of contribution.doctorChecks ?? []) {
31
+ contributors.addDoctorCheck?.(check);
32
+ }
33
+ }
34
+ };
35
+
36
+ export { ProviderRegistry };
37
+ //# sourceMappingURL=index.js.map
38
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,37 @@
1
+ import { Elysia } from 'elysia';
2
+ import { HostServices } from '../contract/index.js';
3
+
4
+ /**
5
+ * @vibecontrols/plugin-sdk/routes
6
+ *
7
+ * RoutesBuilder — fluent wrapper around an Elysia instance with the four
8
+ * concerns every plugin reimplements: prefix, API-key auth, error handler,
9
+ * request logging.
10
+ *
11
+ * Elysia is a peer dependency — both type and value come from the consumer.
12
+ * If a plugin doesn't ship HTTP routes it never imports this module so
13
+ * elysia stays an optional peer.
14
+ */
15
+
16
+ type ApiKeyResolver = () => string | Promise<string>;
17
+ declare class RoutesBuilder {
18
+ private readonly pluginName;
19
+ private readonly hostServices?;
20
+ private prefix?;
21
+ private apiKeyResolver?;
22
+ private errorHandlerEnabled;
23
+ private loggingEnabled;
24
+ constructor(pluginName: string, hostServices?: HostServices | undefined);
25
+ withPrefix(prefix: string): this;
26
+ withAuth(getApiKey: ApiKeyResolver): this;
27
+ withErrorHandler(): this;
28
+ withLogging(): this;
29
+ /**
30
+ * Build the configured Elysia instance. Plugins typically call
31
+ * `.derive` / `.get` / `.post` on the result before returning it from
32
+ * `createRoutes()`.
33
+ */
34
+ build(): Elysia;
35
+ }
36
+
37
+ export { type ApiKeyResolver, RoutesBuilder };
@@ -0,0 +1,77 @@
1
+ import { Elysia } from 'elysia';
2
+
3
+ // src/routes/index.ts
4
+ var RoutesBuilder = class {
5
+ constructor(pluginName, hostServices) {
6
+ this.pluginName = pluginName;
7
+ this.hostServices = hostServices;
8
+ }
9
+ pluginName;
10
+ hostServices;
11
+ prefix;
12
+ apiKeyResolver;
13
+ errorHandlerEnabled = false;
14
+ loggingEnabled = false;
15
+ withPrefix(prefix) {
16
+ this.prefix = prefix;
17
+ return this;
18
+ }
19
+ withAuth(getApiKey) {
20
+ this.apiKeyResolver = getApiKey;
21
+ return this;
22
+ }
23
+ withErrorHandler() {
24
+ this.errorHandlerEnabled = true;
25
+ return this;
26
+ }
27
+ withLogging() {
28
+ this.loggingEnabled = true;
29
+ return this;
30
+ }
31
+ /**
32
+ * Build the configured Elysia instance. Plugins typically call
33
+ * `.derive` / `.get` / `.post` on the result before returning it from
34
+ * `createRoutes()`.
35
+ */
36
+ build() {
37
+ const opts = this.prefix ? { prefix: this.prefix } : void 0;
38
+ const app = new Elysia(opts);
39
+ if (this.apiKeyResolver) {
40
+ const resolver = this.apiKeyResolver;
41
+ app.onBeforeHandle(async ({ request, set }) => {
42
+ const supplied = request.headers.get("x-api-key");
43
+ const expected = await resolver();
44
+ if (!supplied || supplied !== expected) {
45
+ set.status = 401;
46
+ return { error: "unauthorized" };
47
+ }
48
+ return;
49
+ });
50
+ }
51
+ if (this.errorHandlerEnabled) {
52
+ const pluginName = this.pluginName;
53
+ const logger = this.hostServices?.logger;
54
+ app.onError(({ error, set }) => {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ logger?.error?.(pluginName, "route error", { message });
57
+ set.status = 500;
58
+ return { error: message };
59
+ });
60
+ }
61
+ if (this.loggingEnabled) {
62
+ const pluginName = this.pluginName;
63
+ const logger = this.hostServices?.logger;
64
+ app.onRequest(({ request }) => {
65
+ logger?.debug?.(pluginName, "request", {
66
+ method: request.method,
67
+ url: request.url
68
+ });
69
+ });
70
+ }
71
+ return app;
72
+ }
73
+ };
74
+
75
+ export { RoutesBuilder };
76
+ //# sourceMappingURL=index.js.map
77
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,36 @@
1
+ import { StorageProvider, SdkLogger } from '../contract/index.js';
2
+
3
+ /**
4
+ * Typed wrappers over a `StorageProvider`.
5
+ *
6
+ * `TypedStore<T>` is a single-key handle: `get/set/delete` against a fixed
7
+ * (namespace, key) pair, with JSON serialisation + corrupt-payload recovery.
8
+ * `NamespaceStore` groups multiple typed handles under one namespace and
9
+ * exposes raw get/set/delete for ad-hoc access.
10
+ */
11
+
12
+ declare class TypedStore<T> {
13
+ private readonly storage;
14
+ private readonly namespace;
15
+ private readonly key;
16
+ private readonly logger?;
17
+ private readonly pluginName;
18
+ constructor(storage: StorageProvider, namespace: string, key: string, logger?: SdkLogger | undefined, pluginName?: string);
19
+ get(): Promise<T | null>;
20
+ set(value: T): Promise<void>;
21
+ delete(): Promise<boolean>;
22
+ }
23
+ declare class NamespaceStore {
24
+ private readonly storage;
25
+ private readonly namespace;
26
+ private readonly logger?;
27
+ private readonly pluginName;
28
+ constructor(storage: StorageProvider, namespace: string, logger?: SdkLogger | undefined, pluginName?: string);
29
+ /** Get a typed handle for `(this.namespace, key)`. */
30
+ typed<T>(key: string): TypedStore<T>;
31
+ get<T = unknown>(key: string): Promise<T | null>;
32
+ set<T = unknown>(key: string, value: T): Promise<void>;
33
+ delete(key: string): Promise<boolean>;
34
+ }
35
+
36
+ export { NamespaceStore, TypedStore };
@@ -0,0 +1,67 @@
1
+ // src/storage/typed-store.ts
2
+ var TypedStore = class {
3
+ constructor(storage, namespace, key, logger, pluginName = "plugin") {
4
+ this.storage = storage;
5
+ this.namespace = namespace;
6
+ this.key = key;
7
+ this.logger = logger;
8
+ this.pluginName = pluginName;
9
+ }
10
+ storage;
11
+ namespace;
12
+ key;
13
+ logger;
14
+ pluginName;
15
+ async get() {
16
+ const raw = await this.storage.get(this.namespace, this.key);
17
+ if (raw === null || raw === void 0) return null;
18
+ if (typeof raw !== "string") {
19
+ return raw;
20
+ }
21
+ try {
22
+ return JSON.parse(raw);
23
+ } catch (err) {
24
+ this.logger?.error?.(this.pluginName, "TypedStore: corrupt JSON", {
25
+ namespace: this.namespace,
26
+ key: this.key,
27
+ message: err instanceof Error ? err.message : String(err)
28
+ });
29
+ return null;
30
+ }
31
+ }
32
+ async set(value) {
33
+ await this.storage.set(this.namespace, this.key, JSON.stringify(value));
34
+ }
35
+ async delete() {
36
+ return this.storage.delete(this.namespace, this.key);
37
+ }
38
+ };
39
+ var NamespaceStore = class {
40
+ constructor(storage, namespace, logger, pluginName = "plugin") {
41
+ this.storage = storage;
42
+ this.namespace = namespace;
43
+ this.logger = logger;
44
+ this.pluginName = pluginName;
45
+ }
46
+ storage;
47
+ namespace;
48
+ logger;
49
+ pluginName;
50
+ /** Get a typed handle for `(this.namespace, key)`. */
51
+ typed(key) {
52
+ return new TypedStore(this.storage, this.namespace, key, this.logger, this.pluginName);
53
+ }
54
+ async get(key) {
55
+ return this.storage.get(this.namespace, key);
56
+ }
57
+ async set(key, value) {
58
+ return this.storage.set(this.namespace, key, value);
59
+ }
60
+ async delete(key) {
61
+ return this.storage.delete(this.namespace, key);
62
+ }
63
+ };
64
+
65
+ export { NamespaceStore, TypedStore };
66
+ //# sourceMappingURL=index.js.map
67
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,37 @@
1
+ import { SdkLogger } from '../contract/index.js';
2
+
3
+ /**
4
+ * @vibecontrols/plugin-sdk/subprocess
5
+ *
6
+ * Cross-platform subprocess helpers: graceful kill (SIGTERM → SIGKILL),
7
+ * liveness check, port-pool probing, sleep.
8
+ *
9
+ * Design notes:
10
+ * - `gracefulKill` uses SIGTERM first, polls `process.kill(pid, 0)` to
11
+ * detect exit, then escalates to SIGKILL. On Windows SIGTERM still
12
+ * arrives via Node's signal-emulation; we keep the same code path.
13
+ * - `findAvailablePort` prefers `Bun.listen` (zero-dep, fast) and falls
14
+ * back to `node:net` so the helper works under pure Node hosts too.
15
+ *
16
+ * Cross-platform: pure Node + optional Bun runtime feature detection.
17
+ */
18
+
19
+ declare function sleep(ms: number): Promise<void>;
20
+ /**
21
+ * Returns true if a process with `pid` is reachable from this process.
22
+ * Uses signal 0 — POSIX + Node's Windows shim both recognise it.
23
+ */
24
+ declare function isProcessAlive(pid: number): boolean;
25
+ /**
26
+ * Send SIGTERM, poll until the process exits or `timeoutMs` elapses, then
27
+ * escalate to SIGKILL if necessary.
28
+ */
29
+ declare function gracefulKill(pid: number, timeoutMs?: number, logger?: SdkLogger): Promise<void>;
30
+ /**
31
+ * Probe ports starting at `start` for `range` consecutive numbers; resolve
32
+ * with the first one that bind-tests cleanly. Throws if the whole range is
33
+ * occupied.
34
+ */
35
+ declare function findAvailablePort(start: number, range?: number): Promise<number>;
36
+
37
+ export { findAvailablePort, gracefulKill, isProcessAlive, sleep };
@@ -0,0 +1,69 @@
1
+ import { createServer } from 'net';
2
+
3
+ // src/subprocess/index.ts
4
+ function sleep(ms) {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
7
+ function isProcessAlive(pid) {
8
+ try {
9
+ process.kill(pid, 0);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+ async function gracefulKill(pid, timeoutMs = 3e3, logger) {
16
+ const source = "subprocess";
17
+ if (!isProcessAlive(pid)) return;
18
+ try {
19
+ process.kill(pid, "SIGTERM");
20
+ } catch (err) {
21
+ logger?.warn?.(source, "SIGTERM failed", {
22
+ pid,
23
+ message: err instanceof Error ? err.message : String(err)
24
+ });
25
+ return;
26
+ }
27
+ const pollMs = 50;
28
+ let elapsed = 0;
29
+ while (elapsed < timeoutMs) {
30
+ await sleep(pollMs);
31
+ elapsed += pollMs;
32
+ if (!isProcessAlive(pid)) return;
33
+ }
34
+ try {
35
+ process.kill(pid, "SIGKILL");
36
+ } catch (err) {
37
+ logger?.warn?.(source, "SIGKILL failed", {
38
+ pid,
39
+ message: err instanceof Error ? err.message : String(err)
40
+ });
41
+ }
42
+ }
43
+ async function findAvailablePort(start, range = 200) {
44
+ for (let i = 0; i < range; i++) {
45
+ const port = start + i;
46
+ if (await isPortFree(port)) return port;
47
+ }
48
+ throw new Error(`findAvailablePort: no port free in [${start}, ${start + range - 1}]`);
49
+ }
50
+ function isPortFree(port) {
51
+ return new Promise((resolve) => {
52
+ const server = createServer();
53
+ server.once("error", () => {
54
+ resolve(false);
55
+ });
56
+ server.once("listening", () => {
57
+ server.close(() => resolve(true));
58
+ });
59
+ try {
60
+ server.listen({ port, host: "127.0.0.1" });
61
+ } catch {
62
+ resolve(false);
63
+ }
64
+ });
65
+ }
66
+
67
+ export { findAvailablePort, gracefulKill, isProcessAlive, sleep };
68
+ //# sourceMappingURL=index.js.map
69
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,27 @@
1
+ import { HostServices } from '../contract/index.js';
2
+
3
+ /**
4
+ * @vibecontrols/plugin-sdk/telemetry
5
+ *
6
+ * `TelemetryEmitter` auto-tags every event with the plugin name + version
7
+ * + ISO timestamp before forwarding to `hostServices.telemetry?.emit`.
8
+ * No-op when the host doesn't expose the telemetry surface (e.g. plugin
9
+ * loaded without the `telemetry: true` capability).
10
+ */
11
+
12
+ declare class TelemetryEmitter {
13
+ private readonly pluginName;
14
+ private readonly pluginVersion;
15
+ private readonly hostServices?;
16
+ constructor(pluginName: string, pluginVersion: string, hostServices?: HostServices | undefined);
17
+ /** Emit `eventName` with auto-tagged plugin/version/timestamp. */
18
+ emit(eventName: string, payload?: Record<string, unknown>): void;
19
+ /** `<pluginName>.ready` shorthand. */
20
+ emitReady(context?: Record<string, unknown>): void;
21
+ /** `<pluginName>.error` shorthand — extracts message from Error. */
22
+ emitError(error: Error, context?: Record<string, unknown>): void;
23
+ /** Generic event-type emit (alias of emit). */
24
+ emitEvent(type: string, payload?: Record<string, unknown>): void;
25
+ }
26
+
27
+ export { TelemetryEmitter };
@@ -0,0 +1,41 @@
1
+ // src/telemetry/index.ts
2
+ var TelemetryEmitter = class {
3
+ constructor(pluginName, pluginVersion, hostServices) {
4
+ this.pluginName = pluginName;
5
+ this.pluginVersion = pluginVersion;
6
+ this.hostServices = hostServices;
7
+ }
8
+ pluginName;
9
+ pluginVersion;
10
+ hostServices;
11
+ /** Emit `eventName` with auto-tagged plugin/version/timestamp. */
12
+ emit(eventName, payload) {
13
+ const target = this.hostServices?.telemetry;
14
+ if (!target) return;
15
+ target.emit(eventName, {
16
+ plugin: this.pluginName,
17
+ version: this.pluginVersion,
18
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
19
+ ...payload ?? {}
20
+ });
21
+ }
22
+ /** `<pluginName>.ready` shorthand. */
23
+ emitReady(context) {
24
+ this.emit(`${this.pluginName}.ready`, context);
25
+ }
26
+ /** `<pluginName>.error` shorthand — extracts message from Error. */
27
+ emitError(error, context) {
28
+ this.emit(`${this.pluginName}.error`, {
29
+ message: error.message,
30
+ ...context ?? {}
31
+ });
32
+ }
33
+ /** Generic event-type emit (alias of emit). */
34
+ emitEvent(type, payload) {
35
+ this.emit(type, payload);
36
+ }
37
+ };
38
+
39
+ export { TelemetryEmitter };
40
+ //# sourceMappingURL=index.js.map
41
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,31 @@
1
+ import { HostServices, ProfileContext, VibePluginFactory } from '../contract/index.js';
2
+
3
+ /**
4
+ * Mock factories for unit-testing plugins built on the SDK.
5
+ *
6
+ * The mocks return concrete `HostServices` / `ProfileContext` shapes with
7
+ * every function backed by Bun's built-in `mock()`. Consumers must run
8
+ * tests with `bun test` (the SDK is Bun-native by design).
9
+ */
10
+
11
+ type DeepPartial<T> = T extends object ? {
12
+ [K in keyof T]?: DeepPartial<T[K]>;
13
+ } : T;
14
+ /**
15
+ * Build a fully-stubbed `HostServices`. Every function is a Bun `mock()`,
16
+ * so tests can assert call counts / args. Pass `overrides` to replace
17
+ * specific fields (e.g. `{ telemetry: { emit: customMock } }`).
18
+ */
19
+ declare function createMockHostServices(overrides?: DeepPartial<HostServices>): HostServices;
20
+ declare function createMockProfileContext(overrides?: DeepPartial<ProfileContext>): ProfileContext;
21
+
22
+ /**
23
+ * Test fixtures for downstream plugin authors.
24
+ *
25
+ * Currently exports a minimal `samplePlugin` factory that satisfies
26
+ * `VibePluginFactory` — handy as a baseline plugin object in tests.
27
+ */
28
+
29
+ declare const samplePlugin: VibePluginFactory;
30
+
31
+ export { createMockHostServices, createMockProfileContext, samplePlugin };
@@ -0,0 +1,97 @@
1
+ import { mock } from 'bun:test';
2
+
3
+ // src/testing/mocks.ts
4
+ function createMockHostServices(overrides = {}) {
5
+ const storage = {
6
+ get: mock(async () => null),
7
+ set: mock(async () => void 0),
8
+ delete: mock(async () => true),
9
+ list: mock(async () => [])
10
+ };
11
+ const serviceRegistry = {
12
+ registerService: mock(() => void 0),
13
+ getService: mock(() => void 0),
14
+ listProvidersForType: mock(() => [])
15
+ };
16
+ const base = {
17
+ storage,
18
+ logger: {
19
+ debug: mock(() => void 0),
20
+ info: mock(() => void 0),
21
+ warn: mock(() => void 0),
22
+ error: mock(() => void 0),
23
+ setLevel: mock(() => void 0)
24
+ },
25
+ serviceRegistry,
26
+ getProvider: mock(() => void 0),
27
+ getAgentBaseUrl: mock(() => "http://localhost:3005"),
28
+ getAgentVersion: mock(() => "test"),
29
+ broadcast: mock(() => void 0),
30
+ // workspaceQuery's runtime contract is generic over T; the mock returns
31
+ // a permissive empty data shape that satisfies any caller-chosen T at
32
+ // runtime. The cast is the standard pattern for representing a generic
33
+ // method via a non-generic mock.
34
+ workspaceQuery: mock(async () => ({ data: {} })),
35
+ isGatewayConfigured: mock(() => false),
36
+ getAgentRecordId: mock(async () => null),
37
+ getWorkspaceId: mock(async () => null),
38
+ getConfig: mock(async () => void 0),
39
+ getPluginRegistry: mock(() => "https://verdaccio.tooling.internal.burdenoff.com/"),
40
+ getDataDir: mock(() => "/tmp/sdk-test"),
41
+ cliContributors: {
42
+ addStatusSection: mock(() => void 0),
43
+ addDoctorCheck: mock(() => void 0)
44
+ },
45
+ audit: { emit: mock(() => void 0) },
46
+ telemetry: { emit: mock(() => void 0) },
47
+ os: void 0
48
+ };
49
+ return mergeDeep(base, overrides);
50
+ }
51
+ function createMockProfileContext(overrides = {}) {
52
+ const base = {
53
+ name: "test-profile",
54
+ dataDir: "/tmp/sdk-test/profile",
55
+ logger: {
56
+ debug: mock(() => void 0),
57
+ info: mock(() => void 0),
58
+ warn: mock(() => void 0),
59
+ error: mock(() => void 0)
60
+ },
61
+ audit: { emit: mock(() => void 0) },
62
+ telemetry: { emit: mock(() => void 0) }
63
+ };
64
+ return mergeDeep(base, overrides);
65
+ }
66
+ function mergeDeep(base, overrides) {
67
+ if (overrides === void 0 || overrides === null) return base;
68
+ if (typeof overrides !== "object" || Array.isArray(overrides)) {
69
+ return overrides;
70
+ }
71
+ if (typeof base !== "object" || base === null || Array.isArray(base)) {
72
+ return overrides;
73
+ }
74
+ const out = { ...base };
75
+ for (const [k, v] of Object.entries(overrides)) {
76
+ const baseVal = base[k];
77
+ if (v !== null && typeof v === "object" && !Array.isArray(v) && baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal)) {
78
+ out[k] = mergeDeep(baseVal, v);
79
+ } else {
80
+ out[k] = v;
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+
86
+ // src/testing/fixtures.ts
87
+ var samplePlugin = (_ctx) => ({
88
+ name: "sample-plugin",
89
+ version: "0.0.0",
90
+ description: "Sample plugin for SDK fixture use",
91
+ tags: ["backend"],
92
+ capabilities: { storage: "rw", telemetry: true }
93
+ });
94
+
95
+ export { createMockHostServices, createMockProfileContext, samplePlugin };
96
+ //# sourceMappingURL=index.js.map
97
+ //# sourceMappingURL=index.js.map