deck-plugin-sdk 1.10.3 → 1.11.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 +97 -97
- package/bin/deck-plugin.mjs +158 -158
- package/build-plugin.mjs +172 -172
- package/package.json +60 -60
- package/packages.mjs +107 -107
- package/permissions.json +252 -241
- package/publish.mjs +116 -116
- package/shim/bridge.js +253 -242
- package/template/AGENTS.md +81 -81
- package/template/CLAUDE.md +1 -1
- package/template/LICENSE +21 -21
- package/template/README.md +20 -20
- package/template/gitignore +4 -4
- package/template/plugin.json +10 -10
- package/template/src/assets.d.ts +16 -16
- package/template/src/plugin.tsx +65 -65
- package/types/core/bridge.d.ts +52 -0
- package/validate.mjs +112 -112
package/validate.mjs
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
// The checks a package must pass to be published, that need nothing but its files: shared by
|
|
2
|
-
// `deck-plugin publish` (which runs them before anything leaves the machine) and the registry's
|
|
3
|
-
// own validation (deck-registry's tools/validate.mjs, which adds what needs GitHub: who owns it,
|
|
4
|
-
// the release, the rebuild). One copy, so the two cannot disagree about what is valid.
|
|
5
|
-
//
|
|
6
|
-
// Pure: no network, no filesystem.
|
|
7
|
-
|
|
8
|
-
/** Deck's own modules and the view prefixes: a plugin with one of these ids could never load. */
|
|
9
|
-
export const RESERVED = new Set(["terminals", "editor", "docs", "settings", "deck", "module", "plugin", "browser"]);
|
|
10
|
-
/**
|
|
11
|
-
* Packages the registry's rebuild executes. Each must be the real one from npm, since the rebuild
|
|
12
|
-
* runs on a machine that can merge; see the SECURITY note in deck-registry's tools/validate.mjs.
|
|
13
|
-
*/
|
|
14
|
-
export const BUILD_RUNS = ["deck-plugin-sdk", "esbuild", "react", "lucide-react", "framer-motion"];
|
|
15
|
-
export const MAX_FILE = 10 * 1024 * 1024, MAX_TOTAL = 15 * 1024 * 1024;
|
|
16
|
-
export const ID = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
17
|
-
export const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
18
|
-
|
|
19
|
-
/** A file's name as a release asset: flat, so a language's snippets/x.json is snippets--x.json. */
|
|
20
|
-
export const assetName = (name) => name.replace(/\//g, "--");
|
|
21
|
-
|
|
22
|
-
export function compareVersions(a, b) {
|
|
23
|
-
const x = SEMVER.exec(a), y = SEMVER.exec(b);
|
|
24
|
-
for (let i = 1; i <= 3; i++) if (Number(x[i]) !== Number(y[i])) return Number(x[i]) - Number(y[i]);
|
|
25
|
-
if (x[4] === y[4]) return 0;
|
|
26
|
-
return x[4] === undefined ? 1 : y[4] === undefined ? -1 : x[4] < y[4] ? -1 : 1;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** An id: its shape, and not one of Deck's own. */
|
|
30
|
-
export function checkId(id) {
|
|
31
|
-
const errors = [];
|
|
32
|
-
if (!ID.test(id)) errors.push(`the id \`${id}\` must be lowercase letters, digits and dashes, 40 at most`);
|
|
33
|
-
if (RESERVED.has(id)) errors.push(`\`${id}\` is one of Deck's own names and cannot be a package id`);
|
|
34
|
-
return errors;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** A plugin's plugin.json, for publishing: what the registry shows and what Deck needs. */
|
|
38
|
-
export function checkManifest(m) {
|
|
39
|
-
const errors = [];
|
|
40
|
-
if (typeof m.name !== "string" || !m.name.trim() || m.name.length > 60) errors.push("the name must be 1 to 60 characters");
|
|
41
|
-
if (typeof m.description !== "string" || m.description.trim().length < 10 || m.description.length > 300) {
|
|
42
|
-
errors.push("the description must be 10 to 300 characters");
|
|
43
|
-
}
|
|
44
|
-
if (typeof m.version !== "string" || !SEMVER.test(m.version)) errors.push("`version` must be a version, such as \"1.0.0\"");
|
|
45
|
-
if (typeof m.minDeck !== "string" || !SEMVER.test(m.minDeck)) errors.push("`minDeck` must say which Deck it needs, such as \"1.8.0\"");
|
|
46
|
-
if (m.color !== undefined && !/^#[0-9a-f]{6}$/i.test(m.color)) errors.push("`color` must be #rrggbb");
|
|
47
|
-
return errors;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** The release's files: the allowed names, the required ones, and the size limits. */
|
|
51
|
-
export function checkAssets(kind, assets) {
|
|
52
|
-
const errors = [];
|
|
53
|
-
const ok = (n) => kind === "plugin" ? ["plugin.json", "plugin.js", "mcp.js"].includes(n) : n === "language.json" || /^snippets--[\w.-]+\.json$/.test(n);
|
|
54
|
-
for (const a of assets) if (!ok(a.name)) errors.push(`the release has \`${a.name}\`, which a ${kind} cannot contain`);
|
|
55
|
-
for (const need of kind === "plugin" ? ["plugin.json", "plugin.js"] : ["language.json"]) {
|
|
56
|
-
if (!assets.some((a) => a.name === need)) errors.push(`the release has no \`${need}\``);
|
|
57
|
-
}
|
|
58
|
-
for (const a of assets) if (a.size > MAX_FILE) errors.push(`\`${a.name}\` is ${(a.size / 1048576).toFixed(1)} MB; a file may be 10 MB at most`);
|
|
59
|
-
const total = assets.reduce((n, a) => n + a.size, 0);
|
|
60
|
-
if (total > MAX_TOTAL) errors.push(`the release is ${(total / 1048576).toFixed(1)} MB; a package may be 15 MB at most`);
|
|
61
|
-
for (const a of assets) if ("digest" in a && !/^sha256:[0-9a-f]{64}$/.test(a.digest ?? "")) errors.push(`GitHub gives no sha256 for \`${a.name}\`; upload it again`);
|
|
62
|
-
return errors;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Whether a lockfile lets `npm ci` install only published npm packages. */
|
|
66
|
-
export function checkLockfile(lock) {
|
|
67
|
-
const errors = [];
|
|
68
|
-
if (!lock || lock.lockfileVersion < 2 || typeof lock.packages !== "object") return ["package-lock.json must be npm 7 or newer (lockfileVersion 2 or 3)"];
|
|
69
|
-
for (const [path, p] of Object.entries(lock.packages)) {
|
|
70
|
-
if (path === "") continue;
|
|
71
|
-
if (p.link) { errors.push(`\`${path}\` is a link to a folder; publish it to npm or bundle it`); continue; }
|
|
72
|
-
if (!p.resolved?.startsWith("https://registry.npmjs.org/") || !/^sha(512|384|256)-/.test(p.integrity ?? "")) {
|
|
73
|
-
errors.push(`\`${path}\` does not come from registry.npmjs.org with an integrity hash`);
|
|
74
|
-
}
|
|
75
|
-
const name = path.replace(/^.*node_modules\//, "");
|
|
76
|
-
const runs = BUILD_RUNS.includes(name) || name.startsWith("@esbuild/");
|
|
77
|
-
if (runs && p.name && p.name !== name) errors.push(`\`${name}\` is an alias of \`${p.name}\`; the build runs it, so it must be the real package`);
|
|
78
|
-
}
|
|
79
|
-
if (!lock.packages["node_modules/deck-plugin-sdk"]) errors.push("the lockfile has no deck-plugin-sdk; a plugin is built with it");
|
|
80
|
-
return errors;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* What a plugin's permissions must be, from its manifest and its bundle (`table` is the SDK's
|
|
85
|
-
* permissions.json): only known names, and covering every bridge call the bundle makes. The shim
|
|
86
|
-
* calls the bridge as `b().fsWrite(`, which minifying keeps, so `().fn(` is the pattern, and a
|
|
87
|
-
* method of the same name on something else (a RegExp's .exec) does not count. Reaching Deck's
|
|
88
|
-
* internals directly, around the bridge, is refused outright.
|
|
89
|
-
*/
|
|
90
|
-
export function checkPermissions(declared, bundle, table) {
|
|
91
|
-
const errors = [], notes = [];
|
|
92
|
-
if (!Array.isArray(declared)) {
|
|
93
|
-
return { errors: ['plugin.json must say what it uses: "permissions": [...], or [] when it needs nothing beyond its own data and settings'], notes };
|
|
94
|
-
}
|
|
95
|
-
const unknown = declared.filter((p) => !(p in table.permissions));
|
|
96
|
-
if (unknown.length) errors.push(`unknown permission${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")} (known: ${Object.keys(table.permissions).join(", ")})`);
|
|
97
|
-
if (/__TAURI_INTERNALS__|__TAURI_IPC__|window\.__TAURI__\b/.test(bundle)) {
|
|
98
|
-
errors.push("its bundle reaches Deck's internals directly, around the bridge its permissions apply to");
|
|
99
|
-
}
|
|
100
|
-
const needs = new Map();
|
|
101
|
-
for (const [fn, p] of Object.entries(table.bridge)) {
|
|
102
|
-
if (new RegExp(`\\(\\)\\.${fn}\\(`).test(bundle)) needs.set(p, [...(needs.get(p) ?? []), fn]);
|
|
103
|
-
}
|
|
104
|
-
for (const [mod, p] of Object.entries(table.modules)) {
|
|
105
|
-
if (new RegExp(`__deck\\?\\.${mod}\\b`).test(bundle)) needs.set(p, [...(needs.get(p) ?? []), mod]);
|
|
106
|
-
}
|
|
107
|
-
for (const [p, fns] of needs) {
|
|
108
|
-
if (!declared.includes(p)) errors.push(`it uses ${fns.slice(0, 4).join(", ")}${fns.length > 4 ? ` and ${fns.length - 4} more` : ""}, which ${fns.length > 1 ? "need" : "needs"} the "${p}" permission; add it to "permissions"`);
|
|
109
|
-
}
|
|
110
|
-
if (/\beval\(|new Function\(/.test(bundle)) notes.push("Its bundle uses eval or new Function, which runs code made at run time; a library may do this legitimately.");
|
|
111
|
-
return { errors, notes };
|
|
112
|
-
}
|
|
1
|
+
// The checks a package must pass to be published, that need nothing but its files: shared by
|
|
2
|
+
// `deck-plugin publish` (which runs them before anything leaves the machine) and the registry's
|
|
3
|
+
// own validation (deck-registry's tools/validate.mjs, which adds what needs GitHub: who owns it,
|
|
4
|
+
// the release, the rebuild). One copy, so the two cannot disagree about what is valid.
|
|
5
|
+
//
|
|
6
|
+
// Pure: no network, no filesystem.
|
|
7
|
+
|
|
8
|
+
/** Deck's own modules and the view prefixes: a plugin with one of these ids could never load. */
|
|
9
|
+
export const RESERVED = new Set(["terminals", "editor", "docs", "settings", "deck", "module", "plugin", "browser"]);
|
|
10
|
+
/**
|
|
11
|
+
* Packages the registry's rebuild executes. Each must be the real one from npm, since the rebuild
|
|
12
|
+
* runs on a machine that can merge; see the SECURITY note in deck-registry's tools/validate.mjs.
|
|
13
|
+
*/
|
|
14
|
+
export const BUILD_RUNS = ["deck-plugin-sdk", "esbuild", "react", "lucide-react", "framer-motion"];
|
|
15
|
+
export const MAX_FILE = 10 * 1024 * 1024, MAX_TOTAL = 15 * 1024 * 1024;
|
|
16
|
+
export const ID = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
17
|
+
export const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
18
|
+
|
|
19
|
+
/** A file's name as a release asset: flat, so a language's snippets/x.json is snippets--x.json. */
|
|
20
|
+
export const assetName = (name) => name.replace(/\//g, "--");
|
|
21
|
+
|
|
22
|
+
export function compareVersions(a, b) {
|
|
23
|
+
const x = SEMVER.exec(a), y = SEMVER.exec(b);
|
|
24
|
+
for (let i = 1; i <= 3; i++) if (Number(x[i]) !== Number(y[i])) return Number(x[i]) - Number(y[i]);
|
|
25
|
+
if (x[4] === y[4]) return 0;
|
|
26
|
+
return x[4] === undefined ? 1 : y[4] === undefined ? -1 : x[4] < y[4] ? -1 : 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** An id: its shape, and not one of Deck's own. */
|
|
30
|
+
export function checkId(id) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
if (!ID.test(id)) errors.push(`the id \`${id}\` must be lowercase letters, digits and dashes, 40 at most`);
|
|
33
|
+
if (RESERVED.has(id)) errors.push(`\`${id}\` is one of Deck's own names and cannot be a package id`);
|
|
34
|
+
return errors;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A plugin's plugin.json, for publishing: what the registry shows and what Deck needs. */
|
|
38
|
+
export function checkManifest(m) {
|
|
39
|
+
const errors = [];
|
|
40
|
+
if (typeof m.name !== "string" || !m.name.trim() || m.name.length > 60) errors.push("the name must be 1 to 60 characters");
|
|
41
|
+
if (typeof m.description !== "string" || m.description.trim().length < 10 || m.description.length > 300) {
|
|
42
|
+
errors.push("the description must be 10 to 300 characters");
|
|
43
|
+
}
|
|
44
|
+
if (typeof m.version !== "string" || !SEMVER.test(m.version)) errors.push("`version` must be a version, such as \"1.0.0\"");
|
|
45
|
+
if (typeof m.minDeck !== "string" || !SEMVER.test(m.minDeck)) errors.push("`minDeck` must say which Deck it needs, such as \"1.8.0\"");
|
|
46
|
+
if (m.color !== undefined && !/^#[0-9a-f]{6}$/i.test(m.color)) errors.push("`color` must be #rrggbb");
|
|
47
|
+
return errors;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The release's files: the allowed names, the required ones, and the size limits. */
|
|
51
|
+
export function checkAssets(kind, assets) {
|
|
52
|
+
const errors = [];
|
|
53
|
+
const ok = (n) => kind === "plugin" ? ["plugin.json", "plugin.js", "mcp.js"].includes(n) : n === "language.json" || /^snippets--[\w.-]+\.json$/.test(n);
|
|
54
|
+
for (const a of assets) if (!ok(a.name)) errors.push(`the release has \`${a.name}\`, which a ${kind} cannot contain`);
|
|
55
|
+
for (const need of kind === "plugin" ? ["plugin.json", "plugin.js"] : ["language.json"]) {
|
|
56
|
+
if (!assets.some((a) => a.name === need)) errors.push(`the release has no \`${need}\``);
|
|
57
|
+
}
|
|
58
|
+
for (const a of assets) if (a.size > MAX_FILE) errors.push(`\`${a.name}\` is ${(a.size / 1048576).toFixed(1)} MB; a file may be 10 MB at most`);
|
|
59
|
+
const total = assets.reduce((n, a) => n + a.size, 0);
|
|
60
|
+
if (total > MAX_TOTAL) errors.push(`the release is ${(total / 1048576).toFixed(1)} MB; a package may be 15 MB at most`);
|
|
61
|
+
for (const a of assets) if ("digest" in a && !/^sha256:[0-9a-f]{64}$/.test(a.digest ?? "")) errors.push(`GitHub gives no sha256 for \`${a.name}\`; upload it again`);
|
|
62
|
+
return errors;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Whether a lockfile lets `npm ci` install only published npm packages. */
|
|
66
|
+
export function checkLockfile(lock) {
|
|
67
|
+
const errors = [];
|
|
68
|
+
if (!lock || lock.lockfileVersion < 2 || typeof lock.packages !== "object") return ["package-lock.json must be npm 7 or newer (lockfileVersion 2 or 3)"];
|
|
69
|
+
for (const [path, p] of Object.entries(lock.packages)) {
|
|
70
|
+
if (path === "") continue;
|
|
71
|
+
if (p.link) { errors.push(`\`${path}\` is a link to a folder; publish it to npm or bundle it`); continue; }
|
|
72
|
+
if (!p.resolved?.startsWith("https://registry.npmjs.org/") || !/^sha(512|384|256)-/.test(p.integrity ?? "")) {
|
|
73
|
+
errors.push(`\`${path}\` does not come from registry.npmjs.org with an integrity hash`);
|
|
74
|
+
}
|
|
75
|
+
const name = path.replace(/^.*node_modules\//, "");
|
|
76
|
+
const runs = BUILD_RUNS.includes(name) || name.startsWith("@esbuild/");
|
|
77
|
+
if (runs && p.name && p.name !== name) errors.push(`\`${name}\` is an alias of \`${p.name}\`; the build runs it, so it must be the real package`);
|
|
78
|
+
}
|
|
79
|
+
if (!lock.packages["node_modules/deck-plugin-sdk"]) errors.push("the lockfile has no deck-plugin-sdk; a plugin is built with it");
|
|
80
|
+
return errors;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* What a plugin's permissions must be, from its manifest and its bundle (`table` is the SDK's
|
|
85
|
+
* permissions.json): only known names, and covering every bridge call the bundle makes. The shim
|
|
86
|
+
* calls the bridge as `b().fsWrite(`, which minifying keeps, so `().fn(` is the pattern, and a
|
|
87
|
+
* method of the same name on something else (a RegExp's .exec) does not count. Reaching Deck's
|
|
88
|
+
* internals directly, around the bridge, is refused outright.
|
|
89
|
+
*/
|
|
90
|
+
export function checkPermissions(declared, bundle, table) {
|
|
91
|
+
const errors = [], notes = [];
|
|
92
|
+
if (!Array.isArray(declared)) {
|
|
93
|
+
return { errors: ['plugin.json must say what it uses: "permissions": [...], or [] when it needs nothing beyond its own data and settings'], notes };
|
|
94
|
+
}
|
|
95
|
+
const unknown = declared.filter((p) => !(p in table.permissions));
|
|
96
|
+
if (unknown.length) errors.push(`unknown permission${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")} (known: ${Object.keys(table.permissions).join(", ")})`);
|
|
97
|
+
if (/__TAURI_INTERNALS__|__TAURI_IPC__|window\.__TAURI__\b/.test(bundle)) {
|
|
98
|
+
errors.push("its bundle reaches Deck's internals directly, around the bridge its permissions apply to");
|
|
99
|
+
}
|
|
100
|
+
const needs = new Map();
|
|
101
|
+
for (const [fn, p] of Object.entries(table.bridge)) {
|
|
102
|
+
if (new RegExp(`\\(\\)\\.${fn}\\(`).test(bundle)) needs.set(p, [...(needs.get(p) ?? []), fn]);
|
|
103
|
+
}
|
|
104
|
+
for (const [mod, p] of Object.entries(table.modules)) {
|
|
105
|
+
if (new RegExp(`__deck\\?\\.${mod}\\b`).test(bundle)) needs.set(p, [...(needs.get(p) ?? []), mod]);
|
|
106
|
+
}
|
|
107
|
+
for (const [p, fns] of needs) {
|
|
108
|
+
if (!declared.includes(p)) errors.push(`it uses ${fns.slice(0, 4).join(", ")}${fns.length > 4 ? ` and ${fns.length - 4} more` : ""}, which ${fns.length > 1 ? "need" : "needs"} the "${p}" permission; add it to "permissions"`);
|
|
109
|
+
}
|
|
110
|
+
if (/\beval\(|new Function\(/.test(bundle)) notes.push("Its bundle uses eval or new Function, which runs code made at run time; a library may do this legitimately.");
|
|
111
|
+
return { errors, notes };
|
|
112
|
+
}
|