@ubiquity-os/plugin-sdk 3.4.6 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compression.js +1 -1
- package/dist/compression.mjs +1 -1
- package/dist/configuration.d.mts +121 -0
- package/dist/configuration.d.ts +121 -0
- package/dist/configuration.js +340 -0
- package/dist/configuration.mjs +302 -0
- package/dist/context-3Ck9sBZI.d.mts +107 -0
- package/dist/context-DOUnUNNN.d.ts +107 -0
- package/dist/index.d.mts +7 -106
- package/dist/index.d.ts +7 -106
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/manifest.d.mts +1 -0
- package/dist/manifest.d.ts +1 -0
- package/dist/manifest.js +1 -0
- package/dist/manifest.mjs +1 -0
- package/dist/signature.js +1 -1
- package/dist/signature.mjs +1 -1
- package/package.json +12 -2
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/configuration.ts
|
|
31
|
+
var configuration_exports = {};
|
|
32
|
+
__export(configuration_exports, {
|
|
33
|
+
CONFIG_DEV_FULL_PATH: () => CONFIG_DEV_FULL_PATH,
|
|
34
|
+
CONFIG_ORG_REPO: () => CONFIG_ORG_REPO,
|
|
35
|
+
CONFIG_PROD_FULL_PATH: () => CONFIG_PROD_FULL_PATH,
|
|
36
|
+
ConfigurationHandler: () => ConfigurationHandler
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(configuration_exports);
|
|
39
|
+
var import_value = require("@sinclair/typebox/value");
|
|
40
|
+
var import_js_yaml = __toESM(require("js-yaml"));
|
|
41
|
+
var import_node_buffer = require("node:buffer");
|
|
42
|
+
|
|
43
|
+
// src/configuration/schema.ts
|
|
44
|
+
var import_webhooks = require("@octokit/webhooks");
|
|
45
|
+
var import_typebox = require("@sinclair/typebox");
|
|
46
|
+
var pluginNameRegex = new RegExp("^([0-9a-zA-Z-._]+)\\/([0-9a-zA-Z-._]+)(?::([0-9a-zA-Z-._]+))?(?:@([0-9a-zA-Z-._]+(?:\\/[0-9a-zA-Z-._]+)*))?$");
|
|
47
|
+
function parsePluginIdentifier(value) {
|
|
48
|
+
const matches = pluginNameRegex.exec(value);
|
|
49
|
+
if (!matches) {
|
|
50
|
+
throw new Error(`Invalid plugin name: ${value}`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
owner: matches[1],
|
|
54
|
+
repo: matches[2],
|
|
55
|
+
workflowId: matches[3] || "compute.yml",
|
|
56
|
+
ref: matches[4] || void 0
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function stringLiteralUnion(values) {
|
|
60
|
+
const literals = values.map((value) => import_typebox.Type.Literal(value));
|
|
61
|
+
return import_typebox.Type.Union(literals);
|
|
62
|
+
}
|
|
63
|
+
var emitterType = stringLiteralUnion(import_webhooks.emitterEventNames);
|
|
64
|
+
var runsOnSchema = import_typebox.Type.Array(emitterType, { default: [] });
|
|
65
|
+
var pluginSettingsSchema = import_typebox.Type.Union(
|
|
66
|
+
[
|
|
67
|
+
import_typebox.Type.Null(),
|
|
68
|
+
import_typebox.Type.Object(
|
|
69
|
+
{
|
|
70
|
+
with: import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Unknown(), { default: {} }),
|
|
71
|
+
runsOn: import_typebox.Type.Optional(runsOnSchema),
|
|
72
|
+
skipBotEvents: import_typebox.Type.Optional(import_typebox.Type.Boolean())
|
|
73
|
+
},
|
|
74
|
+
{ default: {} }
|
|
75
|
+
)
|
|
76
|
+
],
|
|
77
|
+
{ default: null }
|
|
78
|
+
);
|
|
79
|
+
var configSchema = import_typebox.Type.Object(
|
|
80
|
+
{
|
|
81
|
+
plugins: import_typebox.Type.Record(import_typebox.Type.String(), pluginSettingsSchema, { default: {} })
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
additionalProperties: true
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// src/types/manifest.ts
|
|
89
|
+
var import_typebox2 = require("@sinclair/typebox");
|
|
90
|
+
var import_webhooks2 = require("@octokit/webhooks");
|
|
91
|
+
var runEvent = import_typebox2.Type.Union(import_webhooks2.emitterEventNames.map((o) => import_typebox2.Type.Literal(o)));
|
|
92
|
+
var exampleCommandExecutionSchema = import_typebox2.Type.Object({
|
|
93
|
+
commandInvocation: import_typebox2.Type.String({ minLength: 1 }),
|
|
94
|
+
githubContext: import_typebox2.Type.Optional(import_typebox2.Type.Record(import_typebox2.Type.String(), import_typebox2.Type.Any())),
|
|
95
|
+
expectedToolCallResult: import_typebox2.Type.Object({
|
|
96
|
+
function: import_typebox2.Type.String({ minLength: 1 }),
|
|
97
|
+
parameters: import_typebox2.Type.Record(import_typebox2.Type.String(), import_typebox2.Type.Any())
|
|
98
|
+
})
|
|
99
|
+
});
|
|
100
|
+
var commandSchema = import_typebox2.Type.Object({
|
|
101
|
+
description: import_typebox2.Type.String({ minLength: 1 }),
|
|
102
|
+
"ubiquity:example": import_typebox2.Type.String({ minLength: 1 }),
|
|
103
|
+
parameters: import_typebox2.Type.Optional(import_typebox2.Type.Record(import_typebox2.Type.String(), import_typebox2.Type.Any())),
|
|
104
|
+
examples: import_typebox2.Type.Optional(import_typebox2.Type.Array(exampleCommandExecutionSchema, { default: [] }))
|
|
105
|
+
});
|
|
106
|
+
var manifestSchema = import_typebox2.Type.Object({
|
|
107
|
+
name: import_typebox2.Type.String({ minLength: 1 }),
|
|
108
|
+
short_name: import_typebox2.Type.String({ minLength: 1 }),
|
|
109
|
+
description: import_typebox2.Type.Optional(import_typebox2.Type.String({ default: "" })),
|
|
110
|
+
commands: import_typebox2.Type.Optional(import_typebox2.Type.Record(import_typebox2.Type.String({ pattern: "^[A-Za-z-_]+$" }), commandSchema, { default: {} })),
|
|
111
|
+
"ubiquity:listeners": import_typebox2.Type.Optional(import_typebox2.Type.Array(runEvent, { default: [] })),
|
|
112
|
+
configuration: import_typebox2.Type.Optional(import_typebox2.Type.Record(import_typebox2.Type.String(), import_typebox2.Type.Any(), { default: {} })),
|
|
113
|
+
skipBotEvents: import_typebox2.Type.Optional(import_typebox2.Type.Boolean({ default: true }))
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// src/configuration.ts
|
|
117
|
+
var CONFIG_PROD_FULL_PATH = ".github/.ubiquity-os.config.yml";
|
|
118
|
+
var CONFIG_DEV_FULL_PATH = ".github/.ubiquity-os.config.dev.yml";
|
|
119
|
+
var CONFIG_ORG_REPO = ".ubiquity-os";
|
|
120
|
+
var ConfigurationHandler = class {
|
|
121
|
+
constructor(_logger, _octokit) {
|
|
122
|
+
this._logger = _logger;
|
|
123
|
+
this._octokit = _octokit;
|
|
124
|
+
}
|
|
125
|
+
_manifestCache = {};
|
|
126
|
+
_manifestPromiseCache = {};
|
|
127
|
+
/**
|
|
128
|
+
* Retrieves the configuration for the current plugin based on its manifest.
|
|
129
|
+
* @param manifest - The plugin manifest containing the `short_name` identifier
|
|
130
|
+
* @param location - Optional repository location (`owner/repo`)
|
|
131
|
+
* @returns The plugin's configuration or null if not found
|
|
132
|
+
**/
|
|
133
|
+
async getSelfConfiguration(manifest, location) {
|
|
134
|
+
const cfg = await this.getConfiguration(location);
|
|
135
|
+
const name = manifest.short_name.split("@")[0];
|
|
136
|
+
const selfConfig = Object.keys(cfg.plugins).find((key) => new RegExp(`^${name}(?:$|@.+)`).exec(key.replace(/:[^@]+/, "")));
|
|
137
|
+
return selfConfig && cfg.plugins[selfConfig] ? cfg.plugins[selfConfig]["with"] : null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Retrieves and merges configuration from organization and repository levels.
|
|
141
|
+
* @param location - Optional repository location (`owner` and `repo`). If not provided, returns the default configuration.
|
|
142
|
+
* @returns The merged plugin configuration with resolved plugin settings.
|
|
143
|
+
*/
|
|
144
|
+
async getConfiguration(location) {
|
|
145
|
+
const defaultConfiguration = import_value.Value.Decode(configSchema, import_value.Value.Default(configSchema, {}));
|
|
146
|
+
if (!location) {
|
|
147
|
+
this._logger.debug("No location was provided, using the default configuration");
|
|
148
|
+
return defaultConfiguration;
|
|
149
|
+
}
|
|
150
|
+
const { owner, repo } = location;
|
|
151
|
+
let mergedConfiguration = defaultConfiguration;
|
|
152
|
+
this._logger.debug("Fetching configurations from the organization and repository", {
|
|
153
|
+
orgRepo: `${owner}/${CONFIG_ORG_REPO}`,
|
|
154
|
+
repo: `${owner}/${repo}`
|
|
155
|
+
});
|
|
156
|
+
const orgConfig = await this._getConfigurationFromRepo(owner, CONFIG_ORG_REPO);
|
|
157
|
+
const repoConfig = await this._getConfigurationFromRepo(owner, repo);
|
|
158
|
+
if (orgConfig.config) {
|
|
159
|
+
mergedConfiguration = this.mergeConfigurations(mergedConfiguration, orgConfig.config);
|
|
160
|
+
}
|
|
161
|
+
if (repoConfig.config) {
|
|
162
|
+
mergedConfiguration = this.mergeConfigurations(mergedConfiguration, repoConfig.config);
|
|
163
|
+
}
|
|
164
|
+
const resolvedPlugins = {};
|
|
165
|
+
this._logger.debug("Found plugins enabled", { repo: `${owner}/${repo}`, plugins: Object.keys(mergedConfiguration.plugins).length });
|
|
166
|
+
for (const [pluginKey, pluginSettings] of Object.entries(mergedConfiguration.plugins)) {
|
|
167
|
+
let pluginIdentifier;
|
|
168
|
+
try {
|
|
169
|
+
pluginIdentifier = parsePluginIdentifier(pluginKey);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
this._logger.error("Invalid plugin identifier; skipping", { plugin: pluginKey, err: error });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const manifest = await this.getManifest(pluginIdentifier);
|
|
175
|
+
let runsOn = pluginSettings?.runsOn ?? [];
|
|
176
|
+
let shouldSkipBotEvents = pluginSettings?.skipBotEvents;
|
|
177
|
+
if (manifest) {
|
|
178
|
+
if (!runsOn.length) {
|
|
179
|
+
runsOn = manifest["ubiquity:listeners"] ?? [];
|
|
180
|
+
}
|
|
181
|
+
if (shouldSkipBotEvents === void 0) {
|
|
182
|
+
shouldSkipBotEvents = manifest.skipBotEvents ?? true;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
shouldSkipBotEvents = true;
|
|
186
|
+
}
|
|
187
|
+
resolvedPlugins[pluginKey] = {
|
|
188
|
+
...pluginSettings,
|
|
189
|
+
with: pluginSettings?.with ?? {},
|
|
190
|
+
runsOn,
|
|
191
|
+
skipBotEvents: shouldSkipBotEvents
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
...mergedConfiguration,
|
|
196
|
+
plugins: resolvedPlugins
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async _getConfigurationFromRepo(owner, repository) {
|
|
200
|
+
const rawData = await this._download({
|
|
201
|
+
repository,
|
|
202
|
+
owner
|
|
203
|
+
});
|
|
204
|
+
this._logger.debug("Downloaded configuration file", { owner, repository });
|
|
205
|
+
if (!rawData) {
|
|
206
|
+
this._logger.debug("No raw configuration data", { owner, repository });
|
|
207
|
+
return { config: null, errors: null, rawData: null };
|
|
208
|
+
}
|
|
209
|
+
const { yaml, errors } = this._parseYaml(rawData);
|
|
210
|
+
const targetRepoConfiguration = yaml;
|
|
211
|
+
this._logger.debug("Decoding configuration", { owner, repository });
|
|
212
|
+
if (targetRepoConfiguration) {
|
|
213
|
+
try {
|
|
214
|
+
const configSchemaWithDefaults = import_value.Value.Default(configSchema, targetRepoConfiguration);
|
|
215
|
+
const errors2 = import_value.Value.Errors(configSchema, configSchemaWithDefaults);
|
|
216
|
+
if (errors2.First()) {
|
|
217
|
+
for (const error of errors2) {
|
|
218
|
+
this._logger.warn("Configuration validation error", { err: error });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const decodedConfig = import_value.Value.Decode(configSchema, configSchemaWithDefaults);
|
|
222
|
+
return { config: decodedConfig, errors: errors2, rawData };
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this._logger.error("Error decoding configuration; Will ignore.", { err: error, owner, repository });
|
|
225
|
+
return { config: null, errors: [error instanceof import_value.TransformDecodeCheckError ? error.error : error], rawData };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this._logger.error("YAML could not be decoded", { owner, repository, errors });
|
|
229
|
+
return { config: null, errors, rawData };
|
|
230
|
+
}
|
|
231
|
+
async _download({ repository, owner }) {
|
|
232
|
+
if (!repository || !owner) {
|
|
233
|
+
this._logger.error("Repo or owner is not defined, cannot download the requested file");
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const pathList = [CONFIG_PROD_FULL_PATH, CONFIG_DEV_FULL_PATH];
|
|
237
|
+
for (const filePath of pathList) {
|
|
238
|
+
try {
|
|
239
|
+
this._logger.debug("Attempting to fetch configuration", { owner, repository, filePath });
|
|
240
|
+
const { data, headers } = await this._octokit.rest.repos.getContent({
|
|
241
|
+
owner,
|
|
242
|
+
repo: repository,
|
|
243
|
+
path: filePath,
|
|
244
|
+
mediaType: { format: "raw" }
|
|
245
|
+
});
|
|
246
|
+
this._logger.debug("Configuration file found", { owner, repository, filePath, rateLimitRemaining: headers?.["x-ratelimit-remaining"], data });
|
|
247
|
+
return data;
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (err && typeof err === "object" && "status" in err && err.status === 404) {
|
|
250
|
+
this._logger.warn("No configuration file found", { owner, repository, filePath });
|
|
251
|
+
} else {
|
|
252
|
+
this._logger.error("Failed to download the requested file", { err, owner, repository, filePath });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
_parseYaml(data) {
|
|
259
|
+
this._logger.debug("Will attempt to parse YAML data", { data });
|
|
260
|
+
try {
|
|
261
|
+
if (data) {
|
|
262
|
+
const parsedData = import_js_yaml.default.load(data);
|
|
263
|
+
this._logger.debug("Parsed yaml data", { parsedData });
|
|
264
|
+
return { yaml: parsedData ?? null, errors: null };
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
this._logger.error("Error parsing YAML", { error });
|
|
268
|
+
return { errors: [error], yaml: null };
|
|
269
|
+
}
|
|
270
|
+
this._logger.debug("Could not parse YAML");
|
|
271
|
+
return { yaml: null, errors: null };
|
|
272
|
+
}
|
|
273
|
+
mergeConfigurations(configuration1, configuration2) {
|
|
274
|
+
const mergedPlugins = {
|
|
275
|
+
...configuration1.plugins,
|
|
276
|
+
...configuration2.plugins
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
...configuration1,
|
|
280
|
+
...configuration2,
|
|
281
|
+
plugins: mergedPlugins
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
getManifest(plugin) {
|
|
285
|
+
return this._fetchActionManifest(plugin);
|
|
286
|
+
}
|
|
287
|
+
async _fetchActionManifest({ owner, repo, ref }) {
|
|
288
|
+
const manifestKey = ref ? `${owner}:${repo}:${ref}` : `${owner}:${repo}`;
|
|
289
|
+
if (this._manifestCache[manifestKey]) {
|
|
290
|
+
return this._manifestCache[manifestKey];
|
|
291
|
+
}
|
|
292
|
+
if (this._manifestPromiseCache[manifestKey]) {
|
|
293
|
+
return this._manifestPromiseCache[manifestKey];
|
|
294
|
+
}
|
|
295
|
+
const manifestPromise = (async () => {
|
|
296
|
+
try {
|
|
297
|
+
const { data } = await this._octokit.rest.repos.getContent({
|
|
298
|
+
owner,
|
|
299
|
+
repo,
|
|
300
|
+
path: "manifest.json",
|
|
301
|
+
ref
|
|
302
|
+
});
|
|
303
|
+
if ("content" in data) {
|
|
304
|
+
const content = import_node_buffer.Buffer.from(data.content, "base64").toString();
|
|
305
|
+
const contentParsed = JSON.parse(content);
|
|
306
|
+
const manifest = this._decodeManifest(contentParsed);
|
|
307
|
+
this._manifestCache[manifestKey] = manifest;
|
|
308
|
+
return manifest;
|
|
309
|
+
}
|
|
310
|
+
} catch (e) {
|
|
311
|
+
this._logger.error("Could not find a valid manifest", { owner, repo, err: e });
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
})();
|
|
315
|
+
this._manifestPromiseCache[manifestKey] = manifestPromise;
|
|
316
|
+
try {
|
|
317
|
+
return await manifestPromise;
|
|
318
|
+
} finally {
|
|
319
|
+
delete this._manifestPromiseCache[manifestKey];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
_decodeManifest(manifest) {
|
|
323
|
+
const errors = [...import_value.Value.Errors(manifestSchema, manifest)];
|
|
324
|
+
if (errors.length) {
|
|
325
|
+
for (const error of errors) {
|
|
326
|
+
this._logger.error("Manifest validation error", { error });
|
|
327
|
+
}
|
|
328
|
+
throw new Error("Manifest is invalid.");
|
|
329
|
+
}
|
|
330
|
+
const defaultManifest = import_value.Value.Default(manifestSchema, manifest);
|
|
331
|
+
return defaultManifest;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
335
|
+
0 && (module.exports = {
|
|
336
|
+
CONFIG_DEV_FULL_PATH,
|
|
337
|
+
CONFIG_ORG_REPO,
|
|
338
|
+
CONFIG_PROD_FULL_PATH,
|
|
339
|
+
ConfigurationHandler
|
|
340
|
+
});
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// src/configuration.ts
|
|
2
|
+
import { TransformDecodeCheckError, Value } from "@sinclair/typebox/value";
|
|
3
|
+
import YAML from "js-yaml";
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
5
|
+
|
|
6
|
+
// src/configuration/schema.ts
|
|
7
|
+
import { emitterEventNames } from "@octokit/webhooks";
|
|
8
|
+
import { Type as T } from "@sinclair/typebox";
|
|
9
|
+
var pluginNameRegex = new RegExp("^([0-9a-zA-Z-._]+)\\/([0-9a-zA-Z-._]+)(?::([0-9a-zA-Z-._]+))?(?:@([0-9a-zA-Z-._]+(?:\\/[0-9a-zA-Z-._]+)*))?$");
|
|
10
|
+
function parsePluginIdentifier(value) {
|
|
11
|
+
const matches = pluginNameRegex.exec(value);
|
|
12
|
+
if (!matches) {
|
|
13
|
+
throw new Error(`Invalid plugin name: ${value}`);
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
owner: matches[1],
|
|
17
|
+
repo: matches[2],
|
|
18
|
+
workflowId: matches[3] || "compute.yml",
|
|
19
|
+
ref: matches[4] || void 0
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function stringLiteralUnion(values) {
|
|
23
|
+
const literals = values.map((value) => T.Literal(value));
|
|
24
|
+
return T.Union(literals);
|
|
25
|
+
}
|
|
26
|
+
var emitterType = stringLiteralUnion(emitterEventNames);
|
|
27
|
+
var runsOnSchema = T.Array(emitterType, { default: [] });
|
|
28
|
+
var pluginSettingsSchema = T.Union(
|
|
29
|
+
[
|
|
30
|
+
T.Null(),
|
|
31
|
+
T.Object(
|
|
32
|
+
{
|
|
33
|
+
with: T.Record(T.String(), T.Unknown(), { default: {} }),
|
|
34
|
+
runsOn: T.Optional(runsOnSchema),
|
|
35
|
+
skipBotEvents: T.Optional(T.Boolean())
|
|
36
|
+
},
|
|
37
|
+
{ default: {} }
|
|
38
|
+
)
|
|
39
|
+
],
|
|
40
|
+
{ default: null }
|
|
41
|
+
);
|
|
42
|
+
var configSchema = T.Object(
|
|
43
|
+
{
|
|
44
|
+
plugins: T.Record(T.String(), pluginSettingsSchema, { default: {} })
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
additionalProperties: true
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// src/types/manifest.ts
|
|
52
|
+
import { Type as T2 } from "@sinclair/typebox";
|
|
53
|
+
import { emitterEventNames as emitterEventNames2 } from "@octokit/webhooks";
|
|
54
|
+
var runEvent = T2.Union(emitterEventNames2.map((o) => T2.Literal(o)));
|
|
55
|
+
var exampleCommandExecutionSchema = T2.Object({
|
|
56
|
+
commandInvocation: T2.String({ minLength: 1 }),
|
|
57
|
+
githubContext: T2.Optional(T2.Record(T2.String(), T2.Any())),
|
|
58
|
+
expectedToolCallResult: T2.Object({
|
|
59
|
+
function: T2.String({ minLength: 1 }),
|
|
60
|
+
parameters: T2.Record(T2.String(), T2.Any())
|
|
61
|
+
})
|
|
62
|
+
});
|
|
63
|
+
var commandSchema = T2.Object({
|
|
64
|
+
description: T2.String({ minLength: 1 }),
|
|
65
|
+
"ubiquity:example": T2.String({ minLength: 1 }),
|
|
66
|
+
parameters: T2.Optional(T2.Record(T2.String(), T2.Any())),
|
|
67
|
+
examples: T2.Optional(T2.Array(exampleCommandExecutionSchema, { default: [] }))
|
|
68
|
+
});
|
|
69
|
+
var manifestSchema = T2.Object({
|
|
70
|
+
name: T2.String({ minLength: 1 }),
|
|
71
|
+
short_name: T2.String({ minLength: 1 }),
|
|
72
|
+
description: T2.Optional(T2.String({ default: "" })),
|
|
73
|
+
commands: T2.Optional(T2.Record(T2.String({ pattern: "^[A-Za-z-_]+$" }), commandSchema, { default: {} })),
|
|
74
|
+
"ubiquity:listeners": T2.Optional(T2.Array(runEvent, { default: [] })),
|
|
75
|
+
configuration: T2.Optional(T2.Record(T2.String(), T2.Any(), { default: {} })),
|
|
76
|
+
skipBotEvents: T2.Optional(T2.Boolean({ default: true }))
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// src/configuration.ts
|
|
80
|
+
var CONFIG_PROD_FULL_PATH = ".github/.ubiquity-os.config.yml";
|
|
81
|
+
var CONFIG_DEV_FULL_PATH = ".github/.ubiquity-os.config.dev.yml";
|
|
82
|
+
var CONFIG_ORG_REPO = ".ubiquity-os";
|
|
83
|
+
var ConfigurationHandler = class {
|
|
84
|
+
constructor(_logger, _octokit) {
|
|
85
|
+
this._logger = _logger;
|
|
86
|
+
this._octokit = _octokit;
|
|
87
|
+
}
|
|
88
|
+
_manifestCache = {};
|
|
89
|
+
_manifestPromiseCache = {};
|
|
90
|
+
/**
|
|
91
|
+
* Retrieves the configuration for the current plugin based on its manifest.
|
|
92
|
+
* @param manifest - The plugin manifest containing the `short_name` identifier
|
|
93
|
+
* @param location - Optional repository location (`owner/repo`)
|
|
94
|
+
* @returns The plugin's configuration or null if not found
|
|
95
|
+
**/
|
|
96
|
+
async getSelfConfiguration(manifest, location) {
|
|
97
|
+
const cfg = await this.getConfiguration(location);
|
|
98
|
+
const name = manifest.short_name.split("@")[0];
|
|
99
|
+
const selfConfig = Object.keys(cfg.plugins).find((key) => new RegExp(`^${name}(?:$|@.+)`).exec(key.replace(/:[^@]+/, "")));
|
|
100
|
+
return selfConfig && cfg.plugins[selfConfig] ? cfg.plugins[selfConfig]["with"] : null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Retrieves and merges configuration from organization and repository levels.
|
|
104
|
+
* @param location - Optional repository location (`owner` and `repo`). If not provided, returns the default configuration.
|
|
105
|
+
* @returns The merged plugin configuration with resolved plugin settings.
|
|
106
|
+
*/
|
|
107
|
+
async getConfiguration(location) {
|
|
108
|
+
const defaultConfiguration = Value.Decode(configSchema, Value.Default(configSchema, {}));
|
|
109
|
+
if (!location) {
|
|
110
|
+
this._logger.debug("No location was provided, using the default configuration");
|
|
111
|
+
return defaultConfiguration;
|
|
112
|
+
}
|
|
113
|
+
const { owner, repo } = location;
|
|
114
|
+
let mergedConfiguration = defaultConfiguration;
|
|
115
|
+
this._logger.debug("Fetching configurations from the organization and repository", {
|
|
116
|
+
orgRepo: `${owner}/${CONFIG_ORG_REPO}`,
|
|
117
|
+
repo: `${owner}/${repo}`
|
|
118
|
+
});
|
|
119
|
+
const orgConfig = await this._getConfigurationFromRepo(owner, CONFIG_ORG_REPO);
|
|
120
|
+
const repoConfig = await this._getConfigurationFromRepo(owner, repo);
|
|
121
|
+
if (orgConfig.config) {
|
|
122
|
+
mergedConfiguration = this.mergeConfigurations(mergedConfiguration, orgConfig.config);
|
|
123
|
+
}
|
|
124
|
+
if (repoConfig.config) {
|
|
125
|
+
mergedConfiguration = this.mergeConfigurations(mergedConfiguration, repoConfig.config);
|
|
126
|
+
}
|
|
127
|
+
const resolvedPlugins = {};
|
|
128
|
+
this._logger.debug("Found plugins enabled", { repo: `${owner}/${repo}`, plugins: Object.keys(mergedConfiguration.plugins).length });
|
|
129
|
+
for (const [pluginKey, pluginSettings] of Object.entries(mergedConfiguration.plugins)) {
|
|
130
|
+
let pluginIdentifier;
|
|
131
|
+
try {
|
|
132
|
+
pluginIdentifier = parsePluginIdentifier(pluginKey);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
this._logger.error("Invalid plugin identifier; skipping", { plugin: pluginKey, err: error });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const manifest = await this.getManifest(pluginIdentifier);
|
|
138
|
+
let runsOn = pluginSettings?.runsOn ?? [];
|
|
139
|
+
let shouldSkipBotEvents = pluginSettings?.skipBotEvents;
|
|
140
|
+
if (manifest) {
|
|
141
|
+
if (!runsOn.length) {
|
|
142
|
+
runsOn = manifest["ubiquity:listeners"] ?? [];
|
|
143
|
+
}
|
|
144
|
+
if (shouldSkipBotEvents === void 0) {
|
|
145
|
+
shouldSkipBotEvents = manifest.skipBotEvents ?? true;
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
shouldSkipBotEvents = true;
|
|
149
|
+
}
|
|
150
|
+
resolvedPlugins[pluginKey] = {
|
|
151
|
+
...pluginSettings,
|
|
152
|
+
with: pluginSettings?.with ?? {},
|
|
153
|
+
runsOn,
|
|
154
|
+
skipBotEvents: shouldSkipBotEvents
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
...mergedConfiguration,
|
|
159
|
+
plugins: resolvedPlugins
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async _getConfigurationFromRepo(owner, repository) {
|
|
163
|
+
const rawData = await this._download({
|
|
164
|
+
repository,
|
|
165
|
+
owner
|
|
166
|
+
});
|
|
167
|
+
this._logger.debug("Downloaded configuration file", { owner, repository });
|
|
168
|
+
if (!rawData) {
|
|
169
|
+
this._logger.debug("No raw configuration data", { owner, repository });
|
|
170
|
+
return { config: null, errors: null, rawData: null };
|
|
171
|
+
}
|
|
172
|
+
const { yaml, errors } = this._parseYaml(rawData);
|
|
173
|
+
const targetRepoConfiguration = yaml;
|
|
174
|
+
this._logger.debug("Decoding configuration", { owner, repository });
|
|
175
|
+
if (targetRepoConfiguration) {
|
|
176
|
+
try {
|
|
177
|
+
const configSchemaWithDefaults = Value.Default(configSchema, targetRepoConfiguration);
|
|
178
|
+
const errors2 = Value.Errors(configSchema, configSchemaWithDefaults);
|
|
179
|
+
if (errors2.First()) {
|
|
180
|
+
for (const error of errors2) {
|
|
181
|
+
this._logger.warn("Configuration validation error", { err: error });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const decodedConfig = Value.Decode(configSchema, configSchemaWithDefaults);
|
|
185
|
+
return { config: decodedConfig, errors: errors2, rawData };
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this._logger.error("Error decoding configuration; Will ignore.", { err: error, owner, repository });
|
|
188
|
+
return { config: null, errors: [error instanceof TransformDecodeCheckError ? error.error : error], rawData };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this._logger.error("YAML could not be decoded", { owner, repository, errors });
|
|
192
|
+
return { config: null, errors, rawData };
|
|
193
|
+
}
|
|
194
|
+
async _download({ repository, owner }) {
|
|
195
|
+
if (!repository || !owner) {
|
|
196
|
+
this._logger.error("Repo or owner is not defined, cannot download the requested file");
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const pathList = [CONFIG_PROD_FULL_PATH, CONFIG_DEV_FULL_PATH];
|
|
200
|
+
for (const filePath of pathList) {
|
|
201
|
+
try {
|
|
202
|
+
this._logger.debug("Attempting to fetch configuration", { owner, repository, filePath });
|
|
203
|
+
const { data, headers } = await this._octokit.rest.repos.getContent({
|
|
204
|
+
owner,
|
|
205
|
+
repo: repository,
|
|
206
|
+
path: filePath,
|
|
207
|
+
mediaType: { format: "raw" }
|
|
208
|
+
});
|
|
209
|
+
this._logger.debug("Configuration file found", { owner, repository, filePath, rateLimitRemaining: headers?.["x-ratelimit-remaining"], data });
|
|
210
|
+
return data;
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (err && typeof err === "object" && "status" in err && err.status === 404) {
|
|
213
|
+
this._logger.warn("No configuration file found", { owner, repository, filePath });
|
|
214
|
+
} else {
|
|
215
|
+
this._logger.error("Failed to download the requested file", { err, owner, repository, filePath });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
_parseYaml(data) {
|
|
222
|
+
this._logger.debug("Will attempt to parse YAML data", { data });
|
|
223
|
+
try {
|
|
224
|
+
if (data) {
|
|
225
|
+
const parsedData = YAML.load(data);
|
|
226
|
+
this._logger.debug("Parsed yaml data", { parsedData });
|
|
227
|
+
return { yaml: parsedData ?? null, errors: null };
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
this._logger.error("Error parsing YAML", { error });
|
|
231
|
+
return { errors: [error], yaml: null };
|
|
232
|
+
}
|
|
233
|
+
this._logger.debug("Could not parse YAML");
|
|
234
|
+
return { yaml: null, errors: null };
|
|
235
|
+
}
|
|
236
|
+
mergeConfigurations(configuration1, configuration2) {
|
|
237
|
+
const mergedPlugins = {
|
|
238
|
+
...configuration1.plugins,
|
|
239
|
+
...configuration2.plugins
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
...configuration1,
|
|
243
|
+
...configuration2,
|
|
244
|
+
plugins: mergedPlugins
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
getManifest(plugin) {
|
|
248
|
+
return this._fetchActionManifest(plugin);
|
|
249
|
+
}
|
|
250
|
+
async _fetchActionManifest({ owner, repo, ref }) {
|
|
251
|
+
const manifestKey = ref ? `${owner}:${repo}:${ref}` : `${owner}:${repo}`;
|
|
252
|
+
if (this._manifestCache[manifestKey]) {
|
|
253
|
+
return this._manifestCache[manifestKey];
|
|
254
|
+
}
|
|
255
|
+
if (this._manifestPromiseCache[manifestKey]) {
|
|
256
|
+
return this._manifestPromiseCache[manifestKey];
|
|
257
|
+
}
|
|
258
|
+
const manifestPromise = (async () => {
|
|
259
|
+
try {
|
|
260
|
+
const { data } = await this._octokit.rest.repos.getContent({
|
|
261
|
+
owner,
|
|
262
|
+
repo,
|
|
263
|
+
path: "manifest.json",
|
|
264
|
+
ref
|
|
265
|
+
});
|
|
266
|
+
if ("content" in data) {
|
|
267
|
+
const content = Buffer.from(data.content, "base64").toString();
|
|
268
|
+
const contentParsed = JSON.parse(content);
|
|
269
|
+
const manifest = this._decodeManifest(contentParsed);
|
|
270
|
+
this._manifestCache[manifestKey] = manifest;
|
|
271
|
+
return manifest;
|
|
272
|
+
}
|
|
273
|
+
} catch (e) {
|
|
274
|
+
this._logger.error("Could not find a valid manifest", { owner, repo, err: e });
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
})();
|
|
278
|
+
this._manifestPromiseCache[manifestKey] = manifestPromise;
|
|
279
|
+
try {
|
|
280
|
+
return await manifestPromise;
|
|
281
|
+
} finally {
|
|
282
|
+
delete this._manifestPromiseCache[manifestKey];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
_decodeManifest(manifest) {
|
|
286
|
+
const errors = [...Value.Errors(manifestSchema, manifest)];
|
|
287
|
+
if (errors.length) {
|
|
288
|
+
for (const error of errors) {
|
|
289
|
+
this._logger.error("Manifest validation error", { error });
|
|
290
|
+
}
|
|
291
|
+
throw new Error("Manifest is invalid.");
|
|
292
|
+
}
|
|
293
|
+
const defaultManifest = Value.Default(manifestSchema, manifest);
|
|
294
|
+
return defaultManifest;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
export {
|
|
298
|
+
CONFIG_DEV_FULL_PATH,
|
|
299
|
+
CONFIG_ORG_REPO,
|
|
300
|
+
CONFIG_PROD_FULL_PATH,
|
|
301
|
+
ConfigurationHandler
|
|
302
|
+
};
|