create-leaderboard-plugin 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/dist/index.js ADDED
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Create Leaderboard Plugin CLI Tool
4
+ *
5
+ * Generates a new leaderboard plugin project structure
6
+ */
7
+ import { readFile } from "fs/promises";
8
+ import { join, resolve } from "path";
9
+ import { existsSync, mkdirSync, writeFileSync, readdirSync } from "fs";
10
+ import prompts from "prompts";
11
+ import { generatePackageJson } from "./templates/package-json";
12
+ import { generateTsConfig } from "./templates/tsconfig";
13
+ import { generateVitestConfig } from "./templates/vitest-config";
14
+ import { generateIndexTs } from "./templates/index-ts";
15
+ import { generateTestTs } from "./templates/test-ts";
16
+ import { generateReadme } from "./templates/readme";
17
+ /**
18
+ * Validate plugin name format (alphanumeric + hyphens, no spaces)
19
+ */
20
+ function validatePluginName(name) {
21
+ return /^[a-z0-9-]+$/.test(name) && name.length > 0;
22
+ }
23
+ /**
24
+ * Get default author from root package.json or environment
25
+ */
26
+ async function getDefaultAuthor() {
27
+ try {
28
+ const rootPackageJsonPath = resolve(process.cwd(), "package.json");
29
+ if (existsSync(rootPackageJsonPath)) {
30
+ const content = await readFile(rootPackageJsonPath, "utf-8");
31
+ const pkg = JSON.parse(content);
32
+ if (pkg.author) {
33
+ return pkg.author;
34
+ }
35
+ }
36
+ }
37
+ catch {
38
+ // Ignore errors
39
+ }
40
+ return "Open Healthcare Network";
41
+ }
42
+ /**
43
+ * Prompt user for plugin information
44
+ */
45
+ async function promptUser() {
46
+ const defaultAuthor = await getDefaultAuthor();
47
+ const response = await prompts([
48
+ {
49
+ type: "text",
50
+ name: "pluginName",
51
+ message: "Plugin name (e.g., 'github', 'slack'):",
52
+ validate: (value) => {
53
+ if (!value)
54
+ return "Plugin name is required";
55
+ if (!validatePluginName(value)) {
56
+ return "Plugin name must contain only lowercase letters, numbers, and hyphens";
57
+ }
58
+ return true;
59
+ },
60
+ },
61
+ {
62
+ type: "text",
63
+ name: "description",
64
+ message: "Plugin description:",
65
+ validate: (value) => {
66
+ if (!value)
67
+ return "Description is required";
68
+ return true;
69
+ },
70
+ },
71
+ {
72
+ type: "text",
73
+ name: "author",
74
+ message: "Author:",
75
+ initial: defaultAuthor,
76
+ validate: (value) => {
77
+ if (!value)
78
+ return "Author is required";
79
+ return true;
80
+ },
81
+ },
82
+ ]);
83
+ if (!response.pluginName || !response.description || !response.author) {
84
+ console.error("Creation cancelled.");
85
+ process.exit(1);
86
+ }
87
+ return {
88
+ pluginName: response.pluginName,
89
+ description: response.description,
90
+ author: response.author,
91
+ packageName: `@leaderboard/plugin-${response.pluginName}`,
92
+ };
93
+ }
94
+ /**
95
+ * Create directory structure
96
+ */
97
+ function createDirectoryStructure(pluginDir) {
98
+ const dirs = [
99
+ pluginDir,
100
+ join(pluginDir, "src"),
101
+ join(pluginDir, "src", "__tests__"),
102
+ ];
103
+ for (const dir of dirs) {
104
+ if (!existsSync(dir)) {
105
+ mkdirSync(dir, { recursive: true });
106
+ }
107
+ }
108
+ }
109
+ /**
110
+ * Generate plugin files
111
+ */
112
+ function generateFiles(pluginDir, options) {
113
+ // package.json
114
+ writeFileSync(join(pluginDir, "package.json"), generatePackageJson(options), "utf-8");
115
+ // tsconfig.json
116
+ writeFileSync(join(pluginDir, "tsconfig.json"), generateTsConfig(), "utf-8");
117
+ // vitest.config.ts
118
+ writeFileSync(join(pluginDir, "vitest.config.ts"), generateVitestConfig(), "utf-8");
119
+ // src/index.ts
120
+ writeFileSync(join(pluginDir, "src", "index.ts"), generateIndexTs(options), "utf-8");
121
+ // src/__tests__/plugin.test.ts
122
+ writeFileSync(join(pluginDir, "src", "__tests__", "plugin.test.ts"), generateTestTs(options), "utf-8");
123
+ // README.md
124
+ writeFileSync(join(pluginDir, "README.md"), generateReadme(options), "utf-8");
125
+ }
126
+ /**
127
+ * Main function
128
+ */
129
+ async function main() {
130
+ console.log("Create Leaderboard Plugin\n");
131
+ // Show usage if help flag is provided
132
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
133
+ console.log("Usage: pnpm create-leaderboard-plugin [target-directory]");
134
+ console.log("\nArguments:");
135
+ console.log(" target-directory Directory where the plugin will be created (default: current directory)");
136
+ console.log("\nExamples:");
137
+ console.log(" pnpm create-leaderboard-plugin .");
138
+ console.log(" pnpm create-leaderboard-plugin ../../plugins/slack");
139
+ console.log(" pnpm create-leaderboard-plugin my-plugin");
140
+ process.exit(0);
141
+ }
142
+ // Get target directory from command line args (default to current directory)
143
+ const targetArg = process.argv[2] || ".";
144
+ const targetDir = resolve(process.cwd(), targetArg);
145
+ // Check if target directory exists and is not empty
146
+ if (existsSync(targetDir)) {
147
+ const files = readdirSync(targetDir);
148
+ if (files.length > 0) {
149
+ console.error(`Error: Directory "${targetArg}" is not empty.`);
150
+ console.error("Please specify an empty directory or a new directory path.");
151
+ process.exit(1);
152
+ }
153
+ }
154
+ // Prompt for information
155
+ const options = await promptUser();
156
+ try {
157
+ // Create directory structure
158
+ console.log(`Creating plugin in: ${targetDir}`);
159
+ createDirectoryStructure(targetDir);
160
+ // Generate files
161
+ console.log("Generating plugin files...");
162
+ generateFiles(targetDir, options);
163
+ console.log(`\n✓ Plugin "${options.packageName}" created successfully!`);
164
+ console.log(`\nNext steps:`);
165
+ if (targetArg !== ".") {
166
+ console.log(` 1. cd ${targetArg}`);
167
+ console.log(` 2. pnpm install`);
168
+ }
169
+ else {
170
+ console.log(` 1. pnpm install`);
171
+ }
172
+ console.log(` ${targetArg !== "." ? "3" : "2"}. Implement your plugin logic in src/index.ts`);
173
+ console.log(` ${targetArg !== "." ? "4" : "3"}. pnpm build`);
174
+ console.log(` ${targetArg !== "." ? "5" : "4"}. Add the plugin to your config.yaml`);
175
+ }
176
+ catch (error) {
177
+ console.error("Error creating plugin:", error);
178
+ process.exit(1);
179
+ }
180
+ }
181
+ // Run main function
182
+ main().catch((error) => {
183
+ console.error("Unhandled error:", error);
184
+ process.exit(1);
185
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Generate src/index.ts template
3
+ */
4
+ export function generateIndexTs(options) {
5
+ return `/**
6
+ * ${options.description}
7
+ */
8
+
9
+ import {
10
+ activityDefinitionQueries,
11
+ activityQueries,
12
+ contributorQueries,
13
+ contributorAggregateDefinitionQueries,
14
+ contributorAggregateQueries,
15
+ badgeDefinitionQueries,
16
+ contributorBadgeQueries,
17
+ type Plugin,
18
+ type PluginContext,
19
+ } from "@ohcnetwork/leaderboard-api";
20
+
21
+ const plugin: Plugin = {
22
+ name: "${options.packageName}",
23
+ version: "0.1.0",
24
+
25
+ async setup(ctx: PluginContext) {
26
+ ctx.logger.info("Setting up ${options.pluginName} plugin...");
27
+
28
+ // TODO: Define activity types here
29
+ // Example:
30
+ // await activityDefinitionQueries.insertOrIgnore(ctx.db, {
31
+ // slug: "activity_slug",
32
+ // name: "Activity Name",
33
+ // description: "Activity description",
34
+ // points: 10,
35
+ // icon: "icon-name",
36
+ // });
37
+
38
+ // TODO: Define contributor aggregate definitions (optional)
39
+ // Example:
40
+ // await contributorAggregateDefinitionQueries.upsert(ctx.db, {
41
+ // slug: "custom_metric",
42
+ // name: "Custom Metric",
43
+ // description: "Example custom metric",
44
+ // });
45
+
46
+ // TODO: Define badge definitions (optional)
47
+ // Example:
48
+ // await badgeDefinitionQueries.upsert(ctx.db, {
49
+ // slug: "example_badge",
50
+ // name: "Example Badge",
51
+ // description: "Achievement badge for custom criteria",
52
+ // variants: {
53
+ // bronze: {
54
+ // description: "Level 1",
55
+ // svg_url: "https://example.com/bronze.svg",
56
+ // },
57
+ // silver: {
58
+ // description: "Level 2",
59
+ // svg_url: "https://example.com/silver.svg",
60
+ // },
61
+ // gold: {
62
+ // description: "Level 3",
63
+ // svg_url: "https://example.com/gold.svg",
64
+ // },
65
+ // },
66
+ // });
67
+
68
+ ctx.logger.info("Setup complete");
69
+ },
70
+
71
+ async scrape(ctx: PluginContext) {
72
+ ctx.logger.info("Starting ${options.pluginName} data scraping...");
73
+
74
+ // TODO: Implement your scraping logic here
75
+ // Example:
76
+ // const data = await fetchDataFromSource(ctx.config);
77
+ //
78
+ // for (const item of data) {
79
+ // // Ensure contributor exists
80
+ // await contributorQueries.upsert(ctx.db, {
81
+ // username: item.user.username,
82
+ // name: item.user.name,
83
+ // role: null,
84
+ // title: null,
85
+ // avatar_url: item.user.avatar_url,
86
+ // bio: null,
87
+ // social_profiles: null,
88
+ // joining_date: null,
89
+ // meta: null,
90
+ // });
91
+ //
92
+ // // Insert activity
93
+ // await activityQueries.upsert(ctx.db, {
94
+ // slug: \`activity-\${item.id}\`,
95
+ // contributor: item.user.username,
96
+ // activity_definition: "activity_slug",
97
+ // title: item.title,
98
+ // occured_at: new Date(item.timestamp).toISOString(),
99
+ // link: item.url,
100
+ // text: item.description,
101
+ // points: null, // Uses default from activity_definition
102
+ // meta: null,
103
+ // });
104
+ // }
105
+
106
+ // TODO: Set custom aggregates (optional)
107
+ // Example:
108
+ // await contributorAggregateQueries.upsert(ctx.db, {
109
+ // aggregate: "custom_metric",
110
+ // contributor: "username",
111
+ // value: {
112
+ // type: "number",
113
+ // value: 42,
114
+ // unit: "items",
115
+ // format: "integer",
116
+ // },
117
+ // meta: { source: "external_api" },
118
+ // });
119
+
120
+ // TODO: Award custom badges (optional)
121
+ // Example:
122
+ // await contributorBadgeQueries.award(ctx.db, {
123
+ // slug: \`example_badge__username__bronze\`,
124
+ // badge: "example_badge",
125
+ // contributor: "username",
126
+ // variant: "bronze",
127
+ // achieved_on: new Date().toISOString().split("T")[0],
128
+ // meta: { reason: "Custom criteria met" },
129
+ // });
130
+
131
+ ctx.logger.info("Scraping complete");
132
+ },
133
+ };
134
+
135
+ export default plugin;
136
+ `;
137
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Generate package.json template
3
+ */
4
+ export function generatePackageJson(options) {
5
+ // Use JSON.stringify to safely escape strings
6
+ const pkg = {
7
+ name: options.packageName,
8
+ version: "0.1.0",
9
+ description: options.description,
10
+ type: "module",
11
+ main: "dist/index.js",
12
+ types: "dist/index.d.ts",
13
+ scripts: {
14
+ build: "tsc",
15
+ test: "vitest run",
16
+ "test:watch": "vitest",
17
+ },
18
+ keywords: ["leaderboard", "plugin"],
19
+ dependencies: {
20
+ "@ohcnetwork/leaderboard-api": "^0.1.0",
21
+ },
22
+ devDependencies: {
23
+ "@types/node": "^20.19.27",
24
+ typescript: "^5.7.3",
25
+ vitest: "^4.0.16",
26
+ },
27
+ author: options.author,
28
+ license: "MIT",
29
+ };
30
+ return JSON.stringify(pkg, null, 2) + "\n";
31
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Generate README.md template
3
+ */
4
+ export function generateReadme(options) {
5
+ const displayName = options.pluginName
6
+ .split("-")
7
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
8
+ .join(" ");
9
+ return `# ${options.packageName}
10
+
11
+ ${options.description}
12
+
13
+ ## Configuration
14
+
15
+ Add the plugin to your \`config.yaml\`:
16
+
17
+ \`\`\`yaml
18
+ leaderboard:
19
+ plugins:
20
+ ${options.pluginName}:
21
+ source: "${options.packageName}"
22
+ config:
23
+ # TODO: Add your plugin configuration options here
24
+ \`\`\`
25
+
26
+ ## Usage
27
+
28
+ 1. Build the plugin:
29
+
30
+ \`\`\`bash
31
+ pnpm build
32
+ \`\`\`
33
+
34
+ 2. Add the plugin to your \`config.yaml\` (see Configuration above)
35
+
36
+ 3. Run the plugin runner:
37
+
38
+ \`\`\`bash
39
+ pnpm data:scrape
40
+ \`\`\`
41
+
42
+ ## Development
43
+
44
+ \`\`\`bash
45
+ # Build the plugin
46
+ pnpm build
47
+
48
+ # Run tests
49
+ pnpm test
50
+
51
+ # Watch mode
52
+ pnpm test:watch
53
+ \`\`\`
54
+
55
+ ## License
56
+
57
+ MIT
58
+ `;
59
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Generate src/__tests__/plugin.test.ts template
3
+ */
4
+ export function generateTestTs(options) {
5
+ return `/**
6
+ * Tests for ${options.pluginName} plugin
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
10
+ import { createDatabase, initializeSchema } from "@ohcnetwork/leaderboard-api";
11
+ import type { Database } from "@ohcnetwork/leaderboard-api";
12
+ import plugin from "../index";
13
+
14
+ describe("${options.pluginName.charAt(0).toUpperCase() + options.pluginName.slice(1)} Plugin", () => {
15
+ let db: Database;
16
+
17
+ beforeEach(async () => {
18
+ db = createDatabase(":memory:");
19
+ await initializeSchema(db);
20
+ });
21
+
22
+ afterEach(async () => {
23
+ await db.close();
24
+ });
25
+
26
+ it("should have correct plugin metadata", () => {
27
+ expect(plugin.name).toBe("${options.packageName}");
28
+ expect(plugin.version).toBeTruthy();
29
+ expect(plugin.scrape).toBeDefined();
30
+ });
31
+
32
+ it("should setup activity definitions", async () => {
33
+ const logger = {
34
+ info: () => {},
35
+ warn: () => {},
36
+ error: () => {},
37
+ debug: () => {},
38
+ };
39
+
40
+ if (plugin.setup) {
41
+ await plugin.setup({
42
+ db,
43
+ config: {},
44
+ orgConfig: {
45
+ name: "Test Org",
46
+ description: "Test",
47
+ url: "https://test.com",
48
+ logo_url: "https://test.com/logo.png",
49
+ },
50
+ logger,
51
+ });
52
+ }
53
+
54
+ // TODO: Add assertions for your activity definitions
55
+ });
56
+
57
+ it("should scrape data", async () => {
58
+ const logger = {
59
+ info: () => {},
60
+ warn: () => {},
61
+ error: () => {},
62
+ debug: () => {},
63
+ };
64
+
65
+ // Setup first if needed
66
+ if (plugin.setup) {
67
+ await plugin.setup({
68
+ db,
69
+ config: {},
70
+ orgConfig: {
71
+ name: "Test Org",
72
+ description: "Test",
73
+ url: "https://test.com",
74
+ logo_url: "https://test.com/logo.png",
75
+ },
76
+ logger,
77
+ });
78
+ }
79
+
80
+ // Then scrape
81
+ await plugin.scrape({
82
+ db,
83
+ config: {},
84
+ orgConfig: {
85
+ name: "Test Org",
86
+ description: "Test",
87
+ url: "https://test.com",
88
+ logo_url: "https://test.com/logo.png",
89
+ },
90
+ logger,
91
+ });
92
+
93
+ // TODO: Add assertions for scraped data
94
+ });
95
+ });
96
+ `;
97
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Generate tsconfig.json template
3
+ */
4
+ export function generateTsConfig() {
5
+ return `{
6
+ "compilerOptions": {
7
+ "target": "ES2017",
8
+ "lib": ["dom", "dom.iterable", "esnext"],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "module": "esnext",
14
+ "moduleResolution": "bundler",
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "outDir": "./dist",
18
+ "rootDir": ".",
19
+ "incremental": true,
20
+ "paths": {
21
+ "@/*": ["./*"]
22
+ },
23
+ "noUncheckedIndexedAccess": true,
24
+ "strictNullChecks": true
25
+ },
26
+ "include": ["**/*.ts", "**/*.mts"],
27
+ "exclude": ["node_modules"]
28
+ }
29
+
30
+ `;
31
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Generate vitest.config.ts template
3
+ */
4
+ export function generateVitestConfig() {
5
+ return `import { defineConfig } from "vitest/config";
6
+
7
+ export default defineConfig({
8
+ test: {
9
+ globals: true,
10
+ environment: "node",
11
+ },
12
+ });
13
+ `;
14
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Types for the create-plugin tool
3
+ */
4
+ export {};
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "create-leaderboard-plugin",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to generate leaderboard plugin projects",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "private": false,
8
+ "bin": {
9
+ "create-leaderboard-plugin": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "clean": "rm -rf dist",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "keywords": [
20
+ "leaderboard",
21
+ "plugin",
22
+ "generator",
23
+ "cli"
24
+ ],
25
+ "author": "Open Healthcare Network",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "prompts": "^2.4.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.19.27",
32
+ "@types/prompts": "^2.4.9",
33
+ "typescript": "^5.7.3"
34
+ }
35
+ }