bunmicro 0.8.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/LICENSE +22 -0
- package/PORTING.md +34 -0
- package/README.md +153 -0
- package/bmi +5 -0
- package/bun.lock +17 -0
- package/bunmicro +5 -0
- package/hlw.md +5 -0
- package/package.json +18 -0
- package/runtime/colorschemes/atom-dark.micro +33 -0
- package/runtime/colorschemes/bubblegum.micro +31 -0
- package/runtime/colorschemes/cmc-16.micro +47 -0
- package/runtime/colorschemes/cmc-tc.micro +43 -0
- package/runtime/colorschemes/darcula.micro +34 -0
- package/runtime/colorschemes/default.micro +1 -0
- package/runtime/colorschemes/dracula-tc.micro +49 -0
- package/runtime/colorschemes/dukedark-tc.micro +38 -0
- package/runtime/colorschemes/dukelight-tc.micro +38 -0
- package/runtime/colorschemes/dukeubuntu-tc.micro +38 -0
- package/runtime/colorschemes/geany.micro +29 -0
- package/runtime/colorschemes/gotham.micro +29 -0
- package/runtime/colorschemes/gruvbox-tc.micro +29 -0
- package/runtime/colorschemes/gruvbox.micro +26 -0
- package/runtime/colorschemes/material-tc.micro +36 -0
- package/runtime/colorschemes/monokai-dark.micro +28 -0
- package/runtime/colorschemes/monokai.micro +34 -0
- package/runtime/colorschemes/one-dark.micro +39 -0
- package/runtime/colorschemes/railscast.micro +37 -0
- package/runtime/colorschemes/simple.micro +33 -0
- package/runtime/colorschemes/solarized-tc.micro +31 -0
- package/runtime/colorschemes/solarized.micro +30 -0
- package/runtime/colorschemes/sunny-day.micro +29 -0
- package/runtime/colorschemes/twilight.micro +40 -0
- package/runtime/colorschemes/zenburn.micro +30 -0
- package/runtime/help/actions.md +161 -0
- package/runtime/help/colors.md +421 -0
- package/runtime/help/commands.md +161 -0
- package/runtime/help/copypaste.md +149 -0
- package/runtime/help/defaultkeys.md +141 -0
- package/runtime/help/help.md +63 -0
- package/runtime/help/keybindings.md +760 -0
- package/runtime/help/linter.md +90 -0
- package/runtime/help/options.md +701 -0
- package/runtime/help/plugins.md +544 -0
- package/runtime/help/tutorial.md +112 -0
- package/runtime/jsplugins/chapter/chapter.js +108 -0
- package/runtime/jsplugins/diff/diff.js +46 -0
- package/runtime/jsplugins/example/example.js +108 -0
- package/runtime/jsplugins/linter/linter.js +281 -0
- package/runtime/plugins/autoclose/autoclose.lua +75 -0
- package/runtime/plugins/ftoptions/ftoptions.lua +17 -0
- package/runtime/plugins/literate/README.md +5 -0
- package/runtime/plugins/literate/literate.lua +55 -0
- package/runtime/plugins/status/help/status.md +21 -0
- package/runtime/plugins/status/status.lua +62 -0
- package/runtime/syntax/LICENSE +22 -0
- package/runtime/syntax/PowerShell.yaml +128 -0
- package/runtime/syntax/README.md +63 -0
- package/runtime/syntax/ada.yaml +43 -0
- package/runtime/syntax/apacheconf.yaml +59 -0
- package/runtime/syntax/arduino.yaml +101 -0
- package/runtime/syntax/asciidoc.yaml +51 -0
- package/runtime/syntax/asm.yaml +123 -0
- package/runtime/syntax/ats.yaml +99 -0
- package/runtime/syntax/awk.yaml +44 -0
- package/runtime/syntax/b.yaml +87 -0
- package/runtime/syntax/bat.yaml +57 -0
- package/runtime/syntax/c.yaml +60 -0
- package/runtime/syntax/caddyfile.yaml +23 -0
- package/runtime/syntax/cake.yaml +7 -0
- package/runtime/syntax/clojure.yaml +38 -0
- package/runtime/syntax/cmake.yaml +42 -0
- package/runtime/syntax/coffeescript.yaml +56 -0
- package/runtime/syntax/colortest.yaml +19 -0
- package/runtime/syntax/conky.yaml +17 -0
- package/runtime/syntax/cpp.yaml +91 -0
- package/runtime/syntax/crontab.yaml +36 -0
- package/runtime/syntax/crystal.yaml +72 -0
- package/runtime/syntax/csharp.yaml +52 -0
- package/runtime/syntax/css.yaml +44 -0
- package/runtime/syntax/csx.yaml +8 -0
- package/runtime/syntax/cuda.yaml +68 -0
- package/runtime/syntax/cython.yaml +52 -0
- package/runtime/syntax/d.yaml +121 -0
- package/runtime/syntax/dart.yaml +46 -0
- package/runtime/syntax/default.yaml +10 -0
- package/runtime/syntax/dockerfile.yaml +36 -0
- package/runtime/syntax/dot.yaml +29 -0
- package/runtime/syntax/elixir.yaml +30 -0
- package/runtime/syntax/elm.yaml +38 -0
- package/runtime/syntax/erb.yaml +42 -0
- package/runtime/syntax/erlang.yaml +45 -0
- package/runtime/syntax/fish.yaml +48 -0
- package/runtime/syntax/forth.yaml +34 -0
- package/runtime/syntax/fortran.yaml +64 -0
- package/runtime/syntax/freebsd-kernel.yaml +14 -0
- package/runtime/syntax/fsharp.yaml +48 -0
- package/runtime/syntax/gdscript.yaml +61 -0
- package/runtime/syntax/gemini.yaml +19 -0
- package/runtime/syntax/gentoo-ebuild.yaml +48 -0
- package/runtime/syntax/gentoo-etc-portage.yaml +23 -0
- package/runtime/syntax/git-commit.yaml +35 -0
- package/runtime/syntax/git-config.yaml +14 -0
- package/runtime/syntax/git-rebase-todo.yaml +19 -0
- package/runtime/syntax/gleam.yaml +69 -0
- package/runtime/syntax/glsl.yaml +26 -0
- package/runtime/syntax/gnuplot.yaml +15 -0
- package/runtime/syntax/go.yaml +62 -0
- package/runtime/syntax/godoc.yaml +17 -0
- package/runtime/syntax/golo.yaml +73 -0
- package/runtime/syntax/gomod.yaml +31 -0
- package/runtime/syntax/graphql.yaml +47 -0
- package/runtime/syntax/groff.yaml +30 -0
- package/runtime/syntax/groovy.yaml +111 -0
- package/runtime/syntax/haml.yaml +16 -0
- package/runtime/syntax/hare.yaml +52 -0
- package/runtime/syntax/haskell.yaml +52 -0
- package/runtime/syntax/hc.yaml +52 -0
- package/runtime/syntax/html.yaml +70 -0
- package/runtime/syntax/html4.yaml +25 -0
- package/runtime/syntax/html5.yaml +25 -0
- package/runtime/syntax/ini.yaml +23 -0
- package/runtime/syntax/inputrc.yaml +14 -0
- package/runtime/syntax/java.yaml +37 -0
- package/runtime/syntax/javascript.yaml +76 -0
- package/runtime/syntax/jinja2.yaml +19 -0
- package/runtime/syntax/json.yaml +39 -0
- package/runtime/syntax/jsonnet.yaml +92 -0
- package/runtime/syntax/julia.yaml +57 -0
- package/runtime/syntax/justfile.yaml +40 -0
- package/runtime/syntax/keymap.yaml +27 -0
- package/runtime/syntax/kickstart.yaml +16 -0
- package/runtime/syntax/kotlin.yaml +66 -0
- package/runtime/syntax/kvlang.yaml +67 -0
- package/runtime/syntax/ledger.yaml +14 -0
- package/runtime/syntax/lfe.yaml +17 -0
- package/runtime/syntax/lilypond.yaml +26 -0
- package/runtime/syntax/lisp.yaml +17 -0
- package/runtime/syntax/log.yaml +92 -0
- package/runtime/syntax/lua.yaml +111 -0
- package/runtime/syntax/mail.yaml +25 -0
- package/runtime/syntax/makefile.yaml +38 -0
- package/runtime/syntax/man.yaml +12 -0
- package/runtime/syntax/markdown.yaml +49 -0
- package/runtime/syntax/mc.yaml +23 -0
- package/runtime/syntax/meson.yaml +51 -0
- package/runtime/syntax/micro.yaml +34 -0
- package/runtime/syntax/mpdconf.yaml +13 -0
- package/runtime/syntax/msbuild.yaml +6 -0
- package/runtime/syntax/nanorc.yaml +16 -0
- package/runtime/syntax/nftables.yaml +30 -0
- package/runtime/syntax/nginx.yaml +22 -0
- package/runtime/syntax/nim.yaml +27 -0
- package/runtime/syntax/nix.yaml +32 -0
- package/runtime/syntax/nu.yaml +114 -0
- package/runtime/syntax/objc.yaml +60 -0
- package/runtime/syntax/ocaml.yaml +43 -0
- package/runtime/syntax/octave.yaml +83 -0
- package/runtime/syntax/odin.yaml +64 -0
- package/runtime/syntax/pascal.yaml +45 -0
- package/runtime/syntax/patch.yaml +14 -0
- package/runtime/syntax/peg.yaml +16 -0
- package/runtime/syntax/perl.yaml +58 -0
- package/runtime/syntax/php.yaml +60 -0
- package/runtime/syntax/pkg-config.yaml +12 -0
- package/runtime/syntax/po.yaml +12 -0
- package/runtime/syntax/pony.yaml +37 -0
- package/runtime/syntax/pov.yaml +21 -0
- package/runtime/syntax/privoxy-action.yaml +14 -0
- package/runtime/syntax/privoxy-config.yaml +10 -0
- package/runtime/syntax/privoxy-filter.yaml +12 -0
- package/runtime/syntax/proto.yaml +40 -0
- package/runtime/syntax/prql.yaml +84 -0
- package/runtime/syntax/puppet.yaml +22 -0
- package/runtime/syntax/python2.yaml +60 -0
- package/runtime/syntax/python3.yaml +62 -0
- package/runtime/syntax/r.yaml +32 -0
- package/runtime/syntax/raku.yaml +42 -0
- package/runtime/syntax/reST.yaml +18 -0
- package/runtime/syntax/renpy.yaml +15 -0
- package/runtime/syntax/rpmspec.yaml +43 -0
- package/runtime/syntax/ruby.yaml +73 -0
- package/runtime/syntax/rust.yaml +78 -0
- package/runtime/syntax/sage.yaml +60 -0
- package/runtime/syntax/scad.yaml +53 -0
- package/runtime/syntax/scala.yaml +33 -0
- package/runtime/syntax/sed.yaml +13 -0
- package/runtime/syntax/sh.yaml +69 -0
- package/runtime/syntax/sls.yaml +15 -0
- package/runtime/syntax/smalltalk.yaml +55 -0
- package/runtime/syntax/solidity.yaml +41 -0
- package/runtime/syntax/sql.yaml +35 -0
- package/runtime/syntax/stata.yaml +67 -0
- package/runtime/syntax/svelte.yaml +27 -0
- package/runtime/syntax/swift.yaml +103 -0
- package/runtime/syntax/systemd.yaml +16 -0
- package/runtime/syntax/tcl.yaml +18 -0
- package/runtime/syntax/terraform.yaml +87 -0
- package/runtime/syntax/tex.yaml +32 -0
- package/runtime/syntax/toml.yaml +56 -0
- package/runtime/syntax/twig.yaml +55 -0
- package/runtime/syntax/typescript.yaml +49 -0
- package/runtime/syntax/v.yaml +80 -0
- package/runtime/syntax/vala.yaml +26 -0
- package/runtime/syntax/verilog.yaml +60 -0
- package/runtime/syntax/vhdl.yaml +37 -0
- package/runtime/syntax/vi.yaml +31 -0
- package/runtime/syntax/vue.yaml +64 -0
- package/runtime/syntax/xml.yaml +37 -0
- package/runtime/syntax/xresources.yaml +14 -0
- package/runtime/syntax/yaml.yaml +34 -0
- package/runtime/syntax/yum.yaml +12 -0
- package/runtime/syntax/zig.yaml +52 -0
- package/runtime/syntax/zscript.yaml +72 -0
- package/runtime/syntax/zsh.yaml +52 -0
- package/src/buffer/buffer.js +126 -0
- package/src/buffer/loc.js +38 -0
- package/src/buffer/message.js +29 -0
- package/src/config/colorscheme.js +109 -0
- package/src/config/config.js +118 -0
- package/src/config/defaults.js +102 -0
- package/src/display/ansi-style.js +60 -0
- package/src/highlight/highlighter.js +237 -0
- package/src/highlight/parser.js +137 -0
- package/src/index.js +5942 -0
- package/src/lua/engine.js +38 -0
- package/src/platform/archive.js +50 -0
- package/src/platform/clipboard.js +160 -0
- package/src/platform/commands.js +140 -0
- package/src/plugins/js-bridge.js +902 -0
- package/src/plugins/manager.js +619 -0
- package/src/runtime/registry.js +89 -0
- package/src/screen/cell-buffer.js +81 -0
- package/src/screen/events.js +263 -0
- package/src/screen/screen.js +118 -0
- package/src/screen/vt100.js +391 -0
- package/src/shell/shell.js +70 -0
- package/todo.txt +359 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readdir, readFile, rm, unlink } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, extname, join, sep } from "node:path";
|
|
4
|
+
import { fetchHttp, downloadFile } from "../platform/commands.js";
|
|
5
|
+
import { extractAndStrip } from "../platform/archive.js";
|
|
6
|
+
import { createLuaEngine } from "../lua/engine.js";
|
|
7
|
+
import { execCommand, runBackgroundShell, runCommand } from "../shell/shell.js";
|
|
8
|
+
import { Loc } from "../buffer/loc.js";
|
|
9
|
+
import { BTDefault, BTHelp, BTInfo, BTLog, BTRaw, BTScratch, byteOffset, BufferCore } from "../buffer/buffer.js";
|
|
10
|
+
import { MTError, MTInfo, MTWarning, newMessage, newMessageAtLine } from "../buffer/message.js";
|
|
11
|
+
|
|
12
|
+
const VALID_PLUGIN_NAME = /^[_A-Za-z0-9]+$/;
|
|
13
|
+
|
|
14
|
+
export class PluginManager {
|
|
15
|
+
constructor({ config, runtime, repoRoot }) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.runtime = runtime;
|
|
18
|
+
this.repoRoot = repoRoot;
|
|
19
|
+
this.plugins = [];
|
|
20
|
+
this.lua = null;
|
|
21
|
+
this.loadError = null;
|
|
22
|
+
this.lintLog = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async init() {
|
|
26
|
+
this.plugins = [];
|
|
27
|
+
await this.scanUserInit();
|
|
28
|
+
await this.scanUserPlugins();
|
|
29
|
+
await this.scanBuiltinPlugins();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async loadAll() {
|
|
33
|
+
try {
|
|
34
|
+
this.lua ??= await createLuaEngine();
|
|
35
|
+
await this.installMicroBridge();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
this.loadError = error;
|
|
38
|
+
return error;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const plugin of this.plugins) {
|
|
42
|
+
if (this.config.getGlobalOption(plugin.name) === false) continue;
|
|
43
|
+
try {
|
|
44
|
+
await plugin.load(this.lua, this.config);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
plugin.error = error;
|
|
47
|
+
this.loadError = error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return this.loadError;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async run(fn, ...args) {
|
|
54
|
+
for (const plugin of this.plugins) {
|
|
55
|
+
if (!plugin.loaded) continue;
|
|
56
|
+
if (this.config.getGlobalOption(plugin.name) === false) continue;
|
|
57
|
+
try {
|
|
58
|
+
await plugin.call(this.lua, fn, ...args);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
plugin.error = error;
|
|
61
|
+
this.loadError = error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return this.loadError;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async runBool(fn, ...args) {
|
|
68
|
+
let ok = true;
|
|
69
|
+
for (const plugin of this.plugins) {
|
|
70
|
+
if (!plugin.loaded) continue;
|
|
71
|
+
if (this.config.getGlobalOption(plugin.name) === false) continue;
|
|
72
|
+
try {
|
|
73
|
+
const result = await plugin.call(this.lua, fn, ...args);
|
|
74
|
+
if (result === false) ok = false;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
plugin.error = error;
|
|
77
|
+
this.loadError = error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return ok;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
list() {
|
|
84
|
+
return this.plugins.map((plugin) => ({
|
|
85
|
+
name: plugin.name,
|
|
86
|
+
builtin: plugin.builtin,
|
|
87
|
+
loaded: plugin.loaded,
|
|
88
|
+
error: plugin.error?.message ?? "",
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async scanUserInit() {
|
|
93
|
+
const initlua = join(this.config.configDir, "init.lua");
|
|
94
|
+
if (!existsSync(initlua)) return;
|
|
95
|
+
this.plugins.push(new Plugin({ name: "initlua", dirName: "initlua", srcs: [fileSource(initlua)], builtin: false }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async scanUserPlugins() {
|
|
99
|
+
const plugdir = join(this.config.configDir, "plug");
|
|
100
|
+
if (!existsSync(plugdir)) return;
|
|
101
|
+
const entries = await readdir(plugdir, { withFileTypes: true });
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (!entry.isDirectory()) continue;
|
|
104
|
+
const plugin = await scanPluginDirectory(join(plugdir, entry.name), entry.name, false);
|
|
105
|
+
if (plugin) this.plugins.push(plugin);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async scanBuiltinPlugins() {
|
|
110
|
+
const plugdir = join(this.repoRoot, "runtime", "plugins");
|
|
111
|
+
if (!existsSync(plugdir)) return;
|
|
112
|
+
const entries = await readdir(plugdir, { withFileTypes: true });
|
|
113
|
+
const userNames = new Set(this.plugins.map((plugin) => plugin.name));
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
if (!entry.isDirectory() || userNames.has(entry.name)) continue;
|
|
116
|
+
const plugin = await scanPluginDirectory(join(plugdir, entry.name), entry.name, true);
|
|
117
|
+
if (plugin) this.plugins.push(plugin);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Plugin management (CLI) ──────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
async pluginCommand(subcmd, args) {
|
|
124
|
+
switch (subcmd) {
|
|
125
|
+
case "list": return this._cmdList();
|
|
126
|
+
case "available":
|
|
127
|
+
case "avail": return this._cmdAvailable();
|
|
128
|
+
case "search": return this._cmdSearch(args);
|
|
129
|
+
case "install": return this._cmdInstall(args);
|
|
130
|
+
case "remove": return this._cmdRemove(args);
|
|
131
|
+
case "update": return this._cmdUpdate(args);
|
|
132
|
+
default:
|
|
133
|
+
console.error(`Invalid plugin command: ${subcmd}`);
|
|
134
|
+
console.error("Valid commands: list, available, search, install, remove, update");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_cmdList() {
|
|
140
|
+
const infos = this.plugins.map((p) => {
|
|
141
|
+
const ver = p.builtin ? "" : (this._installedVersion(p.name) ?? "");
|
|
142
|
+
const tag = p.builtin ? " (built-in)" : (ver ? ` ${ver}` : "");
|
|
143
|
+
return `${p.name}${tag}`;
|
|
144
|
+
});
|
|
145
|
+
if (infos.length === 0) {
|
|
146
|
+
console.log("No plugins found.");
|
|
147
|
+
} else {
|
|
148
|
+
console.log("Installed plugins:");
|
|
149
|
+
for (const line of infos) console.log(` ${line}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async _cmdAvailable() {
|
|
154
|
+
const packages = await this._fetchAllPackages((msg) => process.stderr.write(msg + "\n"));
|
|
155
|
+
if (packages.length === 0) {
|
|
156
|
+
console.log("No plugins available (check pluginchannels setting and network).");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
console.log("Available plugins:");
|
|
160
|
+
for (const pkg of packages) console.log(` ${pkg.Name}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async _cmdSearch(keywords) {
|
|
164
|
+
if (keywords.length === 0) {
|
|
165
|
+
console.error("Usage: -plugin search <keyword> [keyword...]");
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
const packages = await this._fetchAllPackages((msg) => process.stderr.write(msg + "\n"));
|
|
169
|
+
const results = packages.filter((pkg) => _matchPlugin(pkg, keywords));
|
|
170
|
+
console.log(`${results.length} plugin${results.length === 1 ? "" : "s"} found`);
|
|
171
|
+
for (const pkg of results) {
|
|
172
|
+
console.log("----------------");
|
|
173
|
+
console.log(_formatPlugin(pkg));
|
|
174
|
+
}
|
|
175
|
+
if (results.length > 0) console.log("----------------");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async _cmdInstall(names) {
|
|
179
|
+
if (names.length === 0) {
|
|
180
|
+
console.error("Usage: -plugin install <name> [name...]");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const packages = await this._fetchAllPackages((msg) => process.stderr.write(msg + "\n"));
|
|
184
|
+
let anyInstalled = false;
|
|
185
|
+
for (const name of names) {
|
|
186
|
+
const pkg = packages.find((p) => p.Name?.toLowerCase() === name.toLowerCase());
|
|
187
|
+
if (!pkg) { console.error(`Unknown plugin "${name}"`); continue; }
|
|
188
|
+
if (!pkg.Versions?.length) { console.error(`No versions available for "${name}"`); continue; }
|
|
189
|
+
const plugDir = join(this.config.configDir, "plug", pkg.Name);
|
|
190
|
+
if (existsSync(plugDir)) {
|
|
191
|
+
const cur = this._installedVersion(pkg.Name);
|
|
192
|
+
const latest = String(pkg.Versions[0].Version ?? "");
|
|
193
|
+
if (cur && _compareSemver(latest, cur) > 0) {
|
|
194
|
+
console.log(`${pkg.Name} is already installed but out-of-date: use '-plugin update ${pkg.Name}' to update`);
|
|
195
|
+
} else {
|
|
196
|
+
console.log(`${pkg.Name} is already installed`);
|
|
197
|
+
}
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
await this._downloadAndInstall(pkg, plugDir);
|
|
202
|
+
anyInstalled = true;
|
|
203
|
+
} catch (e) {
|
|
204
|
+
console.error(`Error installing ${pkg.Name}: ${e.message}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (anyInstalled) console.log("One or more plugins installed.");
|
|
208
|
+
else console.log("Nothing to install / update");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async _cmdRemove(names) {
|
|
212
|
+
if (names.length === 0) {
|
|
213
|
+
console.error("Usage: -plugin remove <name> [name...]");
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
let removed = [];
|
|
217
|
+
for (const name of names) {
|
|
218
|
+
if (name === "initlua") {
|
|
219
|
+
console.log("initlua cannot be removed, but can be disabled via settings.");
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const builtin = this.plugins.find((p) => p.name === name && p.builtin);
|
|
223
|
+
if (builtin) {
|
|
224
|
+
console.log(`${name} is a built-in plugin which cannot be removed, but can be disabled via settings.`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const plugDir = join(this.config.configDir, "plug", name);
|
|
228
|
+
if (!existsSync(plugDir)) {
|
|
229
|
+
console.log(`Plugin "${name}" is not installed`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
await rm(plugDir, { recursive: true, force: true });
|
|
233
|
+
removed.push(name);
|
|
234
|
+
}
|
|
235
|
+
if (removed.length > 0) console.log(`Removed ${removed.join(" ")}`);
|
|
236
|
+
else console.log("No plugins removed");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async _cmdUpdate(names) {
|
|
240
|
+
const packages = await this._fetchAllPackages((msg) => process.stderr.write(msg + "\n"));
|
|
241
|
+
const targets = names.length > 0
|
|
242
|
+
? names
|
|
243
|
+
: this.plugins.filter((p) => !p.builtin && p.name !== "initlua").map((p) => p.name);
|
|
244
|
+
if (targets.length === 0) { console.log("No user plugins to update."); return; }
|
|
245
|
+
let anyUpdated = false;
|
|
246
|
+
for (const name of targets) {
|
|
247
|
+
const pkg = packages.find((p) => p.Name?.toLowerCase() === name.toLowerCase());
|
|
248
|
+
if (!pkg) { console.log(`Unknown plugin "${name}"`); continue; }
|
|
249
|
+
if (!pkg.Versions?.length) continue;
|
|
250
|
+
const latest = String(pkg.Versions[0].Version ?? "");
|
|
251
|
+
const cur = this._installedVersion(pkg.Name);
|
|
252
|
+
if (cur && cur !== "unknown" && _compareSemver(latest, cur) <= 0) {
|
|
253
|
+
console.log(`${pkg.Name} is already up to date (${cur})`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const plugDir = join(this.config.configDir, "plug", pkg.Name);
|
|
257
|
+
if (existsSync(plugDir)) await rm(plugDir, { recursive: true, force: true });
|
|
258
|
+
try {
|
|
259
|
+
await this._downloadAndInstall(pkg, plugDir);
|
|
260
|
+
anyUpdated = true;
|
|
261
|
+
} catch (e) {
|
|
262
|
+
console.error(`Error updating ${pkg.Name}: ${e.message}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!anyUpdated) console.log("Nothing to update");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async _fetchAllPackages(onStatus) {
|
|
269
|
+
if (this._packages) return this._packages;
|
|
270
|
+
const channelUrls = [].concat(this.config.getGlobalOption("pluginchannels") ?? []).map(String);
|
|
271
|
+
const repoUrls = [].concat(this.config.getGlobalOption("pluginrepos") ?? []).map(String);
|
|
272
|
+
const packages = [];
|
|
273
|
+
const seen = new Set();
|
|
274
|
+
|
|
275
|
+
for (const channelUrl of channelUrls) {
|
|
276
|
+
onStatus?.(`Fetching channel: ${channelUrl}`);
|
|
277
|
+
try {
|
|
278
|
+
const data = Bun.JSON5.parse(await fetchHttp(channelUrl));
|
|
279
|
+
if (Array.isArray(data)) repoUrls.push(...data.map(String));
|
|
280
|
+
} catch (e) {
|
|
281
|
+
onStatus?.(`Warning: failed to fetch channel ${channelUrl}: ${e.message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const repoUrl of repoUrls) {
|
|
285
|
+
onStatus?.(`Fetching repository: ${repoUrl}`);
|
|
286
|
+
try {
|
|
287
|
+
const data = Bun.JSON5.parse(await fetchHttp(repoUrl));
|
|
288
|
+
const list = Array.isArray(data) ? data : [data];
|
|
289
|
+
for (const pkg of list) {
|
|
290
|
+
if (!pkg?.Name || seen.has(pkg.Name)) continue;
|
|
291
|
+
seen.add(pkg.Name);
|
|
292
|
+
packages.push(pkg);
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
onStatus?.(`Warning: failed to fetch repo ${repoUrl}: ${e.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
this._packages = packages;
|
|
299
|
+
return packages;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async _downloadAndInstall(pkg, plugDir) {
|
|
303
|
+
const version = pkg.Versions[0];
|
|
304
|
+
const versionStr = String(version.Version ?? "");
|
|
305
|
+
console.log(`Downloading "${pkg.Name}" (${versionStr}) from "${version.Url}"`);
|
|
306
|
+
const tmpFile = join(this.config.configDir, `_tmp_${pkg.Name}.zip`);
|
|
307
|
+
try {
|
|
308
|
+
await downloadFile(String(version.Url), tmpFile);
|
|
309
|
+
await extractAndStrip(tmpFile, plugDir);
|
|
310
|
+
// Write version marker for future update checks
|
|
311
|
+
await Bun.write(join(plugDir, "_installed_version.txt"), versionStr);
|
|
312
|
+
} finally {
|
|
313
|
+
if (existsSync(tmpFile)) await unlink(tmpFile).catch(() => {});
|
|
314
|
+
}
|
|
315
|
+
console.log(`Installed ${pkg.Name} ${versionStr}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
_installedVersion(name) {
|
|
319
|
+
const plugDir = join(this.config.configDir, "plug", name);
|
|
320
|
+
if (!existsSync(plugDir)) return null;
|
|
321
|
+
// Check version marker written by _downloadAndInstall
|
|
322
|
+
const marker = join(plugDir, "_installed_version.txt");
|
|
323
|
+
if (existsSync(marker)) {
|
|
324
|
+
try { return readFileSync(marker, "utf8").trim(); } catch {}
|
|
325
|
+
}
|
|
326
|
+
// Fallback: scan JSON files for version info
|
|
327
|
+
try {
|
|
328
|
+
for (const f of readdirSync(plugDir)) {
|
|
329
|
+
if (!f.endsWith(".json")) continue;
|
|
330
|
+
try {
|
|
331
|
+
const data = JSON.parse(readFileSync(join(plugDir, f), "utf8"));
|
|
332
|
+
const list = Array.isArray(data) ? data : [data];
|
|
333
|
+
const ver = list[0]?.Versions?.[0]?.Version;
|
|
334
|
+
if (ver) return String(ver);
|
|
335
|
+
} catch {}
|
|
336
|
+
}
|
|
337
|
+
} catch {}
|
|
338
|
+
return "unknown";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── End plugin management ────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
async installMicroBridge() {
|
|
344
|
+
this.lua.setGlobal("import", (pkg) => this.importPackage(pkg));
|
|
345
|
+
await this.lua.doString(`
|
|
346
|
+
if module == nil then
|
|
347
|
+
function module(name, mode)
|
|
348
|
+
local t = _G[name] or {}
|
|
349
|
+
_G[name] = t
|
|
350
|
+
if mode == package.seeall then
|
|
351
|
+
setmetatable(t, { __index = _G })
|
|
352
|
+
end
|
|
353
|
+
_ENV = t
|
|
354
|
+
return t
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
-- gopher-lua compat: PUC Lua 5.4 rejects %x (x non-digit, non-%) in gsub
|
|
359
|
+
-- replacement strings; gopher-lua treats them as literal characters.
|
|
360
|
+
-- Wrap string.gsub so that such sequences are escaped before handing off.
|
|
361
|
+
do
|
|
362
|
+
local _gsub = string.gsub
|
|
363
|
+
function string.gsub(s, pattern, repl, n)
|
|
364
|
+
if type(repl) == "string" then
|
|
365
|
+
repl = _gsub(repl, "%%([^%d%%])", function(c) return "%%" .. c end)
|
|
366
|
+
end
|
|
367
|
+
if n ~= nil then return _gsub(s, pattern, repl, n) end
|
|
368
|
+
return _gsub(s, pattern, repl)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
`, "micro-compat");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
importPackage(pkg) {
|
|
375
|
+
if (pkg === "micro/config") return this.configPackage();
|
|
376
|
+
if (pkg === "runtime") return { GOOS: process.platform, GOARCH: process.arch };
|
|
377
|
+
if (pkg === "micro") return { Log: console.log, TermMessage: console.log, TermError: console.error, SetStatusInfoFn: (name) => { this.statusInfoFns ??= new Set(); this.statusInfoFns.add(name); }, CurPane: () => this.curPaneAdapter ?? null };
|
|
378
|
+
if (pkg === "micro/shell") return this.shellPackage();
|
|
379
|
+
if (pkg === "micro/buffer") return this.bufferPackage();
|
|
380
|
+
if (pkg === "micro/util") return this.utilPackage();
|
|
381
|
+
if (pkg === "humanize") return { Bytes: humanizeBytes };
|
|
382
|
+
if (pkg === "strings") return { TrimSpace: (s) => String(s).trim() };
|
|
383
|
+
if (pkg === "path/filepath" || pkg === "filepath") return { Dir: dirname, Base: basename, Split: splitPath };
|
|
384
|
+
if (pkg === "os") return { PathSeparator: sep, Stat: (path) => ({ _unsupported: true, path }) };
|
|
385
|
+
return {};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
configPackage() {
|
|
389
|
+
return {
|
|
390
|
+
RegisterCommonOption: (plugin, option, value) => this.config.registerCommonOption(plugin, option, value),
|
|
391
|
+
RegisterGlobalOption: (option, value) => this.config.registerGlobalOption(option, value),
|
|
392
|
+
GetGlobalOption: (option) => this.config.getGlobalOption(option),
|
|
393
|
+
SetGlobalOptionNative: (option, value) => this.config.setGlobalOptionNative(option, value),
|
|
394
|
+
MakeCommand: (name, fn) => { this.commands ??= new Map(); this.commands.set(name, fn); },
|
|
395
|
+
TryBindKey: () => false,
|
|
396
|
+
NoComplete: () => [],
|
|
397
|
+
FileComplete: () => [],
|
|
398
|
+
HelpComplete: () => [],
|
|
399
|
+
OptionComplete: () => [],
|
|
400
|
+
OptionValueComplete: () => [],
|
|
401
|
+
Reload: () => undefined,
|
|
402
|
+
AddRuntimeFileFromMemory: (kind, name, data) => this.runtime.addMemoryFile(kind, name, data),
|
|
403
|
+
AddRuntimeFile: (plugin, kind, relpath) => this.addPluginRuntimeFile(plugin, kind, relpath),
|
|
404
|
+
ListRuntimeFiles: (kind) => this.runtime.list(kind).map((file) => file.name),
|
|
405
|
+
ReadRuntimeFile: async (kind, name) => (await this.runtime.find(kind, name)?.text()) ?? "",
|
|
406
|
+
NewRTFiletype: () => this.runtime.files.push([]) - 1,
|
|
407
|
+
RTColorscheme: 0, RTSyntax: 1, RTHelp: 2, RTPlugin: 3, RTSyntaxHeader: 4,
|
|
408
|
+
ConfigDir: this.config.configDir,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
addPluginRuntimeFile(plugin, kind, relpath) {
|
|
413
|
+
const owner = this.plugins.find((p) => p.name === plugin);
|
|
414
|
+
if (!owner) return;
|
|
415
|
+
const path = join(this.repoRoot, "runtime", "plugins", owner.dirName, relpath);
|
|
416
|
+
this.runtime.files[kind].push({ name: basename(path, extname(path)), path, text: async () => await readFile(path, "utf8") });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
shellPackage() {
|
|
420
|
+
// JobSpawn: mirrors Go's shell.JobSpawn(cmd, args, onStdout, onStderr, onExit, ...userArgs).
|
|
421
|
+
// Spawns cmd asynchronously; calls onExit(output, userArgsTable) on exit.
|
|
422
|
+
// onStdout/onStderr called per-chunk if provided (linter passes nil for both).
|
|
423
|
+
// userArgs is a plain JS array. wasmoon's ProxyTypeExtension wraps it with __index that
|
|
424
|
+
// applies (key - 1) for numeric keys, so Lua's args[1] → array[0], args[2] → array[1], etc.
|
|
425
|
+
// This matches Go/luar's 1-indexed Lua table convention without any manual conversion.
|
|
426
|
+
const spawnJob = async (argv, onStdout, onStderr, onExit, userArgs) => {
|
|
427
|
+
let proc;
|
|
428
|
+
try {
|
|
429
|
+
proc = Bun.spawn(argv, { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
|
|
430
|
+
} catch (err) {
|
|
431
|
+
this.lintLog.push(`[JobSpawn] spawn "${argv[0]}": ${err.message}`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const collectStream = async (stream, onChunk) => {
|
|
435
|
+
const chunks = [];
|
|
436
|
+
if (stream) {
|
|
437
|
+
const reader = stream.getReader();
|
|
438
|
+
const dec = new TextDecoder();
|
|
439
|
+
while (true) {
|
|
440
|
+
const { done, value } = await reader.read();
|
|
441
|
+
if (done) break;
|
|
442
|
+
const text = dec.decode(value, { stream: true });
|
|
443
|
+
chunks.push(text);
|
|
444
|
+
if (typeof onChunk === "function") await onChunk(text, userArgs);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return chunks.join("");
|
|
448
|
+
};
|
|
449
|
+
const [out, err] = await Promise.all([
|
|
450
|
+
collectStream(proc.stdout, onStdout),
|
|
451
|
+
collectStream(proc.stderr, onStderr),
|
|
452
|
+
]);
|
|
453
|
+
await proc.exited;
|
|
454
|
+
const output = out + err;
|
|
455
|
+
if (output.trim()) this.lintLog.push(`[${argv[0]}]\n${output.trim()}`);
|
|
456
|
+
if (typeof onExit === "function") await onExit(output, userArgs);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
ExecCommand: (name, ...args) => execCommand(name, args),
|
|
461
|
+
RunCommand: (input) => runCommand(input),
|
|
462
|
+
RunBackgroundShell: (input) => runBackgroundShell(input),
|
|
463
|
+
// JobSpawn(cmd, args, onStdout, onStderr, onExit, ...userArgs)
|
|
464
|
+
JobSpawn: (cmd, luaArgs, onStdout, onStderr, onExit, ...userArgs) => {
|
|
465
|
+
const argv = [String(cmd), ...luaSeqToArray(luaArgs).map(String)];
|
|
466
|
+
spawnJob(argv, onStdout ?? null, onStderr ?? null, onExit ?? null, userArgs)
|
|
467
|
+
.catch((e) => this.lintLog.push(`[JobSpawn] ${argv[0]}: ${e.message}`));
|
|
468
|
+
},
|
|
469
|
+
// JobStart(cmdStr, onStdout, onStderr, onExit, ...userArgs) — shell-split single string
|
|
470
|
+
JobStart: (cmdStr, onStdout, onStderr, onExit, ...userArgs) => {
|
|
471
|
+
const argv = String(cmdStr ?? "").trim().split(/\s+/).filter(Boolean);
|
|
472
|
+
if (!argv.length) return;
|
|
473
|
+
spawnJob(argv, onStdout ?? null, onStderr ?? null, onExit ?? null, userArgs)
|
|
474
|
+
.catch((e) => this.lintLog.push(`[JobStart] ${argv[0]}: ${e.message}`));
|
|
475
|
+
},
|
|
476
|
+
JobStop: () => undefined,
|
|
477
|
+
JobSend: () => undefined,
|
|
478
|
+
RunInteractiveShell: () => ["", "shell bridge not implemented"],
|
|
479
|
+
RunTermEmulator: () => undefined,
|
|
480
|
+
TermEmuSupported: () => true,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
bufferPackage() {
|
|
485
|
+
return {
|
|
486
|
+
Loc: (x, y) => new Loc(x, y),
|
|
487
|
+
NewMessage: newMessage,
|
|
488
|
+
NewMessageAtLine: newMessageAtLine,
|
|
489
|
+
NewBuffer: (text, path) => new BufferCore({ text, path }),
|
|
490
|
+
NewBufferFromFile: async (path) => new BufferCore({ text: await Bun.file(path).text(), path }),
|
|
491
|
+
MTInfo, MTWarning, MTError,
|
|
492
|
+
BTDefault: BTDefault.Kind, BTHelp: BTHelp.Kind, BTLog: BTLog.Kind, BTScratch: BTScratch.Kind, BTRaw: BTRaw.Kind, BTInfo: BTInfo.Kind,
|
|
493
|
+
ByteOffset: byteOffset,
|
|
494
|
+
Log: console.log,
|
|
495
|
+
LogBuf: () => null,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
utilPackage() {
|
|
500
|
+
return {
|
|
501
|
+
RuneAt: (s, i) => Array.from(String(s))[i] ?? "",
|
|
502
|
+
GetLeadingWhitespace: (s) => String(s).match(/^\s*/)?.[0] ?? "",
|
|
503
|
+
IsWordChar: (ch) => /^[A-Za-z0-9_]$/.test(String(ch)),
|
|
504
|
+
String: (v) => String(v),
|
|
505
|
+
RuneStr: (v) => String(v),
|
|
506
|
+
CharacterCountInString: (s) => Array.from(String(s)).length,
|
|
507
|
+
Version: "0.1.0-bun",
|
|
508
|
+
SemVersion: "0.1.0",
|
|
509
|
+
HttpRequest: async (url) => await fetchHttp(String(url)),
|
|
510
|
+
Unzip: () => { throw new Error("Unzip bridge is not implemented here; use platform/archive.js"); },
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function _matchPlugin(pkg, keywords) {
|
|
516
|
+
return keywords.every((kw) => {
|
|
517
|
+
const k = kw.toLowerCase();
|
|
518
|
+
return (
|
|
519
|
+
pkg.Name?.toLowerCase().includes(k) ||
|
|
520
|
+
pkg.Description?.toLowerCase().includes(k) ||
|
|
521
|
+
pkg.Tags?.some((t) => t.toLowerCase() === k)
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function _formatPlugin(pkg) {
|
|
527
|
+
let out = `Plugin: ${pkg.Name}`;
|
|
528
|
+
if (pkg.Author) out += `\nAuthor: ${pkg.Author}`;
|
|
529
|
+
if (pkg.Description) out += `\n\n${pkg.Description}`;
|
|
530
|
+
if (pkg.Versions?.length) out += `\nLatest: ${pkg.Versions[0].Version}`;
|
|
531
|
+
return out;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function _compareSemver(a, b) {
|
|
535
|
+
const pa = String(a).replace(/^v/, "").split(".").map((n) => parseInt(n) || 0);
|
|
536
|
+
const pb = String(b).replace(/^v/, "").split(".").map((n) => parseInt(n) || 0);
|
|
537
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
538
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
539
|
+
if (d !== 0) return d;
|
|
540
|
+
}
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
class Plugin {
|
|
545
|
+
constructor({ name, dirName, srcs, info = null, builtin = false }) {
|
|
546
|
+
this.name = name;
|
|
547
|
+
this.dirName = dirName;
|
|
548
|
+
this.srcs = srcs;
|
|
549
|
+
this.info = info;
|
|
550
|
+
this.builtin = builtin;
|
|
551
|
+
this.loaded = false;
|
|
552
|
+
this.error = null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async load(lua, config) {
|
|
556
|
+
if (config.getGlobalOption(this.name) === false) return;
|
|
557
|
+
for (const src of this.srcs) {
|
|
558
|
+
const data = await src.text();
|
|
559
|
+
await lua.doString(`local _ENV = module("${this.name}", package.seeall)\n${data}`, src.name);
|
|
560
|
+
}
|
|
561
|
+
this.loaded = true;
|
|
562
|
+
config.registerGlobalOption(this.name, true);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async call(lua, fn, ...args) {
|
|
566
|
+
for (let i = 0; i < args.length; i++) lua.setGlobal(`__micro_arg${i + 1}`, args[i]);
|
|
567
|
+
const argv = args.map((_, i) => `__micro_arg${i + 1}`).join(", ");
|
|
568
|
+
return lua.doString(`if ${this.name}.${fn} ~= nil then return ${this.name}.${fn}(${argv}) end`, `${this.name}.${fn}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function scanPluginDirectory(dir, defaultName, builtin) {
|
|
573
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
574
|
+
const srcs = [];
|
|
575
|
+
let info = null;
|
|
576
|
+
let name = defaultName;
|
|
577
|
+
for (const entry of entries) {
|
|
578
|
+
const fullPath = join(dir, entry.name);
|
|
579
|
+
if (entry.isFile() && entry.name.endsWith(".lua")) {
|
|
580
|
+
srcs.push(fileSource(fullPath));
|
|
581
|
+
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
582
|
+
const parsed = JSON.parse(await readFile(fullPath, "utf8"));
|
|
583
|
+
info = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
584
|
+
if (info?.Name) name = info.Name;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (!VALID_PLUGIN_NAME.test(name) || srcs.length === 0) return null;
|
|
588
|
+
return new Plugin({ name, dirName: basename(dir), srcs, info, builtin });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function fileSource(path) {
|
|
592
|
+
return {
|
|
593
|
+
name: basename(path),
|
|
594
|
+
path,
|
|
595
|
+
text: () => readFile(path, "utf8"),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function humanizeBytes(bytes) {
|
|
600
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
601
|
+
let value = Number(bytes);
|
|
602
|
+
let unit = 0;
|
|
603
|
+
while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit++; }
|
|
604
|
+
return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function splitPath(path) {
|
|
608
|
+
return [dirname(path), basename(path)];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Lua sequential table arrives from wasmoon as a 0-indexed JS array.
|
|
612
|
+
// This helper normalises it to a plain JS array regardless of form.
|
|
613
|
+
function luaSeqToArray(table) {
|
|
614
|
+
if (!table) return [];
|
|
615
|
+
if (Array.isArray(table)) return table;
|
|
616
|
+
if (typeof table !== "object") return [];
|
|
617
|
+
const keys = Object.keys(table).map(Number).filter((n) => Number.isInteger(n) && n >= 0).sort((a, b) => a - b);
|
|
618
|
+
return keys.map((k) => table[k]);
|
|
619
|
+
}
|