@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.
- package/LICENSE +22 -0
- package/README.md +206 -0
- package/boilerplate/.github/workflows/release.template.yml +57 -0
- package/boilerplate/README.md +41 -0
- package/boilerplate/bunfig.toml +13 -0
- package/boilerplate/eslint.config.base.js +36 -0
- package/boilerplate/lefthook.base.yml +10 -0
- package/boilerplate/package.template.json +52 -0
- package/boilerplate/tsconfig.base.json +20 -0
- package/dist/audit/index.d.ts +17 -0
- package/dist/audit/index.js +19 -0
- package/dist/broadcast/index.d.ts +15 -0
- package/dist/broadcast/index.js +14 -0
- package/dist/cli/index.d.ts +75 -0
- package/dist/cli/index.js +104 -0
- package/dist/config/index.d.ts +34 -0
- package/dist/config/index.js +66 -0
- package/dist/contract/index.d.ts +118 -0
- package/dist/contract/index.js +16 -0
- package/dist/http/index.d.ts +35 -0
- package/dist/http/index.js +70 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +595 -0
- package/dist/lifecycle/index.d.ts +31 -0
- package/dist/lifecycle/index.js +30 -0
- package/dist/log/index.d.ts +22 -0
- package/dist/log/index.js +25 -0
- package/dist/providers/index.d.ts +29 -0
- package/dist/providers/index.js +38 -0
- package/dist/routes/index.d.ts +37 -0
- package/dist/routes/index.js +77 -0
- package/dist/storage/index.d.ts +36 -0
- package/dist/storage/index.js +67 -0
- package/dist/subprocess/index.d.ts +37 -0
- package/dist/subprocess/index.js +69 -0
- package/dist/telemetry/index.d.ts +27 -0
- package/dist/telemetry/index.js +41 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.js +97 -0
- 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
|