error-ux-cli 1.0.0 → 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/lib/dashboard.mjs CHANGED
@@ -74,7 +74,7 @@ function LiveStatus() {
74
74
  return e(Text, {color: 'yellow'}, statuses[tick % statuses.length]);
75
75
  }
76
76
 
77
- function IntroPanel({done}) {
77
+ function IntroPanel({done, name}) {
78
78
  return e(
79
79
  Box,
80
80
  {borderStyle: 'round', width: 68, paddingX: 1, marginBottom: 1},
@@ -83,8 +83,8 @@ function IntroPanel({done}) {
83
83
  Box,
84
84
  {flexDirection: 'column', justifyContent: 'center'},
85
85
  done
86
- ? e(Text, {bold: true, color: 'green'}, 'Welcome Aniruth')
87
- : e(AnimatedText, {text: 'Welcome Aniruth', speed: 1}),
86
+ ? e(Text, {bold: true, color: 'green'}, `Welcome ${name}`)
87
+ : e(AnimatedText, {text: `Welcome ${name}`, speed: 1}),
88
88
  e(Text, {dimColor: true}, 'terminal cockpit online'),
89
89
  e(LiveStatus),
90
90
  e(LoadingBar, {width: 24})
@@ -115,7 +115,7 @@ function Dashboard({data}) {
115
115
  return e(
116
116
  Box,
117
117
  {flexDirection: 'column'},
118
- e(IntroPanel, {done: stage > 0}),
118
+ e(IntroPanel, {done: stage > 0, name: data.username}),
119
119
  stage >= 1 && e(
120
120
  Box,
121
121
  {borderStyle: 'double', width: 68, marginBottom: 1},
@@ -0,0 +1,284 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { Box, Text, render, useApp, useInput } from 'ink';
3
+ import pg from './pgClient.js';
4
+
5
+ const e = React.createElement;
6
+
7
+ // --- Components ---
8
+
9
+ function Header({ config, level, context, isQueryMode }) {
10
+ let path = 'DB';
11
+ if (level >= 1) path += ` > ${context.db}`;
12
+ if (level >= 2) path += ` > ${context.schema}`;
13
+ if (level >= 3) path += ` > ${context.table}`;
14
+ if (isQueryMode) path += ' > CUSTOM QUERY';
15
+
16
+ return e(
17
+ Box,
18
+ { borderStyle: 'double', borderColor: 'magenta', paddingX: 1, marginBottom: 1 },
19
+ e(Text, { color: 'cyan', bold: true }, ' \u26A1 PG GOD-MODE EXPLORER '),
20
+ e(Text, { color: 'white' }, ` [${path}] `)
21
+ );
22
+ }
23
+
24
+ function Pane({ title, width, children, active, color = 'cyan' }) {
25
+ return e(
26
+ Box,
27
+ {
28
+ flexDirection: 'column',
29
+ borderStyle: 'double',
30
+ borderColor: active ? 'magenta' : 'gray',
31
+ width,
32
+ height: 20,
33
+ paddingX: 1,
34
+ marginRight: 1
35
+ },
36
+ e(Text, { bold: true, color: active ? 'magenta' : color }, title),
37
+ e(Box, { flexDirection: 'column', marginTop: 1 }, children)
38
+ );
39
+ }
40
+
41
+ function ColumnRow({ column, selected }) {
42
+ const primaryIcon = column.is_primary ? '🔑 ' : ' ';
43
+ return e(
44
+ Box,
45
+ null,
46
+ e(Text, { color: selected ? 'magenta' : 'white' }, primaryIcon),
47
+ e(Box, { width: 15 }, e(Text, { color: selected ? 'magenta' : 'white' }, column.column_name)),
48
+ e(Box, { width: 12 }, e(Text, { dimColor: true }, column.data_type)),
49
+ e(Text, { dimColor: true }, column.is_nullable === 'YES' ? 'null' : 'not null')
50
+ );
51
+ }
52
+
53
+ // --- Main Explorer ---
54
+
55
+ function Explorer({ credentials }) {
56
+ const { exit } = useApp();
57
+ const [level, setLevel] = useState(0); // 0: DB, 1: Schema, 2: Table
58
+ const [context, setContext] = useState({ db: '', schema: '', table: '' });
59
+ const [items, setItems] = useState([]);
60
+ const [selectedIndex, setSelectedIndex] = useState(0);
61
+ const [search, setSearch] = useState('');
62
+ const [metadata, setMetadata] = useState([]);
63
+ const [preview, setPreview] = useState([]);
64
+ const [error, setError] = useState(null);
65
+ const [isQueryMode, setIsQueryMode] = useState(false);
66
+ const [customQuery, setCustomQuery] = useState('');
67
+ const [queryResult, setQueryResult] = useState(null);
68
+
69
+ // Filtered items based on search
70
+ const filteredItems = useMemo(() => {
71
+ if (!search) return items;
72
+ return items.filter(item => item.toLowerCase().includes(search.toLowerCase()));
73
+ }, [items, search]);
74
+
75
+ // Fetch Level Data
76
+ useEffect(() => {
77
+ if (isQueryMode) return;
78
+ const fetchData = async () => {
79
+ try {
80
+ setError(null);
81
+ setSearch('');
82
+ setSelectedIndex(0);
83
+
84
+ if (level === 0) {
85
+ const dbs = await pg.listDatabases(credentials);
86
+ setItems(dbs);
87
+ } else if (level === 1) {
88
+ const schemas = await pg.listSchemas({ ...credentials, database: context.db });
89
+ setItems(schemas);
90
+ } else if (level === 2) {
91
+ const tables = await pg.listTables({ ...credentials, database: context.db }, context.schema);
92
+ setItems(tables);
93
+ }
94
+ } catch (err) {
95
+ setError(err.message);
96
+ }
97
+ };
98
+ fetchData();
99
+ }, [level, context.db, context.schema, credentials, isQueryMode]);
100
+
101
+ // Fetch Table Metadata/Preview
102
+ useEffect(() => {
103
+ if (isQueryMode) return;
104
+ if (level === 2 && filteredItems[selectedIndex]) {
105
+ const tableName = filteredItems[selectedIndex];
106
+ const fetchTableDetails = async () => {
107
+ try {
108
+ const meta = await pg.getTableMetadata({ ...credentials, database: context.db }, context.schema, tableName);
109
+ setMetadata(meta);
110
+ const data = await pg.getTablePreview({ ...credentials, database: context.db }, context.schema, tableName);
111
+ setPreview(data);
112
+ } catch (err) {
113
+ setMetadata([]);
114
+ setPreview([]);
115
+ }
116
+ };
117
+ fetchTableDetails();
118
+ } else {
119
+ setMetadata([]);
120
+ setPreview([]);
121
+ }
122
+ }, [level, selectedIndex, filteredItems, context.db, context.schema, credentials, isQueryMode]);
123
+
124
+ // Keyboard Input
125
+ useInput((input, key) => {
126
+ if (isQueryMode) {
127
+ if (key.escape) {
128
+ setIsQueryMode(false);
129
+ setCustomQuery('');
130
+ return;
131
+ }
132
+ if (key.return) {
133
+ const runQuery = async () => {
134
+ try {
135
+ const res = await pg.manualQuery({ ...credentials, database: context.db || 'postgres' }, customQuery);
136
+ setQueryResult(res);
137
+ setError(null);
138
+ } catch (err) {
139
+ setError(err.message);
140
+ setQueryResult(null);
141
+ }
142
+ };
143
+ runQuery();
144
+ return;
145
+ }
146
+ if (key.backspace) {
147
+ setCustomQuery(prev => prev.slice(0, -1));
148
+ return;
149
+ }
150
+ if (input) {
151
+ setCustomQuery(prev => prev + input);
152
+ return;
153
+ }
154
+ return;
155
+ }
156
+
157
+ if (key.escape) {
158
+ if (level > 0) setLevel(level - 1);
159
+ else exit();
160
+ }
161
+
162
+ if (key.upArrow) setSelectedIndex(prev => Math.max(0, prev - 1));
163
+ if (key.downArrow) setSelectedIndex(prev => Math.min(filteredItems.length - 1, prev + 1));
164
+
165
+ if (key.return) {
166
+ const selected = filteredItems[selectedIndex];
167
+ if (!selected) return;
168
+
169
+ if (level === 0) {
170
+ setContext(prev => ({ ...prev, db: selected }));
171
+ setLevel(1);
172
+ } else if (level === 1) {
173
+ setContext(prev => ({ ...prev, schema: selected }));
174
+ setLevel(2);
175
+ }
176
+ }
177
+
178
+ // Toggle Query Mode
179
+ if (input.toLowerCase() === 's') {
180
+ setIsQueryMode(true);
181
+ return;
182
+ }
183
+
184
+ if (!key.ctrl && !key.meta && input.length === 1 && /^[a-z0-9_]$/i.test(input)) {
185
+ setSearch(prev => prev + input);
186
+ setSelectedIndex(0);
187
+ }
188
+ if (key.backspace) {
189
+ setSearch(prev => prev.slice(0, -1));
190
+ setSelectedIndex(0);
191
+ }
192
+ });
193
+
194
+ const listTitle = level === 0 ? 'Databases' : level === 1 ? 'Schemas' : 'Tables';
195
+
196
+ return e(
197
+ Box,
198
+ { flexDirection: 'column' },
199
+ e(Header, { config: credentials, level, context, isQueryMode }),
200
+
201
+ e(
202
+ Box,
203
+ { flexDirection: 'row' },
204
+ // Navigator or Query Box
205
+ isQueryMode ? e(
206
+ Pane,
207
+ { title: '[ SQL CONSOLE ]', width: 80, active: true },
208
+ e(Text, { color: 'yellow' }, 'Query: ' + customQuery + '_'),
209
+ e(Text, { dimColor: true, marginTop: 1 }, 'Press [Enter] to Execute | [Esc] to Exit'),
210
+ queryResult && e(
211
+ Box,
212
+ { flexDirection: 'column', marginTop: 1 },
213
+ e(Text, { color: 'cyan' }, 'Result (First 5 Rows):'),
214
+ e(Text, null, JSON.stringify(queryResult.slice(0, 5), null, 2).slice(0, 500))
215
+ )
216
+ ) : e(
217
+ React.Fragment,
218
+ null,
219
+ e(
220
+ Pane,
221
+ { title: `[ ${listTitle} ]`, width: 35, active: true },
222
+ search && e(Text, { color: 'yellow' }, `Search: ${search}`),
223
+ (() => {
224
+ const visibleCount = 15;
225
+ const startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), Math.max(0, filteredItems.length - visibleCount)));
226
+ const visibleItems = filteredItems.slice(startIndex, startIndex + visibleCount);
227
+
228
+ return [
229
+ startIndex > 0 && e(Text, { dimColor: true, alignSelf: 'center' }, '^ more ^'),
230
+ ...visibleItems.map((item, i) => {
231
+ const actualIndex = startIndex + i;
232
+ const isSelected = actualIndex === selectedIndex;
233
+ return e(Text, { key: item, color: isSelected ? 'magenta' : 'white', wrap: 'truncate-end' },
234
+ isSelected ? `\u25B6 ${item}` : ` ${item}`);
235
+ }),
236
+ (startIndex + visibleCount < filteredItems.length) && e(Text, { dimColor: true, alignSelf: 'center' }, 'v more v')
237
+ ];
238
+ })(),
239
+ filteredItems.length === 0 && e(Text, { dimColor: true }, ' No entries ')
240
+ ),
241
+
242
+ // Schema Detail
243
+ level === 2 && e(
244
+ Pane,
245
+ { title: '[ Table Schema ]', width: 50, active: false, color: 'cyan' },
246
+ ...metadata.slice(0, 15).map((col, i) => e(ColumnRow, { key: i, column: col }))
247
+ ),
248
+
249
+ // Help Message
250
+ level < 2 && e(
251
+ Pane,
252
+ { title: '[ Help ]', width: 50, active: false, color: 'gray' },
253
+ e(Text, null, 'Use \u2191\u2193 to navigate'),
254
+ e(Text, null, 'Press [Enter] to select'),
255
+ e(Text, null, 'Type to filter entities'),
256
+ e(Text, { color: 'cyan' }, 'Press [s] for Custom SQL'),
257
+ e(Text, null, 'Press [Esc] to go back')
258
+ )
259
+ )
260
+ ),
261
+
262
+ // Data Preview
263
+ !isQueryMode && level === 2 && e(
264
+ Box,
265
+ {
266
+ borderStyle: 'double',
267
+ borderColor: 'gray',
268
+ width: 86,
269
+ paddingX: 1,
270
+ marginTop: 1,
271
+ flexDirection: 'column'
272
+ },
273
+ e(Text, { bold: true, color: 'yellow' }, '[ Data Preview (Top 5 Rows) ]'),
274
+ preview.length > 0 ? e(Text, null, JSON.stringify(preview[0]).slice(0, 80) + '...') : e(Text, { dimColor: true }, 'No data or table empty')
275
+ ),
276
+
277
+ error && e(Text, { color: 'red', bold: true }, `\n\u26A0 DATABASE ERROR: ${error}`)
278
+ );
279
+ }
280
+
281
+ export async function renderExplorer(credentials) {
282
+ const instance = render(e(Explorer, { credentials }));
283
+ await instance.waitUntilExit();
284
+ }
package/lib/git.js CHANGED
@@ -1,6 +1,79 @@
1
1
  const { execSync } = require('child_process');
2
2
  const fs = require('fs/promises');
3
3
  const path = require('path');
4
+ const https = require('https');
5
+
6
+ /**
7
+ * Syncs vault data with a private GitHub Gist.
8
+ * @param {string} token GitHub PAT (standard user PAT)
9
+ * @param {string} gistId The ID of the Secret Gist
10
+ * @param {string} data Encrypted JSON blob (required for PUSH)
11
+ * @param {string} action 'push' or 'pull'
12
+ * @returns {Promise<string|null>}
13
+ */
14
+ async function gistSync(token, gistId, data, action = 'pull') {
15
+ return new Promise((resolve, reject) => {
16
+ const options = {
17
+ hostname: 'api.github.com',
18
+ path: `/gists/${gistId}`,
19
+ method: action === 'push' ? 'PATCH' : 'GET',
20
+ headers: {
21
+ 'Authorization': `Bearer ${token}`,
22
+ 'User-Agent': 'error-ux-cli',
23
+ 'Accept': 'application/vnd.github+json',
24
+ 'X-GitHub-Api-Version': '2022-11-28'
25
+ }
26
+ };
27
+
28
+ if (action === 'push') {
29
+ options.headers['Content-Type'] = 'application/json';
30
+ }
31
+
32
+ const req = https.request(options, (res) => {
33
+ let body = '';
34
+ res.on('data', (d) => body += d);
35
+ res.on('end', () => {
36
+ if (res.statusCode >= 400) {
37
+ return reject(new Error(`GitHub Error: ${res.statusCode} - ${body}`));
38
+ }
39
+
40
+ try {
41
+ const result = JSON.parse(body);
42
+ if (action === 'pull') {
43
+ // We assume the data is in a file named 'vault.json' or similar.
44
+ // We'll just take the first file's content.
45
+ const files = result.files || {};
46
+ const fileName = Object.keys(files)[0];
47
+ if (fileName) {
48
+ resolve(files[fileName].content);
49
+ } else {
50
+ resolve(null);
51
+ }
52
+ } else {
53
+ resolve(body);
54
+ }
55
+ } catch (e) {
56
+ reject(e);
57
+ }
58
+ });
59
+ });
60
+
61
+ req.on('error', (e) => reject(e));
62
+
63
+ if (action === 'push' && data) {
64
+ const payload = JSON.stringify({
65
+ files: {
66
+ 'vault.json': {
67
+ content: data
68
+ }
69
+ }
70
+ });
71
+ req.write(payload);
72
+ }
73
+
74
+ req.end();
75
+ });
76
+ }
4
77
 
5
78
  function runGit(command) {
6
79
  try {
@@ -90,8 +163,17 @@ async function getNextBranchVersion(name) {
90
163
  async function branchExists(name) {
91
164
  const branch = sanitizeBranchName(name);
92
165
  try {
166
+ // Check local branches and current remote branches
93
167
  const output = runGit(`git branch -a --list ${quoteShellArg(branch)} ${quoteShellArg(`remotes/origin/${branch}`)}`);
94
- return Boolean(String(output || '').trim());
168
+ if (Boolean(String(output || '').trim())) return true;
169
+
170
+ // Force a remote check if possible
171
+ try {
172
+ const remoteOutput = runGit(`git ls-remote --heads origin ${quoteShellArg(branch)}`);
173
+ return Boolean(String(remoteOutput || '').trim());
174
+ } catch (e) {
175
+ return false;
176
+ }
95
177
  } catch (error) {
96
178
  return false;
97
179
  }
@@ -190,8 +272,7 @@ async function getPushContext(args, savedRepos, rl) {
190
272
  }
191
273
 
192
274
  if (!targetRepo || !targetRepo.url) {
193
- console.error('Error: No target repository configured.');
194
- return null;
275
+ targetRepo = { name: 'origin', url: null };
195
276
  }
196
277
 
197
278
  return {
@@ -210,7 +291,12 @@ async function automatedPush(targetRepo, message, branchBase) {
210
291
  if (!currentRemote) {
211
292
  await setRemoteUrl(targetRepo.url, false);
212
293
  } else if (currentRemote !== targetRepo.url) {
213
- await setRemoteUrl(targetRepo.url, true);
294
+ // If it exists but differs, update it
295
+ try {
296
+ await setRemoteUrl(targetRepo.url, true);
297
+ } catch (e) {
298
+ // If it somehow fails, try to force it or just ignore if it's already there
299
+ }
214
300
  }
215
301
 
216
302
  const branch = sanitizeBranchName(branchBase);
@@ -257,6 +343,7 @@ module.exports = {
257
343
  getNextBranchVersion,
258
344
  getRemoteUrl,
259
345
  getRepoRoot,
346
+ gistSync,
260
347
  initRepo,
261
348
  isGitRepo,
262
349
  runGit,
@@ -0,0 +1,102 @@
1
+ const { Pool } = require('pg');
2
+
3
+ /**
4
+ * Creates a connection pool for a specific database.
5
+ */
6
+ function createPool(config) {
7
+ return new Pool({
8
+ host: config.host || 'localhost',
9
+ port: config.port || 5432,
10
+ user: config.user,
11
+ password: config.password,
12
+ database: config.database,
13
+ ssl: config.useSsl ? { rejectUnauthorized: false } : false,
14
+ connectionTimeoutMillis: 5000
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Runs a query and handles the pool lifecycle for one-off tasks.
20
+ */
21
+ async function query(config, sql, params = []) {
22
+ const pool = createPool(config);
23
+ try {
24
+ const res = await pool.query(sql, params);
25
+ return res.rows;
26
+ } finally {
27
+ await pool.end();
28
+ }
29
+ }
30
+
31
+ async function testConnection(config) {
32
+ return query(config, 'SELECT 1');
33
+ }
34
+
35
+ async function listDatabases(config) {
36
+ const rows = await query(config, 'SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname ASC');
37
+ return rows.map(r => r.datname);
38
+ }
39
+
40
+ async function listSchemas(config) {
41
+ const rows = await query(config, "SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog') ORDER BY schema_name ASC");
42
+ return rows.map(r => r.schema_name);
43
+ }
44
+
45
+ async function listTables(config, schema = 'public') {
46
+ const rows = await query(config, "SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE' ORDER BY table_name ASC", [schema]);
47
+ return rows.map(r => r.table_name);
48
+ }
49
+
50
+ async function getTableMetadata(config, schema, table) {
51
+ const sql = `
52
+ SELECT
53
+ column_name,
54
+ data_type,
55
+ is_nullable,
56
+ column_default,
57
+ (
58
+ SELECT 'YES'
59
+ FROM information_schema.key_column_usage kcu
60
+ JOIN information_schema.table_constraints tc ON kcu.constraint_name = tc.constraint_name
61
+ WHERE kcu.table_name = c.table_name
62
+ AND kcu.column_name = c.column_name
63
+ AND tc.constraint_type = 'PRIMARY KEY'
64
+ ) as is_primary
65
+ FROM information_schema.columns c
66
+ WHERE table_schema = $1 AND table_name = $2
67
+ ORDER BY ordinal_position ASC
68
+ `;
69
+ return query(config, sql, [schema, table]);
70
+ }
71
+
72
+ async function getTablePreview(config, schema, table, limit = 5) {
73
+ // Note: We use double quotes for schema/table names to handle mixed case/special chars safely
74
+ const sql = `SELECT * FROM "${schema}"."${table}" LIMIT ${limit}`;
75
+ return query(config, sql);
76
+ }
77
+
78
+ async function searchColumns(config, columnName) {
79
+ const sql = `
80
+ SELECT table_schema, table_name
81
+ FROM information_schema.columns
82
+ WHERE column_name ILIKE $1
83
+ AND table_schema NOT IN ('information_schema', 'pg_catalog')
84
+ ORDER BY table_schema, table_name
85
+ `;
86
+ return query(config, `%${columnName}%`);
87
+ }
88
+
89
+ async function manualQuery(config, sql) {
90
+ return query(config, sql);
91
+ }
92
+
93
+ module.exports = {
94
+ testConnection,
95
+ listDatabases,
96
+ listSchemas,
97
+ listTables,
98
+ getTableMetadata,
99
+ getTablePreview,
100
+ searchColumns,
101
+ manualQuery
102
+ };
package/lib/plugins.js ADDED
@@ -0,0 +1,94 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const PLUGIN_DIR = path.join(os.homedir(), '.error-ux', 'plugins');
6
+
7
+ function ensurePluginDir() {
8
+ if (!fs.existsSync(PLUGIN_DIR)) {
9
+ fs.mkdirSync(PLUGIN_DIR, { recursive: true });
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Scans the plugin directory and loads valid JS plugins.
15
+ * Only called for Admin users.
16
+ */
17
+ function loadPlugins() {
18
+ ensurePluginDir();
19
+ const plugins = {};
20
+ const files = fs.readdirSync(PLUGIN_DIR);
21
+
22
+ for (const file of files) {
23
+ if (file.endsWith('.js')) {
24
+ try {
25
+ const pluginPath = path.join(PLUGIN_DIR, file);
26
+ // Clear cache to allow reloading if needed
27
+ delete require.cache[require.resolve(pluginPath)];
28
+ const plugin = require(pluginPath);
29
+
30
+ if (plugin.name && typeof plugin.run === 'function') {
31
+ plugins[plugin.name.toLowerCase()] = {
32
+ name: plugin.name,
33
+ description: plugin.description || 'No description provided.',
34
+ run: plugin.run
35
+ };
36
+ }
37
+ } catch (err) {
38
+ console.error(`Failed to load plugin "${file}": ${err.message}`);
39
+ }
40
+ }
41
+ }
42
+ return plugins;
43
+ }
44
+
45
+ /**
46
+ * Returns an object containing the source code of all .js plugins.
47
+ * Used for Cloud Sync.
48
+ */
49
+ function getAllPluginSource() {
50
+ ensurePluginDir();
51
+ const source = {};
52
+ const files = fs.readdirSync(PLUGIN_DIR);
53
+ for (const file of files) {
54
+ if (file.endsWith('.js')) {
55
+ const content = fs.readFileSync(path.join(PLUGIN_DIR, file), 'utf8');
56
+ source[file] = content;
57
+ }
58
+ }
59
+ return source;
60
+ }
61
+
62
+ /**
63
+ * Saves or updates a plugin file.
64
+ */
65
+ function savePlugin(filename, content) {
66
+ ensurePluginDir();
67
+ const target = path.join(PLUGIN_DIR, filename);
68
+ fs.writeFileSync(target, content, 'utf8');
69
+ }
70
+
71
+ /**
72
+ * Deletes a plugin by name.
73
+ */
74
+ function deletePlugin(name) {
75
+ const filename = name.endsWith('.js') ? name : `${name}.js`;
76
+ const target = path.join(PLUGIN_DIR, filename);
77
+ if (fs.existsSync(target)) {
78
+ fs.unlinkSync(target);
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+
84
+ function getPluginDir() {
85
+ return PLUGIN_DIR;
86
+ }
87
+
88
+ module.exports = {
89
+ loadPlugins,
90
+ getPluginDir,
91
+ getAllPluginSource,
92
+ savePlugin,
93
+ deletePlugin
94
+ };