napcat-plugin-debug-cli 1.0.0 → 1.2.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.
Files changed (3) hide show
  1. package/cli.mjs +85 -0
  2. package/package.json +21 -3
  3. package/vite.mjs +233 -0
package/cli.mjs CHANGED
@@ -5053,6 +5053,8 @@ function parseArgs() {
5053
5053
  opts.watchAll = true;
5054
5054
  } else if (arg === "--verbose" || arg === "-v") {
5055
5055
  opts.verbose = true;
5056
+ } else if (arg === "--deploy" || arg === "-d") {
5057
+ opts.deploy = args[++i] || ".";
5056
5058
  } else if (arg.startsWith("ws://") || arg.startsWith("wss://")) {
5057
5059
  opts.wsUrl = arg;
5058
5060
  }
@@ -5070,6 +5072,7 @@ napcat-plugin-debug CLI — NapCat 插件调试 & 热重载
5070
5072
  -t, --token <token> 认证 token
5071
5073
  -w, --watch <dir> 监听目录自动热重载
5072
5074
  -W, --watch-all 监听远程插件目录所有插件
5075
+ -d, --deploy [dir] 部署插件 dist/ 到远程插件目录并重载 (默认: .)
5073
5076
  -v, --verbose 详细输出
5074
5077
  -h, --help 帮助
5075
5078
 
@@ -5079,6 +5082,7 @@ napcat-plugin-debug CLI — NapCat 插件调试 & 热重载
5079
5082
  load <id> 加载插件
5080
5083
  unload <id> 卸载插件
5081
5084
  info <id> 插件详情
5085
+ deploy [dir] 部署插件到远程并重载
5082
5086
  watch <dir> 开始监听
5083
5087
  unwatch 停止监听
5084
5088
  status 服务状态
@@ -5202,6 +5206,73 @@ function createWatcher(watchPath, onPluginChange) {
5202
5206
  }
5203
5207
  };
5204
5208
  }
5209
+ function copyDirRecursive(src, dest) {
5210
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
5211
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
5212
+ const srcPath = path.join(src, entry.name);
5213
+ const destPath = path.join(dest, entry.name);
5214
+ if (entry.isDirectory()) copyDirRecursive(srcPath, destPath);
5215
+ else fs.copyFileSync(srcPath, destPath);
5216
+ }
5217
+ }
5218
+ function countFiles(dir) {
5219
+ let count = 0;
5220
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
5221
+ if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
5222
+ else count++;
5223
+ }
5224
+ return count;
5225
+ }
5226
+ async function deployPlugin(projectDir, remotePluginPath, rpc) {
5227
+ const distDir = path.resolve(projectDir, "dist");
5228
+ if (!fs.existsSync(distDir)) {
5229
+ logErr(`dist/ 目录不存在: ${distDir}`);
5230
+ logInfo("请先运行 pnpm run build 构建插件");
5231
+ return false;
5232
+ }
5233
+ const distPkgPath = path.join(distDir, "package.json");
5234
+ if (!fs.existsSync(distPkgPath)) {
5235
+ logErr("dist/package.json 不存在,无法确定插件名称");
5236
+ return false;
5237
+ }
5238
+ let pluginName;
5239
+ try {
5240
+ const pkg = JSON.parse(fs.readFileSync(distPkgPath, "utf-8"));
5241
+ pluginName = pkg.name;
5242
+ if (!pluginName) {
5243
+ logErr("dist/package.json 中缺少 name 字段");
5244
+ return false;
5245
+ }
5246
+ } catch (e) {
5247
+ logErr(`解析 dist/package.json 失败: ${e.message}`);
5248
+ return false;
5249
+ }
5250
+ const destDir = path.join(remotePluginPath, pluginName);
5251
+ logInfo(`部署 ${co(pluginName, C.bold, C.cyan)} → ${co(destDir, C.dim)}`);
5252
+ try {
5253
+ if (fs.existsSync(destDir)) {
5254
+ fs.rmSync(destDir, { recursive: true, force: true });
5255
+ }
5256
+ copyDirRecursive(distDir, destDir);
5257
+ logOk(`文件复制完成 (${countFiles(distDir)} 个文件)`);
5258
+ } catch (e) {
5259
+ logErr(`复制文件失败: ${e.message}`);
5260
+ return false;
5261
+ }
5262
+ try {
5263
+ await rpc.call("reloadPlugin", pluginName);
5264
+ logOk(`${co(pluginName, C.green, C.bold)} 重载成功`);
5265
+ } catch {
5266
+ try {
5267
+ logInfo("插件未注册,尝试从目录加载...");
5268
+ await rpc.call("loadDirectoryPlugin", destDir);
5269
+ logOk(`${co(pluginName, C.green, C.bold)} 首次加载成功`);
5270
+ } catch (e2) {
5271
+ logWarn(`自动加载失败: ${e2.message},请手动 load ${pluginName}`);
5272
+ }
5273
+ }
5274
+ return true;
5275
+ }
5205
5276
  async function main() {
5206
5277
  const opts = parseArgs();
5207
5278
  console.log(co("\n napcat-plugin-debug CLI", C.bold, C.cyan));
@@ -5254,6 +5325,11 @@ async function main() {
5254
5325
  } catch (e) {
5255
5326
  logWarn(`获取信息失败: ${e.message}`);
5256
5327
  }
5328
+ if (opts.deploy && remotePluginPath && rpc) {
5329
+ const ok = await deployPlugin(path.resolve(opts.deploy), remotePluginPath, rpc);
5330
+ ws.close(1e3);
5331
+ process.exit(ok ? 0 : 1);
5332
+ }
5257
5333
  if (opts.watch) {
5258
5334
  watcher = createWatcher(path.resolve(opts.watch), onFileChange);
5259
5335
  watcher.start();
@@ -5361,6 +5437,15 @@ function startRepl(rpc, watcher, remotePath, onFileChange) {
5361
5437
  `);
5362
5438
  break;
5363
5439
  }
5440
+ case "deploy": {
5441
+ if (!remotePath) {
5442
+ logErr("远程插件目录未知,无法部署");
5443
+ break;
5444
+ }
5445
+ const dir = args[0] || ".";
5446
+ await deployPlugin(path.resolve(dir), remotePath, rpc);
5447
+ break;
5448
+ }
5364
5449
  case "watch": {
5365
5450
  if (!args[0]) {
5366
5451
  logErr("用法: watch <dir>");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "napcat-plugin-debug-cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "NapCat 插件调试 CLI — 连接调试服务实现热重载",
6
6
  "author": "NapNeko",
@@ -8,14 +8,32 @@
8
8
  "bin": {
9
9
  "napcat-debug": "./cli.mjs"
10
10
  },
11
+ "exports": {
12
+ ".": "./cli.mjs",
13
+ "./vite": "./vite.mjs"
14
+ },
11
15
  "files": [
12
- "cli.mjs"
16
+ "cli.mjs",
17
+ "vite.mjs"
13
18
  ],
19
+ "peerDependencies": {
20
+ "vite": ">=5.0.0",
21
+ "ws": ">=8.0.0"
22
+ },
23
+ "peerDependenciesMeta": {
24
+ "vite": {
25
+ "optional": true
26
+ },
27
+ "ws": {
28
+ "optional": false
29
+ }
30
+ },
14
31
  "keywords": [
15
32
  "napcat",
16
33
  "plugin",
17
34
  "debug",
18
35
  "hmr",
19
- "hot-reload"
36
+ "hot-reload",
37
+ "vite-plugin"
20
38
  ]
21
39
  }
package/vite.mjs ADDED
@@ -0,0 +1,233 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const C = {
5
+ reset: "\x1B[0m",
6
+ bold: "\x1B[1m",
7
+ dim: "\x1B[2m",
8
+ red: "\x1B[31m",
9
+ green: "\x1B[32m",
10
+ yellow: "\x1B[33m",
11
+ blue: "\x1B[34m",
12
+ magenta: "\x1B[35m",
13
+ cyan: "\x1B[36m",
14
+ gray: "\x1B[90m"
15
+ };
16
+ const co = (t, ...c) => c.join("") + t + C.reset;
17
+ const PREFIX = co("[napcat-hmr]", C.magenta, C.bold);
18
+ const log = (m) => console.log(`${PREFIX} ${m}`);
19
+ const logOk = (m) => console.log(`${PREFIX} ${co("✓", C.green)} ${m}`);
20
+ const logErr = (m) => console.log(`${PREFIX} ${co("✗", C.red)} ${m}`);
21
+ const logHmr = (m) => console.log(`${PREFIX} ${co("🔥", C.magenta)} ${co(m, C.magenta)}`);
22
+ class SimpleRpcClient {
23
+ ws;
24
+ nextId = 1;
25
+ pending = /* @__PURE__ */ new Map();
26
+ constructor(ws) {
27
+ this.ws = ws;
28
+ ws.on("message", (raw) => {
29
+ try {
30
+ const msg = JSON.parse(raw.toString());
31
+ if (msg.jsonrpc === "2.0" && msg.id != null) {
32
+ const p = this.pending.get(msg.id);
33
+ if (p) {
34
+ this.pending.delete(msg.id);
35
+ if (msg.error) p.reject(new Error(msg.error.message));
36
+ else p.resolve(msg.result);
37
+ }
38
+ }
39
+ } catch {
40
+ }
41
+ });
42
+ }
43
+ call(method, ...params) {
44
+ return new Promise((resolve, reject) => {
45
+ const id = this.nextId++;
46
+ this.pending.set(id, { resolve, reject });
47
+ const req = { jsonrpc: "2.0", id, method, params };
48
+ this.ws.send(JSON.stringify(req));
49
+ setTimeout(() => {
50
+ if (this.pending.has(id)) {
51
+ this.pending.delete(id);
52
+ reject(new Error("RPC timeout"));
53
+ }
54
+ }, 1e4);
55
+ });
56
+ }
57
+ get connected() {
58
+ return this.ws?.readyState === 1;
59
+ }
60
+ close() {
61
+ try {
62
+ this.ws?.close(1e3);
63
+ } catch {
64
+ }
65
+ }
66
+ }
67
+ function copyDirRecursive(src, dest) {
68
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
69
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
70
+ const srcPath = path.join(src, entry.name);
71
+ const destPath = path.join(dest, entry.name);
72
+ if (entry.isDirectory()) copyDirRecursive(srcPath, destPath);
73
+ else fs.copyFileSync(srcPath, destPath);
74
+ }
75
+ }
76
+ function countFiles(dir) {
77
+ let count = 0;
78
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
79
+ if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
80
+ else count++;
81
+ }
82
+ return count;
83
+ }
84
+ function napcatHmrPlugin(options = {}) {
85
+ const {
86
+ wsUrl = "ws://127.0.0.1:8998",
87
+ token,
88
+ enabled = true,
89
+ autoConnect = true
90
+ } = options;
91
+ let rpc = null;
92
+ let remotePluginPath = null;
93
+ let connecting = false;
94
+ let config;
95
+ let isFirstBuild = true;
96
+ async function connect() {
97
+ if (rpc?.connected) return true;
98
+ if (connecting) return false;
99
+ connecting = true;
100
+ try {
101
+ const { default: WebSocket } = await import('ws');
102
+ let url = wsUrl;
103
+ if (token) {
104
+ const u = new URL(url);
105
+ u.searchParams.set("token", token);
106
+ url = u.toString();
107
+ }
108
+ return await new Promise((resolve) => {
109
+ const ws = new WebSocket(url);
110
+ const timeout = setTimeout(() => {
111
+ ws.close();
112
+ connecting = false;
113
+ resolve(false);
114
+ }, 5e3);
115
+ ws.on("open", () => {
116
+ clearTimeout(timeout);
117
+ });
118
+ ws.on("message", async (raw) => {
119
+ try {
120
+ const msg = JSON.parse(raw.toString());
121
+ if (msg.method === "welcome") {
122
+ rpc = new SimpleRpcClient(ws);
123
+ try {
124
+ const info = await rpc.call("getDebugInfo");
125
+ remotePluginPath = info.pluginPath;
126
+ logOk(`已连接调试服务 (${info.loadedCount}/${info.pluginCount} 插件)`);
127
+ log(`远程插件目录: ${co(info.pluginPath, C.dim)}`);
128
+ } catch {
129
+ }
130
+ connecting = false;
131
+ resolve(true);
132
+ }
133
+ } catch {
134
+ }
135
+ });
136
+ ws.on("error", () => {
137
+ clearTimeout(timeout);
138
+ connecting = false;
139
+ resolve(false);
140
+ });
141
+ ws.on("close", () => {
142
+ rpc = null;
143
+ remotePluginPath = null;
144
+ connecting = false;
145
+ });
146
+ });
147
+ } catch (e) {
148
+ connecting = false;
149
+ return false;
150
+ }
151
+ }
152
+ async function deployAndReload(distDir) {
153
+ if (!rpc?.connected || !remotePluginPath) {
154
+ logErr("未连接到调试服务,跳过部署");
155
+ return;
156
+ }
157
+ const pkgPath = path.join(distDir, "package.json");
158
+ if (!fs.existsSync(pkgPath)) {
159
+ logErr("dist/package.json 不存在,跳过部署");
160
+ return;
161
+ }
162
+ let pluginName;
163
+ try {
164
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
165
+ pluginName = pkg.name;
166
+ if (!pluginName) {
167
+ logErr("dist/package.json 中缺少 name 字段");
168
+ return;
169
+ }
170
+ } catch {
171
+ logErr("解析 dist/package.json 失败");
172
+ return;
173
+ }
174
+ const destDir = path.join(remotePluginPath, pluginName);
175
+ try {
176
+ if (fs.existsSync(destDir)) {
177
+ fs.rmSync(destDir, { recursive: true, force: true });
178
+ }
179
+ copyDirRecursive(distDir, destDir);
180
+ } catch (e) {
181
+ logErr(`复制文件失败: ${e.message}`);
182
+ return;
183
+ }
184
+ try {
185
+ await rpc.call("reloadPlugin", pluginName);
186
+ logHmr(`${co(pluginName, C.green, C.bold)} 已重载 (${countFiles(distDir)} 个文件)`);
187
+ } catch {
188
+ try {
189
+ await rpc.call("loadDirectoryPlugin", destDir);
190
+ logOk(`${co(pluginName, C.green, C.bold)} 首次加载成功`);
191
+ } catch (e2) {
192
+ logErr(`加载失败: ${e2.message}`);
193
+ }
194
+ }
195
+ }
196
+ return {
197
+ name: "napcat-hmr",
198
+ apply: "build",
199
+ configResolved(resolvedConfig) {
200
+ config = resolvedConfig;
201
+ },
202
+ async buildStart() {
203
+ if (!enabled) return;
204
+ if (!autoConnect) return;
205
+ if (isFirstBuild) {
206
+ log(`连接 ${co(wsUrl, C.cyan)}...`);
207
+ const ok = await connect();
208
+ if (!ok) {
209
+ logErr(`无法连接调试服务 ${wsUrl}`);
210
+ log("请确认 napcat-plugin-debug 已启用");
211
+ log("仅构建模式,不自动部署");
212
+ }
213
+ }
214
+ },
215
+ async writeBundle() {
216
+ if (!enabled) return;
217
+ const distDir = path.resolve(config.build.outDir);
218
+ if (!rpc?.connected) {
219
+ const ok = await connect();
220
+ if (!ok) return;
221
+ }
222
+ await deployAndReload(distDir);
223
+ isFirstBuild = false;
224
+ },
225
+ closeBundle() {
226
+ if (config.build.watch) return;
227
+ rpc?.close();
228
+ rpc = null;
229
+ }
230
+ };
231
+ }
232
+
233
+ export { napcatHmrPlugin as default, napcatHmrPlugin };