@motrix/plugin-cli 2.0.0 → 2.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/README.md +35 -9
- package/dist/bin/motrix-plugin.js +117 -50
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -35,7 +35,16 @@ motrix-plugin validate
|
|
|
35
35
|
```
|
|
36
36
|
Validate `motrix-plugin.json` against
|
|
37
37
|
[`@motrix/plugin-manifest-schema`](https://www.npmjs.com/package/@motrix/plugin-manifest-schema) —
|
|
38
|
-
the same schema the Motrix host uses when installing a plugin.
|
|
38
|
+
the same schema the Motrix host uses when installing a plugin. Beyond the
|
|
39
|
+
schema this also applies the rules a schema cannot express:
|
|
40
|
+
|
|
41
|
+
- **hooks ⇒ hostPermissions** — contributing any hook requires at least one
|
|
42
|
+
`hostPermissions` entry, or the host refuses to install the plugin
|
|
43
|
+
- **locale coverage** — every `%key%` the manifest interpolates must exist in
|
|
44
|
+
`en-US`, including the ones under `contributes.configuration`
|
|
45
|
+
|
|
46
|
+
`pack` runs this identical gate, so the two commands can never disagree about
|
|
47
|
+
whether a plugin is shippable.
|
|
39
48
|
|
|
40
49
|
### Schema versioning
|
|
41
50
|
|
|
@@ -49,22 +58,39 @@ staying current on the CLI keeps you current on the schema too.
|
|
|
49
58
|
```bash
|
|
50
59
|
motrix-plugin lint
|
|
51
60
|
```
|
|
52
|
-
Static checks
|
|
53
|
-
(
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
Static checks on the **packed bundle**, so run `pack` first: top-level
|
|
62
|
+
side-effect detection (guest code must not run effectful code at module
|
|
63
|
+
scope), a size warning as the bundle approaches the 1 MiB cap, and advisories
|
|
64
|
+
for `invokesCommands` entries whose callee may not be installed.
|
|
56
65
|
|
|
57
66
|
```bash
|
|
58
67
|
motrix-plugin validate-host-permissions
|
|
59
68
|
```
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
Reviews `hostPermissions` in more depth than `validate` does: each pattern is
|
|
70
|
+
parsed with the host's own match-pattern grammar, and broad grants
|
|
71
|
+
(`<all_urls>`, `https://*/*`) are reported as warnings so you can narrow them.
|
|
62
72
|
|
|
63
73
|
```bash
|
|
64
74
|
motrix-plugin pack
|
|
65
75
|
```
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
Produce the `.moext` distributable at `dist/<id>-<version>.moext`.
|
|
77
|
+
|
|
78
|
+
Every gate — schema, hooks ⇒ hostPermissions, locale coverage — runs *before*
|
|
79
|
+
esbuild writes anything, so a rejected plugin leaves no half-built bundle
|
|
80
|
+
behind and a `.moext` the host would refuse to install cannot be created in the
|
|
81
|
+
first place. The bundle is then checked against the 1 MiB cap and the archive
|
|
82
|
+
against 5 MiB.
|
|
83
|
+
|
|
84
|
+
The archive is **reproducible**: entries are written in sorted order with
|
|
85
|
+
pinned timestamps and modes, so the same source tree always yields the same
|
|
86
|
+
bytes, on any machine and in any timezone. `pack` prints the resulting sha256 —
|
|
87
|
+
that is the value a registry entry's `package.sha256` needs, and re-packing an
|
|
88
|
+
unchanged tree must reprint it.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
Packed → …/dist/acme.demo-1.0.0.moext (4540 bytes)
|
|
92
|
+
sha256 5d69eaf3bb44992952beafc67d818ff376b8478de4b32b936ce5e603b963605d
|
|
93
|
+
```
|
|
68
94
|
|
|
69
95
|
```bash
|
|
70
96
|
pnpm create motrix-plugin my-plugin [template]
|
|
@@ -14954,6 +14954,73 @@ var ManifestSchema = external_exports.object({
|
|
|
14954
14954
|
contributes: ContributesSchema,
|
|
14955
14955
|
l10n: external_exports.string().max(256).optional()
|
|
14956
14956
|
}).strict();
|
|
14957
|
+
var HOST_PERMISSIONS_REQUIRED_FOR_HOOKS = "plugin.manifest.host_permissions_required_for_hooks";
|
|
14958
|
+
var LOCALE_MISSING_EN_US = "plugin.manifest.locale.missing_en_us";
|
|
14959
|
+
var LOCALE_MISSING_KEYS = "plugin.manifest.locale.missing_keys";
|
|
14960
|
+
function requireHostPermissionsForHooks(manifest, id = "<unknown>") {
|
|
14961
|
+
const hooks = manifest.contributes?.hooks;
|
|
14962
|
+
const hasHooks = hooks != null && Object.keys(hooks).length > 0;
|
|
14963
|
+
if (!hasHooks) return;
|
|
14964
|
+
const hostPermissions = Array.isArray(manifest.hostPermissions) ? manifest.hostPermissions : [];
|
|
14965
|
+
if (hostPermissions.length === 0) {
|
|
14966
|
+
throw new Error(
|
|
14967
|
+
`${id}: ${HOST_PERMISSIONS_REQUIRED_FOR_HOOKS}: declared hooks require at least one hostPermissions entry`
|
|
14968
|
+
);
|
|
14969
|
+
}
|
|
14970
|
+
}
|
|
14971
|
+
function validateManifest(manifest, id) {
|
|
14972
|
+
const label = id ?? (typeof manifest?.id === "string" ? manifest.id : "<unknown>");
|
|
14973
|
+
let parsed;
|
|
14974
|
+
try {
|
|
14975
|
+
parsed = ManifestSchema.parse(manifest);
|
|
14976
|
+
} catch (err) {
|
|
14977
|
+
const issues = err.issues;
|
|
14978
|
+
const detail = Array.isArray(issues) ? issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ") : err?.message ?? String(err);
|
|
14979
|
+
throw new Error(`${label}: invalid motrix-plugin.json \u2014 ${detail}`);
|
|
14980
|
+
}
|
|
14981
|
+
requireHostPermissionsForHooks(parsed, label);
|
|
14982
|
+
return parsed;
|
|
14983
|
+
}
|
|
14984
|
+
function flattenLocaleKeys(source, prefix = "", out = /* @__PURE__ */ new Set()) {
|
|
14985
|
+
for (const [key, value] of Object.entries(source)) {
|
|
14986
|
+
const path9 = prefix ? `${prefix}.${key}` : key;
|
|
14987
|
+
if (typeof value === "string") out.add(path9);
|
|
14988
|
+
else if (value && typeof value === "object") {
|
|
14989
|
+
flattenLocaleKeys(value, path9, out);
|
|
14990
|
+
}
|
|
14991
|
+
}
|
|
14992
|
+
return out;
|
|
14993
|
+
}
|
|
14994
|
+
function collectLocaleRefs(manifest) {
|
|
14995
|
+
const found = /* @__PURE__ */ new Set();
|
|
14996
|
+
const walk = (node) => {
|
|
14997
|
+
if (typeof node === "string") {
|
|
14998
|
+
const m = node.match(/^%([^%]+)%$/);
|
|
14999
|
+
if (m) found.add(m[1]);
|
|
15000
|
+
return;
|
|
15001
|
+
}
|
|
15002
|
+
if (Array.isArray(node)) {
|
|
15003
|
+
for (const item of node) walk(item);
|
|
15004
|
+
return;
|
|
15005
|
+
}
|
|
15006
|
+
if (node && typeof node === "object") {
|
|
15007
|
+
for (const value of Object.values(node)) walk(value);
|
|
15008
|
+
}
|
|
15009
|
+
};
|
|
15010
|
+
walk(manifest);
|
|
15011
|
+
return [...found];
|
|
15012
|
+
}
|
|
15013
|
+
function checkLocaleCoverage(manifest, baseLocale) {
|
|
15014
|
+
if (!manifest.l10n) return;
|
|
15015
|
+
if (baseLocale == null) throw new Error(LOCALE_MISSING_EN_US);
|
|
15016
|
+
const refs = collectLocaleRefs(manifest);
|
|
15017
|
+
if (refs.length === 0) return;
|
|
15018
|
+
const available = flattenLocaleKeys(baseLocale);
|
|
15019
|
+
const missing = refs.filter((key) => !available.has(key));
|
|
15020
|
+
if (missing.length > 0) {
|
|
15021
|
+
throw new Error(`${LOCALE_MISSING_KEYS}: ${missing.sort().join(", ")}`);
|
|
15022
|
+
}
|
|
15023
|
+
}
|
|
14957
15024
|
|
|
14958
15025
|
// src/commands/lint.ts
|
|
14959
15026
|
async function lint(projectDir) {
|
|
@@ -14992,6 +15059,7 @@ async function lint(projectDir) {
|
|
|
14992
15059
|
}
|
|
14993
15060
|
|
|
14994
15061
|
// src/commands/pack.ts
|
|
15062
|
+
import { createHash } from "crypto";
|
|
14995
15063
|
import { createWriteStream } from "fs";
|
|
14996
15064
|
import { mkdir as mkdir2, readdir as readdir2, readFile as readFile4, stat as stat3, unlink } from "fs/promises";
|
|
14997
15065
|
import path6 from "path";
|
|
@@ -15003,52 +15071,32 @@ import { readFile as readFile3 } from "fs/promises";
|
|
|
15003
15071
|
import path5 from "path";
|
|
15004
15072
|
async function validateLocaleCoverage(projectDir, manifest) {
|
|
15005
15073
|
if (!manifest.l10n) return;
|
|
15006
|
-
|
|
15007
|
-
let enUS;
|
|
15074
|
+
let baseLocale = null;
|
|
15008
15075
|
try {
|
|
15009
|
-
|
|
15010
|
-
|
|
15011
|
-
throw new Error("plugin.manifest.locale.missing_en_us");
|
|
15012
|
-
}
|
|
15013
|
-
const keys = /* @__PURE__ */ new Set();
|
|
15014
|
-
function walk(o, prefix = "") {
|
|
15015
|
-
for (const [k, v] of Object.entries(o)) {
|
|
15016
|
-
const p = prefix ? `${prefix}.${k}` : k;
|
|
15017
|
-
if (typeof v === "string") keys.add(p);
|
|
15018
|
-
else if (v && typeof v === "object") walk(v, p);
|
|
15019
|
-
}
|
|
15020
|
-
}
|
|
15021
|
-
walk(enUS);
|
|
15022
|
-
const collected = [];
|
|
15023
|
-
const visit = (s) => {
|
|
15024
|
-
if (typeof s === "string") {
|
|
15025
|
-
const m = s.match(/^%([^%]+)%$/);
|
|
15026
|
-
if (m) collected.push(m[1]);
|
|
15027
|
-
}
|
|
15028
|
-
};
|
|
15029
|
-
visit(manifest.name);
|
|
15030
|
-
visit(manifest.description);
|
|
15031
|
-
for (const c of manifest.contributes?.commands ?? []) visit(c.title);
|
|
15032
|
-
const missing = collected.filter((k) => !keys.has(k));
|
|
15033
|
-
if (missing.length > 0) {
|
|
15034
|
-
throw new Error(
|
|
15035
|
-
`plugin.manifest.locale.missing_keys: ${missing.join(", ")}`
|
|
15076
|
+
baseLocale = JSON.parse(
|
|
15077
|
+
await readFile3(path5.join(projectDir, manifest.l10n, "en-US.json"), "utf8")
|
|
15036
15078
|
);
|
|
15079
|
+
} catch {
|
|
15080
|
+
baseLocale = null;
|
|
15037
15081
|
}
|
|
15082
|
+
checkLocaleCoverage(manifest, baseLocale);
|
|
15038
15083
|
}
|
|
15039
15084
|
|
|
15040
15085
|
// src/commands/pack.ts
|
|
15041
15086
|
var BUNDLE_MAX = 1024 * 1024;
|
|
15042
15087
|
var TOTAL_MAX = 5 * 1024 * 1024;
|
|
15088
|
+
var DOS_EPOCH = new Date(1980, 0, 1, 0, 0, 0);
|
|
15089
|
+
var ENTRY_MODE = 33188;
|
|
15043
15090
|
async function pack(opts) {
|
|
15044
15091
|
const projectDir = path6.resolve(opts.projectDir);
|
|
15045
15092
|
const manifestPath = path6.join(
|
|
15046
15093
|
projectDir,
|
|
15047
15094
|
opts.manifest ?? "motrix-plugin.json"
|
|
15048
15095
|
);
|
|
15049
|
-
const manifest =
|
|
15096
|
+
const manifest = validateManifest(
|
|
15050
15097
|
JSON.parse(await readFile4(manifestPath, "utf8"))
|
|
15051
15098
|
);
|
|
15099
|
+
await validateLocaleCoverage(projectDir, manifest);
|
|
15052
15100
|
const entry = opts.entry ?? "src/index.ts";
|
|
15053
15101
|
const outDir = path6.join(projectDir, opts.outDir ?? "dist");
|
|
15054
15102
|
await mkdir2(outDir, { recursive: true });
|
|
@@ -15072,41 +15120,54 @@ async function pack(opts) {
|
|
|
15072
15120
|
`bundle ${bundle.byteLength} bytes > ${BUNDLE_MAX} byte cap`
|
|
15073
15121
|
);
|
|
15074
15122
|
}
|
|
15075
|
-
await validateLocaleCoverage(projectDir, manifest);
|
|
15076
15123
|
const moextOut = path6.join(outDir, `${manifest.id}-${manifest.version}.moext`);
|
|
15077
|
-
const
|
|
15078
|
-
|
|
15079
|
-
|
|
15124
|
+
const entries = [
|
|
15125
|
+
{ abs: manifestPath, rel: "motrix-plugin.json" },
|
|
15126
|
+
{ abs: bundlePath, rel: "dist/plugin.js" }
|
|
15127
|
+
];
|
|
15080
15128
|
for (const f of ["icon.png", "LICENSE", "CHANGELOG.md"]) {
|
|
15081
15129
|
try {
|
|
15082
15130
|
await stat3(path6.join(projectDir, f));
|
|
15083
|
-
|
|
15131
|
+
entries.push({ abs: path6.join(projectDir, f), rel: f });
|
|
15084
15132
|
} catch {
|
|
15085
15133
|
}
|
|
15086
15134
|
}
|
|
15087
15135
|
if (manifest.l10n) {
|
|
15136
|
+
const localeDir = path6.join(projectDir, manifest.l10n);
|
|
15137
|
+
let localeEntries = [];
|
|
15088
15138
|
try {
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15139
|
+
localeEntries = await readdir2(localeDir);
|
|
15140
|
+
} catch (err) {
|
|
15141
|
+
if (err?.code !== "ENOENT") throw err;
|
|
15142
|
+
}
|
|
15143
|
+
for (const name of localeEntries) {
|
|
15144
|
+
const abs = path6.join(localeDir, name);
|
|
15145
|
+
if (!(await stat3(abs)).isFile()) continue;
|
|
15146
|
+
entries.push({ abs, rel: `${manifest.l10n}/${name}` });
|
|
15096
15147
|
}
|
|
15097
15148
|
}
|
|
15149
|
+
entries.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
15150
|
+
const z2 = new yazl.ZipFile();
|
|
15151
|
+
for (const { abs, rel } of entries) {
|
|
15152
|
+
z2.addFile(abs, rel, { mtime: DOS_EPOCH, mode: ENTRY_MODE });
|
|
15153
|
+
}
|
|
15098
15154
|
z2.end();
|
|
15099
15155
|
await new Promise((res, rej) => {
|
|
15100
15156
|
z2.on("error", rej);
|
|
15101
15157
|
const ws = createWriteStream(moextOut);
|
|
15102
15158
|
z2.outputStream.pipe(ws).on("close", () => res()).on("error", rej);
|
|
15103
15159
|
});
|
|
15104
|
-
const
|
|
15105
|
-
if (
|
|
15160
|
+
const archive = await readFile4(moextOut);
|
|
15161
|
+
if (archive.byteLength > TOTAL_MAX) {
|
|
15106
15162
|
await unlink(moextOut);
|
|
15107
|
-
throw new Error(`moext ${
|
|
15163
|
+
throw new Error(`moext ${archive.byteLength} bytes > ${TOTAL_MAX} byte cap`);
|
|
15108
15164
|
}
|
|
15109
|
-
return {
|
|
15165
|
+
return {
|
|
15166
|
+
outFile: moextOut,
|
|
15167
|
+
bundleSize: bundle.byteLength,
|
|
15168
|
+
totalSize: archive.byteLength,
|
|
15169
|
+
sha256: createHash("sha256").update(archive).digest("hex")
|
|
15170
|
+
};
|
|
15110
15171
|
}
|
|
15111
15172
|
|
|
15112
15173
|
// src/commands/validate.ts
|
|
@@ -15119,7 +15180,7 @@ async function validate(projectDir) {
|
|
|
15119
15180
|
path7.join(projectDir, "motrix-plugin.json"),
|
|
15120
15181
|
"utf8"
|
|
15121
15182
|
);
|
|
15122
|
-
const m =
|
|
15183
|
+
const m = validateManifest(JSON.parse(raw));
|
|
15123
15184
|
if (isReservedPublisher(m.id)) {
|
|
15124
15185
|
throw new Error(`publisher name is reserved: "${m.id}"`);
|
|
15125
15186
|
}
|
|
@@ -15192,7 +15253,7 @@ async function validateHostPermissions(projectDir) {
|
|
|
15192
15253
|
}
|
|
15193
15254
|
|
|
15194
15255
|
// src/bin/motrix-plugin.ts
|
|
15195
|
-
var program = new Command().name("motrix-plugin").description("Motrix plugin developer tools").version("2.
|
|
15256
|
+
var program = new Command().name("motrix-plugin").description("Motrix plugin developer tools").version("2.1.0");
|
|
15196
15257
|
program.command("init <name>").addOption(
|
|
15197
15258
|
new Option("-t, --template <id>", "template").choices(["basic-resolver", "post-action"]).default("basic-resolver")
|
|
15198
15259
|
).option("-p, --publisher <pub>", "publisher name", "me").action(
|
|
@@ -15207,8 +15268,14 @@ program.command("init <name>").addOption(
|
|
|
15207
15268
|
}
|
|
15208
15269
|
);
|
|
15209
15270
|
program.command("pack").action(async () => {
|
|
15210
|
-
|
|
15211
|
-
|
|
15271
|
+
try {
|
|
15272
|
+
const r = await pack({ projectDir: process.cwd() });
|
|
15273
|
+
console.log(`Packed \u2192 ${r.outFile} (${r.totalSize} bytes)`);
|
|
15274
|
+
console.log(`sha256 ${r.sha256}`);
|
|
15275
|
+
} catch (e) {
|
|
15276
|
+
console.error(e.message);
|
|
15277
|
+
process.exit(1);
|
|
15278
|
+
}
|
|
15212
15279
|
});
|
|
15213
15280
|
program.command("validate").action(async () => {
|
|
15214
15281
|
const r = await validate(process.cwd());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motrix/plugin-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"motrix-plugin": "dist/bin/motrix-plugin.js",
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
"directory": "packages/plugin-cli"
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run"
|
|
25
|
+
},
|
|
21
26
|
"dependencies": {
|
|
22
27
|
"chokidar": "^4",
|
|
23
28
|
"commander": "^12",
|
|
@@ -25,16 +30,11 @@
|
|
|
25
30
|
"yazl": "^2"
|
|
26
31
|
},
|
|
27
32
|
"devDependencies": {
|
|
33
|
+
"@motrix/plugin-manifest-schema": "workspace:*",
|
|
28
34
|
"@types/node": "^24",
|
|
29
35
|
"tsup": "^8",
|
|
30
36
|
"typescript": "^5.6",
|
|
31
37
|
"vitest": "^3",
|
|
32
|
-
"zod": "^4"
|
|
33
|
-
"@motrix/plugin-manifest-schema": "2.0.0"
|
|
34
|
-
},
|
|
35
|
-
"scripts": {
|
|
36
|
-
"build": "tsup",
|
|
37
|
-
"typecheck": "tsc --noEmit",
|
|
38
|
-
"test": "vitest run"
|
|
38
|
+
"zod": "^4"
|
|
39
39
|
}
|
|
40
|
-
}
|
|
40
|
+
}
|