@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
package/dist/index.js ADDED
@@ -0,0 +1,595 @@
1
+ import { Elysia } from 'elysia';
2
+ import { createServer } from 'net';
3
+
4
+ // src/contract/index.ts
5
+ var FULL_TRUST_CAPS = {
6
+ storage: "rw",
7
+ secrets: "rw",
8
+ gateway: true,
9
+ broadcast: true,
10
+ subprocess: true,
11
+ audit: true,
12
+ telemetry: true,
13
+ singletonOnly: false,
14
+ requiresIsolation: false
15
+ };
16
+
17
+ // src/lifecycle/index.ts
18
+ function createLifecycleHooks(spec) {
19
+ const { name, onInit, onShutdown, telemetryEventName, skipPlatforms } = spec;
20
+ return {
21
+ onServerStart: async (_app, hostServices) => {
22
+ if (skipPlatforms && skipPlatforms.includes(process.platform)) {
23
+ process.stderr.write(
24
+ `[${name}] skipping init on unsupported platform '${process.platform}'
25
+ `
26
+ );
27
+ return;
28
+ }
29
+ if (onInit) {
30
+ await onInit(hostServices);
31
+ }
32
+ if (telemetryEventName) {
33
+ hostServices.telemetry?.emit(telemetryEventName, { plugin: name });
34
+ }
35
+ },
36
+ onServerStop: async (hostServices) => {
37
+ if (onShutdown) {
38
+ await onShutdown(hostServices);
39
+ }
40
+ }
41
+ };
42
+ }
43
+
44
+ // src/cli/multimode.ts
45
+ function pickOutputMode(flags) {
46
+ if (flags.json) return "json";
47
+ if (flags.plain) return "plain";
48
+ if (flags.interactive) return "interactive";
49
+ return "auto";
50
+ }
51
+ function isCi() {
52
+ return !!process.env.CI || !!process.env.NO_COLOR || process.env.TERM === "dumb";
53
+ }
54
+ function stdoutIsTty() {
55
+ return Boolean(process.stdout.isTTY);
56
+ }
57
+ async function runMultimode(opts) {
58
+ const data = await opts.fetchData();
59
+ const mode = opts.mode ?? "auto";
60
+ if (mode === "json") {
61
+ const shaped = opts.json ? opts.json(data) : data;
62
+ process.stdout.write(`${JSON.stringify(shaped, null, 2)}
63
+ `);
64
+ return;
65
+ }
66
+ if (mode === "plain") {
67
+ await opts.plain(data);
68
+ return;
69
+ }
70
+ const wantInteractive = (mode === "interactive" || stdoutIsTty() && !isCi()) && !!opts.interactive;
71
+ if (wantInteractive && opts.interactive) {
72
+ try {
73
+ await opts.interactive(data);
74
+ return;
75
+ } catch {
76
+ }
77
+ }
78
+ await opts.plain(data);
79
+ }
80
+ function maybePrintJson(flags, data) {
81
+ if (!flags.json) return false;
82
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
83
+ `);
84
+ return true;
85
+ }
86
+
87
+ // src/cli/redaction.ts
88
+ var SENSITIVE_KEY_RE = /(token|secret|password|apikey|api_key|key|auth|credential|email)/i;
89
+ function redact(value) {
90
+ return redactInner(value);
91
+ }
92
+ function redactInner(value) {
93
+ if (value === null || value === void 0) return value;
94
+ if (Array.isArray(value)) return value.map(redactInner);
95
+ if (typeof value === "object") {
96
+ const out = {};
97
+ for (const [k, v] of Object.entries(value)) {
98
+ if (SENSITIVE_KEY_RE.test(k)) {
99
+ out[k] = "[redacted]";
100
+ continue;
101
+ }
102
+ out[k] = redactInner(v);
103
+ }
104
+ return out;
105
+ }
106
+ return value;
107
+ }
108
+
109
+ // src/cli/command-builder.ts
110
+ var CliCommandBuilder = class {
111
+ constructor(program) {
112
+ this.program = program;
113
+ }
114
+ program;
115
+ /**
116
+ * Register a `<name>` sub-command that fetches once and renders via
117
+ * `--json` / `--plain` / interactive (when stdout is a TTY).
118
+ */
119
+ addStatusCommand(name, spec) {
120
+ this.program.command(name).description(spec.description).option("--json", "emit JSON for scripting").option("--plain", "force plain text (no opentui)").action(async (opts) => {
121
+ const mode = pickOutputMode(opts);
122
+ await runMultimode({
123
+ mode,
124
+ fetchData: spec.fetchData,
125
+ plain: async (data) => {
126
+ const view = spec.redact ? redact(data) : data;
127
+ if (spec.format) {
128
+ await spec.format(view);
129
+ return;
130
+ }
131
+ process.stdout.write(`${JSON.stringify(view, null, 2)}
132
+ `);
133
+ },
134
+ json: (data) => spec.redact ? redact(data) : data
135
+ });
136
+ });
137
+ return this;
138
+ }
139
+ /** Escape hatch: hand back the underlying commander Command. */
140
+ command() {
141
+ return this.program;
142
+ }
143
+ };
144
+ var RoutesBuilder = class {
145
+ constructor(pluginName, hostServices) {
146
+ this.pluginName = pluginName;
147
+ this.hostServices = hostServices;
148
+ }
149
+ pluginName;
150
+ hostServices;
151
+ prefix;
152
+ apiKeyResolver;
153
+ errorHandlerEnabled = false;
154
+ loggingEnabled = false;
155
+ withPrefix(prefix) {
156
+ this.prefix = prefix;
157
+ return this;
158
+ }
159
+ withAuth(getApiKey) {
160
+ this.apiKeyResolver = getApiKey;
161
+ return this;
162
+ }
163
+ withErrorHandler() {
164
+ this.errorHandlerEnabled = true;
165
+ return this;
166
+ }
167
+ withLogging() {
168
+ this.loggingEnabled = true;
169
+ return this;
170
+ }
171
+ /**
172
+ * Build the configured Elysia instance. Plugins typically call
173
+ * `.derive` / `.get` / `.post` on the result before returning it from
174
+ * `createRoutes()`.
175
+ */
176
+ build() {
177
+ const opts = this.prefix ? { prefix: this.prefix } : void 0;
178
+ const app = new Elysia(opts);
179
+ if (this.apiKeyResolver) {
180
+ const resolver = this.apiKeyResolver;
181
+ app.onBeforeHandle(async ({ request, set }) => {
182
+ const supplied = request.headers.get("x-api-key");
183
+ const expected = await resolver();
184
+ if (!supplied || supplied !== expected) {
185
+ set.status = 401;
186
+ return { error: "unauthorized" };
187
+ }
188
+ return;
189
+ });
190
+ }
191
+ if (this.errorHandlerEnabled) {
192
+ const pluginName = this.pluginName;
193
+ const logger = this.hostServices?.logger;
194
+ app.onError(({ error, set }) => {
195
+ const message = error instanceof Error ? error.message : String(error);
196
+ logger?.error?.(pluginName, "route error", { message });
197
+ set.status = 500;
198
+ return { error: message };
199
+ });
200
+ }
201
+ if (this.loggingEnabled) {
202
+ const pluginName = this.pluginName;
203
+ const logger = this.hostServices?.logger;
204
+ app.onRequest(({ request }) => {
205
+ logger?.debug?.(pluginName, "request", {
206
+ method: request.method,
207
+ url: request.url
208
+ });
209
+ });
210
+ }
211
+ return app;
212
+ }
213
+ };
214
+
215
+ // src/telemetry/index.ts
216
+ var TelemetryEmitter = class {
217
+ constructor(pluginName, pluginVersion, hostServices) {
218
+ this.pluginName = pluginName;
219
+ this.pluginVersion = pluginVersion;
220
+ this.hostServices = hostServices;
221
+ }
222
+ pluginName;
223
+ pluginVersion;
224
+ hostServices;
225
+ /** Emit `eventName` with auto-tagged plugin/version/timestamp. */
226
+ emit(eventName, payload) {
227
+ const target = this.hostServices?.telemetry;
228
+ if (!target) return;
229
+ target.emit(eventName, {
230
+ plugin: this.pluginName,
231
+ version: this.pluginVersion,
232
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
233
+ ...payload ?? {}
234
+ });
235
+ }
236
+ /** `<pluginName>.ready` shorthand. */
237
+ emitReady(context) {
238
+ this.emit(`${this.pluginName}.ready`, context);
239
+ }
240
+ /** `<pluginName>.error` shorthand — extracts message from Error. */
241
+ emitError(error, context) {
242
+ this.emit(`${this.pluginName}.error`, {
243
+ message: error.message,
244
+ ...context ?? {}
245
+ });
246
+ }
247
+ /** Generic event-type emit (alias of emit). */
248
+ emitEvent(type, payload) {
249
+ this.emit(type, payload);
250
+ }
251
+ };
252
+
253
+ // src/log/index.ts
254
+ var BoundLogger = class {
255
+ constructor(logger, source) {
256
+ this.logger = logger;
257
+ this.source = source;
258
+ }
259
+ logger;
260
+ source;
261
+ info(message, meta) {
262
+ this.logger?.info?.(this.source, message, meta);
263
+ }
264
+ warn(message, meta) {
265
+ this.logger?.warn?.(this.source, message, meta);
266
+ }
267
+ error(message, meta) {
268
+ this.logger?.error?.(this.source, message, meta);
269
+ }
270
+ debug(message, meta) {
271
+ this.logger?.debug?.(this.source, message, meta);
272
+ }
273
+ };
274
+
275
+ // src/storage/typed-store.ts
276
+ var TypedStore = class {
277
+ constructor(storage, namespace, key, logger, pluginName = "plugin") {
278
+ this.storage = storage;
279
+ this.namespace = namespace;
280
+ this.key = key;
281
+ this.logger = logger;
282
+ this.pluginName = pluginName;
283
+ }
284
+ storage;
285
+ namespace;
286
+ key;
287
+ logger;
288
+ pluginName;
289
+ async get() {
290
+ const raw = await this.storage.get(this.namespace, this.key);
291
+ if (raw === null || raw === void 0) return null;
292
+ if (typeof raw !== "string") {
293
+ return raw;
294
+ }
295
+ try {
296
+ return JSON.parse(raw);
297
+ } catch (err) {
298
+ this.logger?.error?.(this.pluginName, "TypedStore: corrupt JSON", {
299
+ namespace: this.namespace,
300
+ key: this.key,
301
+ message: err instanceof Error ? err.message : String(err)
302
+ });
303
+ return null;
304
+ }
305
+ }
306
+ async set(value) {
307
+ await this.storage.set(this.namespace, this.key, JSON.stringify(value));
308
+ }
309
+ async delete() {
310
+ return this.storage.delete(this.namespace, this.key);
311
+ }
312
+ };
313
+ var NamespaceStore = class {
314
+ constructor(storage, namespace, logger, pluginName = "plugin") {
315
+ this.storage = storage;
316
+ this.namespace = namespace;
317
+ this.logger = logger;
318
+ this.pluginName = pluginName;
319
+ }
320
+ storage;
321
+ namespace;
322
+ logger;
323
+ pluginName;
324
+ /** Get a typed handle for `(this.namespace, key)`. */
325
+ typed(key) {
326
+ return new TypedStore(this.storage, this.namespace, key, this.logger, this.pluginName);
327
+ }
328
+ async get(key) {
329
+ return this.storage.get(this.namespace, key);
330
+ }
331
+ async set(key, value) {
332
+ return this.storage.set(this.namespace, key, value);
333
+ }
334
+ async delete(key) {
335
+ return this.storage.delete(this.namespace, key);
336
+ }
337
+ };
338
+
339
+ // src/config/index.ts
340
+ var ConfigManager = class {
341
+ constructor(pluginName, hostServices, logger) {
342
+ this.pluginName = pluginName;
343
+ this.hostServices = hostServices;
344
+ this.logger = logger;
345
+ this.envPrefix = `VIBE_${pluginName.toUpperCase().replace(/-/g, "_")}_`;
346
+ }
347
+ pluginName;
348
+ hostServices;
349
+ logger;
350
+ envPrefix;
351
+ envName(key) {
352
+ return `${this.envPrefix}${key.toUpperCase().replace(/-/g, "_")}`;
353
+ }
354
+ /**
355
+ * Resolve a string config value. Returns `defaultValue` (or undefined)
356
+ * when neither env nor host config has it.
357
+ */
358
+ async get(key, defaultValue) {
359
+ const fromEnv = process.env[this.envName(key)];
360
+ if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
361
+ const fromHost = await this.hostServices?.getConfig?.(`${this.pluginName}.${key}`);
362
+ if (fromHost !== void 0 && fromHost !== "") return fromHost;
363
+ return defaultValue;
364
+ }
365
+ /** Like `get`, but throw if the key resolves to undefined / empty. */
366
+ async getRequired(key) {
367
+ const v = await this.get(key);
368
+ if (v === void 0 || v === "") {
369
+ throw new Error(
370
+ `[${this.pluginName}] required config '${key}' is missing (env ${this.envName(key)} or host config '${this.pluginName}.${key}')`
371
+ );
372
+ }
373
+ return v;
374
+ }
375
+ async getInt(key, defaultValue) {
376
+ const raw = await this.get(key);
377
+ if (raw === void 0) return defaultValue;
378
+ const parsed = Number.parseInt(raw, 10);
379
+ if (Number.isNaN(parsed)) {
380
+ this.logger?.warn?.(this.pluginName, "ConfigManager: invalid int", {
381
+ key,
382
+ raw
383
+ });
384
+ return defaultValue;
385
+ }
386
+ return parsed;
387
+ }
388
+ async getBoolean(key, defaultValue) {
389
+ const raw = await this.get(key);
390
+ if (raw === void 0) return defaultValue;
391
+ const v = raw.toLowerCase();
392
+ if (v === "true" || v === "1" || v === "yes" || v === "on") return true;
393
+ if (v === "false" || v === "0" || v === "no" || v === "off") return false;
394
+ this.logger?.warn?.(this.pluginName, "ConfigManager: invalid bool", {
395
+ key,
396
+ raw
397
+ });
398
+ return defaultValue;
399
+ }
400
+ };
401
+ function sleep(ms) {
402
+ return new Promise((resolve) => setTimeout(resolve, ms));
403
+ }
404
+ function isProcessAlive(pid) {
405
+ try {
406
+ process.kill(pid, 0);
407
+ return true;
408
+ } catch {
409
+ return false;
410
+ }
411
+ }
412
+ async function gracefulKill(pid, timeoutMs = 3e3, logger) {
413
+ const source = "subprocess";
414
+ if (!isProcessAlive(pid)) return;
415
+ try {
416
+ process.kill(pid, "SIGTERM");
417
+ } catch (err) {
418
+ logger?.warn?.(source, "SIGTERM failed", {
419
+ pid,
420
+ message: err instanceof Error ? err.message : String(err)
421
+ });
422
+ return;
423
+ }
424
+ const pollMs = 50;
425
+ let elapsed = 0;
426
+ while (elapsed < timeoutMs) {
427
+ await sleep(pollMs);
428
+ elapsed += pollMs;
429
+ if (!isProcessAlive(pid)) return;
430
+ }
431
+ try {
432
+ process.kill(pid, "SIGKILL");
433
+ } catch (err) {
434
+ logger?.warn?.(source, "SIGKILL failed", {
435
+ pid,
436
+ message: err instanceof Error ? err.message : String(err)
437
+ });
438
+ }
439
+ }
440
+ async function findAvailablePort(start, range = 200) {
441
+ for (let i = 0; i < range; i++) {
442
+ const port = start + i;
443
+ if (await isPortFree(port)) return port;
444
+ }
445
+ throw new Error(`findAvailablePort: no port free in [${start}, ${start + range - 1}]`);
446
+ }
447
+ function isPortFree(port) {
448
+ return new Promise((resolve) => {
449
+ const server = createServer();
450
+ server.once("error", () => {
451
+ resolve(false);
452
+ });
453
+ server.once("listening", () => {
454
+ server.close(() => resolve(true));
455
+ });
456
+ try {
457
+ server.listen({ port, host: "127.0.0.1" });
458
+ } catch {
459
+ resolve(false);
460
+ }
461
+ });
462
+ }
463
+
464
+ // src/http/index.ts
465
+ var HttpClient = class {
466
+ constructor(baseUrl, options = {}) {
467
+ this.baseUrl = baseUrl;
468
+ this.timeoutMs = options.timeoutMs ?? 1e4;
469
+ this.maxAttempts = options.maxAttempts ?? 3;
470
+ this.defaultHeaders = options.defaultHeaders ?? {};
471
+ this.fetchImpl = options.fetchImpl ?? fetch;
472
+ }
473
+ baseUrl;
474
+ timeoutMs;
475
+ maxAttempts;
476
+ defaultHeaders;
477
+ fetchImpl;
478
+ async get(path, options) {
479
+ return this.request("GET", path, void 0, options);
480
+ }
481
+ async post(path, body, options) {
482
+ return this.request("POST", path, body, options);
483
+ }
484
+ async request(method, path, body, options) {
485
+ const url = `${this.baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
486
+ const headers = {
487
+ ...this.defaultHeaders,
488
+ ...options?.headers ?? {}
489
+ };
490
+ if (body !== void 0 && headers["content-type"] === void 0) {
491
+ headers["content-type"] = "application/json";
492
+ }
493
+ let lastError;
494
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
495
+ const controller = new AbortController();
496
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
497
+ const callerSignal = options?.signal;
498
+ const onCallerAbort = () => controller.abort();
499
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
500
+ try {
501
+ const res = await this.fetchImpl(url, {
502
+ method,
503
+ headers,
504
+ body: body === void 0 ? void 0 : JSON.stringify(body),
505
+ signal: controller.signal
506
+ });
507
+ if (!res.ok) {
508
+ throw new Error(`HTTP ${res.status} ${res.statusText} for ${method} ${url}`);
509
+ }
510
+ const text = await res.text();
511
+ if (!text) return void 0;
512
+ try {
513
+ return JSON.parse(text);
514
+ } catch {
515
+ return text;
516
+ }
517
+ } catch (err) {
518
+ lastError = err;
519
+ if (attempt >= this.maxAttempts) break;
520
+ const backoff = 100 * 2 ** (attempt - 1);
521
+ await new Promise((r) => setTimeout(r, backoff));
522
+ } finally {
523
+ clearTimeout(timer);
524
+ callerSignal?.removeEventListener("abort", onCallerAbort);
525
+ }
526
+ }
527
+ throw lastError instanceof Error ? lastError : new Error(`HttpClient: request failed for ${method} ${url}`);
528
+ }
529
+ };
530
+
531
+ // src/providers/index.ts
532
+ var ProviderRegistry = class {
533
+ constructor(hostServices) {
534
+ this.hostServices = hostServices;
535
+ }
536
+ hostServices;
537
+ getServiceRegistry() {
538
+ return this.hostServices?.serviceRegistry;
539
+ }
540
+ registerProvider(type, name, provider) {
541
+ this.hostServices?.serviceRegistry?.registerService(type, name, provider);
542
+ }
543
+ getProvider(type, name) {
544
+ return this.hostServices?.serviceRegistry?.getService(type, name);
545
+ }
546
+ listProviders(type) {
547
+ const reg = this.hostServices?.serviceRegistry;
548
+ return reg?.listProvidersForType?.(type) ?? [];
549
+ }
550
+ /**
551
+ * Register a CLI contribution bundle (status sections + doctor checks).
552
+ * No-op when the host doesn't expose `cliContributors`.
553
+ */
554
+ withCliContribution(contribution) {
555
+ const contributors = this.hostServices?.cliContributors;
556
+ if (!contributors) return;
557
+ for (const section of contribution.statusSections ?? []) {
558
+ contributors.addStatusSection?.(section);
559
+ }
560
+ for (const check of contribution.doctorChecks ?? []) {
561
+ contributors.addDoctorCheck?.(check);
562
+ }
563
+ }
564
+ };
565
+
566
+ // src/audit/index.ts
567
+ var AuditLogger = class {
568
+ constructor(source, hostServices) {
569
+ this.source = source;
570
+ this.hostServices = hostServices;
571
+ }
572
+ source;
573
+ hostServices;
574
+ emit(event, payload) {
575
+ this.hostServices?.audit?.emit(event, {
576
+ source: this.source,
577
+ ...payload ?? {}
578
+ });
579
+ }
580
+ };
581
+
582
+ // src/broadcast/index.ts
583
+ var BroadcastEmitter = class {
584
+ constructor(hostServices) {
585
+ this.hostServices = hostServices;
586
+ }
587
+ hostServices;
588
+ broadcast(type, payload) {
589
+ this.hostServices?.broadcast?.(type, payload);
590
+ }
591
+ };
592
+
593
+ export { AuditLogger, BoundLogger, BroadcastEmitter, CliCommandBuilder, ConfigManager, FULL_TRUST_CAPS, HttpClient, NamespaceStore, ProviderRegistry, RoutesBuilder, TelemetryEmitter, TypedStore, createLifecycleHooks, findAvailablePort, gracefulKill, isProcessAlive, maybePrintJson, pickOutputMode, redact, runMultimode, sleep };
594
+ //# sourceMappingURL=index.js.map
595
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,31 @@
1
+ import { HostServices } from '../contract/index.js';
2
+
3
+ /**
4
+ * @vibecontrols/plugin-sdk/lifecycle
5
+ *
6
+ * Collapses the boilerplate every plugin re-types around `onServerStart` /
7
+ * `onServerStop`: emit a one-shot ready telemetry, optionally skip on a
8
+ * platform list, dispatch user init/shutdown hooks.
9
+ *
10
+ * Cross-platform: pure JS, no fs / spawn.
11
+ */
12
+
13
+ interface LifecycleSpec {
14
+ /** Plugin name — used for telemetry tagging + skip log messages. */
15
+ name: string;
16
+ /** Optional init handler — called with hostServices on onServerStart. */
17
+ onInit?: (hostServices: HostServices) => void | Promise<void>;
18
+ /** Optional shutdown handler — called with hostServices on onServerStop. */
19
+ onShutdown?: (hostServices: HostServices) => void | Promise<void>;
20
+ /** Telemetry event name to auto-emit on init (after onInit returns). */
21
+ telemetryEventName?: string;
22
+ /** process.platform values the plugin does NOT support — early-return on match. */
23
+ skipPlatforms?: NodeJS.Platform[];
24
+ }
25
+ interface LifecycleHooks {
26
+ onServerStart: (app: unknown, hostServices: HostServices) => Promise<void>;
27
+ onServerStop: (hostServices: HostServices) => Promise<void>;
28
+ }
29
+ declare function createLifecycleHooks(spec: LifecycleSpec): LifecycleHooks;
30
+
31
+ export { type LifecycleHooks, type LifecycleSpec, createLifecycleHooks };
@@ -0,0 +1,30 @@
1
+ // src/lifecycle/index.ts
2
+ function createLifecycleHooks(spec) {
3
+ const { name, onInit, onShutdown, telemetryEventName, skipPlatforms } = spec;
4
+ return {
5
+ onServerStart: async (_app, hostServices) => {
6
+ if (skipPlatforms && skipPlatforms.includes(process.platform)) {
7
+ process.stderr.write(
8
+ `[${name}] skipping init on unsupported platform '${process.platform}'
9
+ `
10
+ );
11
+ return;
12
+ }
13
+ if (onInit) {
14
+ await onInit(hostServices);
15
+ }
16
+ if (telemetryEventName) {
17
+ hostServices.telemetry?.emit(telemetryEventName, { plugin: name });
18
+ }
19
+ },
20
+ onServerStop: async (hostServices) => {
21
+ if (onShutdown) {
22
+ await onShutdown(hostServices);
23
+ }
24
+ }
25
+ };
26
+ }
27
+
28
+ export { createLifecycleHooks };
29
+ //# sourceMappingURL=index.js.map
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,22 @@
1
+ import { SdkLogger } from '../contract/index.js';
2
+
3
+ /**
4
+ * @vibecontrols/plugin-sdk/log
5
+ *
6
+ * `BoundLogger` binds the `source` field once so plugin code reads
7
+ * `log.info("started")` instead of `logger.info("my-plugin", "started")`
8
+ * at every call-site. No-op when the underlying logger is absent.
9
+ */
10
+
11
+ type Meta = Record<string, unknown>;
12
+ declare class BoundLogger {
13
+ private readonly logger;
14
+ private readonly source;
15
+ constructor(logger: SdkLogger | undefined, source: string);
16
+ info(message: string, meta?: Meta): void;
17
+ warn(message: string, meta?: Meta): void;
18
+ error(message: string, meta?: Meta): void;
19
+ debug(message: string, meta?: Meta): void;
20
+ }
21
+
22
+ export { BoundLogger };
@@ -0,0 +1,25 @@
1
+ // src/log/index.ts
2
+ var BoundLogger = class {
3
+ constructor(logger, source) {
4
+ this.logger = logger;
5
+ this.source = source;
6
+ }
7
+ logger;
8
+ source;
9
+ info(message, meta) {
10
+ this.logger?.info?.(this.source, message, meta);
11
+ }
12
+ warn(message, meta) {
13
+ this.logger?.warn?.(this.source, message, meta);
14
+ }
15
+ error(message, meta) {
16
+ this.logger?.error?.(this.source, message, meta);
17
+ }
18
+ debug(message, meta) {
19
+ this.logger?.debug?.(this.source, message, meta);
20
+ }
21
+ };
22
+
23
+ export { BoundLogger };
24
+ //# sourceMappingURL=index.js.map
25
+ //# sourceMappingURL=index.js.map