@moxxy/cli 0.0.12 → 0.1.1
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/README.md +278 -112
- package/bin/moxxy +10 -0
- package/package.json +36 -53
- package/src/api-client.js +286 -0
- package/src/cli.js +349 -0
- package/src/commands/agent.js +413 -0
- package/src/commands/auth.js +326 -0
- package/src/commands/channel.js +285 -0
- package/src/commands/doctor.js +261 -0
- package/src/commands/events.js +80 -0
- package/src/commands/gateway.js +428 -0
- package/src/commands/heartbeat.js +145 -0
- package/src/commands/init.js +954 -0
- package/src/commands/mcp.js +278 -0
- package/src/commands/plugin.js +583 -0
- package/src/commands/provider.js +1934 -0
- package/src/commands/settings.js +224 -0
- package/src/commands/skill.js +125 -0
- package/src/commands/template.js +237 -0
- package/src/commands/uninstall.js +196 -0
- package/src/commands/update.js +406 -0
- package/src/commands/vault.js +219 -0
- package/src/help.js +392 -0
- package/src/lib/plugin-registry.js +98 -0
- package/src/platform.js +40 -0
- package/src/sse-client.js +79 -0
- package/src/tui/action-wizards.js +130 -0
- package/src/tui/app.jsx +859 -0
- package/src/tui/components/action-picker.jsx +86 -0
- package/src/tui/components/chat-panel.jsx +120 -0
- package/src/tui/components/footer.jsx +13 -0
- package/src/tui/components/header.jsx +45 -0
- package/src/tui/components/input-area.jsx +384 -0
- package/src/tui/components/messages/ask-message.jsx +13 -0
- package/src/tui/components/messages/assistant-message.jsx +165 -0
- package/src/tui/components/messages/channel-message.jsx +18 -0
- package/src/tui/components/messages/event-message.jsx +22 -0
- package/src/tui/components/messages/hive-status.jsx +34 -0
- package/src/tui/components/messages/skill-message.jsx +31 -0
- package/src/tui/components/messages/system-message.jsx +12 -0
- package/src/tui/components/messages/thinking.jsx +25 -0
- package/src/tui/components/messages/tool-group.jsx +62 -0
- package/src/tui/components/messages/tool-message.jsx +66 -0
- package/src/tui/components/messages/user-message.jsx +12 -0
- package/src/tui/components/model-picker.jsx +138 -0
- package/src/tui/components/multiline-input.jsx +72 -0
- package/src/tui/events-handler.js +730 -0
- package/src/tui/helpers.js +59 -0
- package/src/tui/hooks/use-command-handler.js +451 -0
- package/src/tui/index.jsx +55 -0
- package/src/tui/input-utils.js +26 -0
- package/src/tui/markdown-renderer.js +66 -0
- package/src/tui/mcp-wizard.js +136 -0
- package/src/tui/model-picker.js +174 -0
- package/src/tui/slash-commands.js +26 -0
- package/src/tui/store.js +12 -0
- package/src/tui/theme.js +17 -0
- package/src/ui.js +109 -0
- package/bin/moxxy.js +0 -2
- package/dist/chunk-23LZYKQ6.mjs +0 -1131
- package/dist/chunk-2FZEA3NG.mjs +0 -457
- package/dist/chunk-3KDPLS22.mjs +0 -1131
- package/dist/chunk-3QRJTRBT.mjs +0 -1102
- package/dist/chunk-6DZX6EAA.mjs +0 -37
- package/dist/chunk-A4WRDUNY.mjs +0 -1242
- package/dist/chunk-C46NSEKG.mjs +0 -211
- package/dist/chunk-CAUXONEF.mjs +0 -1131
- package/dist/chunk-CPL5V56X.mjs +0 -1131
- package/dist/chunk-CTBVTTBG.mjs +0 -440
- package/dist/chunk-FHHLXTEZ.mjs +0 -1121
- package/dist/chunk-FXY3GPVA.mjs +0 -1126
- package/dist/chunk-GSNMMI3H.mjs +0 -530
- package/dist/chunk-HHOAOGUS.mjs +0 -1242
- package/dist/chunk-ITBO7BKI.mjs +0 -1243
- package/dist/chunk-J33O35WX.mjs +0 -532
- package/dist/chunk-N5JTPB6U.mjs +0 -820
- package/dist/chunk-NGVL4Q5C.mjs +0 -1102
- package/dist/chunk-Q2OCMNYI.mjs +0 -1131
- package/dist/chunk-QDVRLN6D.mjs +0 -1121
- package/dist/chunk-QO2JONHP.mjs +0 -1131
- package/dist/chunk-RVAPILHA.mjs +0 -1242
- package/dist/chunk-S7YBOV7E.mjs +0 -1131
- package/dist/chunk-SHIG6Y5L.mjs +0 -1074
- package/dist/chunk-SOFST2PV.mjs +0 -1242
- package/dist/chunk-SUNUYS6G.mjs +0 -1243
- package/dist/chunk-TMZWETMH.mjs +0 -1242
- package/dist/chunk-TYD7NMMI.mjs +0 -581
- package/dist/chunk-TYQ3YS42.mjs +0 -1068
- package/dist/chunk-UALWCJ7F.mjs +0 -1131
- package/dist/chunk-UQZKODNW.mjs +0 -1124
- package/dist/chunk-USC6R2ON.mjs +0 -1242
- package/dist/chunk-W32EQCVC.mjs +0 -823
- package/dist/chunk-WMB5ENMC.mjs +0 -1242
- package/dist/chunk-WNHA5JAP.mjs +0 -1242
- package/dist/cli-2AIWTL6F.mjs +0 -8
- package/dist/cli-2QKJ5UUL.mjs +0 -8
- package/dist/cli-4RIS6DQX.mjs +0 -8
- package/dist/cli-5RH4VBBL.mjs +0 -7
- package/dist/cli-7MK4YGOP.mjs +0 -7
- package/dist/cli-B4KH6MZI.mjs +0 -8
- package/dist/cli-CGO2LZ6Z.mjs +0 -8
- package/dist/cli-CVP26EL2.mjs +0 -8
- package/dist/cli-DDRVVNAV.mjs +0 -8
- package/dist/cli-E7U56QVQ.mjs +0 -8
- package/dist/cli-EQNRMLL3.mjs +0 -8
- package/dist/cli-F5RUHHH4.mjs +0 -8
- package/dist/cli-LX6FFSEF.mjs +0 -8
- package/dist/cli-LY74GWKR.mjs +0 -6
- package/dist/cli-MAT3ZJHI.mjs +0 -8
- package/dist/cli-NJXXTQYF.mjs +0 -8
- package/dist/cli-O4ZGFAZG.mjs +0 -8
- package/dist/cli-ORVLI3UQ.mjs +0 -8
- package/dist/cli-PV43ZVKA.mjs +0 -8
- package/dist/cli-REVD6ISM.mjs +0 -8
- package/dist/cli-TBX76KQX.mjs +0 -8
- package/dist/cli-THCGF7SQ.mjs +0 -8
- package/dist/cli-TLX5ENVM.mjs +0 -8
- package/dist/cli-TMNI5ZYE.mjs +0 -8
- package/dist/cli-TNJHCBQA.mjs +0 -6
- package/dist/cli-TUX22CZP.mjs +0 -8
- package/dist/cli-XJVH7EEP.mjs +0 -8
- package/dist/cli-XXOW4VXJ.mjs +0 -8
- package/dist/cli-XZ5RESNB.mjs +0 -6
- package/dist/cli-YCBYZ76Q.mjs +0 -8
- package/dist/cli-ZLMQCU7X.mjs +0 -8
- package/dist/dist-2VGKJRBH.mjs +0 -6820
- package/dist/dist-37BNX4QG.mjs +0 -7081
- package/dist/dist-7LTHRYKA.mjs +0 -11569
- package/dist/dist-7XJPQW5C.mjs +0 -6950
- package/dist/dist-AYMVOW7T.mjs +0 -7123
- package/dist/dist-BHUWCDRS.mjs +0 -7132
- package/dist/dist-FAXRJMEN.mjs +0 -6812
- package/dist/dist-HQGANM3P.mjs +0 -6976
- package/dist/dist-KATLOZQV.mjs +0 -7054
- package/dist/dist-KLSB6YHV.mjs +0 -6964
- package/dist/dist-LKIOZQ42.mjs +0 -17
- package/dist/dist-UYA4RJUH.mjs +0 -2792
- package/dist/dist-ZYHCBILM.mjs +0 -6993
- package/dist/index.d.mts +0 -23
- package/dist/index.d.ts +0 -23
- package/dist/index.js +0 -25531
- package/dist/index.mjs +0 -18
- package/dist/src-APP5P3UD.mjs +0 -1386
- package/dist/src-D5HMDDVE.mjs +0 -1324
- package/dist/src-EK3WD4AU.mjs +0 -1327
- package/dist/src-LSZFLMFN.mjs +0 -1400
- package/dist/src-T77DFTFP.mjs +0 -1407
- package/dist/src-WIOCZRAC.mjs +0 -1397
- package/dist/src-YK6CHCMW.mjs +0 -1400
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { p, handleCancel, withSpinner, isInteractive, showResult } from '../ui.js';
|
|
2
|
+
import {
|
|
3
|
+
pluginPaths,
|
|
4
|
+
readRegistry,
|
|
5
|
+
writeRegistry,
|
|
6
|
+
ensurePluginsDir,
|
|
7
|
+
readPluginMeta,
|
|
8
|
+
validatePluginMeta,
|
|
9
|
+
isProcessAlive,
|
|
10
|
+
sanitizeLogFileName,
|
|
11
|
+
buildPluginEnv,
|
|
12
|
+
BUILTIN_PLUGINS,
|
|
13
|
+
} from '../lib/plugin-registry.js';
|
|
14
|
+
import { execSync, spawn } from 'node:child_process';
|
|
15
|
+
import { existsSync, openSync, unlinkSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { platform } from 'node:os';
|
|
18
|
+
import { showHelp } from '../help.js';
|
|
19
|
+
|
|
20
|
+
// ── Lifecycle functions ──
|
|
21
|
+
|
|
22
|
+
async function installPlugin(pluginName) {
|
|
23
|
+
ensurePluginsDir();
|
|
24
|
+
const { pluginsDir } = pluginPaths();
|
|
25
|
+
|
|
26
|
+
const registry = readRegistry();
|
|
27
|
+
if (registry.plugins[pluginName]) {
|
|
28
|
+
p.log.warn(`${pluginName} is already installed.`);
|
|
29
|
+
return registry.plugins[pluginName];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await withSpinner(`Installing ${pluginName}...`, async () => {
|
|
33
|
+
execSync(`npm install ${pluginName}`, { cwd: pluginsDir, stdio: 'pipe', timeout: 120_000 });
|
|
34
|
+
}, `${pluginName} downloaded.`);
|
|
35
|
+
|
|
36
|
+
const meta = readPluginMeta(pluginName);
|
|
37
|
+
const { valid, errors } = validatePluginMeta(meta);
|
|
38
|
+
if (!valid) {
|
|
39
|
+
p.log.error(`Invalid plugin: ${errors.join(', ')}`);
|
|
40
|
+
p.log.info('Removing invalid package...');
|
|
41
|
+
try { execSync(`npm uninstall ${pluginName}`, { cwd: pluginsDir, stdio: 'pipe' }); } catch { /* best effort */ }
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Run plugin:install hook if present
|
|
46
|
+
if (meta.scripts?.['plugin:install']) {
|
|
47
|
+
const pluginDir = join(pluginsDir, 'node_modules', pluginName);
|
|
48
|
+
await withSpinner('Running post-install hook...', async () => {
|
|
49
|
+
execSync('npm run plugin:install', { cwd: pluginDir, stdio: 'pipe', timeout: 120_000 });
|
|
50
|
+
}, 'Post-install complete.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const builtin = BUILTIN_PLUGINS.find(b => b.name === pluginName);
|
|
54
|
+
const port = meta.moxxy?.port || builtin?.defaultPort || null;
|
|
55
|
+
|
|
56
|
+
const entry = {
|
|
57
|
+
name: pluginName,
|
|
58
|
+
version: meta.version || '0.0.0',
|
|
59
|
+
status: 'installed',
|
|
60
|
+
enabled: false,
|
|
61
|
+
builtin: !!builtin,
|
|
62
|
+
pid: null,
|
|
63
|
+
port,
|
|
64
|
+
installedAt: new Date().toISOString(),
|
|
65
|
+
startedAt: null,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
registry.plugins[pluginName] = entry;
|
|
69
|
+
writeRegistry(registry);
|
|
70
|
+
|
|
71
|
+
showResult('Plugin installed', {
|
|
72
|
+
Name: meta.moxxy?.displayName || pluginName,
|
|
73
|
+
Version: entry.version,
|
|
74
|
+
Port: port || 'none',
|
|
75
|
+
Status: entry.status,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return entry;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function startPlugin(pluginName) {
|
|
82
|
+
const registry = readRegistry();
|
|
83
|
+
const entry = registry.plugins[pluginName];
|
|
84
|
+
if (!entry) {
|
|
85
|
+
p.log.error(`${pluginName} is not installed. Install it first.`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (entry.status === 'running' && entry.pid && isProcessAlive(entry.pid)) {
|
|
90
|
+
p.log.warn(`${pluginName} is already running (PID ${entry.pid}).`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const meta = readPluginMeta(pluginName);
|
|
95
|
+
if (!meta) {
|
|
96
|
+
p.log.error(`Cannot read plugin metadata for ${pluginName}.`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { pluginsDir, logsDir } = pluginPaths();
|
|
101
|
+
const pluginDir = join(pluginsDir, 'node_modules', pluginName);
|
|
102
|
+
const logFile = join(logsDir, sanitizeLogFileName(pluginName));
|
|
103
|
+
const port = entry.port || meta.moxxy?.port || null;
|
|
104
|
+
const env = buildPluginEnv(pluginName, port);
|
|
105
|
+
|
|
106
|
+
const logFd = openSync(logFile, 'a');
|
|
107
|
+
|
|
108
|
+
const child = spawn('npm', ['run', 'plugin:start'], {
|
|
109
|
+
cwd: pluginDir,
|
|
110
|
+
detached: true,
|
|
111
|
+
stdio: ['ignore', logFd, logFd],
|
|
112
|
+
env,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
child.unref();
|
|
116
|
+
|
|
117
|
+
entry.status = 'running';
|
|
118
|
+
entry.pid = child.pid;
|
|
119
|
+
entry.startedAt = new Date().toISOString();
|
|
120
|
+
writeRegistry(registry);
|
|
121
|
+
|
|
122
|
+
// Verify health if plugin declares a port
|
|
123
|
+
if (port) {
|
|
124
|
+
let healthy = false;
|
|
125
|
+
for (let i = 0; i < 10; i++) {
|
|
126
|
+
await new Promise(r => setTimeout(r, 500));
|
|
127
|
+
try {
|
|
128
|
+
const resp = await fetch(`http://localhost:${port}`, { signal: AbortSignal.timeout(1000) });
|
|
129
|
+
if (resp.ok || resp.status < 500) {
|
|
130
|
+
healthy = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
} catch { /* retry */ }
|
|
134
|
+
}
|
|
135
|
+
if (healthy) {
|
|
136
|
+
p.log.success(`${pluginName} started and listening on port ${port} (PID ${child.pid})`);
|
|
137
|
+
} else {
|
|
138
|
+
p.log.warn(`${pluginName} started (PID ${child.pid}) but port ${port} not responding yet.`);
|
|
139
|
+
p.log.info(`Check logs with: moxxy plugin logs ${pluginName}`);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
p.log.success(`${pluginName} started (PID ${child.pid}).`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function stopPlugin(pluginName) {
|
|
147
|
+
const registry = readRegistry();
|
|
148
|
+
const entry = registry.plugins[pluginName];
|
|
149
|
+
if (!entry) {
|
|
150
|
+
p.log.error(`${pluginName} is not installed.`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!entry.pid || !isProcessAlive(entry.pid)) {
|
|
155
|
+
p.log.warn(`${pluginName} is not running.`);
|
|
156
|
+
entry.status = 'stopped';
|
|
157
|
+
entry.pid = null;
|
|
158
|
+
entry.startedAt = null;
|
|
159
|
+
writeRegistry(registry);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Run plugin:stop hook if present
|
|
164
|
+
const meta = readPluginMeta(pluginName);
|
|
165
|
+
if (meta?.scripts?.['plugin:stop']) {
|
|
166
|
+
const { pluginsDir } = pluginPaths();
|
|
167
|
+
const pluginDir = join(pluginsDir, 'node_modules', pluginName);
|
|
168
|
+
const env = buildPluginEnv(pluginName, entry.port);
|
|
169
|
+
try {
|
|
170
|
+
execSync('npm run plugin:stop', { cwd: pluginDir, env, stdio: 'pipe', timeout: 5000 });
|
|
171
|
+
} catch { /* best effort */ }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
process.kill(entry.pid, 'SIGTERM');
|
|
176
|
+
p.log.success(`${pluginName} stopped (PID ${entry.pid}).`);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err.code === 'ESRCH') {
|
|
179
|
+
p.log.warn(`Process ${entry.pid} not found. Cleaning up stale entry.`);
|
|
180
|
+
} else {
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
entry.status = 'stopped';
|
|
186
|
+
entry.pid = null;
|
|
187
|
+
entry.startedAt = null;
|
|
188
|
+
writeRegistry(registry);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function restartPlugin(pluginName) {
|
|
192
|
+
p.log.step('Stopping plugin...');
|
|
193
|
+
await stopPlugin(pluginName);
|
|
194
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
195
|
+
p.log.step('Starting plugin...');
|
|
196
|
+
await startPlugin(pluginName);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function updatePlugin(pluginName) {
|
|
200
|
+
const registry = readRegistry();
|
|
201
|
+
const entry = registry.plugins[pluginName];
|
|
202
|
+
if (!entry) {
|
|
203
|
+
p.log.error(`${pluginName} is not installed.`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { pluginsDir } = pluginPaths();
|
|
208
|
+
const wasRunning = entry.pid && isProcessAlive(entry.pid);
|
|
209
|
+
const oldVersion = entry.version;
|
|
210
|
+
|
|
211
|
+
// Stop if running
|
|
212
|
+
if (wasRunning) {
|
|
213
|
+
p.log.step('Stopping plugin before update...');
|
|
214
|
+
await stopPlugin(pluginName);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Re-fetch the latest package
|
|
218
|
+
await withSpinner(`Updating ${pluginName}...`, async () => {
|
|
219
|
+
execSync(`npm install ${pluginName}@latest`, { cwd: pluginsDir, stdio: 'pipe', timeout: 120_000 });
|
|
220
|
+
}, `${pluginName} downloaded.`);
|
|
221
|
+
|
|
222
|
+
const meta = readPluginMeta(pluginName);
|
|
223
|
+
const { valid, errors } = validatePluginMeta(meta);
|
|
224
|
+
if (!valid) {
|
|
225
|
+
p.log.error(`Updated package is invalid: ${errors.join(', ')}`);
|
|
226
|
+
p.log.info('Rolling back...');
|
|
227
|
+
try { execSync(`npm install ${pluginName}@${oldVersion}`, { cwd: pluginsDir, stdio: 'pipe', timeout: 120_000 }); } catch { /* best effort */ }
|
|
228
|
+
if (wasRunning) await startPlugin(pluginName);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Run plugin:install hook if present (post-update setup)
|
|
233
|
+
if (meta.scripts?.['plugin:install']) {
|
|
234
|
+
const pluginDir = join(pluginsDir, 'node_modules', pluginName);
|
|
235
|
+
await withSpinner('Running post-install hook...', async () => {
|
|
236
|
+
execSync('npm run plugin:install', { cwd: pluginDir, stdio: 'pipe', timeout: 120_000 });
|
|
237
|
+
}, 'Post-install complete.');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const newVersion = meta.version || '0.0.0';
|
|
241
|
+
entry.version = newVersion;
|
|
242
|
+
writeRegistry(registry);
|
|
243
|
+
|
|
244
|
+
// Restart if it was running before
|
|
245
|
+
if (wasRunning) {
|
|
246
|
+
p.log.step('Restarting plugin...');
|
|
247
|
+
await startPlugin(pluginName);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (oldVersion === newVersion) {
|
|
251
|
+
p.log.success(`${pluginName} is already at the latest version (v${newVersion}).`);
|
|
252
|
+
} else {
|
|
253
|
+
showResult('Plugin updated', {
|
|
254
|
+
Name: meta.moxxy?.displayName || pluginName,
|
|
255
|
+
'Old version': oldVersion,
|
|
256
|
+
'New version': newVersion,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function uninstallPlugin(pluginName) {
|
|
262
|
+
const registry = readRegistry();
|
|
263
|
+
const entry = registry.plugins[pluginName];
|
|
264
|
+
if (!entry) {
|
|
265
|
+
p.log.error(`${pluginName} is not installed.`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Stop if running
|
|
270
|
+
if (entry.pid && isProcessAlive(entry.pid)) {
|
|
271
|
+
await stopPlugin(pluginName);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Run plugin:uninstall hook if present
|
|
275
|
+
const meta = readPluginMeta(pluginName);
|
|
276
|
+
if (meta?.scripts?.['plugin:uninstall']) {
|
|
277
|
+
const { pluginsDir } = pluginPaths();
|
|
278
|
+
const pluginDir = join(pluginsDir, 'node_modules', pluginName);
|
|
279
|
+
const env = buildPluginEnv(pluginName, entry.port);
|
|
280
|
+
try {
|
|
281
|
+
execSync('npm run plugin:uninstall', { cwd: pluginDir, env, stdio: 'pipe', timeout: 10_000 });
|
|
282
|
+
} catch { /* best effort */ }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const { pluginsDir, logsDir } = pluginPaths();
|
|
286
|
+
await withSpinner(`Uninstalling ${pluginName}...`, async () => {
|
|
287
|
+
execSync(`npm uninstall ${pluginName}`, { cwd: pluginsDir, stdio: 'pipe', timeout: 60_000 });
|
|
288
|
+
}, `${pluginName} removed.`);
|
|
289
|
+
|
|
290
|
+
// Clean log file
|
|
291
|
+
const logFile = join(logsDir, sanitizeLogFileName(pluginName));
|
|
292
|
+
try { unlinkSync(logFile); } catch { /* may not exist */ }
|
|
293
|
+
|
|
294
|
+
delete registry.plugins[pluginName];
|
|
295
|
+
writeRegistry(registry);
|
|
296
|
+
|
|
297
|
+
p.log.success(`${pluginName} uninstalled.`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function enablePlugin(pluginName) {
|
|
301
|
+
const registry = readRegistry();
|
|
302
|
+
const entry = registry.plugins[pluginName];
|
|
303
|
+
if (!entry) {
|
|
304
|
+
p.log.error(`${pluginName} is not installed.`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
entry.enabled = true;
|
|
308
|
+
writeRegistry(registry);
|
|
309
|
+
p.log.success(`${pluginName} enabled for auto-start.`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function disablePlugin(pluginName) {
|
|
313
|
+
const registry = readRegistry();
|
|
314
|
+
const entry = registry.plugins[pluginName];
|
|
315
|
+
if (!entry) {
|
|
316
|
+
p.log.error(`${pluginName} is not installed.`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
entry.enabled = false;
|
|
320
|
+
writeRegistry(registry);
|
|
321
|
+
p.log.success(`${pluginName} disabled for auto-start.`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function pluginLogs(pluginName) {
|
|
325
|
+
const { logsDir } = pluginPaths();
|
|
326
|
+
const logFile = join(logsDir, sanitizeLogFileName(pluginName));
|
|
327
|
+
|
|
328
|
+
if (!existsSync(logFile)) {
|
|
329
|
+
p.log.warn(`No log file found at ${logFile}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
p.log.info(`Tailing ${logFile} (Ctrl+C to stop)`);
|
|
334
|
+
|
|
335
|
+
let tail;
|
|
336
|
+
if (platform() === 'win32') {
|
|
337
|
+
tail = spawn('powershell', ['-Command', `Get-Content -Path "${logFile}" -Wait -Tail 50`], { stdio: 'inherit' });
|
|
338
|
+
} else {
|
|
339
|
+
tail = spawn('tail', ['-f', logFile], { stdio: 'inherit' });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
await new Promise((resolve) => {
|
|
343
|
+
process.on('SIGINT', () => {
|
|
344
|
+
tail.kill();
|
|
345
|
+
resolve();
|
|
346
|
+
});
|
|
347
|
+
tail.on('close', resolve);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function listPlugins() {
|
|
352
|
+
const registry = readRegistry();
|
|
353
|
+
const plugins = Object.values(registry.plugins);
|
|
354
|
+
|
|
355
|
+
if (plugins.length === 0) {
|
|
356
|
+
p.log.info('No plugins installed.');
|
|
357
|
+
p.log.info('Install one with: moxxy plugin install <package>');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const plug of plugins) {
|
|
362
|
+
const alive = plug.pid && isProcessAlive(plug.pid);
|
|
363
|
+
const statusIcon = alive ? '\u2705' : plug.status === 'error' ? '\u274c' : '\u23f8\ufe0f';
|
|
364
|
+
const status = alive ? 'running' : plug.status === 'error' ? 'error' : 'stopped';
|
|
365
|
+
const autoStart = plug.enabled ? ' [auto-start]' : '';
|
|
366
|
+
const builtinTag = plug.builtin ? ' (built-in)' : '';
|
|
367
|
+
const portInfo = plug.port ? ` :${plug.port}` : '';
|
|
368
|
+
|
|
369
|
+
p.log.info(`${statusIcon} ${plug.name} v${plug.version} [${status}]${portInfo}${autoStart}${builtinTag}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── Interactive menu ──
|
|
374
|
+
|
|
375
|
+
async function interactiveMenu(client) {
|
|
376
|
+
const action = await p.select({
|
|
377
|
+
message: 'Plugin action',
|
|
378
|
+
options: [
|
|
379
|
+
{ value: 'list', label: 'List', hint: 'show installed plugins' },
|
|
380
|
+
{ value: 'install', label: 'Install', hint: 'install a new plugin' },
|
|
381
|
+
{ value: 'manage', label: 'Manage', hint: 'start/stop/uninstall a plugin' },
|
|
382
|
+
],
|
|
383
|
+
});
|
|
384
|
+
handleCancel(action);
|
|
385
|
+
|
|
386
|
+
switch (action) {
|
|
387
|
+
case 'list':
|
|
388
|
+
listPlugins();
|
|
389
|
+
break;
|
|
390
|
+
case 'install':
|
|
391
|
+
await interactiveInstall();
|
|
392
|
+
break;
|
|
393
|
+
case 'manage':
|
|
394
|
+
await interactiveManage();
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function interactiveInstall() {
|
|
400
|
+
const registry = readRegistry();
|
|
401
|
+
|
|
402
|
+
const options = BUILTIN_PLUGINS.map(b => ({
|
|
403
|
+
value: b.name,
|
|
404
|
+
label: b.label,
|
|
405
|
+
hint: `${b.hint}${registry.plugins[b.name] ? ' (installed)' : ''}`,
|
|
406
|
+
}));
|
|
407
|
+
options.push({ value: '__custom__', label: 'Custom', hint: 'enter an npm package name' });
|
|
408
|
+
|
|
409
|
+
const selected = await p.select({
|
|
410
|
+
message: 'Choose a plugin to install',
|
|
411
|
+
options,
|
|
412
|
+
});
|
|
413
|
+
handleCancel(selected);
|
|
414
|
+
|
|
415
|
+
let pluginName = selected;
|
|
416
|
+
if (selected === '__custom__') {
|
|
417
|
+
pluginName = await p.text({
|
|
418
|
+
message: 'Enter npm package name',
|
|
419
|
+
placeholder: 'my-plugin or @scope/my-plugin',
|
|
420
|
+
validate: (v) => {
|
|
421
|
+
if (!v || !v.trim()) return 'Package name is required';
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
handleCancel(pluginName);
|
|
425
|
+
pluginName = pluginName.trim();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
await installPlugin(pluginName);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function interactiveManage() {
|
|
432
|
+
const registry = readRegistry();
|
|
433
|
+
const plugins = Object.values(registry.plugins);
|
|
434
|
+
|
|
435
|
+
if (plugins.length === 0) {
|
|
436
|
+
p.log.info('No plugins installed. Install one first.');
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const selected = await p.select({
|
|
441
|
+
message: 'Select a plugin to manage',
|
|
442
|
+
options: plugins.map(plug => {
|
|
443
|
+
const alive = plug.pid && isProcessAlive(plug.pid);
|
|
444
|
+
const status = alive ? 'running' : 'stopped';
|
|
445
|
+
return {
|
|
446
|
+
value: plug.name,
|
|
447
|
+
label: plug.name,
|
|
448
|
+
hint: `v${plug.version} [${status}]${plug.builtin ? ' (built-in)' : ''}`,
|
|
449
|
+
};
|
|
450
|
+
}),
|
|
451
|
+
});
|
|
452
|
+
handleCancel(selected);
|
|
453
|
+
|
|
454
|
+
const entry = registry.plugins[selected];
|
|
455
|
+
const alive = entry.pid && isProcessAlive(entry.pid);
|
|
456
|
+
|
|
457
|
+
const actions = [];
|
|
458
|
+
if (!alive) {
|
|
459
|
+
actions.push({ value: 'start', label: 'Start', hint: 'start the plugin' });
|
|
460
|
+
}
|
|
461
|
+
if (alive) {
|
|
462
|
+
actions.push({ value: 'stop', label: 'Stop', hint: 'stop the plugin' });
|
|
463
|
+
actions.push({ value: 'restart', label: 'Restart', hint: 'restart the plugin' });
|
|
464
|
+
}
|
|
465
|
+
if (entry.enabled) {
|
|
466
|
+
actions.push({ value: 'disable', label: 'Disable auto-start', hint: 'disable automatic startup' });
|
|
467
|
+
} else {
|
|
468
|
+
actions.push({ value: 'enable', label: 'Enable auto-start', hint: 'enable automatic startup' });
|
|
469
|
+
}
|
|
470
|
+
actions.push({ value: 'update', label: 'Update', hint: 're-fetch the latest version' });
|
|
471
|
+
actions.push({ value: 'logs', label: 'Logs', hint: 'tail plugin logs' });
|
|
472
|
+
actions.push({ value: 'uninstall', label: 'Uninstall', hint: 'remove the plugin' });
|
|
473
|
+
|
|
474
|
+
const action = await p.select({
|
|
475
|
+
message: `Action for ${selected}`,
|
|
476
|
+
options: actions,
|
|
477
|
+
});
|
|
478
|
+
handleCancel(action);
|
|
479
|
+
|
|
480
|
+
switch (action) {
|
|
481
|
+
case 'start':
|
|
482
|
+
await startPlugin(selected);
|
|
483
|
+
break;
|
|
484
|
+
case 'stop':
|
|
485
|
+
await stopPlugin(selected);
|
|
486
|
+
break;
|
|
487
|
+
case 'restart':
|
|
488
|
+
await restartPlugin(selected);
|
|
489
|
+
break;
|
|
490
|
+
case 'enable':
|
|
491
|
+
enablePlugin(selected);
|
|
492
|
+
break;
|
|
493
|
+
case 'disable':
|
|
494
|
+
disablePlugin(selected);
|
|
495
|
+
break;
|
|
496
|
+
case 'update':
|
|
497
|
+
await updatePlugin(selected);
|
|
498
|
+
break;
|
|
499
|
+
case 'logs':
|
|
500
|
+
await pluginLogs(selected);
|
|
501
|
+
break;
|
|
502
|
+
case 'uninstall':
|
|
503
|
+
await uninstallPlugin(selected);
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── CLI subcommand helpers ──
|
|
509
|
+
|
|
510
|
+
function resolvePluginName(args) {
|
|
511
|
+
const name = args.join(' ').trim();
|
|
512
|
+
if (!name) {
|
|
513
|
+
p.log.error('Plugin name is required.');
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
return name;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ── Command router ──
|
|
520
|
+
|
|
521
|
+
export async function runPlugin(client, args) {
|
|
522
|
+
const sub = args[0];
|
|
523
|
+
|
|
524
|
+
switch (sub) {
|
|
525
|
+
case 'list':
|
|
526
|
+
listPlugins();
|
|
527
|
+
break;
|
|
528
|
+
case 'install': {
|
|
529
|
+
const name = resolvePluginName(args.slice(1));
|
|
530
|
+
if (name) await installPlugin(name);
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case 'start': {
|
|
534
|
+
const name = resolvePluginName(args.slice(1));
|
|
535
|
+
if (name) await startPlugin(name);
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
case 'stop': {
|
|
539
|
+
const name = resolvePluginName(args.slice(1));
|
|
540
|
+
if (name) await stopPlugin(name);
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
case 'restart': {
|
|
544
|
+
const name = resolvePluginName(args.slice(1));
|
|
545
|
+
if (name) await restartPlugin(name);
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case 'update': {
|
|
549
|
+
const name = resolvePluginName(args.slice(1));
|
|
550
|
+
if (name) await updatePlugin(name);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case 'uninstall': {
|
|
554
|
+
const name = resolvePluginName(args.slice(1));
|
|
555
|
+
if (name) await uninstallPlugin(name);
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
case 'logs': {
|
|
559
|
+
const name = resolvePluginName(args.slice(1));
|
|
560
|
+
if (name) await pluginLogs(name);
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
case 'enable': {
|
|
564
|
+
const name = resolvePluginName(args.slice(1));
|
|
565
|
+
if (name) enablePlugin(name);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
case 'disable': {
|
|
569
|
+
const name = resolvePluginName(args.slice(1));
|
|
570
|
+
if (name) disablePlugin(name);
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
default:
|
|
574
|
+
if (!sub && isInteractive()) {
|
|
575
|
+
await interactiveMenu(client);
|
|
576
|
+
} else if (sub) {
|
|
577
|
+
p.log.error(`Unknown plugin action: ${sub}`);
|
|
578
|
+
showHelp('plugin', p);
|
|
579
|
+
} else {
|
|
580
|
+
showHelp('plugin', p);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|