deck-plugin-sdk 1.7.0 → 1.9.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.
@@ -217,6 +217,8 @@ export declare function noteTermOutput(pane: string, chunk: string): void;
217
217
  export declare function forgetTermPane(pane: string): void;
218
218
  /** Where a pane's page is on screen, in Deck's CSS pixels, while it shows one. */
219
219
  export declare const pageBox: (pane: string) => DOMRect | null;
220
+ /** The Deck window a pane's page is drawn in: the main one, or a floating tab window's. */
221
+ export declare const paneWindow: (pane: string) => Window;
220
222
  /** Close the pane's webview to give back its memory, keeping the tab and its address. */
221
223
  export declare function discardBrowserPane(pane: string): Promise<void>;
222
224
  export declare function trackBrowser(pane: string, el: HTMLElement, url: string): () => void;
@@ -0,0 +1,3 @@
1
+ // GENERATED by sdk/gen-types.mjs from Deck's source — do not edit.
2
+ import { type ReactNode } from "react";
3
+ export declare function createPortal(children: ReactNode, container: Element | DocumentFragment, key?: string | null): ReactNode;
@@ -45,6 +45,12 @@ export interface LayoutTab {
45
45
  hidden?: boolean;
46
46
  /** The tab group it is in (LayoutWorkspace.groups); a group's tabs sit together in one zone. */
47
47
  group?: string;
48
+ /**
49
+ * Torn off into a window of its own (core/floatwin.ts): the window's id. Such a tab is in no
50
+ * zone, though it keeps its x/y to go back to. A restart reopens the window (Layout.floats has
51
+ * where it was).
52
+ */
53
+ window?: string;
48
54
  }
49
55
  export interface LayoutWorkspace {
50
56
  id: string;
@@ -57,9 +63,19 @@ export interface LayoutWorkspace {
57
63
  /** Its colour on the sidebar's collapsed rail, by index (Terminals' TONES); none picks one from the name. */
58
64
  color?: number;
59
65
  }
66
+ /** Where a floating window was, in physical screen pixels (its size and place before any maximise), to reopen it there. */
67
+ export interface FloatBounds {
68
+ id: string;
69
+ x: number;
70
+ y: number;
71
+ w: number;
72
+ h: number;
73
+ max?: boolean;
74
+ }
60
75
  export interface Layout {
61
76
  workspaces: LayoutWorkspace[];
62
77
  activeWs: string;
78
+ floats?: FloatBounds[];
63
79
  }
64
80
  export declare function publishLayout(l: Layout): void;
65
81
  export declare function getLayout(): Layout | null;
@@ -202,6 +218,20 @@ export declare function showActiveTab(w: LayoutWorkspace): LayoutWorkspace;
202
218
  * the zone nor back to its first tab; otherwise the first zone left.
203
219
  */
204
220
  export declare function nextActiveAfterClose(tabs: readonly LayoutTab[], closing: string): string | undefined;
221
+ /**
222
+ * Tear a tab off into window `win`, or (`win` undefined) dock it back. A workspace always keeps
223
+ * one docked tab: floating its last one leaves a fresh terminal behind, as the main window has
224
+ * to show something. Tearing off the active tab makes its neighbour active.
225
+ */
226
+ export declare function setTabWindow(w: LayoutWorkspace, tabId: string, win: string | undefined, keepGroup?: boolean): LayoutWorkspace;
227
+ /**
228
+ * Merge floating window `from`'s tabs into another window: floating window `into`, or (`into`
229
+ * null) the main window, into the active zone of the project it shows. They go in behind the tab
230
+ * that window shows, which stays shown: a tab dropped somewhere does not take you away from the one
231
+ * you were on. The tabs move between projects when the window they land in shows another one's,
232
+ * since a window holds one project's tabs.
233
+ */
234
+ export declare function mergeWindow(l: Layout, from: string, into: string | null): Layout;
205
235
  /** The first free spot on the top row, for a zone created without a position of its own. */
206
236
  export declare function nextZoneSpot(tabs: readonly LayoutTab[], cols?: number, rows?: number): {
207
237
  x: number;
@@ -431,6 +461,12 @@ export declare function sendInput(paneId: string, text: string): void;
431
461
  * layout and re-fires the observer that called us. Measuring first and resizing once — only when
432
462
  * the size actually differs — means the observer's next pass finds nothing to do and it settles.
433
463
  */
464
+ /**
465
+ * The terminals drawn in `doc` take its window's scale again: its window moved onto a monitor of
466
+ * other scaling (a floating window reopened where it was, or dragged there). The same hand-over
467
+ * as a move between windows (see attach).
468
+ */
469
+ export declare function rebindTerminals(doc: Document): void;
434
470
  export declare function refit(id: string): void;
435
471
  /**
436
472
  * Clear the screen, the way the shell expects.
@@ -0,0 +1,52 @@
1
+ // GENERATED by sdk/gen-types.mjs from Deck's source — do not edit.
2
+ type Cleanup = void | (() => void);
3
+ type Hook = (win: Window) => Cleanup;
4
+ /** Every Deck window: the main one, then each floating one. */
5
+ export declare const allWindows: () => Window[];
6
+ /**
7
+ * A Deck window's Tauri label: "main", or a floating window's (floatwin.rs). A floating window is
8
+ * only registered here once its label exists, so every hook can rely on it.
9
+ */
10
+ export declare function windowLabel(w: Window): string;
11
+ /**
12
+ * Whether `x` is an element, in any Deck window. Not `instanceof Element`: an element in a
13
+ * floating window is an instance of THAT window's Element, so the main window's check is false.
14
+ */
15
+ export declare function isElement(x: unknown): x is Element;
16
+ /**
17
+ * A ResizeObserver from the window `el` is drawn in. One made by the main window watches an element
18
+ * in a floating window only when the main window next draws, so a terminal opened in a float that
19
+ * waited for its size could sit blank for seconds.
20
+ */
21
+ export declare function resizeObserverFor(el: Element, cb: ResizeObserverCallback): ResizeObserver;
22
+ /** The window an element is drawn in: the main one, or a floating window's. */
23
+ export declare function winOf(el: Element | null | undefined): Window;
24
+ /**
25
+ * The Deck window the user is in: the one they last clicked, typed or moved focus in. Not
26
+ * document.hasFocus() alone: with a floating window in front, the main window's document still
27
+ * says it has focus too (WebView2, for a popup it opened), so the first match was always main.
28
+ */
29
+ export declare function activeWindow(): Window;
30
+ /** What has focus in the window the user is in: what `document.activeElement` means in one window. */
31
+ export declare function activeElement(): Element | null;
32
+ /** Run `fn` for every Deck window, now and as floats open; what it returns runs when that window goes, or on the returned unsubscribe. */
33
+ export declare function onEachWindow(fn: Hook): () => void;
34
+ /** A floating window opened (floatwin.ts), its label known: every hook runs for it. */
35
+ export declare function addWindow(win: Window): void;
36
+ /** A floating window is going: what each hook installed in it is undone. */
37
+ export declare function removeWindow(win: Window): void;
38
+ /**
39
+ * Listen on every Deck window (or every window's document), now and as floats open; returns the
40
+ * unsubscribe. For what closes a menu or a dialog wherever the user is: Escape, a click outside.
41
+ */
42
+ export declare function onAll<K extends keyof WindowEventMap>(type: K, fn: (e: WindowEventMap[K]) => void, opts?: boolean | AddEventListenerOptions, on?: "window" | "document"): () => void;
43
+ /**
44
+ * Make the main realm's `navigator.clipboard` work from every Deck window. The browser refuses a
45
+ * clipboard call from a document that does not have focus ("Document is not focused"), and the
46
+ * realm's clipboard is the MAIN window's: while the user works in a floating window, every copy and
47
+ * paste failed, a terminal's Ctrl+C clearing the selection with nothing copied. Each call now goes
48
+ * to the clipboard of the window the user is in, which is main's own when that is where they are.
49
+ * Installed once, before plugins load, so their copies and pastes get it too.
50
+ */
51
+ export declare function routeClipboard(): void;
52
+ export {};
@@ -13,6 +13,12 @@ export declare function fileUri(path: string): monaco.Uri;
13
13
  export declare function setUntitledLanguage(path: string, id: string | null): void;
14
14
  /** Take `path` for `owner`; false when another editor's buffer has it. */
15
15
  export declare function claimUntitled(path: string, owner: string): boolean;
16
+ /**
17
+ * Put `value` into `m` as the one edit between their common start and end, rather than replacing
18
+ * the whole text: a cursor outside the change stays where it is. A whole-text replace (setValue, or
19
+ * an edit over the full range) moved every cursor to the top of the file, mid-typing.
20
+ */
21
+ export declare function replaceText(m: monaco.editor.ITextModel, value: string): void;
16
22
  export declare function getFileModel(path: string, initial: string): monaco.editor.ITextModel;
17
23
  export declare function setModelValue(path: string, value: string): void;
18
24
  export declare function disposeModel(path: string | undefined): void;
package/validate.mjs ADDED
@@ -0,0 +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
+ }