deck-plugin-sdk 1.11.0 → 1.12.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 +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 -248
- package/publish.mjs +116 -116
- package/shim/bridge.js +253 -249
- package/shim/p2p.d.ts +1 -1
- package/shim/settings.d.ts +1 -1
- package/shim/terminals.d.ts +1 -1
- package/shim/ui.d.ts +10 -10
- 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/components/FileIcon.d.ts +1 -1
- package/types/components/TabStrip.d.ts +5 -2
- package/types/core/{settingsstore.d.ts → app/settingsstore.d.ts} +1 -1
- package/types/core/bridge.d.ts +1190 -1167
- package/types/core/{bookmarks.d.ts → browser/bookmarks.d.ts} +4 -2
- package/types/core/{browser.d.ts → browser/browser.d.ts} +1 -1
- package/types/core/{editorsettings.d.ts → editor/editorsettings.d.ts} +1 -1
- package/types/core/{icontheme.d.ts → editor/icontheme.d.ts} +1 -1
- package/types/core/workspaces/layout.d.ts +168 -0
- package/types/core/workspaces/terminals.d.ts +252 -0
- package/types/core/workspaces/zones.d.ts +215 -0
- package/types/modules/editor/MonacoHosts.d.ts +3 -3
- package/types/modules/editor/{DiffPeek.d.ts → git/DiffPeek.d.ts} +1 -1
- package/types/modules/editor/models.d.ts +1 -1
- package/types/modules/editor/{tree.d.ts → tree/tree.d.ts} +1 -1
- package/validate.mjs +112 -112
- package/types/core/terminals.d.ts +0 -573
- /package/types/core/{p2p.d.ts → app/p2p.d.ts} +0 -0
- /package/types/core/{theme.d.ts → app/theme.d.ts} +0 -0
- /package/types/core/{timeFormat.d.ts → app/timeFormat.d.ts} +0 -0
- /package/types/core/{browserurl.d.ts → browser/browserurl.d.ts} +0 -0
- /package/types/core/{dap.d.ts → editor/dap.d.ts} +0 -0
- /package/types/core/{debugtargets.d.ts → editor/debugtargets.d.ts} +0 -0
- /package/types/core/{decode.d.ts → editor/decode.d.ts} +0 -0
- /package/types/core/{editorsession.d.ts → editor/editorsession.d.ts} +0 -0
- /package/types/core/{inlay.d.ts → editor/inlay.d.ts} +0 -0
- /package/types/core/{langmap.d.ts → editor/langmap.d.ts} +0 -0
- /package/types/core/{lsp.d.ts → editor/lsp.d.ts} +0 -0
- /package/types/core/{monaco.d.ts → editor/monaco.d.ts} +0 -0
- /package/types/core/{patterntokens.d.ts → editor/patterntokens.d.ts} +0 -0
- /package/types/core/{snippets.d.ts → editor/snippets.d.ts} +0 -0
- /package/types/core/{treehide.d.ts → editor/treehide.d.ts} +0 -0
- /package/types/core/{keys.d.ts → keys/keys.d.ts} +0 -0
- /package/types/core/{keystore.d.ts → keys/keystore.d.ts} +0 -0
- /package/types/core/{notifications.d.ts → notifications/notifications.d.ts} +0 -0
- /package/types/core/{notifyapi.d.ts → notifications/notifyapi.d.ts} +0 -0
- /package/types/core/{fileview.d.ts → plugins/fileview.d.ts} +0 -0
- /package/types/core/{portal.d.ts → workspaces/portal.d.ts} +0 -0
- /package/types/core/{remotepath.d.ts → workspaces/remotepath.d.ts} +0 -0
- /package/types/core/{shellpath.d.ts → workspaces/shellpath.d.ts} +0 -0
- /package/types/core/{windows.d.ts → workspaces/windows.d.ts} +0 -0
- /package/types/modules/editor/{diffline.d.ts → git/diffline.d.ts} +0 -0
- /package/types/modules/editor/{hunks.d.ts → git/hunks.d.ts} +0 -0
- /package/types/modules/editor/{MarkdownLive.d.ts → viewers/MarkdownLive.d.ts} +0 -0
- /package/types/modules/editor/{viewers.d.ts → viewers/viewers.d.ts} +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// GENERATED by sdk/gen-types.mjs from Deck's source — do not edit.
|
|
2
|
+
import { LayoutTab, LayoutWorkspace, Layout } from "./layout";
|
|
3
|
+
/**
|
|
4
|
+
* How many columns the zone board has. Here rather than only in the UI because the fallback
|
|
5
|
+
* placement below has to agree with what the overview can draw.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ZONE_BOARD = 3;
|
|
8
|
+
/**
|
|
9
|
+
* Every tab's cell on the canvas.
|
|
10
|
+
*
|
|
11
|
+
* Placed tabs claim their stored coordinates first; unplaced ones then fill the free cells in
|
|
12
|
+
* reading order. Two consequences, both load-bearing:
|
|
13
|
+
*
|
|
14
|
+
* - A layout written before zones existed has no coordinates at all, so tab i lands on cell i —
|
|
15
|
+
* the first row, left to right, then the next — without anything on disk being rewritten.
|
|
16
|
+
* - An unplaced tab never lands on a cell a placed tab already holds. Tabs that share a cell ARE
|
|
17
|
+
* one zone, so a tab created without a position (the pinboard, an agent over MCP) must not
|
|
18
|
+
* silently join whichever zone happens to sit at its list index. Before zones held several
|
|
19
|
+
* tabs that collision only drew two zones on top of each other; now it would merge them.
|
|
20
|
+
*/
|
|
21
|
+
export declare function zoneCells(tabs: readonly LayoutTab[], cols: number): Map<string, {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
}>;
|
|
25
|
+
/** Where a tab sits on the canvas — which is to say, which zone it belongs to. */
|
|
26
|
+
export declare function zoneAt(tabs: readonly LayoutTab[], id: string, cols?: number): {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
};
|
|
30
|
+
/** One zone: a cell, the tabs in it in list order, and the one it shows. */
|
|
31
|
+
export interface Zone {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
tabs: LayoutTab[];
|
|
35
|
+
front: LayoutTab;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The tabs grouped into zones, one per occupied cell, in the order each cell first appears.
|
|
39
|
+
*
|
|
40
|
+
* The front tab is the one not marked `hidden` — the last one shown there, which is what makes a
|
|
41
|
+
* zone open on the tab you left it on. A cell whose tabs are ALL hidden (its front was closed
|
|
42
|
+
* from outside, by an agent) shows its first, rather than nothing.
|
|
43
|
+
*/
|
|
44
|
+
export declare function zones(tabs: readonly LayoutTab[], cols?: number): Zone[];
|
|
45
|
+
/** The zone holding a tab. */
|
|
46
|
+
export declare function zoneOf(tabs: readonly LayoutTab[], id: string): Zone | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* The zone to move to from `id` going `dir`, as the tab it would show, or null when there is
|
|
49
|
+
* none that way.
|
|
50
|
+
*
|
|
51
|
+
* Nearest in a straight line: same row for left/right, same column for up/down, smallest step.
|
|
52
|
+
* No wrapping — running out of canvas should feel like an edge, not teleport you across it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function zoneToward(tabs: readonly LayoutTab[], id: string, dir: "left" | "right" | "up" | "down"): LayoutTab | null;
|
|
55
|
+
/**
|
|
56
|
+
* Every tab with explicit coordinates, at the cell it already appeared in.
|
|
57
|
+
*
|
|
58
|
+
* Needed before writing any coordinate: an unplaced tab's position depends on what else is
|
|
59
|
+
* placed, so placing one tab can move another that was relying on the fallback.
|
|
60
|
+
*/
|
|
61
|
+
export declare function placeAll(tabs: readonly LayoutTab[]): LayoutTab[];
|
|
62
|
+
/**
|
|
63
|
+
* Put a zone at a spot on the canvas, swapping with whatever is already there.
|
|
64
|
+
*
|
|
65
|
+
* Every tab in the zone moves, since the zone IS the tabs sharing a cell — moving only the one
|
|
66
|
+
* named would split it in two.
|
|
67
|
+
*
|
|
68
|
+
* Swap rather than push-and-shuffle: a zone you did not touch should not move because another
|
|
69
|
+
* one arrived next to it, and with sparse coordinates there is no row to shuffle along anyway.
|
|
70
|
+
*/
|
|
71
|
+
export declare function moveZone(tabs: readonly LayoutTab[], id: string, to: {
|
|
72
|
+
x: number;
|
|
73
|
+
y: number;
|
|
74
|
+
}): LayoutTab[];
|
|
75
|
+
/**
|
|
76
|
+
* Move a tab to just before or after another — the order a zone's strip shows its tabs in, which
|
|
77
|
+
* is their order in the list.
|
|
78
|
+
*
|
|
79
|
+
* Every tab's cell is pinned first (placeAll): a tab with no stored position takes its cell from
|
|
80
|
+
* its index, so reordering the list could otherwise move a zone you never touched.
|
|
81
|
+
*/
|
|
82
|
+
export declare function reorderTab(tabs: readonly LayoutTab[], srcId: string, overId: string, after: boolean,
|
|
83
|
+
/** A drag in the strip says which group the tab ends up in: the one of the tab it is held over
|
|
84
|
+
* (null: none, held over a tab outside any group or over a group's chip). Left out, the tabs
|
|
85
|
+
* either side decide (settleGroup). */
|
|
86
|
+
group?: string | null): LayoutTab[];
|
|
87
|
+
/**
|
|
88
|
+
* A tab group's tabs sit together in their zone's row: a tab dropped between two of one group's
|
|
89
|
+
* tabs joins it, and one moved away from every other tab of its own group leaves it. Next to one
|
|
90
|
+
* of its group's tabs (at either end) it stays.
|
|
91
|
+
*/
|
|
92
|
+
export declare function settleGroup(tabs: LayoutTab[], id: string): LayoutTab[];
|
|
93
|
+
/** A tab group: its tabs carry its id. Its colour is an index into GROUP_COLORS. */
|
|
94
|
+
export interface TabGroup {
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
color: number;
|
|
98
|
+
folded?: boolean;
|
|
99
|
+
}
|
|
100
|
+
/** Chrome's group colours, tuned for Deck's dark surfaces. */
|
|
101
|
+
export declare const GROUP_COLORS: string[];
|
|
102
|
+
/** Move a tab to just after the last other tab of `group` in its zone, if it has any there. */
|
|
103
|
+
export declare function besideGroup(tabs: readonly LayoutTab[], id: string, group: string): LayoutTab[];
|
|
104
|
+
/**
|
|
105
|
+
* Put a tab in a group: `groupId`, or a new one in the next colour not yet used (its id is
|
|
106
|
+
* returned, so its editor can open). The tab moves next to the group's other tabs.
|
|
107
|
+
*/
|
|
108
|
+
export declare function addToGroup(w: LayoutWorkspace, tabId: string, groupId?: string): {
|
|
109
|
+
w: LayoutWorkspace;
|
|
110
|
+
groupId: string;
|
|
111
|
+
};
|
|
112
|
+
/** Take a tab out of its group, to just after the group's tabs. */
|
|
113
|
+
export declare function leaveGroup(w: LayoutWorkspace, tabId: string): LayoutWorkspace;
|
|
114
|
+
/**
|
|
115
|
+
* Move a whole group, its tabs in their order, to just before or after `overId`. Over another
|
|
116
|
+
* group's tab it goes before or after that group as a whole, never into the middle of it.
|
|
117
|
+
*/
|
|
118
|
+
export declare function moveGroup(tabs: readonly LayoutTab[], groupId: string, overId: string, after: boolean): LayoutTab[];
|
|
119
|
+
/**
|
|
120
|
+
* Fold a group, or open it. A group holding the tab its zone shows is not folded over it: the
|
|
121
|
+
* zone moves to the nearest tab outside the group that is not itself folded away (after the group
|
|
122
|
+
* first, then before it). With none, `makeTab` gives a new tab for the zone to open on; without
|
|
123
|
+
* it, the group stays open.
|
|
124
|
+
*/
|
|
125
|
+
export declare function toggleGroupFold(w: LayoutWorkspace, groupId: string, makeTab?: () => LayoutTab): LayoutWorkspace;
|
|
126
|
+
/** The group goes; its tabs stay where they are. */
|
|
127
|
+
export declare function ungroup(w: LayoutWorkspace, groupId: string): LayoutWorkspace;
|
|
128
|
+
/** Change a group's name, colour or fold. */
|
|
129
|
+
export declare function setGroup(w: LayoutWorkspace, groupId: string, patch: Partial<Omit<TabGroup, "id">>): LayoutWorkspace;
|
|
130
|
+
/**
|
|
131
|
+
* The open tab is never folded away: however it came to be open (the tab beside a closed one, the
|
|
132
|
+
* switcher, a shortcut, an agent's notification), a folded group holding it opens.
|
|
133
|
+
*/
|
|
134
|
+
export declare function openActiveGroup(w: LayoutWorkspace): LayoutWorkspace;
|
|
135
|
+
/** A group with no tabs left goes (its last tab closed, moved to another zone, taken out). */
|
|
136
|
+
export declare function pruneGroups(w: LayoutWorkspace): LayoutWorkspace;
|
|
137
|
+
/**
|
|
138
|
+
* Move one tab to another zone's cell — joining the zone there as the tab it shows, or making a
|
|
139
|
+
* new zone in an empty cell. The zone it left keeps its other tabs.
|
|
140
|
+
*/
|
|
141
|
+
export declare function moveTabToZone(tabs: readonly LayoutTab[], tabId: string, to: {
|
|
142
|
+
x: number;
|
|
143
|
+
y: number;
|
|
144
|
+
}): LayoutTab[];
|
|
145
|
+
/**
|
|
146
|
+
* Add a tab to the zone holding `besideId`, as the tab it shows.
|
|
147
|
+
*
|
|
148
|
+
* The others there are marked hidden here rather than left to the active-tab normaliser, so the
|
|
149
|
+
* result is right even for a zone that is not the active one.
|
|
150
|
+
*/
|
|
151
|
+
export declare function addTabToZone(tabs: readonly LayoutTab[], besideId: string, tab: LayoutTab): LayoutTab[];
|
|
152
|
+
/**
|
|
153
|
+
* A closed tab put back in workspace `w` (core/workspaces/closedtabs.ts): in front in the zone you are in,
|
|
154
|
+
* with new ids for the tab and its panes, since the old panes' shells and state are gone. Out of
|
|
155
|
+
* any group and any window it was in. Returns the workspace and each old pane's new id.
|
|
156
|
+
*/
|
|
157
|
+
export declare function reopenTab(w: LayoutWorkspace, tab: LayoutTab, fresh?: () => string): {
|
|
158
|
+
w: LayoutWorkspace;
|
|
159
|
+
ids: Record<string, string>;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Make a workspace's active tab the one its zone shows.
|
|
163
|
+
*
|
|
164
|
+
* Run on every layout write (mutateLayout), so whatever made a tab active — a click in the
|
|
165
|
+
* strip, the compass, an agent notification jumping to a pane in a tab behind another — the
|
|
166
|
+
* zone ends up showing it, and remembers it next time. One place rather than every caller that
|
|
167
|
+
* sets activeTab, of which there are many and will be more.
|
|
168
|
+
*/
|
|
169
|
+
export declare function showActiveTab(w: LayoutWorkspace): LayoutWorkspace;
|
|
170
|
+
/**
|
|
171
|
+
* Which tab becomes active when `closing` goes: the one beside it in the same zone (the next, or
|
|
172
|
+
* the one before when it was last), as a browser does, so closing a tab neither throws you out of
|
|
173
|
+
* the zone nor back to its first tab; otherwise the first zone left.
|
|
174
|
+
*/
|
|
175
|
+
export declare function nextActiveAfterClose(tabs: readonly LayoutTab[], closing: string): string | undefined;
|
|
176
|
+
/**
|
|
177
|
+
* Tear a tab off into window `win`, or (`win` undefined) dock it back. A workspace always keeps
|
|
178
|
+
* one docked tab: floating its last one leaves a fresh terminal behind, as the main window has
|
|
179
|
+
* to show something. Tearing off the active tab makes its neighbour active.
|
|
180
|
+
*/
|
|
181
|
+
export declare function setTabWindow(w: LayoutWorkspace, tabId: string, win: string | undefined, keepGroup?: boolean): LayoutWorkspace;
|
|
182
|
+
/**
|
|
183
|
+
* Merge floating window `from`'s tabs into another window: floating window `into`, or (`into`
|
|
184
|
+
* null) the main window, into the active zone of the project it shows. They go in behind the tab
|
|
185
|
+
* that window shows, which stays shown: a tab dropped somewhere does not take you away from the one
|
|
186
|
+
* you were on. The tabs move between projects when the window they land in shows another one's,
|
|
187
|
+
* since a window holds one project's tabs.
|
|
188
|
+
*/
|
|
189
|
+
export declare function mergeWindow(l: Layout, from: string, into: string | null): Layout;
|
|
190
|
+
/**
|
|
191
|
+
* Close floating window `id` into the main window: its tabs go back to their zones, each behind the
|
|
192
|
+
* tab its zone shows, so closing a window does not take you away from the tab you are on. With
|
|
193
|
+
* `discard` (its X, Alt+F4), the tabs Deck opened into it (`transient`) close instead; the caller
|
|
194
|
+
* destroys their panes.
|
|
195
|
+
*/
|
|
196
|
+
export declare function dockWindow(l: Layout, id: string, discard?: boolean): Layout;
|
|
197
|
+
/** The first free spot on the top row, for a zone created without a position of its own. */
|
|
198
|
+
export declare function nextZoneSpot(tabs: readonly LayoutTab[], cols?: number, rows?: number): {
|
|
199
|
+
x: number;
|
|
200
|
+
y: number;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Is there nowhere left to put a zone?
|
|
204
|
+
*
|
|
205
|
+
* Counts zones, not tabs: a zone can hold several tabs and still take one cell.
|
|
206
|
+
*
|
|
207
|
+
* The board is a fixed 3x3 — deliberately, so the overview and the Ctrl+Alt compass are always
|
|
208
|
+
* the same nine cells. A tenth zone has no spot: it drew off the edge of the overview and the
|
|
209
|
+
* compass could not reach it, so it existed but could not be seen or switched to.
|
|
210
|
+
*
|
|
211
|
+
* Checked by every caller that creates one, rather than inside nextZoneSpot, because refusing
|
|
212
|
+
* and reporting why is the caller's job — the keyboard shortcut says so in a toast, and the
|
|
213
|
+
* layout op tells the agent that asked.
|
|
214
|
+
*/
|
|
215
|
+
export declare function zoneBoardFull(tabs: readonly LayoutTab[], cols?: number, rows?: number): boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GENERATED by sdk/gen-types.mjs from Deck's source — do not edit.
|
|
2
|
-
import { monaco } from "../../core/monaco";
|
|
3
|
-
import { type EditorSettings } from "../../core/editorsettings";
|
|
4
|
-
import type { DirtyRange } from "./hunks";
|
|
2
|
+
import { monaco } from "../../core/editor/monaco";
|
|
3
|
+
import { type EditorSettings } from "../../core/editor/editorsettings";
|
|
4
|
+
import type { DirtyRange } from "./git/hunks";
|
|
5
5
|
declare function MonacoEditorInner({ path, rel, value, onChange, onSave, gotoLine, gotoCol, gotoNonce, highlight, dirty, settings, onEditor, clean, onStageChange, onUnstageChange, onReadBase }: {
|
|
6
6
|
path: string;
|
|
7
7
|
rel?: string | null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GENERATED by sdk/gen-types.mjs from Deck's source — do not edit.
|
|
2
2
|
import { type RefObject } from "react";
|
|
3
|
-
import { monaco } from "
|
|
3
|
+
import { monaco } from "../../../core/editor/monaco";
|
|
4
4
|
import { type DirtyRange } from "./hunks";
|
|
5
5
|
/**
|
|
6
6
|
* The peek, for the editor in `edRef` showing `path` with the gutter's `ranges`. Returns what to
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// GENERATED by sdk/gen-types.mjs from Deck's source — do not edit.
|
|
2
|
-
import { monaco } from "../../core/monaco";
|
|
2
|
+
import { monaco } from "../../core/editor/monaco";
|
|
3
3
|
export declare const viewStates: Map<string, monaco.editor.ICodeEditorViewState | null>;
|
|
4
4
|
/**
|
|
5
5
|
* An untitled buffer (Ctrl+N): `untitled:Untitled-N`. It has no file behind it, so its model gets
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// GENERATED by sdk/gen-types.mjs from Deck's source — do not edit.
|
|
2
|
-
import type { GitStatusRow } from "
|
|
2
|
+
import type { GitStatusRow } from "../../../core/bridge";
|
|
3
3
|
/** Last path segment, trailing separators ignored. */
|
|
4
4
|
export declare const baseName: (p: string) => string;
|
|
5
5
|
/** Windows paths use both separators; normalize to "/" for comparison and display. */
|
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
|
+
}
|