@streamfox/create-streamfox-plugin 0.3.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/dist/cli.cjs ADDED
@@ -0,0 +1,637 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_node_path2 = __toESM(require("path"), 1);
28
+ var import_commander = require("commander");
29
+ var import_prompts = __toESM(require("prompts"), 1);
30
+
31
+ // package.json
32
+ var package_default = {
33
+ name: "@streamfox/create-streamfox-plugin",
34
+ version: "0.3.0",
35
+ description: "Standalone CLI to scaffold StreamFox plugin projects",
36
+ type: "module",
37
+ bin: {
38
+ "create-streamfox-plugin": "dist/cli.cjs"
39
+ },
40
+ main: "./dist/index.cjs",
41
+ module: "./dist/index.js",
42
+ types: "./dist/index.d.ts",
43
+ exports: {
44
+ ".": {
45
+ types: "./dist/index.d.ts",
46
+ import: "./dist/index.js",
47
+ require: "./dist/index.cjs"
48
+ }
49
+ },
50
+ files: [
51
+ "dist",
52
+ "README.md"
53
+ ],
54
+ scripts: {
55
+ build: "tsup",
56
+ format: "prettier --write .",
57
+ "format:check": "prettier --check .",
58
+ test: "vitest run",
59
+ typecheck: "tsc --noEmit",
60
+ check: "npm run format:check && npm run typecheck && npm test && npm run build"
61
+ },
62
+ dependencies: {
63
+ commander: "^12.1.0",
64
+ prompts: "^2.4.2"
65
+ },
66
+ devDependencies: {
67
+ "@types/node": "^24.6.0",
68
+ "@types/prompts": "^2.4.9",
69
+ prettier: "^3.6.2",
70
+ tsup: "^8.5.0",
71
+ typescript: "^5.9.2",
72
+ vitest: "^2.1.9"
73
+ },
74
+ engines: {
75
+ node: ">=20"
76
+ }
77
+ };
78
+
79
+ // src/scaffold.ts
80
+ var import_promises = require("fs/promises");
81
+ var import_node_path = __toESM(require("path"), 1);
82
+ var CAPABILITIES = [
83
+ "catalog",
84
+ "meta",
85
+ "stream",
86
+ "subtitles",
87
+ "plugin_catalog"
88
+ ];
89
+ var DEFAULT_PRESET = "meta";
90
+ var DEFAULT_SDK_VERSION = "^0.2.0";
91
+ function sortedCapabilities(values) {
92
+ const unique = Array.from(new Set(values));
93
+ return CAPABILITIES.filter((capability) => unique.includes(capability));
94
+ }
95
+ async function ensureTargetDoesNotExist(targetDir) {
96
+ try {
97
+ await (0, import_promises.access)(targetDir);
98
+ throw new Error(`Target directory already exists: ${targetDir}`);
99
+ } catch (error) {
100
+ if (error.code === "ENOENT") {
101
+ return;
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+ function makePackageJson(name, language, sdkVersion) {
107
+ const scripts = language === "ts" ? {
108
+ dev: "tsx watch src/server.ts",
109
+ build: "tsc -p tsconfig.json",
110
+ format: "prettier --write .",
111
+ "format:check": "prettier --check .",
112
+ start: "node dist/server.js",
113
+ test: "vitest run",
114
+ typecheck: "tsc --noEmit"
115
+ } : {
116
+ dev: "node --watch src/server.js",
117
+ build: 'echo "No build step for JavaScript template"',
118
+ format: "prettier --write .",
119
+ "format:check": "prettier --check .",
120
+ start: "node src/server.js",
121
+ test: "vitest run"
122
+ };
123
+ const normalizedScripts = {
124
+ ...scripts,
125
+ check: language === "ts" ? "npm run format:check && npm run typecheck && npm test && npm run build" : "npm run format:check && npm test && npm run build"
126
+ };
127
+ const packageJson = {
128
+ name,
129
+ version: "0.1.0",
130
+ private: true,
131
+ type: "module",
132
+ scripts: normalizedScripts,
133
+ dependencies: {
134
+ "@streamfox/plugin-sdk": sdkVersion
135
+ },
136
+ devDependencies: language === "ts" ? {
137
+ "@types/node": "^24.6.0",
138
+ prettier: "^3.6.2",
139
+ tsx: "^4.20.5",
140
+ typescript: "^5.9.2",
141
+ vitest: "^2.1.9"
142
+ } : {
143
+ prettier: "^3.6.2",
144
+ vitest: "^2.1.9"
145
+ }
146
+ };
147
+ return `${JSON.stringify(packageJson, null, 2)}
148
+ `;
149
+ }
150
+ function resourceBlock(capability, advanced) {
151
+ switch (capability) {
152
+ case "catalog":
153
+ return `catalog: {
154
+ endpoints: [
155
+ {
156
+ id: "top",
157
+ name: "Top",
158
+ mediaTypes: ["movie"],
159
+ filters: [{ key: "genre", valueType: "string" }],
160
+ },
161
+ ],
162
+ handler: async () => ({
163
+ items: [],
164
+ }),
165
+ },`;
166
+ case "meta":
167
+ return `meta: {
168
+ mediaTypes: ["movie"],
169
+ includes: ["videos", "links"],
170
+ handler: async () => ({
171
+ item: ${advanced ? `{
172
+ summary: {
173
+ id: { namespace: "imdb", value: "tt1254207" },
174
+ mediaType: "movie",
175
+ title: "Big Buck Bunny",
176
+ links: [],
177
+ },
178
+ defaultVideoID: "main",
179
+ trailers: [{ transport: { kind: "youtube", id: "aqz-KE-bpKQ" } }],
180
+ videos: [
181
+ {
182
+ id: "main",
183
+ title: "Main",
184
+ streams: [{ transport: { kind: "http", url: "https://example.com/video.mp4" } }],
185
+ },
186
+ ],
187
+ }` : "null"},
188
+ }),
189
+ },`;
190
+ case "stream":
191
+ return `stream: {
192
+ mediaTypes: ["movie"],
193
+ supportedTransports: ${advanced ? `["http", "torrent", "usenet", "archive", "youtube"]` : `["http"]`},
194
+ handler: async () => ({
195
+ streams: [
196
+ {
197
+ transport: { kind: "http", url: "https://example.com/video.mp4", mode: "stream" },
198
+ hints: {
199
+ notWebReady: true,
200
+ proxyHeaders: { request: { "User-Agent": "StreamFox" } },
201
+ },
202
+ },
203
+ ${advanced ? ` {
204
+ transport: { kind: "torrent", infoHash: "abcdef", peerDiscovery: ["tracker:udp://tracker.example.com:80"] },
205
+ selection: { fileIndex: 0 },
206
+ },
207
+ {
208
+ transport: { kind: "usenet", nzbURL: "https://example.com/file.nzb", servers: ["nntps://user:pass@news.example.com:563/4"] },
209
+ },
210
+ {
211
+ transport: {
212
+ kind: "archive",
213
+ format: "zip",
214
+ files: [{ url: "https://example.com/archive.zip", bytes: 1024 }],
215
+ },
216
+ selection: { fileMustInclude: "movie.mkv" },
217
+ },
218
+ ` : ""} ],
219
+ }),
220
+ },`;
221
+ case "subtitles":
222
+ return `subtitles: {
223
+ mediaTypes: ["movie", "episode"],
224
+ defaultLanguages: ["en"],
225
+ handler: async (request, { settings }) => {
226
+ const configuredLanguages = Array.isArray(settings.languages)
227
+ ? settings.languages
228
+ : [];
229
+ const languagePreferences =
230
+ configuredLanguages.length > 0 ? configuredLanguages : (request.languagePreferences ?? []);
231
+
232
+ void languagePreferences;
233
+ void settings.includeHI;
234
+
235
+ return {
236
+ subtitles: [],
237
+ };
238
+ },
239
+ },`;
240
+ case "plugin_catalog":
241
+ return `pluginCatalog: {
242
+ endpoints: [
243
+ {
244
+ id: "featured",
245
+ name: "Featured",
246
+ pluginKinds: ["catalog", "meta", "stream", "subtitles"],
247
+ tags: ["official"],
248
+ },
249
+ ],
250
+ handler: async () => ({
251
+ plugins: [
252
+ {
253
+ id: "com.example.recommended",
254
+ name: "Recommended",
255
+ version: "1.0.0",
256
+ pluginKinds: ["catalog", "meta"],
257
+ distribution: {
258
+ transport: "https",
259
+ manifestURL: "https://plugins.example.com/recommended/manifest",
260
+ },
261
+ manifestSnapshot: {
262
+ plugin: { id: "com.example.recommended" },
263
+ },
264
+ },
265
+ ],
266
+ }),
267
+ },`;
268
+ default:
269
+ return "";
270
+ }
271
+ }
272
+ function makeInstallBlock(preset) {
273
+ if (preset !== "subtitles") {
274
+ return "";
275
+ }
276
+ return ` install: {
277
+ configurationRequired: true,
278
+ title: "Subtitle Settings",
279
+ description: "Configure subtitle defaults before installing this plugin.",
280
+ fields: [
281
+ settings.multiSelect("languages", {
282
+ label: "Languages",
283
+ options: [
284
+ { label: "English", value: "en" },
285
+ { label: "Greek", value: "el" },
286
+ { label: "Spanish", value: "es" },
287
+ ],
288
+ defaultValue: ["en"],
289
+ }),
290
+ settings.checkbox("includeHI", {
291
+ label: "Include hearing impaired",
292
+ defaultValue: true,
293
+ }),
294
+ ],
295
+ },
296
+ `;
297
+ }
298
+ function makePluginFile(name, preset, capabilities, advanced) {
299
+ const resources = capabilities.map((capability) => resourceBlock(capability, advanced)).join("\n ");
300
+ const install = makeInstallBlock(preset);
301
+ const importSpec = install.length > 0 ? "definePlugin, settings" : "definePlugin";
302
+ const installBlock = install.length > 0 ? `${install}` : "";
303
+ return `import { ${importSpec} } from "@streamfox/plugin-sdk";
304
+
305
+ export const plugin = definePlugin({
306
+ plugin: {
307
+ id: "com.example.${name}",
308
+ name: "${name}",
309
+ version: "0.1.0",
310
+ description: "Generated StreamFox plugin scaffold",
311
+ },
312
+ ${installBlock} resources: {
313
+ ${resources}
314
+ },
315
+ });
316
+ `;
317
+ }
318
+ function makeServerFile(language) {
319
+ const pluginImport = language === "ts" ? "./plugin" : "./plugin.js";
320
+ return `import { serve } from "@streamfox/plugin-sdk";
321
+ import { plugin } from "${pluginImport}";
322
+
323
+ const { url, installURL, launchURL } = await serve(plugin, {
324
+ port: Number(process.env.PORT ?? 7000),
325
+ integration: {
326
+ installScheme: "streamfox",
327
+ launchBaseURL: "https://streamfox.app/#",
328
+ autoOpen: "none",
329
+ },
330
+ });
331
+
332
+ console.log("Plugin manifest:", url);
333
+ console.log("Plugin installer deeplink:", installURL);
334
+ console.log("Plugin launch URL:", launchURL);
335
+ `;
336
+ }
337
+ function makeVitestFile(language) {
338
+ const pluginImport = language === "ts" ? "../src/plugin" : "../src/plugin.js";
339
+ return `import { describe, expect, it } from "vitest";
340
+ import { createServer } from "@streamfox/plugin-sdk";
341
+ import { plugin } from "${pluginImport}";
342
+
343
+ describe("scaffold smoke", () => {
344
+ it("serves manifest and studio config", async () => {
345
+ const app = createServer(plugin, { frontend: false });
346
+
347
+ const manifestResponse = await app.request("/manifest");
348
+ expect(manifestResponse.status).toBe(200);
349
+
350
+ const studioResponse = await app.request("/studio-config");
351
+ expect(studioResponse.status).toBe(200);
352
+ });
353
+ });
354
+ `;
355
+ }
356
+ var tsConfig = `{
357
+ "compilerOptions": {
358
+ "target": "ES2022",
359
+ "module": "ESNext",
360
+ "moduleResolution": "Bundler",
361
+ "strict": true,
362
+ "declaration": true,
363
+ "outDir": "dist",
364
+ "rootDir": "src",
365
+ "types": ["node"]
366
+ },
367
+ "include": ["src"]
368
+ }
369
+ `;
370
+ var prettierConfig = `{
371
+ "semi": true,
372
+ "singleQuote": false,
373
+ "trailingComma": "all"
374
+ }
375
+ `;
376
+ var prettierIgnore = `dist
377
+ node_modules
378
+ .DS_Store
379
+ `;
380
+ function makeReadme(projectName, preset, capabilities, advanced) {
381
+ const capabilitiesList = capabilities.map((capability) => `- ${capability}`).join("\n");
382
+ const endpointForCapability = (capability) => {
383
+ switch (capability) {
384
+ case "catalog":
385
+ return "/catalog/:mediaType/:catalogID";
386
+ case "meta":
387
+ return "/meta/:mediaType/:itemID";
388
+ case "stream":
389
+ return "/stream/:mediaType/:itemID";
390
+ case "subtitles":
391
+ return "/subtitles/:mediaType/:itemID";
392
+ case "plugin_catalog":
393
+ return "/plugin_catalog/:catalogID/:pluginKind";
394
+ default:
395
+ return `/${capability}`;
396
+ }
397
+ };
398
+ const endpointLines = [
399
+ "- GET /manifest",
400
+ "- GET /studio-config",
401
+ ...capabilities.map(
402
+ (capability) => `- GET ${endpointForCapability(capability)}`
403
+ )
404
+ ].join("\n");
405
+ return `# ${projectName}
406
+
407
+ Generated with create-streamfox-plugin.
408
+
409
+ Preset: \`${preset}\`
410
+ Advanced template: \`${advanced ? "enabled" : "disabled"}\`
411
+
412
+ ## Scripts
413
+
414
+ - npm run dev
415
+ - npm run build
416
+ - npm run format
417
+ - npm run start
418
+ - npm run test
419
+
420
+ ## Implemented Capabilities
421
+
422
+ ${capabilitiesList}
423
+
424
+ ## Stream Model
425
+
426
+ - Unified transport model via \`stream.transport\`
427
+ - Capability declaration via \`resources.stream.supportedTransports\`
428
+ - Optional selection controls via \`stream.selection\`
429
+
430
+ ## Endpoints
431
+
432
+ ${endpointLines}
433
+ `;
434
+ }
435
+ async function scaffoldProject(options) {
436
+ const capabilities = sortedCapabilities([
437
+ options.preset,
438
+ ...options.extraCapabilities ?? []
439
+ ]);
440
+ const sdkVersion = (options.sdkVersion ?? DEFAULT_SDK_VERSION).trim() || DEFAULT_SDK_VERSION;
441
+ const advanced = options.advanced ?? false;
442
+ await ensureTargetDoesNotExist(options.targetDir);
443
+ const srcDir = import_node_path.default.join(options.targetDir, "src");
444
+ const testDir = import_node_path.default.join(options.targetDir, "test");
445
+ await (0, import_promises.mkdir)(srcDir, { recursive: true });
446
+ await (0, import_promises.mkdir)(testDir, { recursive: true });
447
+ await (0, import_promises.writeFile)(
448
+ import_node_path.default.join(options.targetDir, "package.json"),
449
+ makePackageJson(options.projectName, options.language, sdkVersion)
450
+ );
451
+ await (0, import_promises.writeFile)(
452
+ import_node_path.default.join(options.targetDir, ".prettierrc.json"),
453
+ prettierConfig
454
+ );
455
+ await (0, import_promises.writeFile)(
456
+ import_node_path.default.join(options.targetDir, ".prettierignore"),
457
+ prettierIgnore
458
+ );
459
+ await (0, import_promises.writeFile)(
460
+ import_node_path.default.join(options.targetDir, "README.md"),
461
+ makeReadme(options.projectName, options.preset, capabilities, advanced)
462
+ );
463
+ if (options.language === "ts") {
464
+ await (0, import_promises.writeFile)(import_node_path.default.join(options.targetDir, "tsconfig.json"), tsConfig);
465
+ await (0, import_promises.writeFile)(
466
+ import_node_path.default.join(srcDir, "plugin.ts"),
467
+ makePluginFile(
468
+ options.projectName,
469
+ options.preset,
470
+ capabilities,
471
+ advanced
472
+ )
473
+ );
474
+ await (0, import_promises.writeFile)(import_node_path.default.join(srcDir, "server.ts"), makeServerFile("ts"));
475
+ await (0, import_promises.writeFile)(import_node_path.default.join(testDir, "plugin.test.ts"), makeVitestFile("ts"));
476
+ } else {
477
+ await (0, import_promises.writeFile)(
478
+ import_node_path.default.join(srcDir, "plugin.js"),
479
+ makePluginFile(
480
+ options.projectName,
481
+ options.preset,
482
+ capabilities,
483
+ advanced
484
+ )
485
+ );
486
+ await (0, import_promises.writeFile)(import_node_path.default.join(srcDir, "server.js"), makeServerFile("js"));
487
+ await (0, import_promises.writeFile)(import_node_path.default.join(testDir, "plugin.test.js"), makeVitestFile("js"));
488
+ }
489
+ }
490
+
491
+ // src/cli.ts
492
+ function capabilitiesLabel() {
493
+ return CAPABILITIES.join(", ");
494
+ }
495
+ function parsePreset(input) {
496
+ if (CAPABILITIES.includes(input)) {
497
+ return input;
498
+ }
499
+ throw new import_commander.InvalidArgumentError(
500
+ `Invalid preset '${input}'. Use one of: ${capabilitiesLabel()}`
501
+ );
502
+ }
503
+ function parseCapabilitiesList(input) {
504
+ const parsed = input.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
505
+ for (const capability of parsed) {
506
+ if (!CAPABILITIES.includes(capability)) {
507
+ throw new import_commander.InvalidArgumentError(
508
+ `Invalid capability '${capability}'. Use one of: ${capabilitiesLabel()}`
509
+ );
510
+ }
511
+ }
512
+ return Array.from(new Set(parsed));
513
+ }
514
+ var program = new import_commander.Command();
515
+ program.name("create-streamfox-plugin").version(package_default.version, "-v, --version", "display the current CLI version").description("Scaffold StreamFox plugin projects with modern JS/TS presets").showHelpAfterError().argument("[directory]", "output directory").option("--ts", "use TypeScript template").option("--js", "use JavaScript template").option(
516
+ "--preset <preset>",
517
+ `plugin preset: ${capabilitiesLabel()}`,
518
+ parsePreset
519
+ ).option(
520
+ "--capabilities <capabilities>",
521
+ "extra capabilities as comma-separated list",
522
+ parseCapabilitiesList
523
+ ).option("--advanced", "generate advanced capability examples").option(
524
+ "--sdk-version <range>",
525
+ "@streamfox/plugin-sdk version/range",
526
+ DEFAULT_SDK_VERSION
527
+ ).option("--yes", "skip prompts and use defaults").action(
528
+ async (directoryArg, options) => {
529
+ if (options.ts && options.js) {
530
+ throw new import_commander.InvalidArgumentError("Choose either --ts or --js, not both.");
531
+ }
532
+ const promptDefaults = {
533
+ directory: directoryArg ?? "my-media-plugin",
534
+ language: options.ts ? "ts" : options.js ? "js" : "ts",
535
+ preset: options.preset ?? DEFAULT_PRESET,
536
+ extraCapabilities: options.capabilities ?? [],
537
+ advanced: options.advanced ?? false,
538
+ sdkVersion: options.sdkVersion ?? DEFAULT_SDK_VERSION
539
+ };
540
+ const shouldPrompt = !options.yes;
541
+ let directory = promptDefaults.directory;
542
+ let language = promptDefaults.language;
543
+ let preset = promptDefaults.preset;
544
+ let extraCapabilities = promptDefaults.extraCapabilities;
545
+ let advanced = promptDefaults.advanced;
546
+ let sdkVersion = promptDefaults.sdkVersion;
547
+ if (shouldPrompt) {
548
+ const answers = await (0, import_prompts.default)(
549
+ [
550
+ {
551
+ type: "text",
552
+ name: "directory",
553
+ message: "Project directory",
554
+ initial: directory
555
+ },
556
+ {
557
+ type: "select",
558
+ name: "language",
559
+ message: "Template language",
560
+ choices: [
561
+ { title: "TypeScript", value: "ts" },
562
+ { title: "JavaScript", value: "js" }
563
+ ],
564
+ initial: language === "ts" ? 0 : 1
565
+ },
566
+ {
567
+ type: "select",
568
+ name: "preset",
569
+ message: "Plugin preset",
570
+ choices: CAPABILITIES.map((capability) => ({
571
+ title: capability,
572
+ value: capability
573
+ })),
574
+ initial: CAPABILITIES.indexOf(preset)
575
+ },
576
+ {
577
+ type: "multiselect",
578
+ name: "extraCapabilities",
579
+ message: "Extra capabilities (optional)",
580
+ choices: CAPABILITIES.map((capability) => ({
581
+ title: capability,
582
+ value: capability
583
+ })),
584
+ instructions: false,
585
+ min: 0,
586
+ hint: "Space to select"
587
+ },
588
+ {
589
+ type: "confirm",
590
+ name: "advanced",
591
+ message: "Generate advanced capability examples?",
592
+ initial: advanced
593
+ },
594
+ {
595
+ type: "text",
596
+ name: "sdkVersion",
597
+ message: "SDK dependency version/range",
598
+ initial: sdkVersion
599
+ }
600
+ ],
601
+ {
602
+ onCancel: () => {
603
+ process.exit(1);
604
+ }
605
+ }
606
+ );
607
+ directory = answers.directory;
608
+ language = answers.language;
609
+ preset = answers.preset ?? promptDefaults.preset;
610
+ extraCapabilities = answers.extraCapabilities ?? [];
611
+ advanced = Boolean(answers.advanced ?? promptDefaults.advanced);
612
+ sdkVersion = answers.sdkVersion ?? promptDefaults.sdkVersion;
613
+ }
614
+ const targetDir = import_node_path2.default.resolve(process.cwd(), directory);
615
+ const projectName = import_node_path2.default.basename(targetDir);
616
+ await scaffoldProject({
617
+ targetDir,
618
+ projectName,
619
+ language,
620
+ preset,
621
+ extraCapabilities,
622
+ advanced,
623
+ sdkVersion
624
+ });
625
+ console.log(`Created ${projectName} at ${targetDir}`);
626
+ console.log("Next steps:");
627
+ console.log(` cd ${directory}`);
628
+ console.log(" npm install");
629
+ console.log(" npm run dev");
630
+ }
631
+ );
632
+ void program.parseAsync(process.argv).catch((error) => {
633
+ const message = error instanceof Error ? error.message : String(error);
634
+ console.error(`Error: ${message}`);
635
+ process.exit(1);
636
+ });
637
+ //# sourceMappingURL=cli.cjs.map