@mterminal/manifest-validator 0.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/LICENSE +21 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +203 -0
- package/package.json +29 -0
- package/src/index.test.ts +107 -0
- package/src/index.ts +417 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mTerminal contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
type ActivationEvent = 'onStartupFinished' | 'onSelection' | `onCommand:${string}` | `onView:${string}` | `onTabType:${string}` | `onUri:${string}` | `onEvent:${string}`;
|
|
2
|
+
interface CommandContribution {
|
|
3
|
+
id: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
category?: string;
|
|
6
|
+
icon?: string;
|
|
7
|
+
args?: Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
type: 'string' | 'number' | 'boolean';
|
|
10
|
+
required?: boolean;
|
|
11
|
+
default?: unknown;
|
|
12
|
+
description?: string;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
interface KeybindingContribution {
|
|
16
|
+
command: string;
|
|
17
|
+
key: string;
|
|
18
|
+
when?: string;
|
|
19
|
+
args?: unknown;
|
|
20
|
+
}
|
|
21
|
+
interface PanelContribution {
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
icon?: string;
|
|
25
|
+
location: 'sidebar' | 'sidebar.bottom' | 'bottombar';
|
|
26
|
+
initialCollapsed?: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface StatusBarContribution {
|
|
29
|
+
id: string;
|
|
30
|
+
align: 'left' | 'right';
|
|
31
|
+
text?: string;
|
|
32
|
+
icon?: string;
|
|
33
|
+
tooltip?: string;
|
|
34
|
+
command?: string;
|
|
35
|
+
refreshOn?: string[];
|
|
36
|
+
priority?: number;
|
|
37
|
+
}
|
|
38
|
+
interface ContextMenuContribution {
|
|
39
|
+
command: string;
|
|
40
|
+
context: string;
|
|
41
|
+
when?: string;
|
|
42
|
+
group?: string;
|
|
43
|
+
label?: string;
|
|
44
|
+
}
|
|
45
|
+
interface TabTypeContribution {
|
|
46
|
+
id: string;
|
|
47
|
+
title: string;
|
|
48
|
+
icon?: string;
|
|
49
|
+
}
|
|
50
|
+
interface DecoratorContribution {
|
|
51
|
+
id: string;
|
|
52
|
+
appliesTo: 'terminal.output';
|
|
53
|
+
}
|
|
54
|
+
interface ThemeContribution {
|
|
55
|
+
id: string;
|
|
56
|
+
label: string;
|
|
57
|
+
path: string;
|
|
58
|
+
}
|
|
59
|
+
type AiProviderId = 'anthropic' | 'openai' | 'ollama';
|
|
60
|
+
interface AiBindingContribution {
|
|
61
|
+
id: string;
|
|
62
|
+
label: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
supportsCore?: boolean;
|
|
65
|
+
providers?: AiProviderId[];
|
|
66
|
+
defaultProvider?: AiProviderId;
|
|
67
|
+
defaultModels?: Partial<Record<AiProviderId, string>>;
|
|
68
|
+
}
|
|
69
|
+
interface SecretContribution {
|
|
70
|
+
key: string;
|
|
71
|
+
label: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
link?: string;
|
|
74
|
+
placeholder?: string;
|
|
75
|
+
}
|
|
76
|
+
interface JsonSchema {
|
|
77
|
+
type?: 'object' | 'string' | 'number' | 'boolean' | 'array';
|
|
78
|
+
title?: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
default?: unknown;
|
|
81
|
+
enum?: Array<string | number>;
|
|
82
|
+
properties?: Record<string, JsonSchema>;
|
|
83
|
+
items?: JsonSchema;
|
|
84
|
+
required?: string[];
|
|
85
|
+
minimum?: number;
|
|
86
|
+
maximum?: number;
|
|
87
|
+
pattern?: string;
|
|
88
|
+
}
|
|
89
|
+
interface PublisherInfo {
|
|
90
|
+
authorId: string;
|
|
91
|
+
keyId: string;
|
|
92
|
+
}
|
|
93
|
+
interface ExtensionManifest {
|
|
94
|
+
id: string;
|
|
95
|
+
packageName: string;
|
|
96
|
+
version: string;
|
|
97
|
+
displayName?: string;
|
|
98
|
+
description?: string;
|
|
99
|
+
author?: string;
|
|
100
|
+
icon?: string;
|
|
101
|
+
homepageUrl?: string;
|
|
102
|
+
repoUrl?: string;
|
|
103
|
+
category?: string;
|
|
104
|
+
apiVersionRange: string;
|
|
105
|
+
mainEntry: string | null;
|
|
106
|
+
rendererEntry: string | null;
|
|
107
|
+
activationEvents: ActivationEvent[];
|
|
108
|
+
capabilities: string[];
|
|
109
|
+
enabledApiProposals: string[];
|
|
110
|
+
allowedNetworkDomains: string[];
|
|
111
|
+
publisher: PublisherInfo;
|
|
112
|
+
providedServices: Record<string, {
|
|
113
|
+
version: string;
|
|
114
|
+
}>;
|
|
115
|
+
consumedServices: Record<string, {
|
|
116
|
+
versionRange: string;
|
|
117
|
+
optional?: boolean;
|
|
118
|
+
}>;
|
|
119
|
+
contributes: {
|
|
120
|
+
commands: CommandContribution[];
|
|
121
|
+
keybindings: KeybindingContribution[];
|
|
122
|
+
settings: JsonSchema | null;
|
|
123
|
+
panels: PanelContribution[];
|
|
124
|
+
statusBar: StatusBarContribution[];
|
|
125
|
+
contextMenu: ContextMenuContribution[];
|
|
126
|
+
tabTypes: TabTypeContribution[];
|
|
127
|
+
decorators: DecoratorContribution[];
|
|
128
|
+
themes: ThemeContribution[];
|
|
129
|
+
secrets: SecretContribution[];
|
|
130
|
+
aiBindings: AiBindingContribution[];
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
declare const CAPABILITY_WHITELIST: readonly ["child-process", "network:limited", "network:full", "filesystem:read", "filesystem:write", "clipboard", "notifications", "keychain"];
|
|
134
|
+
type Capability = (typeof CAPABILITY_WHITELIST)[number];
|
|
135
|
+
interface ValidationOk {
|
|
136
|
+
ok: true;
|
|
137
|
+
manifest: ExtensionManifest;
|
|
138
|
+
}
|
|
139
|
+
interface ValidationErr {
|
|
140
|
+
ok: false;
|
|
141
|
+
errors: string[];
|
|
142
|
+
}
|
|
143
|
+
type ValidationResult = ValidationOk | ValidationErr;
|
|
144
|
+
declare function validateManifest(input: unknown): ValidationResult;
|
|
145
|
+
|
|
146
|
+
export { type ActivationEvent, type AiBindingContribution, type AiProviderId, CAPABILITY_WHITELIST, type Capability, type CommandContribution, type ContextMenuContribution, type DecoratorContribution, type ExtensionManifest, type JsonSchema, type KeybindingContribution, type PanelContribution, type PublisherInfo, type SecretContribution, type StatusBarContribution, type TabTypeContribution, type ThemeContribution, type ValidationErr, type ValidationOk, type ValidationResult, validateManifest };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var CAPABILITY_WHITELIST = [
|
|
3
|
+
"child-process",
|
|
4
|
+
"network:limited",
|
|
5
|
+
"network:full",
|
|
6
|
+
"filesystem:read",
|
|
7
|
+
"filesystem:write",
|
|
8
|
+
"clipboard",
|
|
9
|
+
"notifications",
|
|
10
|
+
"keychain"
|
|
11
|
+
];
|
|
12
|
+
var KEBAB_CASE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
13
|
+
var SEMVER_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
14
|
+
var SEMVER_RANGE = /^[\^~><=*\d.\sxX|-]+$/;
|
|
15
|
+
function validateManifest(input) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
const o = isObject(input) ? input : {};
|
|
18
|
+
const packageName = typeof o.name === "string" ? o.name : "";
|
|
19
|
+
if (!packageName) errors.push('missing "name"');
|
|
20
|
+
const version = typeof o.version === "string" ? o.version : "";
|
|
21
|
+
if (!version) errors.push('missing "version"');
|
|
22
|
+
else if (!SEMVER_VERSION.test(version)) errors.push(`invalid semver "version": ${version}`);
|
|
23
|
+
const mt = isObject(o.mterminal) ? o.mterminal : null;
|
|
24
|
+
if (!mt) {
|
|
25
|
+
errors.push('missing "mterminal" block');
|
|
26
|
+
return { ok: false, errors };
|
|
27
|
+
}
|
|
28
|
+
const id = typeof mt.id === "string" ? mt.id : "";
|
|
29
|
+
if (!id) errors.push('missing "mterminal.id"');
|
|
30
|
+
else if (!KEBAB_CASE.test(id)) errors.push(`"mterminal.id" must be kebab-case: ${id}`);
|
|
31
|
+
const engines = isObject(o.engines) ? o.engines : {};
|
|
32
|
+
const apiVersionRange = typeof engines["mterminal-api"] === "string" ? engines["mterminal-api"] : "";
|
|
33
|
+
if (!apiVersionRange) errors.push('missing "engines.mterminal-api"');
|
|
34
|
+
else if (!SEMVER_RANGE.test(apiVersionRange))
|
|
35
|
+
errors.push(`invalid semver range "engines.mterminal-api": ${apiVersionRange}`);
|
|
36
|
+
const publisherRaw = isObject(mt.publisher) ? mt.publisher : null;
|
|
37
|
+
const publisher = { authorId: "", keyId: "" };
|
|
38
|
+
if (!publisherRaw) {
|
|
39
|
+
errors.push('missing "mterminal.publisher"');
|
|
40
|
+
} else {
|
|
41
|
+
if (typeof publisherRaw.authorId !== "string" || !publisherRaw.authorId)
|
|
42
|
+
errors.push('missing "mterminal.publisher.authorId"');
|
|
43
|
+
else publisher.authorId = publisherRaw.authorId;
|
|
44
|
+
if (typeof publisherRaw.keyId !== "string" || !publisherRaw.keyId)
|
|
45
|
+
errors.push('missing "mterminal.publisher.keyId"');
|
|
46
|
+
else publisher.keyId = publisherRaw.keyId;
|
|
47
|
+
}
|
|
48
|
+
const main = typeof o.main === "string" ? o.main : null;
|
|
49
|
+
const renderer = typeof o.renderer === "string" ? o.renderer : null;
|
|
50
|
+
const activationEvents = readArrayOf(mt.activationEvents, isString);
|
|
51
|
+
if (activationEvents.length === 0)
|
|
52
|
+
errors.push('"mterminal.activationEvents" is required and must be a non-empty array');
|
|
53
|
+
for (const ev of activationEvents) {
|
|
54
|
+
if (!isValidActivationEvent(ev)) errors.push(`unknown activation event: ${ev}`);
|
|
55
|
+
}
|
|
56
|
+
const capabilities = readArrayOf(mt.capabilities, isString);
|
|
57
|
+
for (const cap of capabilities) {
|
|
58
|
+
if (!CAPABILITY_WHITELIST.includes(cap))
|
|
59
|
+
errors.push(`capability "${cap}" is not in the whitelist`);
|
|
60
|
+
}
|
|
61
|
+
const allowedNetworkDomains = readArrayOf(mt.allowedNetworkDomains, isString);
|
|
62
|
+
if (capabilities.includes("network:full") && allowedNetworkDomains.length === 0) {
|
|
63
|
+
errors.push('"network:full" capability requires "mterminal.allowedNetworkDomains" with at least one entry');
|
|
64
|
+
}
|
|
65
|
+
const enabledApiProposals = readArrayOf(mt.enabledApiProposals, isString);
|
|
66
|
+
const providedServices = readProvidedServices(mt.providedServices, errors);
|
|
67
|
+
const consumedServices = readConsumedServices(mt.consumedServices, errors);
|
|
68
|
+
const contributes = readContributes(mt.contributes, errors);
|
|
69
|
+
const declarativeOk = Array.isArray(contributes.themes) && contributes.themes.length > 0;
|
|
70
|
+
if (!main && !renderer && !declarativeOk) {
|
|
71
|
+
errors.push('extension must declare "main", "renderer", or at least one declarative theme contribution');
|
|
72
|
+
}
|
|
73
|
+
if (errors.length) return { ok: false, errors };
|
|
74
|
+
const manifest = {
|
|
75
|
+
id,
|
|
76
|
+
packageName,
|
|
77
|
+
version,
|
|
78
|
+
displayName: typeof mt.displayName === "string" ? mt.displayName : void 0,
|
|
79
|
+
description: typeof o.description === "string" ? o.description : void 0,
|
|
80
|
+
author: typeof o.author === "string" ? o.author : isObject(o.author) && typeof o.author.name === "string" ? o.author.name : void 0,
|
|
81
|
+
icon: typeof mt.icon === "string" ? mt.icon : void 0,
|
|
82
|
+
homepageUrl: typeof o.homepage === "string" ? o.homepage : void 0,
|
|
83
|
+
repoUrl: typeof o.repository === "string" ? o.repository : isObject(o.repository) && typeof o.repository.url === "string" ? o.repository.url : void 0,
|
|
84
|
+
category: typeof mt.category === "string" ? mt.category : void 0,
|
|
85
|
+
apiVersionRange,
|
|
86
|
+
mainEntry: main,
|
|
87
|
+
rendererEntry: renderer,
|
|
88
|
+
activationEvents,
|
|
89
|
+
capabilities,
|
|
90
|
+
enabledApiProposals,
|
|
91
|
+
allowedNetworkDomains,
|
|
92
|
+
publisher,
|
|
93
|
+
providedServices,
|
|
94
|
+
consumedServices,
|
|
95
|
+
contributes
|
|
96
|
+
};
|
|
97
|
+
return { ok: true, manifest };
|
|
98
|
+
}
|
|
99
|
+
function isObject(v) {
|
|
100
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
101
|
+
}
|
|
102
|
+
function isString(v) {
|
|
103
|
+
return typeof v === "string";
|
|
104
|
+
}
|
|
105
|
+
function readArrayOf(v, guard) {
|
|
106
|
+
if (!Array.isArray(v)) return [];
|
|
107
|
+
return v.filter(guard);
|
|
108
|
+
}
|
|
109
|
+
var ACTIVATION_PREFIXES = [
|
|
110
|
+
"onCommand:",
|
|
111
|
+
"onView:",
|
|
112
|
+
"onTabType:",
|
|
113
|
+
"onUri:",
|
|
114
|
+
"onEvent:"
|
|
115
|
+
];
|
|
116
|
+
function isValidActivationEvent(ev) {
|
|
117
|
+
if (ev === "onStartupFinished") return true;
|
|
118
|
+
if (ev === "onSelection") return true;
|
|
119
|
+
return ACTIVATION_PREFIXES.some((p) => ev.startsWith(p) && ev.length > p.length);
|
|
120
|
+
}
|
|
121
|
+
function readProvidedServices(v, errors) {
|
|
122
|
+
const out = {};
|
|
123
|
+
if (!isObject(v)) return out;
|
|
124
|
+
for (const [id, entry] of Object.entries(v)) {
|
|
125
|
+
if (!isObject(entry) || typeof entry.version !== "string") {
|
|
126
|
+
errors.push(`providedServices["${id}"] missing "version"`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
out[id] = { version: entry.version };
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
function readConsumedServices(v, errors) {
|
|
134
|
+
const out = {};
|
|
135
|
+
if (!isObject(v)) return out;
|
|
136
|
+
for (const [id, entry] of Object.entries(v)) {
|
|
137
|
+
if (!isObject(entry) || typeof entry.versionRange !== "string") {
|
|
138
|
+
errors.push(`consumedServices["${id}"] missing "versionRange"`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
out[id] = {
|
|
142
|
+
versionRange: entry.versionRange,
|
|
143
|
+
optional: typeof entry.optional === "boolean" ? entry.optional : void 0
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function readContributes(v, errors) {
|
|
149
|
+
const c = isObject(v) ? v : {};
|
|
150
|
+
return {
|
|
151
|
+
commands: readArrayOf(
|
|
152
|
+
c.commands,
|
|
153
|
+
(x) => isObject(x) && typeof x.id === "string"
|
|
154
|
+
),
|
|
155
|
+
keybindings: readArrayOf(
|
|
156
|
+
c.keybindings,
|
|
157
|
+
(x) => isObject(x) && typeof x.command === "string" && typeof x.key === "string"
|
|
158
|
+
),
|
|
159
|
+
settings: isObject(c.settings) ? c.settings : null,
|
|
160
|
+
panels: readArrayOf(c.panels, (x) => {
|
|
161
|
+
if (!isObject(x)) return false;
|
|
162
|
+
if (typeof x.id !== "string" || typeof x.title !== "string") return false;
|
|
163
|
+
const loc = x.location;
|
|
164
|
+
if (loc !== "sidebar" && loc !== "sidebar.bottom" && loc !== "bottombar") {
|
|
165
|
+
errors.push(`panel "${String(x.id)}" has invalid location "${String(loc)}"`);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}),
|
|
170
|
+
statusBar: readArrayOf(
|
|
171
|
+
c.statusBar,
|
|
172
|
+
(x) => isObject(x) && typeof x.id === "string" && (x.align === "left" || x.align === "right")
|
|
173
|
+
),
|
|
174
|
+
contextMenu: readArrayOf(
|
|
175
|
+
c.contextMenu,
|
|
176
|
+
(x) => isObject(x) && typeof x.command === "string" && typeof x.context === "string"
|
|
177
|
+
),
|
|
178
|
+
tabTypes: readArrayOf(
|
|
179
|
+
c.tabTypes,
|
|
180
|
+
(x) => isObject(x) && typeof x.id === "string" && typeof x.title === "string"
|
|
181
|
+
),
|
|
182
|
+
decorators: readArrayOf(
|
|
183
|
+
c.decorators,
|
|
184
|
+
(x) => isObject(x) && typeof x.id === "string" && x.appliesTo === "terminal.output"
|
|
185
|
+
),
|
|
186
|
+
themes: readArrayOf(
|
|
187
|
+
c.themes,
|
|
188
|
+
(x) => isObject(x) && typeof x.id === "string" && typeof x.label === "string" && typeof x.path === "string"
|
|
189
|
+
),
|
|
190
|
+
secrets: readArrayOf(
|
|
191
|
+
c.secrets,
|
|
192
|
+
(x) => isObject(x) && typeof x.key === "string" && typeof x.label === "string"
|
|
193
|
+
),
|
|
194
|
+
aiBindings: readArrayOf(
|
|
195
|
+
c.aiBindings,
|
|
196
|
+
(x) => isObject(x) && typeof x.id === "string" && typeof x.label === "string"
|
|
197
|
+
)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
CAPABILITY_WHITELIST,
|
|
202
|
+
validateManifest
|
|
203
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mterminal/manifest-validator",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "shared manifest validator for mterminal extensions",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"tsup": "^8.3.5",
|
|
21
|
+
"typescript": "^5.6.3",
|
|
22
|
+
"vitest": "^2.1.5"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"test": "vitest run"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { validateManifest } from './index'
|
|
3
|
+
|
|
4
|
+
const baseManifest = () => ({
|
|
5
|
+
name: 'mterminal-plugin-demo',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
main: 'dist/main.cjs',
|
|
8
|
+
engines: { 'mterminal-api': '^1.0.0' },
|
|
9
|
+
mterminal: {
|
|
10
|
+
id: 'demo',
|
|
11
|
+
displayName: 'demo',
|
|
12
|
+
publisher: { authorId: 'gh-1234', keyId: 'gh-1234:key1' },
|
|
13
|
+
activationEvents: ['onStartupFinished'],
|
|
14
|
+
capabilities: ['clipboard'],
|
|
15
|
+
contributes: {},
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('validateManifest', () => {
|
|
20
|
+
it('accepts a minimal valid manifest', () => {
|
|
21
|
+
const result = validateManifest(baseManifest())
|
|
22
|
+
expect(result.ok).toBe(true)
|
|
23
|
+
if (result.ok) {
|
|
24
|
+
expect(result.manifest.id).toBe('demo')
|
|
25
|
+
expect(result.manifest.publisher.authorId).toBe('gh-1234')
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('rejects missing mterminal block', () => {
|
|
30
|
+
const result = validateManifest({ name: 'foo', version: '1.0.0' })
|
|
31
|
+
expect(result.ok).toBe(false)
|
|
32
|
+
if (!result.ok) expect(result.errors.join('\n')).toMatch(/mterminal/)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('rejects non-kebab-case id', () => {
|
|
36
|
+
const m = baseManifest()
|
|
37
|
+
m.mterminal.id = 'BadName'
|
|
38
|
+
const result = validateManifest(m)
|
|
39
|
+
expect(result.ok).toBe(false)
|
|
40
|
+
if (!result.ok) expect(result.errors.join('\n')).toMatch(/kebab-case/)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('rejects bad semver', () => {
|
|
44
|
+
const m = baseManifest()
|
|
45
|
+
m.version = 'not-a-version'
|
|
46
|
+
const result = validateManifest(m)
|
|
47
|
+
expect(result.ok).toBe(false)
|
|
48
|
+
if (!result.ok) expect(result.errors.join('\n')).toMatch(/semver/)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('rejects missing publisher.authorId', () => {
|
|
52
|
+
const m: any = baseManifest()
|
|
53
|
+
delete m.mterminal.publisher.authorId
|
|
54
|
+
const result = validateManifest(m)
|
|
55
|
+
expect(result.ok).toBe(false)
|
|
56
|
+
if (!result.ok) expect(result.errors.join('\n')).toMatch(/authorId/)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('rejects unknown capability', () => {
|
|
60
|
+
const m = baseManifest()
|
|
61
|
+
m.mterminal.capabilities = ['rootkit'] as any
|
|
62
|
+
const result = validateManifest(m)
|
|
63
|
+
expect(result.ok).toBe(false)
|
|
64
|
+
if (!result.ok) expect(result.errors.join('\n')).toMatch(/whitelist/)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('requires allowedNetworkDomains for network:full', () => {
|
|
68
|
+
const m = baseManifest()
|
|
69
|
+
m.mterminal.capabilities = ['network:full']
|
|
70
|
+
const result = validateManifest(m)
|
|
71
|
+
expect(result.ok).toBe(false)
|
|
72
|
+
if (!result.ok) expect(result.errors.join('\n')).toMatch(/allowedNetworkDomains/)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('accepts network:full with allowedNetworkDomains', () => {
|
|
76
|
+
const m: any = baseManifest()
|
|
77
|
+
m.mterminal.capabilities = ['network:full']
|
|
78
|
+
m.mterminal.allowedNetworkDomains = ['api.example.com']
|
|
79
|
+
const result = validateManifest(m)
|
|
80
|
+
expect(result.ok).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('rejects invalid activation event', () => {
|
|
84
|
+
const m = baseManifest()
|
|
85
|
+
m.mterminal.activationEvents = ['onAlien:foo'] as any
|
|
86
|
+
const result = validateManifest(m)
|
|
87
|
+
expect(result.ok).toBe(false)
|
|
88
|
+
if (!result.ok) expect(result.errors.join('\n')).toMatch(/activation/)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('requires at least one of main/renderer/declarative themes', () => {
|
|
92
|
+
const m: any = baseManifest()
|
|
93
|
+
delete m.main
|
|
94
|
+
const result = validateManifest(m)
|
|
95
|
+
expect(result.ok).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('accepts theme-only declarative manifest', () => {
|
|
99
|
+
const m: any = baseManifest()
|
|
100
|
+
delete m.main
|
|
101
|
+
m.mterminal.contributes = {
|
|
102
|
+
themes: [{ id: 'foo', label: 'foo', path: './themes/foo.json' }],
|
|
103
|
+
}
|
|
104
|
+
const result = validateManifest(m)
|
|
105
|
+
expect(result.ok).toBe(true)
|
|
106
|
+
})
|
|
107
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
export type ActivationEvent =
|
|
2
|
+
| 'onStartupFinished'
|
|
3
|
+
| 'onSelection'
|
|
4
|
+
| `onCommand:${string}`
|
|
5
|
+
| `onView:${string}`
|
|
6
|
+
| `onTabType:${string}`
|
|
7
|
+
| `onUri:${string}`
|
|
8
|
+
| `onEvent:${string}`
|
|
9
|
+
|
|
10
|
+
export interface CommandContribution {
|
|
11
|
+
id: string
|
|
12
|
+
title?: string
|
|
13
|
+
category?: string
|
|
14
|
+
icon?: string
|
|
15
|
+
args?: Array<{
|
|
16
|
+
name: string
|
|
17
|
+
type: 'string' | 'number' | 'boolean'
|
|
18
|
+
required?: boolean
|
|
19
|
+
default?: unknown
|
|
20
|
+
description?: string
|
|
21
|
+
}>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface KeybindingContribution {
|
|
25
|
+
command: string
|
|
26
|
+
key: string
|
|
27
|
+
when?: string
|
|
28
|
+
args?: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PanelContribution {
|
|
32
|
+
id: string
|
|
33
|
+
title: string
|
|
34
|
+
icon?: string
|
|
35
|
+
location: 'sidebar' | 'sidebar.bottom' | 'bottombar'
|
|
36
|
+
initialCollapsed?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface StatusBarContribution {
|
|
40
|
+
id: string
|
|
41
|
+
align: 'left' | 'right'
|
|
42
|
+
text?: string
|
|
43
|
+
icon?: string
|
|
44
|
+
tooltip?: string
|
|
45
|
+
command?: string
|
|
46
|
+
refreshOn?: string[]
|
|
47
|
+
priority?: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ContextMenuContribution {
|
|
51
|
+
command: string
|
|
52
|
+
context: string
|
|
53
|
+
when?: string
|
|
54
|
+
group?: string
|
|
55
|
+
label?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface TabTypeContribution {
|
|
59
|
+
id: string
|
|
60
|
+
title: string
|
|
61
|
+
icon?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DecoratorContribution {
|
|
65
|
+
id: string
|
|
66
|
+
appliesTo: 'terminal.output'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ThemeContribution {
|
|
70
|
+
id: string
|
|
71
|
+
label: string
|
|
72
|
+
path: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type AiProviderId = 'anthropic' | 'openai' | 'ollama'
|
|
76
|
+
|
|
77
|
+
export interface AiBindingContribution {
|
|
78
|
+
id: string
|
|
79
|
+
label: string
|
|
80
|
+
description?: string
|
|
81
|
+
supportsCore?: boolean
|
|
82
|
+
providers?: AiProviderId[]
|
|
83
|
+
defaultProvider?: AiProviderId
|
|
84
|
+
defaultModels?: Partial<Record<AiProviderId, string>>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface SecretContribution {
|
|
88
|
+
key: string
|
|
89
|
+
label: string
|
|
90
|
+
description?: string
|
|
91
|
+
link?: string
|
|
92
|
+
placeholder?: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface JsonSchema {
|
|
96
|
+
type?: 'object' | 'string' | 'number' | 'boolean' | 'array'
|
|
97
|
+
title?: string
|
|
98
|
+
description?: string
|
|
99
|
+
default?: unknown
|
|
100
|
+
enum?: Array<string | number>
|
|
101
|
+
properties?: Record<string, JsonSchema>
|
|
102
|
+
items?: JsonSchema
|
|
103
|
+
required?: string[]
|
|
104
|
+
minimum?: number
|
|
105
|
+
maximum?: number
|
|
106
|
+
pattern?: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface PublisherInfo {
|
|
110
|
+
authorId: string
|
|
111
|
+
keyId: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ExtensionManifest {
|
|
115
|
+
id: string
|
|
116
|
+
packageName: string
|
|
117
|
+
version: string
|
|
118
|
+
displayName?: string
|
|
119
|
+
description?: string
|
|
120
|
+
author?: string
|
|
121
|
+
icon?: string
|
|
122
|
+
homepageUrl?: string
|
|
123
|
+
repoUrl?: string
|
|
124
|
+
category?: string
|
|
125
|
+
apiVersionRange: string
|
|
126
|
+
mainEntry: string | null
|
|
127
|
+
rendererEntry: string | null
|
|
128
|
+
activationEvents: ActivationEvent[]
|
|
129
|
+
capabilities: string[]
|
|
130
|
+
enabledApiProposals: string[]
|
|
131
|
+
allowedNetworkDomains: string[]
|
|
132
|
+
publisher: PublisherInfo
|
|
133
|
+
providedServices: Record<string, { version: string }>
|
|
134
|
+
consumedServices: Record<string, { versionRange: string; optional?: boolean }>
|
|
135
|
+
contributes: {
|
|
136
|
+
commands: CommandContribution[]
|
|
137
|
+
keybindings: KeybindingContribution[]
|
|
138
|
+
settings: JsonSchema | null
|
|
139
|
+
panels: PanelContribution[]
|
|
140
|
+
statusBar: StatusBarContribution[]
|
|
141
|
+
contextMenu: ContextMenuContribution[]
|
|
142
|
+
tabTypes: TabTypeContribution[]
|
|
143
|
+
decorators: DecoratorContribution[]
|
|
144
|
+
themes: ThemeContribution[]
|
|
145
|
+
secrets: SecretContribution[]
|
|
146
|
+
aiBindings: AiBindingContribution[]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const CAPABILITY_WHITELIST = [
|
|
151
|
+
'child-process',
|
|
152
|
+
'network:limited',
|
|
153
|
+
'network:full',
|
|
154
|
+
'filesystem:read',
|
|
155
|
+
'filesystem:write',
|
|
156
|
+
'clipboard',
|
|
157
|
+
'notifications',
|
|
158
|
+
'keychain',
|
|
159
|
+
] as const
|
|
160
|
+
|
|
161
|
+
export type Capability = (typeof CAPABILITY_WHITELIST)[number]
|
|
162
|
+
|
|
163
|
+
export interface ValidationOk {
|
|
164
|
+
ok: true
|
|
165
|
+
manifest: ExtensionManifest
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface ValidationErr {
|
|
169
|
+
ok: false
|
|
170
|
+
errors: string[]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export type ValidationResult = ValidationOk | ValidationErr
|
|
174
|
+
|
|
175
|
+
const KEBAB_CASE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/
|
|
176
|
+
const SEMVER_VERSION =
|
|
177
|
+
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
178
|
+
const SEMVER_RANGE = /^[\^~><=*\d.\sxX|-]+$/
|
|
179
|
+
|
|
180
|
+
export function validateManifest(input: unknown): ValidationResult {
|
|
181
|
+
const errors: string[] = []
|
|
182
|
+
const o = isObject(input) ? input : {}
|
|
183
|
+
|
|
184
|
+
const packageName = typeof o.name === 'string' ? o.name : ''
|
|
185
|
+
if (!packageName) errors.push('missing "name"')
|
|
186
|
+
|
|
187
|
+
const version = typeof o.version === 'string' ? o.version : ''
|
|
188
|
+
if (!version) errors.push('missing "version"')
|
|
189
|
+
else if (!SEMVER_VERSION.test(version)) errors.push(`invalid semver "version": ${version}`)
|
|
190
|
+
|
|
191
|
+
const mt = isObject(o.mterminal) ? o.mterminal : null
|
|
192
|
+
if (!mt) {
|
|
193
|
+
errors.push('missing "mterminal" block')
|
|
194
|
+
return { ok: false, errors }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const id = typeof mt.id === 'string' ? mt.id : ''
|
|
198
|
+
if (!id) errors.push('missing "mterminal.id"')
|
|
199
|
+
else if (!KEBAB_CASE.test(id)) errors.push(`"mterminal.id" must be kebab-case: ${id}`)
|
|
200
|
+
|
|
201
|
+
const engines = isObject(o.engines) ? o.engines : {}
|
|
202
|
+
const apiVersionRange =
|
|
203
|
+
typeof engines['mterminal-api'] === 'string' ? (engines['mterminal-api'] as string) : ''
|
|
204
|
+
if (!apiVersionRange) errors.push('missing "engines.mterminal-api"')
|
|
205
|
+
else if (!SEMVER_RANGE.test(apiVersionRange))
|
|
206
|
+
errors.push(`invalid semver range "engines.mterminal-api": ${apiVersionRange}`)
|
|
207
|
+
|
|
208
|
+
const publisherRaw = isObject(mt.publisher) ? mt.publisher : null
|
|
209
|
+
const publisher: PublisherInfo = { authorId: '', keyId: '' }
|
|
210
|
+
if (!publisherRaw) {
|
|
211
|
+
errors.push('missing "mterminal.publisher"')
|
|
212
|
+
} else {
|
|
213
|
+
if (typeof publisherRaw.authorId !== 'string' || !publisherRaw.authorId)
|
|
214
|
+
errors.push('missing "mterminal.publisher.authorId"')
|
|
215
|
+
else publisher.authorId = publisherRaw.authorId
|
|
216
|
+
if (typeof publisherRaw.keyId !== 'string' || !publisherRaw.keyId)
|
|
217
|
+
errors.push('missing "mterminal.publisher.keyId"')
|
|
218
|
+
else publisher.keyId = publisherRaw.keyId
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const main = typeof o.main === 'string' ? o.main : null
|
|
222
|
+
const renderer = typeof o.renderer === 'string' ? o.renderer : null
|
|
223
|
+
|
|
224
|
+
const activationEvents = readArrayOf<string>(mt.activationEvents, isString)
|
|
225
|
+
if (activationEvents.length === 0)
|
|
226
|
+
errors.push('"mterminal.activationEvents" is required and must be a non-empty array')
|
|
227
|
+
for (const ev of activationEvents) {
|
|
228
|
+
if (!isValidActivationEvent(ev)) errors.push(`unknown activation event: ${ev}`)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const capabilities = readArrayOf<string>(mt.capabilities, isString)
|
|
232
|
+
for (const cap of capabilities) {
|
|
233
|
+
if (!CAPABILITY_WHITELIST.includes(cap as Capability))
|
|
234
|
+
errors.push(`capability "${cap}" is not in the whitelist`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const allowedNetworkDomains = readArrayOf<string>(mt.allowedNetworkDomains, isString)
|
|
238
|
+
if (capabilities.includes('network:full') && allowedNetworkDomains.length === 0) {
|
|
239
|
+
errors.push('"network:full" capability requires "mterminal.allowedNetworkDomains" with at least one entry')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const enabledApiProposals = readArrayOf<string>(mt.enabledApiProposals, isString)
|
|
243
|
+
|
|
244
|
+
const providedServices = readProvidedServices(mt.providedServices, errors)
|
|
245
|
+
const consumedServices = readConsumedServices(mt.consumedServices, errors)
|
|
246
|
+
const contributes = readContributes(mt.contributes, errors)
|
|
247
|
+
|
|
248
|
+
const declarativeOk =
|
|
249
|
+
Array.isArray(contributes.themes) && contributes.themes.length > 0
|
|
250
|
+
if (!main && !renderer && !declarativeOk) {
|
|
251
|
+
errors.push('extension must declare "main", "renderer", or at least one declarative theme contribution')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (errors.length) return { ok: false, errors }
|
|
255
|
+
|
|
256
|
+
const manifest: ExtensionManifest = {
|
|
257
|
+
id,
|
|
258
|
+
packageName,
|
|
259
|
+
version,
|
|
260
|
+
displayName: typeof mt.displayName === 'string' ? mt.displayName : undefined,
|
|
261
|
+
description: typeof o.description === 'string' ? o.description : undefined,
|
|
262
|
+
author:
|
|
263
|
+
typeof o.author === 'string'
|
|
264
|
+
? o.author
|
|
265
|
+
: isObject(o.author) && typeof o.author.name === 'string'
|
|
266
|
+
? o.author.name
|
|
267
|
+
: undefined,
|
|
268
|
+
icon: typeof mt.icon === 'string' ? mt.icon : undefined,
|
|
269
|
+
homepageUrl: typeof o.homepage === 'string' ? o.homepage : undefined,
|
|
270
|
+
repoUrl:
|
|
271
|
+
typeof o.repository === 'string'
|
|
272
|
+
? o.repository
|
|
273
|
+
: isObject(o.repository) && typeof o.repository.url === 'string'
|
|
274
|
+
? o.repository.url
|
|
275
|
+
: undefined,
|
|
276
|
+
category: typeof mt.category === 'string' ? mt.category : undefined,
|
|
277
|
+
apiVersionRange,
|
|
278
|
+
mainEntry: main,
|
|
279
|
+
rendererEntry: renderer,
|
|
280
|
+
activationEvents: activationEvents as ActivationEvent[],
|
|
281
|
+
capabilities,
|
|
282
|
+
enabledApiProposals,
|
|
283
|
+
allowedNetworkDomains,
|
|
284
|
+
publisher,
|
|
285
|
+
providedServices,
|
|
286
|
+
consumedServices,
|
|
287
|
+
contributes,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { ok: true, manifest }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
294
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isString(v: unknown): v is string {
|
|
298
|
+
return typeof v === 'string'
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function readArrayOf<T>(v: unknown, guard: (x: unknown) => x is T): T[] {
|
|
302
|
+
if (!Array.isArray(v)) return []
|
|
303
|
+
return v.filter(guard)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const ACTIVATION_PREFIXES = [
|
|
307
|
+
'onCommand:',
|
|
308
|
+
'onView:',
|
|
309
|
+
'onTabType:',
|
|
310
|
+
'onUri:',
|
|
311
|
+
'onEvent:',
|
|
312
|
+
] as const
|
|
313
|
+
|
|
314
|
+
function isValidActivationEvent(ev: string): boolean {
|
|
315
|
+
if (ev === 'onStartupFinished') return true
|
|
316
|
+
if (ev === 'onSelection') return true
|
|
317
|
+
return ACTIVATION_PREFIXES.some((p) => ev.startsWith(p) && ev.length > p.length)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function readProvidedServices(
|
|
321
|
+
v: unknown,
|
|
322
|
+
errors: string[],
|
|
323
|
+
): Record<string, { version: string }> {
|
|
324
|
+
const out: Record<string, { version: string }> = {}
|
|
325
|
+
if (!isObject(v)) return out
|
|
326
|
+
for (const [id, entry] of Object.entries(v)) {
|
|
327
|
+
if (!isObject(entry) || typeof entry.version !== 'string') {
|
|
328
|
+
errors.push(`providedServices["${id}"] missing "version"`)
|
|
329
|
+
continue
|
|
330
|
+
}
|
|
331
|
+
out[id] = { version: entry.version }
|
|
332
|
+
}
|
|
333
|
+
return out
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function readConsumedServices(
|
|
337
|
+
v: unknown,
|
|
338
|
+
errors: string[],
|
|
339
|
+
): Record<string, { versionRange: string; optional?: boolean }> {
|
|
340
|
+
const out: Record<string, { versionRange: string; optional?: boolean }> = {}
|
|
341
|
+
if (!isObject(v)) return out
|
|
342
|
+
for (const [id, entry] of Object.entries(v)) {
|
|
343
|
+
if (!isObject(entry) || typeof entry.versionRange !== 'string') {
|
|
344
|
+
errors.push(`consumedServices["${id}"] missing "versionRange"`)
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
out[id] = {
|
|
348
|
+
versionRange: entry.versionRange,
|
|
349
|
+
optional: typeof entry.optional === 'boolean' ? entry.optional : undefined,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return out
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function readContributes(v: unknown, errors: string[]): ExtensionManifest['contributes'] {
|
|
356
|
+
const c = isObject(v) ? v : {}
|
|
357
|
+
return {
|
|
358
|
+
commands: readArrayOf(
|
|
359
|
+
c.commands,
|
|
360
|
+
(x): x is CommandContribution => isObject(x) && typeof x.id === 'string',
|
|
361
|
+
),
|
|
362
|
+
keybindings: readArrayOf(
|
|
363
|
+
c.keybindings,
|
|
364
|
+
(x): x is KeybindingContribution =>
|
|
365
|
+
isObject(x) && typeof x.command === 'string' && typeof x.key === 'string',
|
|
366
|
+
),
|
|
367
|
+
settings: isObject(c.settings) ? (c.settings as JsonSchema) : null,
|
|
368
|
+
panels: readArrayOf(c.panels, (x): x is PanelContribution => {
|
|
369
|
+
if (!isObject(x)) return false
|
|
370
|
+
if (typeof x.id !== 'string' || typeof x.title !== 'string') return false
|
|
371
|
+
const loc = x.location
|
|
372
|
+
if (loc !== 'sidebar' && loc !== 'sidebar.bottom' && loc !== 'bottombar') {
|
|
373
|
+
errors.push(`panel "${String(x.id)}" has invalid location "${String(loc)}"`)
|
|
374
|
+
return false
|
|
375
|
+
}
|
|
376
|
+
return true
|
|
377
|
+
}),
|
|
378
|
+
statusBar: readArrayOf(
|
|
379
|
+
c.statusBar,
|
|
380
|
+
(x): x is StatusBarContribution =>
|
|
381
|
+
isObject(x) && typeof x.id === 'string' && (x.align === 'left' || x.align === 'right'),
|
|
382
|
+
),
|
|
383
|
+
contextMenu: readArrayOf(
|
|
384
|
+
c.contextMenu,
|
|
385
|
+
(x): x is ContextMenuContribution =>
|
|
386
|
+
isObject(x) && typeof x.command === 'string' && typeof x.context === 'string',
|
|
387
|
+
),
|
|
388
|
+
tabTypes: readArrayOf(
|
|
389
|
+
c.tabTypes,
|
|
390
|
+
(x): x is TabTypeContribution =>
|
|
391
|
+
isObject(x) && typeof x.id === 'string' && typeof x.title === 'string',
|
|
392
|
+
),
|
|
393
|
+
decorators: readArrayOf(
|
|
394
|
+
c.decorators,
|
|
395
|
+
(x): x is DecoratorContribution =>
|
|
396
|
+
isObject(x) && typeof x.id === 'string' && x.appliesTo === 'terminal.output',
|
|
397
|
+
),
|
|
398
|
+
themes: readArrayOf(
|
|
399
|
+
c.themes,
|
|
400
|
+
(x): x is ThemeContribution =>
|
|
401
|
+
isObject(x) &&
|
|
402
|
+
typeof x.id === 'string' &&
|
|
403
|
+
typeof x.label === 'string' &&
|
|
404
|
+
typeof x.path === 'string',
|
|
405
|
+
),
|
|
406
|
+
secrets: readArrayOf(
|
|
407
|
+
c.secrets,
|
|
408
|
+
(x): x is SecretContribution =>
|
|
409
|
+
isObject(x) && typeof x.key === 'string' && typeof x.label === 'string',
|
|
410
|
+
),
|
|
411
|
+
aiBindings: readArrayOf(
|
|
412
|
+
c.aiBindings,
|
|
413
|
+
(x): x is AiBindingContribution =>
|
|
414
|
+
isObject(x) && typeof x.id === 'string' && typeof x.label === 'string',
|
|
415
|
+
),
|
|
416
|
+
}
|
|
417
|
+
}
|