@buzzni/saycode-cli 0.6.0 → 0.7.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/THIRD-PARTY-NOTICES.md +1 -345
- package/channel-extension-host.mjs +1349 -0
- package/index.mjs +30434 -828
- package/package.json +2 -1
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { resolve, sep } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
const EXTENSION_ARTIFACT_ACTION_OPERATIONS = ["publishPublic"];
|
|
7
|
+
const EXTENSION_ARTIFACT_SOURCE_TYPES = ["project-file", "personal-chat-file"];
|
|
8
|
+
const ID_PATTERN$2 = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
|
|
9
|
+
const LOCALE_PATTERN$1 = /^[a-z]{2,3}(?:-[A-Z]{2})?$/;
|
|
10
|
+
const EXTENSION_PATTERN = /^[a-z0-9]+$/;
|
|
11
|
+
const MAX_ACTIONS = 32;
|
|
12
|
+
const MAX_TITLE_CHARACTERS = 120;
|
|
13
|
+
const MAX_LOCALES = 16;
|
|
14
|
+
const MAX_EXTENSIONS = 16;
|
|
15
|
+
function fail$4(path, detail) {
|
|
16
|
+
throw new Error(`${path}: ${detail}`);
|
|
17
|
+
}
|
|
18
|
+
function record$1(value, path) {
|
|
19
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail$4(path, "expected object");
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function exactKeys$1(value, required, optional, path) {
|
|
23
|
+
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
24
|
+
const unknown = Object.keys(value).find((key) => !allowed.has(key));
|
|
25
|
+
if (unknown) fail$4(path, `unknown field ${unknown}`);
|
|
26
|
+
const missing = required.find((key) => !(key in value));
|
|
27
|
+
if (missing) fail$4(path, `missing field ${missing}`);
|
|
28
|
+
}
|
|
29
|
+
function text$1(value, path) {
|
|
30
|
+
if (typeof value !== "string" || !value || value.trim() !== value) {
|
|
31
|
+
fail$4(path, "expected non-empty trimmed string");
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function title(value, path) {
|
|
36
|
+
const parsed = text$1(value, path);
|
|
37
|
+
if ([...parsed].length > MAX_TITLE_CHARACTERS) fail$4(path, "title exceeds 120 characters");
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
function uniqueList(values, path) {
|
|
41
|
+
if (new Set(values).size !== values.length) fail$4(path, "duplicate value");
|
|
42
|
+
return values;
|
|
43
|
+
}
|
|
44
|
+
function parseLocalizations(value, path) {
|
|
45
|
+
const localizations = record$1(value, path);
|
|
46
|
+
if (Object.keys(localizations).length > MAX_LOCALES) fail$4(path, "too many locales");
|
|
47
|
+
return Object.fromEntries(Object.entries(localizations).map(([locale, raw]) => {
|
|
48
|
+
if (!LOCALE_PATTERN$1.test(locale)) fail$4(path, `invalid locale ${locale}`);
|
|
49
|
+
const localizationPath = `${path}.${locale}`;
|
|
50
|
+
const localization = record$1(raw, localizationPath);
|
|
51
|
+
exactKeys$1(localization, ["title"], [], localizationPath);
|
|
52
|
+
return [locale, { title: title(localization.title, `${localizationPath}.title`) }];
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
function parseSourceTypes(value, path) {
|
|
56
|
+
if (!Array.isArray(value) || value.length === 0) fail$4(path, "expected non-empty array");
|
|
57
|
+
const parsed = value.map((sourceType, index) => {
|
|
58
|
+
if (!EXTENSION_ARTIFACT_SOURCE_TYPES.includes(sourceType)) {
|
|
59
|
+
fail$4(`${path}[${index}]`, "unsupported source type");
|
|
60
|
+
}
|
|
61
|
+
return sourceType;
|
|
62
|
+
});
|
|
63
|
+
return uniqueList(parsed, path);
|
|
64
|
+
}
|
|
65
|
+
function parseExtensions(value, path) {
|
|
66
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_EXTENSIONS) {
|
|
67
|
+
fail$4(path, "expected 1-16 extensions");
|
|
68
|
+
}
|
|
69
|
+
const parsed = value.map((extension, index) => {
|
|
70
|
+
if (typeof extension !== "string" || !EXTENSION_PATTERN.test(extension)) {
|
|
71
|
+
fail$4(`${path}[${index}]`, "expected lowercase extension without a dot");
|
|
72
|
+
}
|
|
73
|
+
return extension;
|
|
74
|
+
});
|
|
75
|
+
return uniqueList(parsed, path);
|
|
76
|
+
}
|
|
77
|
+
function parseExtensionArtifactActions(value, path = "manifest.contributes.artifactActions") {
|
|
78
|
+
if (!Array.isArray(value) || value.length > MAX_ACTIONS) fail$4(path, "expected at most 32 actions");
|
|
79
|
+
return value.map((raw, index) => {
|
|
80
|
+
const actionPath = `${path}[${index}]`;
|
|
81
|
+
const action = record$1(raw, actionPath);
|
|
82
|
+
exactKeys$1(action, ["id", "title", "operation", "when"], ["localizations"], actionPath);
|
|
83
|
+
const parsedId = text$1(action.id, `${actionPath}.id`);
|
|
84
|
+
if (!ID_PATTERN$2.test(parsedId) || parsedId.includes("..")) fail$4(`${actionPath}.id`, "invalid stable identifier");
|
|
85
|
+
if (!EXTENSION_ARTIFACT_ACTION_OPERATIONS.includes(action.operation)) {
|
|
86
|
+
fail$4(`${actionPath}.operation`, "unsupported operation");
|
|
87
|
+
}
|
|
88
|
+
const when = record$1(action.when, `${actionPath}.when`);
|
|
89
|
+
exactKeys$1(when, ["sourceTypes", "extensions"], [], `${actionPath}.when`);
|
|
90
|
+
return {
|
|
91
|
+
id: parsedId,
|
|
92
|
+
title: title(action.title, `${actionPath}.title`),
|
|
93
|
+
...action.localizations === void 0 ? {} : { localizations: parseLocalizations(action.localizations, `${actionPath}.localizations`) },
|
|
94
|
+
operation: action.operation,
|
|
95
|
+
when: {
|
|
96
|
+
sourceTypes: parseSourceTypes(when.sourceTypes, `${actionPath}.when.sourceTypes`),
|
|
97
|
+
extensions: parseExtensions(when.extensions, `${actionPath}.when.extensions`)
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const CHANNEL_API_VERSION = 1;
|
|
103
|
+
const CHANNEL_MINIMUM_API_VERSION = 1;
|
|
104
|
+
const CHANNEL_PROVIDERS = ["telegram", "slack", "discord"];
|
|
105
|
+
const CHANNEL_COMMAND_SCHEMA = Object.freeze({
|
|
106
|
+
pair: Object.freeze(["code!"]),
|
|
107
|
+
projects: Object.freeze([]),
|
|
108
|
+
new: Object.freeze(["project!", "agent", "model", "effort"]),
|
|
109
|
+
use: Object.freeze(["session_id!"]),
|
|
110
|
+
prompt: Object.freeze(["text!"]),
|
|
111
|
+
status: Object.freeze([]),
|
|
112
|
+
stop: Object.freeze([]),
|
|
113
|
+
// No options, like `status` and `stop`. Declarable because a provider that registers commands
|
|
114
|
+
// ahead of time is where its users find them — leaving it out would mean Discord users could
|
|
115
|
+
// only reach `/clear` by typing it as a message, while every other command sits in the picker.
|
|
116
|
+
// `reset` is deliberately not here: Core carries it as an unlisted alias of the same intent, and
|
|
117
|
+
// registering both would put two identical entries in that picker.
|
|
118
|
+
clear: Object.freeze([])
|
|
119
|
+
});
|
|
120
|
+
const MAX_DESCRIPTION = 100;
|
|
121
|
+
const ENTRY_KEYS$1 = /* @__PURE__ */ new Set(["id", "description", "options"]);
|
|
122
|
+
const OPTION_KEYS = /* @__PURE__ */ new Set(["name", "description", "required"]);
|
|
123
|
+
function fail$3(path, detail) {
|
|
124
|
+
throw new Error(`${path}: ${detail}`);
|
|
125
|
+
}
|
|
126
|
+
function hasControlCharacter(value) {
|
|
127
|
+
for (const character of value) {
|
|
128
|
+
const code = character.codePointAt(0) ?? 0;
|
|
129
|
+
if (code < 32 || code === 127) return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
function description(value, path) {
|
|
134
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.length > MAX_DESCRIPTION) {
|
|
135
|
+
fail$3(path, "invalid description");
|
|
136
|
+
}
|
|
137
|
+
if (hasControlCharacter(value)) fail$3(path, "invalid description");
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
function parseChannelCommandDeclarations(value, path) {
|
|
141
|
+
if (!Array.isArray(value)) fail$3(path, "expected array");
|
|
142
|
+
const names = Object.keys(CHANNEL_COMMAND_SCHEMA);
|
|
143
|
+
if (value.length === 0 || value.length > names.length) {
|
|
144
|
+
fail$3(path, `expected between 1 and ${names.length} commands`);
|
|
145
|
+
}
|
|
146
|
+
const seen = /* @__PURE__ */ new Set();
|
|
147
|
+
return value.map((item, index) => {
|
|
148
|
+
const entryPath = `${path}[${index}]`;
|
|
149
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) fail$3(entryPath, "expected object");
|
|
150
|
+
const source = item;
|
|
151
|
+
const extra = Object.keys(source).find((key) => !ENTRY_KEYS$1.has(key));
|
|
152
|
+
if (extra !== void 0) fail$3(entryPath, `unknown field ${extra}`);
|
|
153
|
+
const id2 = source.id;
|
|
154
|
+
if (typeof id2 !== "string" || !Object.prototype.hasOwnProperty.call(CHANNEL_COMMAND_SCHEMA, id2)) {
|
|
155
|
+
fail$3(`${entryPath}.id`, "unknown command");
|
|
156
|
+
}
|
|
157
|
+
if (seen.has(id2)) fail$3(path, `duplicate command ${id2}`);
|
|
158
|
+
seen.add(id2);
|
|
159
|
+
return {
|
|
160
|
+
id: id2,
|
|
161
|
+
description: description(source.description, `${entryPath}.description`),
|
|
162
|
+
options: parseOptions(source.options, `${entryPath}.options`, CHANNEL_COMMAND_SCHEMA[id2])
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function parseOptions(value, path, expected) {
|
|
167
|
+
if (value === void 0) {
|
|
168
|
+
if (expected.length > 0) fail$3(path, `expected ${expected.length} options`);
|
|
169
|
+
return Object.freeze([]);
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(value)) fail$3(path, "expected array");
|
|
172
|
+
if (value.length !== expected.length) fail$3(path, `expected ${expected.length} options`);
|
|
173
|
+
return Object.freeze(value.map((item, index) => {
|
|
174
|
+
const optionPath = `${path}[${index}]`;
|
|
175
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) fail$3(optionPath, "expected object");
|
|
176
|
+
const source = item;
|
|
177
|
+
const extra = Object.keys(source).find((key) => !OPTION_KEYS.has(key));
|
|
178
|
+
if (extra !== void 0) fail$3(optionPath, `unknown field ${extra}`);
|
|
179
|
+
const spec = expected[index];
|
|
180
|
+
const required = spec.endsWith("!");
|
|
181
|
+
const name = required ? spec.slice(0, -1) : spec;
|
|
182
|
+
if (source.name !== name) fail$3(`${optionPath}.name`, `expected ${name}`);
|
|
183
|
+
if (source.required !== required) fail$3(`${optionPath}.required`, `expected ${required}`);
|
|
184
|
+
return Object.freeze({
|
|
185
|
+
name,
|
|
186
|
+
required,
|
|
187
|
+
description: description(source.description, `${optionPath}.description`)
|
|
188
|
+
});
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
const MAX_CHANNEL_CONTRIBUTIONS = CHANNEL_PROVIDERS.length;
|
|
192
|
+
const ID_PATTERN$1 = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
|
|
193
|
+
const ENTRY_KEYS = /* @__PURE__ */ new Set(["id", "provider", "commands"]);
|
|
194
|
+
function fail$2(path, detail) {
|
|
195
|
+
throw new Error(`${path}: ${detail}`);
|
|
196
|
+
}
|
|
197
|
+
function parseExtensionChannelContributions(value) {
|
|
198
|
+
if (!Array.isArray(value)) fail$2("manifest.contributes.channels", "expected array");
|
|
199
|
+
if (value.length === 0 || value.length > MAX_CHANNEL_CONTRIBUTIONS) {
|
|
200
|
+
fail$2("manifest.contributes.channels", `expected between 1 and ${MAX_CHANNEL_CONTRIBUTIONS} adapters`);
|
|
201
|
+
}
|
|
202
|
+
const providers = /* @__PURE__ */ new Set();
|
|
203
|
+
return value.map((item, index) => {
|
|
204
|
+
const path = `manifest.contributes.channels[${index}]`;
|
|
205
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) fail$2(path, "expected object");
|
|
206
|
+
const source = item;
|
|
207
|
+
const extra = Object.keys(source).find((key) => !ENTRY_KEYS.has(key));
|
|
208
|
+
if (extra !== void 0) fail$2(path, `unknown field ${extra}`);
|
|
209
|
+
const id2 = source.id;
|
|
210
|
+
if (typeof id2 !== "string" || !ID_PATTERN$1.test(id2) || id2.includes("..")) {
|
|
211
|
+
fail$2(`${path}.id`, "invalid identifier");
|
|
212
|
+
}
|
|
213
|
+
const provider = source.provider;
|
|
214
|
+
if (typeof provider !== "string" || !CHANNEL_PROVIDERS.includes(provider)) {
|
|
215
|
+
fail$2(`${path}.provider`, "unsupported provider");
|
|
216
|
+
}
|
|
217
|
+
if (providers.has(provider)) fail$2("manifest.contributes.channels", "duplicate provider");
|
|
218
|
+
providers.add(provider);
|
|
219
|
+
const commands = source.commands === void 0 ? void 0 : parseChannelCommandDeclarations(source.commands, `${path}.commands`);
|
|
220
|
+
return {
|
|
221
|
+
id: id2,
|
|
222
|
+
provider,
|
|
223
|
+
...commands ? { commands } : {}
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const EXTENSION_PERMISSIONS = [
|
|
228
|
+
"projects.read",
|
|
229
|
+
"remoteFiles.read",
|
|
230
|
+
"remoteFiles.write",
|
|
231
|
+
"machine.execute",
|
|
232
|
+
"network.fetch",
|
|
233
|
+
"notifications.show",
|
|
234
|
+
"storage.read",
|
|
235
|
+
"storage.write",
|
|
236
|
+
"browserViewer.open",
|
|
237
|
+
"browserViewer.install",
|
|
238
|
+
"browserViewer.installChrome",
|
|
239
|
+
"artifacts.publishPublic",
|
|
240
|
+
// One permission per channel wire method (`src/domain/channelProtocol.ts`), deliberately not a
|
|
241
|
+
// single adapter grant. Receiving and acking messages is transport work; reading a session,
|
|
242
|
+
// driving it, or approving a tool call once are three larger and separately-refusable
|
|
243
|
+
// authorities, and a blanket permission would hand all of them to anything that could merely
|
|
244
|
+
// read a message. Declaring one here only makes it askable — every call is still checked
|
|
245
|
+
// against the live grant at dispatch.
|
|
246
|
+
"channels.receive",
|
|
247
|
+
"channels.ack",
|
|
248
|
+
"channels.send",
|
|
249
|
+
"channels.transport",
|
|
250
|
+
"sessions.read",
|
|
251
|
+
"sessions.control",
|
|
252
|
+
"sessions.approveOnce"
|
|
253
|
+
];
|
|
254
|
+
const SAYCODE_EXTENSION_API_VERSION = 3;
|
|
255
|
+
const SAYCODE_EXTENSION_MINIMUM_API_VERSION = 2;
|
|
256
|
+
const MAX_EXTENSION_API_WINDOW_WIDTH = 2;
|
|
257
|
+
const SAYCODE_EXTENSION_ENGINE_VERSION = "1.0.0";
|
|
258
|
+
const SAYCODE_CHANNEL_API_VERSION = CHANNEL_API_VERSION;
|
|
259
|
+
const SAYCODE_CHANNEL_MINIMUM_API_VERSION = CHANNEL_MINIMUM_API_VERSION;
|
|
260
|
+
class UnsupportedChannelApiVersionError extends Error {
|
|
261
|
+
constructor(channelApiVersion) {
|
|
262
|
+
super(`manifest.channelApiVersion: channel API version ${String(channelApiVersion)} is unsupported; Desktop supports ${String(SAYCODE_CHANNEL_API_VERSION)}`);
|
|
263
|
+
this.channelApiVersion = channelApiVersion;
|
|
264
|
+
}
|
|
265
|
+
name = "UnsupportedChannelApiVersionError";
|
|
266
|
+
}
|
|
267
|
+
class UnsupportedExtensionApiVersionError extends Error {
|
|
268
|
+
constructor(apiVersion, minimumSupportedApiVersion, supportedApiVersion) {
|
|
269
|
+
super(`manifest.apiVersion: Extension API version ${String(apiVersion)} is unsupported; Desktop supports ${minimumSupportedApiVersion === supportedApiVersion ? String(supportedApiVersion) : `${minimumSupportedApiVersion}-${supportedApiVersion}`}`);
|
|
270
|
+
this.apiVersion = apiVersion;
|
|
271
|
+
this.minimumSupportedApiVersion = minimumSupportedApiVersion;
|
|
272
|
+
this.supportedApiVersion = supportedApiVersion;
|
|
273
|
+
}
|
|
274
|
+
name = "UnsupportedExtensionApiVersionError";
|
|
275
|
+
}
|
|
276
|
+
const ID_PATTERN = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
|
|
277
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
278
|
+
const ENGINE_RANGE_PATTERN = /^(?:\^|~|>=|<=|>|<)?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
279
|
+
const ENTRYPOINT_PATTERN = /\.(?:cjs|mjs|js)$/;
|
|
280
|
+
const LOCALE_PATTERN = /^[a-z]{2,3}(?:-[A-Z]{2})?$/;
|
|
281
|
+
const CONTRIBUTION_KEYS = /* @__PURE__ */ new Set(["commands", "settings", "panels", "projectTemplates", "machineActions", "artifactActions", "channels"]);
|
|
282
|
+
const V2_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
283
|
+
"browserViewer.open",
|
|
284
|
+
"browserViewer.install",
|
|
285
|
+
"browserViewer.installChrome"
|
|
286
|
+
]);
|
|
287
|
+
const V3_PERMISSIONS = /* @__PURE__ */ new Set(["artifacts.publishPublic"]);
|
|
288
|
+
const CHANNEL_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
289
|
+
"channels.receive",
|
|
290
|
+
"channels.ack",
|
|
291
|
+
"channels.send",
|
|
292
|
+
"channels.transport",
|
|
293
|
+
"sessions.read",
|
|
294
|
+
"sessions.control",
|
|
295
|
+
"sessions.approveOnce"
|
|
296
|
+
]);
|
|
297
|
+
const TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
298
|
+
"id",
|
|
299
|
+
"version",
|
|
300
|
+
"apiVersion",
|
|
301
|
+
"channelApiVersion",
|
|
302
|
+
"engines",
|
|
303
|
+
"entrypoint",
|
|
304
|
+
"permissions",
|
|
305
|
+
"activationEvents",
|
|
306
|
+
"contributes"
|
|
307
|
+
]);
|
|
308
|
+
function semver(value) {
|
|
309
|
+
const [major, minor, patch] = value.split(/[+-]/, 1)[0].split(".").map(Number);
|
|
310
|
+
return [major ?? 0, minor ?? 0, patch ?? 0];
|
|
311
|
+
}
|
|
312
|
+
function compareSemver(left, right) {
|
|
313
|
+
for (let index = 0; index < 3; index += 1) {
|
|
314
|
+
const difference = left[index] - right[index];
|
|
315
|
+
if (difference !== 0) return difference;
|
|
316
|
+
}
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
function engineSupports(range, version) {
|
|
320
|
+
const operator = range.match(/^(\^|~|>=|<=|>|<)/)?.[0] ?? "";
|
|
321
|
+
const base = semver(range.slice(operator.length));
|
|
322
|
+
const candidate = semver(version);
|
|
323
|
+
const compared = compareSemver(candidate, base);
|
|
324
|
+
if (operator === "^") {
|
|
325
|
+
if (compared < 0 || candidate[0] !== base[0]) return false;
|
|
326
|
+
if (base[0] > 0) return true;
|
|
327
|
+
if (candidate[1] !== base[1]) return false;
|
|
328
|
+
return base[1] > 0 || candidate[2] === base[2];
|
|
329
|
+
}
|
|
330
|
+
if (operator === "~") return candidate[0] === base[0] && candidate[1] === base[1] && compared >= 0;
|
|
331
|
+
if (operator === ">=") return compared >= 0;
|
|
332
|
+
if (operator === "<=") return compared <= 0;
|
|
333
|
+
if (operator === ">") return compared > 0;
|
|
334
|
+
if (operator === "<") return compared < 0;
|
|
335
|
+
return compared === 0;
|
|
336
|
+
}
|
|
337
|
+
function fail$1(path, detail) {
|
|
338
|
+
throw new Error(`${path}: ${detail}`);
|
|
339
|
+
}
|
|
340
|
+
function parseApiVersion(value, options) {
|
|
341
|
+
const current = options.supportedApiVersion;
|
|
342
|
+
const floor = options.minimumSupportedApiVersion ?? current;
|
|
343
|
+
if (floor > current || current - floor + 1 > MAX_EXTENSION_API_WINDOW_WIDTH) {
|
|
344
|
+
fail$1(
|
|
345
|
+
"manifest.apiVersion",
|
|
346
|
+
`invalid Desktop API support window [${floor}, ${current}]`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < floor || value > current) {
|
|
350
|
+
throw new UnsupportedExtensionApiVersionError(value, floor, current);
|
|
351
|
+
}
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
function record(value, path) {
|
|
355
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail$1(path, "expected object");
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
function string(value, path) {
|
|
359
|
+
if (typeof value !== "string" || value.trim() !== value || value.length === 0) {
|
|
360
|
+
fail$1(path, "expected non-empty trimmed string");
|
|
361
|
+
}
|
|
362
|
+
return value;
|
|
363
|
+
}
|
|
364
|
+
function id(value, path) {
|
|
365
|
+
const parsed = string(value, path);
|
|
366
|
+
if (!ID_PATTERN.test(parsed) || parsed.includes("..")) fail$1(path, "invalid stable identifier");
|
|
367
|
+
return parsed;
|
|
368
|
+
}
|
|
369
|
+
function relativePath(value, path, requireScript = false) {
|
|
370
|
+
const parsed = string(value, path);
|
|
371
|
+
const segments = parsed.split("/");
|
|
372
|
+
if (parsed.startsWith("/") || parsed.includes("\\") || segments.some((segment) => segment === "" || segment === "." || segment === "..") || requireScript && !ENTRYPOINT_PATTERN.test(parsed)) {
|
|
373
|
+
fail$1(path, "expected safe relative path");
|
|
374
|
+
}
|
|
375
|
+
return parsed;
|
|
376
|
+
}
|
|
377
|
+
function array(value, path) {
|
|
378
|
+
if (!Array.isArray(value)) fail$1(path, "expected array");
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
function assertOnlyKeys(value, allowed, path) {
|
|
382
|
+
const unknown = Object.keys(value).find((key) => !allowed.has(key));
|
|
383
|
+
if (unknown) fail$1(path, `unknown field ${unknown}`);
|
|
384
|
+
}
|
|
385
|
+
function contributionList(value, path, allowedKeys, parse) {
|
|
386
|
+
return array(value, path).map((raw, index) => {
|
|
387
|
+
const entryPath = `${path}[${index}]`;
|
|
388
|
+
const entry = record(raw, entryPath);
|
|
389
|
+
assertOnlyKeys(entry, new Set(allowedKeys), entryPath);
|
|
390
|
+
return parse(entry, entryPath);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function parseChannelApiVersion(value, apiVersion) {
|
|
394
|
+
if (value === void 0) return void 0;
|
|
395
|
+
if (apiVersion < 3) fail$1("manifest.channelApiVersion", "requires Extension API version 3");
|
|
396
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < SAYCODE_CHANNEL_MINIMUM_API_VERSION || value > SAYCODE_CHANNEL_API_VERSION) throw new UnsupportedChannelApiVersionError(value);
|
|
397
|
+
return value;
|
|
398
|
+
}
|
|
399
|
+
function parseContributions(value, apiVersion, channelApiVersion) {
|
|
400
|
+
const parsed = record(value, "manifest.contributes");
|
|
401
|
+
assertOnlyKeys(parsed, CONTRIBUTION_KEYS, "manifest.contributes");
|
|
402
|
+
if (parsed.channels !== void 0) {
|
|
403
|
+
if (apiVersion < 3) fail$1("manifest.contributes.channels", "requires Extension API version 3");
|
|
404
|
+
if (channelApiVersion === void 0) {
|
|
405
|
+
fail$1("manifest.contributes.channels", "requires manifest.channelApiVersion");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (apiVersion < 2 && parsed.machineActions !== void 0) {
|
|
409
|
+
fail$1("manifest.contributes.machineActions", "requires Extension API version 2");
|
|
410
|
+
}
|
|
411
|
+
if (apiVersion < 3 && parsed.artifactActions !== void 0) {
|
|
412
|
+
fail$1("manifest.contributes.artifactActions", "requires Extension API version 3");
|
|
413
|
+
}
|
|
414
|
+
const result = {};
|
|
415
|
+
if (parsed.commands !== void 0) {
|
|
416
|
+
result.commands = contributionList(
|
|
417
|
+
parsed.commands,
|
|
418
|
+
"manifest.contributes.commands",
|
|
419
|
+
["id", "title", "panelId"],
|
|
420
|
+
(entry, path) => ({
|
|
421
|
+
id: id(entry.id, `${path}.id`),
|
|
422
|
+
title: string(entry.title, `${path}.title`),
|
|
423
|
+
...entry.panelId === void 0 ? {} : { panelId: id(entry.panelId, `${path}.panelId`) }
|
|
424
|
+
})
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
if (parsed.settings !== void 0) {
|
|
428
|
+
result.settings = contributionList(
|
|
429
|
+
parsed.settings,
|
|
430
|
+
"manifest.contributes.settings",
|
|
431
|
+
["id", "title", "type"],
|
|
432
|
+
(entry, path) => {
|
|
433
|
+
const type = string(entry.type, `${path}.type`);
|
|
434
|
+
if (type !== "boolean" && type !== "number" && type !== "string") {
|
|
435
|
+
fail$1(`${path}.type`, "unsupported setting type");
|
|
436
|
+
}
|
|
437
|
+
return { id: id(entry.id, `${path}.id`), title: string(entry.title, `${path}.title`), type };
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
if (parsed.panels !== void 0) {
|
|
442
|
+
result.panels = contributionList(
|
|
443
|
+
parsed.panels,
|
|
444
|
+
"manifest.contributes.panels",
|
|
445
|
+
["id", "title", "entrypoint"],
|
|
446
|
+
(entry, path) => ({
|
|
447
|
+
id: id(entry.id, `${path}.id`),
|
|
448
|
+
title: string(entry.title, `${path}.title`),
|
|
449
|
+
entrypoint: relativePath(entry.entrypoint, `${path}.entrypoint`)
|
|
450
|
+
})
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
if (parsed.projectTemplates !== void 0) {
|
|
454
|
+
result.projectTemplates = contributionList(
|
|
455
|
+
parsed.projectTemplates,
|
|
456
|
+
"manifest.contributes.projectTemplates",
|
|
457
|
+
[
|
|
458
|
+
"id",
|
|
459
|
+
"title",
|
|
460
|
+
"description",
|
|
461
|
+
"stack",
|
|
462
|
+
"firstPrompt",
|
|
463
|
+
"devServerCommand",
|
|
464
|
+
"assetsRoot",
|
|
465
|
+
"localizations"
|
|
466
|
+
],
|
|
467
|
+
(entry, path) => {
|
|
468
|
+
let localizations;
|
|
469
|
+
if (entry.localizations !== void 0) {
|
|
470
|
+
const rawLocalizations = record(entry.localizations, `${path}.localizations`);
|
|
471
|
+
localizations = {};
|
|
472
|
+
for (const [locale, rawLocalization] of Object.entries(rawLocalizations)) {
|
|
473
|
+
if (!LOCALE_PATTERN.test(locale)) fail$1(`${path}.localizations`, `invalid locale ${locale}`);
|
|
474
|
+
const localizationPath = `${path}.localizations.${locale}`;
|
|
475
|
+
const localization = record(rawLocalization, localizationPath);
|
|
476
|
+
assertOnlyKeys(
|
|
477
|
+
localization,
|
|
478
|
+
/* @__PURE__ */ new Set(["title", "description", "firstPrompt"]),
|
|
479
|
+
localizationPath
|
|
480
|
+
);
|
|
481
|
+
localizations[locale] = {
|
|
482
|
+
...localization.title === void 0 ? {} : { title: string(localization.title, `${localizationPath}.title`) },
|
|
483
|
+
...localization.description === void 0 ? {} : { description: string(localization.description, `${localizationPath}.description`) },
|
|
484
|
+
...localization.firstPrompt === void 0 ? {} : { firstPrompt: string(localization.firstPrompt, `${localizationPath}.firstPrompt`) }
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
id: id(entry.id, `${path}.id`),
|
|
490
|
+
title: string(entry.title, `${path}.title`),
|
|
491
|
+
assetsRoot: relativePath(entry.assetsRoot, `${path}.assetsRoot`),
|
|
492
|
+
...entry.description === void 0 ? {} : { description: string(entry.description, `${path}.description`) },
|
|
493
|
+
...entry.stack === void 0 ? {} : { stack: string(entry.stack, `${path}.stack`) },
|
|
494
|
+
...entry.firstPrompt === void 0 ? {} : { firstPrompt: string(entry.firstPrompt, `${path}.firstPrompt`) },
|
|
495
|
+
...entry.devServerCommand === void 0 ? {} : { devServerCommand: string(entry.devServerCommand, `${path}.devServerCommand`) },
|
|
496
|
+
...localizations === void 0 ? {} : { localizations }
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
if (parsed.machineActions !== void 0) {
|
|
502
|
+
result.machineActions = contributionList(
|
|
503
|
+
parsed.machineActions,
|
|
504
|
+
"manifest.contributes.machineActions",
|
|
505
|
+
["id", "title", "command", "when"],
|
|
506
|
+
(entry, path) => {
|
|
507
|
+
const when = record(entry.when, `${path}.when`);
|
|
508
|
+
assertOnlyKeys(when, /* @__PURE__ */ new Set(["online"]), `${path}.when`);
|
|
509
|
+
if (when.online !== true) fail$1(`${path}.when.online`, "expected true");
|
|
510
|
+
return {
|
|
511
|
+
id: id(entry.id, `${path}.id`),
|
|
512
|
+
title: string(entry.title, `${path}.title`),
|
|
513
|
+
command: id(entry.command, `${path}.command`),
|
|
514
|
+
when: { online: true }
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (parsed.artifactActions !== void 0) {
|
|
520
|
+
result.artifactActions = parseExtensionArtifactActions(parsed.artifactActions);
|
|
521
|
+
}
|
|
522
|
+
if (parsed.channels !== void 0) {
|
|
523
|
+
result.channels = parseExtensionChannelContributions(parsed.channels);
|
|
524
|
+
}
|
|
525
|
+
const ids = [
|
|
526
|
+
...result.commands ?? [],
|
|
527
|
+
...result.settings ?? [],
|
|
528
|
+
...result.panels ?? [],
|
|
529
|
+
...result.projectTemplates ?? [],
|
|
530
|
+
...result.machineActions ?? [],
|
|
531
|
+
...result.artifactActions ?? [],
|
|
532
|
+
...result.channels ?? []
|
|
533
|
+
].map((entry) => entry.id);
|
|
534
|
+
if (new Set(ids).size !== ids.length) fail$1("manifest.contributes", "duplicate contribution id");
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
function parseExtensionManifest(value, options) {
|
|
538
|
+
const manifest = record(value, "manifest");
|
|
539
|
+
assertOnlyKeys(manifest, TOP_LEVEL_KEYS, "manifest");
|
|
540
|
+
const parsedId = id(manifest.id, "manifest.id");
|
|
541
|
+
const version = string(manifest.version, "manifest.version");
|
|
542
|
+
if (!SEMVER_PATTERN.test(version)) fail$1("manifest.version", "expected semantic version");
|
|
543
|
+
const apiVersion = parseApiVersion(manifest.apiVersion, options);
|
|
544
|
+
const channelApiVersion = parseChannelApiVersion(manifest.channelApiVersion, apiVersion);
|
|
545
|
+
const engines = record(manifest.engines, "manifest.engines");
|
|
546
|
+
assertOnlyKeys(engines, /* @__PURE__ */ new Set(["saycode"]), "manifest.engines");
|
|
547
|
+
const saycodeEngine = string(engines.saycode, "manifest.engines.saycode");
|
|
548
|
+
if (!ENGINE_RANGE_PATTERN.test(saycodeEngine)) {
|
|
549
|
+
fail$1("manifest.engines.saycode", "expected simple semantic version range");
|
|
550
|
+
}
|
|
551
|
+
if (options.saycodeVersion && !engineSupports(saycodeEngine, options.saycodeVersion)) {
|
|
552
|
+
fail$1(
|
|
553
|
+
"manifest.engines.saycode",
|
|
554
|
+
`extension requires Saycode ${saycodeEngine}, but Desktop provides ${options.saycodeVersion}`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const permissions = array(manifest.permissions, "manifest.permissions").map((permission, index) => {
|
|
558
|
+
const parsed = string(permission, `manifest.permissions[${index}]`);
|
|
559
|
+
if (!EXTENSION_PERMISSIONS.includes(parsed)) {
|
|
560
|
+
fail$1("manifest.permissions", `unknown permission ${parsed}`);
|
|
561
|
+
}
|
|
562
|
+
const typed = parsed;
|
|
563
|
+
if (apiVersion < 2 && V2_PERMISSIONS.has(typed)) {
|
|
564
|
+
fail$1(`manifest.permissions[${index}]`, "requires Extension API version 2");
|
|
565
|
+
}
|
|
566
|
+
if (apiVersion < 3 && V3_PERMISSIONS.has(typed)) {
|
|
567
|
+
fail$1(`manifest.permissions[${index}]`, "requires Extension API version 3");
|
|
568
|
+
}
|
|
569
|
+
if (CHANNEL_PERMISSIONS.has(typed)) {
|
|
570
|
+
if (apiVersion < 3) fail$1(`manifest.permissions[${index}]`, "requires Extension API version 3");
|
|
571
|
+
if (channelApiVersion === void 0) {
|
|
572
|
+
fail$1(`manifest.permissions[${index}]`, "requires manifest.channelApiVersion");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return typed;
|
|
576
|
+
});
|
|
577
|
+
if (new Set(permissions).size !== permissions.length) {
|
|
578
|
+
fail$1("manifest.permissions", "duplicate permission");
|
|
579
|
+
}
|
|
580
|
+
const activationEvents = array(manifest.activationEvents, "manifest.activationEvents").map(
|
|
581
|
+
(event, index) => string(event, `manifest.activationEvents[${index}]`)
|
|
582
|
+
);
|
|
583
|
+
const contributes = parseContributions(manifest.contributes, apiVersion, channelApiVersion);
|
|
584
|
+
if (contributes.channels?.length && !permissions.includes("channels.receive")) {
|
|
585
|
+
fail$1("manifest.contributes.channels", "requires channels.receive permission");
|
|
586
|
+
}
|
|
587
|
+
const strandedChannelPermission = permissions.find((permission) => CHANNEL_PERMISSIONS.has(permission));
|
|
588
|
+
if (strandedChannelPermission !== void 0 && contributes.channels === void 0) {
|
|
589
|
+
fail$1("manifest.permissions", `${strandedChannelPermission} requires a channels contribution`);
|
|
590
|
+
}
|
|
591
|
+
for (const [index, channel] of (contributes.channels ?? []).entries()) {
|
|
592
|
+
if (!channel.id.startsWith(`${parsedId}.`)) {
|
|
593
|
+
fail$1(
|
|
594
|
+
`manifest.contributes.channels[${index}].id`,
|
|
595
|
+
"expected an id declared by the same extension"
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (contributes.artifactActions?.length && !permissions.includes("artifacts.publishPublic")) {
|
|
600
|
+
fail$1("manifest.contributes.artifactActions", "requires artifacts.publishPublic permission");
|
|
601
|
+
}
|
|
602
|
+
for (const [index, action] of (contributes.artifactActions ?? []).entries()) {
|
|
603
|
+
if (!action.id.startsWith(`${parsedId}.`)) {
|
|
604
|
+
fail$1(
|
|
605
|
+
`manifest.contributes.artifactActions[${index}].id`,
|
|
606
|
+
"expected an id declared by the same extension"
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const panelIds = new Set((contributes.panels ?? []).map((panel) => panel.id));
|
|
611
|
+
for (const [index, command] of (contributes.commands ?? []).entries()) {
|
|
612
|
+
if (command.panelId !== void 0 && (!command.panelId.startsWith(`${parsedId}.`) || !panelIds.has(command.panelId))) {
|
|
613
|
+
fail$1(
|
|
614
|
+
`manifest.contributes.commands[${index}].panelId`,
|
|
615
|
+
"expected a panel declared by the same extension"
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const commandIds = new Set((contributes.commands ?? []).map((command) => command.id));
|
|
620
|
+
for (const [index, action] of (contributes.machineActions ?? []).entries()) {
|
|
621
|
+
if (!action.id.startsWith(`${parsedId}.`) || !action.command.startsWith(`${parsedId}.`) || !commandIds.has(action.command)) {
|
|
622
|
+
fail$1(
|
|
623
|
+
`manifest.contributes.machineActions[${index}].id/command`,
|
|
624
|
+
"expected ids declared by the same extension"
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
id: parsedId,
|
|
630
|
+
version,
|
|
631
|
+
apiVersion,
|
|
632
|
+
...channelApiVersion === void 0 ? {} : { channelApiVersion },
|
|
633
|
+
engines: { saycode: saycodeEngine },
|
|
634
|
+
entrypoint: relativePath(manifest.entrypoint, "manifest.entrypoint", true),
|
|
635
|
+
permissions,
|
|
636
|
+
activationEvents,
|
|
637
|
+
contributes
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function fail(detail) {
|
|
641
|
+
throw new Error(`extension wire message must be JSON-serializable: ${detail}`);
|
|
642
|
+
}
|
|
643
|
+
function plainRecord(value, path) {
|
|
644
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail(`${path} must be an object`);
|
|
645
|
+
const prototype = Object.getPrototypeOf(value);
|
|
646
|
+
if (prototype !== Object.prototype && prototype !== null) fail(`${path} must be a plain object`);
|
|
647
|
+
return value;
|
|
648
|
+
}
|
|
649
|
+
function text(value, path) {
|
|
650
|
+
if (typeof value !== "string" || value.length === 0) fail(`${path} must be a non-empty string`);
|
|
651
|
+
return value;
|
|
652
|
+
}
|
|
653
|
+
function exactKeys(value, keys, path) {
|
|
654
|
+
const allowed = new Set(keys);
|
|
655
|
+
const unknown = Object.keys(value).find((key) => !allowed.has(key));
|
|
656
|
+
if (unknown) fail(`${path} contains unknown field ${unknown}`);
|
|
657
|
+
const missing = keys.find((key) => !(key in value));
|
|
658
|
+
if (missing) fail(`${path} is missing ${missing}`);
|
|
659
|
+
}
|
|
660
|
+
const MAX_CHANNEL_NATIVE_PAYLOAD_BYTES = 4096;
|
|
661
|
+
function parseChannelReplyFormatResult(value, request) {
|
|
662
|
+
const result = plainRecord(value, "channel format result");
|
|
663
|
+
const permitted = request.approval ? ["chunks", "blocks", "components"] : ["chunks"];
|
|
664
|
+
const unexpected = Object.keys(result).find((key) => !permitted.includes(key));
|
|
665
|
+
if (unexpected) fail(`channel format result contains unknown field ${unexpected}`);
|
|
666
|
+
if (!("chunks" in result)) fail("channel format result is missing chunks");
|
|
667
|
+
if (!Array.isArray(result.chunks) || result.chunks.length < 1 || result.chunks.length > request.maxChunks) {
|
|
668
|
+
fail("invalid channel chunk count");
|
|
669
|
+
}
|
|
670
|
+
const chunks = result.chunks.map((chunk) => {
|
|
671
|
+
const part = text(chunk, "channel chunk");
|
|
672
|
+
if (part.length > request.maxChunkUtf16Units || /[\uD800-\uDFFF]/.test(part.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, ""))) fail("invalid channel chunk");
|
|
673
|
+
return part;
|
|
674
|
+
});
|
|
675
|
+
if (chunks.join("") !== request.text) fail("channel chunks differ from approved reply");
|
|
676
|
+
const blocks = result.blocks === void 0 ? void 0 : asArray(result.blocks, "channel blocks");
|
|
677
|
+
const components = result.components === void 0 ? void 0 : asArray(result.components, "channel components");
|
|
678
|
+
if (blocks && components) fail("a reply may carry one provider payload, not both");
|
|
679
|
+
for (const payload of [blocks, components]) {
|
|
680
|
+
if (payload && new TextEncoder().encode(JSON.stringify(payload)).length > MAX_CHANNEL_NATIVE_PAYLOAD_BYTES) {
|
|
681
|
+
fail("channel provider payload exceeds its size budget");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
chunks,
|
|
686
|
+
...blocks ? { blocks } : {},
|
|
687
|
+
...components ? { components } : {}
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function asArray(value, detail) {
|
|
691
|
+
if (!Array.isArray(value)) fail(`invalid ${detail}`);
|
|
692
|
+
return value;
|
|
693
|
+
}
|
|
694
|
+
function parseChannelApprovalRequest(value) {
|
|
695
|
+
const approval = plainRecord(value, "request.params.approval");
|
|
696
|
+
exactKeys(approval, ["approvalHandle", "approveLabel", "denyLabel"], "request.params.approval");
|
|
697
|
+
const approvalHandle = text(approval.approvalHandle, "request.params.approval.approvalHandle");
|
|
698
|
+
const approveLabel = text(approval.approveLabel, "request.params.approval.approveLabel");
|
|
699
|
+
const denyLabel = text(approval.denyLabel, "request.params.approval.denyLabel");
|
|
700
|
+
if (approvalHandle.length > 80 || approveLabel.length > 75 || denyLabel.length > 75) {
|
|
701
|
+
fail("invalid channel approval bounds");
|
|
702
|
+
}
|
|
703
|
+
return { approvalHandle, approveLabel, denyLabel };
|
|
704
|
+
}
|
|
705
|
+
function finiteNumber(value, path) {
|
|
706
|
+
if (typeof value !== "number" || !Number.isFinite(value)) fail(`${path} must be a finite number`);
|
|
707
|
+
return value;
|
|
708
|
+
}
|
|
709
|
+
function jsonValue(value, path, ancestors = /* @__PURE__ */ new Set()) {
|
|
710
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
711
|
+
if (typeof value === "number") return finiteNumber(value, path);
|
|
712
|
+
if (typeof value !== "object") fail(`${path} contains ${typeof value}`);
|
|
713
|
+
if (ancestors.has(value)) fail(`${path} contains a cycle`);
|
|
714
|
+
ancestors.add(value);
|
|
715
|
+
try {
|
|
716
|
+
if (Array.isArray(value)) return value.map((item, index) => jsonValue(item, `${path}[${index}]`, ancestors));
|
|
717
|
+
const source = plainRecord(value, path);
|
|
718
|
+
const result = {};
|
|
719
|
+
for (const [key, item] of Object.entries(source)) {
|
|
720
|
+
result[key] = jsonValue(item, `${path}.${key}`, ancestors);
|
|
721
|
+
}
|
|
722
|
+
return result;
|
|
723
|
+
} finally {
|
|
724
|
+
ancestors.delete(value);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function parseRequest(message) {
|
|
728
|
+
exactKeys(message, ["kind", "id", "method", "params"], "request");
|
|
729
|
+
const id2 = text(message.id, "request.id");
|
|
730
|
+
const method = text(message.method, "request.method");
|
|
731
|
+
const params = plainRecord(message.params, "request.params");
|
|
732
|
+
switch (method) {
|
|
733
|
+
case "handshake":
|
|
734
|
+
exactKeys(params, ["apiVersion", "extensionId", "extensionVersion"], "request.params");
|
|
735
|
+
return {
|
|
736
|
+
kind: "request",
|
|
737
|
+
id: id2,
|
|
738
|
+
method,
|
|
739
|
+
params: {
|
|
740
|
+
apiVersion: finiteNumber(params.apiVersion, "request.params.apiVersion"),
|
|
741
|
+
extensionId: text(params.extensionId, "request.params.extensionId"),
|
|
742
|
+
extensionVersion: text(params.extensionVersion, "request.params.extensionVersion")
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
case "activate":
|
|
746
|
+
exactKeys(params, ["activationEvent"], "request.params");
|
|
747
|
+
return {
|
|
748
|
+
kind: "request",
|
|
749
|
+
id: id2,
|
|
750
|
+
method,
|
|
751
|
+
params: { activationEvent: text(params.activationEvent, "request.params.activationEvent") }
|
|
752
|
+
};
|
|
753
|
+
case "deactivate":
|
|
754
|
+
exactKeys(params, ["reason"], "request.params");
|
|
755
|
+
return { kind: "request", id: id2, method, params: { reason: text(params.reason, "request.params.reason") } };
|
|
756
|
+
case "channel.lifecycle": {
|
|
757
|
+
exactKeys(params, ["apiVersion", "channelId", "state", "connectionId", "provider", "connectionRevision"], "request.params");
|
|
758
|
+
if (params.apiVersion !== 1 || params.state !== "start" && params.state !== "stop" || !["telegram", "slack", "discord"].includes(String(params.provider)) || !Number.isSafeInteger(params.connectionRevision) || Number(params.connectionRevision) < 1) fail("invalid channel lifecycle");
|
|
759
|
+
const channelId = text(params.channelId, "request.params.channelId");
|
|
760
|
+
const connectionId = text(params.connectionId, "request.params.connectionId");
|
|
761
|
+
if (channelId.length > 200 || connectionId.length > 200) fail("channel lifecycle identifier too long");
|
|
762
|
+
return { kind: "request", id: id2, method, params: {
|
|
763
|
+
apiVersion: 1,
|
|
764
|
+
channelId,
|
|
765
|
+
connectionId,
|
|
766
|
+
state: params.state,
|
|
767
|
+
provider: params.provider,
|
|
768
|
+
connectionRevision: Number(params.connectionRevision)
|
|
769
|
+
} };
|
|
770
|
+
}
|
|
771
|
+
case "channel.formatReply": {
|
|
772
|
+
const formatKeys = [
|
|
773
|
+
"apiVersion",
|
|
774
|
+
"channelId",
|
|
775
|
+
"connectionId",
|
|
776
|
+
"provider",
|
|
777
|
+
"connectionRevision",
|
|
778
|
+
"requestId",
|
|
779
|
+
"kind",
|
|
780
|
+
"text",
|
|
781
|
+
"maxChunkUtf16Units",
|
|
782
|
+
"maxChunks"
|
|
783
|
+
];
|
|
784
|
+
exactKeys(
|
|
785
|
+
params,
|
|
786
|
+
"approval" in params ? [...formatKeys, "approval"] : formatKeys,
|
|
787
|
+
"request.params"
|
|
788
|
+
);
|
|
789
|
+
const lifecycle = parseRequest({ kind: "request", id: id2, method: "channel.lifecycle", params: {
|
|
790
|
+
apiVersion: params.apiVersion,
|
|
791
|
+
channelId: params.channelId,
|
|
792
|
+
connectionId: params.connectionId,
|
|
793
|
+
provider: params.provider,
|
|
794
|
+
connectionRevision: params.connectionRevision,
|
|
795
|
+
state: "start"
|
|
796
|
+
} });
|
|
797
|
+
if (lifecycle.method !== "channel.lifecycle") fail("invalid channel formatter");
|
|
798
|
+
const requestId = text(params.requestId, "request.params.requestId");
|
|
799
|
+
const body = text(params.text, "request.params.text");
|
|
800
|
+
if (requestId.length > 200 || new TextEncoder().encode(body).length > 65536 || params.kind !== "final" && params.kind !== "status" && params.kind !== "approval" || !Number.isSafeInteger(params.maxChunks) || Number(params.maxChunks) < 1 || Number(params.maxChunks) > 64 || !Number.isSafeInteger(params.maxChunkUtf16Units) || Number(params.maxChunkUtf16Units) < 1 || Number(params.maxChunkUtf16Units) > 65536) fail("invalid channel format bounds");
|
|
801
|
+
if (params.approval !== void 0 !== (params.kind === "approval")) {
|
|
802
|
+
fail("invalid channel approval request");
|
|
803
|
+
}
|
|
804
|
+
const approval = params.approval === void 0 ? void 0 : parseChannelApprovalRequest(params.approval);
|
|
805
|
+
const { state: _state, ...identity } = lifecycle.params;
|
|
806
|
+
return { kind: "request", id: id2, method, params: {
|
|
807
|
+
...identity,
|
|
808
|
+
requestId,
|
|
809
|
+
text: body,
|
|
810
|
+
kind: params.kind,
|
|
811
|
+
maxChunks: Number(params.maxChunks),
|
|
812
|
+
maxChunkUtf16Units: Number(params.maxChunkUtf16Units),
|
|
813
|
+
...approval ? { approval } : {}
|
|
814
|
+
} };
|
|
815
|
+
}
|
|
816
|
+
case "invokeCommand": {
|
|
817
|
+
exactKeys(params, ["commandId", "args"], "request.params");
|
|
818
|
+
if (!Array.isArray(params.args)) fail("request.params.args must be an array");
|
|
819
|
+
return {
|
|
820
|
+
kind: "request",
|
|
821
|
+
id: id2,
|
|
822
|
+
method,
|
|
823
|
+
params: {
|
|
824
|
+
commandId: text(params.commandId, "request.params.commandId"),
|
|
825
|
+
args: params.args.map((item, index) => jsonValue(item, `request.params.args[${index}]`))
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
case "capability.invoke": {
|
|
830
|
+
exactKeys(params, ["permission", "action", "args"], "request.params");
|
|
831
|
+
const permission = text(params.permission, "request.params.permission");
|
|
832
|
+
if (!EXTENSION_PERMISSIONS.includes(permission)) {
|
|
833
|
+
fail(`request.params.permission is unknown permission ${permission}`);
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
kind: "request",
|
|
837
|
+
id: id2,
|
|
838
|
+
method,
|
|
839
|
+
params: {
|
|
840
|
+
permission,
|
|
841
|
+
action: text(params.action, "request.params.action"),
|
|
842
|
+
args: jsonValue(params.args, "request.params.args")
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
default:
|
|
847
|
+
throw new Error(`unknown request method: ${method}`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function parseResponse(message) {
|
|
851
|
+
const id2 = text(message.id, "response.id");
|
|
852
|
+
if (message.ok === true) {
|
|
853
|
+
exactKeys(message, ["kind", "id", "ok", "result"], "response");
|
|
854
|
+
return { kind: "response", id: id2, ok: true, result: jsonValue(message.result, "response.result") };
|
|
855
|
+
}
|
|
856
|
+
if (message.ok === false) {
|
|
857
|
+
exactKeys(message, ["kind", "id", "ok", "error"], "response");
|
|
858
|
+
const error = plainRecord(message.error, "response.error");
|
|
859
|
+
exactKeys(error, ["code", "message"], "response.error");
|
|
860
|
+
return {
|
|
861
|
+
kind: "response",
|
|
862
|
+
id: id2,
|
|
863
|
+
ok: false,
|
|
864
|
+
error: { code: text(error.code, "response.error.code"), message: text(error.message, "response.error.message") }
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
fail("response.ok must be boolean");
|
|
868
|
+
}
|
|
869
|
+
function parseEvent(message) {
|
|
870
|
+
exactKeys(message, ["kind", "event", "payload"], "event");
|
|
871
|
+
const event = text(message.event, "event.event");
|
|
872
|
+
const payload = plainRecord(message.payload, "event.payload");
|
|
873
|
+
if (event === "ready") {
|
|
874
|
+
exactKeys(payload, ["apiVersion"], "event.payload");
|
|
875
|
+
return { kind: "event", event, payload: { apiVersion: finiteNumber(payload.apiVersion, "event.payload.apiVersion") } };
|
|
876
|
+
}
|
|
877
|
+
if (event === "log") {
|
|
878
|
+
exactKeys(payload, ["level", "message"], "event.payload");
|
|
879
|
+
const level = text(payload.level, "event.payload.level");
|
|
880
|
+
if (level !== "debug" && level !== "info" && level !== "warn" && level !== "error") {
|
|
881
|
+
fail("event.payload.level is unsupported");
|
|
882
|
+
}
|
|
883
|
+
return { kind: "event", event, payload: { level, message: text(payload.message, "event.payload.message") } };
|
|
884
|
+
}
|
|
885
|
+
if (event === "contributionsChanged") {
|
|
886
|
+
exactKeys(payload, [], "event.payload");
|
|
887
|
+
return { kind: "event", event, payload: {} };
|
|
888
|
+
}
|
|
889
|
+
fail(`unknown event ${event}`);
|
|
890
|
+
}
|
|
891
|
+
function parseExtensionWireMessage(value) {
|
|
892
|
+
const message = plainRecord(value, "message");
|
|
893
|
+
if (message.kind === "request") return parseRequest(message);
|
|
894
|
+
if (message.kind === "response") return parseResponse(message);
|
|
895
|
+
if (message.kind === "event") return parseEvent(message);
|
|
896
|
+
fail("message.kind is unknown");
|
|
897
|
+
}
|
|
898
|
+
const TIMEOUT = Symbol("extension-request-timeout");
|
|
899
|
+
function requestEnvelope(value) {
|
|
900
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
901
|
+
const record2 = value;
|
|
902
|
+
if (record2.kind !== "request" || typeof record2.id !== "string" || typeof record2.method !== "string") return null;
|
|
903
|
+
if (!record2.id || !record2.method) return null;
|
|
904
|
+
return { id: record2.id, method: record2.method };
|
|
905
|
+
}
|
|
906
|
+
function responseId(value) {
|
|
907
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
908
|
+
const record2 = value;
|
|
909
|
+
if (record2.kind !== "response" || typeof record2.id !== "string" || !record2.id) return null;
|
|
910
|
+
return record2.id;
|
|
911
|
+
}
|
|
912
|
+
class ExtensionHostRuntime {
|
|
913
|
+
constructor(options) {
|
|
914
|
+
this.options = options;
|
|
915
|
+
}
|
|
916
|
+
channelHandlers = /* @__PURE__ */ new Map();
|
|
917
|
+
channelFormatters = /* @__PURE__ */ new Map();
|
|
918
|
+
channelStates = /* @__PURE__ */ new Map();
|
|
919
|
+
channelCalls = /* @__PURE__ */ new Map();
|
|
920
|
+
channelEpoch = 0;
|
|
921
|
+
deactivating = false;
|
|
922
|
+
active = false;
|
|
923
|
+
disposed = false;
|
|
924
|
+
handshaken = false;
|
|
925
|
+
inFlight = 0;
|
|
926
|
+
activating = false;
|
|
927
|
+
activation = null;
|
|
928
|
+
module = null;
|
|
929
|
+
commandHandlers = /* @__PURE__ */ new Map();
|
|
930
|
+
ongoingRequestIds = /* @__PURE__ */ new Set();
|
|
931
|
+
respondedIds = /* @__PURE__ */ new Set();
|
|
932
|
+
pendingCapabilityRequests = /* @__PURE__ */ new Map();
|
|
933
|
+
capabilityRequestSeq = 0;
|
|
934
|
+
/** Lets the loaded extension module ask Desktop to run a capability on its behalf. */
|
|
935
|
+
invokeCapability(permission, action, args) {
|
|
936
|
+
if (this.disposed) return Promise.reject(new Error("extension host is disposed"));
|
|
937
|
+
if (!this.handshaken) return Promise.reject(new Error("capability invoke requires a completed handshake"));
|
|
938
|
+
if (!this.active && !this.activating) {
|
|
939
|
+
return Promise.reject(new Error("capability invoke requires an active extension"));
|
|
940
|
+
}
|
|
941
|
+
const id2 = `capability:${this.options.manifest.id}:${++this.capabilityRequestSeq}`;
|
|
942
|
+
return new Promise((resolve2, reject) => {
|
|
943
|
+
const timer = setTimeout(() => {
|
|
944
|
+
this.pendingCapabilityRequests.delete(id2);
|
|
945
|
+
reject(new Error(`capability.invoke ${permission}/${action} timed out`));
|
|
946
|
+
}, this.options.requestTimeoutMs ?? 5e3);
|
|
947
|
+
this.pendingCapabilityRequests.set(id2, {
|
|
948
|
+
resolve: (result) => {
|
|
949
|
+
clearTimeout(timer);
|
|
950
|
+
resolve2(result);
|
|
951
|
+
},
|
|
952
|
+
reject: (error) => {
|
|
953
|
+
clearTimeout(timer);
|
|
954
|
+
reject(error);
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
try {
|
|
958
|
+
this.options.send({ kind: "request", id: id2, method: "capability.invoke", params: { permission, action, args } });
|
|
959
|
+
} catch (error) {
|
|
960
|
+
this.pendingCapabilityRequests.delete(id2);
|
|
961
|
+
clearTimeout(timer);
|
|
962
|
+
reject(error instanceof Error ? error : new Error("capability.invoke request could not be sent"));
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
start() {
|
|
967
|
+
if (this.disposed) throw new Error("extension host is disposed");
|
|
968
|
+
this.options.send({
|
|
969
|
+
kind: "event",
|
|
970
|
+
event: "ready",
|
|
971
|
+
payload: { apiVersion: this.options.manifest.apiVersion }
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
async receive(value) {
|
|
975
|
+
const pendingId = responseId(value);
|
|
976
|
+
if (pendingId && this.pendingCapabilityRequests.has(pendingId)) {
|
|
977
|
+
const pending = this.pendingCapabilityRequests.get(pendingId);
|
|
978
|
+
this.pendingCapabilityRequests.delete(pendingId);
|
|
979
|
+
let message;
|
|
980
|
+
try {
|
|
981
|
+
message = parseExtensionWireMessage(value);
|
|
982
|
+
} catch (error) {
|
|
983
|
+
pending.reject(error instanceof Error ? error : new Error("invalid capability.invoke response"));
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (message.kind !== "response") {
|
|
987
|
+
pending.reject(new Error("expected a capability.invoke response"));
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (message.ok) pending.resolve(message.result);
|
|
991
|
+
else pending.reject(new Error(message.error.message));
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const envelope = requestEnvelope(value);
|
|
995
|
+
if (!envelope || this.ongoingRequestIds.has(envelope.id) || this.respondedIds.has(envelope.id)) return;
|
|
996
|
+
this.rememberRequestId(envelope.id);
|
|
997
|
+
if (this.disposed) {
|
|
998
|
+
this.sendError(envelope.id, "HOST_DISPOSED", "extension host is disposed");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
let request;
|
|
1002
|
+
try {
|
|
1003
|
+
const message = parseExtensionWireMessage(value);
|
|
1004
|
+
if (message.kind !== "request") throw new Error("host accepts requests only");
|
|
1005
|
+
request = message;
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
const message = error instanceof Error ? error.message : "invalid request";
|
|
1008
|
+
const code = message.startsWith("unknown request method:") ? "UNKNOWN_METHOD" : "INVALID_REQUEST";
|
|
1009
|
+
this.sendError(envelope.id, code, message);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const maxConcurrent = this.options.maxConcurrentRequests ?? 8;
|
|
1013
|
+
if (this.inFlight >= maxConcurrent) {
|
|
1014
|
+
this.sendError(request.id, "HOST_BUSY", "extension host request limit reached");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
this.ongoingRequestIds.add(request.id);
|
|
1018
|
+
this.inFlight += 1;
|
|
1019
|
+
const operation = this.dispatch(request).finally(() => {
|
|
1020
|
+
this.ongoingRequestIds.delete(request.id);
|
|
1021
|
+
this.inFlight -= 1;
|
|
1022
|
+
});
|
|
1023
|
+
try {
|
|
1024
|
+
const result = await this.withTimeout(operation);
|
|
1025
|
+
if (result === TIMEOUT) {
|
|
1026
|
+
this.sendError(request.id, "REQUEST_TIMEOUT", `extension request ${request.method} timed out`);
|
|
1027
|
+
} else {
|
|
1028
|
+
this.options.send({ kind: "response", id: request.id, ok: true, result });
|
|
1029
|
+
}
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
const failure = this.failure(error);
|
|
1032
|
+
this.sendError(request.id, failure.code, failure.message);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
async dispose() {
|
|
1036
|
+
if (this.disposed) return;
|
|
1037
|
+
this.disposed = true;
|
|
1038
|
+
this.channelEpoch += 1;
|
|
1039
|
+
for (const [id2, pending] of this.pendingCapabilityRequests) {
|
|
1040
|
+
this.pendingCapabilityRequests.delete(id2);
|
|
1041
|
+
pending.reject(new Error("extension host is disposed"));
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
if (this.active) await this.module?.deactivate?.("host-disposed");
|
|
1045
|
+
} finally {
|
|
1046
|
+
this.active = false;
|
|
1047
|
+
this.module = null;
|
|
1048
|
+
this.commandHandlers.clear();
|
|
1049
|
+
this.channelHandlers.clear();
|
|
1050
|
+
this.channelFormatters.clear();
|
|
1051
|
+
this.channelStates.clear();
|
|
1052
|
+
this.channelCalls.clear();
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
async dispatch(request) {
|
|
1056
|
+
if (request.method === "handshake") {
|
|
1057
|
+
const manifest = this.options.manifest;
|
|
1058
|
+
const params = request.params;
|
|
1059
|
+
if (params.apiVersion !== manifest.apiVersion || params.extensionId !== manifest.id || params.extensionVersion !== manifest.version) {
|
|
1060
|
+
throw { code: "HANDSHAKE_MISMATCH", message: "Desktop handshake does not match the extension manifest" };
|
|
1061
|
+
}
|
|
1062
|
+
this.handshaken = true;
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
if (!this.handshaken) throw { code: "HANDSHAKE_REQUIRED", message: "extension host handshake is required" };
|
|
1066
|
+
if (request.method === "activate") {
|
|
1067
|
+
if (this.deactivating) throw { code: "NOT_ACTIVE", message: "extension host is stopping" };
|
|
1068
|
+
if (this.active) return null;
|
|
1069
|
+
if (!this.options.manifest.activationEvents.includes(request.params.activationEvent)) {
|
|
1070
|
+
throw { code: "ACTIVATION_DENIED", message: "activation event is not declared in the manifest" };
|
|
1071
|
+
}
|
|
1072
|
+
await this.ensureActivated();
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
if (request.method === "deactivate") {
|
|
1076
|
+
if (this.deactivating) throw { code: "NOT_ACTIVE", message: "extension host is stopping" };
|
|
1077
|
+
this.deactivating = true;
|
|
1078
|
+
this.channelEpoch += 1;
|
|
1079
|
+
if (this.activation) {
|
|
1080
|
+
try {
|
|
1081
|
+
await this.activation;
|
|
1082
|
+
} catch {
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
try {
|
|
1086
|
+
if (this.active) await this.module?.deactivate?.(request.params.reason);
|
|
1087
|
+
} finally {
|
|
1088
|
+
this.active = false;
|
|
1089
|
+
this.commandHandlers.clear();
|
|
1090
|
+
this.channelHandlers.clear();
|
|
1091
|
+
this.channelFormatters.clear();
|
|
1092
|
+
this.channelStates.clear();
|
|
1093
|
+
this.channelCalls.clear();
|
|
1094
|
+
this.deactivating = false;
|
|
1095
|
+
}
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
if (request.method === "capability.invoke") {
|
|
1099
|
+
throw { code: "UNSUPPORTED_DIRECTION", message: "capability.invoke is extension-initiated and cannot be dispatched to the host" };
|
|
1100
|
+
}
|
|
1101
|
+
if (!this.active) {
|
|
1102
|
+
throw { code: "NOT_ACTIVE", message: "extension command handler is not active" };
|
|
1103
|
+
}
|
|
1104
|
+
if (request.method === "channel.lifecycle") return this.deliverChannel(request.params);
|
|
1105
|
+
if (request.method === "channel.formatReply") return this.formatChannelReply(request.params);
|
|
1106
|
+
const handler = this.commandHandlers.get(request.params.commandId);
|
|
1107
|
+
if (!handler) {
|
|
1108
|
+
throw { code: "COMMAND_NOT_REGISTERED", message: `extension command is not registered: ${request.params.commandId}` };
|
|
1109
|
+
}
|
|
1110
|
+
const result = await handler(...request.params.args);
|
|
1111
|
+
const validated = parseExtensionWireMessage({ kind: "response", id: "result", ok: true, result });
|
|
1112
|
+
if (validated.kind !== "response" || !validated.ok) throw new Error("extension returned an invalid result");
|
|
1113
|
+
return validated.result;
|
|
1114
|
+
}
|
|
1115
|
+
async deliverChannel(event) {
|
|
1116
|
+
const contribution = this.options.manifest.contributes.channels?.find((channel) => channel.id === event.channelId);
|
|
1117
|
+
if (this.options.manifest.channelApiVersion !== 1 || !contribution || contribution.provider !== event.provider) {
|
|
1118
|
+
throw { code: "CHANNEL_NOT_DECLARED", message: "channel contribution/provider is not declared" };
|
|
1119
|
+
}
|
|
1120
|
+
const key = JSON.stringify([event.channelId, event.connectionId]);
|
|
1121
|
+
if (!this.channelStates.has(key) && !this.channelCalls.has(key) && (/* @__PURE__ */ new Set([...this.channelStates.keys(), ...this.channelCalls.keys()])).size >= 256) {
|
|
1122
|
+
throw { code: "CHANNEL_CAPACITY", message: "channel lifecycle capacity reached" };
|
|
1123
|
+
}
|
|
1124
|
+
const epoch = this.channelEpoch;
|
|
1125
|
+
const prior = this.channelCalls.get(key) ?? Promise.resolve();
|
|
1126
|
+
const next = prior.catch(() => void 0).then(async () => {
|
|
1127
|
+
if (this.disposed || !this.active || this.deactivating || epoch !== this.channelEpoch) {
|
|
1128
|
+
throw { code: "NOT_ACTIVE", message: "channel host is not active" };
|
|
1129
|
+
}
|
|
1130
|
+
const previous = this.channelStates.get(key);
|
|
1131
|
+
if (previous && (event.connectionRevision < previous.connectionRevision || event.connectionRevision === previous.connectionRevision && previous.state === "stop" && event.state === "start")) {
|
|
1132
|
+
throw { code: "STALE_CHANNEL", message: "channel lifecycle generation is stale" };
|
|
1133
|
+
}
|
|
1134
|
+
if (previous?.connectionRevision === event.connectionRevision && previous.state === event.state) return null;
|
|
1135
|
+
const handler = this.channelHandlers.get(event.channelId);
|
|
1136
|
+
if (!handler) throw { code: "CHANNEL_NOT_REGISTERED", message: "channel handler is not registered" };
|
|
1137
|
+
this.channelStates.set(key, Object.freeze({ ...event }));
|
|
1138
|
+
if (previous?.state === "start" && event.state === "start") {
|
|
1139
|
+
await handler(Object.freeze({ ...previous, state: "stop" }));
|
|
1140
|
+
if (this.disposed || !this.active || this.deactivating || epoch !== this.channelEpoch) {
|
|
1141
|
+
throw { code: "NOT_ACTIVE", message: "channel host stopped" };
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
await handler(Object.freeze({ ...event }));
|
|
1145
|
+
return null;
|
|
1146
|
+
});
|
|
1147
|
+
this.channelCalls.set(key, next);
|
|
1148
|
+
try {
|
|
1149
|
+
return await next;
|
|
1150
|
+
} finally {
|
|
1151
|
+
if (this.channelCalls.get(key) === next) this.channelCalls.delete(key);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
async formatChannelReply(input) {
|
|
1155
|
+
const key = JSON.stringify([input.channelId, input.connectionId]);
|
|
1156
|
+
const current = this.channelStates.get(key);
|
|
1157
|
+
const epoch = this.channelEpoch;
|
|
1158
|
+
const formatter = this.channelFormatters.get(input.channelId);
|
|
1159
|
+
const isCurrent = () => !this.disposed && !this.deactivating && this.active && epoch === this.channelEpoch && this.channelStates.get(key) === current && current?.state === "start" && current.provider === input.provider && current.connectionRevision === input.connectionRevision;
|
|
1160
|
+
if (!isCurrent() || !formatter) throw { code: "CHANNEL_FORMAT_UNAVAILABLE", message: "channel formatter is not active" };
|
|
1161
|
+
const result = await formatter(Object.freeze({ ...input }));
|
|
1162
|
+
if (!isCurrent()) throw { code: "CHANNEL_FORMAT_WITHDRAWN", message: "channel formatter stopped" };
|
|
1163
|
+
return parseChannelReplyFormatResult(result, input);
|
|
1164
|
+
}
|
|
1165
|
+
async withTimeout(operation) {
|
|
1166
|
+
let timer;
|
|
1167
|
+
const timeout = new Promise((resolve2) => {
|
|
1168
|
+
timer = setTimeout(() => resolve2(TIMEOUT), this.options.requestTimeoutMs ?? 5e3);
|
|
1169
|
+
});
|
|
1170
|
+
try {
|
|
1171
|
+
return await Promise.race([operation, timeout]);
|
|
1172
|
+
} finally {
|
|
1173
|
+
if (timer) clearTimeout(timer);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
async ensureActivated() {
|
|
1177
|
+
if (this.active) return;
|
|
1178
|
+
if (!this.activation) {
|
|
1179
|
+
const activation = this.activateExtension();
|
|
1180
|
+
this.activation = activation;
|
|
1181
|
+
void activation.finally(() => {
|
|
1182
|
+
if (this.activation === activation) this.activation = null;
|
|
1183
|
+
}).catch(() => void 0);
|
|
1184
|
+
}
|
|
1185
|
+
await this.activation;
|
|
1186
|
+
}
|
|
1187
|
+
async activateExtension() {
|
|
1188
|
+
this.activating = true;
|
|
1189
|
+
try {
|
|
1190
|
+
const loaded = this.module ?? extensionModule(
|
|
1191
|
+
await this.options.loadModule(this.options.manifest.entrypoint)
|
|
1192
|
+
);
|
|
1193
|
+
if (this.disposed) throw { code: "HOST_DISPOSED", message: "extension host is disposed" };
|
|
1194
|
+
const registered = /* @__PURE__ */ new Map();
|
|
1195
|
+
const registeredChannels = /* @__PURE__ */ new Map();
|
|
1196
|
+
const registeredFormatters = /* @__PURE__ */ new Map();
|
|
1197
|
+
await loaded.activate({
|
|
1198
|
+
extensionId: this.options.manifest.id,
|
|
1199
|
+
channels: {
|
|
1200
|
+
register: (channelId, handler) => {
|
|
1201
|
+
if (!this.activating || this.options.manifest.channelApiVersion !== 1 || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredChannels.has(channelId)) {
|
|
1202
|
+
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "channel must be declared and registered once" };
|
|
1203
|
+
}
|
|
1204
|
+
registeredChannels.set(channelId, handler);
|
|
1205
|
+
},
|
|
1206
|
+
registerFormatter: (channelId, formatter) => {
|
|
1207
|
+
if (!this.activating || this.options.manifest.channelApiVersion !== 1 || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof formatter !== "function" || registeredFormatters.has(channelId)) {
|
|
1208
|
+
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "formatter must be declared and registered once" };
|
|
1209
|
+
}
|
|
1210
|
+
registeredFormatters.set(channelId, formatter);
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
commands: {
|
|
1214
|
+
register: (commandId, handler) => {
|
|
1215
|
+
this.validateCommandRegistration(commandId, handler, registered);
|
|
1216
|
+
registered.set(commandId, handler);
|
|
1217
|
+
}
|
|
1218
|
+
},
|
|
1219
|
+
invokeCapability: (permission, action, args) => this.invokeCapability(permission, action, args)
|
|
1220
|
+
});
|
|
1221
|
+
if (this.disposed) {
|
|
1222
|
+
await loaded.deactivate?.("host-disposed");
|
|
1223
|
+
throw { code: "HOST_DISPOSED", message: "extension host is disposed" };
|
|
1224
|
+
}
|
|
1225
|
+
this.module = loaded;
|
|
1226
|
+
this.channelHandlers.clear();
|
|
1227
|
+
this.channelFormatters.clear();
|
|
1228
|
+
for (const [id2, formatter] of registeredFormatters) this.channelFormatters.set(id2, formatter);
|
|
1229
|
+
for (const [id2, handler] of registeredChannels) this.channelHandlers.set(id2, handler);
|
|
1230
|
+
this.commandHandlers.clear();
|
|
1231
|
+
for (const [commandId, handler] of registered) this.commandHandlers.set(commandId, handler);
|
|
1232
|
+
this.active = true;
|
|
1233
|
+
} finally {
|
|
1234
|
+
this.activating = false;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
rememberRequestId(id2) {
|
|
1238
|
+
this.respondedIds.add(id2);
|
|
1239
|
+
const limit = Math.max(1, this.options.maxRememberedRequestIds ?? 4096);
|
|
1240
|
+
while (this.respondedIds.size > limit) {
|
|
1241
|
+
const oldest = this.respondedIds.values().next().value;
|
|
1242
|
+
if (!oldest) return;
|
|
1243
|
+
this.respondedIds.delete(oldest);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
failure(error) {
|
|
1247
|
+
if (error && typeof error === "object") {
|
|
1248
|
+
const record2 = error;
|
|
1249
|
+
if (typeof record2.code === "string" && typeof record2.message === "string") {
|
|
1250
|
+
return { code: record2.code, message: record2.message };
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return { code: "EXTENSION_ERROR", message: error instanceof Error ? error.message : "extension request failed" };
|
|
1254
|
+
}
|
|
1255
|
+
sendError(id2, code, message) {
|
|
1256
|
+
this.options.send({ kind: "response", id: id2, ok: false, error: { code, message } });
|
|
1257
|
+
}
|
|
1258
|
+
validateCommandRegistration(commandId, handler, registered) {
|
|
1259
|
+
if (!commandId.startsWith(`${this.options.manifest.id}.`)) {
|
|
1260
|
+
throw new Error(`command must be namespaced by ${this.options.manifest.id}: ${commandId}`);
|
|
1261
|
+
}
|
|
1262
|
+
if (!this.options.manifest.contributes.commands?.some((command) => command.id === commandId)) {
|
|
1263
|
+
throw new Error(`command is not declared in the manifest: ${commandId}`);
|
|
1264
|
+
}
|
|
1265
|
+
if (typeof handler !== "function") throw new Error(`command handler must be a function: ${commandId}`);
|
|
1266
|
+
if (registered.has(commandId)) throw new Error(`duplicate command registration: ${commandId}`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function extensionModule(imported) {
|
|
1270
|
+
if ("default" in imported) return imported.default;
|
|
1271
|
+
return imported;
|
|
1272
|
+
}
|
|
1273
|
+
function protocolUrl(extensionId2, relativePath2) {
|
|
1274
|
+
return `saycode-extension://${extensionId2}/${relativePath2}`;
|
|
1275
|
+
}
|
|
1276
|
+
async function startExtensionHost(deps) {
|
|
1277
|
+
const manifestText = await deps.fetchText(protocolUrl(deps.extensionId, "extension.json"));
|
|
1278
|
+
const manifest = parseExtensionManifest(JSON.parse(manifestText), {
|
|
1279
|
+
supportedApiVersion: SAYCODE_EXTENSION_API_VERSION,
|
|
1280
|
+
// Must match what main accepted at install time, or an extension inside the support
|
|
1281
|
+
// window would install fine and then fail to boot in its own renderer.
|
|
1282
|
+
minimumSupportedApiVersion: SAYCODE_EXTENSION_MINIMUM_API_VERSION,
|
|
1283
|
+
saycodeVersion: SAYCODE_EXTENSION_ENGINE_VERSION
|
|
1284
|
+
});
|
|
1285
|
+
const runtime = new ExtensionHostRuntime({
|
|
1286
|
+
manifest,
|
|
1287
|
+
loadModule: (entrypoint) => deps.importModule(protocolUrl(deps.extensionId, entrypoint)),
|
|
1288
|
+
send: (message) => deps.bridge.send(message)
|
|
1289
|
+
});
|
|
1290
|
+
deps.bridge.onMessage((message) => {
|
|
1291
|
+
void runtime.receive(message);
|
|
1292
|
+
});
|
|
1293
|
+
runtime.start();
|
|
1294
|
+
return runtime;
|
|
1295
|
+
}
|
|
1296
|
+
const PROTOCOL = "saycode-extension://";
|
|
1297
|
+
function resolveExtensionFile(extensionDir2, extensionId2, url) {
|
|
1298
|
+
const prefix = `${PROTOCOL}${extensionId2}/`;
|
|
1299
|
+
if (!url.startsWith(prefix)) throw new Error("extension file outside this extension");
|
|
1300
|
+
const relative = decodeURIComponent(url.slice(prefix.length));
|
|
1301
|
+
const root = resolve(extensionDir2);
|
|
1302
|
+
const target = resolve(root, relative);
|
|
1303
|
+
if (target !== root && !target.startsWith(root + sep)) throw new Error("extension file outside this extension");
|
|
1304
|
+
return target;
|
|
1305
|
+
}
|
|
1306
|
+
async function startNodeExtensionHost(input) {
|
|
1307
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1308
|
+
input.input.onLine((line) => {
|
|
1309
|
+
let message;
|
|
1310
|
+
try {
|
|
1311
|
+
message = JSON.parse(line);
|
|
1312
|
+
} catch {
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
for (const listener of listeners) listener(message);
|
|
1316
|
+
});
|
|
1317
|
+
return startExtensionHost({
|
|
1318
|
+
extensionId: input.extensionId,
|
|
1319
|
+
bridge: {
|
|
1320
|
+
send: (message) => input.write(JSON.stringify(message)),
|
|
1321
|
+
onMessage: (listener) => {
|
|
1322
|
+
listeners.add(listener);
|
|
1323
|
+
return () => {
|
|
1324
|
+
listeners.delete(listener);
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
fetchText: (url) => readFile(resolveExtensionFile(input.extensionDir, input.extensionId, url), "utf8"),
|
|
1329
|
+
importModule: (url) => import(
|
|
1330
|
+
/* @vite-ignore */
|
|
1331
|
+
pathToFileURL(resolveExtensionFile(input.extensionDir, input.extensionId, url)).href
|
|
1332
|
+
)
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
const [extensionId, extensionDir] = process.argv.slice(2);
|
|
1336
|
+
if (!extensionId || !extensionDir) process.exit(2);
|
|
1337
|
+
const lines = createInterface({ input: process.stdin });
|
|
1338
|
+
lines.on("close", () => process.exit(0));
|
|
1339
|
+
void startNodeExtensionHost({
|
|
1340
|
+
extensionId,
|
|
1341
|
+
extensionDir,
|
|
1342
|
+
input: { onLine: (listener) => {
|
|
1343
|
+
lines.on("line", listener);
|
|
1344
|
+
} },
|
|
1345
|
+
write: (line) => {
|
|
1346
|
+
process.stdout.write(`${line}
|
|
1347
|
+
`);
|
|
1348
|
+
}
|
|
1349
|
+
}).catch(() => process.exit(1));
|