@daemonitor/plugins 1.0.3 → 1.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 CHANGED
@@ -1,6 +1,9 @@
1
- export { PluginConfigProvider } from "./lib/PluginConfigProvider.js";
2
- export { BasePlugin } from "./lib/BasePlugin.js";
3
- export { MonitoringPlugin } from "./lib/MonitoringPlugin.js";
4
- export { PluginLoader } from "./lib/PluginLoader.js";
5
- export { PluginManager } from "./lib/PluginManager.js";
6
- export * as Plugins from "./plugins/index.js";
1
+ // Export core plugin utilities
2
+ export { PluginConfigProvider } from "./lib/PluginConfigProvider";
3
+ export { PluginManager } from "./lib/PluginManager";
4
+ export { PluginLoader } from "./lib/PluginLoader";
5
+ // Only export creation functions, not types
6
+ export { createBasePlugin } from "./lib/BasePlugin";
7
+ export { createMonitoringPlugin } from "./lib/MonitoringPlugin";
8
+ // Export the PM2 plugin factory function
9
+ export { createPM2Plugin } from "./plugins/monitoring/PM2Plugin";
@@ -1,17 +1,27 @@
1
- import { PluginConfigProvider } from "./PluginConfigProvider.js";
2
- class BasePlugin {
3
- static currentPluginIndex = 0;
4
- alias;
5
- name;
6
- description;
7
- config;
8
- constructor(alias, name, description) {
9
- this.config = PluginConfigProvider.get(alias);
10
- this.name = name;
11
- this.description = description;
12
- }
13
- getName() {
14
- return this.name;
15
- }
1
+ import { PluginConfigProvider } from "./PluginConfigProvider";
2
+ let currentPluginIndex = 0;
3
+ export function createBasePlugin(alias, name, description, setupFn, monitorFn, refreshFn, teardownFn) {
4
+ const config = PluginConfigProvider.get(alias);
5
+ const plugin = {
6
+ alias,
7
+ name,
8
+ description,
9
+ config,
10
+ setup: async function () {
11
+ return setupFn(this);
12
+ },
13
+ monitor: async function () {
14
+ return monitorFn(this);
15
+ },
16
+ refresh: async function () {
17
+ return refreshFn(this);
18
+ },
19
+ teardown: async function () {
20
+ return teardownFn(this);
21
+ },
22
+ getName: function () {
23
+ return this.name;
24
+ }
25
+ };
26
+ return plugin;
16
27
  }
17
- export { BasePlugin };
@@ -1,24 +1,40 @@
1
- import { BasePlugin } from "./BasePlugin.js";
2
- import { PluginManager } from "@daemonitor/plugins";
3
- class MonitoringPlugin extends BasePlugin {
4
- static currentPluginIndex = 0;
5
- instanceName;
6
- refreshTimer;
7
- uniqueId;
8
- pluginIndex;
9
- constructor(alias, name, description) {
10
- super(alias, name, description);
11
- this.pluginIndex = MonitoringPlugin.currentPluginIndex++;
12
- this.instanceName = this.config.name || name;
13
- this.uniqueId = this.config?.uniqueId || this.alias + "-" + this.pluginIndex;
14
- }
15
- async send(data, unique_id) {
16
- for (const apiConnection of PluginManager.API_CONNECTIONS) {
17
- await apiConnection.sendData({ name: this.instanceName, ...data }, this.name, unique_id || this.uniqueId);
1
+ import { PluginManager } from "./PluginManager";
2
+ let monitoringPluginIndex = 0;
3
+ export function createMonitoringPlugin(alias, name, description, setupFn, monitorFn, refreshFn, teardownFn) {
4
+ const config = PluginManager.getConfig ? PluginManager.getConfig(alias) : {};
5
+ const pluginIndex = monitoringPluginIndex++;
6
+ const instanceName = config?.name || name;
7
+ const uniqueId = config?.uniqueId || `${alias}-${pluginIndex}`;
8
+ const plugin = {
9
+ alias,
10
+ name,
11
+ description,
12
+ config,
13
+ instanceName,
14
+ uniqueId,
15
+ pluginIndex,
16
+ setup: async function () {
17
+ return setupFn(this);
18
+ },
19
+ monitor: async function () {
20
+ return monitorFn(this);
21
+ },
22
+ refresh: async function () {
23
+ return refreshFn(this);
24
+ },
25
+ teardown: async function () {
26
+ return teardownFn(this);
27
+ },
28
+ getName: function () {
29
+ return this.name;
30
+ },
31
+ send: async function (data, unique_id) {
32
+ // Access API_CONNECTIONS from PluginManager
33
+ const connections = PluginManager.API_CONNECTIONS || [];
34
+ for (const apiConnection of connections) {
35
+ await apiConnection.sendData({ name: this.instanceName, ...data }, this.name, unique_id || this.uniqueId);
36
+ }
18
37
  }
19
- }
20
- getName() {
21
- return this.name;
22
- }
38
+ };
39
+ return plugin;
23
40
  }
24
- export { MonitoringPlugin };
@@ -1,10 +1,17 @@
1
- class PluginConfigProvider {
1
+ export class PluginConfigProvider {
2
2
  static config = {};
3
- static async get(key) {
4
- if (!PluginConfigProvider.config[key]) {
5
- return null;
6
- }
7
- return PluginConfigProvider.config[key];
3
+ /**
4
+ * Load the full client config (the parsed client.config.json). Per-plugin
5
+ * sections are keyed by alias (e.g. config.docker, config.pm2). Without this
6
+ * the per-alias `get()` below always returns null and plugins silently fall
7
+ * back to their hardcoded defaults.
8
+ */
9
+ static loadAll(config) {
10
+ PluginConfigProvider.config = config || {};
11
+ }
12
+ /** Synchronous lookup of a plugin's config section by alias. */
13
+ static get(key) {
14
+ const section = PluginConfigProvider.config?.[key];
15
+ return section == null ? null : section;
8
16
  }
9
17
  }
10
- export { PluginConfigProvider };
@@ -1,13 +1,28 @@
1
- import PluginClasses from "../plugins/index.js";
2
- export class PluginLoader {
3
- static async loadPlugins() {
4
- const plugins = [];
5
- const pluginClasses = PluginClasses;
6
- for (const file of Object.keys(pluginClasses)) {
7
- const PluginClass = pluginClasses[file];
8
- const pluginInstance = new PluginClass();
9
- plugins.push(pluginInstance);
1
+ import pluginsFactory from "../plugins/index";
2
+ export const PluginLoader = {
3
+ // Load all plugins using the factories
4
+ loadPlugins: async function () {
5
+ try {
6
+ const plugins = [];
7
+ // Create plugin instances using the factory functions
8
+ const pluginFactories = pluginsFactory;
9
+ for (const pluginName of Object.keys(pluginFactories)) {
10
+ const createPlugin = pluginFactories[pluginName];
11
+ if (typeof createPlugin === 'function') {
12
+ try {
13
+ const plugin = await createPlugin();
14
+ plugins.push(plugin);
15
+ }
16
+ catch (err) {
17
+ console.error(`Error creating plugin ${pluginName}:`, err);
18
+ }
19
+ }
20
+ }
21
+ return plugins;
22
+ }
23
+ catch (error) {
24
+ console.error("Error loading plugins:", error);
25
+ return [];
10
26
  }
11
- return plugins;
12
27
  }
13
- }
28
+ };
@@ -1,19 +1,37 @@
1
- import { PluginLoader } from "./PluginLoader.js";
2
- class PluginManager {
3
- static API_CONNECTIONS = [];
4
- availablePlugins;
5
- activePlugins;
6
- configuredPlugins;
7
- constructor(configuredPlugins) {
8
- this.availablePlugins = [];
9
- this.activePlugins = [];
10
- this.configuredPlugins = configuredPlugins;
11
- }
12
- async initialize() {
13
- return this.loadPlugins().then(() => this.setupAll());
14
- }
15
- async loadPlugins() {
16
- return await PluginLoader.loadPlugins().then(plugins => {
1
+ import { PluginLoader } from "./PluginLoader";
2
+ import { PluginConfigProvider } from "./PluginConfigProvider";
3
+ // Singleton state
4
+ const state = {
5
+ availablePlugins: [],
6
+ activePlugins: [],
7
+ configuredPlugins: []
8
+ };
9
+ // Export as namespace instead of class
10
+ export const PluginManager = {
11
+ // Global connections array
12
+ API_CONNECTIONS: [],
13
+ // Initialize the manager. Accepts either the legacy string[] of plugin
14
+ // aliases, or the full client config object ({ plugins: [...], docker: {...}, ... }).
15
+ // Passing the full object also populates PluginConfigProvider so plugins
16
+ // receive their per-alias config section (dockerBin, refreshInterval, etc.).
17
+ initialize: async function (config) {
18
+ let configuredPlugins;
19
+ if (Array.isArray(config)) {
20
+ configuredPlugins = config;
21
+ }
22
+ else {
23
+ configuredPlugins = config?.plugins || [];
24
+ PluginConfigProvider.loadAll(config);
25
+ }
26
+ state.configuredPlugins = configuredPlugins.map((p) => String(p).toLowerCase());
27
+ await this.loadPlugins();
28
+ await this.setupAll();
29
+ return;
30
+ },
31
+ // Load all available plugins
32
+ loadPlugins: async function () {
33
+ try {
34
+ const plugins = await PluginLoader.loadPlugins();
17
35
  if (!plugins) {
18
36
  throw new Error("Failed to load plugins.");
19
37
  }
@@ -23,47 +41,63 @@ class PluginManager {
23
41
  else {
24
42
  console.log(`Loaded ${plugins.length} plugins.`);
25
43
  }
26
- this.availablePlugins = plugins;
27
- });
28
- }
29
- async addConnector(apiConnection) {
30
- PluginManager.API_CONNECTIONS.push(apiConnection);
31
- }
32
- async setupAll() {
44
+ state.availablePlugins = plugins;
45
+ }
46
+ catch (error) {
47
+ console.error("Error loading plugins:", error);
48
+ throw error;
49
+ }
50
+ },
51
+ // Add a connector for plugins to use
52
+ addConnector: function (apiConnection) {
53
+ this.API_CONNECTIONS.push(apiConnection);
54
+ },
55
+ // Get connections for plugins
56
+ getConnections: function () {
57
+ return this.API_CONNECTIONS;
58
+ },
59
+ // Get config for a plugin by alias
60
+ getConfig: function (alias) {
61
+ return PluginConfigProvider.get(alias);
62
+ },
63
+ // Set up all the configured plugins
64
+ setupAll: async function () {
33
65
  console.log("Setting up plugins...");
34
- // const configuredPlugins = await AppConfigProvider.get("plugins")
35
- console.log("Configured plugins:", this.configuredPlugins.join(", "));
36
- if (!this.configuredPlugins)
66
+ console.log("Configured plugins:", state.configuredPlugins.join(", "));
67
+ if (!state.configuredPlugins || state.configuredPlugins.length === 0) {
37
68
  throw new Error("No plugins configured. Set up config.json first.");
38
- this.activePlugins = this.availablePlugins.filter(plugin => {
39
- return this.configuredPlugins.includes(plugin.getName().toLowerCase());
69
+ }
70
+ state.activePlugins = state.availablePlugins.filter(plugin => {
71
+ return state.configuredPlugins.includes(plugin.getName().toLowerCase());
40
72
  });
41
- if (!this.activePlugins)
73
+ if (state.activePlugins.length === 0) {
42
74
  throw new Error("No plugins to set up.");
43
- for (const plugin of this.activePlugins) {
75
+ }
76
+ for (const plugin of state.activePlugins) {
44
77
  await plugin.setup();
45
78
  }
46
- }
47
- async monitorAll() {
79
+ },
80
+ // Start monitoring with all plugins
81
+ monitorAll: async function () {
48
82
  const results = [];
49
- if (!this.availablePlugins)
83
+ if (!state.availablePlugins || state.availablePlugins.length === 0) {
50
84
  throw new Error("Plugins not loaded yet! Call loadPlugins() first.");
51
- if (this.availablePlugins.length === 0)
52
- throw new Error("No plugins loaded.");
53
- if (!PluginManager.API_CONNECTIONS || PluginManager.API_CONNECTIONS.length === 0)
85
+ }
86
+ if (this.API_CONNECTIONS.length === 0) {
54
87
  throw new Error("No connections loaded yet! Call addConnector() first.");
55
- console.log(`Monitoring ${this.activePlugins.length} plugins...`);
56
- for (const plugin of this.activePlugins) {
88
+ }
89
+ console.log(`Monitoring ${state.activePlugins.length} plugins...`);
90
+ for (const plugin of state.activePlugins) {
57
91
  console.log(` + ${plugin.getName()}`);
58
- const data = plugin.monitor();
92
+ const data = await plugin.monitor();
59
93
  results.push(data);
60
94
  }
61
95
  return results;
62
- }
63
- async teardownAll() {
64
- for (const plugin of this.activePlugins) {
96
+ },
97
+ // Tear down all plugins
98
+ teardownAll: async function () {
99
+ for (const plugin of state.activePlugins) {
65
100
  await plugin.teardown();
66
101
  }
67
102
  }
68
- }
69
- export { PluginManager };
103
+ };
@@ -0,0 +1 @@
1
+ export default defineNuxtConfig({});
@@ -1,12 +1,27 @@
1
- import { EwelinkPlugin } from "./monitoring/EwelinkPlugin.js";
2
- import { OSPlugin } from "./monitoring/OSPlugin.js";
3
- import { PM2Plugin } from "./monitoring/PM2Plugin.js";
4
- import { MongoDBPlugin } from "./monitoring/MongoDBPlugin.js";
5
- import { WebsitePlugin } from "./monitoring/WebsitePlugin.js";
1
+ // Import plugin factory functions
2
+ import { createPM2Plugin } from "./monitoring/PM2Plugin";
3
+ import { createDockerPlugin } from "./monitoring/DockerPlugin";
4
+ import { createLxcPlugin } from "./monitoring/LxcPlugin";
5
+ import { createOSPlugin } from "./monitoring/OSPlugin";
6
+ import { createWebsitePlugin } from "./monitoring/WebsitePlugin";
7
+ // In future, implement other plugin factory functions using the same pattern
8
+ // import { createOSPlugin } from "./monitoring/OSPlugin"
9
+ // import { createMongoDBPlugin } from "./monitoring/MongoDBPlugin"
10
+ // import { createWebsitePlugin } from "./monitoring/WebsitePlugin"
11
+ // import { createCloudflarePlugin } from "./monitoring/CloudflarePlugin"
12
+ // import { createEwelinkPlugin } from "./monitoring/EwelinkPlugin"
13
+ // Export factory functions to create plugin instances.
14
+ // (Keys are display-only; the manager activates a plugin when its getName()
15
+ // lowercased matches an alias in the config `plugins` array.)
6
16
  export default {
7
- EwelinkPlugin,
8
- MongoDBPlugin,
9
- OSPlugin,
10
- PM2Plugin,
11
- WebsitePlugin
17
+ PM2Plugin: createPM2Plugin,
18
+ Docker: createDockerPlugin,
19
+ Lxc: createLxcPlugin,
20
+ OS: createOSPlugin,
21
+ Website: createWebsitePlugin,
22
+ // CloudflarePlugin: createCloudflarePlugin,
23
+ // EwelinkPlugin: createEwelinkPlugin,
24
+ // MongoDBPlugin: createMongoDBPlugin,
25
+ // OSPlugin: createOSPlugin,
26
+ // WebsitePlugin: createWebsitePlugin,
12
27
  };
@@ -0,0 +1,84 @@
1
+ import { MonitoringPlugin } from "../../lib/MonitoringPlugin.js";
2
+ /**
3
+ * CloudflarePlugin - Receives push updates from Cloudflare Workers
4
+ *
5
+ * This plugin receives data directly from a client's Cloudflare Worker
6
+ * rather than polling for it. The plugin sets up a monitoring endpoint,
7
+ * and the Cloudflare Worker pushes data to this endpoint on a schedule.
8
+ */
9
+ export class CloudflarePlugin extends MonitoringPlugin {
10
+ lastReportedData = null;
11
+ lastReportTime = 0;
12
+ reportingKey;
13
+ maxDataAge;
14
+ constructor() {
15
+ super("cloudflare", "Cloudflare", "Cloudflare Deployment Monitoring Plugin (Push Mode)");
16
+ this.reportingKey = this.config.reportingKey;
17
+ this.maxDataAge = this.config.maxDataAge || 5 * 60 * 1000; // Default 5 minutes
18
+ }
19
+ async setup() {
20
+ if (!this.reportingKey) {
21
+ console.error("Cloudflare reporting key not provided");
22
+ return Promise.reject("Reporting key not provided");
23
+ }
24
+ // Register this instance to receive push updates
25
+ try {
26
+ // Normally this would make an API call to register this plugin instance
27
+ // to receive push updates from Cloudflare Workers with this reporting key
28
+ console.log(`Cloudflare plugin ready to receive push updates for key: ${this.reportingKey}`);
29
+ return Promise.resolve();
30
+ }
31
+ catch (error) {
32
+ console.error("Failed to register for Cloudflare push updates:", error);
33
+ return Promise.reject("Registration failed");
34
+ }
35
+ }
36
+ /**
37
+ * This method is called when data is pushed from a Cloudflare Worker
38
+ * It would typically be called by a webhook or socket connection
39
+ */
40
+ async receiveUpdate(data, key) {
41
+ // Verify the reporting key matches
42
+ if (key !== this.reportingKey) {
43
+ console.error("Invalid reporting key for Cloudflare update");
44
+ return;
45
+ }
46
+ this.lastReportedData = {
47
+ ...data,
48
+ receivedAt: Date.now()
49
+ };
50
+ this.lastReportTime = Date.now();
51
+ // Forward the data to the monitoring system
52
+ await this.send({
53
+ ...data,
54
+ timestamp: Date.now(),
55
+ status: "ok"
56
+ });
57
+ }
58
+ // This method checks if data is still fresh
59
+ async refresh() {
60
+ const now = Date.now();
61
+ const dataAge = now - this.lastReportTime;
62
+ // If we have data and it's still fresh, do nothing
63
+ if (this.lastReportedData && dataAge < this.maxDataAge) {
64
+ return;
65
+ }
66
+ // If data is stale or doesn't exist, report error status
67
+ await this.send({
68
+ timestamp: now,
69
+ error: `No data received in the last ${dataAge / 60000} minutes`,
70
+ status: "error",
71
+ lastReportTime: this.lastReportTime
72
+ });
73
+ }
74
+ async monitor() {
75
+ // Set up interval just to check for stale data
76
+ this.refreshTimer = setInterval(this.refresh.bind(this), this.config.refreshInterval || 60000 // Default to 1 minute
77
+ );
78
+ }
79
+ async teardown() {
80
+ // Unregister from receiving push updates
81
+ clearInterval(this.refreshTimer);
82
+ return Promise.resolve();
83
+ }
84
+ }
@@ -0,0 +1,170 @@
1
+ import { createMonitoringPlugin } from "../../lib/MonitoringPlugin";
2
+ import { exec } from "child_process";
3
+ import { promisify } from "util";
4
+ import { hostname } from "os";
5
+ const execAsync = promisify(exec);
6
+ const EXEC_OPTS = { maxBuffer: 32 * 1024 * 1024 }; // containers.length can be large
7
+ const HEALTH_RE = /\((healthy|unhealthy|health: starting|starting)\)/i;
8
+ /** Convert docker's "83.86MiB" / "1.952GiB" memory strings to bytes. */
9
+ function memToBytes(value) {
10
+ if (!value)
11
+ return 0;
12
+ const unit = value.replace(/[0-9.\s]/g, "").toUpperCase();
13
+ const num = parseFloat(value.replace(/[^0-9.]/g, "")) || 0;
14
+ const mult = {
15
+ B: 1, KB: 1e3, KIB: 1024, MB: 1e6, MIB: 1024 ** 2,
16
+ GB: 1e9, GIB: 1024 ** 3, TB: 1e12, TIB: 1024 ** 4,
17
+ };
18
+ return Math.round(num * (mult[unit] ?? 1));
19
+ }
20
+ /**
21
+ * Collect all containers (including stopped) in a small, fixed number of
22
+ * exec calls regardless of how many containers exist:
23
+ * 1. `docker ps -a` — names, image, state, status, compose labels
24
+ * 2. `docker stats` — live CPU / memory for running containers (one batch)
25
+ * 3. `docker inspect` — restart count, health, exit code (one batch)
26
+ */
27
+ async function collectContainers(bin) {
28
+ const psFmt = '{{.ID}}|{{.Names}}|{{.Image}}|{{.State}}|{{.Status}}|{{.Label "com.docker.compose.project"}}|{{.Label "com.docker.compose.service"}}';
29
+ const { stdout: psOut } = await execAsync(`${bin} ps -a --format '${psFmt}'`, EXEC_OPTS);
30
+ const containers = [];
31
+ const byId = new Map();
32
+ for (const line of psOut.split("\n")) {
33
+ if (!line.trim())
34
+ continue;
35
+ const [id, name, image, state, status, project, service] = line.split("|");
36
+ const running = (state || "").toLowerCase() === "running";
37
+ const healthMatch = HEALTH_RE.exec(status || "");
38
+ const c = {
39
+ id,
40
+ name,
41
+ service: service || "",
42
+ image,
43
+ state: state || "",
44
+ status: status || "",
45
+ running,
46
+ health: healthMatch ? healthMatch[1].replace(/^health:\s*/i, "").toLowerCase() : "",
47
+ cpu: 0, mem: 0, memLimit: 0, memPercent: 0, restarts: 0, exitCode: 0,
48
+ };
49
+ c.__project = project || "";
50
+ containers.push(c);
51
+ byId.set(id, c);
52
+ }
53
+ if (!containers.length)
54
+ return containers;
55
+ // 2. live stats for running containers (one batched call, best-effort)
56
+ try {
57
+ const { stdout: statsOut } = await execAsync(`${bin} stats --no-stream --format '{{.ID}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}'`, EXEC_OPTS);
58
+ for (const line of statsOut.split("\n")) {
59
+ if (!line.trim())
60
+ continue;
61
+ const [id, cpuPerc, memUsage, memPerc] = line.split("|");
62
+ const c = byId.get(id);
63
+ if (!c)
64
+ continue;
65
+ c.cpu = parseFloat((cpuPerc || "").replace("%", "")) || 0;
66
+ const [used, limit] = (memUsage || "").split("/").map((p) => p.trim());
67
+ c.mem = memToBytes(used);
68
+ c.memLimit = memToBytes(limit);
69
+ c.memPercent = parseFloat((memPerc || "").replace("%", "")) || 0;
70
+ }
71
+ }
72
+ catch (err) {
73
+ console.error("docker: stats collection failed:", err.message);
74
+ }
75
+ // 3. restart count + exit code (one batched inspect, best-effort).
76
+ // NOTE: we deliberately do NOT read .State.Health here — on some engines the
77
+ // template errors with `map has no entry for key "Health"` for containers
78
+ // without a healthcheck, which would fail the whole batch. Health is parsed
79
+ // from the ps `Status` string ("(healthy)" / "(unhealthy)") instead.
80
+ try {
81
+ const ids = containers.map((c) => c.id).join(" ");
82
+ const inspectFmt = '{{.Id}}|{{.RestartCount}}|{{.State.ExitCode}}';
83
+ const { stdout: inspOut } = await execAsync(`${bin} inspect --format '${inspectFmt}' ${ids}`, EXEC_OPTS);
84
+ for (const line of inspOut.split("\n")) {
85
+ if (!line.trim())
86
+ continue;
87
+ const [fullId, restarts, exitCode] = line.split("|");
88
+ const c = byId.get(fullId.slice(0, 12));
89
+ if (!c)
90
+ continue;
91
+ c.restarts = parseInt(restarts, 10) || 0;
92
+ c.exitCode = parseInt(exitCode, 10) || 0;
93
+ }
94
+ }
95
+ catch (err) {
96
+ console.error("docker: inspect collection failed:", err.message);
97
+ }
98
+ return containers;
99
+ }
100
+ export function createDockerPlugin() {
101
+ let refreshTimer = null;
102
+ let available = false;
103
+ let dockerBin = "docker";
104
+ const setupFn = async (plugin) => {
105
+ dockerBin = plugin.config?.dockerBin || plugin.config?.bin || "docker";
106
+ try {
107
+ await execAsync(`${dockerBin} version --format '{{.Server.Version}}'`, EXEC_OPTS);
108
+ available = true;
109
+ }
110
+ catch (err) {
111
+ // Don't throw: a missing docker engine shouldn't take down the whole client.
112
+ available = false;
113
+ console.error(`docker: engine not available via "${dockerBin}" — plugin idle:`, err.message);
114
+ }
115
+ };
116
+ const refreshFn = async (plugin) => {
117
+ if (!available)
118
+ return;
119
+ try {
120
+ const all = await collectContainers(dockerBin);
121
+ // Optional filtering by compose project.
122
+ const only = plugin.config?.projects || [];
123
+ const exclude = plugin.config?.exclude || [];
124
+ // Group by compose project (standalone containers group under their own name).
125
+ const groups = new Map();
126
+ for (const c of all) {
127
+ const project = c.__project || c.name;
128
+ delete c.__project;
129
+ if (only.length && !only.includes(project))
130
+ continue;
131
+ if (exclude.includes(project))
132
+ continue;
133
+ const arr = groups.get(project) || [];
134
+ arr.push(c);
135
+ groups.set(project, arr);
136
+ }
137
+ const host = hostname();
138
+ for (const [project, containers] of groups) {
139
+ const running = containers.filter((c) => c.running).length;
140
+ const payload = {
141
+ name: project,
142
+ engine: "docker",
143
+ host,
144
+ project,
145
+ running,
146
+ total: containers.length,
147
+ containers,
148
+ timestamp: Date.now(),
149
+ };
150
+ await plugin.send(payload, `docker-${project}`);
151
+ }
152
+ }
153
+ catch (err) {
154
+ console.error("docker: refresh error:", err.message);
155
+ }
156
+ };
157
+ const monitorFn = async (plugin) => {
158
+ if (!available)
159
+ return;
160
+ await refreshFn(plugin);
161
+ refreshTimer = setInterval(() => refreshFn(plugin), plugin.config?.refreshInterval || 60000);
162
+ };
163
+ const teardownFn = async () => {
164
+ if (refreshTimer)
165
+ clearInterval(refreshTimer);
166
+ refreshTimer = null;
167
+ };
168
+ return createMonitoringPlugin("docker", "docker", "Docker container monitoring plugin", setupFn, monitorFn, refreshFn, teardownFn);
169
+ }
170
+ export default createDockerPlugin;
@@ -1,6 +1,6 @@
1
1
  import ewelink from "ewelink-api";
2
2
  import * as dotenv from "dotenv";
3
- import { MonitoringPlugin } from "../../lib/MonitoringPlugin.js";
3
+ import { MonitoringPlugin } from "~/lib/MonitoringPlugin";
4
4
  dotenv.config();
5
5
  export class EwelinkPlugin extends MonitoringPlugin {
6
6
  connection;
@@ -0,0 +1,126 @@
1
+ import { createMonitoringPlugin } from "../../lib/MonitoringPlugin";
2
+ import { exec } from "child_process";
3
+ import { promisify } from "util";
4
+ import { hostname } from "os";
5
+ const execAsync = promisify(exec);
6
+ const EXEC_OPTS = { maxBuffer: 32 * 1024 * 1024 };
7
+ /** Parse LXD memory-limit strings like "1GB", "512MiB", "2GiB" to bytes. */
8
+ function limitToBytes(value) {
9
+ if (!value)
10
+ return 0;
11
+ const unit = String(value).replace(/[0-9.\s]/g, "").toUpperCase();
12
+ const num = parseFloat(String(value).replace(/[^0-9.]/g, "")) || 0;
13
+ const mult = {
14
+ "": 1, B: 1, KB: 1e3, KIB: 1024, MB: 1e6, MIB: 1024 ** 2,
15
+ GB: 1e9, GIB: 1024 ** 3, TB: 1e12, TIB: 1024 ** 4,
16
+ };
17
+ return Math.round(num * (mult[unit] ?? 1));
18
+ }
19
+ export function createLxcPlugin() {
20
+ let refreshTimer = null;
21
+ let available = false;
22
+ let lxcBin = "lxc";
23
+ // cumulative CPU usage (ns) + wall clock (ms) per instance, for %-of-core calc
24
+ const prevCpu = new Map();
25
+ const setupFn = async (plugin) => {
26
+ lxcBin = plugin.config?.lxcBin || plugin.config?.bin || "lxc";
27
+ try {
28
+ await execAsync(`${lxcBin} version`, EXEC_OPTS);
29
+ available = true;
30
+ }
31
+ catch (err) {
32
+ available = false;
33
+ console.error(`lxc: "${lxcBin}" not available — plugin idle:`, err.message);
34
+ }
35
+ };
36
+ const refreshFn = async (plugin) => {
37
+ if (!available)
38
+ return;
39
+ try {
40
+ const { stdout } = await execAsync(`${lxcBin} list --format json`, EXEC_OPTS);
41
+ const instances = JSON.parse(stdout || "[]");
42
+ const only = plugin.config?.instances || [];
43
+ const exclude = plugin.config?.exclude || [];
44
+ const host = hostname();
45
+ const now = Date.now();
46
+ const groups = new Map();
47
+ for (const inst of instances) {
48
+ const name = inst.name;
49
+ if (!name)
50
+ continue;
51
+ if (only.length && !only.includes(name))
52
+ continue;
53
+ if (exclude.includes(name))
54
+ continue;
55
+ const running = String(inst.status || "").toLowerCase() === "running";
56
+ const st = inst.state || {};
57
+ // CPU %: delta of cumulative usage(ns) over wall time -> % of one core
58
+ const usage = Number(st.cpu?.usage) || 0;
59
+ let cpu = 0;
60
+ const p = prevCpu.get(name);
61
+ if (running && p && now > p.t) {
62
+ const dUsage = usage - p.usage;
63
+ const dWallNs = (now - p.t) * 1e6;
64
+ if (dWallNs > 0 && dUsage >= 0)
65
+ cpu = Math.round((dUsage / dWallNs) * 1000) / 10;
66
+ }
67
+ prevCpu.set(name, { usage, t: now });
68
+ const mem = Number(st.memory?.usage) || 0;
69
+ const memLimit = limitToBytes(inst.config?.["limits.memory"] || inst.expanded_config?.["limits.memory"]);
70
+ const memPercent = memLimit ? Math.round((mem / memLimit) * 1000) / 10 : 0;
71
+ const container = {
72
+ id: name,
73
+ name,
74
+ service: "",
75
+ image: inst.config?.["image.description"] || inst.type || "lxc",
76
+ state: String(inst.status || "").toLowerCase(),
77
+ status: inst.status || "",
78
+ running,
79
+ health: "",
80
+ cpu,
81
+ mem,
82
+ memLimit,
83
+ memPercent,
84
+ restarts: 0,
85
+ exitCode: 0,
86
+ };
87
+ // Grouping: a user.daemonitor.group config key, else the host.
88
+ const group = inst.config?.["user.daemonitor.group"] ||
89
+ inst.expanded_config?.["user.daemonitor.group"] || host;
90
+ const arr = groups.get(group) || [];
91
+ arr.push(container);
92
+ groups.set(group, arr);
93
+ }
94
+ for (const [project, containers] of groups) {
95
+ const running = containers.filter((c) => c.running).length;
96
+ const payload = {
97
+ name: project,
98
+ engine: "lxc",
99
+ host,
100
+ project,
101
+ running,
102
+ total: containers.length,
103
+ containers,
104
+ timestamp: now,
105
+ };
106
+ await plugin.send(payload, `lxc-${project}`);
107
+ }
108
+ }
109
+ catch (err) {
110
+ console.error("lxc: refresh error:", err.message);
111
+ }
112
+ };
113
+ const monitorFn = async (plugin) => {
114
+ if (!available)
115
+ return;
116
+ await refreshFn(plugin);
117
+ refreshTimer = setInterval(() => refreshFn(plugin), plugin.config?.refreshInterval || 60000);
118
+ };
119
+ const teardownFn = async () => {
120
+ if (refreshTimer)
121
+ clearInterval(refreshTimer);
122
+ refreshTimer = null;
123
+ };
124
+ return createMonitoringPlugin("lxc", "lxc", "LXC / LXD / Incus container monitoring plugin", setupFn, monitorFn, refreshFn, teardownFn);
125
+ }
126
+ export default createLxcPlugin;
@@ -1,29 +1,43 @@
1
- import { arch, cpus, freemem, hostname, loadavg, networkInterfaces, platform, release, totalmem, type, uptime } from "os";
2
- import { MonitoringPlugin } from "../../lib/MonitoringPlugin.js";
3
- export class OSPlugin extends MonitoringPlugin {
4
- constructor() {
5
- super("os", "OS", "OS Monitoring Plugin");
6
- }
7
- async setup() {
8
- // no setup required
9
- return Promise.resolve();
1
+ import { createMonitoringPlugin } from "../../lib/MonitoringPlugin";
2
+ import { arch, cpus, freemem, hostname, loadavg, networkInterfaces, platform, release, totalmem, type, uptime, } from "os";
3
+ import { readFileSync } from "fs";
4
+ // True available memory: on Linux, os.freemem() returns MemFree, which excludes
5
+ // reclaimable page cache and makes hosts look near-full. MemAvailable is the
6
+ // kernel's estimate of memory obtainable without swapping. Fall back to freemem()
7
+ // on non-Linux (or if /proc is unreadable).
8
+ function availableMem() {
9
+ if (platform() === "linux") {
10
+ try {
11
+ const info = readFileSync("/proc/meminfo", "utf8");
12
+ const m = info.match(/^MemAvailable:\s+(\d+)\s*kB/m);
13
+ if (m)
14
+ return parseInt(m[1], 10) * 1024;
15
+ }
16
+ catch {
17
+ // fall through to freemem()
18
+ }
10
19
  }
11
- async refresh() {
20
+ return freemem();
21
+ }
22
+ // Host OS metrics: cpu (from cpus().times), memory, load, uptime, addresses.
23
+ // Matches the frontend `os` fleetAdapter case.
24
+ export function createOSPlugin() {
25
+ let refreshTimer = null;
26
+ const collect = () => {
12
27
  const interfaces = networkInterfaces();
13
28
  const addresses = {};
14
- for (let [key, value] of Object.entries(interfaces)) {
15
- if (value) {
16
- let found = value.find(port => (port.family === "IPv4") && (port.internal !== true));
17
- if (found) {
18
- let { address, netmask, mac } = found;
19
- addresses[key] = { address, netmask, mac };
20
- }
21
- }
29
+ for (const [key, value] of Object.entries(interfaces)) {
30
+ if (!value)
31
+ continue;
32
+ const found = value.find((p) => p.family === "IPv4" && p.internal !== true);
33
+ if (found)
34
+ addresses[key] = { address: found.address, netmask: found.netmask, mac: found.mac };
22
35
  }
23
- await this.send({
36
+ return {
24
37
  addresses,
25
38
  totalmem: totalmem(),
26
39
  freemem: freemem(),
40
+ available: availableMem(),
27
41
  loadavg: loadavg(),
28
42
  uptime: uptime(),
29
43
  hostname: hostname(),
@@ -31,13 +45,26 @@ export class OSPlugin extends MonitoringPlugin {
31
45
  release: release(),
32
46
  type: type(),
33
47
  arch: arch(),
34
- cpus: cpus()
35
- });
36
- }
37
- async monitor() {
38
- this.refreshTimer = setInterval(this.refresh.bind(this), this.config.refreshInterval || 5000);
39
- }
40
- async teardown() {
41
- clearInterval(this.refreshTimer);
42
- }
48
+ cpus: cpus(),
49
+ };
50
+ };
51
+ const refreshFn = async (plugin) => {
52
+ try {
53
+ await plugin.send({ name: hostname(), ...collect() }, "os");
54
+ }
55
+ catch (err) {
56
+ console.error("os: refresh error:", err.message);
57
+ }
58
+ };
59
+ const monitorFn = async (plugin) => {
60
+ await refreshFn(plugin);
61
+ refreshTimer = setInterval(() => refreshFn(plugin), plugin.config?.refreshInterval || 30000);
62
+ };
63
+ const teardownFn = async () => {
64
+ if (refreshTimer)
65
+ clearInterval(refreshTimer);
66
+ refreshTimer = null;
67
+ };
68
+ return createMonitoringPlugin("os", "os", "Host OS monitoring plugin", async () => { }, monitorFn, refreshFn, teardownFn);
43
69
  }
70
+ export default createOSPlugin;
@@ -1,48 +1,68 @@
1
- import pm2 from "pm2";
2
- import { MonitoringPlugin } from "../../lib/MonitoringPlugin.js";
3
- export class PM2Plugin extends MonitoringPlugin {
4
- constructor() {
5
- super("pm2", "PM2", "PM2 Monitoring Plugin");
6
- }
7
- async setup() {
8
- return new Promise((resolve, reject) => {
9
- try {
10
- pm2.connect(async (err) => {
1
+ import { createMonitoringPlugin } from "../../lib/MonitoringPlugin";
2
+ // Factory function to create PM2 plugin
3
+ export function createPM2Plugin() {
4
+ // Create plugin state
5
+ let pm2Instance = null;
6
+ let refreshTimer = null;
7
+ // Define setup function
8
+ const setupFn = async (plugin) => {
9
+ try {
10
+ // First, check if PM2 is available globally (injected by the client-test)
11
+ if (global.pm2Library) {
12
+ console.log("Using globally provided PM2 instance");
13
+ pm2Instance = global.pm2Library;
14
+ }
15
+ else {
16
+ // Otherwise use dynamic import approach which is ESM friendly
17
+ try {
18
+ const pm2Module = await import('pm2');
19
+ pm2Instance = pm2Module.default || pm2Module;
20
+ // If import fails, try CommonJS require as fallback
21
+ if (!pm2Instance) {
22
+ try {
23
+ // @ts-ignore - Dynamically require pm2
24
+ pm2Instance = require('pm2');
25
+ }
26
+ catch (requireErr) {
27
+ console.error("Failed to require PM2:", requireErr);
28
+ throw new Error(`Neither import nor require could load pm2: ${requireErr.message}`);
29
+ }
30
+ }
31
+ }
32
+ catch (err) {
33
+ console.error("Failed to import PM2:", err);
34
+ throw new Error(`PM2 import failed: ${err.message}`);
35
+ }
36
+ }
37
+ // Return a promise for the PM2 connection
38
+ return new Promise((resolve, reject) => {
39
+ pm2Instance.connect((err) => {
11
40
  if (err) {
12
- console.error(err);
41
+ console.error("PM2 connection failed:", err);
13
42
  reject(err);
14
43
  }
15
44
  else {
16
45
  resolve();
17
46
  }
18
47
  });
19
- }
20
- catch (e) {
21
- console.error("ERROR", e);
22
- reject(e);
23
- }
24
- });
25
- }
26
- async refresh() {
48
+ });
49
+ }
50
+ catch (e) {
51
+ console.error("PM2Plugin setup error:", e);
52
+ throw e;
53
+ }
54
+ };
55
+ // Define refresh function
56
+ const refreshFn = async (plugin) => {
57
+ if (!pm2Instance) {
58
+ console.error("PM2 instance not initialized");
59
+ return;
60
+ }
27
61
  const instanceCounts = {};
28
- pm2.list(async (err, list) => {
62
+ pm2Instance.list(async (err, list) => {
29
63
  if (!err) {
30
64
  for (const p of list) {
31
- let { pid, name, pm_id, monit,
32
- // exit_code,
33
- // prev_restart_delay,
34
- // versioning,
35
- // axm_dynamic,
36
- // axm_actions,
37
- // merge_logs,
38
- // vizion,
39
- // instance_var,
40
- // pmx,
41
- // automation,
42
- // treekill,
43
- // windowsHide,
44
- // kill_retry_time
45
- } = p;
65
+ let { pid, name, pm_id, monit, } = p;
46
66
  if (instanceCounts[name] === undefined) {
47
67
  instanceCounts[name] = 0;
48
68
  }
@@ -69,18 +89,26 @@ export class PM2Plugin extends MonitoringPlugin {
69
89
  status,
70
90
  pm_uptime,
71
91
  };
72
- await this.send(payload, instance_id);
92
+ await plugin.send(payload, instance_id);
73
93
  }
74
94
  }
75
95
  else {
76
96
  console.error(err);
77
97
  }
78
98
  });
79
- }
80
- async monitor() {
81
- this.refreshTimer = setInterval(this.refresh.bind(this), this.config.refreshInterval || 5000);
82
- }
83
- async teardown() {
84
- clearInterval(this.refreshTimer);
85
- }
99
+ };
100
+ // Define monitor function
101
+ const monitorFn = async (plugin) => {
102
+ refreshTimer = setInterval(() => refreshFn(plugin), plugin.config.refreshInterval || 5000);
103
+ };
104
+ // Define teardown function
105
+ const teardownFn = async (plugin) => {
106
+ if (refreshTimer) {
107
+ clearInterval(refreshTimer);
108
+ }
109
+ };
110
+ // Create and return the plugin
111
+ return createMonitoringPlugin("pm2", "PM2", "PM2 Monitoring Plugin", setupFn, monitorFn, refreshFn, teardownFn);
86
112
  }
113
+ // Export a default factory function for plugin loader
114
+ export default createPM2Plugin;
@@ -1,46 +1,72 @@
1
- import { MonitoringPlugin } from "../../lib/MonitoringPlugin.js";
2
- export class WebsitePlugin extends MonitoringPlugin {
3
- constructor() {
4
- super("website", "Website", "Website Monitoring Plugin");
5
- }
6
- async setup() {
7
- // no setup required
8
- return Promise.resolve();
9
- }
10
- async refresh() {
11
- const startTimestamp = Date.now();
12
- const result = await fetch(this.config.url);
13
- const endTimestamp = Date.now();
14
- const text = await result.text();
15
- const status = result.status;
16
- const headers = result.headers;
17
- const ok = result.ok;
18
- const redirected = result.redirected;
19
- const type = result.type;
20
- const url = result.url;
21
- const body = text;
22
- const duration = endTimestamp - startTimestamp;
23
- const expectedStrings = this.config.expectedStrings || [];
24
- const unexpectedStrings = this.config.unexpectedStrings || [];
25
- const hasAllExpectedStrings = expectedStrings.every(expectedString => body.indexOf(expectedString) !== -1);
26
- const hasAnyUnexpectedStrings = unexpectedStrings.some(unexpectedString => body.indexOf(unexpectedString) !== -1);
27
- await this.send({
28
- status,
29
- duration,
30
- headers,
31
- ok,
32
- redirected,
33
- type,
34
- url,
35
- body,
36
- hasAllExpectedStrings,
37
- hasAnyUnexpectedStrings
38
- });
39
- }
40
- async monitor() {
41
- this.refreshTimer = setInterval(this.refresh.bind(this), this.config.refreshInterval || 5000);
42
- }
43
- async teardown() {
44
- clearInterval(this.refreshTimer);
45
- }
1
+ import { createMonitoringPlugin } from "../../lib/MonitoringPlugin";
2
+ // HTTP uptime/content checks. Emits one client_state per endpoint, matching the
3
+ // frontend `web`/`website` fleetAdapter case ({ url, name, ok, status, duration }).
4
+ export function createWebsitePlugin() {
5
+ let refreshTimer = null;
6
+ const check = async (plugin, ep) => {
7
+ const start = Date.now();
8
+ const label = ep.name || ep.url;
9
+ const uid = `web-${(ep.name || ep.url).replace(/[^a-zA-Z0-9_.-]+/g, "-")}`;
10
+ try {
11
+ const ctrl = new AbortController();
12
+ const t = ep.timeout ? setTimeout(() => ctrl.abort(), ep.timeout) : null;
13
+ const res = await fetch(ep.url, { signal: ctrl.signal, redirect: "follow" });
14
+ if (t)
15
+ clearTimeout(t);
16
+ const body = await res.text();
17
+ const duration = Date.now() - start;
18
+ const expected = ep.expectedStrings || [];
19
+ const unexpected = ep.unexpectedStrings || [];
20
+ const hasAllExpected = expected.every((s) => body.indexOf(s) !== -1);
21
+ const hasAnyUnexpected = unexpected.some((s) => body.indexOf(s) !== -1);
22
+ const statusOk = ep.expectedStatusCode ? res.status === ep.expectedStatusCode : res.ok;
23
+ const ok = statusOk && hasAllExpected && !hasAnyUnexpected;
24
+ await plugin.send({
25
+ name: label,
26
+ url: res.url || ep.url,
27
+ status: res.status,
28
+ duration,
29
+ ok,
30
+ redirected: res.redirected,
31
+ hasAllExpectedStrings: hasAllExpected,
32
+ hasAnyUnexpectedStrings: hasAnyUnexpected,
33
+ }, uid);
34
+ }
35
+ catch (err) {
36
+ await plugin.send({
37
+ name: label,
38
+ url: ep.url,
39
+ status: 0,
40
+ duration: Date.now() - start,
41
+ ok: false,
42
+ error: err.message,
43
+ }, uid);
44
+ }
45
+ };
46
+ const endpointsOf = (plugin) => {
47
+ const cfg = plugin.config || {};
48
+ if (Array.isArray(cfg.endpoints))
49
+ return cfg.endpoints;
50
+ if (cfg.url)
51
+ return [{ name: cfg.name, url: cfg.url, expectedStrings: cfg.expectedStrings, unexpectedStrings: cfg.unexpectedStrings }];
52
+ return [];
53
+ };
54
+ const refreshFn = async (plugin) => {
55
+ const eps = endpointsOf(plugin);
56
+ for (const ep of eps) {
57
+ if (ep?.url)
58
+ await check(plugin, ep);
59
+ }
60
+ };
61
+ const monitorFn = async (plugin) => {
62
+ await refreshFn(plugin);
63
+ refreshTimer = setInterval(() => refreshFn(plugin), plugin.config?.refreshInterval || 60000);
64
+ };
65
+ const teardownFn = async () => {
66
+ if (refreshTimer)
67
+ clearInterval(refreshTimer);
68
+ refreshTimer = null;
69
+ };
70
+ return createMonitoringPlugin("web", "web", "Website / HTTP uptime monitoring plugin", async () => { }, monitorFn, refreshFn, teardownFn);
46
71
  }
72
+ export default createWebsitePlugin;
package/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ // Export core plugin utilities
2
+ export { PluginConfigProvider } from "./lib/PluginConfigProvider"
3
+ export { PluginManager } from "./lib/PluginManager"
4
+ export { PluginLoader } from "./lib/PluginLoader"
5
+
6
+ // Only export creation functions, not types
7
+ export { createBasePlugin } from "./lib/BasePlugin"
8
+ export { createMonitoringPlugin } from "./lib/MonitoringPlugin"
9
+
10
+ // Export the PM2 plugin factory function
11
+ export { createPM2Plugin } from "./plugins/monitoring/PM2Plugin"
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@daemonitor/plugins",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "daemonitor plugins",
5
5
  "types": "types/index.d.ts",
6
- "main": "dist/index.js",
6
+ "main": "index.ts",
7
+ "type": "module",
7
8
  "scripts": {
8
9
  "build": "tsc",
9
10
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -12,24 +13,42 @@
12
13
  "dist",
13
14
  "types"
14
15
  ],
15
- "type": "module",
16
+ "exports": {
17
+ ".": "./index.ts",
18
+ "./lib/*": "./lib/*.ts",
19
+ "./plugins/*": "./plugins/*.ts"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "main": "dist/index.js",
24
+ "types": "types/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./types/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./lib/*": "./dist/lib/*.js",
31
+ "./plugins/*": "./dist/plugins/*.js"
32
+ }
33
+ },
16
34
  "author": "",
17
35
  "license": "ISC",
18
36
  "packageManager": "pnpm@8.2.0",
37
+ "devDependencies": {
38
+ "nuxt": "^3.15.4",
39
+ "vue": "^3.5.13",
40
+ "vue-router": "^4.5.0"
41
+ },
19
42
  "dependencies": {
20
43
  "dotenv": "^16.0.3",
21
44
  "ewelink-api": "^3.1.1",
22
45
  "node-machine-id": "^1.1.12",
23
46
  "ohmyfetch": "^0.4.21",
24
- "pm2": "^5.1.0",
25
47
  "string-to-color": "^2.2.2",
26
48
  "ts-node": "^10.9.1",
27
49
  "@daemonitor/common": "^1.0.0",
28
- "tsconfig-paths": "^4.2.0"
29
- },
30
- "exports": {
31
- ".": "./dist/index.js",
32
- "./types": "./dist/types/index.js"
50
+ "tsconfig-paths": "^4.2.0",
51
+ "pm2": "^6.0.5"
33
52
  },
34
53
  "repository": {
35
54
  "type": "git",
@@ -38,6 +57,5 @@
38
57
  "bugs": {
39
58
  "url": "https://github.com/daemonitor/plugins/issues"
40
59
  },
41
-
42
60
  "homepage": "https://github.com/daemonitor/plugins#readme"
43
61
  }