@owncast/plugin-sdk 0.1.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/LICENSE +21 -0
- package/README.md +48 -0
- package/bin/owncast-plugin.js +345 -0
- package/index.d.ts +459 -0
- package/index.js +443 -0
- package/package.json +47 -0
- package/scripts/postinstall.js +159 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2026 Gabe Kangas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @owncast/plugin-sdk
|
|
2
|
+
|
|
3
|
+
SDK for authoring [Owncast](https://owncast.online) plugins in JavaScript or TypeScript. Plugins compile to WebAssembly and run sandboxed inside the Owncast server.
|
|
4
|
+
|
|
5
|
+
Most authors don't install this directly — instead, scaffold a new project with `npm create owncast-plugin <name>` and the generated `package.json` already lists it as a dependency.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm create owncast-plugin my-plugin
|
|
11
|
+
cd my-plugin
|
|
12
|
+
npm install # postinstall fetches the per-platform wasm toolchain
|
|
13
|
+
npm run build # bundles src/plugin.js → my-plugin.wasm + my-plugin.ocpkg
|
|
14
|
+
npm test # runs scenarios from __tests__/
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then drop `my-plugin.ocpkg` into your Owncast server's `plugins/` directory and enable it from the admin.
|
|
18
|
+
|
|
19
|
+
## Writing a plugin
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const { definePlugin, owncast, filter } = require("@owncast/plugin-sdk");
|
|
23
|
+
|
|
24
|
+
module.exports = definePlugin({
|
|
25
|
+
onChatMessage(msg) {
|
|
26
|
+
owncast.chat.send(`echo: ${msg.body}`);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
filterChatMessage(msg) {
|
|
30
|
+
return msg.body.includes("spam") ? filter.drop("spam") : filter.pass();
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Declare the permissions your plugin uses (`chat.send` for the example above) in `plugin.manifest.json`. The full author guide covers every event handler, host API, and the testing harness:
|
|
36
|
+
|
|
37
|
+
**[→ Owncast Plugin Author Guide](https://github.com/owncast/plugin-sdk/blob/main/docs/PLUGIN_AUTHOR_GUIDE.md)**
|
|
38
|
+
|
|
39
|
+
## What's in the package
|
|
40
|
+
|
|
41
|
+
- `index.js` — runtime: `definePlugin`, the `owncast.*` host wrappers, the `filter` constructor.
|
|
42
|
+
- `index.d.ts` — TypeScript declarations for editor autocomplete on every event payload and host API.
|
|
43
|
+
- `bin/owncast-plugin` — CLI: `build`, `test`, `serve`, `package` subcommands.
|
|
44
|
+
- `scripts/postinstall.js` — downloads the per-platform wasm toolchain (`extism-js`, `wasm-merge`, `wasm-opt`) and the Go test/serve runner on install.
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `owncast-plugin build` — bundle src/plugin.{js,ts} into <name>.wasm
|
|
3
|
+
// `owncast-plugin test` — run scenarios in __tests__/ against the wasm
|
|
4
|
+
// `owncast-plugin serve` — run a localhost dev HTTP server
|
|
5
|
+
// `owncast-plugin package` — produce a single-file <name>.ocpkg suitable
|
|
6
|
+
// for distribution / installation
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { execFileSync } = require("child_process");
|
|
11
|
+
const esbuild = require("esbuild");
|
|
12
|
+
const JSZip = require("jszip");
|
|
13
|
+
|
|
14
|
+
const cmd = process.argv[2] || "build";
|
|
15
|
+
const restArgs = process.argv.slice(3);
|
|
16
|
+
|
|
17
|
+
if (cmd === "build") {
|
|
18
|
+
buildMain().catch(fail);
|
|
19
|
+
} else if (cmd === "test") {
|
|
20
|
+
testMain(restArgs);
|
|
21
|
+
} else if (cmd === "serve") {
|
|
22
|
+
serveMain(restArgs);
|
|
23
|
+
} else if (cmd === "package") {
|
|
24
|
+
packageMain().catch(fail);
|
|
25
|
+
} else {
|
|
26
|
+
console.error(`unknown command: ${cmd}\nusage: owncast-plugin <build|test|serve|package>`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fail(e) {
|
|
31
|
+
console.error(`${cmd} failed: ${e.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function testMain(args) {
|
|
36
|
+
runBinary("owncast-plugin-test", args);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function serveMain(args) {
|
|
40
|
+
runBinary("owncast-plugin-serve", args);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runBinary(name, args) {
|
|
44
|
+
const cache = findCacheDir();
|
|
45
|
+
const bin = path.join(cache, name);
|
|
46
|
+
if (!fs.existsSync(bin)) {
|
|
47
|
+
console.error(
|
|
48
|
+
`${name} not found at ${bin}\n` +
|
|
49
|
+
`In production this is fetched by the SDK postinstall. For the PoC, ` +
|
|
50
|
+
`build it via: cd owncast && go build -o tools/${name} ./cmd/${name}`
|
|
51
|
+
);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const env = {
|
|
55
|
+
...process.env,
|
|
56
|
+
LD_LIBRARY_PATH: `${path.join(cache, "lib")}:${process.env.LD_LIBRARY_PATH || ""}`
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
execFileSync(bin, args.length > 0 ? args : [process.cwd()], { stdio: "inherit", env });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
process.exit(typeof e.status === "number" ? e.status : 1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function buildMain() {
|
|
66
|
+
const cwd = process.cwd();
|
|
67
|
+
const manifestPath = path.join(cwd, "plugin.manifest.json");
|
|
68
|
+
if (!fs.existsSync(manifestPath)) {
|
|
69
|
+
throw new Error("plugin.manifest.json not found in current directory");
|
|
70
|
+
}
|
|
71
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
72
|
+
const name = manifest.name;
|
|
73
|
+
if (!name) throw new Error("manifest.name is required");
|
|
74
|
+
|
|
75
|
+
// Detect entry point.
|
|
76
|
+
let entry = null;
|
|
77
|
+
for (const candidate of ["src/plugin.ts", "src/plugin.js", "plugin.ts", "plugin.js"]) {
|
|
78
|
+
const p = path.join(cwd, candidate);
|
|
79
|
+
if (fs.existsSync(p)) {
|
|
80
|
+
entry = p;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!entry) throw new Error("no plugin source found (expected src/plugin.ts or plugin.js)");
|
|
85
|
+
|
|
86
|
+
// Synthesize an entry that injects the manifest, requires user code,
|
|
87
|
+
// then re-exports the SDK runtime exports as wasm-visible exports.
|
|
88
|
+
const buildDir = path.join(cwd, ".owncast-build");
|
|
89
|
+
fs.mkdirSync(buildDir, { recursive: true });
|
|
90
|
+
const synthEntry = path.join(buildDir, "entry.js");
|
|
91
|
+
const manifestJSON = JSON.stringify(manifest);
|
|
92
|
+
// Always emit register/on_event/on_filter as wasm exports. The SDK derives
|
|
93
|
+
// subscriptions at runtime from the plugin's handler methods and merges
|
|
94
|
+
// them into the manifest returned by register(). The host then only calls
|
|
95
|
+
// on_event/on_filter for plugins actually subscribed to that event, so
|
|
96
|
+
// unused exports are harmless.
|
|
97
|
+
const entrySrc = `const sdk = require("@owncast/plugin-sdk");
|
|
98
|
+
const MANIFEST_BASE = ${manifestJSON};
|
|
99
|
+
require(${JSON.stringify(entry)});
|
|
100
|
+
function register() {
|
|
101
|
+
const manifest = Object.assign({}, MANIFEST_BASE, { subscriptions: sdk.describeSubscriptions() });
|
|
102
|
+
Host.outputString(JSON.stringify(manifest));
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
function on_event() {
|
|
106
|
+
const envelope = JSON.parse(Host.inputString());
|
|
107
|
+
sdk.dispatchEvent(envelope);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
function on_filter() {
|
|
111
|
+
const envelope = JSON.parse(Host.inputString());
|
|
112
|
+
const result = sdk.dispatchFilter(envelope);
|
|
113
|
+
Host.outputString(JSON.stringify(result));
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
function on_http_request() {
|
|
117
|
+
const request = JSON.parse(Host.inputString());
|
|
118
|
+
const response = sdk.dispatchHttp(request);
|
|
119
|
+
Host.outputString(JSON.stringify(response));
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
module.exports = { register, on_event, on_filter, on_http_request };
|
|
123
|
+
`;
|
|
124
|
+
fs.writeFileSync(synthEntry, entrySrc);
|
|
125
|
+
|
|
126
|
+
// Bundle to a single CJS file targeting the QuickJS runtime extism-js uses.
|
|
127
|
+
const bundledJS = path.join(buildDir, "bundle.js");
|
|
128
|
+
await esbuild.build({
|
|
129
|
+
entryPoints: [synthEntry],
|
|
130
|
+
bundle: true,
|
|
131
|
+
format: "cjs",
|
|
132
|
+
platform: "neutral",
|
|
133
|
+
target: "es2020",
|
|
134
|
+
outfile: bundledJS,
|
|
135
|
+
logLevel: "warning"
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Generate index.d.ts declaring exports + host imports based on permissions.
|
|
139
|
+
const dts = path.join(buildDir, "index.d.ts");
|
|
140
|
+
fs.writeFileSync(dts, generateInterface(manifest));
|
|
141
|
+
|
|
142
|
+
// Find toolchain.
|
|
143
|
+
const cache = findCacheDir();
|
|
144
|
+
const extismJs = path.join(cache, "extism-js");
|
|
145
|
+
if (!fs.existsSync(extismJs)) {
|
|
146
|
+
throw new Error(`extism-js not found at ${extismJs} — run \`npm install\` to fetch the toolchain`);
|
|
147
|
+
}
|
|
148
|
+
const env = {
|
|
149
|
+
...process.env,
|
|
150
|
+
PATH: `${cache}:${process.env.PATH}`,
|
|
151
|
+
LD_LIBRARY_PATH: `${path.join(cache, "lib")}:${process.env.LD_LIBRARY_PATH || ""}`
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const wasmOut = path.join(cwd, `${name}.wasm`);
|
|
155
|
+
execFileSync(extismJs, [bundledJS, "-i", dts, "-o", wasmOut], { stdio: "inherit", env });
|
|
156
|
+
|
|
157
|
+
// If the project ships static assets in ./assets/, mirror them to the
|
|
158
|
+
// canonical deployment layout (<name>-assets/) so plugin.Server finds them
|
|
159
|
+
// without per-deployment renames. We use a symlink so edits to assets/
|
|
160
|
+
// show up live during dev (no rebuild needed for HTML/CSS changes).
|
|
161
|
+
const assetsSrc = path.join(cwd, "assets");
|
|
162
|
+
if (fs.existsSync(assetsSrc) && fs.statSync(assetsSrc).isDirectory()) {
|
|
163
|
+
const assetsDest = path.join(cwd, `${name}-assets`);
|
|
164
|
+
let needsLink = true;
|
|
165
|
+
if (fs.existsSync(assetsDest)) {
|
|
166
|
+
try {
|
|
167
|
+
const st = fs.lstatSync(assetsDest);
|
|
168
|
+
if (st.isSymbolicLink() && fs.realpathSync(assetsDest) === fs.realpathSync(assetsSrc)) {
|
|
169
|
+
needsLink = false;
|
|
170
|
+
} else {
|
|
171
|
+
fs.rmSync(assetsDest, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
fs.rmSync(assetsDest, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (needsLink) {
|
|
178
|
+
fs.symlinkSync(path.resolve(assetsSrc), assetsDest, "dir");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`built ${path.relative(cwd, wasmOut)}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// `owncast-plugin package` — bundle the project into a single .ocpkg file
|
|
186
|
+
// (zip archive with plugin.manifest.json, plugin.wasm, and optional assets/).
|
|
187
|
+
// Builds the wasm first if it doesn't exist.
|
|
188
|
+
async function packageMain() {
|
|
189
|
+
const cwd = process.cwd();
|
|
190
|
+
const manifestPath = path.join(cwd, "plugin.manifest.json");
|
|
191
|
+
if (!fs.existsSync(manifestPath)) {
|
|
192
|
+
throw new Error("plugin.manifest.json not found in current directory");
|
|
193
|
+
}
|
|
194
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
195
|
+
const name = manifest.name;
|
|
196
|
+
if (!name) throw new Error("manifest.name is required");
|
|
197
|
+
|
|
198
|
+
const wasmPath = path.join(cwd, `${name}.wasm`);
|
|
199
|
+
if (!fs.existsSync(wasmPath)) {
|
|
200
|
+
await buildMain();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const assetsDir = path.join(cwd, "assets");
|
|
204
|
+
const zip = new JSZip();
|
|
205
|
+
zip.file("plugin.manifest.json", fs.readFileSync(manifestPath));
|
|
206
|
+
zip.file("plugin.wasm", fs.readFileSync(wasmPath));
|
|
207
|
+
let fileCount = 2;
|
|
208
|
+
if (fs.existsSync(assetsDir) && fs.statSync(assetsDir).isDirectory()) {
|
|
209
|
+
for (const file of walkFiles(assetsDir)) {
|
|
210
|
+
const rel = path.relative(assetsDir, file).split(path.sep).join("/");
|
|
211
|
+
zip.file(`assets/${rel}`, fs.readFileSync(file));
|
|
212
|
+
fileCount++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const outPath = path.join(cwd, `${name}.ocpkg`);
|
|
217
|
+
const buf = await zip.generateAsync({
|
|
218
|
+
type: "nodebuffer",
|
|
219
|
+
compression: "DEFLATE",
|
|
220
|
+
compressionOptions: { level: 6 }
|
|
221
|
+
});
|
|
222
|
+
fs.writeFileSync(outPath, buf);
|
|
223
|
+
const sizeKb = Math.round(fs.statSync(outPath).size / 1024);
|
|
224
|
+
console.log(`packaged ${path.relative(cwd, outPath)} (${sizeKb} KB, ${fileCount} files)`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function* walkFiles(dir) {
|
|
228
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
229
|
+
// Resolve symlinks so the assets/ → <name>-assets/ link the build CLI
|
|
230
|
+
// makes doesn't cause us to skip files. statSync follows.
|
|
231
|
+
const full = path.join(dir, entry.name);
|
|
232
|
+
let info;
|
|
233
|
+
try {
|
|
234
|
+
info = fs.statSync(full);
|
|
235
|
+
} catch {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (info.isDirectory()) {
|
|
239
|
+
yield* walkFiles(full);
|
|
240
|
+
} else if (info.isFile()) {
|
|
241
|
+
yield full;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function generateInterface(manifest) {
|
|
247
|
+
const exports = [
|
|
248
|
+
"register(): I32",
|
|
249
|
+
"on_event(): I32",
|
|
250
|
+
"on_filter(): I32",
|
|
251
|
+
"on_http_request(): I32"
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
const perms = new Set(manifest.permissions || []);
|
|
255
|
+
const imports = [];
|
|
256
|
+
if (perms.has("chat.send")) {
|
|
257
|
+
imports.push("owncast_send_chat(textPtr: PTR): void");
|
|
258
|
+
imports.push("owncast_send_chat_action(textPtr: PTR): void");
|
|
259
|
+
imports.push("owncast_send_chat_system(bodyPtr: PTR): void");
|
|
260
|
+
imports.push("owncast_send_chat_to(clientId: I64, textPtr: PTR): void");
|
|
261
|
+
}
|
|
262
|
+
if (perms.has("chat.history")) {
|
|
263
|
+
imports.push("owncast_chat_history(limit: I32): PTR");
|
|
264
|
+
imports.push("owncast_chat_clients(): PTR");
|
|
265
|
+
}
|
|
266
|
+
if (perms.has("chat.moderate")) {
|
|
267
|
+
imports.push("owncast_delete_message(idPtr: PTR): void");
|
|
268
|
+
imports.push("owncast_kick_client(clientId: I64): void");
|
|
269
|
+
}
|
|
270
|
+
if (perms.has("notifications.send")) {
|
|
271
|
+
imports.push("owncast_notify_discord(textPtr: PTR): void");
|
|
272
|
+
imports.push("owncast_notify_browser_push(payloadPtr: PTR): void");
|
|
273
|
+
imports.push("owncast_notify_fediverse(payloadPtr: PTR): void");
|
|
274
|
+
}
|
|
275
|
+
if (perms.has("users.read")) {
|
|
276
|
+
imports.push("owncast_users_list(): PTR");
|
|
277
|
+
imports.push("owncast_user_get(idPtr: PTR): PTR");
|
|
278
|
+
}
|
|
279
|
+
if (perms.has("users.moderate")) {
|
|
280
|
+
imports.push("owncast_user_set_enabled(idPtr: PTR, enabled: I32, reasonPtr: PTR): void");
|
|
281
|
+
imports.push("owncast_ban_ip(ipPtr: PTR): void");
|
|
282
|
+
}
|
|
283
|
+
if (perms.has("storage.upload")) {
|
|
284
|
+
imports.push("owncast_storage_upload(namePtr: PTR, dataPtr: PTR): PTR");
|
|
285
|
+
}
|
|
286
|
+
if (perms.has("fediverse.post")) {
|
|
287
|
+
imports.push("owncast_fediverse_post(textPtr: PTR): PTR");
|
|
288
|
+
}
|
|
289
|
+
if (perms.has("storage.kv")) {
|
|
290
|
+
imports.push("owncast_kv_get(keyPtr: PTR): PTR");
|
|
291
|
+
imports.push("owncast_kv_set(keyPtr: PTR, valPtr: PTR): void");
|
|
292
|
+
}
|
|
293
|
+
if (perms.has("events.emit")) imports.push("owncast_emit_event(eventTypePtr: PTR, payloadPtr: PTR): void");
|
|
294
|
+
if (perms.has("http.sse")) imports.push("owncast_sse_send(channelPtr: PTR, eventPtr: PTR, dataPtr: PTR): void");
|
|
295
|
+
if (perms.has("server.read")) {
|
|
296
|
+
imports.push("owncast_stream_current(): PTR");
|
|
297
|
+
imports.push("owncast_server_info(): PTR");
|
|
298
|
+
imports.push("owncast_server_socials(): PTR");
|
|
299
|
+
imports.push("owncast_server_federation(): PTR");
|
|
300
|
+
imports.push("owncast_stream_broadcaster(): PTR");
|
|
301
|
+
imports.push("owncast_server_tags(): PTR");
|
|
302
|
+
}
|
|
303
|
+
if (perms.has("videoconfig.read")) {
|
|
304
|
+
imports.push("owncast_video_config_read(): PTR");
|
|
305
|
+
}
|
|
306
|
+
if (perms.has("videoconfig.write")) {
|
|
307
|
+
imports.push("owncast_video_config_write(configPtr: PTR): PTR");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let out = `declare module 'main' {\n`;
|
|
311
|
+
for (const e of exports) out += ` export function ${e};\n`;
|
|
312
|
+
out += `}\n`;
|
|
313
|
+
if (imports.length > 0) {
|
|
314
|
+
out += `\ndeclare module 'extism:host' {\n interface user {\n`;
|
|
315
|
+
for (const i of imports) out += ` ${i};\n`;
|
|
316
|
+
out += ` }\n}\n`;
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function findCacheDir() {
|
|
322
|
+
// Look in node_modules/@owncast/plugin-sdk/bin/.cache (when used as a dep)
|
|
323
|
+
// and in the repo's tools/ dir (when developing). The dev candidate
|
|
324
|
+
// assumes Node resolved __dirname through any symlink to the real SDK
|
|
325
|
+
// path (sdks/js/bin/), then walks up to the repo root.
|
|
326
|
+
const candidates = [
|
|
327
|
+
path.join(__dirname, ".cache"),
|
|
328
|
+
path.join(__dirname, "..", "bin", ".cache"),
|
|
329
|
+
path.join(__dirname, "..", "..", "..", "tools")
|
|
330
|
+
];
|
|
331
|
+
// Pick the first candidate that has any of the expected tools — different
|
|
332
|
+
// commands need different binaries (build needs extism-js, test needs
|
|
333
|
+
// owncast-plugin-test) but they share a cache.
|
|
334
|
+
for (const c of candidates) {
|
|
335
|
+
if (
|
|
336
|
+
fs.existsSync(path.join(c, "extism-js")) ||
|
|
337
|
+
fs.existsSync(path.join(c, "owncast-plugin-test")) ||
|
|
338
|
+
fs.existsSync(path.join(c, "owncast-plugin-serve"))
|
|
339
|
+
) {
|
|
340
|
+
return c;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return candidates[0];
|
|
344
|
+
}
|
|
345
|
+
|